mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Export: document.export takes a pixel width or height for raster files
PNG, PNG-8, JPEG, WebP, GIF, TIFF, BMP, Targa and PSD exports take `width` or `height` in pixels. Each file is scaled so that its own artboard, `rect` or art bounds is that wide or high, so with `useArtboards` every file gets its own scale. `width` wins over `height`, and either wins over `ppi` and `scale`, in the order an Export for Screens row reads its size. The scale stays within 0.01 to 64 pixels per point and the file stores the resolution that results. A value that is not a positive number is refused. The per-file scale comes from `ScreenSize::scale_for`, which Export for Screens rows already use. Rows keep their own size: their format settings still drop `width` and `height`. `document.formats` lists the two options for every raster format.
This commit is contained in:
@@ -17,6 +17,7 @@ use vectorcraft_render::encode::{RasterExportOptions, RasterFormat, bmp, psd, tg
|
||||
use super::super::{EngineError, Result, bad, color_value, parse_range};
|
||||
use super::Format;
|
||||
use super::imagemap::{self, MapKind};
|
||||
use super::screens::ScreenSize;
|
||||
|
||||
const C: &str = "document.export";
|
||||
|
||||
@@ -147,6 +148,10 @@ struct RasterOptions {
|
||||
scale: Option<f64>,
|
||||
/// Pixels per inch (wins over `scale`).
|
||||
ppi: Option<f64>,
|
||||
/// The width of each file in pixels (wins over `height`, `ppi` and `scale`).
|
||||
width: Option<f64>,
|
||||
/// The height of each file in pixels (wins over `ppi` and `scale`).
|
||||
height: Option<f64>,
|
||||
quality: Option<u8>,
|
||||
background: Option<Value>,
|
||||
anti_alias: Option<String>,
|
||||
@@ -179,7 +184,24 @@ struct RasterOptions {
|
||||
rich_black: Option<bool>,
|
||||
}
|
||||
|
||||
/// `ppi` with its scale kept within 0.01–64 pixels per point.
|
||||
fn clamped_ppi(ppi: f64) -> f64 {
|
||||
(ppi / 72.0).clamp(0.01, 64.0) * 72.0
|
||||
}
|
||||
|
||||
impl RasterOptions {
|
||||
/// The pixel size each file is scaled to: `width`, else `height`; `None` when neither is set.
|
||||
fn pixel_size(&self) -> Result<Option<ScreenSize>> {
|
||||
let checked = |key: &str, n: f64| {
|
||||
if n.is_finite() && n > 0.0 { Ok(n) } else { Err(bad(C, format!("{key} must be a positive number, not {n}"))) }
|
||||
};
|
||||
match (self.width, self.height) {
|
||||
(Some(w), _) => checked("width", w).map(|w| Some(ScreenSize::Width(w))),
|
||||
(None, Some(h)) => checked("height", h).map(|h| Some(ScreenSize::Height(h))),
|
||||
(None, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The render and encoder settings: `ppi`, else 72 × `scale`, within 0.01–64 pixels per point;
|
||||
/// `background`, else `page` (the document's background).
|
||||
fn settings(&self, page: Option<[u8; 3]>) -> Result<RasterExportOptions> {
|
||||
@@ -190,7 +212,7 @@ impl RasterOptions {
|
||||
let color_model = parse(self.color_model.as_deref(), jpeg::ColorModel::from_id, "colorModel", "rgb, cmyk or gray")?;
|
||||
let embed_icc = self.embed_icc.unwrap_or(true);
|
||||
Ok(RasterExportOptions {
|
||||
ppi: (ppi / 72.0).clamp(0.01, 64.0) * 72.0,
|
||||
ppi: clamped_ppi(ppi),
|
||||
background: match &self.background {
|
||||
Some(v) => background(v).map_err(|e| bad(C, e))?,
|
||||
None => page,
|
||||
@@ -519,8 +541,8 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
||||
// New Document → Background Contents: White makes the export opaque, unless `background`
|
||||
// says otherwise (JPEG has no alpha: white either way).
|
||||
let page = (doc.setup.background == vectorcraft_doc::Background::White).then_some([255; 3]);
|
||||
let settings = o.settings(page)?;
|
||||
let scale = settings.scale();
|
||||
let mut settings = o.settings(page)?;
|
||||
let size = o.pixel_size()?;
|
||||
let format = match f.id {
|
||||
"png" => RasterFormat::Png,
|
||||
"jpg" => RasterFormat::Jpeg,
|
||||
@@ -550,6 +572,11 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
||||
vectorcraft_doc::cancel::check()?;
|
||||
vectorcraft_doc::cancel::progress(done as u64, total, &format!("Rendering artboard {} of {total}", done + 1));
|
||||
let region = doc.artboards.get(b).ok_or_else(|| bad(C, format!("no artboard {}", b + 1)))?.rect;
|
||||
// `width` or `height`: each file gets the scale that makes its region that size.
|
||||
if let Some(size) = size {
|
||||
settings.ppi = clamped_ppi(72.0 * size.scale_for(C, region)?);
|
||||
}
|
||||
let scale = settings.scale();
|
||||
check_format_size(f, region.width() * scale, region.height() * scale)?;
|
||||
vectorcraft_render::raster_size(region, scale).map_err(|e| bad(C, e))?;
|
||||
enc.maps.extend(imagemap::build(doc, region, scale, map, enc.files.len()));
|
||||
|
||||
@@ -75,6 +75,20 @@ pub(super) const RANGE: FormatOption = FormatOption {
|
||||
|
||||
const SCALE: FormatOption = FormatOption { name: "scale", ty: "number", default: "1", description: "pixels per point (0.01–64)" };
|
||||
|
||||
const WIDTH: FormatOption = FormatOption {
|
||||
name: "width",
|
||||
ty: "number",
|
||||
default: "null",
|
||||
description: "the width of each file in pixels: each artboard, rect or art bounds gets its own scale (0.01–64 pixels per point, stored in the file); wins over height, ppi and scale",
|
||||
};
|
||||
|
||||
const HEIGHT: FormatOption = FormatOption {
|
||||
name: "height",
|
||||
ty: "number",
|
||||
default: "null",
|
||||
description: "the height of each file in pixels, as width; wins over ppi and scale",
|
||||
};
|
||||
|
||||
const QUALITY: FormatOption =
|
||||
FormatOption { name: "quality", ty: "integer", default: "90", description: "JPEG quality 0–100 (the JPEG Options dialog shows 0–10)" };
|
||||
|
||||
@@ -198,6 +212,8 @@ const PALETTE_OPTIONS: &[FormatOption] = &[
|
||||
RECT,
|
||||
PPI,
|
||||
SCALE,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
BACKGROUND,
|
||||
ANTI_ALIAS,
|
||||
INTERLACED,
|
||||
@@ -219,7 +235,7 @@ const BYTE_ORDER: FormatOption = FormatOption {
|
||||
};
|
||||
|
||||
const TIFF_OPTIONS: &[FormatOption] =
|
||||
&[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, BACKGROUND, ANTI_ALIAS, COLOR_MODEL, LZW, BYTE_ORDER, EMBED_ICC];
|
||||
&[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, WIDTH, HEIGHT, BACKGROUND, ANTI_ALIAS, COLOR_MODEL, LZW, BYTE_ORDER, EMBED_ICC];
|
||||
|
||||
const BMP_COLOR_MODEL: FormatOption =
|
||||
FormatOption { name: "colorModel", ty: "string", default: "\"rgb\"", description: "rgb or gray (greys at every depth)" };
|
||||
@@ -256,6 +272,8 @@ const BMP_OPTIONS: &[FormatOption] = &[
|
||||
RECT,
|
||||
PPI,
|
||||
SCALE,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
BACKGROUND,
|
||||
ANTI_ALIAS,
|
||||
BMP_COLOR_MODEL,
|
||||
@@ -275,7 +293,7 @@ const TGA_DEPTH: FormatOption = FormatOption {
|
||||
description: "bits per pixel: 16 (one-bit alpha), 24 (flattened on white) or 32 (keeps transparency)",
|
||||
};
|
||||
|
||||
const TGA_OPTIONS: &[FormatOption] = &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, BACKGROUND, ANTI_ALIAS, TGA_DEPTH];
|
||||
const TGA_OPTIONS: &[FormatOption] = &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, WIDTH, HEIGHT, BACKGROUND, ANTI_ALIAS, TGA_DEPTH];
|
||||
|
||||
const PSD_LAYERS: FormatOption = FormatOption {
|
||||
name: "layers",
|
||||
@@ -306,6 +324,8 @@ const PSD_OPTIONS: &[FormatOption] = &[
|
||||
RECT,
|
||||
PPI,
|
||||
SCALE,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
BACKGROUND,
|
||||
ANTI_ALIAS,
|
||||
COLOR_MODEL,
|
||||
@@ -374,7 +394,7 @@ pub const FORMATS: &[Format] = &[
|
||||
read: true,
|
||||
write: true,
|
||||
raster: true,
|
||||
options: &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, BACKGROUND, ANTI_ALIAS, INTERLACED, EMBED_ICC],
|
||||
options: &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, WIDTH, HEIGHT, BACKGROUND, ANTI_ALIAS, INTERLACED, EMBED_ICC],
|
||||
},
|
||||
Format {
|
||||
id: "jpg",
|
||||
@@ -392,6 +412,8 @@ pub const FORMATS: &[Format] = &[
|
||||
RECT,
|
||||
PPI,
|
||||
SCALE,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
BACKGROUND,
|
||||
ANTI_ALIAS,
|
||||
QUALITY,
|
||||
@@ -411,7 +433,7 @@ pub const FORMATS: &[Format] = &[
|
||||
read: true,
|
||||
write: true,
|
||||
raster: true,
|
||||
options: &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, BACKGROUND, ANTI_ALIAS, LOSSLESS, WEBP_QUALITY],
|
||||
options: &[ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, RECT, PPI, SCALE, WIDTH, HEIGHT, BACKGROUND, ANTI_ALIAS, LOSSLESS, WEBP_QUALITY],
|
||||
},
|
||||
Format {
|
||||
id: "tiff",
|
||||
|
||||
@@ -95,9 +95,9 @@ impl ScreenSize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixels per point for `region`.
|
||||
fn scale_for(self, region: Rect) -> Result<f64> {
|
||||
let per = |px: f64, pt: f64| (pt > 0.0).then(|| px / pt).ok_or_else(|| bad(C, "an empty artboard has no width or height to scale to"));
|
||||
/// Pixels per point for `region` (`cmd` names the command in the error).
|
||||
pub(super) fn scale_for(self, cmd: &str, region: Rect) -> Result<f64> {
|
||||
let per = |px: f64, pt: f64| (pt > 0.0).then(|| px / pt).ok_or_else(|| bad(cmd, "an empty artboard has no width or height to scale to"));
|
||||
match self {
|
||||
Self::Width(w) => per(w, region.width()),
|
||||
Self::Height(h) => per(h, region.height()),
|
||||
@@ -264,7 +264,7 @@ impl Row {
|
||||
// The resolution is stored in the file too.
|
||||
ScreenSize::Ppi(ppi) => o["ppi"] = json!(ppi),
|
||||
size => {
|
||||
o["scale"] = json!(size.scale_for(region)?);
|
||||
o["scale"] = json!(size.scale_for(C, region)?);
|
||||
if let Some(m) = o.as_object_mut() {
|
||||
m.remove("ppi");
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ fn formats_list_the_raster_options() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("document.formats", &json!({})).unwrap();
|
||||
let opts = |id: &str| r["formats"].as_array().unwrap().iter().find(|f| f["id"] == id).unwrap()["options"].clone();
|
||||
for k in ["ppi", "scale", "background", "antiAlias", "interlaced"] {
|
||||
for k in ["ppi", "scale", "width", "height", "background", "antiAlias", "interlaced"] {
|
||||
assert!(opts("png").get(k).is_some(), "png lists {k}");
|
||||
}
|
||||
assert_eq!(opts("png")["antiAlias"]["default"], "art");
|
||||
@@ -334,3 +334,64 @@ fn export_rect_refuses_what_cannot_be_exported() {
|
||||
assert!(s.execute("document.export", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width_or_height_sets_the_pixel_size_of_a_raster_export() {
|
||||
let mut s = session(200.0, 100.0, 1);
|
||||
let size = |s: &mut Session, p: Value| rgba(&b64(&s.execute("document.export", &p).unwrap())).dimensions();
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "width": 512})), (512, 256));
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "height": 128})), (256, 128));
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "width": 512, "height": 50})), (512, 256), "width wins over height");
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "width": 512, "scale": 4, "ppi": 300})), (512, 256), "width wins over scale and ppi");
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "height": 50, "ppi": 300})), (100, 50), "height wins over ppi");
|
||||
assert_eq!(size(&mut s, json!({"format": "webp", "width": 400, "height": null})), (400, 200), "a null height is no height");
|
||||
assert_eq!(size(&mut s, json!({"format": "png", "width": 1})), (2, 1), "the scale stays at 0.01 pixels per point or more");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_width_or_height_that_is_not_a_positive_number_is_refused() {
|
||||
let mut s = session(200.0, 100.0, 1);
|
||||
for (p, msg) in [
|
||||
(json!({"width": 0}), "width must be a positive number, not 0"),
|
||||
(json!({"width": -1}), "width must be a positive number, not -1"),
|
||||
(json!({"height": -2.5}), "height must be a positive number, not -2.5"),
|
||||
(json!({"width": -1, "height": 100}), "width must be a positive number, not -1"),
|
||||
] {
|
||||
let mut p = p.clone();
|
||||
p["format"] = json!("png");
|
||||
let e = s.execute("document.export", &p).unwrap_err().to_string();
|
||||
assert!(e.contains(msg), "{p}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_artboard_file_gets_its_own_scale_for_a_width() {
|
||||
let mut s = session(200.0, 100.0, 1);
|
||||
s.execute("artboard.new", &json!({"x": 300, "y": 0, "width": 100, "height": 400})).unwrap();
|
||||
let r = s.execute("document.export", &json!({"format": "png", "useArtboards": true, "width": 512})).unwrap();
|
||||
let sizes: Vec<(u32, u32)> = r["files"].as_array().unwrap().iter().map(|f| rgba(&b64(f)).dimensions()).collect();
|
||||
assert_eq!(sizes, [(512, 256), (512, 2048)], "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_sized_by_width_stores_the_resolution_that_results() {
|
||||
let mut s = session(200.0, 100.0, 1);
|
||||
// 512 px over 200 pt is 2.56 pixels per point: 184.32 ppi.
|
||||
for format in ["jpg", "png"] {
|
||||
let file = b64(&s.execute("document.export", &json!({"format": format, "width": 512})).unwrap());
|
||||
let (x, y) = super::ppi::resolution(&file).unwrap();
|
||||
assert!((x - 184.32).abs() < 0.6 && (y - 184.32).abs() < 0.6, "{format}: {x} × {y}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_for_screens_rows_keep_their_own_size() {
|
||||
let mut s = session(200.0, 100.0, 1);
|
||||
let r = s
|
||||
.execute(
|
||||
"document.exportForScreens",
|
||||
&json!({"formats": [{"format": "png", "scale": "512w"}], "settings": {"png": {"height": 10, "width": 30}}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(rgba(&b64(&r["files"][0])).dimensions(), (512, 256), "{r}");
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ the two can still win that race.
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`, `ai`; for a Template, `template` and `ait`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it, and a name the panel returns with the extension of a format the panel does not list has it replaced (in a native save, `card.png` and `card.ait` are saved as `card.vectorcraft`, and `notes.v2`, whose extension names no format, is saved as `notes.v2.vectorcraft`); when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
|
||||
| SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`, `profile` (`svg11`/`tiny12`), `encoding` (`utf8`/`utf16`/`latin1`), `embedFonts`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centered in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). In the desktop app the dialog reads its files off the UI thread: `__info` is empty until they are read, `ui.dialog.confirm` returns `{background: true, status}`, `ui.inspect` lists the files still being read in `placing`, and the file is placed (or the cursor loaded) once it is read. With a file, it runs `file.place` centered in the view, at once. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files (in the desktop app each file is read off the UI thread and placed once it is read, and `ui.inspect` lists it in `placing` until then) and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
|
||||
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, width?, height?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `width` or `height` sets the size of each file in pixels instead (`width` wins), and the file stores the resolution that results; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
|
||||
| `app.export` JPEG options | `{path?, format: jpg, quality?: 0–100, colorModel?: rgb \| cmyk \| gray, method?: baseline \| optimized \| progressive, scans?: 3–5, embedIcc?, imageMap?: none \| client \| server}` | `cmyk` writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); `embedIcc` (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; `imageMap` also writes `<stem>.html` (client-side `<map>`) or `<stem>.map` (NCSA) beside the image for the objects with a URL and an Image Map shape (`attributes.set`), returned in `linked`. The `jpgOptions` dialog has these fields too (`quality` is shown 0–10; a CMYK document defaults to `cmyk`) |
|
||||
| `app.export` PNG-8 and GIF | `{path?, format: png8 \| gif, colors?: 2–256, reduction?: perceptual \| selective \| adaptive \| web \| blackWhite \| gray, dither?: none \| diffusion \| pattern \| noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options}` | a palette of at most `colors` entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless `transparency` is false, partly transparent edges are blended over `matte` (`none` keeps their colour). `png8` writes an indexed `.png` (1–8 bits per pixel); Export for Screens rows take both. The `png8Options` / `gifOptions` dialogs have these fields |
|
||||
| `app.export` Text and WebP | `{path?, format: txt, encoding?: utf8 \| utf16, lineEndings?: lf \| crlf, selectionOnly?}` / `{format: webp, lossless?, quality?}` | Text writes the stories in stacking order (back to front; a thread once; hidden objects and template layers left out), each line ended by `lineEndings`; `utf16` is little-endian with a byte order mark. Export As → Text opens `txtOptions` (fields `path`, `encoding`, `lineEndings`, `selectionOnly`; OK writes the file). WebP is always lossless: `lossless: false` adds a warning |
|
||||
|
||||
+4
-1
@@ -648,7 +648,10 @@ way.
|
||||
|
||||
Raster exports take more options in `options`: `ppi` (72, 150, 300…; stored in the file), `background`
|
||||
(`transparent`, `white`, `black` or `"#rrggbb"`), `antiAlias` (`none`, `art`, `type`: text snapped to pixels) and,
|
||||
for PNG, `interlaced`.
|
||||
for PNG, `interlaced`. `width` or `height` sets the size of each file in pixels: each artboard (with `useArtboards`,
|
||||
each file), `rect` or art bounds gets the scale that makes it that wide or high, within 0.01–64 pixels per point, and
|
||||
the file stores the resolution that results. `width` wins over `height`, and either one wins over `ppi` and `scale`. A
|
||||
value that is not a positive number is refused. Export for Screens rows keep their own size (`scale: "512w"`).
|
||||
|
||||
Long-tail commands:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user