mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
PDF/.ai import: a .ai opens on its artboard with its bleed round it (#1266)
A PDF-compatible .ai with a bleed writes its PDF part's page as the
artboard grown by the bleed: the crop box takes the bleed in, the trim
box is the artboard and the bleed box the bleed. The importer made the
crop box the artboard, so the bleed opened as more white page with no
bleed guide; and since the editing data's artboard no longer matched
that page, its layers weren't read either ("its editing data is for a
different page").
Opening a PDF-compatible .ai with the crop box (the default) now takes
its trim boxes as the artboards. Any import on the trim boxes (Crop To
› Trim, for a PDF too) sets the document's bleed to how far the bleed
boxes reach past them on each side (ISO 32000-1, 14.11.2), kept to
0–72 pt, and the document read from the editing data keeps it. Art in
the bleed shows outside the artboard, within the bleed guide. Plain
PDFs still open on their crop box by default, and Place is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3428a224bb
commit
a99b1200ff
11 files changed
+124
-11
No files matched your search
@@ -200,6 +200,13 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
|
||||
let (mut order, mut known) = (vec![], HashSet::new());
|
||||
for (i, &number) in picked.iter().enumerate() {
|
||||
let Some(page) = pages.get(number) else { continue };
|
||||
// Artboards on the trim boxes have the bleed boxes round them as the document's bleed
|
||||
// (the widest of the pages' on each side), so the art there shows and prints as bleed.
|
||||
if opts.crop == CropTo::Trim {
|
||||
for (side, b) in doc.setup.bleed.iter_mut().zip(crate::pages::bleed(page)) {
|
||||
*side = side.max(b);
|
||||
}
|
||||
}
|
||||
// The chosen box sits at (x, 0); the page draws round it.
|
||||
let (init, frame) = crate::pages::frame(page, opts.crop);
|
||||
let mut ab = Rect::new(x, 0.0, x + frame.width(), frame.height());
|
||||
|
||||
@@ -70,7 +70,7 @@ pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_w
|
||||
pub use encrypt::Encryption;
|
||||
pub use export::{export, export_with_report, page_areas};
|
||||
pub use import::{OFF_ARTBOARD_NOTE, import, import_with_report};
|
||||
pub use pages::{PageInfo, PdfInfo, illustrator_data, info, is_postscript};
|
||||
pub use pages::{PageInfo, PdfInfo, has_illustrator_data, illustrator_data, info, is_postscript};
|
||||
pub use post::{THUMBNAIL_SIZE, Thumbnail};
|
||||
pub use presets::*;
|
||||
pub use print::*;
|
||||
@@ -148,6 +148,7 @@ settings::choice! {
|
||||
Art = "art", "Art";
|
||||
/// The visible page area (what viewers show).
|
||||
Crop = "crop", "Crop";
|
||||
/// The finished page: an import takes the bleed box round it as the document's bleed.
|
||||
Trim = "trim", "Trim";
|
||||
Bleed = "bleed", "Bleed";
|
||||
/// The whole sheet.
|
||||
|
||||
@@ -5,6 +5,7 @@ use hayro_syntax::object::{Dict, Stream};
|
||||
use hayro_syntax::page::Page;
|
||||
use hayro_syntax::{DecryptionError, LoadPdfError, Pdf};
|
||||
use kurbo::{Affine, Rect};
|
||||
use vectorcraft_doc::setup::MAX_BLEED;
|
||||
|
||||
use crate::{Choice, CropTo, ImportOptions, PdfError};
|
||||
|
||||
@@ -128,6 +129,15 @@ pub(crate) fn frame(page: &Page<'_>, which: CropTo) -> (Affine, Rect) {
|
||||
(init, if b.is_finite() && b.area() > 0.0 { b } else { Rect::new(0.0, 0.0, w as f64, h as f64) })
|
||||
}
|
||||
|
||||
/// How far `page`'s bleed box reaches past its trim box as the page shows (rotated): the bleed
|
||||
/// `[top, bottom, left, right]` in points, kept to 0–[`MAX_BLEED`].
|
||||
pub(crate) fn bleed(page: &Page<'_>) -> [f64; 4] {
|
||||
let init = Affine::new(page.initial_transform(true).as_coeffs());
|
||||
let (trim, bleed) = (init.transform_rect_bbox(page_box(page, CropTo::Trim)), init.transform_rect_bbox(page_box(page, CropTo::Bleed)));
|
||||
[trim.y0 - bleed.y0, bleed.y1 - trim.y1, trim.x0 - bleed.x0, bleed.x1 - trim.x1]
|
||||
.map(|b| if b.is_finite() { b.clamp(0.0, MAX_BLEED) } else { 0.0 })
|
||||
}
|
||||
|
||||
/// The `PieceInfo` key such files keep their private data under.
|
||||
const PRIVATE_DATA_OWNER: &[u8] = b"Illustrator"; // brand-ok: the key the files use
|
||||
|
||||
@@ -149,6 +159,12 @@ pub fn illustrator_data(bytes: &[u8], password: Option<&str>) -> Option<Vec<u8>>
|
||||
(!data.is_empty()).then_some(data)
|
||||
}
|
||||
|
||||
/// Does the PDF's first page carry an editor's private data (a PDF-compatible `.ai`)? Without
|
||||
/// reading the data.
|
||||
pub fn has_illustrator_data(bytes: &[u8], password: Option<&str>) -> bool {
|
||||
open(bytes, password).is_ok_and(|pdf| pdf.pages().first().is_some_and(has_private_data))
|
||||
}
|
||||
|
||||
/// `stream` decoded, up to `most` bytes: the decoder is run here rather than by the PDF reader's
|
||||
/// `decoded()`, which has no limit, so a stream of zeros can't grow to the size of the memory. Only a
|
||||
/// stream that is plain or only deflated is read (the private data is no other); else, or when it
|
||||
|
||||
@@ -52,6 +52,45 @@ fn each_crop_box_sizes_the_artboard_and_places_the_art_in_it() {
|
||||
assert_eq!(ImportOptions::default().crop, CropTo::Crop);
|
||||
}
|
||||
|
||||
/// 10 mm in points.
|
||||
const BLEED: f64 = 10.0 * 72.0 / 25.4;
|
||||
|
||||
/// A 300 × 200 sheet with a 200 × 100 trim box, the bleed box 10 mm round it, and a red rectangle
|
||||
/// reaching 20 pt past the trim box's left and bottom edges into the bleed.
|
||||
fn bled_page() -> PdfPage {
|
||||
PdfPage {
|
||||
bleed: Some([50.0 - BLEED, 50.0 - BLEED, 250.0 + BLEED, 150.0 + BLEED]),
|
||||
trim: Some([50.0, 50.0, 250.0, 150.0]),
|
||||
..PdfPage::new(300.0, 200.0, "1 0 0 rg 30 30 100 60 re f")
|
||||
}
|
||||
}
|
||||
|
||||
/// The same within the precision of a box read from a file (32-bit floats).
|
||||
fn near(a: [f64; 4], b: [f64; 4]) -> bool {
|
||||
a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_box_artboards_take_the_bleed_box_round_them_as_the_document_bleed() {
|
||||
let bytes = pdf(&[bled_page()], None);
|
||||
let trim = ImportOptions { crop: CropTo::Trim, ..Default::default() };
|
||||
let d = open(&bytes, trim.clone());
|
||||
let ab = d.artboards[0].rect;
|
||||
assert!(close(ab, Rect::new(0.0, 0.0, 200.0, 100.0)), "{ab:?}");
|
||||
assert!(near(d.setup.bleed, [BLEED; 4]), "{:?}", d.setup.bleed);
|
||||
// The rectangle comes in whole, reaching into the bleed (y down: the bottom bleed is below).
|
||||
assert!(close(art(&d), Rect::new(-20.0, 60.0, 80.0, 120.0)), "{:?}", art(&d));
|
||||
assert!(d.setup.bleed_rect(ab).contains_rect(art(&d)));
|
||||
// Each side as the page shows it: PDF space is y up, and a rotated page turns its bleed.
|
||||
let uneven = PdfPage { bleed: Some([49.0, 48.0, 253.0, 154.0]), ..bled_page() };
|
||||
let turned = PdfPage { rotate: 90, ..uneven.clone() };
|
||||
assert!(near(open(&pdf(&[uneven], None), trim.clone()).setup.bleed, [4.0, 2.0, 1.0, 3.0]));
|
||||
assert!(near(open(&pdf(&[turned], None), trim).setup.bleed, [1.0, 3.0, 2.0, 4.0]));
|
||||
// On the crop box (the sheet here) there is no bleed round the artboard.
|
||||
let d = open(&bytes, ImportOptions::default());
|
||||
assert_eq!((d.artboards[0].rect.width(), d.setup.bleed), (300.0, [0.0; 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_boxes_fall_back_to_the_crop_box_and_boxes_are_clipped_to_the_media_box() {
|
||||
let page = PdfPage { crop: Some([10.0, 10.0, 290.0, 390.0]), art: Some([-50.0, 0.0, 100.0, 100.0]), ..PdfPage::new(300.0, 400.0, "") };
|
||||
|
||||
Reference in new issue
Block a user