Files
vectorcraft/crates/render/src/options.rs
T

313 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NodeId>,
/// 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<proof::ProofSetup>,
/// 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<NodeId>,
/// 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<Arc<Vec<NodeId>>>,
/// 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<NodeId>,
/// 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> {
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<u8> {
(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<u32> {
(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<u8> {
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<u8>,
}
impl Rendered {
/// Un-premultiplied RGBA8 copy.
pub fn to_straight(&self) -> Vec<u8> {
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<Vec<u8>, 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<Vec<u8>, String> {
encode::jpeg(self, quality, None)
}
/// Encode as lossless WebP.
pub fn to_webp(&self) -> Result<Vec<u8>, 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]]
}
}