mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Fix #4937: artboards can have a background fill, included in exports and print
This commit is contained in:
@@ -57,6 +57,7 @@ pub use dxf::{UNSUPPORTED, Unsupported, unsupported};
|
||||
pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, encode_with_warnings};
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
pub use export::export_source;
|
||||
pub(crate) use export::with_artboard_fills;
|
||||
pub(crate) use export::{export_crop, isolated};
|
||||
pub(crate) use load::check_not_lossy_overwrite;
|
||||
use load::err;
|
||||
@@ -132,6 +133,8 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_affinity;
|
||||
#[cfg(test)]
|
||||
mod tests_artboard_fill;
|
||||
#[cfg(test)]
|
||||
mod tests_pdf;
|
||||
#[cfg(test)]
|
||||
mod tests_pdfcolor;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin.<plug-in id>, which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in their layers), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as <stem>.html / <stem>.map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path",
|
||||
"{path?, format?: svg|svgz|pdf|ai|png|jpg|webp|gif|png8|txt|dxf|eps|emf|wmf|tiff|bmp|tga|psd|vectorcraft|template (default: from the path's extension, else png; png8 writes an indexed .png; ait, a .ait template, is written by file.saveAsTemplate; an exporter plug-in's format is plugin.<plug-in id>, which an extension only it lists picks too), params?: {…} (an exporter plug-in's parameters; missing ones take their defaults, see plugin.info), selectedOnly?: false (the selected objects alone, in their layers), includeArtboardFill?: true (raster, PDF, SVG and EPS draw each exported artboard's background color behind its art, a transparent background too; false leaves it out), artboard?: 0, artboards?: [i…], rect?: [x, y, width, height] (raster formats: that area of the page in points, y down, instead of an artboard; not with artboard, artboards, range or useArtboards), range?: \"1-3, 5\" | \"all\" (1-based; PDF and .ai write one page per artboard, default all; SVG writes one file per artboard, {stem}-{artboard}.svg; raster formats write one artboard), useArtboards?: true (raster: one file per chosen artboard, default all, {stem}-{artboard}.{ext}; pdf/ai: every page) | false (pdf/ai/raster: the bounds of the visible art; SVG has it as an SVG option), ppi?: 72 (raster formats: pixels per inch, stored in the file; wins over scale), scale?: 1 (pixels per point), background?: transparent|white|black|\"#rrggbb\" (jpg: white when transparent), antiAlias?: none|art (default)|type (text snapped to pixels), interlaced?: false (png, Adam7), embedIcc?: true (png: an sRGB chunk marks the colours as sRGB), quality?: 90 (jpg: 0–100), colorModel?: rgb|cmyk|gray (jpg), method?: baseline|optimized|progressive, scans?: 3 (3–5, progressive), embedIcc?: true, imageMap?: none|client|server (an HTML or NCSA map of the objects with a URL, written as <stem>.html / <stem>.map), colors?: 256 (gif/png8: 2–256), reduction?: perceptual|selective (default)|adaptive|web|blackWhite|gray, dither?: none|diffusion (default)|pattern|noise, ditherAmount?: 100, transparency?: true, matte?: white|\"#rrggbb\"|none, interlaced?, lossless?: true (webp: lossy WebP isn't available yet: written lossless, with a warning), encoding?: utf8|utf16 (txt: the stories in stacking order, back to front, a thread once; utf16 with a byte order mark), lineEndings?: lf|crlf, selectionOnly?: false, svg?: {styling, outlineText, images, objectIds, decimals, minify, responsive, useArtboards, preserveEditing, metadata, fewerTspans, hiddenLayers, encoding, profile, embedFonts} (see document.formats), …the SVG options given flat instead, …the PDF options of document.exportPdf, …the DXF options of document.exportDxf (useArtboards: one drawing per artboard), …the EPS options of document.exportEps (useArtboards: one file per artboard, {stem}_{artboard}.eps, after a central {stem}.eps of the whole document when there are two or more and art shows; else the visible art), …emf and wmf write one picture of the artboard (useArtboards: true one file per chosen artboard, false the bounds of the visible art; EMF keeps curves, clipping, transparent images and gradients as images clipped to their shape; WMF flattens curves into polygons behind a placeable header; what a format leaves out comes back in warnings), colorModel?: rgb|cmyk|gray (tiff: rgb keeps transparency as an alpha channel), lzw?: true, byteOrder?: little|big, embedIcc?: true, colorModel?: rgb|gray (bmp), depth?: 24 (bmp: 1 black and white, 4|8 a palette by reduction and dither, 16, 24, 32 with alpha), fileFormat?: windows|os2 (1, 4, 8 or 24 bits), rle?: false (4 and 8 bits, windows), flipRows?: false (top-down rows), depth?: 24 (tga: 16 one-bit alpha, 24, 32 with alpha), colorModel?: rgb|cmyk|gray (psd), layers?: true (each top-level layer a pixel layer with its opacity and blend mode; a background colour is a Background layer; false: one flat image, on white), maxEditability?: false (layers and sublayers become groups, each object a layer named as in the Layers panel, text by its text; clipping, masked or knockout layers stay one layer), hiddenLayers?: false (hidden layers and objects written hidden instead of left out), embedIcc?: true (at most 30000 pixels a side; formats without alpha are flattened on white)} → {path, format, bytes, warnings, files?: [path…] (several), linked?: [path…] (linked images, image maps)}; no path → {dataBase64, format, bytes, warnings, files?: [{name, dataBase64}], linked?: [{name, dataBase64}]}. Never changes the document's path",
|
||||
has_doc,
|
||||
export::export
|
||||
),
|
||||
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export for Screens",
|
||||
["File", "Export"],
|
||||
None,
|
||||
"{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with <filename> for the document, <name> for the artboard, asset, layer or object, <index> from 1, #### for the counter zero-padded to the run of #, <format>, <scale>, <suffix> (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, <date>, <time>, <YYYY>, <MM>, <DD>, <hh>, <mm> and <ss>; a trailing .ext is dropped, as in `COMPANY-Logo-<filename>-####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
"{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), includeArtboardFill?: true (Include Artboard Fill: each artboard's background color behind its art; false leaves it out), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with <filename> for the document, <name> for the artboard, asset, layer or object, <index> from 1, #### for the counter zero-padded to the run of #, <format>, <scale>, <suffix> (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, <date>, <time>, <YYYY>, <MM>, <DD>, <hh>, <mm> and <ss>; a trailing .ext is dropped, as in `COMPANY-Logo-<filename>-####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
has_doc,
|
||||
screens::export_for_screens
|
||||
),
|
||||
@@ -72,7 +72,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Save Export Preset",
|
||||
[],
|
||||
None,
|
||||
"{name, settings: {formats, settings, preset, name, prefix, counterStart, counterPer, folders, subfolders, includeBleed, antiAlias, folder, fullDocument} (as document.exportForScreens; other keys are left out)} save (or replace) a batch export preset, kept with the preferences; built-in names are refused → {name, created, settings}",
|
||||
"{name, settings: {formats, settings, preset, name, prefix, counterStart, counterPer, folders, subfolders, includeBleed, includeArtboardFill, antiAlias, folder, fullDocument} (as document.exportForScreens; other keys are left out)} save (or replace) a batch export preset, kept with the preferences; built-in names are refused → {name, created, settings}",
|
||||
always,
|
||||
screens::presets_save
|
||||
),
|
||||
|
||||
@@ -565,6 +565,9 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
||||
(RasterFormat::Jpeg, Some(m)) => MapKind::from_id(m).ok_or_else(|| bad(C, format!("imageMap `{m}`: none, client or server")))?,
|
||||
_ => MapKind::None,
|
||||
};
|
||||
// The artboards' background colors, behind the art (a transparent background too).
|
||||
let filled = super::export::with_artboard_fills(doc, p, chosen.iter().copied()).map_err(|e| bad(C, e))?;
|
||||
let doc = &*filled;
|
||||
let mut enc = Encoded::default();
|
||||
let mut renderer = vectorcraft_render::Renderer::new();
|
||||
let total = chosen.len() as u64;
|
||||
|
||||
@@ -300,6 +300,9 @@ fn encode_document(doc: &Document, p: &Value, saving: bool) -> Result<Encoded> {
|
||||
if pages.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
// The artboards' background colors, behind the art (the editing data keeps them as colors).
|
||||
let filled = super::export::with_artboard_fills(doc, p, pages.iter().filter_map(|(b, ..)| *b)).map_err(|e| bad(C, e))?;
|
||||
let doc = &*filled;
|
||||
let regions: Vec<Rect> = pages.iter().map(|(_, region, _)| *region).collect();
|
||||
let expanded = expand_transparent_tile_images(doc, &set.flatten)?;
|
||||
let source = expanded.as_ref().unwrap_or(doc);
|
||||
|
||||
@@ -146,6 +146,21 @@ pub(super) fn art_over(doc: &Document, rect: vectorcraft_geom::Rect) -> Document
|
||||
d
|
||||
}
|
||||
|
||||
/// `doc` with the background colors of `artboards` drawn behind its art
|
||||
/// ([`Document::with_artboard_fills`]), what exports and print draw, unless `p` turns
|
||||
/// `includeArtboardFill` off (Export for Screens › Include Artboard Fill; on by default).
|
||||
pub(crate) fn with_artboard_fills<'a>(
|
||||
doc: &'a Document,
|
||||
p: &Value,
|
||||
artboards: impl IntoIterator<Item = usize>,
|
||||
) -> std::result::Result<Cow<'a, Document>, String> {
|
||||
let include = match p.get("includeArtboardFill") {
|
||||
None | Some(Value::Null) => true,
|
||||
Some(v) => v.as_bool().ok_or("`includeArtboardFill` must be true or false")?,
|
||||
};
|
||||
Ok(include.then(|| doc.with_artboard_fills(artboards)).flatten().map_or(Cow::Borrowed(doc), Cow::Owned))
|
||||
}
|
||||
|
||||
/// `p` without its artboard choice, also inside its SVG options (for documents made of one
|
||||
/// synthetic artboard, and for callers that pick the artboard themselves).
|
||||
pub(super) fn without_artboards(p: &Value) -> Value {
|
||||
|
||||
@@ -284,6 +284,10 @@ fn overflow_warning(cmd: &str, doc: &Document, p: &Value) -> Result<Option<Strin
|
||||
/// native document with its save options, whose pages may be left blank).
|
||||
pub(super) fn encode_carrying(cmd: &str, doc: &Document, p: &Value, native: impl FnOnce() -> Result<Vec<u8>>) -> Result<(Vec<u8>, Vec<String>)> {
|
||||
let mut opts = options(cmd, doc, p)?;
|
||||
// The pages' background colors, behind the art (the editing data keeps them as colors).
|
||||
let pages = opts.artboards.clone().unwrap_or_else(|| (0..doc.artboards.len()).collect());
|
||||
let filled = super::export::with_artboard_fills(doc, p, pages).map_err(|e| bad(cmd, e))?;
|
||||
let doc = &*filled;
|
||||
if opts.settings.thumbnails {
|
||||
opts.thumbnails = thumbnails(doc, &opts).map_err(|e| pdf_error(cmd, e))?;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use super::encode::single_artboard;
|
||||
use super::export::{export_crop, isolated, selected_artwork, without_artboards};
|
||||
use super::naming::{self, NameParts};
|
||||
use super::{
|
||||
ARTBOARD_PARAMS, ArtboardPick, Format, artboard_file_names, create_dir, distinct_names, encode_with_warnings, unique_file_names,
|
||||
ARTBOARD_PARAMS, ArtboardPick, Format, artboard_file_names, create_dir, distinct_names, encode_with_warnings, merge, unique_file_names,
|
||||
with_black_output, writable, write_file,
|
||||
};
|
||||
|
||||
@@ -289,6 +289,8 @@ struct ScreenParams {
|
||||
zip: bool,
|
||||
full_document: bool,
|
||||
include_bleed: bool,
|
||||
/// Include Artboard Fill: the artboards' background colors behind the art (default on).
|
||||
include_artboard_fill: Option<bool>,
|
||||
subfolders: bool,
|
||||
preset: Option<String>,
|
||||
prefix: Option<String>,
|
||||
@@ -579,7 +581,7 @@ pub struct ScreenExportPreset {
|
||||
|
||||
/// What a preset keeps: the formats, their settings, the naming and the folders; not which art
|
||||
/// (it changes with the document) nor where to (unless given).
|
||||
pub const PRESET_KEYS: [&str; 13] = [
|
||||
pub const PRESET_KEYS: [&str; 14] = [
|
||||
"formats",
|
||||
"settings",
|
||||
"preset",
|
||||
@@ -590,6 +592,7 @@ pub const PRESET_KEYS: [&str; 13] = [
|
||||
"folders",
|
||||
"subfolders",
|
||||
"includeBleed",
|
||||
"includeArtboardFill",
|
||||
"antiAlias",
|
||||
"folder",
|
||||
"fullDocument",
|
||||
@@ -708,7 +711,9 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let crop = rows.iter().any(|r| r.format.id != "pdf");
|
||||
let items = items(&doc, &o, selection, &selected, &naming.filename, crop, true)?;
|
||||
// Appearance of Black › Printing / Exporting for RGB raster rows.
|
||||
let rows = rows.into_iter().map(|r| Row { options: with_black_output(s, &r.options), ..r }).collect();
|
||||
// Include Artboard Fill off: the encoders leave the background colors out.
|
||||
let fill = if o.include_artboard_fill == Some(false) { json!({ "includeArtboardFill": false }) } else { json!({}) };
|
||||
let rows = rows.into_iter().map(|r| Row { options: merge(with_black_output(s, &r.options), fill.clone()), ..r }).collect();
|
||||
let export = ScreenExport { cmd: C, o, rows, naming, items };
|
||||
match &mut s.screens_hand_off {
|
||||
Some(slot) => {
|
||||
@@ -830,6 +835,10 @@ fn encode_and_write(export: ScreenExport, writer: &mut dyn FnMut(&str, &[u8]) ->
|
||||
.entry(b)
|
||||
.or_insert_with(|| {
|
||||
let mut d = single_artboard(&item.doc, item.region, &item.name);
|
||||
// It keeps its background color.
|
||||
if let (Some(a), Some(from)) = (d.artboards.first_mut(), item.doc.artboards.get(b)) {
|
||||
a.color = from.color;
|
||||
}
|
||||
super::save::keep_art_on(&mut d, item.region);
|
||||
Arc::new(d)
|
||||
})
|
||||
|
||||
@@ -169,7 +169,9 @@ pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result<Enco
|
||||
// Draw linked files as vectors when available, or their previews with a warning. Keep
|
||||
// that preparation out of the native attachment so missing links remain relinkable.
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
// The artboards' background colors, behind the art (the editing data keeps them as colors).
|
||||
let filled = super::export::with_artboard_fills(&full, p, boards.iter().flatten().copied())?;
|
||||
let doc = &*filled;
|
||||
// A chosen artboard's file holds the art over it; otherwise the file holds all of it.
|
||||
let regions: Option<Vec<vectorcraft_geom::Rect>> =
|
||||
chosen.then(|| boards.iter().flatten().filter_map(|b| doc.artboards.get(*b)).map(|a| a.rect).collect());
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Artboard background colors in output (#4937): raster, PDF, SVG and EPS exports, Export for
|
||||
//! Screens and print draw each artboard's color behind its art, inside the artboard;
|
||||
//! `includeArtboardFill: false` (Include Artboard Fill) leaves it out; the editing data keeps it as
|
||||
//! the artboard's color, not as art.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
||||
const RED: [u8; 4] = [255, 0, 0, 255];
|
||||
|
||||
/// Two 60×40 pt artboards, a 20 pt square (white fill, black stroke) on the first, which is red.
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 60, "height": 40, "artboards": 2})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 0, "color": "#ff0000"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn b64(v: &Value) -> Vec<u8> {
|
||||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap()
|
||||
}
|
||||
|
||||
fn with(mut p: Value, more: &Value) -> Value {
|
||||
if let (Some(o), Some(m)) = (p.as_object_mut(), more.as_object()) {
|
||||
o.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// The pixels of the PNG `document.export` writes of the active document with `p`, on transparency.
|
||||
fn png(s: &mut Session, p: Value) -> image::RgbaImage {
|
||||
let v = s.execute("document.export", &with(json!({"format": "png", "background": "transparent"}), &p)).unwrap();
|
||||
image::load_from_memory(&b64(&v)).unwrap().to_rgba8()
|
||||
}
|
||||
|
||||
/// The file `document.export` writes with `p`.
|
||||
fn file(s: &mut Session, p: Value) -> Vec<u8> {
|
||||
b64(&s.execute("document.export", &p).unwrap())
|
||||
}
|
||||
|
||||
/// The file `bytes` named `name`, opened in a new session.
|
||||
fn opened(name: &str, bytes: &[u8]) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// What the file `bytes` named `name` shows near its top-left corner, opened and drawn.
|
||||
fn corner(name: &str, bytes: &[u8]) -> [u8; 4] {
|
||||
png(&mut opened(name, bytes), json!({"artboard": 0})).get_pixel(2, 2).0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_exports_paint_the_color_behind_the_art() {
|
||||
let mut s = session();
|
||||
let on = png(&mut s, json!({"artboard": 0}));
|
||||
assert_eq!(on.get_pixel(2, 2).0, RED, "a transparent background still shows the color");
|
||||
assert_eq!(on.get_pixel(20, 20).0, [255, 255, 255, 255], "the art over it");
|
||||
assert_eq!(png(&mut s, json!({"artboard": 0, "includeArtboardFill": false})).get_pixel(2, 2)[3], 0, "left out");
|
||||
assert_eq!(png(&mut s, json!({"artboard": 1})).get_pixel(2, 2)[3], 0, "an artboard without a color");
|
||||
let jpg = file(&mut s, json!({"format": "jpg", "artboard": 0, "quality": 100}));
|
||||
let px = image::load_from_memory(&jpg).unwrap().to_rgb8().get_pixel(2, 2).0;
|
||||
assert!(px[0] > 240 && px[1] < 16 && px[2] < 16, "JPEG too: {px:?}");
|
||||
for bad in [json!("no"), json!(1)] {
|
||||
assert!(s.execute("document.export", &json!({"format": "png", "includeArtboardFill": bad})).is_err(), "{bad}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().doc.layers.len(), 1, "the document gains no layer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_color_fills_its_artboard_alone_behind_the_art() {
|
||||
let s = session();
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let board = doc.artboards[0].rect;
|
||||
let filled = doc.with_artboard_fills([0, 1]).unwrap();
|
||||
let img = vectorcraft_render::Renderer::new().render_region(&filled, board.inflate(10.0, 10.0), 1.0, false);
|
||||
let rgba = img.to_straight();
|
||||
let px = |x: usize, y: usize| rgba[(y * img.width as usize + x) * 4..][..4].to_vec();
|
||||
assert_eq!(px(5, 5)[3], 0, "outside the artboard");
|
||||
assert_eq!(px(15, 15), RED, "inside it");
|
||||
assert_eq!(px(30, 30), [255, 255, 255, 255], "the art over it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_and_svg_show_the_color_and_their_editing_data_keeps_it_as_the_artboards() {
|
||||
let mut s = session();
|
||||
for (format, name) in [("pdf", "a.pdf"), ("svg", "a.svg")] {
|
||||
let p = json!({"format": format, "artboard": 0, "preserveEditing": false});
|
||||
assert_eq!(corner(name, &file(&mut s, p.clone())), RED, "{format}");
|
||||
assert_eq!(corner(name, &file(&mut s, with(p, &json!({"includeArtboardFill": false}))))[3], 0, "{format}");
|
||||
}
|
||||
let reopened = opened("a.pdf", &file(&mut s, json!({"format": "pdf", "preserveEditing": true})));
|
||||
let (back, doc) = (&reopened.doc().unwrap().doc, &s.doc().unwrap().doc);
|
||||
assert_eq!(back.artboards[0].color, doc.artboards[0].color);
|
||||
assert_eq!(back.layers.len(), doc.layers.len(), "no art made of it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eps_and_postscript_print_draw_the_color() {
|
||||
let mut s = session();
|
||||
// The only red the document has, the first artboard's color: RGB, or CMYK PostScript's.
|
||||
let red = |t: &str| t.contains("1 0 0 rg\n") || t.contains("0 0.9725 1 0 k\n");
|
||||
let eps = |s: &mut Session, more: Value| {
|
||||
String::from_utf8_lossy(&file(s, with(json!({"format": "eps", "useArtboards": true, "artboard": 0}), &more))).into_owned()
|
||||
};
|
||||
assert!(red(&eps(&mut s, json!({}))), "EPS");
|
||||
assert!(!red(&eps(&mut s, json!({"includeArtboardFill": false}))), "EPS without it");
|
||||
let ps = |s: &mut Session, more: Value| {
|
||||
String::from_utf8_lossy(&b64(&s.execute("file.print", &with(json!({"format": "postscript"}), &more)).unwrap())).into_owned()
|
||||
};
|
||||
assert!(red(&ps(&mut s, json!({}))), "print");
|
||||
assert!(!red(&ps(&mut s, json!({"includeArtboardFill": false}))), "print without it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_for_screens_includes_the_fill_unless_unchecked() {
|
||||
let mut s = session();
|
||||
let screens = |s: &mut Session, more: Value| {
|
||||
let p = with(json!({"artboards": [0], "formats": [{"format": "png"}], "settings": {"png": {"background": "transparent"}}}), &more);
|
||||
s.execute("document.exportForScreens", &p).unwrap()
|
||||
};
|
||||
let corner = |r: &Value| image::load_from_memory(&b64(&r["files"][0])).unwrap().to_rgba8().get_pixel(2, 2).0;
|
||||
assert_eq!(corner(&screens(&mut s, json!({}))), RED, "on by default");
|
||||
assert_eq!(corner(&screens(&mut s, json!({"includeArtboardFill": false})))[3], 0, "unchecked");
|
||||
assert_eq!(s.execute("document.exportSettings", &json!({})).unwrap()["settings"]["includeArtboardFill"], false, "remembered");
|
||||
assert!(s.execute("document.exportForScreens", &json!({"includeArtboardFill": "no", "formats": [{"format": "png"}]})).is_err());
|
||||
// A document row's artboard keeps its color, as a color.
|
||||
let r = screens(&mut s, json!({"formats": [{"format": "vectorcraft"}]}));
|
||||
let back = vectorcraft_format::load(&b64(&r["files"][0])).unwrap();
|
||||
assert_eq!(back.artboards[0].color, s.doc().unwrap().doc.artboards[0].color);
|
||||
assert_eq!(back.layers.len(), 1, "not as art");
|
||||
}
|
||||
@@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Print",
|
||||
[],
|
||||
Some("Cmd+P"),
|
||||
"{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out), forPrinter?: bool (the PDF goes to a print queue: a page whose paper is turned to landscape is written upright, the art turned a quarter anticlockwise, as a printer driver turns a landscape job; macOS prints a landscape page as it is, cut off on upright paper; paper wide in Portrait and transverse pages stay as they are)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device",
|
||||
"{preset?: name (as print.setup's: its settings instead of the document's; the saved settings stay), settings?: {…settings of print.setup} (over the document's or the preset's), includeArtboardFill?: true (each artboard's background color prints behind its art; false leaves it out), path?, format?: pdf (default: a print-ready PDF) | postscript (the default for a .ps path: a PostScript file with DSC comments, %%Pages, a page per sheet with its paper size, marks, separations with their ink and halftone screen, the fixed flatness, negatives), level?: 3|2 (the PostScript language level), flattenerPreset?: \"medium\" (PostScript: the preset transparency is flattened with — high, low or a saved one, see flattener.presets.list), acknowledgeLoss?: bool (with path: write over the file the document was read from although reading it left things out), forPrinter?: bool (the PDF goes to a print queue: a page whose paper is turned to landscape is written upright, the art turned a quarter anticlockwise, as a printer driver turns a landscape job; macOS prints a landscape page as it is, cut off on upright paper; paper wide in Portrait and transverse pages stay as they are)} print the document, one page per sheet of paper (per tile, per ink, per copy) → {pages, format, path, bytes, warnings}; no path → {dataBase64, bytes, pages, format, warnings}. Raster effects print at the document's raster effects resolution; a PDF leaves halftone screens and flatness to the output device",
|
||||
has_doc,
|
||||
print
|
||||
),
|
||||
@@ -176,7 +176,9 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// Placed documents print their files' art.
|
||||
let (full, _) = super::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
// The artboards' background colors print behind the art.
|
||||
let filled = super::fileio::with_artboard_fills(&full, p, 0..full.artboards.len()).map_err(|e| bad(PRINT, e))?;
|
||||
let doc = &*filled;
|
||||
// Raster effects print as images at the document's raster effects resolution.
|
||||
let flat = super::rasterfx::flatten_raster_effects(doc);
|
||||
let doc = flat.as_ref().unwrap_or(doc);
|
||||
|
||||
Reference in New Issue
Block a user