Since #1557 the page background was painted inside the page
transparency group, as its initial backdrop, so that a Multiply
highlight under reading colours blends with the dark background. With
document colours that made every object blend with the white paper:
a green /BM /Screen fill over an empty part of the page came out
white, and Difference or Exclusion magenta. Only Normal, Multiply,
Darken and HardLight, for which blending with white gives back the
source colour, looked right. 0.8.0 was correct.
The page's objects composite with an initially transparent backdrop
and the page group is then put on the paper (ISO 32000-2, page group).
Vendored hayro patch (18) now paints the background inside the group
only under reading colours, where #1548 needs it, and outside it
otherwise, as before #1557.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
An image or a nested group is drawn into a layer of its own, which
covers the whole canvas (transparent where nothing was drawn), so it
can't be composited with Compose::Copy. What is under its shape is
now cleared first and the layer then composites over it:
- an unmasked image's shape is its placement rectangle: the
interpreter hands it to the device (`Device::knockout_shape`), which
clears it with a transparent Copy fill;
- a nested group's shape is the union of its elements' shapes: the
interpreter draws its content once in the device's erase mode
(`knockout_erase_wanted`, `begin_knockout_erase`,
`end_knockout_erase`), where every paint clears under its shape and
nothing is layered or drawn as an image, and then draws the group,
always in a layer of its own (drawn straight onto the knockout
group, its own elements knocked each other out).
A masked image's shape is its mask; it composites as before, which
matches MuPDF where the mask is opaque or transparent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
In a knockout group (/K true) each element composites with the group's
initial backdrop, not with the elements drawn before it (ISO 32000-2,
knockout groups). The renderer read /K (hayro-interpret patch 28) but
drew such a group like any other, so overlapping semi-transparent
shapes in it blended with each other.
hayro-interpret patch (39) tells the device a group is a knockout
group (`Device::set_knockout`, default no-op). hayro patch (19) then
draws that group's fills, strokes, glyph outlines and shadings with
Compose::Copy, which vello_cpu weights by coverage, so each replaces
what the group drew under its shape. Images and nested groups are
drawn into a group layer of their own and still composite normally:
copying a whole layer would erase everything outside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Review on #2070: fit_zoom fitted the document's largest page in every
two-page display, so a smaller spread stayed small in full screen. While
presenting, one spread at a time, it now fits the spread shown, the cover
page on its own included; in the window, and while scrolling, the zoom stays
one for the document. The spread arithmetic is one helper, shared with
step_page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
Full screen is the page on black and nothing else: one page (or one spread,
when two-page view was on, #1353) fitted to the screen, from the current
page; a click, Space, Enter, the arrows and Page Up/Down turn pages, a
shift-click and Backspace go back; Esc brings the display from before
back at the page reached. The quick tools are not drawn in full screen.
Transitions, auto-advance, loop and the cursor hiding stay planned.
Refs #379.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306)
The veraPDF fixture's JPX header names CIEJab (enumerated colour space 19),
which hayro-jpeg2000 refuses, so the image failed and the page drew blank.
When the decoder refuses the image and its colr boxes name such a space, it is
decoded again from a copy naming greyscale, sRGB or CMYK by component count,
as MuPDF and Poppler draw it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: copy the JP2 stream only when a colour enum needs replacing (#3306)
Review follow-up on #3363: a stream the decoder refused for another reason was
copied for nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* Renumber the JP2 colour-space patch (21): #3378 takes hayro-syntax (20)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(parse): accept spaces between stream and its line break
Microsoft Reporting Services writes `stream \r\n`, a space before the line break.
hayro-syntax and pdfcraft-cos took the stream data to start at the space, so the
content stream failed to inflate: the page rendered blank (white, or black with
inverted colours) and an edited save corrupted the stream. Spaces and tabs are now
skipped, but only when a line break follows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Number the stream end-of-line patch (20), as vendor/README does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M2: a tiling pattern drawn once shows its cell at full scale (#3349)
PowerPoint writes a picture fill as a tiling pattern with /XStep 2147480100.
Fitting that step into the 3000 px cell pixmap shrank the picture below a
pixel. When what is painted can only show the first cell, the cell's /BBox is
drawn at full scale with a transparent border and padded instead of tiled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a tiling cell whose scale isn't finite keeps the old path (#3349)
Review follow-up on #3356: a /BBox 1e-40 wide passes the zero-area check and
gives an infinite cell scale, which would send non-finite geometry to the cell
renderer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a stroke's miter counts in the one-cell check (#3349)
Review follow-up on #3356: shows_one_cell took the line width as a stroke's
reach; a miter join reaches half the width times the miter limit. The reach
formula patch (9) uses is now one function, stroke_reach, used by both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* M2: a skewed stroke's reach uses the transform's largest stretch (#3349)
Review on #3356: under a skew, a direction between the axes stretches
further than either axis, so the larger axis stretch could place a miter
tip short of the next cell when it reached it, and the one-cell path left
that cell unpainted. The reach now uses the spectral norm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A CAC puts its encryption, logon and signing certificates in the store
under the same name, so the digital ID list showed several identical
entries and only one of them makes a signature that validates. Grey out
the ones whose key usage or extended key usage doesn't allow document
signing, with the reason, the same way an ID without a usable key is
already treated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M14: keep long menus usable in short windows
* Menus: scroll each top-level menu once, from its caller, so View's list isn't nested in a second scroll area
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(preflight): identify standards by XML namespace
* fix(preflight): bound XMP attribute parsing work
* fix(preflight): cap XMP during stream decoding
* Preflight: read the PDF/A and PDF/UA identification by its usual prefix when the XMP can't be parsed as XML
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: the earlier assertion reopened the same path, which takes
the existing-tab shortcut. This one hands the running app a new file with
preparation held, checks Minimized(false) and Focus are sent before release,
and checks the new file is the selected tab once it is prepared.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Opening a PDF from Finder (or Open With, or a Dock drop) while PdfCraft was
already running added the tab but left the window where it was: behind
Finder, minimized, or full screen on another Space, so nothing seemed to
happen. The same applies to files handed over by another launch on Windows
and Linux, which arrive through the same OsEvent::Open path.
After opening the files, un-minimize the window and focus it, which on
macOS activates the app and orders the window front (switching to its
full-screen Space).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Layout notes are lowercase clauses joined with semicolons, and the bar
put the first right after the layout's own sentence ('11 fields. the
form's usage rights...'). The first note now starts with a capital.
A two-digit year reads back as 1950 to 2049, so 2050 written as 50 came
back as 1950. A date is now written in the data picture only when it
reads back from it unchanged.
Review on #3239: the Adobe Glyph List Specification's `uni` and `u` forms
take uppercase hexadecimal digits only (section 2), and its examples map
`uni20ac` to an empty string. glyph_name_to_text accepted lowercase too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
The template field behind a SOM path is now found by its number, as the
form numbers same-named siblings and repeated instances; the live form's
value refresh uses the same lookup. A data picture without a year, month
and day (date{YYYYMM}) keeps ISO dates in the data, since it can't hold
the date the field shows.
The layout revision replaces the placeholder pages and adds the fields,
so usage rights (/Perms /UR3) signed over the form as published no
longer match it, and a viewer may then open the form with every field
locked. write_form now takes them off with the helper strip already
needed, and the layout report says so.
A field's data picture (<bind><picture>, such as date{YYYYMMDD}) is how
its value is written in the XFA data, but PdfCraft always wrote dates
there as ISO (2026-10-18) and read them back as ISO, so a form that keeps
its dates as 20261018 got a different format from PdfCraft than from
other viewers, and data those viewers wrote was shown as stored.
The parser keeps <bind><picture> as Field::data_picture. A typed date is
still read in the field's own pattern and turned into ISO; it is then
written in the data picture when the field has one. Reading goes the
other way, on layout and when values are taken from the data of a form
laid out before. A value that doesn't match its data picture is read as
ISO, so data written by earlier versions still reads. The date-letters
rule for XFA picture clauses moves to one helper (date_letters) that the
layout's date_pattern now uses too.
Join multiple streams in a page's /Contents array with a space (b' ') rather than a newline (b'\n') in autotag::content_bytes, alt::page_content, and content::page_content so a % comment at the end of one stream continues through the start of the next stream per ISO 32000-2 section 7.8.2.
Update alt::artifact_marked_content to join page content streams with pdfcraft_content::Pieces::join, parse operators across the joined stream, and rewrite only matching BDC spans via Pieces::splice so operators split across stream boundaries and cross-stream comments are preserved.
Fixes#3217
M3 / PR #3101: avoid stale organizer anchors and synchronize the active comparison gap choice across both panes through menu and control paths. Three regression tests fail before these fixes and pass after them. Document the gap helpers and test contracts.
M3: preserve reading position while joining page edges, with persisted defaults and opt-in UI control. Translate both labels in all 22 catalogs, update attribution, and cover catalog completeness with a regression test. Rebase PR #3101 onto current main while retaining navigation and rendering fixes.
Review follow-up: a red/blue regression for the centre-coverage pass. The
shared diagonal sits at x + y = 50.25 in device space (`cm` moves the mesh
down a quarter point), so pixel (20, 30) has its corner in the red triangle
and its centre in the blue one. It must stay red with either triangle
first; replacing `or_insert_with` with `insert` turns it blue when the red
triangle comes first, and the test fails.
Rebased onto main; the vendored hayro-interpret patch is renumbered 36
(41 before; 35 is taken by PR #2977).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
The mesh sampler took each pixel's colour at its corner, and only when the
corner was strictly inside a triangle. A corner on an edge two triangles
share belonged to neither, so a Type 4-7 shading drew a white seam along
every shared edge that ran through the sample grid, and dropped pixels
along the mesh's own sides (a 30 x 30 square cut along its diagonal:
812 of 900 pixels; MuPDF 900).
Such a pixel now goes to the triangle that covers its centre, edges
included (Triangle::covers), and still takes the colour at its corner, so
every pixel that was drawn keeps its colour. Vendored hayro-interpret
patch 41.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* Viewer: accelerate coarse wheel scrolling (#1072)
Line/Page notches moved a fixed eased step, so a fast spin felt slow.
Accumulate one frame of coarse input per axis, scale by arrival rate
(1.0 idle, up to 6.0 spinning), and apply it directly like precise
touchpad input.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
* Viewer: add only the fast-spin surplus on top of egui smoothing
Single notches flow through egui untouched, so isolated input matches
the scroll speed preference exactly; page units are never boosted.
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
---------
Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>