Merge pull request #2044 from midasmatterhorn/fix/issue-2031-large-canvas-scale

eps: a Large Canvas editing copy is read at the page's scale (#2031)
This commit is contained in:
BFlat
2026-10-16 02:14:00 +07:00
committed by GitHub
5 changed files with 100 additions and 8 deletions
+25 -4
View File
@@ -114,9 +114,12 @@ pub(crate) fn slot_frame(name: Option<&str>) -> usize {
name.and_then(|n| n.strip_prefix(TEXT_SLOT)).and_then(|rest| rest.split_once(':')).and_then(|(_, f)| f.parse().ok()).unwrap_or(0)
}
/// Read the structure of the editing data `data`.
pub fn read(data: &[u8]) -> Result<Structure, String> {
/// Read the structure of the editing data `data`, applying the file's `%AI24_LargeCanvasScale` only
/// when `large_canvas` is set: an EPS's page is drawn at the full size, but a `.ai`'s PDF part may
/// keep the reduced one, which the PDF import reads as it is (#2031).
pub fn read(data: &[u8], large_canvas: bool) -> Result<Structure, String> {
let mut r = Reader::new(data);
r.large_canvas = large_canvas.then_some(1.0);
r.run()?;
r.finish()
}
@@ -391,6 +394,10 @@ struct Reader<'a> {
/// The artboard's size and centre in files without artboards or crop marks.
art_size: Option<(f64, f64)>,
template_center: Option<(f64, f64)>,
/// `%AI24_LargeCanvasScale`: a Large Canvas document's editing data is written at 1/N scale
/// (see [`Reader::ensure_space`]); 1 (no scaling) when the file doesn't declare one, `None`
/// when the declared factor is not applied ([`read`]).
large_canvas: Option<f64>,
/// This reader reads a pattern's definition (see [`Reader::define_pattern`]).
pattern: Option<PatternHead>,
/// The tile's top left (y down) of each pattern read, by name.
@@ -525,6 +532,7 @@ impl<'a> Reader<'a> {
slot_names: HashMap::new(),
art_size: None,
template_center: None,
large_canvas: None,
pattern: None,
pattern_tiles: HashMap::new(),
text: None,
@@ -660,6 +668,16 @@ impl<'a> Reader<'a> {
} else if let Some(v) = line.strip_prefix("AI3_TemplateBox:") {
self.template_center = two(v);
self.template = four(v).or(self.template);
} else if let Some(v) = line.strip_prefix("AI24_LargeCanvasScale:") {
// Illustrator's Large Canvas writes the editing data (art and boxes) at 1/N scale, N
// being this; the printed page applies the same factor, so the art reaches the page's
// size once it is applied to the art space ([`Self::ensure_space`]). A value outside
// the range Illustrator writes is left alone.
if let (Some(scale), Ok(n)) = (&mut self.large_canvas, v.trim().parse::<f64>())
&& (1.0..=100.0).contains(&n)
{
*scale = n;
}
} else if let Some(v) = line.strip_prefix("AI9_ColorModel:") {
self.color_model = Some(v.trim() == "2");
}
@@ -1858,14 +1876,17 @@ impl<'a> Reader<'a> {
Some(Rect::new(x0, y0, x1, y1)).filter(|r| r.width() > 0.0 && r.height() > 0.0)
}
/// Art space → the document (fixed when the art starts, from the artboards read by then).
/// Art space → the document (fixed when the art starts, from the artboards read by then). A
/// Large Canvas editing copy is written at 1/N scale ([`Self::large_canvas`]): the factor makes
/// its art and boxes reach the page's size, which is what the page it is opened beside holds.
fn ensure_space(&mut self) -> Affine {
if let Some(m) = self.to_doc {
return m;
}
let first = self.first_board().or_else(|| self.bbox_board());
let top_left = first.map_or(Point::new(0.0, 792.0), |r| Point::new(r.x0, r.y1));
let m = Affine::new([1.0, 0.0, 0.0, -1.0, -top_left.x, top_left.y]);
let s = self.large_canvas.unwrap_or(1.0);
let m = Affine::new([s, 0.0, 0.0, -s, -s * top_left.x, s * top_left.y]);
self.to_doc = Some(m);
m
}
+5
View File
@@ -9,6 +9,11 @@ use vectorcraft_doc::{AppearanceItem, ColorMode, Document, Knockout, LayerColor,
use super::*;
/// [`super::read`] as an EPS reads it.
fn read(data: &[u8]) -> Result<Structure, String> {
super::read(data, true)
}
/// Editing data: header comments (crop marks 0 0 200 100), then `body`.
fn stream(body: &str) -> String {
format!(
+27 -1
View File
@@ -583,7 +583,7 @@ impl Story {
/// Roughly where the story's type lies on the document: its frame's box, or round its lines
/// for point type (what the page draws of it lies there).
pub(super) fn region(&self, template: (f64, f64), to_doc: Affine) -> Option<Rect> {
let size = self.runs.iter().map(|(_, s)| s.size).fold(0.0, f64::max);
let size = self.runs.iter().map(|(_, s)| s.size).fold(0.0, f64::max) * art_scale(to_doc);
let bounds = |segments: &[[f64; 8]]| path_of(segments, template, to_doc).bounds();
let r = match self.frames.first()? {
FrameKind::Area(s) => bounds(s)?,
@@ -634,6 +634,9 @@ impl Story {
if text.trim().is_empty() && !threaded {
return None;
}
// The text document measures in the art's units too: the document's scale carries its
// sizes and spacings onto the page (a Large Canvas copy is written at 1:10; #2031).
let scale = scale * art_scale(to_doc);
let leading = self.line_leading(scale);
let chars: Vec<char> = text.chars().collect();
let mut runs = vec![];
@@ -776,6 +779,13 @@ fn char_style(s: &Style, scale: f64, leading: Option<f64>) -> CharStyle {
}
}
/// How many document points one unit of the art is: `to_doc`'s scale, which carries the art's
/// lengths onto the document (10 for a Large Canvas copy written at 1:10; see `ai::read`).
fn art_scale(to_doc: Affine) -> f64 {
let [a, b, ..] = to_doc.as_coeffs();
(a * a + b * b).sqrt().max(1e-9)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -852,6 +862,22 @@ mod tests {
assert!((s.line_leading(0.5).unwrap() - 7.0).abs() < 1e-9);
}
/// #2031: a Large Canvas editing copy measures its text document in its art's units, a tenth
/// of the page's: the story's size and leading reach the page through `to_doc`'s scale.
#[test]
fn a_story_in_a_scaled_art_space_keeps_its_size_and_leading() {
let s = story("one\rtwo\r", &[0.0, 20.0], &Para::default());
// A copy written at 1:10 (see `ai::read::ensure_space`): its art's units reach the
// document, 100 points to the page, through `to_doc`'s scale.
let to_doc = Affine::scale(10.0) * Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 100.0]);
let t = match s.node(NodeId(1), 0, (0.0, 0.0), to_doc).unwrap().kind {
NodeKind::Text(t) => t,
_ => panic!("not text"),
};
assert_eq!(t.runs[0].style.size, 130.0, "13 pt at the page's scale");
assert_eq!(t.runs[0].style.leading, Some(200.0), "a 20-unit line pitch at the page's scale");
}
#[test]
fn leading_between_lines_of_a_paragraph_is_their_distance() {
// A line break inside a paragraph has no paragraph spacing between its lines.
+7 -3
View File
@@ -872,7 +872,9 @@ struct Frame<'a> {
/// nothing is compared. `outlined`: the page's type was read as outlines, so it has none to put in
/// the text objects.
fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, trust: bool) -> Result<(Document, Vec<String>), String> {
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, warnings } = ai::read(data)?;
// An EPS (its page is its art) draws a Large Canvas page at full size; a `.ai` keeps its art
// space, as its PDF part may (#2031).
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, warnings } = ai::read(data, matches!(page, Page::Art))?;
let (mut shown, mut hidden) = (vec![], vec![]);
let mut frame = None;
if let Some(visible) = visible {
@@ -881,11 +883,13 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr
Page::Artboard => artboard.ok_or("its editing data doesn't say where its artboard is")?,
};
let page_rect = visible.artboards.first().map(|a| a.rect).ok_or("it has no page")?;
if (art_box.width() - page_rect.width()).abs() > 0.5 || (art_box.height() - page_rect.height()).abs() > 0.5 {
// The layers' art in the document's terms: a Large Canvas editing copy is written at 1/N
// scale, which `to_doc` carries, so its art and the page reach the same size (#2031).
let rect = to_doc.transform_rect_bbox(art_box);
if (rect.width() - page_rect.width()).abs() > 0.5 || (rect.height() - page_rect.height()).abs() > 0.5 {
return Err("its editing data is for a different page".into());
}
// The page's area in the layers' document, and the page's type moved there.
let rect = to_doc.transform_rect_bbox(art_box);
let shift = Affine::translate(Vec2::new(rect.x0 - page_rect.x0, rect.y0 - page_rect.y0));
for l in &visible.layers {
if let Some(c) = l.children() {
+36
View File
@@ -34,6 +34,15 @@ fn native(layers: &str) -> String {
)
}
/// The editing copy of a Large Canvas file (#2031): art and boxes written at 1/`scale`, declared
/// with `%AI24_LargeCanvasScale` as Illustrator writes it. The file's page is `size × scale`
/// points.
fn native_large_canvas(size: f64, scale: f64, layers: &str) -> String {
format!(
"%!PS-Adobe-3.0 \n%%BoundingBox: 0 0 {size} {size}\n%%HiResBoundingBox: 0 0 {size} {size}\n%AI3_Cropmarks: 0 0 {size} {size}\n%AI24_LargeCanvasScale: {scale}\n%AI5_NumLayers: 1\n{layers}%%Trailer\n"
)
}
/// A layer as the format writes it.
fn layer(name: &str, visible: bool, body: &str) -> String {
format!("%AI5_BeginLayer\n{} 1 1 1 0 0 1 0 79 128 255 0 50 0 Lb\n({name}) Ln\n{body}\nLB\n%AI5_EndLayer--\n", u8::from(visible))
@@ -239,6 +248,33 @@ fn an_ai_file_that_cant_be_read_through_its_editing_data_keeps_its_pdf_part() {
assert!(w.iter().any(|w| w.contains("different page")), "{w:?}");
}
/// #2031: a Large Canvas editing copy (`%AI24_LargeCanvasScale: 10`) writes its art and boxes at
/// 1/10 scale; the import scales them back, so its layers are read instead of being rejected as
/// another page's — with the art at the page's size. Art that doesn't match the page at that
/// scale is still another page's.
#[test]
fn a_large_canvas_eps_keeps_its_layers_at_the_pages_size() {
// 10 art units for the page's 100 points: a 0.4-unit square at (5, 5) is the page's 4-point
// square at (50, 50).
let art = native_large_canvas(10.0, 10.0, &layer("Art", true, "0 0 1 0 k\n5 5 m\n5.4 5 L\n5.4 5.4 L\n5 5.4 L\nf"));
let r = import(&eps(&page_square(50, 50), &art)).unwrap();
let d = &r.document;
assert_eq!(names(d), ["Art"], "{:?}", r.warnings);
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
let b = d.layers[0].children().unwrap()[0].visual_bounds().unwrap();
assert!((b.x0 - 50.0).abs() < 1e-6 && (b.y0 - 46.0).abs() < 1e-6, "at the page's place: {b:?}");
assert!((b.width() - 4.0).abs() < 1e-6 && (b.height() - 4.0).abs() < 1e-6, "at the page's size: {b:?}");
// A declared scale the art doesn't fit still makes it another page's.
let art = native_large_canvas(20.0, 10.0, &layer("Art", true, &square(10, 10)));
let r = import(&eps(&page_square(10, 10), &art)).unwrap();
assert_eq!(names(&r.document), ["Layer 1"], "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.contains("different page")), "{:?}", r.warnings);
// A `.ai` keeps its art space: its PDF part is read at the size it declares.
let art = native_large_canvas(100.0, 10.0, &layer("Art", true, &square(10, 10)));
let (d, w, read) = crate::layered_ai(&private(&art), pdf_part(10.0, 86.0), vec![], false, false);
assert!(read && names(&d) == ["Art"], "{w:?}");
}
/// A `.ai` whose layers can't be read comes in as its PDF part, a page per artboard in a row; its
/// pages still go where its editing data puts its artboards, with their art, and keep their names (#1068).
#[test]