- release.yml: new linux-riscv64 job (cross-compiled on ubuntu-24.04 against
ports.ubuntu.com riscv64 libs, tar.gz only, CLI smoke-tested under QEMU),
ported from GridCraft; the draft release job needs it, so SHA256SUMS covers it.
- packaging/linux/package.sh: CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE/EMULATOR
support, as in GridCraft.
- cargo xtask version set 0.6.0; README, hardware parity and ROADMAP log note
the new platform.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- apps/designcraft/Cargo.toml: the macOS target keeps objc2 0.6 / objc2-app-kit 0.3 (IME) and
adds fmv-macos-events =0.1.0, which uses the same objc2 and objc2-app-kit versions.
- apps/designcraft/src/main.rs: the shared Arc<gpu::Startup>, catch_unwind and restart flow
stay; the Apple-event handlers are installed before run_native (outside catch_unwind, held
until the window ends), the app drains the inbox each frame, and documents that arrived are
noted on the Startup. The open dialog keeps the INDD/INDT filter and ends with All files.
The open-filter test checks both; the save-dialog test is added.
- apps/designcraft/src/gpu.rs: Startup::opening records the documents the OS handed this run;
a start again after a graphics failure (both finish and the first-frame watch) passes them
as file arguments, since Finder doesn't send them twice.
- apps/designcraft/src/apple_events.rs: poll returns the documents it handed the app, and
queued() returns those still in the inbox when the window ends.
- crates/engine/src/cmd/file.rs: file.open decides by content first (IDML by its mimetype
entry, then a DesignCraft package or JSON), then an InDesign file by its signature or
.indd/.indt extension, then a .idml or .designcraft extension for the reader's own error;
anything else is "not a DesignCraft or IDML file". INDD stays in file.openBytes. Save
commands add .designcraft as on file-open-os; the params docs describe all of it.
- crates/engine/src/cmd/mod.rs: re-exports from both sides (is_document,
with_document_extension and layout::chapter_numbering).
- file.save / file.saveAs / file.saveACopy add the .designcraft extension to a
given path that lacks it and report the path written; saving back to the
document's own file keeps its name.
- file.open tells DesignCraft and IDML packages apart by content, so files
without an extension open; anything else is "not a DesignCraft or IDML file".
- Dropped or web-picked files without an extension open as documents when their
content is one.
- Desktop save dialogs filter on the suggested extension and add it to a name
typed without it, asking before replacing an existing file; the open dialog
gains "All files".
- macOS: open-documents Apple events (Finder, Dock, Open With) through
fmv-macos-events, queued until the window is ready; Info.plist declares the
.designcraft type and an imported .idml type with CFBundleDocumentTypes.
- Windows MSI: .designcraft ProgId with an Open verb, IDML under Open with.
- Linux: shared-mime-info for both types and MimeType in the .desktop file.
Fixes#158, #211, #375
Ported from VectorCraft's start-up fixes (#502, #545, #651, the display-GPU
choice, #806, #964) and reconciled with the backend fallback of #297:
- The app picks the window's adapter itself (eframe's native_adapter_selector,
apps/designcraft/src/gpu.rs): adapters that can't present never, then
WGPU_ADAPTER_NAME, hardware before software, native backends before OpenGL,
the GPU that drives the primary display (Windows EnumDisplayDevices through
winsafe, Linux /sys/class/drm), the power preference (WGPU_POWER_PREF;
otherwise power saving on Windows and macOS, the system's order elsewhere),
then DX12 before Vulkan. Every adapter and the choice are logged.
- wgpu's indirect-call validation is off (WGPU_VALIDATION_INDIRECT_CALL=1 turns
it back on): its compute shader failed to compile on an Intel Mac and lost
the Metal device (#334, first error).
- run_native runs under catch_unwind. A wgpu error or a panic in wgpu,
egui-wgpu or naga during the first frames, or a first frame that doesn't
reach the screen in 20 s, starts the app again without that adapter
(DESIGNCRAFT_GPU_SKIP; exec on Unix, a new process on Windows). When no
adapter of the start's backends is left, it starts again with the next
backend. Each restart leaves out one more adapter or backend, so they end.
Adapters without PCI ids (Metal) are keyed by name.
- The backend fallback (gpu/backend.rs, gpu.json) stays for driver faults that
take the process down. Windows tries DirectX 12, then OpenGL, then Vulkan,
each in its own instance: Vulkan last because creating its instance loads
every installed Vulkan driver, and OpenGL kept out of the DX12 instance
because creating it crashed in AMD's atio6axx.dll (#167).
- main returns an ExitCode and logs why the app stopped to standard error and
the log file, which is all a Windows start without a console leaves (#273).
README, architecture, gaps, hardware and target-app parity docs and ROADMAP
describe the choice, the restarts and the variables.
Conflict: crates/ui-egui/src/lib.rs: next's missing-fonts test; the branch's side repeated control_bar_shows_its_second_row (next's newer copy stays) and key_names_sent_as_text_are_dropped, whose IME version replaces next's copy in place
The Type tool shows an input method's marked text in the story as a
preview of text.insert, underlined (the converting clause twice), with
the candidate window at the caret. The commit is one text.insert and one
undo step; a cancelled composition leaves the document as it was. Keys,
shortcuts, the clipboard and Undo wait while the IME composes; a click,
a tool or document switch, or any journaled command keeps the marked
text as typed. On macOS the app tells the input context to drop marked
text the tool kept.
With a split window, only the pane last clicked takes keyboard events
(both panes saw them, so typing went in twice).
The web build drops only real key names from text events, and nothing
while its text input has the keyboard, so autocompleted words survive.
The control channel's ui.ime, the headless MCP backend's ui.ime and the
MCP ime tool send IME events.
The layout-parity files (ROADMAP, compose lib/tests, engine text, fonts fontdb, idml import/tests, canvas) resolve as in the merge of main into feat/layout-parity.
Conflict: ROADMAP.md: as feat/layout-parity's merge
Conflict: crates/compose/src/lib.rs: as feat/layout-parity's merge; this branch's trimmed trailing spaces follow main's warichu::place and come before the drop cap splice
Conflict: crates/compose/src/tests.rs: both sides' tests
Conflict: crates/engine/src/cmd/text.rs: as feat/layout-parity's merge; this branch's TypingFormat/typing_format and main's warichu_cmd
Conflict: crates/fonts/src/fontdb.rs: this branch's styles/traits_of/linked_style over style_traits; style_traits falls back to the family without main's format suffix (exact_style_traits)
Conflict: crates/idml/src/import.rs, crates/idml/src/tests.rs, crates/ui-egui/src/canvas.rs: as feat/layout-parity's merge
Conflict: crates/ui-egui/src/control.rs: main's page_index and automation_contract_tests, then this branch's tests module
Conflict: crates/ui-egui/src/lib.rs: this branch's ClipboardFn and main's Inbox of ImportedFile
Conflict: crates/ui-egui/src/menus.rs: main's Quick Apply tests, then this branch's copy and paste keys test
Conflict: crates/compose/src/tests.rs: tests only, both (#310's tight word spacing test, #316's GREP style test)
#316's overlay test calls overlays() with the drop_end argument next added (#83).
One shared compiler (designcraft_compose::grep) translates the GREP dialect
documents and users write: \l \u \L \U letter classes, \h \H \v \V \R \N,
\r paragraph end and \n forced line break, ~ special-character codes,
Unicode-aware POSIX classes, and literal [ & -- inside character classes.
It compiles with fancy-regex, so lookahead, lookbehind, atomic groups,
possessive quantifiers, backreferences and \K work; a backtrack limit stops
pathological patterns with an error.
GREP styles skip a pattern that fails to compile or gives up on a search,
with one log warning. Find/Change returns the error. The paragraph style
dialog validates with the same compiler. Fixes en dash (~=) mapping to an
em dash in Find/Change.
Upstream main moved 9e4f694 -> 736acff by 10 squashes: #258, #285, #153, #280, #288, #281,
#176, #299, #298, #297. New to next: #258 (datamerge test), #153 (temporary files and release
inputs), #280 (warichu), #176 (PDF image conversion cache), #297 (graphics backend before the
window). Already on next: #285, #288, #281, #299, #298.
Conflict: ROADMAP.md: next's lines; CJK1 takes main's wording (ruby unfinished, warichu set after the line is broken)
Conflict: crates/compose/src/lib.rs: list labels (#298): next's list_label structure; paragraph start / break moves: next's `go` (with #295's sub-columns) under the maintainer's #285 comment; Cursor: next's start/next_page/next_baseline with tops; line placement: #280's warichu::place right after layout_line, before next's trimmed trailing spaces and the drop cap splice (so neither is stacked and the spaces follow the closed-up end_x); caret(): next's drop-cap branch first, then #280's stacked-row metrics
Conflict: crates/compose/src/tests.rs: next's tests kept; #280's tests auto-merged
Conflict: crates/engine/src/cmd/text.rs: both (next's TypingFormat/typing_format, #280's warichu_cmd)
Conflict: crates/idml/src/tests.rs: next's side; #280's warichu round-trip test auto-merged
Conflict: crates/pdf/src/export.rs: #176's content cache and test counter with next's RasterMode (no Exporter `interpolate` field on next, so the maintainer's `mode.interpolate &= self.interpolate` is not carried); test exporter gets next's drawing_items field
Conflict: crates/ui-egui/src/canvas.rs: #280's compose::highlight_quads (its RTL branch equals next's mixed-direction spans) followed by next's drop-cap highlight
Conflict: crates/ui-egui/src/menus.rs: next's formatting_affects_text test kept
Auto-merge duplicates removed: #288's table-style tests and #298's list test in idml tests.rs,
#288's tests in doc styles.rs and #285's docx test (both files equal next's), #281's dialog
tests in dialogs.rs (equals next's) and its menu_button/menu_max_height/clipped_submenu in
menus.rs. contributors/people.toml takes main's (next had a duplicated
[people.emanuelebonaventura] table from the 9e4f694 merge, which broke the ui-egui build
script's TOML parse).
Maintainer edits carried over: #285 break characters and Next Column through one closure
(next's `go`, comment added); #281 viewport-aware menu_button in the app bar and the Paragraph
menu, per-dialog widths clamped to the window (all already on next); #280 warichu after the
last-line rules (layout_line with ends_para/forced_mid) and rules at each warichu row's
baseline (rule_baseline in render and pdf text_rules); #176 CMYK fallback through
lossless(.., mode) and its updated contract tests; #258 test fixes; #153 and #297 as landed.
* Desktop: pick the graphics backend before the window exists; DirectX 12 on Windows
On an AMD hybrid-graphics laptop (Radeon RX 6800M beside an integrated
Radeon, with two versions of amdvlk64.dll loaded into one process) the
app died a few seconds after launch with an access violation inside
AMD's Vulkan driver at the first swapchain present. Nothing reached the
log: a fault inside a driver is a native crash the engine's panic guard
never sees. wgpu's default backend set (PRIMARY | GL) enumerates Vulkan
first, so every start on that machine went down the same path, while
WGPU_BACKEND=dx12 and WGPU_BACKEND=gl ran fine and the headless CPU
renderer was never affected.
The desktop app now chooses the backend before creating the window
(apps/designcraft/src/gpu.rs): DirectX 12 first on Windows, then Vulkan,
then OpenGL; Vulkan then OpenGL on Linux; Metal on macOS. OpenGL stays
in the set as the in-process fallback when the chosen backend has no
adapter. WGPU_BACKEND still overrides everything and turns the fallback
off.
Start-up fallback: gpu.json in the settings directory records the
backend being tried until the third frame begins (the first has been
presented by then) or the app exits normally. A record left behind means
that backend never showed a frame, so the next start skips it, tries the
next candidate and says so in the status bar. The list starts over when
every candidate has failed or the app version changes;
DESIGNCRAFT_NO_PREFS records nothing. The adapter, backend and driver
version are logged at start-up for bug reports.
Verified on the affected machine: a default start runs on DirectX 12; a
simulated crashed DX12 start moved to Vulkan, which faulted for real,
and the start after that came up on OpenGL with the status line. Nine
unit tests cover the chooser and the record; cargo xtask ci passes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Merge origin/main; on Windows OpenGL isn't added to the DX12 or Vulkan instance (#167)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A break character is stored as the last character of the paragraph it
ends, followed by the usual '\n' separator. The story model, paragraph
ranges, styles, editing and every exporter keep splitting paragraphs on
'\n' only, so the change stays in the places that create, lay out or
write break characters:
- IDML import writes the break and ends the paragraph, so the range that
follows keeps its own paragraph style. NextOddPage and NextEvenPage map
to new odd and even page break characters; IDML export writes a
paragraph that ends in a break as that one Br with its
ParagraphBreakType, so export and re-import give the same story.
- Word and RTF import, Type > Insert Break Character (with odd and even
page breaks added) and typing end the paragraph after a break. Typing
between a break and its return goes to the next paragraph; deleting
either deletes both.
- Layout: a break that ends a paragraph leaves no empty line; the next
paragraph starts in the next column, frame or page, as a paragraph start
option would. Page breaks and Next Page start options move to the next
frame on a later page; odd and even ones to the next page with that
page number parity (no such frame: overset). A break inside a paragraph
from an older document still moves the rest of that paragraph on.
Storing the break as a paragraph separator of its own would have changed
every place that counts or splits paragraphs on '\n'.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Landed: #119#294#144#199#248#222#223. #119#144#199#248#223 were
already on next; #222 (Spanish hyphenation) and #294 (Ukrainian catalog
test) are new here and come in whole. Each squash equals its landed PR
head (fetched to refs/remotes/pr-landed/N).
Conflict: contributors/people.toml: next's side (keeps emanuelebonaventura between moppeeer and thaynej)
Conflict: crates/doc/src/styles.rs: next's side (next's cell/table style tests)
Conflict: crates/pdf/src/export.rs: next's side in both hunks (no Exporter `interpolate` field on next; `let i = mode.interpolate` kept once)
Conflict: crates/ui-egui/src/dialogs.rs: confirm: next's `paragraphRules` arm; basic_character_formats: upstream's side (maintainer's font menu, see below)
Conflict: crates/ui-egui/src/i18n.rs: next's translated "Tint:" row
Conflict: crates/ui-egui/src/lib.rs: next's side (keeps ClipboardFn)
Conflict: crates/ui-egui/src/widgets.rs: next's side (next's numeric field grid test)
Maintainer edits in the landed PR heads:
- #119 (bfe02c9): inbox and PDF page dialog imports go through open_file; import_file uses opens_as_document. Already on next.
- #144 (fa5b942): paragraph_style_options reads a cloned doc; already on next. Basic Character Formats' Font Family is a ComboBox over font_menu_body (blank row clears a sparse family, "—" when unset). Taken from upstream in place of next's font_combo + "×" button.
- #199 (7afa9b7): one Properties test module with both tests. Already on next.
- #248 (856a432): people.toml keeps moppeeer and thaynej. Already on next.
- #222 (ac9b68d): ROADMAP Language line combines #199's "Properties ▸ Character" with #222's Spanish hyphenation note. Comes in with #222.
- #223 (c3d8a07): `mode.interpolate &= self.interpolate` and the decode check in lossless. The decode check is on next; the interpolate line is not, because next has no Exporter `interpolate` field and RasterMode::for_standard already turns interpolation off for every standard, PDF/A included.
Text set in Spanish was not hyphenated at all. It now uses Javier
Bezos's Liang patterns, hyph-es.tex 5.0 from hyph-utf8 under MIT,
through the hypher crate 0.1.8 with only its alloc and spanish
features. The full feature, which embeds 48 languages under several
licences, stays off.
- hyphen::Lang names the hyphenator for a run: English, or a hypher
language. Lang::for_language maps a language name or locale code.
- word_breaks_in and hyphen_points_in take the language. word_breaks
and hyphen_points stay as the English entry points.
- Composition hyphenates a non-English run whose language has
patterns and skips the rest. The word cache key includes the
language.
- NOTICE carries the Bezos copyright line and the MIT text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #89: preserve browser import context and PDF options
* #91: reject PDF pages before narrowing on Wasm
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: crates/compose/src/lib.rs: start options, the break after the previous paragraph and a break inside a paragraph go through a local `go` that sends NextColumn to next's span-aware next_column (#276) and the rest to #285's Cursor::start; the in-paragraph break takes #283's `brk` (the line's last glyph); #283's ends_para uses is_break_char; Cursor keeps next's resume() and adds #285's start/next_page (#285's old next_baseline header dropped, next's is kept); the table branch is #285's
Conflict: crates/idml/src/tests.rs: tests only; kept both
Conflict: crates/textimport/src/docx.rs: tests only; kept both
A break character is stored as the last character of the paragraph it
ends, followed by the usual '\n' separator. The story model, paragraph
ranges, styles, editing and every exporter keep splitting paragraphs on
'\n' only, so the change stays in the places that create, lay out or
write break characters:
- IDML import writes the break and ends the paragraph, so the range that
follows keeps its own paragraph style. NextOddPage and NextEvenPage map
to new odd and even page break characters; IDML export writes a
paragraph that ends in a break as that one Br with its
ParagraphBreakType, so export and re-import give the same story.
- Word and RTF import, Type > Insert Break Character (with odd and even
page breaks added) and typing end the paragraph after a break. Typing
between a break and its return goes to the next paragraph; deleting
either deletes both.
- Layout: a break that ends a paragraph leaves no empty line; the next
paragraph starts in the next column, frame or page, as a paragraph start
option would. Page breaks and Next Page start options move to the next
frame on a later page; odd and even ones to the next page with that
page number parity (no such frame: overset). A break inside a paragraph
from an older document still moves the rest of that paragraph on.
Storing the break as a paragraph separator of its own would have changed
every place that counts or splits paragraphs on '\n'.
InDesign documents and templates (.indd, .indt) open through File › Open,
drop, the web inbox, file.open, file.openBytes, file.openIdml and the CLI.
The engine converts them to IDML in memory with indd-utils
(github.com/aiongg/indd-utils, rev 9cf456d, MIT OR Apache-2.0, no
dependencies, builds for wasm) and imports the result through the existing
IDML path. The converter's warnings come first in the open result's
`warnings`. A file the converter can't read is an open error ("can't
convert the InDesign file: ..."), shown in the Can't Open the File alert.
Routing: a path or name ending in .indd/.indt, or bytes starting with the
INDD signature (the web inbox passes the name without its extension).
opens_as_document and the desktop and web Open dialogs list indd/indt.
Tests use synthetic bytes only: signature-bearing junk, a zero-filled
.indd and a short .indt all fail as open errors with no document added;
a dropped unreadable .indd raises the open alert.
Conflict: crates/ui-egui/src/lib.rs: kept #119's drain_inbox, import_file and ImportRequest (and its Services.inbox doc); import_file's and confirm_pdf_import's file.place/file.openBytes go through #80's open_file, so a failure shows the alert; import_file uses #80's opens_as_document for layout files
Conflict: crates/ui-egui/src/dialogs.rs: kept #119's pdfImport arm (async confirm, target activation); its native file.place goes through #80's open_file
Trailing spaces and drop caps both join the placed line; hit testing checks
the drop cap first, then the closest line (hit_line). hit_char also picks
the drop cap's character under the point, so a double click on the cap
selects from it (the follow-up both PRs named).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with the CJK/Arabic work: trailing spaces follow the line
only in left-to-right paragraphs without right-to-left glyphs (layout_line
now takes the bidi info); style linking moves to ScopedFonts so the
document's own fonts count (FontDb delegates to scope 0); threading keeps
the receiving story's direction and Vertical Type, and frames split off by
threading or removed from a thread keep theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with the CJK/Arabic composition work: line metrics keep
the first-baseline tops and add the CJK alignment shift; the CJK line
reference adjustment applies before baseline-grid snapping; layout_line
takes the bidi info and the drop cap is spliced in after it; vertical text
is now the story's direction (FrameSpec::vertical); IDML insets use main's
validated inset_spacing over the object style chain; format's JSON upgrade
(vertical frames) and model upgrade (format v2 defaults) both run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Adds the craftrules standard (standards/contributors.md) to Help > About DesignCraft, which now
has tabs About · Contributors · Models (`help.about {tab}` selects one).
contributors/contributors.json is generated by craftrules/scripts/contributors.py and baked
into the binary by crates/ui-egui/build.rs as static tables (crates/ui-egui/src/credits.rs).
Each contributor line: @username, opt-in display/real name, PRs, commits, lines +/−/Δ,
binary assets +/−, first/last commit. Sortable by every column; alphabetical ignores
case and the leading @.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors docs: craftrules is two levels up (../../craftrules)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section
build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Contributors: regenerate (verified people.toml entries)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Port the CJK1 and AR1 changes from 3ecdb78 and de83cdc onto current main. Resolve translation and font-stack conflicts by retaining main localization and craft-fonts integration while preserving all typesetting code, tests, and documentation.
Validation: all six CI gates passed, including 616 tests and WASM checks.
- Double-clicking a text frame with the Selection tool edits its text. The
canvas counts presses itself (egui reports double clicks only on release,
so the press never saw them): 2 clicks select the word or run of spaces
under the pointer, 3 the line, 4 the paragraph, 5 the story
(`text.selectAt`; pointer events carry `clicks`).
- The caret moves past spaces typed at the end of a line: trailing spaces
follow the line undrawn instead of being dropped.
- Bold and Italic (Shift+Cmd+B / Shift+Cmd+I, B and I in the Character
panel) toggle the family's own styles per run, keeping the other trait
(Italic + Bold = Bold Italic). Formatting chosen at a caret applies to
what is typed next.
- Copy, Cut and Paste reach the system clipboard however they are chosen,
including from the native macOS menu; without a native menu the copy and
paste keys work for objects too.
- Formatting Affects Text (J, Tools panel) sends swatches, colours, tints,
None, Swap and Default to the text of the selected frames; the proxy and
the Swatches and Color panels show the text's colours.
Decode the merged composite of .psd/.psb files in designcraft-images
(public file format spec) instead of the psd crate, which mapped
channels by count: grayscale with extra alpha/spot channels rendered
yellow, blue or invisible, and RGB + one spot channel was read as RGBA.
Channels now map by colour mode (Bitmap, Grayscale, Duotone, Indexed,
RGB, CMYK via the uncalibrated CMYK formula, Lab; 8 and 16 bit; raw and
PackBits). The first extra channel is the alpha only when the layer
count is negative (also via Lr16/Lr32), un-matted from white; other
extra channels are ignored. Damaged or oversized files return None
without panicking. Drops the psd dependency and its panic paths.
A CMYK TIFF went into every PDF, PDF/X-4 included, as a DeviceRGB image:
load_image sent anything that wasn't JPEG, PNG, GIF or WebP through
designcraft_images::decode_rgba, and `image` decodes CMYK TIFFs to RGB. 100%
K came out as RGB black, which the press separates as four-colour black
(#35).
designcraft-images now reads a CMYK TIFF's ink values with the tiff crate
(the version `image` already uses): 8- or 16-bit, interleaved, opaque or
with unassociated alpha. PDF export writes them through krilla's CustomImage
as DeviceCMYK, as it does a CMYK JPEG, and leaves out the TIFF's own ICC
profile so the numbers go to the output intent's press unchanged. PDF/A
exports stay RGB. A CMYK TIFF this can't read (planar, premultiplied alpha,
another ink set, over 256 MiB) is still converted, now with a warning. The
screen keeps decoding to RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extend interface language selection with Chinese and Arabic translations
across menus, dialogs, panels, tooltips, and common application controls.
Keep English fallback for untranslated strings, return borrowed translation
text, and validate translation key uniqueness and language switching.
Bundle redistributable Noto CJK and Arabic fonts with OFL license files and
ASSETS.md attribution so native and WASM builds have portable glyph coverage.
Register Arabic regular and semibold families while retaining existing font
fallbacks for proportional and monospace controls.
Render Arabic labels using Unicode bidirectional runs and contextual shaping.
Wrap logical text at grapheme boundaries before ordering each visual line,
mirror neutral punctuation, preserve logical source text for accessibility,
and handle truncation, empty lines, and mixed Latin text and numbers. Keep
ASCII labels on the normal rendering path. Disable selection on shaped RTL
labels because egui's default label cursor mapping assumes LTR text.
Adapt welcome controls, menu ordering, popup alignment, and new-document rows
for RTL layout. Bound popup geometry and avoid unsupported RTL grid behavior;
keep numeric inputs LTR. Isolate user-provided document names from Latin
application and zoom metadata. Interface language changes leave document
binding, paragraph direction, editable text, and engine state unchanged.
Add regressions for contextual Arabic joining, bidirectional punctuation,
mixed document names, logical wrapping, truncation, language switching, and
finite widget geometry. Update the roadmap with localization coverage.
Apply equivalent chunk-iteration and PDF Option/borrow simplifications needed
by the current Rust toolchain's strict Clippy checks.
Validation:
- cargo xtask ci: fmt, Clippy, native tests, assets, and layers passed;
native tests total 517 passed and 2 ignored.
- UI regression suite: 26 passed.
- Headless UI screenshots verified Arabic welcome/new-document creation,
menus, mixed-script document names, and switching back to Chinese/English.
- Default WASM gate is blocked by this host's missing precompiled wasm32
standard library. All 15 WASM crates pass using the installed Rust sources:
RUSTC_BOOTSTRAP=1 CARGO_UNSTABLE_BUILD_STD=std,panic_abort
target/debug/xtask wasm.
Fixes the Windows installer: the Start Menu shortcut and Add/Remove Programs icon were blank, and the shortcut description was wrong. Release builds no longer open a console window behind the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>