The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The MSI registered no file association. The only PrintCraft entry a
user could pick in "Choose an app on your PC" was the advertised Start
Menu shortcut, which Windows can't run as a file handler ("This app
can't run on your PC").
The MSI now uses Windows' Default Programs registration: a
PrintCraft.Document ProgID (open command, icon), .pdf OpenWithProgids,
Applications\printcraft.exe (friendly name, supported types), and
Capabilities\FileAssociations listed under RegisteredApplications.
PrintCraft appears under "Open with" and in Settings > Default apps.
It never takes over an existing default: .pdf's own default value is
left alone, and the user chooses.
Checked locally with WiX 5.0.2: the MSI builds, passes ICE validation
(only the existing ICE61 note on same-version upgrades), and its
Registry table holds exactly these values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release.yml's comments say "See docs/releasing.md", but the workflow came
over from PhotoCraft without its document. It now describes PrintCraft's
pipeline as it is: cutting a release and test runs, every artifact
(including Windows on ARM64), the craft-fonts pin, each platform's
packaging, signing and notarization, the release-environment secrets,
and the packaging checks.
Written from release.yml and the packaging scripts. It says plainly that
the binaries don't embed the commit yet, and packaging/env.sh no longer
points to a build_info.rs that doesn't exist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md, CLAUDE.md and crates/fonts/README.md linked to storytold/craftrules, which outside contributors can't open. The links stay for team members, now labelled internal, and AGENTS.md says up front that every rule a contributor here must follow is stated in AGENTS.md and CLAUDE.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its Status line said 38 tools and listed form authoring, redaction tools and MCP page-image resources as planned; 123 tools and all three ship. It now names the areas the tools cover and points to printcraft-cli tools for the current list instead of a count that goes stale, and the MCP section lists the resources/* methods and resource templates. ROADMAP's M13 row said 91 tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Field Properties > Options > Check Box Style (Button Style for radio
buttons): Check, Circle, Cross, Diamond, Square or Star, as Acrobat
offers. The choice is stored in each widget's /MK /CA with Acrobat's
ZapfDingbats codes (4 l 8 u n H), so other viewers draw the same mark,
and PrintCraft draws it as paths, needing no symbol font. Styles set
by other apps are read back too; the defaults stay a check for check
boxes and a circle for radio buttons.
- forms: CheckStyle, check_style(), FieldProps::check_style; set_props
writes /MK /CA (resolving an indirect /MK so its colours survive) and
redraws; text and choice fields refuse a style.
- engine: Document::field_check_style.
- automation: form_set_props takes check_style, form_fields reports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- #93: field detection already ran when Prepare a form starts on a
document without fields, and as the form.detect command, but nothing in
the tool panel offered it. Prepare a form > Fields now lists Detect
form fields; on a form that already has fields it only adds new ones.
- #95: with several fields selected (Shift/Cmd-click), Delete and the
context menu's Delete removed only the right-clicked one. They now
remove every selected field as one undoable step ("Delete 3 fields").
One prepare::delete_selected serves both.
Editing the shared properties of several fields at once is still to do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session's log line: issues #70, #72, #73, #74 and #76 fixed, and
#79, #80 and #81 noted as open. The parity tables are refreshed from
`cargo xtask parity` (805 features, 50.2% shipped, 53.7% weighted),
with the by-area shares and the 0.2.1 release. Docs only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Text tool, a click on the page replaced the box being typed
with a new empty one, throwing the text away. A real mouse click
usually arrives as press and release in one frame, so the page saw the
click before the editor saw its text box lose focus.
- The page click now finishes the open box first (one finish() shared
with the editor), then starts a new box where you clicked. Both input
timings end the same way, so the tool no longer drops back to Select
after one box.
- The box being typed gets Done (keep it, back to Select) and Discard
(as Escape) buttons beside it, as the reporter suggested. Pressing
them doesn't count as a click away.
- The box just kept is not selected while the next one is typed, so the
Format panel styles the new text.
- content_ui::page_input returns a bool; its Outcome::done was never
set.
Reproduced and checked in the real app on Windows with OS mouse and
keyboard input (SendInput): before, the first box vanished; now both
boxes stay and a third opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Glyphs are rendered on whole pixels: egui's sub-pixel binning draws
each glyph at quarter-pixel offsets, which it notes makes text
blurrier, most visibly at the 100-150 % scaling common on Windows.
- Light theme: a mild gamma (0.75) on glyph coverage darkens the
antialiased edges of dark text, which egui renders linearly (thin and
grey next to the system's text). The dark theme keeps egui's curve.
- The faintest text colour met only 3.3:1 (light) and 3.9:1 (dark).
It is now #6B6B73 / #97979E, so every text colour meets WCAG AA
(4.5:1) on every surface in both themes, enforced by a test.
Compared in headless renders at 1x and 2x: crisper stems at 1x, little
change at 2x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux package description, AppStream metadata (categories,
keywords, a screenshot that doesn't exist), the MSI's comment and the
web embedding example still described PhotoCraft. They now describe a
PDF workbench, AppStream lists application/pdf, and the screenshot is
one of ours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finder hands files to an app as an Apple event, not on the command
line. winit doesn't handle it, so AppKit answered "PrintCraft cannot
open files in the PDF document format". The bundle also declared no PDF
document type: its Info.plist still carried the sibling image editor's
types.
- Info.plist declares PDF (by extension and MIME type) as a document
type, role Editor, rank Alternate: offered under Open With and
settable as the default, without taking over on install.
- apps/printcraft registers open-documents and quit Apple event
handlers through fmv-macos-events (=0.1.0, the audited safe wrapper
PhotoCraft uses, since the Objective-C interop needs unsafe), before
the launch event arrives.
- printcraft-ui-egui gains OsEvent and PrintCraftApp::os_events, polled
every frame: Open opens each file as File > Open does; Quit closes the
window like its close button, so unsaved changes are asked about.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Text extracted from a PDF can hold C0 control characters (fonts without
a usable /ToUnicode), which XML forbids, and Word refuses the whole
document over one ("Word experienced an error trying to open the
file"). The Word, HTML and RTF writers now drop the characters XML
forbids. Also:
- the page size stays within Word's 22 in (a till receipt is taller,
and Word read it as a negative height), and margins shrink on small
pages so text keeps room; images fit the real text width;
- a non-finite font size falls back to 12 pt;
- zip entries get a valid DOS date (1980-01-01) instead of zero.
Checked by opening the exports in Microsoft Word: control characters,
U+FFFE/U+FFFF, a 33 in receipt with a 40-row table, narrow, tiny and
wide pages all open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Next page went to the right-hand page of the spread already on screen,
so the view didn't scroll and the current page snapped back to the
left-hand one. Next and previous page (Ctrl/Cmd+arrows, the rail's
buttons and the NextPage/PrevPage actions) now step a spread at a time
in two-page view, with or without a cover page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shippori Mincho (and its OFL file) leaves assets/fonts/: the Japanese fonts now
live in storytold/craft-fonts. printcraft-fonts gets craft-fonts' build.rs recipe:
with CRAFT_FONTS_DIR=<checkout> it embeds the manifest's fonts as CRAFT_FONTS
(wasm32: only BIZ UDPGothic Regular); without it CRAFT_FONTS is empty and the
app builds, tests and runs as before, minus the Japanese face.
- SHIPPORI_MINCHO is now Option<&[u8]> from CRAFT_FONTS; japanese_glyph
(was shippori_glyph) uses document_japanese_font() (Shippori Mincho, then
BIZ UDMincho, then any Jpan face) and returns GlyphError::NoFont without one.
- Editing PDF text into Japanese without craft-fonts is a clear EditError that
leaves the page untouched.
- UI: every Jpan craft font (BIZ UDPGothic first) is the last fallback of
every egui family, sharing the same static bytes.
- Tests: Japanese glyph/edit tests skip without craft-fonts; new UI tests for
no-tofu Japanese with craft-fonts and working fonts without it.
- CI: the Linux test job checks out craft-fonts at a pinned SHA and runs the
tests with CRAFT_FONTS_DIR. Releases build every platform with it
(CRAFT_FONTS_REQUIRED=1) and ship each font's OFL as OFL-<family>.txt.
- ATTRIBUTION.toml gains [[build_input]] (checked by xtask assets); NOTICE,
AGENTS.md, CLAUDE.md, README and docs say fonts live in craft-fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was only for finding the WARP crash (fixed in the previous commit) and
was merged with #66 by accident; it ran a 60-iteration crash-dump loop and
edited the runner's registry on every push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On GitHub's Windows 11 ARM64 runner (no GPU, so wgpu uses Microsoft's WARP)
tests/view.rs died with STATUS_ACCESS_VIOLATION in about 1 run in 4. A crash
dump puts it inside WARP's pixel-shader JIT
(d3d10warp!PixelJitProgram::ClassifyVars, on WARP's own thread pool) when the
two tests that render pixels compile shaders on separate devices at once.
Single-threaded, or each test alone, it never crashed (180 runs); bigger
thread stacks didn't help (6/30 vs 7/30), so it's that race, not our code.
The app has one device on a real GPU and can't hit it.
Those two tests now hold a per-binary lock for their whole run (taken before
the harness, so released after its device drops). Other test binaries render
from at most one test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the Windows installer: the Start Menu shortcut and Add/Remove Programs icon were blank. (printcraft#57)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cross-compiled ARM64 binary can't run on the x64 packaging runner; the
windows-arm64 workflow installs and runs it on ARM64 instead. (x86 builds
still run there: x64 Windows runs them.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- package.ps1 takes -Arch arm64 (aarch64-pc-windows-msvc, WiX -arch arm64)
and now checks each binary's PE Machine as well as its Subsystem, so an x64
build can never ship labelled arm64.
- release.yml builds, signs and packages a Windows ARM64 MSI and portable zip
next to x64 and x86, cross-compiled on the x64 runner so signing and WiX
run exactly as they do for x64.
- New windows-arm64.yml (PRs touching Cargo/packaging, main, weekly):
packages the ARM64 MSI the same way (unsigned), installs it on a Windows 11
ARM64 runner, checks both installed binaries are ARM64, opens/reads/edits/
renders/saves a PDF with the installed CLI, uninstalls; and runs the whole
test suite natively on ARM64. The MSI is kept as an artifact for 7 days.
- README: Windows builds for x64, x86 and ARM64.
Requested by @maxffarrell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups on top of the table detection:
- Each block's column comes from a precomputed map and a binary search
instead of scanning every column's members, so pages with many text
blocks stay linear.
- A table has at most 64 columns. Rows are materialized to every column, so
a hostile page laid out as a staircase of blocks would otherwise make
(blocks / 2)^2 cells. Test: a_staircase_of_blocks_is_not_a_huge_table.
- RTF: a row's cell boundaries are written before its cells (RTF requires
it), and a spanning cell ends at the last column it spans. The test checks
the Totals row now spans two columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
XFA forms aren't read yet, and opening one gave no hint: a dynamic XFA form
showed only its "Please wait…" placeholder page, and a static one looked like
an ordinary form though Acrobat shows its XFA data in preference to the
fields that get filled.
- render: DocInfo.xfa (Static / Dynamic) from /AcroForm /XFA; dynamic when
/NeedsRendering is true or there are no fields. Exported for doc_info
(automation and CLI) as "xfa".
- ui: the notice bar explains each case (above the form-fields notice); a
static form keeps its Highlight fields button.
- Tests: tests/xfa.rs (dynamic with and without /NeedsRendering, static,
ordinary forms, a malformed /XFA). Parity notes for form.xfa-static and
form.xfa-dynamic (still planned: this explains XFA, it doesn't support it).
Reported by @Sudachi-dev.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In Edit a PDF ▸ Edit text, dragging a paragraph's box moves it, and dragging
the handle on its right edge rewraps it to a new width; a click still opens
it for typing.
- edit: BlockStyle gains offset (user space) and width. rewrite_block puts the
move inside the paragraph's own q … Q as a cm, taken back through the CTM's
linear part so a page-space drag lands right in scaled/rotated content
(the CTM is now recorded at each line's origin). Non-finite numbers are
refused; widths are clamped to one character .. 14 400 pt.
- ui: BlockDrag on DocView; the box follows the pointer (or its right edge
does) and the release applies one undoable Edit text step. The width handle
shows on upright pages only (on a quarter-turned page the screen's
horizontal is the text's vertical).
- automation: text_edit takes dx, dy and width.
- Tests: engine (move, move under a scaling cm, narrower/wider rewrap,
non-finite and zero inputs), kittest pointer drags, automation tool.
Parity: new edit.text-move-resize (shipped).
Requested by @TCB13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a slow Windows runner the test selected text before the page had loaded,
which selects nothing, so no highlight was made and the author check failed
(seen on PR #59's CI). Settle rendering first, as the comment tests do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- printcraft.exe release builds use the GUI subsystem, so launching it no
longer opens a console window next to it (debug builds keep the console).
- The MSI's Icon id gains its .ico extension: Windows Installer requires it
for advertised shortcuts and ARPPRODUCTICON, so the Start Menu shortcut and
Add/Remove Programs showed a blank icon.
- Guards: package.ps1 checks the PE subsystem of both binaries (app = GUI,
CLI = console) before packaging; packaging-lint fails on an Icon id without
.ico/.exe.
- The shortcut's description said "image editor"; it's a PDF workbench.
Reported, with the exact fixes, by @MikailuReeves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The edit applies on a later frame, so reading the annotation straight away
raced (failed about 1 run in 3 locally). Poll for up to ~100 frames.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments were signed with the login name (or set_option author), with no way
to change it in the app. Preferences gains an Identity section whose name
field (labelled for screen readers) sets the author of new comments; it is
saved with the settings and restored (an empty name keeps the login-name
default; restored names are capped at 200 characters). Japanese labels
added. Test: identity.rs. Parity comment.author-identity: shipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment tools' quick bar had colour and line thickness but no opacity,
though comments already carry it (/CA) and the Properties dialog edits it.
An Opacity slider joins them for every tool; set_opacity clamps to 10-100 %
so a comment can't be made invisible. Test: a strikeout made at 50 % carries
/CA 0.5. Parity comment.properties-bar: shipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine and agent tool already made squiggly underlines; the UI had no
way to. CommentTool::Squiggly joins Underline and Strikethrough in the
Highlight flyout and the Comment tool's Markup section, as the
comment.squiggly command, and existing squiggly comments edit as themselves.
Test: comments.rs::the_squiggly_tool_marks_selected_text. Parity
comment.squiggly: shipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both features worked but had no test, so parity listed them as partial:
- core.drag-drop-open: a file dropped on the window opens in a tab
(ui.rs::dropping_a_pdf_on_the_window_opens_it, through egui's DroppedFile).
- view.annotation-hover: hovering a comment shows a popup with its author and
text (comments.rs::hovering_a_comment_shows_its_author_and_text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>