mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 01:20:59 +00:00
Save As .ai: write the editing data's header, palette and layers in the full layout
The editing data now has the layout of the .ai files it was learned from: - Lines end in CR LF. Line breaks inside strings stay escaped. - The header adds %%For (the document's author), %%Canvassize 16383, %AI7_ImageSettings, %AI5_ArtFlags, %AI5_TargetResolution, %AI5_OpenViewLayers (a digit for each layer, 7 shown and 6 hidden) and %AI7_GridSettings (the document's grid spacing and subdivisions), in the order those files have them. - The setup starts with 0 Bn (no gradient definitions). - The palette starts with the Registration swatch: Xs in a CMYK document, Xz with black's CMYK equivalent in an RGB one. Reading the file back does not add a swatch for it. - Each layer and sublayer carries its name as an XML id in an /ArtDictionary after Ln. An object named like a layer gets a numbered id. - The LastModified dates of the PieceInfo dictionaries end in +00'00'.
This commit is contained in:
@@ -8,15 +8,20 @@
|
||||
//! groups and compound paths (§5.3.5), guides (§6) and layers (§9), with the newer forms the reader
|
||||
//! reads from files users sent:
|
||||
//!
|
||||
//! - Header comments: `%%BoundingBox`, `%%HiResBoundingBox`, `%AI5_FileFormat 14.0`, the first
|
||||
//! artboard as `%AI3_Cropmarks` and `%AI5_ArtSize`, `%AI5_RulerUnits`, `%AI9_ColorModel` (1 RGB, 2
|
||||
//! CMYK), `%AI5_NumLayers`, and the version comment `%%AI8_CreatorVersion` ([`CREATOR_VERSION`]).
|
||||
//! - Setup: the color swatches (`%AI5_BeginPalette`, `0 0 Pb`, each swatch's color operator and
|
||||
//! `(name) Pc`, a color group as `0 (name) count Pg` before its swatches, `PB`), then the
|
||||
//! artboards as the `ArtboardArray` of the `/Document` dictionary, on `%_` lines.
|
||||
//! - Header comments: `%%For` (the document's author), `%%Canvassize`, `%%BoundingBox`,
|
||||
//! `%%HiResBoundingBox`, `%AI5_FileFormat 14.0`, `%AI7_ImageSettings`, the first artboard as
|
||||
//! `%AI3_Cropmarks` and `%AI5_ArtSize`, `%AI5_RulerUnits`, `%AI9_ColorModel` (1 RGB, 2 CMYK),
|
||||
//! `%AI5_ArtFlags`, `%AI5_TargetResolution`, `%AI5_NumLayers`, `%AI5_OpenViewLayers` (a digit for
|
||||
//! each layer, 7 shown and 6 hidden), `%AI7_GridSettings` (§2.1.1), and the version comment
|
||||
//! `%%AI8_CreatorVersion` ([`CREATOR_VERSION`]).
|
||||
//! - Setup: `0 Bn` (no gradient definitions, §3), the color swatches (`%AI5_BeginPalette`,
|
||||
//! `0 0 Pb`, the Registration swatch, each swatch's color operator and `(name) Pc`, a color group as `0 (name) count Pg`
|
||||
//! before its swatches, `PB`), then the artboards as the `ArtboardArray` of the `/Document`
|
||||
//! dictionary, on `%_` lines.
|
||||
//! - Layers: `%AI5_BeginLayer`, `visible preview unlocked printing dimmed 0 1 colorIndex r g b 0
|
||||
//! dimPercent 0 Lb` (14 operands; a color that isn't one of the preset layer colors has the
|
||||
//! index -1), `(name) Ln`, its art and sublayers, `LB`, `%AI5_EndLayer--`.
|
||||
//! index -1), `(name) Ln`, the layer's name as an XML id in an `/ArtDictionary`, its art and
|
||||
//! sublayers, `LB`, `%AI5_EndLayer--`.
|
||||
//! - Colors: `g` gray, `k` CMYK, `c m y k r g b Xa` RGB (Lab is written as RGB); a spot color
|
||||
//! `c m y k (name) tint x` (CMYK) or `c m y k r g b|L a b (name) tint 1|2 Xx` (RGB or Lab); a
|
||||
//! global process color `… (name) tint model Xk`; Registration `1 1 1 1 ([Registration]) tint
|
||||
@@ -25,7 +30,7 @@
|
||||
//! an `/ArtDictionary` whose `AI10_ArtUID` is the name as an XML id. Guides are `(N) *`, the
|
||||
//! path, `(N) *` (`(n) *` when closed).
|
||||
//!
|
||||
//! Strings are written in UTF-8. Art space has y up, its origin at the first artboard's bottom-left
|
||||
//! Lines end in CR LF. Strings are written in UTF-8. Art space has y up, its origin at the first artboard's bottom-left
|
||||
//! corner. What the form above can't hold is written as plain filled and stroked paths, each such
|
||||
//! loss a warning: type as its outlines, gradients as their average color, pattern fills as their
|
||||
//! tiles cut to the shape, clipping groups and clipping layers as their art cut to the clipping
|
||||
@@ -60,6 +65,16 @@ const ROUNDTRIP_VERSION: u32 = 24;
|
||||
const ROUNDTRIP_STREAM_TYPE: u32 = 2;
|
||||
/// What starts the data streams of Zstandard-compressed data.
|
||||
const ZSTD_MARKER: &[u8] = b"%AI24_ZStandard_Data";
|
||||
/// `%%Canvassize`: the side of the canvas in points, as the files users sent have it.
|
||||
const CANVAS_SIZE: u32 = 16383;
|
||||
/// `%AI5_ArtFlags` (§2.1.1, Table 4), as the files users sent have them: page setup, print tiles,
|
||||
/// placed images, preview patterns, split long paths, tile full pages, printer's default
|
||||
/// screens, auto default screens, compatible gradient printing.
|
||||
const ART_FLAGS: &str = "0 0 0 1 0 0 1 0 0";
|
||||
/// `%AI5_TargetResolution` (§2.1.1).
|
||||
const TARGET_RESOLUTION: u32 = 800;
|
||||
/// The gridlines' gray: VectorCraft's default grid color (200 of 255).
|
||||
const GRID_GRAY: f64 = 200.0 / 255.0;
|
||||
|
||||
/// The first line of a PostScript file (the DSC version comment).
|
||||
const PS_HEADER: &str = "%!PS-Adobe-3.0"; // brand-ok: the PostScript header comment
|
||||
@@ -98,7 +113,6 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
for layer in &doc.layers {
|
||||
w.layer(layer);
|
||||
}
|
||||
let layers = doc.layers.len();
|
||||
let (width, height) = (board.width(), board.height());
|
||||
let art = doc
|
||||
.layers
|
||||
@@ -109,15 +123,22 @@ pub fn export_ai_editing(doc: &Document, o: &AiEditingOptions) -> Result<AiEditi
|
||||
.filter(|b| b.is_finite())
|
||||
.reduce(|a, b| a.union(b))
|
||||
.unwrap_or(Rect::new(0.0, 0.0, width, height));
|
||||
let header = header(doc, o, &w, art, (width, height), layers);
|
||||
let header = header(doc, o, &w, art, (width, height));
|
||||
let mut s = header.clone();
|
||||
s.push_str("%%BeginProlog\n%%EndProlog\n%%BeginSetup\n");
|
||||
// No gradient definitions: gradients are written as their average color.
|
||||
s.push_str("%%BeginProlog\n%%EndProlog\n%%BeginSetup\n0 Bn\n");
|
||||
w.palette(&mut s);
|
||||
artboards(&mut s, doc, to_art);
|
||||
s.push_str("%%EndSetup\n");
|
||||
s.push_str(&w.body);
|
||||
s.push_str("%%PageTrailer\ngsave annotatepage grestore showpage\n%%Trailer\n%%EOF\n");
|
||||
Ok(AiEditingOutput { program: s.into_bytes(), header: header.into_bytes(), warnings: w.warnings })
|
||||
// Lines end in CR LF, as in the files users sent; line breaks inside strings are escaped.
|
||||
Ok(AiEditingOutput { program: crlf(&s), header: crlf(&header), warnings: w.warnings })
|
||||
}
|
||||
|
||||
/// `s` with each line ending in CR LF.
|
||||
fn crlf(s: &str) -> Vec<u8> {
|
||||
s.replace('\n', "\r\n").into_bytes()
|
||||
}
|
||||
|
||||
/// `doc` with its geometry effects baked ([`vectorcraft_effects::bake_document`]), in its symbols'
|
||||
@@ -163,17 +184,20 @@ pub fn private_data(doc: &Document, o: &AiEditingOptions) -> Result<(PrivateData
|
||||
|
||||
/// The header comments, through `%%EndComments`. `art`: the visible art's bounds, `size` the first
|
||||
/// artboard's, in art space.
|
||||
fn header(doc: &Document, o: &AiEditingOptions, w: &Writer, art: Rect, (width, height): (f64, f64), layers: usize) -> String {
|
||||
fn header(doc: &Document, o: &AiEditingOptions, w: &Writer, art: Rect, (width, height): (f64, f64)) -> String {
|
||||
let mut s = String::new();
|
||||
s.push_str(PS_HEADER);
|
||||
s.push('\n');
|
||||
let _ = writeln!(s, "%%Creator: VectorCraft {}", env!("CARGO_PKG_VERSION"));
|
||||
let _ = writeln!(s, "%%AI8_CreatorVersion: {CREATOR_VERSION}");
|
||||
// The user and the organization (§2.1.1).
|
||||
let _ = writeln!(s, "%%For: {} ()", ps::bytes(doc.metadata.author.as_bytes()));
|
||||
let _ = writeln!(s, "%%Title: {}", ps::bytes(o.title.as_bytes()));
|
||||
if let Some(t) = o.created {
|
||||
let [y, mo, d, h, mi, _] = vectorcraft_doc::metadata::civil(t);
|
||||
let _ = writeln!(s, "%%CreationDate: ({mo}/{d}/{y:04}) ({h:02}:{mi:02})");
|
||||
}
|
||||
let _ = writeln!(s, "%%Canvassize: {CANVAS_SIZE}");
|
||||
let _ = writeln!(s, "%%BoundingBox: {} {} {} {}", art.x0.floor() as i64, art.y0.floor() as i64, art.x1.ceil() as i64, art.y1.ceil() as i64);
|
||||
s.push_str("%%HiResBoundingBox: ");
|
||||
ps::push_nums(&mut s, &[art.x0, art.y0, art.x1, art.y1]);
|
||||
@@ -181,6 +205,8 @@ fn header(doc: &Document, o: &AiEditingOptions, w: &Writer, art: Rect, (width, h
|
||||
s.push('\n');
|
||||
s.push_str("%%DocumentProcessColors: Cyan Magenta Yellow Black\n");
|
||||
s.push_str("%AI5_FileFormat 14.0\n%AI3_ColorUsage: Color\n");
|
||||
// Linked images aren't embedded (the data holds no images).
|
||||
s.push_str("%AI7_ImageSettings: 0\n");
|
||||
let spots = w.spot_swatches();
|
||||
for (i, (name, _)) in spots.iter().enumerate() {
|
||||
let _ = writeln!(s, "{} {}", if i == 0 { "%%DocumentCustomColors:" } else { "%%+" }, ps::bytes(name.as_bytes()));
|
||||
@@ -201,7 +227,18 @@ fn header(doc: &Document, o: &AiEditingOptions, w: &Writer, art: Rect, (width, h
|
||||
s.push_str("%AI24_LargeCanvasScale: 1\n");
|
||||
let model = if doc.color_mode == vectorcraft_doc::ColorMode::Cmyk { 2 } else { 1 };
|
||||
let _ = writeln!(s, "%AI9_ColorModel: {model}");
|
||||
let _ = writeln!(s, "%AI5_NumLayers: {layers}");
|
||||
let _ = writeln!(s, "%AI5_ArtFlags: {ART_FLAGS}");
|
||||
let _ = writeln!(s, "%AI5_TargetResolution: {TARGET_RESOLUTION}");
|
||||
let _ = writeln!(s, "%AI5_NumLayers: {}", doc.layers.len());
|
||||
// A digit for each layer: 7 shown, 6 hidden.
|
||||
let views: String = doc.layers.iter().map(|l| if l.visible { '7' } else { '6' }).collect();
|
||||
if !views.is_empty() {
|
||||
let _ = writeln!(s, "%AI5_OpenViewLayers: {views}");
|
||||
}
|
||||
// Gridlines and subdivisions (§2.1.1, Table 6): in back, lines, in VectorCraft's grid gray.
|
||||
let (space, subdiv) = (ps::num(doc.grid.spacing), doc.grid.subdivisions);
|
||||
let gray = ps::num(GRID_GRAY);
|
||||
let _ = writeln!(s, "%AI7_GridSettings: {space} {subdiv} {space} {subdiv} 1 0 {gray} {gray} {gray} {gray} {gray} {gray}");
|
||||
s.push_str("%%EndComments\n");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ fn opts() -> AiEditingOptions {
|
||||
AiEditingOptions { title: "Sample".into(), created: Some(0) }
|
||||
}
|
||||
|
||||
/// The program written from `doc`, as text.
|
||||
/// The program written from `doc`, as text with its CR LF line ends read as LF.
|
||||
fn program(doc: &Document) -> String {
|
||||
String::from_utf8(export_ai_editing(doc, &opts()).unwrap().program).unwrap()
|
||||
String::from_utf8(export_ai_editing(doc, &opts()).unwrap().program).unwrap().replace("\r\n", "\n")
|
||||
}
|
||||
|
||||
/// `doc` written, then read back from the private data alone.
|
||||
@@ -266,7 +266,8 @@ fn writing_again_gives_the_same_bytes() {
|
||||
assert_eq!(first.program, export_ai_editing(&doc, &opts()).unwrap().program);
|
||||
assert_eq!(private_data(&doc, &opts()).unwrap().0, private_data(&doc, &opts()).unwrap().0);
|
||||
// What the reader reads back is written as the same program.
|
||||
assert_eq!(program(&read_back(&doc)), String::from_utf8(first.program).unwrap());
|
||||
let read = export_ai_editing(&read_back(&doc), &opts()).unwrap();
|
||||
assert_eq!(read.program, first.program);
|
||||
assert!(first.warnings.is_empty(), "{:?}", first.warnings);
|
||||
}
|
||||
|
||||
@@ -283,17 +284,18 @@ fn the_data_is_zstandard_compressed_with_the_header_as_metadata() {
|
||||
assert!(out.program.starts_with(&out.header));
|
||||
assert_eq!(p.metadata, out.header);
|
||||
let header = String::from_utf8(out.header).unwrap();
|
||||
assert!(header.ends_with("%%EndComments\n"));
|
||||
assert!(header.ends_with("%%EndComments\r\n"));
|
||||
let header = header.replace("\r\n", "\n");
|
||||
for line in [
|
||||
"%%AI8_CreatorVersion: 30.7.0\n",
|
||||
"%AI5_FileFormat 14.0\n",
|
||||
"%%AI8_CreatorVersion: 30.7.0\n%%For: () ()\n%%Title: (Sample)\n",
|
||||
"%%Canvassize: 16383\n",
|
||||
"%AI5_FileFormat 14.0\n%AI3_ColorUsage: Color\n%AI7_ImageSettings: 0\n%%DocumentCustomColors: (Gold)\n%%+ (Sky)\n%%+ (Mint)\n",
|
||||
"%AI3_Cropmarks: 0 0 200 100\n",
|
||||
"%AI5_ArtSize: 200 100\n",
|
||||
"%AI9_ColorModel: 1\n",
|
||||
"%AI5_NumLayers: 2\n",
|
||||
"%%DocumentCustomColors: (Gold)\n%%+ (Sky)\n%%+ (Mint)\n",
|
||||
"%AI9_ColorModel: 1\n%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0\n%AI5_TargetResolution: 800\n%AI5_NumLayers: 2\n",
|
||||
"%AI5_OpenViewLayers: 77\n%AI7_GridSettings: 72 8 72 8 1 0 0.7843 0.7843 0.7843 0.7843 0.7843 0.7843\n%%EndComments\n",
|
||||
] {
|
||||
assert!(header.contains(line), "{line}");
|
||||
assert!(header.contains(line), "{line}\n{header}");
|
||||
}
|
||||
assert_eq!((p.container_version, p.creator_version, p.roundtrip_version, p.roundtrip_stream_type), (12, 30, 24, 2));
|
||||
}
|
||||
@@ -328,7 +330,7 @@ fn many_objects_of_one_name_are_numbered_in_turn() {
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
let text = program(&doc);
|
||||
let ids: std::collections::HashSet<&str> = text.lines().filter(|l| l.starts_with("%_/XMLUID")).collect();
|
||||
let ids: std::collections::HashSet<&str> = text.lines().filter(|l| l.starts_with("%_/XMLUID : (Dot")).collect();
|
||||
assert_eq!(ids.len(), 20_000);
|
||||
assert!(text.contains("(Dot_00000000000000019999_)"));
|
||||
}
|
||||
@@ -340,6 +342,61 @@ fn layers_have_fourteen_operands() {
|
||||
assert!(text.contains("%AI5_BeginLayer\n0 1 0 0 1 0 1 -1 10 200 30 0 70 0 Lb\n(Sub) Ln\n"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layers_are_named_as_xml_ids_after_their_names() {
|
||||
let mut doc = sample();
|
||||
let layer = doc.layers[0].id;
|
||||
let mut n = Node::path(doc.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::default());
|
||||
n.name = Some("Back".into());
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let text = program(&doc);
|
||||
assert!(text.contains("(Back) Ln\n%_/ArtDictionary :\n%_/XMLUID : (Back) ; (AI10_ArtUID) ,\n%_;\n%_\n"), "{text}");
|
||||
assert!(text.contains("(Sub) Ln\n%_/ArtDictionary :\n%_/XMLUID : (Sub) ; (AI10_ArtUID) ,\n"), "{text}");
|
||||
// An object named as a layer is numbered.
|
||||
assert!(text.contains("%_/XMLUID : (Back_00000000000000000001_) ; (AI10_ArtUID) ,"), "{text}");
|
||||
// The reader keeps the names where they were.
|
||||
let read = read_back(&doc);
|
||||
assert_eq!(read.layers[0].name.as_deref(), Some("Back"));
|
||||
let names: Vec<Option<&str>> = read.layers[0].children().unwrap().iter().map(|c| c.name.as_deref()).collect();
|
||||
assert_eq!(names.last(), Some(&Some("Back")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_end_in_cr_lf() {
|
||||
let mut doc = sample();
|
||||
doc.metadata.author = "A\nB".into();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
let text = String::from_utf8(out.program).unwrap();
|
||||
assert_eq!(text.matches('\n').count(), text.matches("\r\n").count());
|
||||
assert_eq!(text.matches('\r').count(), text.matches("\r\n").count());
|
||||
// A line break in a string stays in the string, escaped.
|
||||
assert!(text.contains("%%For: (A\\nB) ()\r\n"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_palette_starts_with_registration() {
|
||||
let mut doc = sample();
|
||||
let rgb = program(&doc);
|
||||
assert!(rgb.contains("0 0 Pb\n"), "{rgb}");
|
||||
let cell = rgb.split("0 0 Pb\n").nth(1).unwrap().lines().take(2).collect::<Vec<_>>();
|
||||
assert!(cell[0].ends_with(" 0 0 0 ([Registration]) 0 1 Xz") && cell[0].split(' ').count() == 11, "{cell:?}");
|
||||
assert_eq!(cell[1], "([Registration]) Pc");
|
||||
doc.color_mode = vectorcraft_doc::ColorMode::Cmyk;
|
||||
assert!(program(&doc).contains("0 0 Pb\n1 1 1 1 ([Registration]) 0 Xs\n([Registration]) Pc\n"));
|
||||
// It's the built-in swatch: reading back adds no swatch.
|
||||
assert_eq!(swatches(&read_back(&doc)), swatches(&doc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_layers_open_hidden() {
|
||||
let mut doc = sample();
|
||||
let mut hidden = Node::layer(doc.alloc_id(), "Off", LayerColor::Preset(1));
|
||||
hidden.visible = false;
|
||||
doc.layers.push(Arc::new(hidden));
|
||||
let text = program(&doc);
|
||||
assert!(text.contains("%AI5_NumLayers: 3\n%AI5_OpenViewLayers: 776\n"), "{text}");
|
||||
}
|
||||
|
||||
/// A 200 × 100 document of what slice 1 writes as plain paths: a gradient fill, a clipping group,
|
||||
/// a compound path, an image, type, a symbol instance and a half-transparent square.
|
||||
fn unsupported() -> Document {
|
||||
|
||||
@@ -113,7 +113,7 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer or sublayer: `%AI5_BeginLayer`, its options `Lb`, its name `Ln`, its art, then
|
||||
/// A layer or sublayer: `%AI5_BeginLayer`, its options `Lb`, its name `Ln` and as an XML id, its art, then
|
||||
/// `after` (outside its clipping path when it clips), `LB`, `%AI5_EndLayer--`.
|
||||
fn write_layer(&mut self, n: &Node, after: &[Arc<Node>]) {
|
||||
let NodeKind::Layer { color, template, printable, children, clip, preview, dim_images } = &n.kind else { return };
|
||||
@@ -131,6 +131,7 @@ impl<'a> Writer<'a> {
|
||||
self.transparency(n);
|
||||
let flag = |b: bool| u8::from(b);
|
||||
let dim = dim_images.unwrap_or(50);
|
||||
let name = n.name.as_deref().unwrap_or("Layer");
|
||||
let _ = writeln!(
|
||||
self.body,
|
||||
"%AI5_BeginLayer\n{} {} {} {} {} 0 1 {index} {r} {g} {b} 0 {dim} 0 Lb\n{} Ln",
|
||||
@@ -139,8 +140,9 @@ impl<'a> Writer<'a> {
|
||||
flag(!n.locked),
|
||||
flag(*printable && !*template),
|
||||
flag(dim_images.is_some() || *template),
|
||||
ps::bytes(n.name.as_deref().unwrap_or("Layer").as_bytes()),
|
||||
ps::bytes(name.as_bytes()),
|
||||
);
|
||||
self.xml_id(if name.is_empty() { "Layer" } else { name });
|
||||
self.state = None;
|
||||
if *clip {
|
||||
self.warn(CLIPPING);
|
||||
@@ -269,10 +271,15 @@ impl<'a> Writer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `n`'s name, after it: an `/ArtDictionary` whose `AI10_ArtUID` is the name as an XML id,
|
||||
/// numbered (`_` and 20 digits, `_`) when another object has that id.
|
||||
/// `n`'s name, after it (see [`Self::xml_id`]).
|
||||
fn name(&mut self, n: &Node) {
|
||||
let Some(name) = n.name.as_deref().filter(|s| !s.is_empty()) else { return };
|
||||
self.xml_id(name);
|
||||
}
|
||||
|
||||
/// An `/ArtDictionary` whose `AI10_ArtUID` is `name` as an XML id, numbered (`_` and 20
|
||||
/// digits, `_`) when a layer or another object has that id.
|
||||
fn xml_id(&mut self, name: &str) {
|
||||
let base = vectorcraft_doc::xml_id::from_object_name(name);
|
||||
let mut id = base.clone();
|
||||
let next = self.next.entry(base.clone()).or_insert(0);
|
||||
@@ -604,11 +611,12 @@ impl<'a> Writer<'a> {
|
||||
|
||||
// ---------- swatches ----------
|
||||
|
||||
/// The swatches: `%AI5_BeginPalette`, `0 0 Pb`, each color swatch's color and `(name) Pc`
|
||||
/// The swatches: `%AI5_BeginPalette`, `0 0 Pb`, the Registration swatch, each color swatch's color and `(name) Pc`
|
||||
/// (the ungrouped ones first, then each color group's after `0 (name) count Pg`), `PB`,
|
||||
/// `%AI5_EndPalette`.
|
||||
pub(super) fn palette(&mut self, s: &mut String) {
|
||||
s.push_str("%AI5_BeginPalette\n0 0 Pb\n");
|
||||
self.registration_cell(s);
|
||||
let doc = self.doc;
|
||||
for sw in doc.swatches.iter() {
|
||||
self.swatch(s, sw);
|
||||
@@ -623,6 +631,22 @@ impl<'a> Writer<'a> {
|
||||
s.push_str("PB\n%AI5_EndPalette\n");
|
||||
}
|
||||
|
||||
/// The Registration swatch, which every palette in the files users sent starts with: in a
|
||||
/// CMYK document `1 1 1 1 ([Registration]) 0 Xs`, in an RGB one black's CMYK equivalent,
|
||||
/// `0 0 0 ([Registration]) 0 1 Xz`; then `([Registration]) Pc`.
|
||||
fn registration_cell(&self, s: &mut String) {
|
||||
let name = ps::bytes(REGISTRATION.as_bytes());
|
||||
if self.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk {
|
||||
let _ = writeln!(s, "1 1 1 1 {name} 0 Xs");
|
||||
} else {
|
||||
let black = Color::rgb(0.0, 0.0, 0.0);
|
||||
let cmyk = self.cms.to_cmyk(&black, self.cms.settings().intent).map(unit);
|
||||
ps::push_nums(s, &cmyk.map(f64::from));
|
||||
let _ = writeln!(s, "0 0 0 {name} 0 1 Xz");
|
||||
}
|
||||
let _ = writeln!(s, "{name} Pc");
|
||||
}
|
||||
|
||||
/// One swatch of the palette: a color swatch's color and `(name) Pc`; gradient and pattern
|
||||
/// swatches are left out.
|
||||
fn swatch(&mut self, s: &mut String, sw: &Swatch) {
|
||||
|
||||
@@ -72,10 +72,11 @@ fn stream(bytes: &[u8]) -> Vec<u8> {
|
||||
s
|
||||
}
|
||||
|
||||
/// `at` (Unix seconds) as a PDF date string in UTC (ISO 32000-1 §7.9.4).
|
||||
/// `at` (Unix seconds) as a PDF date string in UTC (ISO 32000-1 §7.9.4), with the offset written
|
||||
/// as hours and minutes, as in the files users sent.
|
||||
fn pdf_date(at: i64) -> String {
|
||||
let [y, mo, d, h, mi, s] = vectorcraft_doc::metadata::civil(at);
|
||||
format!("(D:{y:04}{mo:02}{d:02}{h:02}{mi:02}{s:02}Z)")
|
||||
format!("(D:{y:04}{mo:02}{d:02}{h:02}{mi:02}{s:02}+00'00')")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -120,7 +121,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dates_are_in_utc() {
|
||||
assert_eq!(pdf_date(0), "(D:19700101000000Z)");
|
||||
assert_eq!(pdf_date(1_793_000_000), "(D:20261026073320Z)");
|
||||
assert_eq!(pdf_date(0), "(D:19700101000000+00'00')");
|
||||
assert_eq!(pdf_date(1_793_000_000), "(D:20261026073320+00'00')");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user