From b7568ef30792095159052b0899e6f212c5ad6886 Mon Sep 17 00:00:00 2001 From: midasdf <205497066+midasdf@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:02:49 +0900 Subject: [PATCH 1/3] Print: Japanese photo and postcard paper sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Japanese photo printing and New Year cards cannot pick their paper: the Print page setup only offers the ISO and US sizes. Add the customary papers at their exact metric sizes, after the existing entries (whose sizes and order are unchanged): L判 89×127 mm, 2L判 127×178, KG判 102×152, はがき 100×148, 往復はがき 148×200, 六切 203×254, 四切 254×305 and 名刺 55×91 (portrait). Names resolve case-, space- and punctuation-insensitively, and take the Japanese customary names as aliases (L判, Lサイズ, L-size, 2L判, KG判, はがき/ハガキ/葉書/ポストカード, 往復はがき, 六切/6切, 四切/4切, 名刺). The Print dialog and `print.set`/`print.get` use the same names, the labels show the customary name with its metric size, and the PDF page size and MediaBox follow the paper. Tests: the layout test pins every new size to 0.1 pt, the aliases, and the unchanged ISO/US sizes and unknown-name rejection; the Print view test checks the labels; and an engine test exports a PDF on はがき (283.46×419.53 pt) and L判 (252.28×359.99 pt) at 254 ppi (ten pixels per millimetre, so the MediaBox is exact) and reads the actual page size, plus the customary name through print.set. Docs: ROADMAP progress log + revision history and header, docs/gaps.md (Gap 15 revision), docs/parity-checklist.md (LRC-PRINT-LAYOUT row), dated 2026-10-22; existing revision rows keep their dates. --- ROADMAP.md | 8 +- crates/engine/src/print/layout.rs | 118 +++++++++++++++++++++++++++++- crates/engine/src/tests_print.rs | 30 +++++++- crates/ui-egui/src/print_view.rs | 15 ++++ docs/gaps.md | 3 +- docs/parity-checklist.md | 5 +- 6 files changed, 171 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f142cd226..4c75b8e6a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~14 points and ~340–600 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (RAW: 83% of Adobe's models decode, 38% verified after the camera-coverage sweep #1327) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (Print: Japanese photo and postcard paper sizes — L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 One-page summary of how close LightCraft is to Adobe Lightroom. Agents: read this, then pick work from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are in @@ -114,6 +114,11 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/ ## Progress log +- 2026-10-22: Print page setup and PDF output gain the Japanese photo and postcard papers — L判 + (89 × 127 mm), 2L判 (127 × 178), KG判 (102 × 152), はがき (100 × 148), 往復はがき (148 × 200), + 六切 (203 × 254), 四切 (254 × 305) and 名刺 (55 × 91) — at their exact metric sizes. The Print dialog + and `print.set` take their customary names, and names are matched case-, space- and + punctuation-insensitively (`L判`, `l`, `L-size` are one paper). - 2026-10-14: Camera-coverage sweep verifies 30 more models on 40 CC0 files, with file and sensor hashes pinned (`docs/raw/coverage.py`: 515 → 545 verified). Eleven candidates excluded after rechecking identity, provenance and camera-image agreement. Phone and drone DNG tone differences remain tracked in #1277. @@ -183,6 +188,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/ | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Print: Japanese photo and postcard paper sizes (L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺) in the page setup, Print dialog and PDF output; headline numbers unchanged | | 2026-10-12 | minor | Italian interface (`it`, #1192): 2,201 of 2,398 messages, locale, menu command and tests; headline numbers unchanged | | 2026-10-13 | minor | Sony ILCA-99M2 camera colour profile in the log; numbers unchanged | | 2026-10-14 | minor | Localization ~52% → ~55% (mean of the 12 key languages): de, es, fr, ja and uk catalogs filled | diff --git a/crates/engine/src/print/layout.rs b/crates/engine/src/print/layout.rs index 26d1818e5..c267c0107 100644 --- a/crates/engine/src/print/layout.rs +++ b/crates/engine/src/print/layout.rs @@ -46,6 +46,41 @@ const PAPER: &[(&str, f64, f64)] = &[ ("5x7", 5.0, 7.0), ("8x10", 8.0, 10.0), ("13x19", 13.0, 19.0), + // Japanese photo printing and postcard sizes, at their exact metric dimensions (the US photo + // sizes above are close but not identical: L is 89 x 127 mm, 5x7 in is 127 x 177.8 mm). + ("l", 89.0 / 25.4, 127.0 / 25.4), + ("2l", 127.0 / 25.4, 178.0 / 25.4), + ("kg", 102.0 / 25.4, 152.0 / 25.4), + ("postcard", 100.0 / 25.4, 148.0 / 25.4), + ("double-postcard", 148.0 / 25.4, 200.0 / 25.4), + ("6cut", 203.0 / 25.4, 254.0 / 25.4), + ("4cut", 254.0 / 25.4, 305.0 / 25.4), + ("business-card", 55.0 / 25.4, 91.0 / 25.4), +]; + +/// Japanese customary names (and a few English spellings) for the papers in [`PAPER`], matched +/// after [`normalize_paper`]: `L判` is `l`, `はがき` is `postcard`, `六切` is `6cut`, and so on. +const PAPER_ALIASES: &[(&str, &str)] = &[ + ("L判", "l"), + ("Lサイズ", "l"), + ("L-size", "l"), + ("2L判", "2l"), + ("2Lサイズ", "2l"), + ("2L-size", "2l"), + ("KG判", "kg"), + ("KGサイズ", "kg"), + ("KG-size", "kg"), + ("はがき", "postcard"), + ("ハガキ", "postcard"), + ("葉書", "postcard"), + ("ポストカード", "postcard"), + ("往復はがき", "double-postcard"), + ("往復ハガキ", "double-postcard"), + ("六切", "6cut"), + ("6切", "6cut"), + ("四切", "4cut"), + ("4切", "4cut"), + ("名刺", "business-card"), ]; /// Names accepted by [`PageSetup::paper`] (besides `custom`). @@ -53,10 +88,34 @@ pub fn paper_names() -> impl Iterator { PAPER.iter().map(|p| p.0) } -/// Portrait size in inches of a named paper (case-insensitive), `None` for unknown names and `custom`. +/// The form [`paper_size`] matches names in: lower case, with whitespace, hyphens, underscores and +/// parentheses ignored and `×`/`*` read as `x`, so `L判`, `l-size` and `l` are one name and `4 × 6` +/// is `4x6`. +fn normalize_paper(name: &str) -> String { + name.chars() + .filter(|c| !c.is_whitespace() && !matches!(c, '-' | '_' | '(' | ')')) + .map(|c| match c { + '×' | '*' => 'x', + c => c, + }) + .collect::() + .to_lowercase() +} + +/// Portrait size in inches of a named paper, `None` for unknown names and `custom`. Case, spaces +/// and punctuation don't matter, and the Japanese customary names (`L判`, `2L判`, `KG判`, `はがき`, +/// `往復はがき`, `六切`, `四切`, `名刺`) are aliases of the papers above. pub fn paper_size(name: &str) -> Option<(f64, f64)> { - let name = name.to_ascii_lowercase(); - PAPER.iter().find(|p| p.0 == name).map(|p| (p.1, p.2)) + let n = normalize_paper(name); + if n.is_empty() { + return None; + } + let canonical = PAPER + .iter() + .find(|p| normalize_paper(p.0) == n) + .map(|p| p.0) + .or_else(|| PAPER_ALIASES.iter().find(|(alias, _)| normalize_paper(alias) == n).map(|(_, name)| *name))?; + PAPER.iter().find(|p| p.0 == canonical).map(|p| (p.1, p.2)) } /// An axis-aligned rectangle in inches. @@ -935,6 +994,59 @@ mod tests { assert!(p.size().is_err()); } + /// Japanese photo and postcard papers: exact metric sizes to 0.1 pt, the customary names as + /// aliases, and the ISO/US papers around them unchanged. + #[test] + fn japanese_photo_and_postcard_papers() { + let pt = |name: &str| { + let p = PageSetup { paper: name.into(), ..Default::default() }; + p.size_points().unwrap_or_else(|e| panic!("{name}: {e}")) + }; + for (name, w_mm, h_mm) in [ + ("L判", 89.0, 127.0), + ("2L判", 127.0, 178.0), + ("KG判", 102.0, 152.0), + ("はがき", 100.0, 148.0), + ("往復はがき", 148.0, 200.0), + ("六切", 203.0, 254.0), + ("四切", 254.0, 305.0), + ("名刺", 55.0, 91.0), + ] { + let (w, h) = pt(name); + let want = (w_mm / 25.4 * 72.0, h_mm / 25.4 * 72.0); + assert!((w - want.0).abs() < 0.1 && (h - want.1).abs() < 0.1, "{name}: {w} x {h} pt, wanted {want:?}"); + assert!((w - h).abs() > 1e-6, "{name} is portrait"); + } + // The customary and English spellings resolve to the same paper, including spaces and case. + for (a, b) in [ + ("L判", "l"), + ("L判", " L "), + ("L判", "L-size"), + ("2L判", "2l"), + ("KG判", "kg"), + ("はがき", "postcard"), + ("はがき", "ハガキ"), + ("往復はがき", "double-postcard"), + ("六切", "6cut"), + ("六切", "6切"), + ("四切", "4cut"), + ("名刺", "business-card"), + ("A4", "a 4"), + ("4 × 6", "4x6"), + ] { + assert_eq!(paper_size(a), paper_size(b), "{a} vs {b}"); + } + assert!(paper_size("poster").is_none()); + // The papers that were already there keep their sizes. + assert_eq!(pt("letter"), (612.0, 792.0)); + assert_eq!(pt("4x6"), (288.0, 432.0)); + assert_eq!(pt("5x7"), (360.0, 504.0)); + assert_eq!(pt("8x10"), (576.0, 720.0)); + assert_eq!(pt("13x19"), (936.0, 1368.0)); + let (w, h) = pt("A4"); + assert!((w - 595.28).abs() < 0.01 && (h - 841.89).abs() < 0.01, "A4 {w} x {h}"); + } + #[test] fn hostile_numbers_are_rejected() { for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0, 1e9] { diff --git a/crates/engine/src/tests_print.rs b/crates/engine/src/tests_print.rs index 898faa889..7ea595ea4 100644 --- a/crates/engine/src/tests_print.rs +++ b/crates/engine/src/tests_print.rs @@ -26,7 +26,13 @@ fn get_set_and_reset() { assert_eq!(g["pageWidth"], 8.5); assert_eq!(g["printable"]["x"], 0.25); assert_eq!(g["settings"]["job"]["ppi"], 240.0); - assert!(g["papers"].as_array().unwrap().iter().any(|p| p["name"] == "a4")); + let papers = g["papers"].as_array().unwrap(); + assert!(papers.iter().any(|p| p["name"] == "a4")); + // The Japanese photo and postcard papers are listed with their metric sizes in inches. + let paper = |name: &str| papers.iter().find(|p| p["name"] == name).cloned().unwrap_or_else(|| panic!("{name}")); + let l = paper("l"); + assert!((l["width"].as_f64().unwrap() - 89.0 / 25.4).abs() < 1e-9 && (l["height"].as_f64().unwrap() - 127.0 / 25.4).abs() < 1e-9); + assert!(papers.iter().any(|p| p["name"] == "postcard") && papers.iter().any(|p| p["name"] == "6cut")); assert_eq!(g["gridCells"].as_array().unwrap().len(), 1); let g = run( @@ -213,6 +219,28 @@ fn export_pdf_prints_the_current_settings() { fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ids})); } +/// The Japanese photo and postcard papers reach the PDF page at their metric sizes: はがき is +/// 100 × 148 mm (283.46 × 419.53 pt) and L is 89 × 127 mm (252.28 × 359.99 pt). 254 ppi is exactly +/// ten pixels per millimetre, so the page pixels are whole millimetres and the MediaBox is exact. +#[test] +fn export_pdf_uses_the_japanese_paper_sizes() { + let mut s = Session::with_demo(); + let id = s.catalog.photos().next().map(|p| p.id.0).unwrap(); + for (paper, w_pt, h_pt) in [("postcard", 283.46, 419.53), ("l", 252.28, 359.99)] { + run(&mut s, "print.set", json!({"layout": {"style": "singleImage", "page": {"paper": paper}}, "job": {"ppi": 254.0}})); + let path = temp_pdf(paper); + run(&mut s, "print.exportPdf", json!({"path": path, "ids": [id]})); + let bytes = std::fs::read(&path).unwrap(); + let (w, h) = media_box(&bytes); + assert!((w - w_pt).abs() < 0.1 && (h - h_pt).abs() < 0.1, "{paper}: {w} x {h} pt, wanted {w_pt} x {h_pt}"); + } + // The customary names are accepted and the page size follows them; an unknown name is refused. + let g = run(&mut s, "print.set", json!({"layout": {"page": {"paper": "はがき"}}})); + assert!((g["pageWidth"].as_f64().unwrap() - 100.0 / 25.4).abs() < 1e-9); + assert!((g["pageHeight"].as_f64().unwrap() - 148.0 / 25.4).abs() < 1e-9); + fails(&mut s, "print.set", json!({"layout": {"page": {"paper": "poster"}}})); +} + #[test] fn export_pdf_with_a_printer_profile() { let mut s = Session::with_demo(); diff --git a/crates/ui-egui/src/print_view.rs b/crates/ui-egui/src/print_view.rs index 0c3745270..918287bdc 100644 --- a/crates/ui-egui/src/print_view.rs +++ b/crates/ui-egui/src/print_view.rs @@ -141,6 +141,15 @@ fn paper_label(name: &str) -> String { "legal" => "Legal".into(), "tabloid" => "Tabloid".into(), "custom" => "Custom".into(), + // Japanese photo and postcard papers: the customary name and its metric size. + "l" => "L判 (89 × 127 mm)".into(), + "2l" => "2L判 (127 × 178 mm)".into(), + "kg" => "KG判 (102 × 152 mm)".into(), + "postcard" => "はがき (100 × 148 mm)".into(), + "double-postcard" => "往復はがき (148 × 200 mm)".into(), + "6cut" => "六切 (203 × 254 mm)".into(), + "4cut" => "四切 (254 × 305 mm)".into(), + "business-card" => "名刺 (55 × 91 mm)".into(), other => other.replace('x', " × ") + " in", } } @@ -959,5 +968,11 @@ mod tests { assert_eq!(trim(2.5), "2.5"); assert_eq!(paper_label("a4"), "A4"); assert_eq!(paper_label("4x6"), "4 × 6 in"); + // The Japanese photo and postcard papers show their customary name and metric size. + assert_eq!(paper_label("l"), "L判 (89 × 127 mm)"); + assert_eq!(paper_label("postcard"), "はがき (100 × 148 mm)"); + assert_eq!(paper_label("double-postcard"), "往復はがき (148 × 200 mm)"); + assert_eq!(paper_label("6cut"), "六切 (203 × 254 mm)"); + assert_eq!(paper_label("4cut"), "四切 (254 × 305 mm)"); } } diff --git a/docs/gaps.md b/docs/gaps.md index 1b4a385fa..d48b30da3 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps: where LightCraft falls short of Lightroom -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-15 · **Change:** minor (gap 15: Print Page Options — page numbers, page info, crop marks) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-22 · **Change:** minor (Gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF) Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is @@ -77,6 +77,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Gap 15: the Print page setup and PDF gain the Japanese photo and postcard paper sizes (L判 89×127, 2L判 127×178, KG判 102×152, はがき 100×148, 往復はがき 148×200, 六切 203×254, 四切 254×305, 名刺 55×91 mm), by their customary names; gap not closed | | 2026-10-18 | patch | Gap 1: six X-Trans CMOS 4 Fujifilm bodies (X-T30 …) take the X-T3's spectral matrices instead of the neutral matrix | | 2026-10-15 | minor | Gap 15 shrinks: the Page panel's Page Options (Page Numbers, Page Info, Crop Marks) print on Single Image and Contact Sheet pages; identity plate, watermark, photo info, Cut Guides, direct printing and JPEG output remain | | 2026-10-14 | minor | Gap 26: at 100k photos stack and album lookups are indexed, so an "In a Stack" or "Album" smart album counts in ~14 ms (was 0.6–2.7 s); open time unchanged | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 4e0531793..5b0cc37e9 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (LRC-PRINT-PAGE: Print Page Options) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (LRC-PRINT-LAYOUT: Japanese photo and postcard paper sizes) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -759,7 +759,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- | LRC-SS-EXPORT | Slideshow export | P2 | 🟡 | `cmd:slideshow.export`, `cmd:slideshow.exportStatus`, `cmd:dialog.slideshowExport`, `cmd:app.slideshowExport`, `path:crates/engine/src/slideshow_export.rs`, `path:crates/codecs/src/video.rs`, `path:crates/ui-egui/src/slideshow_export.rs` | View ▸ Export Slideshow… (and Export PDF / JPEG / Video in Settings ▸ Slideshow): the photos in view as their composed slides (layout, overlays) between the intro and ending screens that are on, in view order. **PDF**: one page per slide at 1024 / 1920 / 3840 px (any 64–8192 by command; page = slide pixels in points, aspect from the aspect preview, "screen" = 16:9), JPEG quality, each page carrying the slide duration and a fade transition for presentation mode, "Automatically show full screen" (`/PageMode /FullScreen`). **JPEG**: `-001.jpg`… in a new folder. **Video**: AV1 (rav1e, fast preset) in MP4 or WebM written by our own muxers, 720p / 1080p (64–2160 by command), 24 / 30 fps (25 / 60 by command); each slide held for its duration (one long sample: variable frame rate) and fades rendered as blended frames (crossfade or through the fade colour); at most 2 hours. Runs in the background with a row in the activity stack (frames or slides done, ✕ cancels); files are written as `.part` and renamed when complete, so a cancelled or failed export leaves nothing; a photo that can't be rendered is skipped with a warning. Missing: H.264 (Classic's codec; patents, no pure-Rust encoder), audio / music in the video (LRC-SS-MUSIC), random order and pan & zoom in exports, the PDF's per-page fades follow the viewer (Acrobat), export presets, Classic's export dialog sizes beyond these, the web build (no export) | | LRC-PRINT-LAYOUTSTYLE | Print layout styles | P2 | ✅ | `cmd:dialog.print`, `cmd:print.set`, `cmd:print.get`, `cmd:print.exportPdf`, `cmd:export.contactSheet`, `crates/ui-egui/src/print_view.rs` | File → Print… (⌘P): Single Image, Contact Sheet, Picture Package and Custom Package, one page preview each; also `print.*` commands for the CLI, control channel and MCP; the older File → Contact Sheet PDF stays | | LRC-PRINT-IMAGESETTINGS | Print image settings | P2 | ✅ | `cmd:print.set`, `crates/engine/src/print/compose.rs`, `crates/ui-egui/src/print_view.rs` | Zoom to Fill Frame, Rotate to Fit, Photo Border (width, colour; package styles) and Inner Stroke (width in points, colour) in the dialog, drawn in the preview and in the PDF; pixel-tested | -| LRC-PRINT-LAYOUT | Print layout | P2 | 🟡 | `cmd:print.set`, `cmd:print.layout.autoLayout`, `cmd:print.layout.newPage`, `cmd:print.layout.clear`, `cmd:app.print`, `cmd:export.contactSheet`, `crates/engine/src/print/layout.rs` | A3/A4/A5/Letter/Legal/Tabloid/4x6/5x7/8x10/13x19 and custom paper, portrait/landscape, per-side margins, grid with column/row spacing and square cells, 72–720 ppi, in a Print dialog with a scaled page preview; no printer-driver Page Setup, no cell size / alignment presets | +| LRC-PRINT-LAYOUT | Print layout | P2 | 🟡 | `cmd:print.set`, `cmd:print.layout.autoLayout`, `cmd:print.layout.newPage`, `cmd:print.layout.clear`, `cmd:app.print`, `cmd:export.contactSheet`, `crates/engine/src/print/layout.rs` | A3/A4/A5/Letter/Legal/Tabloid/4x6/5x7/8x10/13x19, the Japanese photo and postcard sizes (L判 89×127, 2L判 127×178, KG判 102×152, はがき 100×148, 往復はがき 148×200, 六切 203×254, 四切 254×305, 名刺 55×91 mm, by their customary names) and custom paper, portrait/landscape, per-side margins, grid with column/row spacing and square cells, 72–720 ppi, in a Print dialog with a scaled page preview; no printer-driver Page Setup, no cell size / alignment presets | | LRC-PRINT-GUIDES | Print guides | P2 | ✅ | `cmd:print.set`, `cmd:print.cell.move`, `crates/ui-egui/src/print_view.rs` | rulers in in/cm/mm/pt, snap off/cells/grid with a grid size, and Page Margins / Page Bleed (a fixed 0.125 in outline) / Page Grid / Image Cells / Dimensions guides drawn on the preview | | LRC-PRINT-CELLS | Picture-package cells | P2 | ✅ | `cmd:print.cell.add`, `cmd:print.cell.move`, `cmd:print.cell.resize`, `cmd:print.cell.delete`, `cmd:print.cell.stack`, `cmd:print.cell.photo`, `cmd:print.cell.page`, `cmd:print.layout.autoLayout`, `crates/ui-egui/src/print_view.rs` | add cells at standard or custom sizes, drag and resize them on the page preview (snap to cells, grid or off), Send to Back / Backward / Forward / Bring to Front, a photo per cell (Custom Package), Auto Layout packing, New Page, Clear Layout; no Auto Layout for Custom Package | | LRC-PRINT-PAGE | Print page options | P2 | 🟡 | `cmd:print.set`, `cmd:export.contactSheet`, `crates/engine/src/print/compose.rs`, `crates/ui-egui/src/print_view.rs` | page background colour and margins in the Print dialog; Page Options for Single Image and Contact Sheet (`print.set {page: {pageNumbers, pageInfo, cropMarks}}`, checkboxes in the Page section, kept in templates): the page number at the bottom right, the print settings line (sharpening, profile, resolution) at the bottom left, crop marks at each photo's corners, drawn in the preview and in the PDF (`print::compose::tests::crop_marks_sit_outside_each_photo_corner`); filename captions in the contact-sheet PDF; no identity plate, watermark, photo info or package Cut Guides | @@ -797,6 +797,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | LRC-PRINT-LAYOUT: the Japanese photo and postcard paper sizes (L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺) are selectable in the page setup and Print dialog and print to PDF at their metric sizes | | 2026-10-14 | patch | LR-EXP-TYPE: opaque 8-bit PNG/TIFF/WebP use RGB; transparent exports retain alpha (#1670). Statuses and feature counts unchanged. | | 2026-10-15 | minor | LRC-PRINT-PAGE: Page Options (Page Numbers, Page Info, Crop Marks) on Single Image and Contact Sheet pages, in the Print dialog, `print.set` and templates; still 🟡 (no identity plate, watermark, photo info or Cut Guides) | | 2026-10-15 | minor | LR-IMP-FORMATS: Hasselblad 3FR and FFF show their uncompressed RGB preview (no JPEG in these files) in the grid and loupe until developed, and as the stand-in when a file fails to decode; status unchanged (🟡) | From 6b0c89133344d9082871bf9837d972c4d09418ed Mon Sep 17 00:00:00 2001 From: midasdf <205497066+midasdf@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:24:53 +0900 Subject: [PATCH 2/3] Print: match paper names past punctuation and resolve aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paper-name normalisation promised punctuation-insensitive names but dropped only whitespace, hyphens, underscores and parentheses, so `L判。` did not resolve; keep letters and digits only instead. The Print dialog also lower-cased the stored name without resolving it to a canonical paper, so a paper set through an alias (`はがき`) showed `はがき in` and selected no option; add `paper_name` and use it for the selector's current value and option comparison. Tests: the layout aliases test covers periods and Japanese punctuation, and the Print view test covers the resolved current paper. --- crates/engine/src/print/layout.rs | 43 +++++++++++++++++++------------ crates/ui-egui/src/print_view.rs | 16 ++++++++++-- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/crates/engine/src/print/layout.rs b/crates/engine/src/print/layout.rs index c267c0107..be4438424 100644 --- a/crates/engine/src/print/layout.rs +++ b/crates/engine/src/print/layout.rs @@ -88,33 +88,38 @@ pub fn paper_names() -> impl Iterator { PAPER.iter().map(|p| p.0) } -/// The form [`paper_size`] matches names in: lower case, with whitespace, hyphens, underscores and -/// parentheses ignored and `×`/`*` read as `x`, so `L判`, `l-size` and `l` are one name and `4 × 6` -/// is `4x6`. +/// The form [`paper_name`] matches names in: lower case, letters and digits only, and `×`/`*` read +/// as `x`, so `L判`, `L判。`, `l-size` and `(l)` are one name and `4 × 6` is `4x6`. fn normalize_paper(name: &str) -> String { name.chars() - .filter(|c| !c.is_whitespace() && !matches!(c, '-' | '_' | '(' | ')')) - .map(|c| match c { - '×' | '*' => 'x', - c => c, + .filter_map(|c| match c { + '×' | '*' => Some('x'), + c if c.is_alphanumeric() => Some(c), + _ => None, }) .collect::() .to_lowercase() } +/// The canonical name of the paper `name` stands for, `None` for unknown names and `custom`. +/// `L判` is `l`, `はがき` is `postcard`, `六切` is `6cut`, and so on. +pub fn paper_name(name: &str) -> Option<&'static str> { + let n = normalize_paper(name); + if n.is_empty() { + return None; + } + PAPER + .iter() + .find(|p| normalize_paper(p.0) == n) + .map(|p| p.0) + .or_else(|| PAPER_ALIASES.iter().find(|(alias, _)| normalize_paper(alias) == n).map(|(_, name)| *name)) +} + /// Portrait size in inches of a named paper, `None` for unknown names and `custom`. Case, spaces /// and punctuation don't matter, and the Japanese customary names (`L判`, `2L判`, `KG判`, `はがき`, /// `往復はがき`, `六切`, `四切`, `名刺`) are aliases of the papers above. pub fn paper_size(name: &str) -> Option<(f64, f64)> { - let n = normalize_paper(name); - if n.is_empty() { - return None; - } - let canonical = PAPER - .iter() - .find(|p| normalize_paper(p.0) == n) - .map(|p| p.0) - .or_else(|| PAPER_ALIASES.iter().find(|(alias, _)| normalize_paper(alias) == n).map(|(_, name)| *name))?; + let canonical = paper_name(name)?; PAPER.iter().find(|p| p.0 == canonical).map(|p| (p.1, p.2)) } @@ -1036,6 +1041,12 @@ mod tests { ] { assert_eq!(paper_size(a), paper_size(b), "{a} vs {b}"); } + // Punctuation is ignored, including the periods and Japanese marks a pasted name may carry. + for (name, canonical) in [("L判。", "l"), ("L判、", "l"), ("(L判)", "l"), ("l-size.", "l"), ("はがき。", "postcard"), ("6切。", "6cut")] + { + assert_eq!(paper_name(name), Some(canonical), "{name}"); + } + assert_eq!(paper_name("custom"), None); assert!(paper_size("poster").is_none()); // The papers that were already there keep their sizes. assert_eq!(pt("letter"), (612.0, 792.0)); diff --git a/crates/ui-egui/src/print_view.rs b/crates/ui-egui/src/print_view.rs index 918287bdc..fd111b207 100644 --- a/crates/ui-egui/src/print_view.rs +++ b/crates/ui-egui/src/print_view.rs @@ -154,6 +154,12 @@ fn paper_label(name: &str) -> String { } } +/// The paper the selector shows as current: `custom`, or the canonical name an alias stands for, +/// so a `print.set` with `はがき` still selects the `postcard` option. +fn current_paper(paper: &str) -> &str { + if paper.eq_ignore_ascii_case("custom") { "custom" } else { layout::paper_name(paper).unwrap_or(paper) } +} + /// The dialog's body: the settings column and the page preview side by side. pub fn body(app: &mut LightcraftApp, ui: &mut Ui, t: &Tokens) { let ctx = ui.ctx().clone(); @@ -383,8 +389,8 @@ fn settings(app: &mut LightcraftApp, ui: &mut Ui, t: &Tokens, pu: &mut PrintUi, heading(ui, t, "Page Setup"); row(ui, t, "Paper", |ui| { - let current = if l.page.paper.eq_ignore_ascii_case("custom") { "custom".to_string() } else { l.page.paper.to_ascii_lowercase() }; - let c = egui::ComboBox::from_id_salt("printPaper").selected_text(paper_label(¤t)).show_ui(ui, |ui| { + let current = current_paper(&l.page.paper); + let c = egui::ComboBox::from_id_salt("printPaper").selected_text(paper_label(current)).show_ui(ui, |ui| { for name in layout::paper_names().chain(std::iter::once("custom")) { if ui.selectable_label(current == name, paper_label(name)).clicked() { patch(app, &ctx, json!({"layout": {"page": {"paper": name}}})); @@ -974,5 +980,11 @@ mod tests { assert_eq!(paper_label("double-postcard"), "往復はがき (148 × 200 mm)"); assert_eq!(paper_label("6cut"), "六切 (203 × 254 mm)"); assert_eq!(paper_label("4cut"), "四切 (254 × 305 mm)"); + // The selector resolves an alias stored by print.set to the canonical option. + assert_eq!(current_paper("はがき"), "postcard"); + assert_eq!(current_paper("L判"), "l"); + assert_eq!(current_paper("Custom"), "custom"); + assert_eq!(paper_label(current_paper("はがき")), "はがき (100 × 148 mm)"); + assert_eq!(paper_label(current_paper("a4")), "A4"); } } From 767c4a39b602c9b3956ba30bc3e25f88435a505f Mon Sep 17 00:00:00 2001 From: midasdf Date: Fri, 23 Oct 2026 02:28:33 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Print:=20export=20the=20=E3=81=AF=E3=81=8C?= =?UTF-8?q?=E3=81=8D=20alias=20in=20the=20paper-size=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Japanese paper-size test exported only the canonical names and then checked `print.set` with はがき, so the alias-to-MediaBox path was not covered. Export はがき through `print.exportPdf` as well and read the same 283.46 × 419.53 pt page as postcard. --- crates/engine/src/tests_print.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/src/tests_print.rs b/crates/engine/src/tests_print.rs index 7ea595ea4..51277da04 100644 --- a/crates/engine/src/tests_print.rs +++ b/crates/engine/src/tests_print.rs @@ -226,7 +226,7 @@ fn export_pdf_prints_the_current_settings() { fn export_pdf_uses_the_japanese_paper_sizes() { let mut s = Session::with_demo(); let id = s.catalog.photos().next().map(|p| p.id.0).unwrap(); - for (paper, w_pt, h_pt) in [("postcard", 283.46, 419.53), ("l", 252.28, 359.99)] { + for (paper, w_pt, h_pt) in [("postcard", 283.46, 419.53), ("はがき", 283.46, 419.53), ("l", 252.28, 359.99)] { run(&mut s, "print.set", json!({"layout": {"style": "singleImage", "page": {"paper": paper}}, "job": {"ppi": 254.0}})); let path = temp_pdf(paper); run(&mut s, "print.exportPdf", json!({"path": path, "ids": [id]}));