* fix(js): decode ', numeric, and HTML named character references in FormCalc Decode
* test(js): verify all HTML named entities and 8-byte name boundary (#6464)
* docs(js): remove duplicate decode_entities summary and document LATIN1_ENTITIES order (#6464)
* fix(engine): map URL text line rects with view_rect_to_user and filter non-finite coordinates
* test(engine): assert concrete rotated user rects and overflow guards (#6469)
A cubic whose second control point is its end point (as the `y` operator builds,
ISO 32000-2 8.5.2.1 Table 58) has a zero derivative at its end. kurbo 0.13.1's
offset curve looks for cusps with a root search on the curvature, which grows
without bound towards that end; at some widths the search stops on the end point
and normalising the zero tangent there gives NaN. vello_common's fill drops any
outline with NaN, so the whole stroke vanished (a two-curve `y` path drew nothing
at 16, 32 and 63 pt; a PDFBox tracker file lost its 32 pt "XX" outline).
The NaN fallback that #4913 added to `flatten::stroke` now also moves such a
control point the stroke tolerance towards the previous control point before
stroking again (the curve moves by at most 4/9 of the tolerance). Outlines that
come back finite take the old path unchanged. The vendor ledger row also gets
back #4913's entry, which a merge had dropped.
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: `info` lists the repairs made while opening, as Document Properties does (misc.cli-inspect)
`pdfcraft-cli info` now prints a `repairs` array: the notes about damage
repaired while opening that the app shows in Document Properties and
doc_info reports. It opens the document as the app does, once (the session
the dynamic XFA summary already used), so notes the app adds after reading
the objects, such as a page tree rebuilt from the pages found by scanning,
are listed too. A file the app cannot open for editing falls back to what
reading its objects repaired; a sound file prints an empty array.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M2: `info` runs none of the document's scripts while inspecting it (misc.cli-inspect)
The session `info` opens to read the app's repair notes and the dynamic XFA
layout now has JavaScript turned off, so an XFA form's initialize and
calculate scripts, which the app runs on opening, do not run during
inspection of an untrusted file. Before this branch the session was opened
for dynamic XFA forms only, and their scripts ran there too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M2: `info` reads the standards and repairs from the session it opens, and a laid-out form keeps its repair notes (misc.cli-inspect)
`info` now takes the declared standards (`Document::standards`, as the
Standards panel reads them) and the repair notes from the session it
opened, and reads the file once more only when the app cannot open it.
When opening lays out a dynamic XFA form, or writes any other revision,
the notes about damage repaired while reading the file now stay with the
re-read document, so Document Properties and `info` both list them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
---------
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M2: a form XObject that draws itself is drawn once (hayro-interpret patch 89)
A form whose content names itself, directly or through another form, a
pattern or a glyph, drew itself again at every level until the page's
nested-paint budget (50,000) ran out. PDFBOX-5782's poppler-748 page 2,
whose form names a /Pages dictionary as /X0 and so finds itself in the
page's resources, took 38 s.
hayro-interpret now keeps the forms being drawn on the context and skips
a form already on it, reporting ContentTruncated (ISO 32000-2 §8.10.1: a
form is a self-contained description). The lenient fallback from a form's
own resources to the page's stays: MuPDF, Poppler and PDFium all draw a
name from the page's resources when the form's /Resources has no
XObject subdictionary (MuPDF and Poppler also when it has an empty one).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* fix(render): a form that reaches itself through a pattern or a glyph is drawn once (hayro-interpret patch 89)
The forms being drawn are now kept per page and thread rather than per
context, so the tiling patterns, Type 3 glyphs, soft masks and groups
painted inside a form, which hayro interprets in contexts of their own,
see that the form is being drawn and skip it inside themselves. A form
drawn again after it has finished draws as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
* chore: re-run CI after the Fill & Sign save comparison failed on the clock (#6066)
`fill_sign_enter_adds_lines_outside_click_commits_and_escape_cancels`
compares two saves whose /ModDate fell in different seconds; it passes
locally on this commit. No change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
---------
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
TIFF predictor 2 (ISO 32000-2 §7.4.4.4, Table 10) went through the
bit-level BitChunks/BitWriter path for every bit depth, re-reading the
previous output pixel through a new BitReader per pixel. Whole-byte
samples (8 and 16 bits per component) are now undone on byte slices,
each sample added to the same component of the pixel to its left,
wrapping at its width; 1-, 2- and 4-bit samples keep the bit-level path.
The output is byte-identical, including the dropped partial last row.
A page of 45 full-page 8-bit grey images so predicted (PDFBOX-1350)
renders in about 4 s instead of 23 s with the same pixels.
Vendored hayro-syntax patch 42.
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M5: a comment whose colour is written [r g b null] is drawn, as CMYK with the null read as 0 (view.render-annotation-appearances)
ISO 32000-2 Table 166 /C (and /IC, Tables 178, 180, 181; /MK /BC and /BG,
Table 192) is an array of 0, 1, 3 or 4 numbers whose length picks the
colour space. A four-entry array with a non-number in it, such as
[1 0 0 null] (PDFBOX-2699), made the /C parser give up, so a shape
without an appearance stream was not drawn at all. PDFium, MuPDF and
Poppler all read such an array as CMYK with the non-number as 0.
- annot: a four-entry /C or /IC reads non-numbers as 0; other lengths
still need every entry to be a number.
- annot: the properties' fill uses the same parser (it read
[r g b null] as RGB before), and /C no longer drops a non-number
to make three numbers.
- forms: /MK /BC and /BG read a four-entry array the same way instead
of dropping the non-number and drawing RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: comment properties report the stroke colour as drawn; a reference among four colour entries is not read as 0 (view.render-annotation-appearances)
props() reads a non-FreeText /C through the appearance parser, so a
[1 0 0 null] stroke reports CMYK cyan as drawn, and grey and CMYK
strokes report their converted colour. A reference among the four
entries of a /C array (ISO 32000-2 7.3.10) is left unreadable instead
of standing in as 0, so /C [1 0 0 6 0 R] is never drawn cyan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: re-run CI (fill_sign Escape test depends on the clock's second, passes locally)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: the comment list reports a comment's colour as drawn (view.render-annotation-appearances)
summaries() accepted only a three-entry /C, so a Square with /C [1 0 0 null],
drawn cyan and reported cyan by props(), had no colour in the comment list;
grey and CMYK colours were dropped the same way. Non-FreeText comments now
read /C through drawn_color (the array itself followed when it is given by
reference), keeping the three-number fallback and the FreeText /DA handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M5: the render inspector lists a comment's colour as drawn (view.render-annotation-appearances)
The inspector accepted only a three-entry /C, with a referenced entry read
as 0, so a /C [1 0 0 null], a grey or a CMYK colour listed none, unlike the
comment list and the drawing. Non-FreeText comments now read /C by the
appearance builder's rule (one entry grey, four CMYK with a non-number as 0
and a reference unreadable, finite and clamped), with the three-number
fallback reading referenced entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
---------
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Combine files: add a drop as one step, in order, and name what it can't take
Files dropped on the Combine files tab are handled as one batch: they join
the end of the list in the order dropped, as one undo step, from their path
or, when a drop carries only bytes, from those. A folder still adds the
files in it and its subfolders. Files of a kind Combine doesn't take are
left out and named in a notice, instead of joining the list as files that
can't be read. In a browser the dropped files are read one after another,
so they keep their order too.
* Combine files: let the web build use Incoming for dropped files
* Parity: Combine files takes drops in order, as one step, and names what it leaves out
The test compared two saves' bytes, which carry the save time, so it failed
when the saves landed in different seconds (seen on CI). It now checks that
the edit generation, the unsaved state and the undo step are unchanged and
that the one annotation is all there is.
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
Refs #5989. Ctrl/Cmd+A selects every object on the page; copied objects paste as page content that draws the same, within a document or into another, with organize's bounded deep copy across documents. Headless: object_copy, object_paste, object_move all.
Review asked for the near misses of joining wrapped lines. A short line,
a numbered item and a symbol-font bullet were already covered; these add
a line beside another column and a poem between paragraphs.
Resolved against main's ruled tables (#5154, #6626) and clipboard export:
the new tests pass main's rules argument to tables(), and the parity
notes keep both sides' wording.
Brings the Thai word-breaking branch up to date with main. The only conflict was the edit.add-text parity entry: main's evidence and notes (print inks) are kept, with the new Thai test added and the Thai wrapping sentence updated.
A legacy THSarabunPSK subset whose map gives a shifted alternate (glyphs
342-368) as the nominal mark, its nominal glyph dropped, shaped that
alternate wherever the nominal was needed, drawing the mark out of place.
The shaping copy now reaches an alternate only through its presentation
form, so text that needs the dropped nominal is refused instead.
A complete program of the source's family in another style, such as one
its descriptor marks bold, stopped the bundled Regular Thai outlines from
completing a Regular subset, and the text was then drawn in that bold
program. Count only a program in the source's style, as the subset
completion itself does.
The source shaper refused any Thai text in a program without GPOS mark
positioning, so a row of Thai digits failed although nothing in it is set
on a base. Require the positioning only for text with a vowel or tone
mark, or Sara Am.
The resource limit was checked before a form's resources were merged into
the page's, so one form could take the page past it and every later form
was then refused, even one whose resources were all the page's already.
Check the merged dictionary instead.
The first program of the face that could shape a span was chosen even when
its embedding permissions forbid embedding it, and the edit then failed
while a later program of the same face could have been used. Check that a
program may be embedded before choosing it.
A space that no subset of the face encodes was drawn as a quarter-em gap,
narrower than the text-space fallback the source font uses elsewhere.
Keep each subset's source space with it and use that for the gap.
Every resource name of a page's Thai subsets decoded and kept its own copy
of the font program, so many names of one large program could hold about
2 GiB for a single edit. Names of one font object now share its program,
the subsets of an edit are decoded within a shared 64 MiB allowance, and
an edit that needs more is refused with the reason, changing nothing.
A legacy subset can keep a cmap of its own that maps only Latin, so the
editor program returned it unchanged and an edit such as a consonant with a
tone mark failed although the subset keeps both outlines. Add the PDF's
verified Thai and alternate mappings to the editor-only cmap where the
program's own lacks them, keeping its existing mappings.
* M10: Correct recognized text keeps the window's tabs and toolbar and shows the page (ocr.correct-recognized-text)
Scan & OCR > Correct recognized text drew the OCR Verify screen before
the tab strip and the mode bar and returned, so it took the whole
window: the tabs, menus and toolbar went away (on macOS the traffic
lights landed on the PAGES caption). Its source pane drew only a
document the screen had recognized itself, so opened on a document
(already searchable or not) it was blank until a run.
The screen now takes the document area under the tab strip and the
mode bar; the command palette shows over it too. Before a run, and
after a review is accepted or rejected, the source pane shows the
document in front at the page it is on. Another mode or the Home tab
puts the screen away, as Exit does; the command brings it back with
its review as it was. A run now shows the first page it reads (the
current page for that scope) instead of page 1.
Fixes#6485
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M10: OCR Verify keeps its review when the document is closed and shows a page it read (ocr.correct-recognized-text)
- Closing a document whose OCR review is still open (File > Close, the tab's
Close, the snapshot bar's Close, Close all) brings the review forward with
a notice to accept or reject it first, instead of closing the document under it.
- After a run the source pane shows a page that was read, over one skipped for
its text; a run that read no pages keeps the page it was started on.
- PAGE shows the page's number over the document's page count.
- A cached preview of another document or page is dropped before rendering.
- The re-entry test checks a review and its correction survive another mode
and Correct recognized text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M10: OCR Verify holds Quit for an open review, lets a failed Accept be rejected, and survives a worker that cannot start (ocr.correct-recognized-text)
- Quit and closing the window bring a document under OCR review forward
with its review instead of exiting, as Close all does.
- Reject also throws away a reading whose Accept failed.
- A run or re-read whose worker thread cannot start leaves the screen
usable and an existing review as it was.
- A page preview that fails to render is not rendered again on every
repaint, only once the document changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M10: the web build's leave warning asks while an OCR review is open (ocr.correct-recognized-text)
A browser reload or a closed tab cannot be held the way Close and Quit are,
so the beforeunload flag the web page reads now also counts an OCR review
that is not yet accepted or rejected, by the same check that holds Close all
and Quit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M10: keep LF line endings in the files the leave-warning change touched (ocr.correct-recognized-text)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
* M10: an OCR review holds a deferred close, a run on another document, and the embedding page's leave warning (ocr.correct-recognized-text)
A close that waited for a save checks again for an OCR review started
meanwhile: the reviewed document stays open, and Close all and Quit stop.
Run OCR on another document while a review is held is refused with a
notice, so the review keeps its corrections. The embedding page's
`host_dirty` hears an open review as work at risk, as the reload warning
does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
---------
Co-authored-by: mtlcl410 <chuanlin@niar.org.tw>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A predictor whose parameters are an indirect object left its row tags in
the decoded font program and glyph map, so the Thai map repair, the glyph
codes and the shaper read the wrong bytes. Resolve the parameters as the
other font readers do, keeping the decode budgets.
Edit text queued Prepare grouped text for every page that came into view, after a full
preparation on a copy of the document on the UI thread to see whether it could. Scrolling cost
that work per page, marked the document changed, added undo steps and inlined shared forms
although nothing was edited.
Now nothing happens while pages are shown. A click (press and release, no drag) on grouped
artwork, routed to the picture by the page router as before, asks once whether that artwork
shows text preparation would open (text::forms::grouped_text_at, which reads the page without
copying or changing it). If it does, the page is prepared as one undoable step and the
paragraph at the point clicked opens for typing. A logo or a group preparation would refuse is
selected as before, with no message and no change. A drag still moves the selected group, and
while a paragraph is open or an edit is waiting the click selects as before. text_prepare is
unchanged.
The docs and the parity entry no longer say text is prepared on entering Edit text.
The repair reads the page's own fonts. It ran only after a group was expanded, so the same
damaged map on text that was never grouped kept reading wrong. Preparation now repairs it
whenever it finds it, recorded in the repair log as before, and PrepareText (text_prepare)
succeeds when it repaired a map even with nothing to expand. A repaired map no longer matches
the damage's fingerprint, so preparing again changes nothing.
Ungroup and text preparation now check the placement in one place, ungroup_ops, which is given
the page's operators and the Do's index:
- The form is looked up by the name the Do writes, byte for byte. Ungroup looked it up by the
picture's display name, a lossy UTF-8 copy, so a form named /F#FF was 'missing'.
- A Do with more than one operand is refused: the picture was read for its first, the renderer
draws its last.
- A Do inside BT…ET or an unfinished path is refused. Inline, the form's text objects would be
nested in the page's, and its clip would take in the page's path. Preparation already refused
these; Ungroup didn't.