hayro-syntax decoded every JPX image with resolve_palette_indices off. That
is right when the image dictionary gives a /ColorSpace (an /Indexed space
needs the raw indices), but with no /ColorSpace the colours come from the
JP2 header (ISO 32000-2 §7.4.9) and nothing applied its palette: the
indices were drawn as a gray image. The palette is now resolved exactly
when the dictionary has no /ColorSpace.
Two unit tests decode a 4x2 palette JP2 written for the test: without a
colour space it is red/blue RGB, with /Indexed the raw indices stay.
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>
* fix(app): a launch without SESSIONNAME hands its files to the console session's window (#3338)
Task Scheduler, services and some remote shells start PdfCraft without
SESSIONNAME, so the launch looked for instance.sock while the window opened
from Explorer listens on instance-Console.sock, and a second window opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
* fix(windows): name the single-instance socket by the session id, not SESSIONNAME (#3338)
Review on #3340 (earlvanze): falling back to the console's socket when
SESSIONNAME is unset could hand a file from a Remote Desktop session to a
console window of the same user in another session. The socket is now named
by the session id in the process token (winsafe, no unsafe): a launch from
Task Scheduler runs in the user's session and finds its window, and
different sessions never share a socket. SESSIONNAME names it only if the id
can't be read.
Tested with a real scheduled task: on main the task's launch (no
SESSIONNAME, session 1) opened a second window; with this change the file
went to the running window in session 1.
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>
* 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.
While the window was being resized, Windows stretched the window's swap
chain, still at the old size, to the new one: the text shimmered and the
left panel's width bounced for as long as the drag lasted. A DirectComposition
visual's last frame is shown at its own size instead. WGPU_DX12_PRESENTATION_SYSTEM
still overrides it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
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>