Commit Graph

451 Commits

Author SHA1 Message Date
cyfung1031 b98505c810
M1: full saves are allocated at their exact size (#523)
`write_full` can't know the output size in advance, so its buffer grows by
doubling and the result kept up to as much again unused: 28 % for a 100 MB
save, 99 % for a 5 MB one. The result is often kept for as long as a document
is open. It becomes the working bytes after edits that need a full rewrite
(repaired files, security changes, redaction) through `write_incremental`, and
it backs Save As ▸ optimized and every new document from Create, Combine,
Extract and Split.

`shrink_to_fit` at the end drops the unused capacity. The bytes are the same.
On macOS the shrink happens in place (2 µs for 100 MB, the buffer doesn't
move), and full-save times are unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:18:59 -04:00
Brandon Thomas 8ddcf63ff8
Fix #445: prefer the discrete GPU on desktops with a monitor on each GPU (#558)
On Linux, `pick_adapter` preferred the integrated GPU whenever it drove
any display, to keep hybrid laptops (#8) on their low-power GPU. A desktop
with one monitor on the Intel iGPU and one on an NVIDIA card looked the
same, so wgpu picked the iGPU, which accepted the Wayland surface but then
failed to configure it, and egui-wgpu panicked.

`linux_display_gpus` now also records whether a GPU drives a built-in
panel connector (eDP, LVDS or DSI). When several GPUs drive a display,
the one driving the built-in panel wins (laptops, unchanged); with no
built-in panel anywhere, the discrete GPU wins. Tests cover the desktop
topology from the issue, the laptop case, connector classification and
reading the panel flag from a fake /sys/class/drm tree.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:06:22 -04:00
Brandon Thomas 5ddf33ccf5
Fix #299: typed and drawn signatures stay upright on rotated pages (#562)
On a page with /Rotate 90, 180 or 270, Fill & Sign placed typed and drawn
signatures and initials sideways or upside down: their box and their
strokes/outlines were laid out along user-space axes, which the page turns,
while the click (or fill_sign_add's "at") is a displayed point. Only image
signatures were counter-rotated (#485).

Both now use the same displayed axes as image signatures
(pdfcraft_model::view_matrix_for without its offset): the box's left edge and
vertical centre are at the displayed point, a drawn signature's ink points are
mapped through the displayed right/up vectors, and a typed signature's
normalised outlines are turned back inside a user-space box laid out along the
turned page (the shared signature_image::upright_box, now also used by
SignatureImage::rect). The appearance is plain geometry, so every viewer shows
it upright without relying on a form /Matrix. Unturned pages are unchanged
(the mapping is the identity there).

typed_signature_shape and fill_sign::{signature_at, typed_signature_at} take
the page rotation; fill_sign::place and fill_sign_add pass it.

Tests: a kittest-file test places a drawn, typed signature and typed initials
on /Rotate 0/90/180/270 pages through fill_sign::place, renders them and checks
each displayed ink box is wider than tall, anchored at the point, identical to
the unturned page, and that an asymmetric drawn stroke is not mirrored; an
automation test does the same for fill_sign_add typed signatures and initials,
before and after save/reopen.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:54:07 -04:00
pauliusiv123-rgb 28804d55d7
Issue #430: clear the recent-files list, or remove one file from it (#557) 2026-10-09 11:53:34 -04:00
Brandon Thomas 9acb29e418
Fix #527: Shift+click extends the text selection (#560)
A plain click on a page cleared the text selection whatever modifiers were
held, so there was no way to extend a selection with Shift+click as in
Acrobat. Now, when Shift is held and there is a text selection on the
clicked page, the click moves the selection head to the nearest text
position and keeps the anchor. Without Shift, or with no selection on that
page, a click still clears the selection. Selections remain single-page:
cross-page selection (the issue's second request) is not part of this
change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:48:50 -04:00
Brandon Thomas e5b37b8b9e
Fix #454: dependencies keep only line tables in dev builds (#559)
On Windows every integration-test binary links the whole app and gets its
own 500-600 MB PDB, mostly debug info for third-party crates, so one
`cargo test --workspace` needs ~38 GB of target dir.

[profile.dev.package."*"] now sets debug = "line-tables-only". "*"
matches only non-workspace packages (crates.io dependencies and the
vendored crates under vendor/, which are excluded from the workspace), so
our own crates keep full debug info and dependency frames still show file
and line in backtraces.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:48:03 -04:00
Brandon Thomas 653f8268c2
Fix #273: ↓ / ↑ turn pages in single-page view (#550)
In single-page view ↓ / ↑ only added to the pending keyboard scroll, so on a
page that fits the window (or once scrolled to the page's end) they did
nothing, while the mouse wheel turned the page there.

The page view now records, after each frame in single-page view, which page
it drew and whether it was at the top and at the bottom of it. ↓ at the
bottom (always, on a page that fits) steps to the next page; ↑ at the top
steps to the previous page and lands on its bottom, so holding ↑ keeps
scrolling back through the document. The edges only count when they belong
to the current page with no jump pending, so a stale frame can't turn two
pages for one press.

Tested by a kittest: Fit page in single-page view, ↓ / ↑ turn one page per
press; Fit width, ↓ scrolls through page 1 before turning, and ↑ from the
top of page 2 returns to the bottom of page 1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:50:59 -04:00
Brandon Thomas 6fce73f902
Fix #429: pen strokes stay unselected and show no author popup (#549)
Cause: apply_edit selected every new annotation after Edit::AddAnnotation,
so each stroke drawn with the Draw (ink) tool became selected and showed
its selection box over the page. With the pointer still resting on the
fresh stroke, the canvas hover popup also showed the comment's author,
covering where the next stroke goes.

Fix: a new Ink annotation made while the ink tool is active is left
unselected (other annotation types, and ink added any other way, are
still selected). The annotation hover popup is suppressed while the ink
or eraser tool is active, as it already was mid-gesture.

Test: kittest draws two pen strokes in a row and checks both exist,
nothing is selected, the pen stays on and no author popup appears; a
square drawn afterwards is still selected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:50:17 -04:00
Brandon Thomas 2aca0692fc
Fix #526: keep text colour in Word, HTML and RTF export (#547)
The export model (`pdfcraft_export::Block`/`Cell`) had no colour, so
`export_pages` dropped the fill colour that `edit::TextBlock` already
carries and every run came out black.

Block and Cell now carry an sRGB colour, mapped from TextBlock.color.
Word runs get `<w:color w:val="RRGGBB"/>`, HTML paragraphs, headings and
cells a `style="color:#RRGGBB"`, and RTF a colour table with `\cfN`.
Black text keeps each format's default; NaN and out-of-range components
clamp.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:49:21 -04:00
Brandon Thomas a593512700
Refs #347: encode the full WinAnsi 0x80-0x9F range (Š š Ž ž Œ œ Ÿ ...) (#546)
pdfcraft_fonts::win_ansi, edit's is_win_ansi_char and the xtask demo
encoder each carried a hand-written subset of WinAnsiEncoding's 0x80-0x9F
row (quotes, dashes, euro, trademark) and left out Š š Ž ž Œ œ Ÿ ƒ † ‡ ˆ
˜ ‰ ‹ ›. Text editing therefore treated š/ž as non-WinAnsi and sent them
to the Type3 fallback font (or refused them without craft-fonts), and
every other generator (forms, annotations, OCR layer, redaction overlay,
signatures, create) wrote them as '?'.

win_ansi now goes through a new win_ansi_byte, which reads the 0x80-0x9F
codes from the same ISO 32000-2 Annex D table the text extractor decodes
with, and is_win_ansi_char reuses it. xtask doesn't depend on
pdfcraft-fonts, so its demo encoder gets the full row with a pointer to
the shared table. č/ć are not in WinAnsi and still use the fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:45:28 -04:00
Brandon Thomas bd57377435
Fix #525 (item 4): phone pattern redacts international numbers whole (#543)
The Phone Numbers redaction pattern accepted only 10 digits (11 with a
country code), the North American form. For "+49 2151 123456" (12 digits)
the `+` candidate was rejected and the scan then matched "2151 123456",
leaving "+49" visible: a partial redaction in a document the user thinks
is clean.

The pattern now accepts, still grouped by at least one separator:
- E.164 numbers with a leading `+`: 8-15 digits in all;
- national numbers with a trunk 0 followed by 6-14 digits, whose first
  group has at least two digits (so decimals don't match) and which are
  not a day-first date (left to the date pattern);
- North American numbers as before.
A separator may now be followed by `(`, so "+49 (0) 2151 123456" is one
match instead of a partial "(0) 2151 123456".

Refs #525. The other items (page-number and date tokens, stamps) are not
addressed here.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:43:11 -04:00
Christian Schiffer 7c358099ff
M14: German interface catalog (de) (#471)
* M14: German interface catalog (de)

Deutsch (`de`) joins the interface languages as a complete catalog of
2,035 entries: every command, All tools entry, dialog, panel and notice.
It is selected from Preferences, and Auto picks it for `de`, `de_DE`,
`de_AT`, `de_CH` and other German locales. Wording uses the informal
"du" and the infinitive for commands; translations are clean-room, written
from the meaning of the English labels.

- de.tsv registered in LANGUAGES (plural rule one|other)
- tests: registration and locale mapping, plurals, coverage of every
  command, All tools label and tl! literal, history labels keeping user
  values; the Latin UI faces cover umlauts, ß and German quotes; the
  control-channel language switch includes `de`
- ATTRIBUTION.toml/.md entry (kind = "translation"), docs/localization.md,
  ROADMAP and parity notes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* M14: register German as the 12th language; translate the reopen-session preference (de, bg)

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 09:26:15 -04:00
fares111111122 21a9624585
M7: Arabic in added text (shaping, bidi order, searchable Type 3 glyphs) (#403)
Add text wrote Arabic as "?" (standard-14 fonts, WinAnsi). Items with
Arabic are now shaped with the craft-fonts Arab face through harfrust
(already in the tree via egui) and laid out line by line in display
order with unicode-bidi (UAX #9), so Arabic mixed with Latin, digits and
brackets reads correctly. Each cluster (a letter with its dots or marks)
is one glyph of a Type 3 font drawn from the face's outlines, with a
ToUnicode entry, so text extraction, copy and search get every letter
once and no font file is embedded.

- fonts: shape_arabic / arabic_glyph / arabic_has / document_arabic_font;
  japanese_glyph's bounded outline code is shared as bounded_outline.
- edit: draw_arabic and type3_arabic; wrapping measures shaped words.
  Fonts get names free on the page (a full save renumbers objects, not
  resource names) and are removed when the item is updated or deleted.
  Outlines are read before anything is written.
- Without the face, adding Arabic is a clear error that changes nothing;
  an existing item holding Arabic still moves, drawn as "?" as before.
- Non-Arabic items are drawn exactly as before.

Refs #250, #280, #369.

Co-authored-by: fares <aw674860@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 09:22:50 -04:00
jcl-bit ee06583523
Never crash: JBIG2 symbol dictionaries can't loop without end (#408)
`cargo xtask fuzz` found a 1.1 KB page whose JBIG2 image never finished
decoding (over two minutes, flat memory). It differs from a valid file by
one byte inside a symbol dictionary's arithmetic-coded data. Past the end
of its data the arithmetic decoder keeps returning values, and two loops
in symbol.rs only move on when those values make progress:

- Export runs (6.5.10): a run of length 0 doesn't advance, and the
  decoder returned 0 for ever. This was the fuzz finding.
- Height classes (6.5.5): a class whose first width is already the
  end-of-class marker decodes no symbol. A read-only sweep of the crate
  found this one. In a synthetic dictionary every class after the first
  symbol adds 13 to the height and decodes nothing, so it only fails
  when the height overflows u32, after 330 million classes (about five
  seconds per image, logged); a delta of 0 would never end.

The sweep found every other loop in hayro-jbig2 bounded by a symbol,
instance or pixel cap or by the end of the data. Bounded is not cheap
everywhere: refinement bitmaps in text regions and aggregate symbols
(up to 10,000 instances of up to 2^28 pixels each) are not changed
here.

Vendored hayro-jbig2 patch (3): a valid dictionary needs at most one
height class per new symbol and `2 x symbols + 1` export runs (they
alternate, so at most one zero-length run before each symbol's). Both
loops get that budget plus `EXTRA_EMPTY_STEPS` (1024), then fail the
dictionary as invalid. Encoders don't write empty steps back to back,
so the slack only tolerates oddities.

The fuzz finding now renders in 0.2 s. Synthetic regression test in
render: two dictionaries of a few arbitrary bytes each, found by
searching for streams that decode the same way (no corpus bytes). The
height-class one is drawn as twenty images so that it stalls well past
the 20 s timeout without the patch on any machine. Each case is red
with its own budget removed and green with it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 09:22:21 -04:00
Brandon Thomas 49fb1b1b35
Tests: one wgpu device compiles shaders at a time in fill_sign.rs (WARP ARM64 crash) (#395)
The image-signature tests (#335, #355) render pixels in parallel; on the
Windows 11 ARM64 runner WARP's pixel-shader JIT then crashes the test process
with STATUS_ACCESS_VIOLATION (3 of the last 4 main runs). Take the same GPU
lock tests/view.rs uses (d341651) in every fill_sign test that renders.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:21:57 -04:00
Brandon Thomas 2f98beba54
Merge pull request #272 from XusBadia/dmg-background
macOS DMG: branded window background and icon layout
2026-10-09 09:21:44 -04:00
Brandon Thomas 37542ad9db Merge remote-tracking branch 'origin/main' into dmg-background
# Conflicts:
#	ATTRIBUTION.md
2026-10-09 09:21:35 -04:00
midasdf 32fbef684a
M5.7: keep natural image stamps upright on rotated pages (#473)
Refs #434 (partial). Preserve displayed bounds and standard appearance orientation through stamp edits; image signatures and explicit rectangles remain outside this fix.

Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 09:21:31 -04:00
Streetblock 8c90a8d1fd
M2: bound lopdf predictor scratch to available samples (#470)
* M2: bound lopdf predictor scratch to available samples

Return empty PNG predictor frames before allocating row buffers. Reject
overflowing packed TIFF row dimensions and bound component scratch to the
samples present in the input, aligning with the existing lopdf#589 fix.

Cover hostile dimensions and valid packed rows with synthetic regressions
and record the vendored patch and its upstream proposal.

Co-Authored-By: OpenAI Codex <codex@openai.com>

* Keep main's M1 roadmap row after merging main

---------

Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-09 09:21:21 -04:00
Dimitar 79eadc2aa8
M14: Bulgarian (Български) interface language (#466)
A complete clean-room catalog (2,035 entries: every command, All tools
label and tl! literal), registered as `bg` with one/other plurals so Auto
picks it on Bulgarian systems. Tests mirror the Russian ones (registration,
plurals, persistence, command and literal coverage), and Bulgarian joins
the Latin/Cyrillic font-coverage test.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:16:16 -04:00
pauliusiv123-rgb 3de2be8a68
Issue #442: reopen the files that were open when PdfCraft last closed (#462) 2026-10-09 09:16:10 -04:00
cyfung1031 3a35a7d6ac
M1: incremental saves are allocated once at their exact size (#402)
`write_incremental` copied the original into a buffer with 4 KB to spare,
then appended the revision. A revision above 4 KB (rotating a few dozen pages,
an image, a long comment thread) outgrew it. The buffer was then reallocated
at twice the size: the whole original was copied a second time, and the
result kept up to the original's size again unused. The engine keeps that
result as the document's working bytes, shared with the renderer and handed
to Save, so the unused half stayed alive. A 100 MB document held a 200 MB
allocation after one such edit, and still did after saving.

The revision is now written on its own, with offsets counted from where it
will start. The result is allocated once at exactly the original's size plus
the revision's, and filled in one pass. The output is byte for byte what it
was before.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:16:01 -04:00
cyfung1031 d6c2a694ec
M3: rendered pixels are moved out of the pixmap, not copied (#404)
`render_page` copied every raster out of vello_cpu's pixmap with
`data_as_u8_slice().to_vec()`. That is a second buffer the size of the raster
(up to 256 MB for a 64 MP page), alive alongside the first for as long as the
copy runs. `Pixmap::take` hands over the pixel vector, and `bytemuck`
reinterprets its four one-byte channels as bytes in place: the same bytes, in
the same buffer.

On a 6182×8000 px page, peak RSS goes from about 425 MB to 246 MB and render
time from 102 ms to 81 ms. Pixels are identical: every page of #260's test
file, at two scales and as a tile, hashes the same before and after.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:15:55 -04:00
cyfung1031 e130af2ef4
M3: renders of a dropped render pool stop at the next operator (#409)
The engine replaces a document's render pool after every edit, undo, redo,
layer toggle and comment-visibility change, and drops it on close. Workers in
the middle of a page kept rendering it to the end before finding that nobody
could receive the answer. On heavy pages that is seconds of CPU per worker,
competing with the new pool re-rendering the same pages.

hayro-interpret, vendored patch 17: `InterpreterSettings::cancelled`, checked
before each content operator. The pool sets it only in `Drop`, when its result
channel is gone, so a partly drawn page can never be delivered. Live pools,
`PageRenderer` and inline pools never set it.

7 heavy pages on 7 workers re-rendered after replacing a busy pool: 2.4–2.5 s
before, 1.1 s after (1.0–1.3 s without a replacement, unchanged).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:15:47 -04:00
jcl-bit 8ce0a905bf
Never crash: dash patterns can't exhaust memory or hang the renderer (#405)
`cargo xtask fuzz` found a 392-byte page whose ExtGState sets
`/D [[11] 0]` on a line that starts at x = 92234775807. Stroke expansion
keeps every dash, so it cut the line into billions of them and allocated
more than 5 GB before the run was stopped. Triage found the same class
in ordinary-looking geometry, and a second bug next to it:

- A tiny dash on an ordinary line (`[0.000001] 0 d` on 20 pt) passed
  10 GB in under two seconds; so did `[1] 0 d` under a shrinking matrix,
  and a long pattern on many short subpaths, because kurbo restarts the
  pattern at every subpath. The render watchdog can skip a stuck page but
  can't stop it allocating, so on a desktop this ends in an abort that
  takes every open document with it.
- A dash array whose entries sum to less than zero (`[-1 -1] 0 d`) made
  kurbo's search for the starting dash loop forever. ISO 32000-2
  §8.4.3.6 requires nonnegative entries.

Vendored hayro patch (5, 6): `usable_dash_pattern` bounds the pieces a
pattern can cut the path into, in user space where kurbo dashes:
(control-polygon length / period + restarts) x entries per period, where
restarts are kurbo's: every MoveTo and ClosePath. Over 250,000 the stroke
is drawn solid, as it is for a pattern with a negative or non-finite
entry or a period that isn't positive. Measured in release builds: a
250,000-piece stroke takes about 16 ms and 75 MB at its peak (a million
took 80 ms and 290 MB). A non-finite phase becomes 0.

Every case now renders in about 0.2 s and a few MB. Synthetic regression
tests in render. Both were red before the fix (the first killed at
2.7 GB, the second stalled past 20 s), and four mutations each turn them
red: the first estimate (one piece per subpath) with a 100k cap runs out
of memory, a 100k cap draws a fine legitimate dash solid, dropping the
negative-entry check draws `[-1 3]` dashed, and dropping it together
with the period check hangs. (The period check alone is defence in
depth: hayro-interpret turns 0 entries into 0.01, so a pattern without
negative entries always has a positive period.)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:15:38 -04:00
feraremag2680 fa52469dbe
sign: read the .p12 files Windows writes (BER, not DER) (#453)
Importing a digital ID exported by Windows failed with "PKCS #12: not a
PKCS #12 file" — at the second byte of the file, before the password was
looked at. Windows writes a .p12 in BER: the PFX, its ContentInfo and
the [0] around the AuthenticatedSafe all use the indefinite length form
(0x80 … 00 00), and the AuthenticatedSafe itself is an OCTET STRING
split into segments. der.rs took definite lengths only, so every one of
those files was turned away. OpenSSL writes definite DER, which is why
the fixtures never showed it.

A Tlv now carries the rules it was read with, and its children are read
the same way: Tlv::parse is the strict DER reader it always was,
Tlv::parse_ber also takes the indefinite form, and Tlv::octets joins the
segments of a constructed OCTET STRING, borrowing when there is nothing
to join. Both are depth-bounded. pkcs12 reads the envelope with the BER
reader; x509 and cms keep the strict one, so nothing that is verified
over its own bytes changes.

The MAC decides what is allowed here: it covers the contents of the
authSafe OCTET STRING, the segments joined, which is what the writer
MACed. So the file is never normalised to DER — that would change the
octets the MAC was computed over and turn a right password into a wrong
one.

The test builds the Windows shape out of the OpenSSL fixtures, twice:
the outer wrappers in BER with the MacData intact, which is what proves
the MAC is taken over the right octets, and then the AuthenticatedSafe
re-encoded as well, with the MacData dropped (RFC 7292 §4 makes it
optional), for a file that is BER the whole way down.


Claude-Session: https://claude.ai/code/session_013Q5dhMC58jk1ePNW37xe1b

Co-authored-by: Fernando Areán <soporte@codebit.es>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 09:15:32 -04:00
midasdf 57c281ca0a
M3: keep overflowed document tabs reachable (#416)
Refs #397. Scroll the tab strip, reveal the active tab and preserve the existing close-confirmation path.

Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)
2026-10-09 09:15:24 -04:00
midasdf c8c7959c0f
M4.6: search bookmark titles without changing the outline (#424)
Refs #257. Show matching entries and their ancestors, keep original edit paths and preserve per-document view state.

Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)
2026-10-09 09:15:18 -04:00
Wesley Haines 3543ca8296
Edit text: fast drags on a paragraph's edge no longer get dropped (#436)
The rewrap handle took the drag target from the pointer's current position.
egui only reports drag_started() once the pointer has moved 6 points (or the
button was held 0.8 s), so a quick outward flick had already left the 7x14 px
handle and the drag was ignored. Resolve the target from press_origin(), as
prepare.rs does, before the hover early-return.

Also: the left edge rewraps too (width plus an x offset), and edge grips span
the box's full height, 6 px either side of the edge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:15:13 -04:00
sanyamsingh001 561377eb1f
Keep document view state consistent when closing tabs and reverting (#439)
Preserve the selected document when an earlier inactive tab closes. Clear the reverted document's form drafts and queued edit only after a successful Revert.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-10-09 09:15:06 -04:00
pauliusiv123-rgb 3c771e0395
Issue #425: long file names and paths on Home end in "…" before the row's details (#443) 2026-10-09 09:15:00 -04:00
MarekSpodBiedry 5c5f4d8b49
Phone photos follow EXIF orientation; image signatures stay upright on rotated pages (#434) (#485)
* Stamps and added images follow a JPEG's EXIF orientation (part of #434)

Phones store a portrait photo as landscape pixels plus an EXIF
Orientation tag. A JPEG went into the PDF as it was (DCTDecode), and PDF
image XObjects have no such tag, so a photo used as a custom stamp, an
added or replaced image, a watermark or background, or a button image
showed up turned 90 degrees.

Before these images are embedded, a JPEG whose Orientation is not 1 is
decoded, turned the way the tag says and re-encoded (quality 92). Its
JFIF resolution is kept, swapped for quarter turns, so the natural size
on the page stays right. Other images, JPEGs without the tag, JPEGs that
fail to decode and photos over 64 megapixels are embedded unchanged, as
before. Fill & Sign image signatures already applied the tag.

* Image signatures stay upright and anchored on rotated pages (part of #434)

On a page with /Rotate 90, 180 or 270 an image signature turned with the
page, because its picture was drawn along user-space axes while the click
or the "at" argument is a displayed point. It was also anchored at the
wrong point: its left edge and vertical centre were taken in user space,
so on a 90 degree page the image centre ended up elsewhere.

The signature's appearance now carries the counter-rotation as its form
/Matrix, which viewers fit to /Rect, so the picture reads upright as
displayed. The page's rotation is kept in /PCPictureRotate, so restyling
regenerates the same matrix and moving and resizing keep it. The box of the
signature runs along the displayed axes, so its left edge and vertical
centre are where they were asked for. The page view matrix in
pdfcraft-model is split out as view_matrix_for.

The Fill & Sign preview and the live move/resize preview draw the picture
the way the appearance does, and the resize handles keep the image's
proportions as displayed. comment_image_preview reports image_rotation,
how far the image is shown turned (0 for one placed upright).

Custom image stamps are left to #473.

* Use is_multiple_of for the quarter-turn checks (clippy) (#434)

* Probe the pointer preview away from egui_kittest's drawn cursor (#434)

The kittest render paints a mouse cursor (a 16 px triangle with a black outline) at the
hover point, so the 2 pt probe beside the pointer landed on its outline when zoom and view
rotation made 2 pt only a pixel or two. Put the stroke's block at the picture's right end and
probe there and at its far margin instead.
2026-10-09 09:14:55 -04:00
Chen Tao 5f570eab01
M2.6: non-embedded Mincho fonts draw half-width katakana (#484)
Since #368, a non-embedded Adobe-Japan1 font with a Mincho name
(HeiseiMin-W3, MS-Mincho, ...) is drawn in document_japanese_font(),
i.e. Shippori Mincho. Shippori Mincho has none of the 63 half-width
katakana (U+FF61-U+FF9F), which are common in Japanese documents.
hayro maps the CID through Adobe-Japan1-UCS2 to Unicode, finds no
glyph in the face's cmap, and falls back to drawing the CID as a glyph
index: half-width katakana came out as unrelated glyphs (ligatures
such as fi and fl).

The Mincho substitute is now BIZ UDMincho Regular, which covers
half-width katakana (and was designed as a metric-compatible
replacement for MS Mincho), falling back to document_japanese_font()
when it isn't built in. The face used for Japanese text written into
PDFs is unchanged. Without craft-fonts nothing changes.

Tests: raster::tests::mincho_substitute_covers_half_width_katakana
(with CRAFT_FONTS_DIR: the Mincho substitute's cmap maps U+FF61-U+FF9F
and 日本; it lacked all 63 half-width katakana before; skips without
craft-fonts). skrifa 0.44 is a dev-dependency of pdfcraft-render for
it, the version pdfcraft-fonts already uses. The parity entry
view.render-cid-fonts cites the test.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:14:46 -04:00
MarekSpodBiedry 271139bd3b
Comments: a new highlight opens its note for typing (#475) (#481)
Adding a highlight selected its card in the Comments panel but left the
note closed; the text field only appeared after double-clicking the card.
Acrobat opens the note for typing as soon as the highlight is made.

apply_edit now opens the inline editor for a new highlight (text drag,
selection then tool, area highlight all pass through it), unless a note
typed into another card is still unsaved, so that text is never dropped.
2026-10-09 09:14:40 -04:00
Erige005 bc1d67cb61
Export to Word: detect tables of at most 63 columns, Word's limit (#479) (#483) 2026-10-09 09:14:34 -04:00
Hal Goff 7d9cc53472
Issue #225: a closed Comments panel stays closed when picking comment tools (#501)
Picking any comment tool opened the Comments panel whenever no right panel
was open, so closing it was undone by the next tool switch. Now closing
Comments (its close button or the rail icon) is remembered, across restarts,
and comment tools leave it closed until the user opens it again (rail, View
menu, Comments ▸ list, or a Reply action). Switching to or closing another
panel doesn't count. Opening a document with comments still shows them, as
Acrobat does.


Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:14:27 -04:00
feraremag2680 966bd0ffce
tests: pin the interface language so the suite passes outside English (#502)
The ui-egui integration tests drive the shell by its English labels, and
`i18n::system_lang` returns `Lang::EN` under `cfg!(test)` so they can. That
only covers the unit tests: an integration test links the library as it
ships, where `cfg!(test)` is off, so `PdfCraftApp::new()` leaves `language`
on `auto` and the shell is drawn in the system language. On a Spanish
Windows eleven of the test binaries fail; a French, Russian, Chinese,
Japanese, Portuguese, Czech or Telugu machine does the same. CI never sees
it because its locale is English.

`tests/theme.rs` and `tests/create.rs` already pin the option; the other
119 harnesses now do too, and the comment in `system_lang` says why, so a
test copied from an existing one keeps the line.

Also `i18n::tests::windows_display_language_query_returns_a_locale_tag`
treated the return of `windows_ui_language()` as a single tag. It is the
preferred-language list, one tag per line, as `first_supported` reads it,
so on a Windows with two or more display languages the assertion tripped
over the newline. It now checks the shape of every line.

Co-authored-by: Fernando Areán <soporte@codebit.es>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 09:14:19 -04:00
Hal Goff e93a6f255f
Never crash: CMap parsing can't panic, overflow or run away; tokens need no spaces (#495)
The font-metrics CMap parser (ToUnicode and embedded CID CMaps) split the
stream on whitespace and sliced hex tokens as UTF-8 text:

- a non-ASCII byte inside `<...>` sliced a character in half and panicked
- `bfrange` arrays and `/Differences` incremented code counters past
  u32::MAX, and CID lookup added `c + (code - lo)` unchecked
- 4 KB of overlapping `bfrange`s could ask for millions of map entries
- legal CMaps with no space between tokens (`<0001><0070>`) mapped
  nothing, so their text was not found for editing

A byte-level lexer now reads hex strings (whitespace inside allowed),
brackets, names, literal strings and comments. Counters use checked or
saturating arithmetic, array entries stop at the range's `hi`, and a CMap
adds at most 1 Mi ToUnicode entries and 64 Ki codespace/CID ranges. A bad
array entry still stands for its code, as before.


Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:14:13 -04:00
MarekSpodBiedry ff496918a1
M7: pick a Japanese fallback face that has every glyph of the edited text (#384) (#472)
Replacing text in a serif line (Times New Roman, say) with Cyrillic failed
with "Japanese fallback font has no glyph for U+0444". The Type 3 fallback
took only the style's first face, Shippori Mincho, which has no Cyrillic,
although BIZ UDMincho and BIZ UDPGothic ship alongside it.

`document_japanese_fonts_for_style` lists every Japanese face in the
existing order of preference, and `type3_font` uses the first one that has
a glyph for every character. If none has them all, the preferred face still
names the missing character, as before.
2026-10-09 09:14:04 -04:00
Streetblock 5c354023ab
M2: handle Unicode dates and preserve inspection fallback pages (#469)
Read the date prefix at a checked UTF-8 boundary and leave malformed dates
unchanged. Commit auxiliary metadata only after successful inspection so
an escaped inspector panic cannot discard the renderer's page geometry.

Add synthetic date-boundary, UTF16BE annotation and independent failure
regressions.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-10-09 09:13:56 -04:00
Streetblock a9a7eea2cc
M1: enforce decoded limits on unfiltered COS streams (#477)
Check the effective output limit before cloning a stream with an empty
decoder chain. Share the raw guard between tolerant and strict decoding,
including empty filter arrays and already-applied Crypt filters.

Add small regressions for exceeded, exact and zero-byte limits and keep
filtered decoding behavior unchanged.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-10-09 09:13:46 -04:00
Brandon Thomas 4e79bbdbdf
CI: no macOS on PRs or main; macOS is built and tested by releases (#512)
The org has 5 macOS slots in total (GitHub Team), shared by every repo's CI and
release builds, and PR CI kept them full, so release builds queued behind it.
macOS now runs only in release.yml; a release that fails on macOS is fixed and
rebuilt.

Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:51:23 -04:00
Xus Badia 084f7636ea macOS DMG: branded window background and icon layout
The DMG opened as a plain Finder window. It now shows the app icon's figure on
the PdfCraft colour, with PdfCraft.app and the Applications link side by side.
Same approach as PhotoCraft's and VectorCraft's.

- packaging/macos/dmg/: background.svg (source; text outlined, no paper grain,
  the figure an <image> of assets/app-icon), background.tiff (1x + 2x HiDPI,
  16-colour palette, Deflate, sRGB; ~95 KB) and dmg-layout.DS_Store (Finder
  view settings: window, icon size 128, positions, background).
- generate.py writes the TIFF and the layout on any OS, byte-for-byte
  reproducible, with nothing from the machine that ran it.
- package.sh copies them into the staging folder; the image is still built with
  hdiutil makehybrid. Nothing extra runs in the signing job.
- The volume name drops the version: the layout finds the background through
  an alias that includes the volume name. The DMG file name keeps the version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:38:19 +02:00
cyfung1031 a6bc63a497
M3: render pool renders each request once when the caller re-sends it (#400)
The canvas rebuilds its render queue from every page, tile and text layer it
is still missing, and calls `set_queue` whenever that list changes, which is
whenever any answer arrives. Requests a worker had already popped were still
missing, so they went back into the queue and an idle worker rendered them a
second time. With one worker every other page was rendered twice; with seven
workers about half of all renders were duplicates. A slow page could also take
every idle worker in turn until the watchdog fired.

The pool now remembers the requests its workers have taken until `try_recv`
hands out their answer. A queued request equal to one of them is skipped, and
an answer removes queued copies of its request. Equal requests to one pool
always produce the same answer (its bytes and settings never change), so
nothing the caller sees changes except that answers arrive sooner.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:46:32 -04:00
midasdf 3146e8767e
M7: preserve Japanese Gothic fallback family and weight (#394)
* M7: select real Japanese fallback faces by family and weight

Refs #184. Keep Type 3 font-name reporting separate in #387; missing weights and the small web font input remain explicit limitations.

Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol)

* Test the Japanese fallback name for a sans source: the Gothic face since #394 matches the source style

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>
2026-10-08 21:48:02 -04:00
Austen 0afe0b7094
Keyboard shortcut to enter page number to jump to (#393)
* added view.read_mode command

* added view.focus_page_input

* Go to page…: Ctrl+Shift+N / ⇧⌘N (Acrobat's), no indexing, rustfmt, catalog entries

Shift+Y alone (the first proposal) would have typed a capital Y in text fields
on Windows and Linux.

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>
2026-10-08 21:37:22 -04:00
sanyamsingh001 9c63f3e8f2
fix(cli): validate page positions and preserve command results (M13) (#391)
Reject invalid page positions before input I/O and keep diagnostic stderr
failures from overriding rendering, text errors or MCP startup. Preserve
real file and protocol output failures and upstream compact MCP behavior.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:37:09 -04:00
Books d4d2e83273
FDF: replies and review status keep their parent comment (fixes #333) (#388)
FDF export dropped /IRT, so a note's replies and its review-status reply
imported as unrelated comments. Each comment is now an indirect object of
the FDF (with a cross-reference table), and a reply's /IRT points at its
parent's object there, as ISO 32000-2 §12.7.8.3.1 allows. Import maps the
FDF objects to the placed comments and links replies in a second pass,
falling back to a parent of the same /NM already in the document.

FDFs with direct comment dictionaries still import. Self-references,
missing parents and cycles are ignored without panicking.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:03:30 -04:00
midasdf 438a269bac
M7: report Japanese Type 3 fallback font names (#387)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 21:03:16 -04:00
Dmitriy Bunakalya dc356f0dfc
Linux: accept file:// URI args so desktop launchers recognise pdfcraft as PDF default (#382)
* Linux: accept file:// URI args so desktop launchers recognise pdfcraft as PDF default

GNOME Files (and other freedesktop-spec launchers) only register an app as the
default handler for a mimetype if its .desktop Exec accepts URIs (%u/%U), not
just paths (%F). pdfcraft's entry used %F, so even after `xdg-mime default
ai.storyteller.pdfcraft.desktop application/pdf`, double-clicking a PDF in
Files silently fell back to another installed viewer.

Switch Exec to %u and decode file:// args back to plain paths in main.rs
before they reach the rest of the app, which only ever opens paths. Other
schemes and plain CLI paths pass through unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* path_from_arg: file://localhost, foreign hosts left alone, Windows drive paths, no indexing; Exec %U (several files)

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

---------

Co-authored-by: Dmitriy Bunakalya <dmitriy.bunakalya@appfollow.io>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 20:04:27 -04:00