diff --git a/.agents/qtgui.md b/.agents/qtgui.md index 982d93825..bd2e85e8a 100644 --- a/.agents/qtgui.md +++ b/.agents/qtgui.md @@ -7,8 +7,9 @@ parameter files exist outside development. ZIP parameter archives and JSON-v2 were temporary alpha experiments, not compatibility obligations. The first 2.0 JSON `.cspar` format is the semantically grouped schema-3 design in [`doc/parameter-json-migration-tracking.md`](../doc/parameter-json-migration-tracking.md). -Draft PR #556 switches the Qt GUI, CLI and recovery to native JSON schema 3, -but it is **not yet merged or release-qualified**. The old JSON-v2 reader is +The Qt GUI, CLI and recovery now use the native JSON schema-3 document; +the project JSON schema and the legacy stitching reader are implemented in +`libcolorscreen` independently of Qt. The old JSON-v2 reader is not a compatibility gate. Preserve historical `.par` reading and loss-checked export. MTF editors use immutable measurement IDs and capture-group IDs; source provenance must retain its physical interpretation through Save As, diff --git a/NEWS b/NEWS index 68b981b47..8e2f36fe8 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,8 @@ Changes planned for version 2.0 (development version 2.0alpha) - New features: - JSON based parameter file format .cspar. Old .par files remains readable. + - Native JSON stitching projects (.csprj), with the original version-1 + text-project reader retained for opening and converting older projects. - QT based graphical user interface - Support for new processes: - Dioptrichrome-B diff --git a/README.md b/README.md index 548464c81..5925647d6 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,22 @@ from the Library of Congress webpages and produce color renderings. # Usage +## Native files for the 2.0 development series + +- **`.cspar`** stores non-destructive image-processing parameters in UTF-8 + JSON schema 3, with an optional linked source image. Historical `.par` + text files remain readable. +- **`.csprj`** stores a multi-image stitching project in UTF-8 JSON schema 2. + Tiles refer to external source images and store only screen geometry and + stitching metadata; the original keyword-text version-1 project reader + remains supported for migration. + +The [2.0 user manual](https://github.com/janhubicka/Color-Screen/wiki/Color-Screen-2.0-User-Manual), +[parameter reference](https://github.com/janhubicka/Color-Screen/wiki/Color-Screen-2.0-Parameter-Format), +and [stitch-project format reference](https://github.com/janhubicka/Color-Screen/wiki/Color-Screen-2.0-Project-Format) +describe the current workflows and schemas. Older Java GUI and CLI articles +remain under [historical 1.0 documentation](https://github.com/janhubicka/Color-Screen/wiki/Color-Screen-1.0-Documentation). + There are two programs installed. `colorscreen` is a command line utility to render into tiff files. See diff --git a/doc/parameter-archive-format.md b/doc/parameter-archive-format.md index 17a10af55..8e07c6961 100644 --- a/doc/parameter-archive-format.md +++ b/doc/parameter-archive-format.md @@ -1,6 +1,6 @@ # Color-Screen parameter file format -**Status: canonical pre-2.0 design, not yet the active writer.** This document +**Status: implemented 2.0 schema-3 format.** This document replaces the historical ZIP-based alpha proposal, which was never used outside development. See [the current implementation tracker](parameter-json-migration-tracking.md), the [machine-readable schema](parameters-schema-v3.json), and the @@ -14,7 +14,7 @@ available. Early ZIP-v1 and JSON-v2 `.cspar` implementations were unreleased experiments. There are **no JSON parameter files requiring a compatibility reader, format-preserving rewrite, or downgrade converter**. -ZIP-based `.csprj` stitching projects are separate and remain supported. +`.csprj` stitching projects are separate from `.cspar`: they now have a native JSON project schema with nested schema-3-compatible geometric mapping data only. The legacy project reader accepts version-1 keyword text. ## Physical representation @@ -22,7 +22,7 @@ A `.cspar` file is one UTF-8 JSON document; no ZIP wrapper, manifest, embedded CSP text, or binary side payload is required. JSON is also valid YAML 1.2 syntax, without importing YAML aliases/tags into our input language. -The current semantic design uses the `org.colorscreen.parameters` format +The implemented schema uses the `org.colorscreen.parameters` format identifier and integer `schema_version` 3. Structural required fields, mode-tagged values and nested types are defined by the checked-in draft-2020-12 JSON Schema. The library's typed decoder additionally verifies capture-ID diff --git a/doc/parameter-json-migration-tracking.md b/doc/parameter-json-migration-tracking.md index 5e7fe10ac..5e847f1dc 100644 --- a/doc/parameter-json-migration-tracking.md +++ b/doc/parameter-json-migration-tracking.md @@ -39,7 +39,8 @@ manifest, UI selection, recovery fallback, or compatibility fixture. The only supported input formats are historical plain-text `.par` and native JSON. Opening never rewrites a source file. Failed serialization, validation or file replacement preserves the original bytes and document target/dirty state. -The ZIP-based `.csprj` stitching container is unrelated and remains supported. +Stitching `.csprj` files are a separate format: the historical reader handles +version-1 keyword text, and the new writer emits project JSON schema 2. Legacy `.par` export remains available with the existing loss-prevention checks. Unknown vendor/postamble metadata must never disappear in conversion: diff --git a/doc/parameter-json-v3-reference.md b/doc/parameter-json-v3-reference.md index 9e05c006c..2dac75f99 100644 --- a/doc/parameter-json-v3-reference.md +++ b/doc/parameter-json-v3-reference.md @@ -1,13 +1,13 @@ # Canonical parameter JSON schema 3 — implementation contract -**Status: draft implementation under CI, not yet merged or released.** +**Status: implemented schema-3 parameter format for the 2.0 development series.** See the [implementation tracker](parameter-json-migration-tracking.md) and the [parameter-file specification](parameter-archive-format.md). Only old plain-text `.par` files exist outside development. There is no compatibility requirement for the discarded ZIP archive or unreleased JSON-v1 or JSON-v2 experiments. The first supported `.cspar` is a single UTF-8 -JSON document. ZIP `.csprj` stitching projects are independent and unchanged. +JSON document. Stitching `.csprj` projects are a separate format: the new project JSON schema embeds only the schema-3 geometric mapping and screen type per tile, while retaining the older version-1 keyword-text project reader. ## Structural ownership diff --git a/doc/qtgui-workflow-roadmap.md b/doc/qtgui-workflow-roadmap.md index f33ae2902..bc7c8a0aa 100644 --- a/doc/qtgui-workflow-roadmap.md +++ b/doc/qtgui-workflow-roadmap.md @@ -802,8 +802,9 @@ distinction remains ambiguous to operators. **15 October 2026 decision:** the only parameter files in circulation are old plain-text `.par` files. The ZIP-based parameter archive and both earlier JSON layouts were development experiments; there is no need to preserve or -migrate from any of them. ZIP-based `.csprj` *stitching projects* remain -supported and are unrelated to the parameter file format. +migrate from any of them. Historical `.csprj` stitching projects use version-1 keyword text; the new +JSON project writer retains the text reader. Project format is independent +of `.cspar` parameter files. The first user-facing `.cspar` format for 2.0 is a single human-readable UTF-8 JSON document with explicitly named, semantically grouped fields. The current diff --git a/doc/wiki/Color-Screen-2.0-Parameter-Format.md b/doc/wiki/Color-Screen-2.0-Parameter-Format.md index d71e9e3a4..9a7776e2a 100644 --- a/doc/wiki/Color-Screen-2.0-Parameter-Format.md +++ b/doc/wiki/Color-Screen-2.0-Parameter-Format.md @@ -6,7 +6,7 @@ The current native `.cspar` parameter file is **UTF-8 JSON (schema 3)**, not a Z ## Compatibility policy -There are no distributed legacy ZIP-based `.cspar` files that need write compatibility. New saves use JSON schema 3. Do not describe legacy project ZIPs as JSON: `.csprj` is a separate format with a proposed migration. +There are no distributed legacy ZIP-based `.cspar` files that need write compatibility. New saves use JSON schema 3. `.csprj` stitching projects are a separate format; the new schema-2 JSON project document contains a compact, schema-3-compatible `screen_geometry` object for each tile, not a complete copy of the parameter document. Its historical reader accepts version-1 keyword text. ## What the file describes diff --git a/doc/wiki/Color-Screen-2.0-Project-Format.md b/doc/wiki/Color-Screen-2.0-Project-Format.md index 13e514c20..15cc1f36d 100644 --- a/doc/wiki/Color-Screen-2.0-Project-Format.md +++ b/doc/wiki/Color-Screen-2.0-Project-Format.md @@ -1,18 +1,55 @@ -# Color-Screen 2.0 — stitching project format and JSON migration +# Color-Screen 2.0 — stitching project JSON format -## Status: proposed change, not yet implemented +A `.csprj` file is a **plain UTF-8 JSON document**, with no ZIP wrapper or embedded image pixels. The native writer uses the project identifier `org.colorscreen.stitch-project` and project `schema_version` **2**. Each tile stores only its screen type and a **schema-3 geometric mapping subtree**, using the same typed codec as [`.cspar`](Color-Screen-2.0-Parameter-Format.md). It does not include irrelevant per-tile color, capture or MTF defaults. -Stitching projects use the `.csprj` extension. The historical project representation is a ZIP archive. The planned 2.0 writer should instead emit **plain UTF-8 JSON** while preserving a reader for the original ZIP representation. No distributed legacy ZIP projects are known, so new writes need not retain the old container. +This is the source-controlled format reference for the 2.0 migration in [PR #561](https://github.com/janhubicka/Color-Screen/pull/561); the published rolling build may precede that change until it is merged and packaged. -## Migration requirements +## Project JSON structure -- Keep `.csprj` as the extension; detect legacy ZIP versus JSON by file signature/content, not filename alone. -- Write new projects only as JSON once the migration lands. -- Preserve project-wide settings, source-image references, per-image parameters, stitching geometry and relationships; do not embed large source images by default. -- Support relative paths resolved from the project location; when an image has moved, allow reassociation without discarding the project. -- Provide explicit schema/version identification and errors for unsupported versions or malformed data. -- Retain the legacy ZIP reader, but do not silently reintroduce a ZIP writer. +| Field | Type | Interpretation | +|---|---|---| +| `format` | string | Exactly `org.colorscreen.stitch-project` | +| `schema_version` | integer | Exactly `2` | +| `grid` | object | Required integer `columns` and `rows` (each 1–10) | +| `pixel_size` | finite number | Pixel scale used by stitching/output | +| `tiles` | array | Exactly `columns × rows` records, in row-major order | -## Verification before documenting a normative schema +Each entry of `tiles` contains precisely these fields: -Test JSON save/load/save round trips, old ZIP load followed by JSON save, missing or moved images, relative and absolute paths, and representative multi-image stitching projects. Compare reconstructed project state, not merely text formatting. Only after implementation should this page list exact JSON keys and defaults as authoritative. +| Field | Type | Interpretation | +|---|---|---| +| `filename` | UTF-8 string | Source-image filename or relative path | +| `angle`, `ratio` | finite numbers | Detected screen angle and ratio | +| `position` | array of 2 finite numbers | Tile's upper-left position in common screen coordinates | +| `image_size` | array of 2 integers | Source image width and height in pixels | +| `screen_size` | array of 2 integers | Tile width and height in screen coordinates | +| `screen_shift` | array of 2 integers | Tile offset in screen coordinates; may be negative | +| `screen_geometry` | JSON object | Exactly `screen_type` (string) and `geometry` (object); `geometry` uses the same keys and units as the `geometry` section of schema-3 `.cspar`, including any mesh | + +The `.cspar` schema-3 codec validates the `screen_geometry` contents against its native typed geometric mapping. A file containing extra capture, color, calibration, MTF or reconstruction settings is **rejected**, rather than accepting values that the stitching project would silently discard. No legacy `.par` text or processing-document defaults are embedded. + +File keys are validated strictly: unknown or missing fields, unsupported versions, incorrect grid dimensions and invalid geometry cause a load error. Parsing is bounded to prevent unexpectedly large projects. Known-screen bitmaps are allocated eagerly on load, so the reader imposes a **resource-safety ceiling of 128 Mi screen cells per tile and 512 Mi cells per project**, in addition to the 10×10 grid limit. These are enforced limits, not recommended physical dimensions; source-image pixel counts are independent. The file is serialized in a consistent order. + +## Paths and portability + +Source images remain external to the project. Relative filenames are resolved from the project file's directory, not the process's current working directory. Moving a project together with its relative-path images therefore preserves the references. + +The project file does not contain the source scan itself. Keep your scans when archiving a project. + +## Older projects and migration + +The **actual historical stitching-project format in `libcolorscreen` is text**, starting with: + +```text +color_screen_stitch_project_version: 1 +num_images: 2 1 +pixel_size: 1.0 +``` + +It then stores tile geometry with `stitch_image_...` keywords and a legacy CSP parameter block per tile. The original text reader remains available; opening an old project and saving it with the new writer produces JSON. The reader chooses between JSON and legacy text by the first non-whitespace character, not by the `.csprj` extension. The former ZIP archives mentioned in some alpha-era parameter documentation were *parameter-file* experiments, not this core stitching project format. + +New files are JSON only; there is no legacy project writer. + +## Tests + +The implementation includes native JSON encode/decode, deterministic save/load/save, malformed-field rejection, project `FILE*` saving/loading, version-1 text import and conversion, and bounds checks on legacy grid dimensions. Exact test results are tracked with PR #561. diff --git a/doc/wiki/Color-Screen-2.0-User-Manual.md b/doc/wiki/Color-Screen-2.0-User-Manual.md index 6a9593dd6..d402da079 100644 --- a/doc/wiki/Color-Screen-2.0-User-Manual.md +++ b/doc/wiki/Color-Screen-2.0-User-Manual.md @@ -26,6 +26,6 @@ The image/cache subsystem is designed for very large scans. Preferences include - `.cspar`: current JSON processing parameters; see [format reference](Color-Screen-2.0-Parameter-Format.md). - `.par`: legacy parameter file, retained for reading. -- `.csprj`: stitching project. The present ZIP-based representation is slated for JSON migration; see [project format](Color-Screen-2.0-Project-Format.md). +- `.csprj`: stitching project. New projects use JSON; historical version-1 text files remain readable. See [project format](Color-Screen-2.0-Project-Format.md). This manual is a source-aligned development draft, not a guarantee that every GUI label is final. GUI testing and screenshots should be used to correct terminology before the 2.0 release. diff --git a/src/colorscreen/colorscreen.C b/src/colorscreen/colorscreen.C index 48da059a4..a1baa44fe 100644 --- a/src/colorscreen/colorscreen.C +++ b/src/colorscreen/colorscreen.C @@ -4168,20 +4168,17 @@ stitch (int argc, char **argv) } if (save_project_filename) { - FILE *f = fopen (save_project_filename, "wt"); - if (!f) + /* A failed JSON encode or interrupted write must not destroy an + existing project. The shared text-file writer stages and atomically + replaces the destination on success. */ + std::string json, error; + if (!prj->serialize_json (&json, &error) + || !write_parameter_text_file (save_project_filename, json, &error)) { - fprintf (stderr, "Error opening project file: %s\n", - save_project_filename); + fprintf (stderr, "Error saving project file %s: %s\n", + save_project_filename, error.c_str ()); return 1; } - if (!prj->save (f)) - { - fprintf (stderr, "Error saving project file: %s\n", - save_project_filename); - return 1; - } - fclose (f); } return 0; } diff --git a/src/libcolorscreen/Makefile.am b/src/libcolorscreen/Makefile.am index 52f8801d6..e9718b43e 100644 --- a/src/libcolorscreen/Makefile.am +++ b/src/libcolorscreen/Makefile.am @@ -1,7 +1,7 @@ include_HEADERS = include/color.h include/colorscreen.h include/imagedata.h include/matrix.h include/scr-to-img.h include/dllpublic.h include/scr-detect-parameters.h include/spectrum-to-xyz.h include/progress-info.h include/sensitivity.h include/precomputed-function.h include/mesh.h include/base.h include/tiff-writer.h include/stitch.h include/lens-correction.h include/tone-curve.h include/finetune.h include/focus-analysis.h include/histogram.h include/colorscreen-config.h include/dufaycolor.h include/wratten.h include/screen-map.h include/paget.h include/render-type-parameters.h include/render-parameters.h include/solver-parameters.h include/detect-regular-screen-parameters.h include/scr-to-img-parameters.h include/lens-warp-correction-parameters.h include/backlight-correction-parameters.h include/scanner-blur-correction-parameters.h include/strips.h include/mtf-parameters.h include/analyze-scanner-blur.h include/cow-vector.h include/cache-stats.h lib_LTLIBRARIES = libcolorscreen.la libcolorscreen_la_SOURCES = render.C render-to-scr.C render-fast.C render-interpolate.C screen.C scr-to-img.C imagedata.C loadsave.C parameter-archive.C render-tile.C scr-detect.C render-scr-detect.C spectrum-to-xyz.C patches.C progress-info.C render-to-file.C color.C sensitivity.C solver.C mesh.C scr-detect-geometry.C analyze-dufay.C analyze-paget.C analyze-strips.C screen-map.C analyze-base.C tiff-writer.C backlight-correction.C stitch-image.C stitch-project.C icc.C render-parameters.C mapalloc.C parse-captureone-lcc.C dufaycolor.C wratten.C spectrum.C spectrum-dyes.C spectrum-illuminants.C spectrum-responses.C tone-curve.C lens-warp-correction.C matrix-profile.C scr-detect-colors.C finetune.C focus-analysis.C homography.C gsl-utils.C scanner-blur-correction.C simulate.C has-regular-screen.C deconvolve.C mtf.C fft.C analyze-scanner-blur.C render-simulate.C out-color-adjustments.C slanted-edge.C denoise.C -EXTRA_DIST = lru-cache.h analyze-base-worker.h gaussian-blur.h icc-srgb.h render-diff.h render-tile.h sharpen.h gsl-utils.h gsl-solver.h loadsave.h parameter-archive.h parameter-json-budget.h mapalloc.h render-interpolate.h render-to-file.h spectrum-dyes.h icc.h nmsimplex.h render-superposeimg.h spectrum.h analyze-dufay.h analyze-paget.h analyze-strips.h analyze-base.h bitmap.h render-fast.h spline.h screen.h render-scr-detect.h patches.h render-to-scr.h render.h solver.h scr-detect.h backlight-correction.h mem-luminosity.h homography.h simulate.h deconvolve.h mtf.h finetune-int.h fft.h render-screen.h render-simulate.h lanczos.h out-color-adjustments.h demosaic.h bspline.h cubic-interpolate.h denoise.h +EXTRA_DIST = lru-cache.h analyze-base-worker.h gaussian-blur.h icc-srgb.h render-diff.h render-tile.h sharpen.h gsl-utils.h gsl-solver.h loadsave.h parameter-archive.h parameter-json-budget.h stitch-project-json.h mapalloc.h render-interpolate.h render-to-file.h spectrum-dyes.h icc.h nmsimplex.h render-superposeimg.h spectrum.h analyze-dufay.h analyze-paget.h analyze-strips.h analyze-base.h bitmap.h render-fast.h spline.h screen.h render-scr-detect.h patches.h render-to-scr.h render.h solver.h scr-detect.h backlight-correction.h mem-luminosity.h homography.h simulate.h deconvolve.h mtf.h finetune-int.h fft.h render-screen.h render-simulate.h lanczos.h out-color-adjustments.h demosaic.h bspline.h cubic-interpolate.h denoise.h if RENDER_EXTRA nodist_libcolorscreen_la_SOURCES = render-extra/render-extra.C diff --git a/src/libcolorscreen/Makefile.in b/src/libcolorscreen/Makefile.in index 087ec8769..228e073f4 100644 --- a/src/libcolorscreen/Makefile.in +++ b/src/libcolorscreen/Makefile.in @@ -495,7 +495,7 @@ top_srcdir = @top_srcdir@ include_HEADERS = include/color.h include/colorscreen.h include/imagedata.h include/matrix.h include/scr-to-img.h include/dllpublic.h include/scr-detect-parameters.h include/spectrum-to-xyz.h include/progress-info.h include/sensitivity.h include/precomputed-function.h include/mesh.h include/base.h include/tiff-writer.h include/stitch.h include/lens-correction.h include/tone-curve.h include/finetune.h include/focus-analysis.h include/histogram.h include/colorscreen-config.h include/dufaycolor.h include/wratten.h include/screen-map.h include/paget.h include/render-type-parameters.h include/render-parameters.h include/solver-parameters.h include/detect-regular-screen-parameters.h include/scr-to-img-parameters.h include/lens-warp-correction-parameters.h include/backlight-correction-parameters.h include/scanner-blur-correction-parameters.h include/strips.h include/mtf-parameters.h include/analyze-scanner-blur.h include/cow-vector.h include/cache-stats.h lib_LTLIBRARIES = libcolorscreen.la libcolorscreen_la_SOURCES = render.C render-to-scr.C render-fast.C render-interpolate.C screen.C scr-to-img.C imagedata.C loadsave.C parameter-archive.C render-tile.C scr-detect.C render-scr-detect.C spectrum-to-xyz.C patches.C progress-info.C render-to-file.C color.C sensitivity.C solver.C mesh.C scr-detect-geometry.C analyze-dufay.C analyze-paget.C analyze-strips.C screen-map.C analyze-base.C tiff-writer.C backlight-correction.C stitch-image.C stitch-project.C icc.C render-parameters.C mapalloc.C parse-captureone-lcc.C dufaycolor.C wratten.C spectrum.C spectrum-dyes.C spectrum-illuminants.C spectrum-responses.C tone-curve.C lens-warp-correction.C matrix-profile.C scr-detect-colors.C finetune.C focus-analysis.C homography.C gsl-utils.C scanner-blur-correction.C simulate.C has-regular-screen.C deconvolve.C mtf.C fft.C analyze-scanner-blur.C render-simulate.C out-color-adjustments.C slanted-edge.C denoise.C -EXTRA_DIST = lru-cache.h analyze-base-worker.h gaussian-blur.h icc-srgb.h render-diff.h render-tile.h sharpen.h gsl-utils.h gsl-solver.h loadsave.h parameter-archive.h parameter-json-budget.h mapalloc.h render-interpolate.h render-to-file.h spectrum-dyes.h icc.h nmsimplex.h render-superposeimg.h spectrum.h analyze-dufay.h analyze-paget.h analyze-strips.h analyze-base.h bitmap.h render-fast.h spline.h screen.h render-scr-detect.h patches.h render-to-scr.h render.h solver.h scr-detect.h backlight-correction.h mem-luminosity.h homography.h simulate.h deconvolve.h mtf.h finetune-int.h fft.h render-screen.h render-simulate.h lanczos.h out-color-adjustments.h demosaic.h bspline.h cubic-interpolate.h denoise.h +EXTRA_DIST = lru-cache.h analyze-base-worker.h gaussian-blur.h icc-srgb.h render-diff.h render-tile.h sharpen.h gsl-utils.h gsl-solver.h loadsave.h parameter-archive.h parameter-json-budget.h stitch-project-json.h mapalloc.h render-interpolate.h render-to-file.h spectrum-dyes.h icc.h nmsimplex.h render-superposeimg.h spectrum.h analyze-dufay.h analyze-paget.h analyze-strips.h analyze-base.h bitmap.h render-fast.h spline.h screen.h render-scr-detect.h patches.h render-to-scr.h render.h solver.h scr-detect.h backlight-correction.h mem-luminosity.h homography.h simulate.h deconvolve.h mtf.h finetune-int.h fft.h render-screen.h render-simulate.h lanczos.h out-color-adjustments.h demosaic.h bspline.h cubic-interpolate.h denoise.h @RENDER_EXTRA_TRUE@nodist_libcolorscreen_la_SOURCES = render-extra/render-extra.C # -no-undefined is needed in order to build DLL on Windows. libcolorscreen_la_LDFLAGS = -version-info 2:0:0 $(WIN_NO_UNDEFINED) diff --git a/src/libcolorscreen/imagedata.C b/src/libcolorscreen/imagedata.C index f369d09a8..fc76600e3 100644 --- a/src/libcolorscreen/imagedata.C +++ b/src/libcolorscreen/imagedata.C @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1343,7 +1344,22 @@ stitch_image_data_loader::init_loader (const char *name, const char **error, m_demosaic = demosaic; if (progress) progress->set_task ("opening stitch project", 1); - FILE *f = fopen (name, "rt"); + /* The GUI passes UTF-8 host paths. The Windows narrow CRT fopen uses + the active code page and fails for non-ASCII project filenames. Legacy + keyword text still needs Windows CRLF translation, hence text mode. */ + FILE *f = nullptr; +#ifdef _WIN32 + try + { + f = _wfopen (std::filesystem::u8path (name).c_str (), L"rt"); + } + catch (...) + { + f = nullptr; + } +#else + f = fopen (name, "rt"); +#endif if (!f) { *error = "Can not open file"; diff --git a/src/libcolorscreen/include/stitch.h b/src/libcolorscreen/include/stitch.h index 5774156ba..03a5e9a4b 100644 --- a/src/libcolorscreen/include/stitch.h +++ b/src/libcolorscreen/include/stitch.h @@ -196,6 +196,8 @@ class stitch_image bool diff (stitch_image &other, progress_info *progress); bool save (FILE *f); bool load (stitch_project *, FILE *f, const char **error); + /* Rebuild maps and known-pixel mask after loading native JSON geometry. */ + bool finish_loaded_parameters (stitch_project *project); inline analyze_base &get_analyzer (); static bool write_row (TIFF *out, int y, uint16_t *outrow, @@ -280,6 +282,8 @@ class stitch_project bool initialize (); void determine_viewport (int &xmin, int &xmax, int &ymin, int &ymax); void determine_angle (); + /* Build a complete native project JSON document before writing any file. */ + DLL_PUBLIC bool serialize_json (std::string *output, std::string *error) const; DLL_PUBLIC bool save (FILE *f); DLL_PUBLIC bool load (FILE *f, const char **error); std::string adjusted_filename (std::string filename, std::string suffix, @@ -403,6 +407,9 @@ class stitch_project /* Per-project source generation ensures stale disk/project loads never satisfy another independently opened stitch document. */ uint64_t m_tile_cache_id = 0; + /* Eager known-screen bitmaps are not core LRU entries; limit their + cumulative footprint while a project is being loaded. */ + uint64_t m_loaded_screen_cells = 0; coord_t rotation_adjustment; friend stitch_image; diff --git a/src/libcolorscreen/parameter-archive.C b/src/libcolorscreen/parameter-archive.C index 1da7d7d06..b71c56015 100644 --- a/src/libcolorscreen/parameter-archive.C +++ b/src/libcolorscreen/parameter-archive.C @@ -3,6 +3,7 @@ This file is part of ColorScreen. */ #include "parameter-archive.h" +#include "stitch-project-json.h" #include "parameter-json-budget.h" #include "include/mesh.h" #include "include/colorscreen.h" @@ -5846,4 +5847,318 @@ rebase_parameter_source_image ( } +/* Stitch project JSON schema 2. Store only the native screen type and + geometric registration subtree of schema-3 processing parameters for each + tile: other .cspar fields are not stitch-project state and must not be + silently accepted and discarded by a project reader. Reuse the existing + bounded JSON tree rather than introducing a second parser or Qt dependency. */ +namespace +{ + +/* Check the rectangular tile layout before constructing a document or + allocating project state. The legacy project is backed by a 10x10 grid. */ +bool +stitch_json_dimensions (int columns, int rows, size_t tiles, + std::string *error) +{ + if (columns < 1 || columns > 10 || rows < 1 || rows > 10 + || tiles != (size_t)columns * (size_t)rows) + return archive_fail (error, "stitch project has invalid tile dimensions"); + return true; +} + +/* Emit finite project coordinates without locale-dependent formatting. */ +bool +stitch_json_number (coord_t number, json_value *out, std::string *error) +{ + if (!my_isfinite ((double)number)) + return archive_fail (error, "stitch project contains non-finite geometry"); + *out = v3_number (json_number ((double)number)); + return true; +} + +/* Read one exact signed project integer with a qualified error. */ +bool +stitch_json_int (const json_value &node, const char *field, int *target, + const std::string &path, std::string *error) +{ + const json_value *value + = v3_mtf_required (node, field, json_value::kind::number, path, error); + return value + && (v2_json_int (*value, target) + || v3_mtf_fail (error, path + "." + field, + "expected a signed integer")); +} + +/* Parse a JSON coordinate pair, retaining the full finite precision. */ +bool +stitch_json_position (const json_value &node, point_t *point, + const std::string &path, std::string *error) +{ + if (node.type != json_value::kind::array || node.array_value.size () != 2 + || !v2_real (node.array_value[0], &point->x) + || !v2_real (node.array_value[1], &point->y)) + return v3_mtf_fail (error, path, "expected two finite coordinates"); + return true; +} +} // anonymous namespace + +/* Serialize the exact persistent tile geometry. Compose its small payload + from a canonical .cspar document so screen and lens conventions have only + one implementation. No irrelevant capture, color or render settings are + written into project files. */ +bool +encode_stitch_project_json_document ( + const stitch_project_json_data &project, std::string *output, + std::string *error) +{ + if (error) + error->clear (); + if (!output) + return archive_fail (error, "missing stitch JSON destination"); + if (!stitch_json_dimensions (project.columns, project.rows, + project.tiles.size (), error)) + return false; + + json_value pixel_size; + if (!stitch_json_number (project.pixel_size, &pixel_size, error)) + return false; + json_value tiles = v3_node (json_value::kind::array); + uint64_t total_cells = 0; + scr_detect_parameters detection; + render_parameters render; + solver_parameters solver; + const std::vector profile_spots; + for (const auto &tile : project.tiles) + { + if (!valid_utf8 (tile.filename) + || tile.filename.empty () + || tile.filename.find ('\0') != std::string::npos + || tile.image_width <= 0 || tile.image_height <= 0 + || tile.screen_width <= 0 || tile.screen_height <= 0) + return archive_fail (error, "invalid stitch tile filename or dimensions"); + const uint64_t cells + = (uint64_t)tile.screen_width * (uint64_t)tile.screen_height; + if (cells > stitch_json_max_screen_cells_per_tile + || total_cells > stitch_json_max_screen_cells_per_project - cells) + return archive_fail (error, "stitch project exceeds screen bitmap budget"); + total_cells += cells; + json_value angle, ratio, pos_x, pos_y; + if (!stitch_json_number (tile.angle, &angle, error) + || !stitch_json_number (tile.ratio, &ratio, error) + || !stitch_json_number (tile.position.x, &pos_x, error) + || !stitch_json_number (tile.position.y, &pos_y, error)) + return false; + std::string parameter_json; + if (!encode_parameter_json_v3_document ( + tile.geometry, detection, render, solver, profile_spots, + ¶meter_json, error)) + return false; + json_parser parser (parameter_json.data (), + parameter_json.data () + parameter_json.size (), + v2_max_document_nodes); + json_value parameters; + if (!parser.parse (¶meters)) + return archive_fail (error, "invalid generated tile parameters: " + + parser.error ()); + json_value screen_geometry = v3_object ({ + {"screen_type", v3_at (v3_at (v3_at (parameters, "process"), + "screen"), "type")}, + {"geometry", v3_at (parameters, "geometry")} + }); + + tiles.array_value.push_back (v3_object ({ + {"filename", v3_string (tile.filename)}, + {"angle", std::move (angle)}, + {"ratio", std::move (ratio)}, + {"position", v3_array ({std::move (pos_x), std::move (pos_y)})}, + {"image_size", v3_array ({ + v3_number (std::to_string (tile.image_width)), + v3_number (std::to_string (tile.image_height))})}, + {"screen_size", v3_array ({ + v3_number (std::to_string (tile.screen_width)), + v3_number (std::to_string (tile.screen_height))})}, + {"screen_shift", v3_array ({ + v3_number (std::to_string (tile.screen_shift_x)), + v3_number (std::to_string (tile.screen_shift_y))})}, + {"screen_geometry", std::move (screen_geometry)} + })); + } + + json_value root = v3_object ({ + {"format", v3_string ("org.colorscreen.stitch-project")}, + {"schema_version", v3_number ("2")}, + {"grid", v3_object ({ + {"columns", v3_number (std::to_string (project.columns))}, + {"rows", v3_number (std::to_string (project.rows))}})}, + {"pixel_size", std::move (pixel_size)}, + {"tiles", std::move (tiles)} + }); + std::string result; + v3_json_text (root, &result); + result += '\n'; + if (result.size () > v2_max_document_bytes + || !parameter_json_resources_fit (result, v2_max_document_nodes)) + return archive_fail (error, "stitch JSON exceeds resource limits"); + *output = std::move (result); + return true; +} + +/* Decode the entire JSON project into temporary typed values. The native + screen-geometry subtree is inserted into otherwise default .cspar state + solely for reuse of the strict typed schema-3 validation. No unrelated + .cspar setting is present in the project file or silently discarded. */ +bool +decode_stitch_project_json_document ( + const std::string &input, stitch_project_json_data *project, + std::string *error) +{ + if (error) + error->clear (); + if (!project) + return archive_fail (error, "missing stitch JSON project destination"); + if (input.size () > v2_max_document_bytes || !valid_utf8 (input)) + return archive_fail (error, "invalid stitch JSON UTF-8 or file size"); + + json_parser parser (input.data (), input.data () + input.size (), + v2_max_document_nodes); + json_value root; + if (!parser.parse (&root)) + return archive_fail (error, "invalid stitch JSON: " + parser.error ()); + if (!v3_mtf_exact_keys ( + root, {"format", "schema_version", "grid", "pixel_size", "tiles"}, + "project", error)) + return false; + const json_value *format = v3_mtf_required ( + root, "format", json_value::kind::string, "project", error); + const json_value *version = v3_mtf_required ( + root, "schema_version", json_value::kind::number, "project", error); + const json_value *grid = v3_mtf_required ( + root, "grid", json_value::kind::object, "project", error); + const json_value *tiles = v3_mtf_required ( + root, "tiles", json_value::kind::array, "project", error); + if (!format || !version || !grid || !tiles + || format->text != "org.colorscreen.stitch-project" + || version->text != "2") + return v3_mtf_fail (error, "project", "unknown project format/version"); + if (!v3_mtf_exact_keys (*grid, {"columns", "rows"}, "project.grid", error)) + return false; + stitch_project_json_data staged; + if (!stitch_json_int (*grid, "columns", &staged.columns, + "project.grid", error) + || !stitch_json_int (*grid, "rows", &staged.rows, + "project.grid", error) + || !v3_mtf_real (root, "pixel_size", &staged.pixel_size, + "project", error) + || !stitch_json_dimensions (staged.columns, staged.rows, + tiles->array_value.size (), error)) + return false; + + /* A default schema-3 document supplies non-geometric values only to the + existing typed decoder; the project file itself stores none of them. */ + scr_to_img_parameters empty_geometry; + scr_detect_parameters detection; + render_parameters render; + solver_parameters solver; + std::vector spots; + std::string defaults; + if (!encode_parameter_json_v3_document ( + empty_geometry, detection, render, solver, spots, + &defaults, error)) + return false; + json_parser defaults_parser (defaults.data (), + defaults.data () + defaults.size (), + v2_max_document_nodes); + json_value default_parameters; + if (!defaults_parser.parse (&default_parameters)) + return archive_fail (error, "internal default parameter JSON is invalid"); + + staged.tiles.reserve (tiles->array_value.size ()); + uint64_t total_cells = 0; + for (size_t index = 0; index < tiles->array_value.size (); ++index) + { + const json_value &node = tiles->array_value[index]; + const std::string where = "project.tiles[" + std::to_string (index) + "]"; + if (!v3_mtf_exact_keys ( + node, {"filename", "angle", "ratio", "position", "image_size", + "screen_size", "screen_shift", "screen_geometry"}, where, error)) + return false; + const json_value *filename = v3_mtf_required ( + node, "filename", json_value::kind::string, where, error); + const json_value *position = v3_mtf_required ( + node, "position", json_value::kind::array, where, error); + const json_value *image_size = v3_mtf_required ( + node, "image_size", json_value::kind::array, where, error); + const json_value *screen_size = v3_mtf_required ( + node, "screen_size", json_value::kind::array, where, error); + const json_value *screen_shift = v3_mtf_required ( + node, "screen_shift", json_value::kind::array, where, error); + const json_value *screen_geometry = v3_mtf_required ( + node, "screen_geometry", json_value::kind::object, where, error); + if (!filename || !position || !image_size || !screen_size + || !screen_shift || !screen_geometry + || image_size->array_value.size () != 2 + || screen_size->array_value.size () != 2 + || screen_shift->array_value.size () != 2 + || !valid_utf8 (filename->text) + || filename->text.empty () + || filename->text.find ('\0') != std::string::npos) + return v3_mtf_fail (error, where, "invalid required tile fields"); + + stitch_project_json_tile tile; + tile.filename = filename->text; + if (!v3_mtf_real (node, "angle", &tile.angle, where, error) + || !v3_mtf_real (node, "ratio", &tile.ratio, where, error) + || !stitch_json_position (*position, &tile.position, + where + ".position", error) + || !v2_json_int (image_size->array_value[0], &tile.image_width) + || !v2_json_int (image_size->array_value[1], &tile.image_height) + || !v2_json_int (screen_size->array_value[0], &tile.screen_width) + || !v2_json_int (screen_size->array_value[1], &tile.screen_height) + || !v2_json_int (screen_shift->array_value[0], &tile.screen_shift_x) + || !v2_json_int (screen_shift->array_value[1], &tile.screen_shift_y) + || tile.image_width <= 0 || tile.image_height <= 0 + || tile.screen_width <= 0 || tile.screen_height <= 0) + return v3_mtf_fail (error, where, "invalid tile geometry"); + const uint64_t cells + = (uint64_t)tile.screen_width * (uint64_t)tile.screen_height; + if (cells > stitch_json_max_screen_cells_per_tile + || total_cells > stitch_json_max_screen_cells_per_project - cells) + return v3_mtf_fail (error, where, "screen bitmap budget exceeded"); + total_cells += cells; + + if (!v3_mtf_exact_keys ( + *screen_geometry, {"screen_type", "geometry"}, + where + ".screen_geometry", error)) + return false; + const json_value *screen_type = v3_mtf_required ( + *screen_geometry, "screen_type", json_value::kind::string, + where + ".screen_geometry", error); + const json_value *mapping = v3_mtf_required ( + *screen_geometry, "geometry", json_value::kind::object, + where + ".screen_geometry", error); + if (!screen_type || !mapping) + return false; + json_value patched = default_parameters; + patched.object_value["process"].object_value["screen"] + .object_value["type"] = *screen_type; + patched.object_value["geometry"] = *mapping; + std::string parameter_json; + v3_json_text (patched, ¶meter_json); + scr_detect_parameters tile_detection; + render_parameters tile_render; + solver_parameters tile_solver; + std::vector tile_spots; + std::string nested_error; + if (!decode_parameter_json_v3_document ( + parameter_json, &tile.geometry, &tile_detection, + &tile_render, &tile_solver, &tile_spots, &nested_error)) + return archive_fail ( + error, where + ".screen_geometry: " + nested_error); + staged.tiles.push_back (std::move (tile)); + } + *project = std::move (staged); + return true; +} + } diff --git a/src/libcolorscreen/stitch-image.C b/src/libcolorscreen/stitch-image.C index 958eb4091..4f2c30a17 100644 --- a/src/libcolorscreen/stitch-image.C +++ b/src/libcolorscreen/stitch-image.C @@ -3,6 +3,7 @@ #include #include #include "include/stitch.h" +#include "stitch-project-json.h" #include "include/tiff-writer.h" #include "include/colorscreen.h" #include "include/screen-map.h" @@ -1299,6 +1300,26 @@ stitch_image::load (stitch_project *prj, FILE *f, const char **error) return false; } #endif + if (!finish_loaded_parameters (prj)) + { + *error = "invalid tile geometry or project screen bitmap budget"; + return false; + } + return true; +} + +/* Reconstruct geometry-dependent tile state after either legacy or JSON load. */ +bool +stitch_image::finish_loaded_parameters (stitch_project *prj) +{ + m_prj = prj; + if (!m_prj || width <= 0 || height <= 0) + return false; + const uint64_t cells = (uint64_t)width * (uint64_t)height; + if (cells > stitch_json_max_screen_cells_per_tile + || m_prj->m_loaded_screen_cells + > stitch_json_max_screen_cells_per_project - cells) + return false; image_data data; data.width=1000; data.height=1000; @@ -1313,6 +1334,9 @@ stitch_image::load (stitch_project *prj, FILE *f, const char **error) return false; param.mesh_trans = mesh_trans; known_pixels = (std::unique_ptr)(compute_known_pixels (scr_to_img_map, 0,0,0,0, NULL)); + if (!known_pixels) + return false; + m_prj->m_loaded_screen_cells += cells; analyzed = true; return true; } diff --git a/src/libcolorscreen/stitch-project-json.h b/src/libcolorscreen/stitch-project-json.h new file mode 100644 index 000000000..08a200ed8 --- /dev/null +++ b/src/libcolorscreen/stitch-project-json.h @@ -0,0 +1,67 @@ +/* Typed data transfer for the native stitching project JSON format. + The image loader and CLI keep owning their stitch_project instances; + the codec never depends on Qt or on live image/cache objects. */ + +#ifndef COLORSCREEN_STITCH_PROJECT_JSON_H +#define COLORSCREEN_STITCH_PROJECT_JSON_H + +#include "include/scr-to-img-parameters.h" +#include "include/dllpublic.h" +#include +#include +#include + +namespace colorscreen +{ + +/* Known-screen pixels use a bit per screen cell outside the global decoded + image cache. Guard both absurd single-tile allocations and aggregate + bitmap growth before any allocation or coordinate arithmetic. */ +constexpr uint64_t stitch_json_max_screen_cells_per_tile + = UINT64_C (128) * 1024 * 1024; +constexpr uint64_t stitch_json_max_screen_cells_per_project + = UINT64_C (512) * 1024 * 1024; + +/* One tile in row-major order. JSON stores only the screen type and + schema-3 geometric mapping rather than a full processing document. + Other per-tile parameter domains are not persistent stitch-project state. */ +struct stitch_project_json_tile +{ + std::string filename; + coord_t angle = 0; + coord_t ratio = 0; + point_t position = {0, 0}; + int image_width = 0; + int image_height = 0; + int screen_width = 0; + int screen_height = 0; + int screen_shift_x = 0; + int screen_shift_y = 0; + scr_to_img_parameters geometry; +}; + +/* Persistent project values; neither source image pixels nor cache state + belong to the project archive. */ +struct stitch_project_json_data +{ + int columns = 0; + int rows = 0; + coord_t pixel_size = 0; + std::vector tiles; +}; + +/* Encode the canonical native project JSON document (schema 2), leaving + OUTPUT untouched on error. */ +DLL_PUBLIC bool encode_stitch_project_json_document ( + const stitch_project_json_data &project, std::string *output, + std::string *error); + +/* Strict and bounded native project JSON reader. Unknown fields, malformed + screen geometry and incorrect tile counts leave PROJECT unchanged. */ +DLL_PUBLIC bool decode_stitch_project_json_document ( + const std::string &input, stitch_project_json_data *project, + std::string *error); + +} + +#endif diff --git a/src/libcolorscreen/stitch-project.C b/src/libcolorscreen/stitch-project.C index 27b92b180..254ad0d53 100644 --- a/src/libcolorscreen/stitch-project.C +++ b/src/libcolorscreen/stitch-project.C @@ -13,6 +13,7 @@ #include "analyze-base.h" #include "loadsave.h" #include "parameter-archive.h" +#include "stitch-project-json.h" namespace colorscreen { @@ -461,27 +462,60 @@ stitch_project::determine_angle () } #define HEADER "color_screen_stitch_project_version: 1" +/* Only the legacy reader uses HEADER. All new .csprj files contain the + native JSON schema, with just the schema-3 screen type and mapping per tile. */ +/* Encode completely before any caller opens a destination for replacement. */ bool -stitch_project::save (FILE *f) +stitch_project::serialize_json (std::string *output, std::string *error) const { - if (fprintf (f, "%s\n", HEADER) < 0) - return false; - /* TODO: hack. */ - setlocale(LC_NUMERIC, "C"); - if (fprintf (f, "num_images: %i %i\n", params.width, params.height) < 0 - || fprintf (f, "pixel_size: %f\n", pixel_size) < 0) - return false; - for (int y = 0; y < params.height; y++) - for (int x = 0; x < params.width; x++) + if (!output || params.width < 1 || params.height < 1 + || params.width > stitching_params::max_dim + || params.height > stitching_params::max_dim) + { + if (error) + *error = "invalid project grid or JSON destination"; + return false; + } + stitch_project_json_data document; + document.columns = params.width; + document.rows = params.height; + document.pixel_size = pixel_size; + document.tiles.reserve ((size_t)params.width * params.height); + for (int y = 0; y < params.height; ++y) + for (int x = 0; x < params.width; ++x) { - if (fprintf (f, "color_screen_stitch_image: %i %i\n", x, y) < 0) - return false; - if (!images[y][x].save (f)) - return false; + const stitch_image &image = images[y][x]; + stitch_project_json_tile tile; + tile.filename = image.filename; + tile.angle = image.angle; + tile.ratio = image.ratio; + tile.position = image.pos; + tile.image_width = image.img_width; + tile.image_height = image.img_height; + tile.screen_width = image.width; + tile.screen_height = image.height; + tile.screen_shift_x = image.xshift; + tile.screen_shift_y = image.yshift; + tile.geometry = image.param; + document.tiles.push_back (std::move (tile)); } - if (fprintf (f, "color_screen_stitch_project_end\n") < 0) + return encode_stitch_project_json_document (document, output, error); +} + +/* Backwards-compatible stream API, now emitting only native JSON. */ +bool +stitch_project::save (FILE *f) +{ + if (!f) return false; - return true; + std::string output, error; + if (!serialize_json (&output, &error)) + { + fprintf (stderr, "Can not serialize stitching project: %s\n", + error.c_str ()); + return false; + } + return fwrite (output.data (), 1, output.size (), f) == output.size (); } static bool @@ -504,10 +538,10 @@ absolute_filename_p (std::string name) if (is_dir_separator (name[0])) return true; #ifdef _WIN32 - if (name.length () > 1 - && ((name[0]>='a' && name [0]<'z') - || (name[0]>='A' && name [0]<'Z')) - && name[1]==':' && is_dir_separator (name[2])) + if (name.length () >= 3 + && ((name[0] >= 'a' && name[0] <= 'z') + || (name[0] >= 'A' && name[0] <= 'Z')) + && name[1] == ':' && is_dir_separator (name[2])) return true; #endif return false; @@ -530,6 +564,90 @@ stitch_project::set_path_by_filename (std::string filename) bool stitch_project::load (FILE *f, const char **error) { + /* Read the format signature without relying on the .csprj extension. + The historical keyword-based project remains a valid input. */ + m_loaded_screen_cells = 0; + if (!f) + { + *error = "missing project file"; + return false; + } + int first = getc (f); + while (first == ' ' || first == '\n' || first == '\r' + || first == '\t') + first = getc (f); + if (first == EOF) + { + *error = "empty project file"; + return false; + } + ungetc (first, f); + if (first == '{') + { + /* Decode into a detached model, including each tile's schema-3 + parameter document, before touching the live stitching project. */ + std::string json; + char buffer[8192]; + while (size_t count = fread (buffer, 1, sizeof (buffer), f)) + { + if (json.size () > 256 * 1024 * 1024 - count) + { + *error = "stitch project JSON exceeds 256 MiB"; + return false; + } + json.append (buffer, count); + } + if (ferror (f)) + { + *error = "error reading stitch project JSON"; + return false; + } + /* The returned error must survive destruction of failed project + objects in image_data_loader::init_loader. */ + static thread_local std::string last_json_error; + stitch_project_json_data document; + if (!decode_stitch_project_json_document ( + json, &document, &last_json_error)) + { + *error = last_json_error.c_str (); + return false; + } + params.width = document.columns; + params.height = document.rows; + pixel_size = document.pixel_size; + for (int y = 0; y < params.height; ++y) + for (int x = 0; x < params.width; ++x) + { + stitch_image &image = images[y][x]; + const stitch_project_json_tile &tile + = document.tiles[(size_t)y * params.width + x]; + image.filename = tile.filename; + image.angle = tile.angle; + image.ratio = tile.ratio; + image.pos = tile.position; + image.img_width = tile.image_width; + image.img_height = tile.image_height; + image.width = tile.screen_width; + image.height = tile.screen_height; + image.xshift = tile.screen_shift_x; + image.yshift = tile.screen_shift_y; + image.param = tile.geometry; + image.left = !x; + image.top = !y; + image.right = x == params.width - 1; + image.bottom = y == params.height - 1; + if (!image.finish_loaded_parameters (this)) + { + *error = "could not initialize JSON stitch tile geometry"; + return false; + } + scr_param.type = image.param.type; + } + if (stitch_info_scale) + stitch_info_scale = images[0][0].param.coordinate1.length () + 1; + return true; + } + /* Legacy text reader: do not drop it when changing the writer. */ /* TODO: hack. */ setlocale(LC_NUMERIC, "C"); if (!expect_keyword (f, HEADER)) @@ -543,6 +661,13 @@ stitch_project::load (FILE *f, const char **error) *error = "error parsing num_images"; return false; } + if (params.width < 1 || params.height < 1 + || params.width > stitching_params::max_dim + || params.height > stitching_params::max_dim) + { + *error = "legacy stitch project dimensions exceed supported grid"; + return false; + } if (!expect_keyword (f, "pixel_size:")) { *error = "expected pixel_size"; diff --git a/src/libcolorscreen/unittests.C b/src/libcolorscreen/unittests.C index 84df74d5a..3c0b2901f 100644 --- a/src/libcolorscreen/unittests.C +++ b/src/libcolorscreen/unittests.C @@ -63,12 +63,281 @@ #include "nmsimplex.h" #include "gsl-solver.h" #include "parameter-archive.h" +#include "stitch-project-json.h" #include "parameter-json-budget.h" using namespace colorscreen; namespace { +/* Native project JSON is lossless and strict; the FILE* entry points must + write JSON while keeping the historical version-1 text reader available. */ +static bool +test_stitch_json_project () +{ + stitch_project_json_data original; + original.columns = 2; + original.rows = 1; + original.pixel_size = 1.25; + for (int x = 0; x != 2; ++x) + { + stitch_project_json_tile tile; + /* UTF-8 names and relative paths are saved unchanged. */ + tile.filename = x ? u8"../tiles/žlutá plate.tiff" + : "../tiles/first image.tiff"; + tile.angle = 1.5 + x; + tile.ratio = 0.75; + tile.position = {(coord_t)(12 + 7 * x), (coord_t)-3.25}; + tile.image_width = 64; + tile.image_height = 48; + tile.screen_width = 16; + tile.screen_height = 12; + tile.screen_shift_x = -2; + tile.screen_shift_y = 3; + tile.geometry.type = Dufay; + tile.geometry.center = {2, 2}; + tile.geometry.coordinate1 = {4, 0}; + tile.geometry.coordinate2 = {0, 4}; + tile.geometry.final_angle = 12.5 + x; + tile.geometry.final_ratio = 1.125; + tile.geometry.final_rotation = 0.25; + tile.geometry.final_mirror = true; + tile.geometry.lens_correction.kr[0] = 0.00025; + original.tiles.push_back (std::move (tile)); + } + std::string json, error; + if (!encode_stitch_project_json_document (original, &json, &error)) + { + fprintf (stderr, "Stitch JSON encode: %s\n", error.c_str ()); + return false; + } + if (json.find ("\"format\":\"org.colorscreen.stitch-project\"") + == std::string::npos + || json.find ("\"screen_geometry\":{\"geometry\"") + == std::string::npos + || json.find ("\"screen_type\":") == std::string::npos + /* No unused capture/MTF/reconstruction defaults are stored in tiles. */ + || json.find ("\"calibration\"") != std::string::npos + || json.find ("\"reconstruction\"") != std::string::npos + || json.find ("color_screen_stitch_project_version: 1") + != std::string::npos) + return false; + stitch_project_json_data parsed; + if (!decode_stitch_project_json_document (json, &parsed, &error) + || parsed.columns != original.columns + || parsed.rows != original.rows + || parsed.pixel_size != original.pixel_size + || parsed.tiles.size () != 2) + { + fprintf (stderr, "Stitch JSON decode: %s\n", error.c_str ()); + return false; + } + for (size_t i = 0; i < parsed.tiles.size (); ++i) + if (parsed.tiles[i].filename != original.tiles[i].filename + || parsed.tiles[i].angle != original.tiles[i].angle + || parsed.tiles[i].ratio != original.tiles[i].ratio + || parsed.tiles[i].position.x != original.tiles[i].position.x + || parsed.tiles[i].position.y != original.tiles[i].position.y + || parsed.tiles[i].image_width != original.tiles[i].image_width + || parsed.tiles[i].image_height != original.tiles[i].image_height + || parsed.tiles[i].screen_shift_x != original.tiles[i].screen_shift_x + || !(parsed.tiles[i].geometry == original.tiles[i].geometry)) + return false; + + std::string canonical; + if (!encode_stitch_project_json_document (parsed, &canonical, &error) + || canonical != json) + return false; + + /* Malformed content must not partly publish the decoded project. */ + const auto reject = [&parsed] (std::string changed) -> bool + { + std::string problem; + return !decode_stitch_project_json_document ( + changed, &parsed, &problem) + && !problem.empty () && parsed.columns == 2 + && parsed.tiles.size () == 2; + }; + if (!reject ("{\"format\":\"org.colorscreen.stitch-project\"," + "\"schema_version\":2,\"grid\":{\"columns\":2,\"rows\":1}," + "\"pixel_size\":1,\"tiles\":[]}") + || !reject (json.substr (0, json.size () - 2)) + || !reject (std::string ("{\"unknown\":null,") + json.substr (1))) + return false; + /* Introducing extraneous nested document domains must fail instead of + silently dropping user edits to fields which stitching does not own. */ + std::string unwanted = json; + const std::string geometry_key = "\"screen_geometry\":{"; + size_t offset = unwanted.find (geometry_key); + if (offset == std::string::npos) + return false; + unwanted.insert (offset + geometry_key.size (), + "\"calibration\":{},"); + if (!reject (unwanted)) + return false; + std::string bad_screen_type = json; + const std::string type_key = "\"screen_type\":\""; + offset = bad_screen_type.find (type_key); + if (offset == std::string::npos) + return false; + const size_t first = offset + type_key.size (); + const size_t last = bad_screen_type.find ('"', first); + if (last == std::string::npos) + return false; + bad_screen_type.replace (first, last - first, "unknown-screen-type"); + if (!reject (bad_screen_type)) + return false; + + /* A hostile geometry size would overflow bitmap_2d's int multiplication + and allocate an unbounded known-screen mask without this guard. */ + std::string huge = json; + const std::string size_key = "\"screen_size\":[16,12]"; + offset = huge.find (size_key); + if (offset == std::string::npos) + return false; + huge.replace (offset, size_key.size (), + "\"screen_size\":[200000,200000]"); + if (!reject (huge)) + return false; + stitch_project_json_data invalid = original; + invalid.tiles[0].screen_width = 200000; + invalid.tiles[0].screen_height = 200000; + std::string preserved = "unchanged"; + if (encode_stitch_project_json_document (invalid, &preserved, &error) + || preserved != "unchanged" || error.empty ()) + return false; + + /* Exercise the actual .csprj writer and JSON read dispatch. */ + auto project = std::make_unique (); + project->params.width = 1; + project->params.height = 1; + project->pixel_size = original.pixel_size; + auto &tile = project->images[0][0]; + tile.filename = original.tiles[0].filename; + tile.angle = original.tiles[0].angle; + tile.ratio = original.tiles[0].ratio; + tile.pos = original.tiles[0].position; + tile.img_width = original.tiles[0].image_width; + tile.img_height = original.tiles[0].image_height; + tile.width = original.tiles[0].screen_width; + tile.height = original.tiles[0].screen_height; + tile.xshift = original.tiles[0].screen_shift_x; + tile.yshift = original.tiles[0].screen_shift_y; + tile.param = original.tiles[0].geometry; + FILE *file = tmpfile (); + if (!file) + return false; + bool saved = project->save (file); + rewind (file); + const int prefix = getc (file); + rewind (file); + auto reloaded = std::make_unique (); + const char *reason = nullptr; + const bool loaded = saved && reloaded->load (file, &reason); + fclose (file); + if (!loaded) + fprintf (stderr, "Stitch project FILE* roundtrip: %s\n", + reason ? reason : "failed to save"); + if (!loaded || prefix != '{' + || reloaded->params.width != 1 || reloaded->params.height != 1 + || reloaded->images[0][0].filename != tile.filename + || !(reloaded->images[0][0].param == tile.param)) + return false; + + /* Linked image paths are relative to the project directory; a drive-root + path is absolute on Windows, including the final alphabet letter Z. */ +#ifdef _WIN32 + reloaded->params.path = "C:\\projects\\"; + if (reloaded->add_path ("Z:\\scans\\plate.tif") + != "Z:\\scans\\plate.tif" + || reloaded->add_path ("c:") != "C:\\projects\\c:") + return false; +#else + reloaded->params.path = "/tmp/stitch-project/"; + if (reloaded->add_path ("../scans/plate.tif") + != "/tmp/stitch-project/../scans/plate.tif" + || reloaded->add_path ("/absolute/plate.tif") + != "/absolute/plate.tif") + return false; +#endif + + /* A real old text project, including the embedded legacy CSP block, + must still load. Re-saving it must produce JSON rather than text. */ + FILE *legacy = tmpfile (); + if (!legacy) + return false; + bool wrote_old + = fprintf (legacy, + "color_screen_stitch_project_version: 1\n" + "num_images: 1 1\npixel_size: 1.250000\n" + "color_screen_stitch_image: 0 0\n") >= 0 + && tile.save (legacy) + && fprintf (legacy, "color_screen_stitch_project_end\n") >= 0; + rewind (legacy); + auto old_project = std::make_unique (); + reason = nullptr; + bool read_old = wrote_old && old_project->load (legacy, &reason); + fclose (legacy); + if (!read_old) + { + fprintf (stderr, "Legacy stitch project load: %s\n", + reason ? reason : "invalid old writer"); + return false; + } + FILE *converted = tmpfile (); + if (!converted) + return false; + bool converted_ok = old_project->save (converted); + rewind (converted); + bool is_json = getc (converted) == '{'; + fclose (converted); + if (!converted_ok || !is_json + || old_project->params.width != 1 + || old_project->images[0][0].filename != tile.filename) + return false; + + /* Reject unsupported legacy dimensions before indexing fixed 10x10 tiles. */ + FILE *bad_legacy = tmpfile (); + if (!bad_legacy) + return false; + fprintf (bad_legacy, + "color_screen_stitch_project_version: 1\n" + "num_images: 11 1\npixel_size: 1.0\n"); + rewind (bad_legacy); + auto oversized = std::make_unique (); + reason = nullptr; + bool accepted = oversized->load (bad_legacy, &reason); + fclose (bad_legacy); + if (accepted || !reason) + return false; + + /* The old keyword reader must enforce the same eager bitmap ceiling + before it can allocate known pixels for a malicious tile. */ + FILE *oversized_tile = tmpfile (); + if (!oversized_tile) + return false; + bool wrote_huge + = fprintf (oversized_tile, + "color_screen_stitch_project_version: 1\n" + "num_images: 1 1\npixel_size: 1.0\n" + "color_screen_stitch_image: 0 0\n") >= 0; + const int old_width = tile.width, old_height = tile.height; + tile.width = 200000; + tile.height = 200000; + wrote_huge = wrote_huge && tile.save (oversized_tile); + tile.width = old_width; + tile.height = old_height; + if (wrote_huge) + wrote_huge = fprintf (oversized_tile, + "color_screen_stitch_project_end\n") >= 0; + rewind (oversized_tile); + auto old_huge = std::make_unique (); + reason = nullptr; + accepted = wrote_huge && old_huge->load (oversized_tile, &reason); + fclose (oversized_tile); + return wrote_huge && !accepted && reason != nullptr; +} + /* Test-only wavelength selection. Optical requests now pass wavelengths as arguments, never through an unsaved scalar in mtf_parameters. */ static void @@ -13454,6 +13723,8 @@ main (int argc, char **argv) [] () { return test_lru_global_memory_budget (); } }, { "cache_statistics_order", "global size/entry/name cache ordering", [] () { return test_cache_statistics_lexicographic_order (); } }, + { "stitch_json_project", "native JSON csprj codec and FILE* project reader", + [] () { return test_stitch_json_project (); } }, { "stitch_tile_global_cache", "decoded stitch tiles use the shared LRU", [] () { return test_stitch_tile_global_cache (); } }, { "cache_memory_preferences", "adaptive and user-selected cache fractions",