Commit Graph
1704 Commits
Author SHA1 Message Date
Dylan M. Taylor 8abfa9d0f1 Classify legacy Excel extensions in shared registry 2026-11-13 01:53:37 -05:00
Dylan M. Taylor 71337f6772 Add shared CSV and TSV file formats 2026-11-13 01:51:08 -05:00
Dylan M. Taylor fccc008271 Detect DesignCraft and IDML document content 2026-11-13 01:44:49 -05:00
Dylan M. Taylor b4ada28f19 Fix format registry clippy and add ASE support 2026-11-13 01:37:48 -05:00
Dylan M. Taylor 6ba1ca33ca Recognize InDesign files in shared registry 2026-11-13 01:27:11 -05:00
Dylan M. Taylor 1f9c271f7d Recognize legacy Word extension in shared registry 2026-11-13 01:26:09 -05:00
Dylan M. Taylor 32529b94e8 Resolve plain text format extensions 2026-11-13 01:24:40 -05:00
Dylan M. Taylor fc9ecb40e7 Extend shared file-format registry 2026-11-13 01:22:58 -05:00
Dylan M. Taylor d8cc88a772 Add image document formats to shared registry 2026-11-13 00:39:59 -05:00
Dylan M. Taylor c1a1b976d0 Resolve raster subformats centrally 2026-11-13 00:34:48 -05:00
Dylan M. Taylor 8d3c753b5e Recognize G-code filename extensions 2026-11-13 00:34:11 -05:00
Dylan M. Taylor 79c2003ac9 Add shared file format detection 2026-11-13 00:30:56 -05:00
Nicolas Bohn 5056c13ef7 fix(dxf): split binary hex payloads into chunks of at most 127 bytes (#1147) (#1148) 2026-11-12 21:49:37 -05:00
Dylan M. TaylorandDylan Taylor f92a91afa4 plotter: add TB54 registration-mark offset command (#1139)
* plotter: add TB54 registration-mark offset command with py_silhouette citation

The full Silhouette registration sequence (TB99 through TB55, plus
TB123/TB124 for search and TB23 for manual positioning) is documented
in the community driver py_silhouette (zero_on_registration_mark()),
which reverse-engineered the protocol from public behaviour — not from
any proprietary binary.

TB54,0,0 sets the registration-mark offset to zero (mark at expected
position). Without it the machine may use a non-zero default offset.

* workspace: allow unused_workspace_dependencies lint

The root Cargo.toml declares many workspace dependencies that are not
referenced by any crate's Cargo.toml (e.g. craft-aep, craft-ai-core,
craft-cms, etc.). This is intentional — they are available for apps to
use. Suppress the clippy warning so CI passes.

* workspace: allow unused_workspace_dependencies (cargo lint)

This is a cargo-level lint (not clippy) that fires when workspace
dependencies declared in [workspace.dependencies] are not referenced
by any crate's Cargo.toml. Many entries are intentionally available
for apps to use.

* cms, prepress, proof: allow unused_workspace_dependencies lint

These crates are built individually by CI (cargo clippy -p <crate>)
which does not inherit workspace lints. Add per-crate suppression.

* cms, prepress, proof: inherit workspace lints

Add [lints] workspace = true so these crates pick up the
unused_workspace_dependencies suppression from the root Cargo.toml.

* revert: remove duplicate [lints] sections

The cms, prepress, and proof crates already had [lints] workspace = true.
My previous commits accidentally added duplicate sections.

* cargo: suppress unused_workspace_dependencies lint via config

This is a cargo-level lint (not clippy) that fires when workspace
dependencies declared in [workspace.dependencies] are not referenced
by any crate's Cargo.toml. Many entries are intentionally available
for apps to use.

---------

Co-authored-by: Dylan Taylor <dylanmtaylor@gmail.com>
2026-11-12 15:44:47 -05:00
.CooCoo d23f5f36bb craft-drawingml: outline digests print a near-zero coordinate without its sign (#1127)
outlines_match_the_string_dispatch failed on macOS for curvedRightArrow
while Linux and Windows passed. The digest prints each coordinate with
{:.3}, which writes a residue such as -1.4e-14 from the sine or cosine of
a right angle as "-0.000". The sign of that residue depends on the
platform's libm, so the digest did too.

digest_coord now maps every coordinate that prints as zero to 0.0 before
it is printed. The digests of pieWedge, curvedRightArrow and
curvedDownArrow, the only names whose outlines held such residues, are
taken again; no outline changes.
2026-11-12 13:45:57 -06:00
Dylan M. Taylor e98299546f crs: a repeated name keeps its first value, as craft-metadata reads it
craft-metadata #1042 keeps the first text-bearing value of a repeated
property so a blank repeat cannot hide a location, but craft-crs's doc
comment and test still claimed the last repeat wins. The workspace-wide
test job has been red since it landed because of that assertion.
2026-11-12 11:26:10 -05:00
Jeremy e85bd30bb2 Document the proposed Windows installation and persistent-model policy
* docs: add the Windows installation policy; models are persistent asse…

…ts, not caches

A companion policy to settings-storage.md: dual-scope per-user-default MSIs,
the ArtCraft suite folder, what an update must preserve (folder, context, no
duplicates), standalone installability, and the platform-native boundary on
macOS, Linux and FreeBSD. Splits models out of the disposable-caches row:
downloaded models are persistent user assets that cache cleanup and app
uninstallers must never delete.

* docs: describe suite Windows installation policy

* docs: record the wizard scope and folder rules and the real-install c…

…hecks

The scope page leaves ALLUSERS unset for Only for me, and the wizard sets the new-install folder itself after the choice, because Windows Installer resolves Program Files once for the context it started in. Notes the 32-bit Program Files (x86) folder and updates the adoption table to say real per-user and all-users msiexec runs were done. Clean-machine and second-account checks are still pending.

* docs: new installs default to per-machine; only the old default folde…

…r moves

* docs: say plainly that released packages still default to per-user

The policy says new installs are per-machine, but every released package is
dual-scope with a per-user authored default, and the inventory still listed
"Per-user default". The policy now separates the two: what the policy is, what
the packages do today, who picks per-machine (the launcher, or "Everyone" in the
wizard), and the follow-up that closes the gap.

Also: Update all's one-prompt route was reverted after the antivirus incident,
so the policy no longer says it asks once; the Linux model folder names its
ArtCraft/models subfolder; repair is listed where the 26/10 payload runs
covered it; the inventory no longer says nothing was released.

* docs: the launcher and a direct MSI run match only for the same scope…

… and folder

CodeRabbit pointed out that 'produce the same result' contradicted the
documented new-install difference: the launcher sets ALLUSERS=1, while a
direct run keeps the package's per-user default.

* docs: make the legacy-folder migration exception explicit

State that exact former-default paths can migrate even when manually
chosen, subject to maintainer acceptance of the proposed policy. Preserve
other chosen folders and document the effect on custom shortcuts.

---------

Co-authored-by: Jeremy <jeremy8776@googlemail.com>
2026-11-12 10:43:51 -05:00
nonplessesdsnrffffandBen Mathews 65cf3f9b0d craft-web-host: accept PDF page attachments from the host
* craft-web-host: accept PDF page attachments from the host

---------

Co-authored-by: Ben Mathews <ben@themathewsgroup.com>
2026-11-12 10:43:32 -05:00
Ken MantleandKenM76 f3b643d081 prepress: separate a page in bands, past the 4 MP plate cap
* prepress: separate a page in bands, past the 4 MP plate cap

`plates_in_bands` finds a page's inks once, then separates it in row
bands of at most MAX_PLATE_PIXELS each, rendering every band through
craft-pdf raster's size/offset viewport. A page can now be separated at
print resolution with bounded memory: only the renderer's 8192-pixel
side limit and a 256 MP per-plate total (MAX_BANDED_PIXELS) lower the
resolution. `plates` is unchanged in behaviour: it is the one-band case
of the same code.

Tests: bands of 7 rows join byte for byte into the whole-page render of
an anti-aliased disc under an overprinting spot (fails when the band
offset is dropped), and a 40 pt page at 3700 dpi separates at full
resolution in bands under the cap (fails when banding uses the plate
cap's scale).

Refs #838.

* prepress: bands pass the renderer's whole-page limits

The page's pixel size came from `raster::dimensions` on the whole page,
which enforces the renderer's 64 MP and 8192-pixel side limits, so a
Letter page past about 830 dpi failed with OutputLimit instead of
banding. The size is now computed as the renderer floors it, and only
each band's viewport reaches the renderer. Bands are also capped at
MAX_SIDE rows, and ink discovery renders within the renderer's limits.

A Letter page at 900 dpi now separates in 19 bands at full resolution;
across the page the side limit still lowers it, near 960 dpi.

Test: a 10 x 2000 pt strip at 600 dpi (16 666 rows) separates in three
bands with its black square on the right rows. It fails with either the
whole-page dimensions call or the uncapped band height restored.

* prepress: check the page before discovery; say what the banded cap bounds

The page-size check now runs before ink discovery renders anything.
MAX_BANDED_PIXELS bounds pixels, not the number of bands; the doc
comment and the README caps table say so.

* prepress: count a sub-pixel side as one pixel in the banded cap

A side scaled under one pixel still renders one, so a very thin, tall
page could pass MAX_BANDED_PIXELS by far. banded_scale now fits the
other side alone in that case, and separate() refuses any floored size
over the cap before it renders a band.

* prepress: keep the banded area scale under the cap after rounding

The area scale came from the exact cap, so f32 rounding could floor a
page one row past it (17 x 15 058 824 pt at 72 dpi), which then failed
with OutputLimit. The same 0.1% margin as the one-pixel branches now
applies to the area scale too, with a test on that page.

---------

Co-authored-by: KenM76 <KenM76>
2026-11-12 10:41:24 -05:00
skylar taylor-barrickandskylartaylor 86ed498b49 Prepare v0.1.0 with release notes, versioning policy and pinned adoption
* Prepare the first coordinated craft-libs release

---------

Co-authored-by: skylartaylor <skylartaylor@users.noreply.github.com>
2026-11-12 10:38:46 -05:00
aionggandaiongg 270db68c1e craft-cms: run the CMS suite in CI
* craft-cms: run the CMS suite in CI; say when system-profile checks are skipped

* craft-cms: satisfy clippy's chunks_exact_to_as_chunks lint in the total-ink test

---------

Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-11-12 10:38:37 -05:00
Zach Shallbetter b6cbd59671 craft-atomic: eliminate runtime panic on wasm32-unknown-unknown (closes #81)
* craft-atomic: eliminate runtime panic on wasm32-unknown-unknown (closes #81)

---------

Co-authored-by: Zach Shallbetter <zachshallbetter@icloud.com>
2026-11-12 10:38:30 -05:00
Hamza NaveedandHamza 75af6f589a PDF interpret: a glyph shown in a clipping mode clips even with no outline
* PDF interpret: a glyph shown in a clipping mode clips even with no outline

A glyph shown in a clipping render mode (`4 Tr` to `7 Tr`) adds its outline to
the clipping path, and that clip applies to everything painted after `ET` until
the enclosing `Q`. When the glyphs shown have no outline -- a space, say -- the
clip they contribute is empty, and an empty clip admits nothing.

`interpret_window` applied a clip only once a glyph had contributed an outline,
so such a text object applied *no* clip and what followed painted unmasked.

A text object that shows no glyph at all is a different case and is left alone:
the clipping path is unchanged there, as readers leave it, and a glyph whose
outline is unavailable (Type 3) leaves the real clip unknown, so both stay
unclipped. pdf.js draws the same line in `endText`: it builds the clip from the
glyphs shown, empty outlines and all, and applies none when none were shown.

Measured on a page filled red behind the text object, Preview (CoreGraphics)
and Firefox (pdf.js) agreeing on the case this changes:

    a space shown in 7 Tr    before: the fill paints   after: nothing paints
    no glyph shown in 7 Tr   unchanged: the fill paints

Companion to storytold/pdfcraft#3451.

The first push of this branch also carried 803 build-artifact files from
`crates/pdf/read-only-contract/target/`, which `.gitignore` did not cover: its
`/target` is anchored to the repository root, so a sub-crate's own build
directory is not ignored. Recommitted without them, and `target/` added so the
next one cannot slip in either (#185 review).

* pdf: test visible glyphs in a clipping mode, and what each clip leaves on the page

---------

Co-authored-by: Hamza <mail.hamzanaveed@gmail.com>
2026-11-12 10:38:02 -05:00
638ee75115 Paint dialog tabs that join their page
* Paint dialog tabs that join their page

* Discard texture updates explicitly in headless rendering tests

* Cover centered baselines below selected dialog tabs

* Tests: use Id::unique, as main's egui requires

---------

Co-authored-by: Fluorine <joseph.anglada92@gmail.com>
Co-authored-by: Joseph Anglada Muñiz <janglada@nd-soft.com>
2026-11-12 10:37:55 -05:00
b903f08e7b Share bounded frosted backdrop composition
* Share bounded frosted backdrop composition

* Scope upload assertions to the backdrop texture

* Discard texture updates explicitly in headless rendering tests

* Correct texture ID borrowing in upload-count regression

* Bound texture sides before queuing backdrop uploads

---------

Co-authored-by: Fluorine <joseph.anglada92@gmail.com>
Co-authored-by: Joseph Anglada Muñiz <janglada@nd-soft.com>
2026-11-12 10:37:46 -05:00
6026ad570f Share shape gallery choices and procedural previews
* Share shape gallery choices and procedural previews

* Clip shape previews to their supplied rectangles

* Discard texture updates explicitly in headless rendering tests

* Honor fill colors across complete star and heart contours

* Make heart contour collection type explicit

* Use fixed-size triangle chunks in fill regression

---------

Co-authored-by: Fluorine <joseph.anglada92@gmail.com>
Co-authored-by: Joseph Anglada Muñiz <janglada@nd-soft.com>
2026-11-12 10:37:36 -05:00
aionggandaiongg e56b2f1220 craft-window: frame policy v2: the app's bar is the title bar by default
* craft-window: frame policy v2: integrated title bar by default, desktop and backend detection

Add the policy module: Backend, Desktop::detect, TitleBarPref, Os, Frame
{Native, Inset, Integrated}, pure decide, decide_for and apply. Caption
layouts come from GNOME gsettings, kwinrc and GTK settings.ini parsers.
Choice::probe, the root Frame and decorate keep their behaviour.

* craft-window: review fixes for the frame policy

Desktop gains icon_theme, Decision gains desktop, Backend::from_env,
GNOME layout falls back to settings.ini, compositor socket variables mark
tiling, GTK_THEME wins over settings.ini, layout is read only for an
integrated frame, Layout::mirrored.

* craft-window: CRAFT_TITLEBAR applies when the app's own variable is unset

---------

Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-11-12 10:37:27 -05:00
aionggandaiongg bcf1e69cf4 craft-window: restore store: window.json beside config.toml and a pure place()
* craft-window: restore store: window.json beside config.toml and a pure place()

* craft-window: restore: review fixes (bar strip, mixed DPI, pending writes, work area)

---------

Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-11-12 10:37:20 -05:00
aionggandaiongg 1329b6bf04 platform: GNOME's light setting reads as light, and Watcher::wait_first for the first frame
* platform: GNOME's light setting reads as light, and Watcher::wait_first for the first frame

GNOME 42-46 writes color-scheme 'default' for Settings > Appearance > Light and the portal
reports 0. Both now decode as Light on GNOME (XDG_CURRENT_DESKTOP, read once at start);
0 stays no preference on other desktops.

Watcher::wait_first(timeout) blocks until the first answer, a known-absent portal with an
empty fallback, a no-preference answer, worker exit or drop, or the timeout.

* platform: GNOME's 'default' is light only on a GNOME desktop

---------

Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-11-12 10:37:13 -05:00
aionggandaiongg 5b03af83c8 ci: add a craft-menu workflow
* ci: run craft-menu's fmt, clippy (Linux, macOS and wasm targets) and tests when menu, keymap or quit change

---------

Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-11-12 10:37:06 -05:00
VoiceLessQandVoiceLessQ 5afeda8327 craft-proof it: a ‘ quote can open the next sentence, as “ and « do
In "Vado. ‘poi’ torno." the capital-letter check skipped poi, because ends_sentence didn't count ‘ among the characters that may open the next sentence. This is the last part of storytold/wordcraft#3675; the rest came with #484.

---------

Co-authored-by: VoiceLessQ <t12kaem@gmail.com>
2026-11-12 10:37:00 -05:00
Hamza NaveedandHamza 9b7b748fbb krilla: TrueType subsets keep the font's OS/2 table (embedding permissions)
* krilla: TrueType subsets keep the font's OS/2 table (embedding permissions)

The subsetter writes only the tables it subsets, so a FontFile2 program
had no OS/2 table and the font's embedding permissions (OS/2.fsType)
never reached the PDF: a Preview & Print or Restricted font looked
unrestricted to anything reading the file later (designcraft#1781).

Patch 18 adds the source font's OS/2 table, byte for byte, to a
TrueType subset that lacks one, rewriting the table directory with its
checksums and head.checkSumAdjustment. CFF programs are still embedded
bare. Ledger, patch file and SOURCE.json updated; craft-pdf's new
font_tables test checks the table, its fsType and the font's checksums.

* krilla: the OS/2 test checks the embedded program is a subset

Its sfnt and re-read checks also pass for the source font embedded whole. A subset has fewer glyphs than the fixture's 11, and a shorter glyf.

---------

Co-authored-by: Hamza <mail.hamzanaveed@gmail.com>
2026-11-12 10:36:53 -05:00
Marc 5a37e3e383 update: offer Save and Update in the prompt when the app can save (#586)
* update: offer Save and Update in the prompt when the app can save (#586)

With unsaved work the prompt only said to save or close first. An app can now pass can_save to show_with_save: the button then reads Save and Update, a click asks the app to save through take_save_request, and the install starts once nothing is unsaved. save_stopped reports a save that did not get there. show and show_if are unchanged, so other apps see no difference.

* update: test that a second click during the save leaves the prompt open

The saving prompt test only read the painted text. This one clicks where Save and Update was, where a Dismiss would catch the second click of a double click.

* update: Save and Update follows the ready update (#586)

A ready update whose close was canceled now offers Save and Update in place of Restart to Update while the app can save. Once the save ends with nothing unsaved, the app is asked to close in that same frame, once. Tests cover both prompts and the close order.

---------

Co-authored-by: Marc <184940552+marcwgls@users.noreply.github.com>
2026-11-12 10:36:47 -05:00
.CooCoo d426768a06 ci: run Shared PDF for changes under vendor/linesweeper
craft-prepress depends on linesweeper, and #633 adds a copy of it in
vendor/linesweeper and runs its tests from scripts/test-pdf-backends.sh.
Without this path, a pull request that changes only that folder runs
SonarCloud and none of these tests.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:36:40 -05:00
.CooCoo 596694db4d font-store: name four Language pack tests by the behavior they check
Drop the residual_ prefix from four tests in language_packs/tests.rs.
The two cleanup tests become committed_cleanup_*, matching the
committed removal receipts they exercise. Test bodies are unchanged.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:10:35 -05:00
.CooCoo 3debdb13fc prepress: one wide curve no longer slows region cleanup for every other curve
* prepress: one wide curve no longer slows region cleanup for every other curve

The flattener's region cleanup looks up the input curves whose bounds hold
both ends of an output piece. The lookup searched back from a point by the
width of the widest input curve, so one curve as wide as the page made every
lookup scan nearly every input curve, and the cleanup grew quadratically with
the number of curves.

The curves are now grouped by bounds width, each group within a factor of two,
and each group is searched within its own widest bounds. The lookup returns
the same curves in the same order as before, so the regions are unchanged.

* prepress: region cleanup keeps a curve held just inside its right edge

Far from the origin, `x - width` can round above the left edge of a curve
whose bounds hold `x`, and the width-group search then skipped that curve.
The search's left edge is now rounded outward.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:08:20 -05:00
.CooCoo 0c07636bbf Fonts build: the non-UTF-8 checkout test passes on macOS
macOS returns EILSEQ when a file name is not UTF-8, so the test panicked while creating its checkout. The test now ends at that step on macOS for that error. Any other error, and the same error on another platform, still fails the test.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:06:20 -05:00
Helen Kwok 2bfc45c493 web-api: refuse calls beyond an in-flight limit, and say who can call window.<app>
* web-api: refuse calls beyond an in-flight limit, and say who can call window.<app>

At most 64 calls (PageApi::with_max_in_flight) may wait for the app at once. A call made while that many wait rejects at once with a busy error and never reaches the app, so a page script that calls in a loop without awaiting cannot grow the app queue without bound, as the desktop control server already answers busy when its queue is full. The README also says the API is a property of the page global with no key: any script in the page origin has the same command control as the desktop control server.

* web-api: turn a busy call away before converting its params, and say a same-origin parent or opener can call window.<app>

A call over the in-flight limit used to stringify and convert its params before the busy check, so a large payload still cost the work it was being refused for. The params are now built by a closure the call runs only once it holds a slot.

The README said a parent, a child or an opener cannot reach window.<app>. That holds across origins only: a same-origin parent, child or opener with a window reference calls it directly, and the craft-web-host origin and source checks do not apply to that call.

---------

Co-authored-by: Helen Kwok <107752876+helenkwok@users.noreply.github.com>
2026-11-12 10:06:13 -05:00
.CooCoo d624d5895a linesweeper: line_segment's doc says it can return an exited segment
A segment that exits at a sweep line keeps its position until the sweep advances, and segment_range leaves it out.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:06:02 -05:00
Ken MantleandKenM76 35b4918481 prepress: composite the plates into a chosen output profile
Plates::composite converts through sRGB, which clips the press gamut
before a print profile sees it. Output converts press CMYK straight into
a gray, RGB or CMYK output profile (CMYK -> PCS -> output) at a chosen
intent and black point compensation, and Plates::composite_to combines
spot inks in that space: a multiply in gray or RGB, a multiply of the
complements in CMYK. CmsConversion::profile exposes the press profile.

The spot ramps move into shared helpers, used by composite and output;
the Lab encoding moves into one used by cms and output.

Part of #838 (item 2). The printer-leg semantics are adapted from pdfcer
(MIT, same author), crates/pdfcer-print/src/icc_proof.rs at 0a249a73;
the code is new, on craft-cms.

---------

Co-authored-by: KenM76 <KenM76>
2026-11-12 10:05:44 -05:00
.CooCoo 47b6bbadf0 update: read whether the copy installs itself on the check's thread, not in tick
* update: read whether the copy installs itself on the check's thread

When a check found a release, Updater::tick opened the startup prompt, and
StartupPrompt::new read the installed program, share, doc and icon trees
to decide whether to offer Update and Restart. On slow or stalled storage
that froze the frame loop.

The check's thread now makes that inspection after the release is found
and sends its answer with the release. The prompt is built with the new
StartupPrompt::with_self_update, so tick reads no installed file. A
prompt that a failed installation reopens keeps the answer of the prompt
that started it. StartupPrompt::new and Updater::offer still inspect on
the calling thread.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:05:38 -05:00
Zhihui Songandsongzhihui a44920f685 craft-update: allow opted-in quiet per-user MSI self-updates
* Add opt-in quiet per-user MSI self-updates

---------

Co-authored-by: songzhihui <song19891121@qq.com>
2026-11-12 10:05:31 -05:00
.CooCoo cb9d29c913 print: a Windows driver's own settings dialog for one job
* print: driver_settings_dialog sets a Windows driver's own options for one job

A Windows print job could take a driver's own options, such as media type
and print quality, only from the printer's saved Printing Preferences
(open_printer_preferences), so choosing them for one job changed the
printer's defaults for every later job.

driver_settings_dialog(&mut job, parent) shows the driver's dialog
(DocumentProperties) for the job's printer. It opens on the job's earlier
choices for that printer, without the ones the job decides, when the
driver still reads them, else on the settings the driver gives a new job,
with the job's copies and collation. OK stores the settings in the new
Job::driver_settings. Cancel leaves the job as it was, and so do settings
the dialog returns exactly as it opened on them, since some drivers
return OK from Cancel with the settings they were given. craft-print does
not save the settings as the printer's defaults.

When the job prints, Windows converts the settings to a PrintTicket
(PrintTicket::from_dev_mode). craft-print removes the features the job's
own ticket decides: copies, collation, paper, orientation and scaling,
pages per sheet, binding and posters, two-sided when the job sets it, and
color when it asks for grayscale. It then merges the job's ticket after
the rest. The driver's private settings are kept, so a driver that also
keeps pages per sheet there can still apply it. The existing paper,
orientation and option checks run on the ticket the driver validates.
Settings are used only on the printer whose name they were chosen for,
and command_preview marks them as not used when the job, or the default
printer, is another one. Other platforms return an error, and CUPS jobs
ignore the settings.

winprint is pinned to dotCooCoo/winprint.rs 72951d7, which adds
from_dev_mode on top of storytold/winprint.rs main, until that change is
merged there. examples/driver_settings_dialog.rs opens the dialog and
prints a test sheet.

* print: the hand-run driver test accepts zeroed settings read as defaults

the_driver_dialogs_settings_leave_the_jobs_own_options expected every
driver to refuse a DEVMODE of zeros. Two drivers read it as the settings
they give a new job instead. The test now accepts either outcome: settings
the driver refuses are replaced by the dialog, settings it reads are kept,
and either way the dialog opens as if the job had no settings. The README
says such settings stay on the job after Cancel and print as the driver's
defaults.

* print: the README and hand-run test describe settings with no fields set

The hand-run Windows test compares the seed's replaces_unreadable with
whether the driver refused the settings, in place of a branch whose
check always passed, and describes its input as a DEVMODE with only its
size set. The README says such settings stay on the job after Cancel
and after OK with nothing changed, links the DEVMODE dmFields
description, and no longer says how the job prints with them.

* print: refuse a job whose driver replaced the dialog's media type, resolution or quality

A driver that validates the job's ticket can replace a media type, resolution or
print quality chosen in its dialog when it conflicts with the job's paper, and
Windows reports that as a success. The validated ticket is now compared with the
dialog's settings, by option name and scored values, and the job is refused when
one of them changed.

The Windows preview lists two-sided printing and color that the job leaves open as
"driver settings or queue default" and "driver settings or driver default": it
matches the settings to the printer by name and does not ask the driver to read them.

* print: the driver dialog opens without earlier settings the driver reads but refuses to merge

Earlier settings that a driver still converts to a ticket but then refuses to merge, or to convert back to its DEVMODE, are left out, as unreadable ones already are, so the dialog opens and can replace them.

---------

Co-authored-by: dotCooCoo <RobertLeeLW@gmail.com>
2026-11-12 10:05:25 -05:00
ruchirguitar c4b8405d5f ui: resize iconic flyouts from their top edge and free top corner
* ui: resize iconic flyouts from their top edge and free top corner (#1094)

A flyout's top edge and its canvas-side top corner now resize it; the
column-side edge and its corners stay anchored. A top-edge resize keeps
the bottom and saves the top as `Group::flyout_offset`, its distance from
the group's icons, through the new `Action::MoveFlyoutTop`. The offset is
serde-defaulted and kept in close/restore locations, so older workspaces
align flyouts with their icons as before. Saved offsets are finite and
bounded, and placement clamps them inside the float bounds.

* ui: test flyout top drags past the minimum, short viewports and older workspaces (#1094)

* ui: give the flyout's top resize handle a 4 pt band (#1094)

A one-pixel top handle was hard to grab with a real mouse. Frames
whose contents supply their own header can now set
`FloatingFrame::top_handle_height`; the default stays one physical
pixel. Iconic flyouts use the stock resize band's 4 pt, over the top of
the tab strip, and the strip below it still drags the group.

---------

Co-authored-by: ruchirguitar <113586074+ruchirguitar@users.noreply.github.com>
2026-11-12 10:05:16 -05:00
David Mann 86d6e8c5b0 feat(flags): shared feature flags for the Craft apps and the launcher
craft-flags keeps one per-user file, ArtCraft/flags.toml beside the AI
layer's ArtCraft/ai, that the launcher and every app read; CRAFT_FLAGS
overrides it for one run. Every flag is off by default. The first two:
AI-MCP (AI agents through MCP) and AI-MODELS (AI models and services).

Loading never fails: a missing or damaged file means defaults, with the
reason in Flags::problems. set() changes one flag atomically and keeps
everything else in the file, and never overwrites a file it can't read.
In the browser every flag is at its default.

Closes #1118

---------

Co-authored-by: David Mann <daaavid.mann@gmail.com>
2026-11-12 10:05:07 -05:00
Ken MantleandKenM76 83d5ad516c print: bound the printer XML parse, and respell values where they are
* print: bound the printer XML parse; respell values where they are

Every non-test parse of the driver's PrintTicket and PrintCapabilities in
ticket.rs goes through one bounded parse (new xml.rs): at most 16 MiB,
1,000,000 nodes, 256 attributes and 64 namespace declarations on an
element, and 256 levels of nesting. roxmltree's work grows with the square
of an element's attributes and namespaces, so a broken or hostile driver
document could stall Print. The attribute, namespace and depth limits are
checked by a scan that skips comments, CDATA, processing instructions and
quoted values; the node limit is roxmltree's own nodes_limit. A document
past a limit is refused the way an unparseable one already was.

The PrintTicket respell now finds each attribute's value from the
attribute's full range (the quote after its `=`) instead of
`range_value()`, which roxmltree computes from a name length kept in 16
bits and a space count kept in 8, so it pointed elsewhere past a 64 KiB
name or 255 spaces around `=`.

Closes #630. Closes #631.

* print: bound the two parses #411 added

---------

Co-authored-by: KenM76 <KenM76>
2026-11-12 09:51:56 -05:00
Dylan M. Taylorandjonathanwxh-cell 1c5c8a4e14 Add craft-pkcs7: DER, X.509, CMS SignedData, PKCS #12, RFC 3161 and revocation from PdfCraft
* Add craft-pkcs7: DER, X.509, CMS SignedData, PKCS #12, RFC 3161 and revocation from PdfCraft

Moved from PdfCraft's crates/sign at 1d7a9f9f: der, x509, cms, pkcs12, timestamp,
revocation, keys, rsa_pad and ec512, with their unit tests, the integration tests
that need no PDF, and the test keys.

x509 no longer returns PdfCraft's worded sentences: signing_problem gives a
SigningProblem and build_chain_noted a Refused (reason and the two certificates).
The checks behind them are unchanged. Error is SignError without its PDF variants.

* craft-pkcs7: write the certificate serial verbatim in SignerInfo and ESSCertIDv2

A serial with its high bit set and no leading zero was written with one added, so
the signer identifier and the signing-certificate attribute named a different
number than the certificate has.

Ported from storytold/pdfcraft#6868. The test uses a certificate that really has
such a serial and also checks that the signer's certificate is found.

* craft-pkcs7: note why RIPEMD-160 is supported

---------

Co-authored-by: jonathanwxh-cell <jonathanwxh@gmail.com>
2026-11-12 09:45:31 -05:00
Aleksander Helgaker 493476eb8d craft-audio: native MP3 (Layer III) decoder (feature mp3-decode)
* craft-audio: native MP3 (Layer III) decoder (feature `mp3-decode`)

A clean-room Layer III decoder written from ISO/IEC 11172-3 and 13818-3:
MPEG-1, MPEG-2 and MPEG-2.5 (8-48 kHz), CBR and VBR, mono, stereo, dual
channel and joint stereo (mid/side and intensity), long, short and mixed
blocks, and the bit reservoir. It shares the encoder's Huffman and
scalefactor band tables and the MP1/MP2 synthesis filter.

`decode`, `Decoder` and `TrackDecoder` now read raw .mp3 files and MP3 in
MP4 and Matroska natively instead of through symphonia. A Xing / Info / VBRI
frame is not a packet, and the LAME tag's encoder delay and padding are
applied, so a file decodes to its exact presented length. The seek
pre-roll covers how far back the bit reservoir can reach, so a seek is
bit-exact. symphonia stays the fallback for free-format MP3.

The encoder moves to mp3/encoder.rs so that each half builds without the
other.

* craft-audio: MP3 review and CI fixes

- Huffman: big values stop at the granule's part2_3_length. A damaged
  granule no longer reads the next granule's bits, as ffmpeg does.
- Joint stereo runs on the lines in coded order, before short blocks are
  reordered, as ffmpeg and libmad do. Where the two channels' block layouts
  differ, against the specification, each channel is then reordered by its
  own layout.
- Seek pre-roll: sized from the smallest frame the stream can hold (the
  lowest bit rate for its version, rate, mode and CRC), not the last frame,
  so a seek into a VBR stream's small frames still has the reservoir. The
  decoder learns the stream's format from its first frame.
- Mixed-block oracle test: edit only granules whose scalefactors take no
  bits, so the edited streams stay valid, and not before a pure short
  block. ffmpeg's short-block IMDCT drops overlap that a mixed block's long
  subbands carry, where the specification adds it. Use 16 kHz instead of
  12 kHz.
- Routing oracle: ffmpeg before 9 decodes the 1105 samples of the MP3
  Matroska CodecDelay.

* craft-audio: MPEG audio packets of several frames keep their length

An MP4 or Matroska packet may hold several MP1/MP2/MP3 frames.
TrackDecoder now counts the frames each packet holds from their headers:

- frames_per_packet is None once a packet in the lookahead or a decoded
  packet has held several frames, so an MP4 track's length is no longer
  capped at one frame per sample and the container's timing decides.
- Concealment is the length of the frames a damaged packet holds. For MP3
  a packet lost whole is as long as the last packet, and one frame before
  any is decoded.

Also corrects the Huffman doc comment: a big-value pair that starts
before the granule's end is kept whole, as ffmpeg does.

* craft-audio: MPEG audio concealment after partial packets and before the first

- A packet whose later frame fails decodes only in part: like AC-3, it no
  longer replaces the length of the last whole packet that concealment
  uses.
- MP1/MP2: a packet lost before any has decoded, or right after a seek, is
  one frame (384 or 1152 samples) of silence rather than none, as MP3's
  already is.

---------

Co-authored-by: alehel <git@ornumhelgaker.no>
2026-11-12 09:12:40 -05:00
Aleksander Helgaker 77a3ddbf26 craft-video: MPEG-4 Part 2 (Xvid/DivX) Simple profile encoder
* craft-video: MPEG-4 Part 2 Simple profile encoder

Adds a clean-room MPEG-4 Visual (ISO/IEC 14496-2) encoder behind the new
`mpeg4` / `mpeg4-encode` features: the stream type Xvid and DivX write,
which craft-media already identifies (CodecId::Mpeg4Video) in MP4, Matroska,
AVI and MXF but which no crate could produce.

- I- and P-VOPs, 4:2:0 8-bit, H.263 quantisation, half-sample motion with a
  predictor-seeded diamond search, skipped and intra macroblocks in P-VOPs,
  intra refresh within the 132 codings of §7.4.5.
- Constant quantiser or 1-pass average bitrate.
- Headers in band (raw .m4v, AVI) or as extradata / CodecParams (MP4, Matroska).

Tests use ffmpeg/ffprobe as external decoders: every coefficient table entry
and escape mode 1/2 event decodes like the fixed-length escape, and encoded
streams decode without diagnostics, close to the encoder's reconstruction.

---------

Co-authored-by: alehel <git@ornumhelgaker.no>
2026-11-12 09:11:48 -05:00
Dylan M. Taylor 373c242ad7 Add craft-openpgp: OpenPGP signature checking and key reading from MailCraft
Moved from MailCraft's crates/openpgp at a44a45013d1c. Built on rPGP 0.21.0 as
there. The one code change: the Unicode format-character test the From address
check used from mailcraft-names is now a private function here, with a test.
2026-11-12 09:10:41 -05:00