use super::geometry::region_pixels; use super::{Arc, NodeId, Rect, encode, proof}; /// Largest raster an export may ask [`Renderer::render_region`] for, per side. The CPU rasteriser /// addresses at most `u16::MAX` pixels per side (and panics at that edge). pub const MAX_RASTER_SIDE: u32 = 32_768; /// Largest side of a render context the renderer makes. The CPU rasteriser keeps pixel positions /// in `u16` and overflows near `u16::MAX` (its tile and bucket arithmetic rounds past it), so /// contexts stay within the side raster exports are limited to. pub(crate) const MAX_CONTEXT_SIDE: u32 = MAX_RASTER_SIDE; /// Largest raster an export may ask for in total (16384², 1 GiB of RGBA pixels). pub const MAX_RASTER_PIXELS: u64 = 1 << 28; /// Pixel size of `region` rendered at `scale` (as [`Renderer::render_region`] rounds it), or an error /// when it exceeds [`MAX_RASTER_SIDE`] / [`MAX_RASTER_PIXELS`]. Raster exports check this first, /// because the renderer can only clamp (wrong picture) or fail (abort) at those sizes. pub fn raster_size(region: Rect, scale: f64) -> Result<(u32, u32), String> { if !(scale.is_finite() && scale > 0.0) { return Err(format!("invalid scale {scale}")); } let (w, h) = region_pixels(region, scale); if !(w <= MAX_RASTER_SIDE && h <= MAX_RASTER_SIDE && u64::from(w) * u64::from(h) <= MAX_RASTER_PIXELS) { return Err(format!( "the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {} megapixels: lower the scale or resolution", MAX_RASTER_PIXELS / 1_000_000 )); } Ok((w, h)) } /// Rendering options. #[derive(Clone, Debug, PartialEq)] pub struct RenderOptions { /// Outline (wireframe) view: 1 px black paths, no paint. pub outline: bool, /// Pasteboard colour behind everything (premultiplied RGBA8); `None` = transparent. pub background: Option<[u8; 4]>, /// Draw artboards as white rectangles (screen view). Export sets this to false. pub artboards: bool, /// Objects not to draw (e.g. the object being edited by a live drag preview). pub hidden: Vec, /// Draw the layers as their Layer Options show them on screen: Preview off in outline, Dim /// Images fading their images (a template layer's too), a template in Preview in Outline view. pub dim_templates: bool, /// Soft proof / separations preview (see [`proof`]). pub proof: Option, /// Overprint Preview (see [`proof`] for what overprints). pub overprint_preview: bool, /// Trim View: clip the artwork to the artboards (nothing on the pasteboard is drawn). pub trim: bool, /// Leave template layers and guides out (exports and thumbnails: they're aids to drawing, not /// artwork). pub skip_templates: bool, /// View › Hide Template: template layers left out of the screen view (guides stay). pub hide_templates: bool, /// View › Hide Guides: paths made into guides left out of the screen view. Exports use /// `skip_templates`, which independently leaves both templates and guides out. pub hide_guides: bool, /// Pattern editing mode's tile edge and swatch bounds colour (Object → Pattern → Tile Edge /// Color), RGB. pub tile_edge: [u8; 3], /// Colour and style of the paths made into guides (Preferences › Guides & Grid, as the ruler /// guides the UI draws): RGB, and whether they are dotted. pub guide: ([u8; 3], bool), /// View Opacity Mask (Alt-click the mask thumbnail): instead of the artwork, show this object's /// opacity mask alone as its coverage in greyscale (white = opaque, black = transparent). pub mask_view: Option, /// Screen view: highlight substituted fonts and glyphs, as Preferences › Type and Document /// Setup ask. pub highlight_substitutions: bool, /// Screen view: Preferences › Type › Highlight Substituted Fonts. Pink behind text drawn in a /// substitute for a missing font, in every document (on by default). pub highlight_substituted_fonts: bool, /// Screen view: Preferences › Type › Highlight Alternate Glyphs. Pink behind glyphs OpenType /// features substituted (ligatures, contextual forms), not Document Setup's missing-glyph /// highlights. pub highlight_alternate_glyphs: bool, /// Edge smoothing (raster export option). pub anti_alias: AntiAlias, /// Screen view: placed documents draw from cached bitmaps made in the background (see /// [`placed_document`]); off, they draw exactly, read when needed. pub progressive_placed: bool, /// Screen view: Image Trace objects draw as their View asks (outlines, the source image…); /// off, they draw their tracing result, as exports and printing do. pub trace_views: bool, /// Screen view with `trace_views` on: the Image Trace objects drawn with the Source Image view /// for this render only, whatever View they have (the Image Trace panel's eye button, while it /// is held down). pub trace_source: Option>>, /// Images are sampled smoothly when scaled or rotated; off, each pixel takes its nearest image /// pixel (Pixel Preview with File Handling › Display Bitmaps as Anti-aliased Images off). pub smooth_images: bool, /// Screen view in isolation mode: the isolated group or layer. Everything around it draws /// dimmed ([`ISOLATION_DIM`]). pub isolated: Option, /// Composite in floating point (exports): translucent art over opaque art comes out exactly /// opaque, where 8-bit compositing leaves alpha 254 on some anti-aliased edges (#787). Off, /// the faster 8-bit pipeline (the canvas). pub precise: bool, /// Exports: Art Optimized edges are supersampled ([`supersample_factor`]): the art is drawn /// with hard edges at several times the size and averaged down, so shapes that meet edge to /// edge leave no seam of the background between them (#983). Off, edges are smoothed shape by /// shape (the canvas). pub supersample: bool, /// Screen views: an image drawn at half its size or smaller samples a copy shrunk by halves /// (box-filtered from the full image, kept with the decoded images), so the full image needn't /// stay in memory or be sampled. Off, images always sample their full pixels (exports). pub image_mips: bool, /// Screen views: an image whose pixels can't be decoded draws as a light gray box crossed /// from corner to corner. Off, it draws nothing (exports, which report it instead). pub mark_undecoded_images: bool, /// Screen views that must render quickly (the canvas's low-resolution fill): an image object /// draws from pixels the shared store already holds for it: the mip /// [`RenderOptions::image_mips`] would draw if the store holds it, else the nearest finer one, /// else the nearest coarser one. An image object the store holds no pixels of draws as a /// light gray box, not crossed, unless the store notes it as undecodable: then it draws as it /// does with this option off ([`RenderOptions::mark_undecoded_images`]). No image object is decoded and /// no mip of one is made. Off, image objects are decoded as they are drawn. pub kept_images_only: bool, /// Screen views while an interaction is in progress (a drag, a dialog's preview): an object /// whose content only moved since an earlier frame draws its cached drop shadows, outer glows /// and filtered content from the rasters made then, at the nearest whole pixel, without /// running the effects again (see `fx`). Off, no raster made that way, or made over one, is /// drawn: the effects run again. pub interacting: bool, /// Screen views during a tool's drag on the canvas: the renderer draws filtered content within /// `fx::DRAG_PIXEL_BUDGET` pixels, at a lower resolution where it would need more, unless it /// can draw the content from a raster kept from an earlier frame. With [`Renderer::threads`] /// above 0 it does the same for drop shadows, outer glows and inner glows. No raster made that /// way is kept (see `fx`). Off, they render as in a still frame. pub dragging: bool, /// Appearance of Black's Rich Black: pure CMYK black (black ink alone at 100%) shows as RGB /// black. Off, it shows through the CMYK profile, a dark grey, as other colours do. pub rich_black: bool, } /// The opacity of the art around an isolated group or layer. pub const ISOLATION_DIM: f32 = 0.5; /// How edges are rasterized (raster export option). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum AntiAlias { /// Hard edges: a pixel is painted when a path, text or image covers at least half of it. /// Raster effects (blur, shadows, glows) and pattern tiles stay smooth. None, /// Smooth edges everywhere. #[default] Art, /// Smooth edges with type snapped to the pixel grid (see `TextLayout::snap_to_pixels`): /// crisper small text. Type, } impl AntiAlias { pub const ALL: [Self; 3] = [Self::None, Self::Art, Self::Type]; /// The id used in command params (`none`, `art`, `type`). pub fn id(self) -> &'static str { match self { Self::None => "none", Self::Art => "art", Self::Type => "type", } } /// The mode an id names (any case). pub fn from_id(id: &str) -> Option { Self::ALL.into_iter().find(|a| a.id().eq_ignore_ascii_case(id)) } /// The name shown in options dialogs. pub fn label(self) -> &'static str { match self { Self::None => "None", Self::Art => "Art Optimized", Self::Type => "Type Optimized", } } /// vello's aliasing threshold: paint a pixel only above half coverage when edges are hard. pub(crate) fn threshold(self) -> Option { (self == Self::None).then_some(127) } } impl Default for RenderOptions { fn default() -> Self { Self { outline: false, background: None, artboards: false, hidden: vec![], dim_templates: true, proof: None, overprint_preview: false, trim: false, skip_templates: false, hide_templates: false, hide_guides: false, tile_edge: vectorcraft_doc::LAYER_COLORS[0].1, guide: ([0x4a, 0xd8, 0xff], false), mask_view: None, highlight_substitutions: false, highlight_substituted_fonts: true, highlight_alternate_glyphs: false, anti_alias: AntiAlias::Art, progressive_placed: false, trace_views: false, trace_source: None, smooth_images: true, isolated: None, precise: false, supersample: false, image_mips: false, mark_undecoded_images: false, kept_images_only: false, interacting: false, dragging: false, rich_black: false, } } } /// The most pixels a supersampled render draws (the frame and its compositing buffers hold a few /// times this many bytes). const MAX_SUPERSAMPLED_PIXELS: u64 = 40_000_000; /// How many times wider and taller a `w`×`h` render is drawn when supersampled: 4, or less where /// that would pass [`MAX_SUPERSAMPLED_PIXELS`] or the renderer's 65,535-pixel side; `None` when not /// even twice fits (the edges are then smoothed shape by shape). pub fn supersample_factor(w: u32, h: u32) -> Option { (2..=4u32).rev().find(|k| { let (sw, sh) = (u64::from(w) * u64::from(*k), u64::from(h) * u64::from(*k)); sw <= u64::from(MAX_CONTEXT_SIDE) && sh <= u64::from(MAX_CONTEXT_SIDE) && sw * sh <= MAX_SUPERSAMPLED_PIXELS }) } /// Average each `k`×`k` block of `src` (`w`×`h` pixels of 4 premultiplied bytes) into one pixel. pub(crate) fn downsample(src: &[u8], w: usize, h: usize, k: usize) -> Vec { let (ow, oh) = (w / k.max(1), h / k.max(1)); let n = (k * k).max(1) as u32; let mut out = vec![0u8; ow * oh * 4]; for (o, px) in out.as_chunks_mut::<4>().0.iter_mut().enumerate() { let (ox, oy) = (o % ow.max(1), o / ow.max(1)); let mut acc = [0u32; 4]; for y in oy * k..(oy + 1) * k { let row = y * w + ox * k; for p in src.get(row * 4..(row + k) * 4).unwrap_or_default().as_chunks::<4>().0 { for (a, v) in acc.iter_mut().zip(p) { *a += u32::from(*v); } } } for (d, a) in px.iter_mut().zip(acc) { *d = ((a + n / 2) / n).min(255) as u8; } } out } /// A rendered image (premultiplied RGBA8, row-major). pub struct Rendered { pub width: u32, pub height: u32, pub pixels: Vec, } impl Rendered { /// Un-premultiplied RGBA8 copy. pub fn to_straight(&self) -> Vec { let mut out = self.pixels.clone(); for px in out.as_chunks_mut::<4>().0 { let a = px[3] as u32; if a != 0 && a != 255 { for c in &mut px[..3] { *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8; } } } out } /// Encode as PNG without metadata (thumbnails, screenshots, embedded rasters; exports use /// [`encode`]). pub fn to_png(&self) -> Result, String> { let mut buf = Vec::new(); let img = image::RgbaImage::from_raw(self.width, self.height, self.to_straight()) .ok_or("PNG encoding failed: pixel buffer doesn't match the image size")?; img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png).map_err(|e| format!("PNG encoding failed: {e}"))?; Ok(buf) } /// Encode as JPEG (flattened on white) at `quality` 1..=100. pub fn to_jpeg(&self, quality: u8) -> Result, String> { encode::jpeg(self, quality, None) } /// Encode as lossless WebP. pub fn to_webp(&self) -> Result, String> { encode::webp(self) } /// Straight-alpha RGBA at (x, y). pub fn pixel(&self, x: u32, y: u32) -> [u8; 4] { let i = ((y * self.width + x) * 4) as usize; let p = &self.pixels[i..i + 4]; let a = p[3] as u32; if a == 0 { return [0, 0, 0, 0]; } let un = |c: u8| ((c as u32 * 255 + a / 2) / a).min(255) as u8; [un(p[0]), un(p[1]), un(p[2]), p[3]] } }