mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user