235 Commits
Author SHA1 Message Date
Patrick LenzenandClaude Opus 5.5 504b549a78 Fix #3621: bound function-based shading reads per import
Review follow-up. A function-based shading is read once per import (kept
per shading pattern and inks), and all of them draw on one budget of
200,000 function evaluations; past it a shading comes in as its average
colour, then flat grey, each with a warning that names the cause. A
gradient read leaves enough of the budget to average another shading.

A colour that changes in step with the sampling grid is checked across
the middle of the domain before an axis is chosen, so it isn't read
along the wrong one. The grey fallback warning for other shadings names
only them.

Tests: the same shading on 2,000 paths is read once; 40 distinct
shadings stay within the budget; a hostile file (5,000 fills, 20
shadings with 500-step functions) opens in 0.8 s, 253 s before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:27:44 +07:00
Patrick LenzenandClaude Opus 5.5 095d2bd2fd Fix #3621: function-based PDF shadings keep their colours
PDF import turned every function-based shading (ShadingType 1) into a
flat mid-grey. Chromium writes a CSS repeating-linear-gradient this way,
so a red and white hatch came in as a grey bar.

A function-based shading whose colour changes along one axis of its
domain only now becomes a linear gradient across the domain: stops
sampled along that axis, each hard colour change (found by halving the
interval between two samples) two stops at one offset. One whose colour
changes in two directions comes in as its average colour, with a
warning. Either paints only over the shading's domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:27:44 +07:00
dotCooCoo bef464ea97 PDF export: the hyphen that ends a hyphenated line is written as text
With type written as text, PDF export drew the hyphen that ends a line
broken inside a word as a path. The file's text had no hyphen where the
canvas draws one, so copied or searched text read "considerations"
where the line shows "con-".

The hyphen is now written as text where the line draws it, as the
character the font gives its glyph to: "-", or U+2010 when the font
gives the glyph to U+2010 and not to "-". It is a text run of its own,
and right-to-left lines keep their /ReversedChars marking. The invisible
text of type without paint holds the hyphens too. Kashidas and list
markers are still drawn as paths.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 19:02:08 +07:00
Chris MainandClaude Opus 5.5 61504c84fb Print: turn only landscape paper, and only for macOS queues
Review of #3925: the spooled job turned every sheet wider than tall, so
custom paper that is wide in Portrait (a 4 × 2 in label) went to the queue
tall, and a Portrait + Transverse page was turned back. Only the sheets
whose paper the layout turned to landscape (not transverse) are written
upright now.

The desktop print service asked for upright pages on every platform, but
craft-print already turns each sheet to its own orientation on Windows:
only macOS asks for them now; Linux keeps main's behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 17:29:32 +07:00
Chris MainandClaude Opus 5.5 41430cc171 Print: landscape pages print upright on macOS print queues
A landscape print job was a page wider than tall, and `lp` on macOS puts
each page on the paper as it is: on upright Letter it printed unturned,
cut off on the right (checked with cupsfilter through an AirPrint
driver). Saving the PDF and printing it from Preview worked because
Preview turns the page.

`file.print` takes `forPrinter`: a page wider than tall is written
upright, the art turned a quarter anticlockwise as a printer driver turns
a landscape job. Page by page, so auto-rotated jobs that mix wide and tall
artboards print right too. The desktop print service asks for it
(`PrintService::turns_landscape`); saved PDFs, PostScript, the preview and
the browser's print dialog keep landscape pages as they are.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 17:29:32 +07:00
Dylan M. TaylorandClaude Opus 5.5 1febfd1d31 Security: render and PDF export bounds for symbol expansion, dotted guides, blending groups and pinned images
CodeRabbit findings #32, #38, #41, #44, #102:

- A frame (and a PDF export) draws symbol instances holding at most 16M objects together
  (`vectorcraft_doc::MAX_SYMBOL_NODES`): nested instances multiplied past the depth limits.
- A dotted guide whose control polygon would make more than 100,000 dashes is drawn solid.
- Groups blending with their backdrop, drawn twice each, are drawn isolated past 4 nested.
- An image is decoded only while it and the images paints hold fit twice the decode allowance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
Taha-Mahmoodi d3a01307ea PDF import: a string entered from many dictionaries in skipped text is read once
Review of #3924: in a flat area of ` <<(` (CMYK or RGBA 32, 60, 60, 40) in an uncompressed image, each `<<` after white space was read as a dictionary, and each read its string, which never ends, to 64 KiB past it: about 16 K bytes a pixel, so a few hundred bytes of it spent the budget and the file was refused.

Whether a byte in a literal string is escaped doesn't depend on where reading began (the run of `\` before it lies after any `(` before it), so the string from any `(` ends after the first `)` that takes the file-wide count of `(` less `)` below the count at that `(`. Those ends are found for the whole file in three linear passes the first time a string read in skipped text runs past 64 bytes: forward for the final count, back for the `(` whose string never ends, forward pairing the rest. Ends further than 64 bytes are kept (at most one for every 16 bytes of the file; a file built to nest past that is refused); nearer ones are read again. So entering a string costs at most 64 bytes, charged as one. A comment's end of line is kept for every comment read, not only the last.
2026-10-24 17:19:59 +07:00
Taha-Mahmoodi 2d5024bf82 PDF import: a run of "<" in skipped text no longer spends the pre-open scan's budget
Review of #3924: every `<<` in a string, comment or stream's data was read as a dictionary of its own, and in a run of `<` (an uncompressed image's flat area of grey 60) every position is one, each read to past the run, so about N²/2 bytes for N `<` and a 1 MiB run refused the file.

The reader starts a dictionary there only where its repair scan tries one (after white space or a dictionary's `>>`) or after an object's `N G obj`: so only those `<<` are read, in a run of `<` only the first. A reading goes on as far as 64 KiB past the last `<<` opened on the way, looks at each nested dictionary's `>>` for a stream and marks each `<<`, so it is not read again; a comment's end of line is kept, as many readings cross the same comment; the `<<<` second-`<` reading in the main pass goes (the reader never starts there). The dictionary text a stream check parses is charged to the budget.
2026-10-24 17:19:59 +07:00
Taha-Mahmoodi 1ec46732b8 PDF import: the pre-open stream scan doesn't let a "stream" inside a literal string hide the real stream after it
Review of #3919: the scan raised its walk-back floor at every `>>stream`, so one inside a literal string moved the floor past the `<<` of the cross-reference stream around it, and that stream was never looked at. The backward walk also counted a `>>` inside a string, and missed a comment between `>>` and `stream`.

The scan now reads the file forward once, as the reader does: literal strings (nested, escaped), hex strings and comments are skipped, `<<` and `>>` pair on a stack, and a dictionary followed by `stream` is a stream's, whose data is skipped. Because the reader can still start at a `<<` inside skipped text (a cross-reference offset or its repair scan), each such `<<` is read as a dictionary of its own, up to 64 KiB, within a budget of four times the file plus 1 MiB; a crafted file that spends it is refused. The scan stays linear.
2026-10-24 17:19:59 +07:00
Taha-Mahmoodi ea55f89c24 Fix #3586: the PDF pre-open scan reads a file dense with stream keywords in linear time
Each `stream` keyword walked back up to 64 KiB through every dictionary before it, so 1 MiB of `<</A <</B 1>> >>stream` took minutes. A walk back now stops where the previous keyword's walk began, and a stream without a Length shares the search for the next `endstream`.
2026-10-24 07:35:07 +07:00
bflatastic 3225315ce1 PDF export: warn about tiles only for a pattern swatch that exists 2026-10-24 05:59:04 +07:00
bflatastic a564fe5d8e PDF export: pattern fills are written as tiling patterns (#3849) 2026-10-24 05:59:04 +07:00
Dylan M. Taylor 5020dc0585 PDF export: past the retry cap, one last run leaves out every image instead of failing 2026-10-24 04:49:07 +07:00
bflatastic 2e71ab5476 PDF import and export: rustfmt 2026-10-24 04:49:07 +07:00
Dylan M. Taylor 4b8e69981e Audit: PDF import and export bounds 2026-10-24 04:49:07 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
bflatastic 422ecc0c77 Artboards: Artboard Options shows video safe areas and sets the pixel aspect ratio that View › Rulers › Show Video Rulers counts the artboard in
Squashed from #3831 onto main.
2026-10-24 03:09:07 +07:00
NetterSilverandClaude Haiku 5.5 58d94f6122 Hash PDF page contents without caching a decoded copy per page
Fixes #3570

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 01:43:59 +07:00
Taha-Mahmoodi b6b1ec7300 PDF import: keep a line's glyphs on course when the file's widths are a little off the font's advances
Cause: since #3742 place_each measures a line's spacing from the file's
own widths, so tracking is only the file's Tc/Tw/TJ. Where the widths
are systematically off the installed font's advances (up to the 1% that
still counts as the same font), those differences add up along a line
and nothing made up for them. With no kerning and every tracking value
0, place_each returned the untracked layout early, without the 0.02 em
placement check (CodeRabbit's review of #3742): a line of embedded
Source Sans 3 with /Widths 0.8% wide opened 3.7 pt off. With the early
return removed the check rejects the line and the line-wide fit takes
over: one whole-thousandth tracking value with the font's kerning,
0.30 pt off (as before #3715). Before #3742 main placed it exactly but
with a run per character (49 and 62 runs).

Change: the early return is gone; every line goes through the check.
Before it, on_course() keeps the file's spacing where the glyphs stay
within 0.01 em of the file's; otherwise each stretch of the line takes
one share after every character (the longest stretch one share keeps
within 0.01 em), so ordinary text stays one run, and a line whose
differences change course (30 m then 30 i) takes a run per stretch (2).

Verification: new test
a_long_line_of_widths_a_little_off_the_fonts_advances_lands_in_one_run
(embedded Source Sans 3, widths x1.008 and x0.992, a kerned and an
unkerned line): one run each, glyphs under 0.24 pt (0.02 em) from where
the file draws them. Without on_course it fails after #3742, with and
without the early return ("glyphs 0.300 pt"); before #3742 it fails on
the run count. It replaces an Inter test drafted for the review: Inter
isn't embedded there, so the file's outlines are the interpreter's
stand-in stretched to the widths, not Inter, and the contours differ by
0.6 pt even with exact widths. tests_text_placement and
text_becomes_point_type pass; cargo xtask ci: all 10 steps passed.

Thanks to the CodeRabbit review on #3742 for the early-return finding.
2026-10-23 10:42:45 +07:00
Taha-Mahmoodi b87cda7d4b PDF import: keep one tracking value on letters whose file widths round off the installed font's advances
Cause: place_each (crates/pdf/src/import_text.rs, #3715) works out each
character's tracking as the file's distance between glyph origins minus
the installed font's advance. A PDF gives widths to the thousandth of an
em (the standard-14 AFM metrics or /Widths), so with a font of 2048 units
an em (Helvetica on macOS) every character is a hair (~0.0002 em) off.
When the line isn't evenly spaced as a whole (a TJ word gap, a space read
from a gap), `even` keeps every character's own value, rounding noise
included, and restyled() splits the line into a run per character.
text_becomes_point_type failed on macOS; Linux CI passed because Helvetica
isn't installed there, its stand-in's widths differ by more than 1% and
place_each falls back to the line-wide fit.

Change: measure the spacing from the file's own widths: each character's
pen is moved by how much wider the file says the characters before it are
than the font's advances, where the file gives the character's width and
the layout sets it as a glyph of its own. The tracking is then the file's
(Tc, Tw, TJ), whatever font is installed; the layout check that the glyphs
land within 0.02 em of the file's places is unchanged.

Verification: text_becomes_point_type and all of tests_text_placement pass
on macOS. New test widths_rounded_off_the_installed_fonts_advances_keep_one_tracking
uses built-in Inter (non-embedded, widths rounded to thousandths), so it
doesn't depend on system fonts; without the fix it fails on any platform
with runs N/e/x/t/ /li/n/e/ /h/e/re. cargo xtask ci: all 10 steps passed.
2026-10-23 09:22:20 +07:00
Jürg Lehni c28ad99678 Type: OpenType SVG colour fonts draw and export as vector art
Each glyph's SVG document is read by the SVG importer, which the engine registers with the text crate.
2026-10-23 09:07:54 +07:00
Jürg Lehni 9de4b40241 Type: COLR colour fonts draw and export as vector art
Glyphs whose paints documents have no form for (sweep gradients, compositing modes) are drawn as images of the glyph.
2026-10-23 05:14:47 +07:00
Angelo Bernardini 16d39d7710 G11 #722: keep imported PDF text boxes in place
Squashed from #3356 onto main.
2026-10-23 04:47:59 +07:00
Taha-Mahmoodi 2bad646821 Gradients: a Perceptual gradient mixes its colours in OKLab, and SVG and PDF write it as an image
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.

- Gradient gains `method` (Classic | Perceptual; serde default Classic,
  written only when Perceptual, so older files open as Classic and Classic
  files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
  sample_keep_model for the stroke meshes, Expand and blends); opacity stays
  linear. Renderers that mix linearly get stops on the perceptual curve,
  placed by halving each half segment where a straight line strays more than
  0.4 of a colour level (a nearly straight segment needs none). Classic
  gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
  Perceptual fill or stroke as an image at the document's raster effects
  resolution, clipped to what it paints. A gradient along or across a stroke
  stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
  noise (the canvas's dithered grids are keyed by method too); SVG and PDF
  write it as an image with the noise, and a dithered Perceptual stroke as an
  image without it, as the canvas draws it (GradientPaint::pixels, SVG's
  raster_paint and PDF's area are told whether they paint a fill).
2026-10-23 01:53:29 +07:00
Mirko Kerezovic d548b4ff74 Fix PDF title display provenance 2026-10-23 01:01:25 +07:00
Mirko Kerezovic 5e9df83dd4 Fix #3646: display PDF document title 2026-10-23 01:01:24 +07:00
bflatastic 26b9cf84ad Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
2026-10-23 00:44:31 +07:00
Taha-Mahmoodi 6b10b159b9 Gradients: dithered type in PDF keeps its resolution when scaled and stays searchable
Review follow-up (CodeRabbit): a gradient drawn as pixels was sized from its
area in the space it is drawn in, so type scaled 4 times got a quarter of the
raster effects resolution; the image is now sized from the area on the page,
within the same pixel caps. And type whose fill is drawn as art (a dithered
or freeform gradient's image, a pattern) went out as outlines only; its
characters are now also written as invisible text (rendering mode 3), as
unpainted type already is, so they can be selected and searched.
2026-10-23 00:08:53 +07:00
Patrick LenzenandClaude Opus 5.5 6903bb7ef8 Fix #3623: a word gap between two sizes reads as a space in PDF import
PDF import measured each gap between two glyphs against the larger of
the line's first glyph size and the next glyph's. A word set beside a
much larger one, apart by positioning and no space glyph (a browser's
margin between two sizes), stayed under a fifth of that em: "≤ 4" at
20 pt and "hours" at 8 pt, 3.4 pt apart, came in as "≤ 4hours".

A gap is now measured against the smaller of the two glyphs beside it.
Lines in one size read their gaps as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:14:35 +07:00
Taha-Mahmoodi cc1a660a6d Gradients: Dither in the Gradient panel draws a gradient fill with fine noise that hides banding
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.

- Gradient gains `dither` (serde default false, written only when on, so
  older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
  gradient sampled a cell per device pixel or finer (quarter-octave steps,
  cached per thread), each channel given seeded triangular noise of up to a
  level before rounding. Same pixels every run; strokes and undithered fills
  draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
  effects resolution, clipped to the shape (the freeform-gradient path);
  characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
  The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
  mcp.md, plugins.md.
2026-10-22 22:38:42 +07:00
Taha-Mahmoodi a1d8952bc8 Gradients: a linear or radial gradient can be drawn as pixels, and SVG and PDF write such a gradient as an image
Shared groundwork for gradient options SVG and PDF can't express (Illustrator
30's dither and perceptual gradients): GradientPaint::pixels samples a placed
linear or radial gradient on a grid (Ramp: its stops, midpoints as stops, mixed
as the renderer mixes them), and draws_as_pixels says which gradients are drawn
that way (freeform ones, so far). The SVG and PDF writers' freeform-gradient
image path takes any such gradient, a linear or radial one at the document's
raster effects resolution. No change in output yet.
2026-10-22 22:38:41 +07:00
Taha-Mahmoodi 4bb6f32911 Gradients: built with Gradient::new, so a new option touches one place 2026-10-22 22:38:41 +07:00
Dylan M. Taylor f64db69b53 Use chrono instead of hand-rolled UTC conversion (#3387)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month tables with chrono as a workspace dependency:
- apps/vectorcraft/src/logging.rs: timestamp (log line time)
- crates/doc/src/metadata.rs: civil (Unix seconds -> [y,m,d,h,mi,s]) and iso8601
- crates/pdf/src/file_info.rs: unix (date -> seconds) and in_month, used by the
  PDF/XMP date parsers (pdf_date, xmp_date)
Output formats and parsing semantics are unchanged and pinned by the existing
tests (PDF D:... and XMP dates, offsets, leap-year rejection, year clamp 0-9999,
pre-epoch clamp). chrono uses only std, so wasm stays safe. AGENTS.md/CLAUDE.md
now say time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock (or a pre-epoch one) as the epoch instead. Years past
9999 are written with {:04} like before, because %Y would add a '+'. Tests
cover the out-of-range and 5-digit-year cases.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01. Restore the old conversion domain with
a cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).

- apps/vectorcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/doc/src/metadata.rs: `civil` cycle-based, keeping its clamp EXACTLY
  as the old code had it: the input is clamped to 0000-01-01..9999-12-31 first
  and the year is clamped to 0..=9999 afterwards. (With the input clamp the old
  `unwrap_or_default()` was already unreachable; this drops it and makes the
  conversion explicit, with identical output.)

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  metadata civil(i64::MIN)[0]=0, civil(i64::MAX)[0]=9999;
          iso8601(i64::MIN)=0000-01-01T00:00:00Z,
          iso8601(i64::MAX)=9999-12-31T23:59:59Z (year clamp, as before)

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:32 -04:00
dotCooCoo 5f6c7b8da9 PDF export: a 16-bit PNG is written at 8 bits for PDF 1.3 and 1.4 instead of failing the export
When the Compression settings keep a PNG as it is, the PNG goes to
the PDF writer unchanged, and the writer refuses images of 16 bits a
sample below PDF 1.5. Saving a document with a placed 16-bit PNG as
PDF 1.4, or as a .ai file at 1.4, failed with "PDF writer error:
SixteenBitImage(...)" and wrote nothing. PDF 1.3 and PDF/X-3 failed
the same way for an opaque 16-bit PNG, and PDF/X-1a failed for a
16-bit gray PNG.

In PDF 1.3 and 1.4 files, such a PNG is now decoded and encoded again
at 8 bits a sample, with the same color type and ICC profile, before
the writer reads it. From PDF 1.5 on, it keeps its 16 bits. A 16-bit
PNG whose pixel data can't be decoded is left out with the note "an
image couldn't be decoded and is missing from the picture".

Docs: docs/mcp.md (PDF options).
2026-10-22 05:57:11 +07:00
KnottyDyesandClaude Opus 5.5 73cbba8b8d PDF import: even gaps alone still read as word spaces
Glyphs placed one by one with even gaps could be letters set apart or
words of one letter each, and a file's positions can't tell them apart.
The spacing a line's letters share is now taken off its gaps only when
the line also shows where its words break: with spaces it draws (as
with Tc), or with gaps wider than the rest (tracked words placed glyph
by glyph). Otherwise every gap past a fifth of an em is a space, as
before, so A, B, C and D placed with ordinary word spaces between them
still come in as "A B C D".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:10:57 +07:00
KnottyDyes b024756e9f PDF import: wide even gaps and right-to-left lines keep their spaces
A line whose shared gap is over half an em is no longer read as letter
spaced: capping that gap at half an em left glyphs set 0.55 em apart
with no spaces. The regression now covers 0.55, 0.75 and 1 em.

Whether a line is right to left is now known before its gaps are read,
from its logical order: the paragraph direction is only set afterwards,
so right-to-left lines were read for letter spacing too.

The revision-history row moves out of the Headline table, where it had
been put by mistake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/target-app-parity.md
2026-10-22 00:10:57 +07:00
bflatastic 14f804cdee Type: ruby (ふりがな) for live text
Squashed from #3448 onto main.
2026-10-22 00:10:56 +07:00
bflatastic 58066428c6 Artboards: the Artboards panel locks an artboard and the art on it
Squashed from #2592 onto main.
2026-10-21 23:38:02 +07:00
dotCooCoo 6807e757af PDF export: PDF and .ai files carry File Info's copyright status, notice and URL
The PDF writer's metadata has no copyright fields, so the XMP packet of
PDF exports and PDF-compatible .ai and .ait saves left out File Info's
copyright status, notice and URL. A PDF exported without its editing
data opened with the status Unknown and an empty notice and URL,
although the reader takes them from dc:rights, xmpRights:WebStatement
and xmpRights:Marked.

The export now adds those three properties to the XMP packet after
the writer has written it, as it does for the modification date.
Marked is True for Copyrighted and False for Public Domain, and is left
out for Unknown. The notice and URL are escaped for XML, and characters
XML 1.0 does not allow are left out. A document without a copyright
gets none of the properties.

Docs: docs/mcp.md (File Info), docs/target-app-parity.md.
2026-10-21 23:32:26 +07:00
Midasdf 32d012f6cc Print: Japanese paper sizes (JIS B4/B5/B6, はがき・封筒・四六判)
The Print dialog's Media list only had Letter/Legal/Tabloid and the ISO A
and B papers, so Japanese papers had to be entered as a custom size, and
"B4"/"B5" were ISO 216 (250 × 353 and 176 × 250 mm) although Japanese
printing uses the JIS sizes (257 × 364 and 182 × 257 mm, JIS X 6310).

Media gains JIS B4, B5 and B6 next to the ISO ones, and はがき
(japanese postcard, 100 × 148 mm), 往復はがき (japanese double postcard,
148 × 200 mm), 封筒 長形3号/長形4号 (japanese envelope #3/#4, 120 × 235
and 90 × 225 mm), 角形2号/角形3号 (kaku #2/#3, 240 × 332 and 216 × 277 mm),
四六判 (yonroku-ban, 127 × 188 mm) and A6, all in millimetres at 72/25.4.
b4 and b5 keep their ISO 216 dimensions so print settings and presets
saved before the Japanese papers existed keep their paper; the dialog
labels say "B4 (ISO)"/"B5 (ISO)" beside "B4 (JIS)"/"B5 (JIS)".

A paper is named by its JSON id or by its dialog label, any case and
spacing ("b5 (jis)", "B5 (JIS)" and "b5jis" all mean the JIS B5), so
agents and the control channel need not know the id spelling; an unknown
name is still refused. Media is written out instead of through the
choice! macro to carry that lookup.

The print.setup/file.print parameter docs and docs/mcp.md list the new
papers; the Print dialog picks them up from Choice::ALL with no UI code
change, and the Japanese labels are translated in ja.tsv (はがき,
往復はがき, 長形3号, 長形4号, 角形2号, 角形3号, 四六判).

Tests: japanese_paper_sizes checks every added size in points, that the
ISO B4/B5 values did not change, name resolution and id/label round
trips, and that each paper prints a PDF page with its own MediaBox size.

Validation: cargo fmt --all -- --check clean; cargo clippy
-p vectorcraft-pdf --all-targets -- -D warnings clean; cargo test
-p vectorcraft-pdf -> 330 passed, 0 failed (328 lib + 2 integration).
2026-10-21 23:32:26 +07:00
KnottyDyesandClaude Opus 5.5 52b63e2198 PDF import: letter-spaced text keeps its word spaces
PDF import read every gap wider than a fifth of an em between two
glyphs as a space. Letter-spaced type (Tc, or glyphs placed one by one
with tracking) has such a gap between every pair of letters, so
"OPEN DAILY" came in as "O P E N D A I L Y".

A line now records every gap between its glyphs and decides which are
spaces when it finishes: the spacing its letters share (the gap a third
of the way up from the narrowest, when the line has three or more) is
taken off each gap first. That shared spacing is at most half an em, so
glyphs set wider apart, such as figures in columns, keep their spaces.
Right-to-left lines and glyphs along a curve read their gaps as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:16:29 +07:00
bflatastic dda52d0b20 Artboards: each artboard can have a background color on the canvas
Squashed from #2403 onto main.
2026-10-21 17:11:08 +07:00
dotCooCoo f009f4f3e5 Part of #2384: imported PDF text keeps its place when the file draws the space beside a wide gap
PDF import gives a space read from a gap between two glyphs tracking of
its own when the line's tracking cannot place the glyphs after it. A
space glyph that the file draws was not measured this way: the gap
before or after it was spread over the whole line as one tracking
value, so the words after it moved. VectorCraft's own PDF export draws
a tracked space and leaves its tracking after it.

A space the file draws between two glyphs on a line is now measured
from the end of the glyph before it to the start of the glyph after it,
and takes tracking of its own on the same terms as a space read from a
gap. Two or more spaces in a row, and a space at the start or end of a
line, keep the line's tracking. The tracking calculation builds a map
of the line's laid-out advances by byte offset once.

A wrapped line whose spaces each have tracking of their own, within a
fiftieth of an em of the first space's tracking, can still come back as
part of justified area type. Wrapped lines whose spaces' trackings
differ by more than that stay point type with their words in place.
2026-10-21 17:11:08 +07:00
00xJ0 096544db40 Test: Press Quality keeps a CMYK image's inks in an RGB document (#3012 review)
The colour space alone could pass a conversion that changed the inks: the exported image is
opened again and its inks compared with the ones placed.
2026-10-21 16:55:17 +07:00
00xJ0 91107c89d2 PDF: a Document CMYK destination; Press Quality converts to it in RGB documents too (#3012 review)
A blank destination is the document's own profile, so in an RGB document Press Quality (Preserve
Numbers, blank) converted CMYK colours and images to RGB. Output › Destination can now be
DOCUMENT_CMYK, the CMYK profile of the colour settings in effect whatever the document's colour
mode (Illustrator's "Document CMYK"); Press Quality uses it, and the Save PDF dialog lists it after
Document profile. Test: in an RGB document a CMYK image keeps its inks and an RGB image comes out
CMYK.
2026-10-21 16:55:17 +07:00
00xJ0 5d0b966548 PDF presets: Press Quality converts to the document's profile, keeping its numbers (#2950)
Press Quality left Output at No conversion, so in a CMYK document RGB images went to the press as
DeviceRGB. It now converts to the destination with the numbers kept, the destination blank (the
document's profile): RGB comes out CMYK in a CMYK document, CMYK values stay exactly, RGB
documents are unchanged, and no profiles are included, as a printer's published [Press Quality]
settings list.
2026-10-21 16:55:17 +07:00
Brian TorresandClaude Opus 5.5 ac7f0eebf8 PDF import and export: keep invisible text (rendering mode 3) as type (#2675)
Text drawn in rendering mode 3 (3 Tr), which is how a scanned page carries
its recognized OCR text, was left out on open (since #2988, with a warning),
so a scanned PDF opened and saved again lost its searchable text.

Import: an invisible glyph now comes in as type with neither fill nor
stroke. It skips the glyph-outline checks that guard how visible type looks,
and it joins only other invisible text on a line (Look::takes no longer
lets it into a stroke-only run). INVISIBLE_TEXT_NOTE now only reports text
that can't be type (text opened as outlines, Type 3 fonts), and
tests_reader_compat::invisible_text_is_reported_when_left_out expects the
invisible line as unpainted type when text opens as type.

Export and print: characters with no fill and no stroke are written as real
text inside a form marked Mark::Invisible; forms::finish sets that form's
text rendering mode to 3, so it draws nothing but stays selectable and
searchable (PDF/X-1a and PDF/A-2b too). The writer has no invisible mode of
its own, so this reuses the marked-form mechanism used for layers and
overprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 04:43:11 +07:00
00xJ0 8d123b19ca Test: check which text the PDF import keeps beside invisible text (#2675 review)
The visible line's text comes in as type and the invisible line's doesn't; as type or outlines,
all the art kept lies on the visible line's baseline, none on the invisible one's.
2026-10-19 22:19:50 +07:00
00xJ0 4aa4b0e70b PDF import: say when invisible text is left out (part of #2675)
Text in rendering mode 3 (invisible: a scanned page's searchable OCR layer) was dropped with no
warning, so a scanned PDF opened and saved again lost its searchable text without a hint. The
import now reports INVISIBLE_TEXT_NOTE when it leaves such text out, for type and outlines alike.

Keeping the text needs a kind of type that is kept but not drawn, and the PDF writer (krilla, in
craft-libs) to write it with `3 Tr`; its text API has fill, stroke and fill-and-stroke only.
2026-10-19 22:19:50 +07:00
Hamza 1c80b23745 Exports: SOURCE_DATE_EPOCH stands in for the clock, so output repeats
PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files
record the wall-clock time they were made in, so the same commands run twice
gave different bytes although the pixels matched (#2880):

  PDF   /CreationDate and /ModDate   D:20261018155816 vs D:20261018155817
  PNG   the `Creation Time` tEXt entry
  .vectorcraft  one byte of the stored date
  SVG   carries no time, and already repeated

`metadata::output_time` is the time a file records: `SOURCE_DATE_EPOCH` when it
holds a non-negative whole number of seconds, else the clock, as the
reproducible-builds specification defines it. A date a command is given
(`file.new {created}`, a save's `modified`) still wins over the variable, and a
blank, negative or non-numeric value is ignored so that an export is never
refused over the environment.

`metadata::now_unix` keeps the real clock and now has exactly two callers, both
of which must not be pinnable from the environment: the recovery heartbeat, and
the seed PDF encryption draws on.
2026-10-19 00:37:55 +07:00