Integration: merge main (one test binary per crate, #8644)

# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/scorecard.md
#	scorecard/file_compat.toml
This commit is contained in:
David Mann
2026-10-22 19:55:06 +02:00
330 changed files with 579 additions and 184 deletions
+1
View File
@@ -70,6 +70,7 @@ People trust PhotoCraft with their work, and a crash loses it. A malformed file,
8. **Performance is a feature.** Benchmark heavy operations on a 24–36 MP image in release. Work per tile in parallel (rayon), skip empty tiles, never scan a full surface per frame (cache per revision), and record before/after timings in the dev log. 8. **Performance is a feature.** Benchmark heavy operations on a 24–36 MP image in release. Work per tile in parallel (rayon), skip empty tiles, never scan a full surface per frame (cache per revision), and record before/after timings in the dev log.
9. **Never panic on input** (see *Never crash* above). A command's `run` closure and anything it calls must return `Err`, never panic, for *any* params or document state: validate params, check bounds before indexing or dividing, and reject absurd sizes before allocating. The `panic_hunt` integration test fuzzes every command with adversarial params and must stay green. 9. **Never panic on input** (see *Never crash* above). A command's `run` closure and anything it calls must return `Err`, never panic, for *any* params or document state: validate params, check bounds before indexing or dividing, and reject absurd sizes before allocating. The `panic_hunt` integration test fuzzes every command with adversarial params and must stay green.
10. **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic).
## 4. Picking work ## 4. Picking work
Generated
+3
View File
@@ -4642,6 +4642,7 @@ version = "0.12.0"
dependencies = [ dependencies = [
"arboard", "arboard",
"async-io", "async-io",
"chrono",
"core-graphics2", "core-graphics2",
"dispatch2", "dispatch2",
"eframe", "eframe",
@@ -4810,6 +4811,7 @@ name = "photocraft-engine"
version = "0.12.0" version = "0.12.0"
dependencies = [ dependencies = [
"blake3", "blake3",
"chrono",
"craft-print", "craft-print",
"exr", "exr",
"flate2", "flate2",
@@ -8713,6 +8715,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
name = "xtask" name = "xtask"
version = "0.12.0" version = "0.12.0"
dependencies = [ dependencies = [
"chrono",
"serde", "serde",
"serde_json", "serde_json",
"toml", "toml",
+2
View File
@@ -59,6 +59,8 @@ egui = { version = "0.36", default-features = false }
egui_kittest = "0.36" egui_kittest = "0.36"
rfd = "0.17" rfd = "0.17"
log = "0.4" log = "0.4"
# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1" rayon = "1"
criterion = { version = "0.5", default-features = false } criterion = { version = "0.5", default-features = false }
getrandom = "0.3" getrandom = "0.3"
+1 -1
View File
@@ -221,7 +221,7 @@ Every screenshot here is the real app at work on public-domain art, rendered off
PhotoCraft's PSD support is a standalone crate written from Adobe's public specification and tested against a corpus of real-world files. PhotoCraft's PSD support is a standalone crate written from Adobe's public specification and tested against a corpus of real-world files.
- **Faithful round trips:** opening and re-saving a document renders the same for 307 of the 309 files in the psd-tools test set and 169 of 170 in our mixed ag-psd/psd-tools set (`crates/io/tests/corpus.rs`; fetch the psd-tools set with `cargo xtask corpus --psd-tools`), and anything we don't model yet (raw blocks, descriptors, extras) is carried over instead of being dropped. A re-saved file is not byte-identical to its source: PhotoCraft rewrites image resources, layer records and the composite. Only the standalone `photocraft-psd` crate, parsing and writing a file without the document model, reproduces every parseable corpus file byte for byte (`crates/psd/tests/corpus.rs`). - **Faithful round trips:** opening and re-saving a document renders the same for 307 of the 309 files in the psd-tools test set and 169 of 170 in our mixed ag-psd/psd-tools set (`crates/io/tests/it/corpus.rs`; fetch the psd-tools set with `cargo xtask corpus --psd-tools`), and anything we don't model yet (raw blocks, descriptors, extras) is carried over instead of being dropped. A re-saved file is not byte-identical to its source: PhotoCraft rewrites image resources, layer records and the composite. Only the standalone `photocraft-psd` crate, parsing and writing a file without the document model, reproduces every parseable corpus file byte for byte (`crates/psd/tests/it/corpus.rs`).
- **Pixels that match:** a composite oracle compares our render with Photoshop's own merged image, covering gradient interpolation (Classic, Perceptual and Linear), layer effects, shape strokes, clipping and fill opacity. - **Pixels that match:** a composite oracle compares our render with Photoshop's own merged image, covering gradient interpolation (Classic, Perceptual and Linear), layer effects, shape strokes, clipping and fill opacity.
- **Large documents:** PSB, 16 and 32-bit files, and CMYK and Lab documents open natively. - **Large documents:** PSB, 16 and 32-bit files, and CMYK and Lab documents open natively.
+1 -1
View File
@@ -137,7 +137,7 @@ Detail and the next releases: [docs/roadmap.md](docs/roadmap.md).
| Date | Change | Summary | | Date | Change | Summary |
|---|---|---| |---|---|---|
| 2026-10-17 | minor | Object Finder retains large enclosed foregrounds (#6548) | | 2026-10-17 | minor | Object Finder retains large enclosed foregrounds (#6548) |
| 2026-10-13 | minor | Focus item 1 quoted the oracle floor as 133 of 256; it is 145 of 258 (`crates/io/tests/corpus.rs`, gaps.md G1). Open issues 579 → 953 (2026-10-13) | | 2026-10-13 | minor | Focus item 1 quoted the oracle floor as 133 of 256; it is 145 of 258 (`crates/io/tests/it/corpus.rs`, gaps.md G1). Open issues 579 → 953 (2026-10-13) |
| 2026-10-15 | minor | Russian catalog covers every dynamic UI label the other catalogs translate | | 2026-10-15 | minor | Russian catalog covers every dynamic UI label the other catalogs translate |
| 2026-10-11 | minor | Toolbox count 56 of 68 (12 missing) with the Convert Point tool (#3204) | | 2026-10-11 | minor | Toolbox count 56 of 68 (12 missing) with the Convert Point tool (#3204) |
| 2026-10-10 | minor | Readiness by audience with hours per tier | | 2026-10-10 | minor | Readiness by audience with hours per tier |
+4
View File
@@ -0,0 +1,4 @@
//! The crate's integration tests, linked into one test binary.
mod cli;
mod orientation;
+1
View File
@@ -33,6 +33,7 @@ eframe = { workspace = true, features = ["wgpu", "accesskit", "wayland", "x11",
egui = { workspace = true } egui = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
log = { workspace = true } log = { workspace = true }
chrono = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rfd = { workspace = true } rfd = { workspace = true }
+40 -17
View File
@@ -91,27 +91,36 @@ fn matches(name: &str, target: &str) -> bool {
} }
/// `2026-10-08T07:59:17.728Z` (UTC, milliseconds). A clock before 1970 reads as the epoch. /// `2026-10-08T07:59:17.728Z` (UTC, milliseconds). A clock before 1970 reads as the epoch.
/// A clock far past chrono's year range still converts (the calendar repeats every 400 years), so
/// logging never panics and never silently resets to the epoch for a convertible instant.
pub fn timestamp(t: SystemTime) -> String { pub fn timestamp(t: SystemTime) -> String {
let since = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default(); let since = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
let secs = since.as_secs(); // Pre-1970 is clamped to the epoch by `unwrap_or_default` above. `as_secs` is `u64`; a value
let (days, rem) = (secs / 86_400, secs % 86_400); // past `i64::MAX` seconds saturates rather than falling back to the epoch.
let (y, m, d) = civil_from_days(days); let secs = i64::try_from(since.as_secs()).unwrap_or(i64::MAX);
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.{:03}Z", rem / 3600, rem % 3600 / 60, rem % 60, since.subsec_millis()) let (y, t) = utc_parts(secs, since.subsec_nanos());
// `{:04}` rather than `%Y`, which writes a `+` before years past 9999.
format!("{y:04}-{}", t.format("%m-%dT%H:%M:%S%.3fZ"))
} }
/// Days since 1970-01-01 to a proleptic Gregorian (year, month, day) (Howard Hinnant's /// Unix seconds (+ nanos) → (proleptic Gregorian year, the same wall-clock instant moved into
/// `civil_from_days`, unsigned because `timestamp` clamps at the epoch). /// chrono's range by whole 400-year cycles). The calendar repeats every 400 years (146_097 days),
fn civil_from_days(days: u64) -> (u64, u64, u64) { /// so month, day and time of day come from chrono and only the year is carried, which keeps every
let z = days + 719_468; /// `i64` exact. Replaces the old hand-rolled `civil_from_days`; chrono is the only calendar logic.
let era = z / 146_097; fn utc_parts(secs: i64, nanos: u32) -> (i64, chrono::NaiveDateTime) {
let doe = z % 146_097; use chrono::Datelike;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
let mp = (5 * doy + 2) / 153; // `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let d = doy - (153 * mp + 2) / 5 + 1; let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let m = if mp < 10 { mp + 3 } else { mp - 9 }; let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, nanos.min(999_999_999)));
let y = yoe + era * 400 + u64::from(m <= 2); match (date, time) {
(y, m, d) (Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.and_time(t))
}
_ => (1970, chrono::NaiveDateTime::default()), // unreachable
}
} }
fn numbered(dir: &Path, n: usize) -> PathBuf { fn numbered(dir: &Path, n: usize) -> PathBuf {
@@ -416,6 +425,20 @@ mod tests {
assert_eq!(timestamp(UNIX_EPOCH + Duration::from_secs(1_709_164_800)), "2024-02-29T00:00:00.000Z"); assert_eq!(timestamp(UNIX_EPOCH + Duration::from_secs(1_709_164_800)), "2024-02-29T00:00:00.000Z");
// Before the epoch (a clock set wrong) clamps instead of panicking. // Before the epoch (a clock set wrong) clamps instead of panicking.
assert_eq!(timestamp(UNIX_EPOCH - Duration::from_secs(5)), "1970-01-01T00:00:00.000Z"); assert_eq!(timestamp(UNIX_EPOCH - Duration::from_secs(5)), "1970-01-01T00:00:00.000Z");
// Past chrono's year range still converts via the 400-year cycle (the old code did too,
// through `civil_from_days`): never a silent reset to the epoch. Platform-safe: Windows'
// SystemTime can't hold this instant, so it is only checked where it exists.
// `u64::MAX / 2` seconds == `i64::MAX` seconds → year 292277026596 (old output).
if let Some(far) = UNIX_EPOCH.checked_add(Duration::from_secs(u64::MAX / 2)) {
assert_eq!(timestamp(far), "292277026596-12-04T15:30:07.000Z");
}
// Years past 9999 keep the plain zero-padded year (no `+`).
assert_eq!(timestamp(UNIX_EPOCH + Duration::from_secs(253_402_300_800)), "10000-01-01T00:00:00.000Z");
// 10_000_000_000_000 s is far beyond chrono's range; old output is year 318857.
// Windows' SystemTime holds only up to about year 30828, so this is checked where it fits.
if let Some(far) = UNIX_EPOCH.checked_add(Duration::from_secs(10_000_000_000_000)) {
assert_eq!(timestamp(far), "318857-05-20T17:46:40.000Z");
}
} }
#[test] #[test]
+5
View File
@@ -0,0 +1,5 @@
//! The crate's integration tests, linked into one test binary.
mod command_line_usage;
mod control_exit_code;
mod startup_without_display;
+3 -3
View File
@@ -108,11 +108,11 @@ The same reader is shared, as an independent copy, with VectorCraft's `vectorcra
* Unit tests on synthetic containers built by the `synth` feature (stored, zlib and zstd entries, * Unit tests on synthetic containers built by the `synth` feature (stored, zlib and zstd entries,
checksums, budgets, cycles, hostile streams, every truncation) and property tests of random checksums, budgets, cycles, hostile streams, every truncation) and property tests of random
mutations, here and in `photocraft-io`'s `tests/affinity.rs`. mutations, here and in `photocraft-io`'s `tests/it/affinity.rs`.
* `cargo xtask corpus --affinity` fetches 21 public CC0/MIT documents, including the four * `cargo xtask corpus --affinity` fetches 21 public CC0/MIT documents, including the four
Affinity 3 `.af` files of [samuel-etver/vector-art](https://github.com/samuel-etver/vector-art) Affinity 3 `.af` files of [samuel-etver/vector-art](https://github.com/samuel-etver/vector-art)
(CC0), at pinned commits, each checked against `xtask/affinity-corpus.sha256`. `tests/real_files.rs` (CC0), at pinned commits, each checked against `xtask/affinity-corpus.sha256`. `tests/it/real_files.rs`
parses them; `photocraft-io`'s `tests/affinity_corpus.rs` imports and flattens each one and parses them; `photocraft-io`'s `tests/it/affinity_corpus.rs` imports and flattens each one and
compares it with the thumbnail Affinity saved in it (mean difference 0–4.6 of 255, with a compares it with the thumbnail Affinity saved in it (mean difference 0–4.6 of 255, with a
ceiling per file). ceiling per file).
* `fuzz/` has `cargo-fuzz` targets for the whole reader (`preview`), the object stream (`stream`) * `fuzz/` has `cargo-fuzz` targets for the whole reader (`preview`), the object stream (`stream`)
+1 -1
View File
@@ -1,4 +1,4 @@
//! Unit tests on synthetic inputs; `tests/real_files.rs` covers public Affinity documents. //! Unit tests on synthetic inputs; `tests/it/real_files.rs` covers public Affinity documents.
mod preview { mod preview {
use crate::container::header; use crate::container::header;
@@ -48,8 +48,10 @@ fn isolated(name: &str, check: impl FnOnce()) {
check(); check();
return; return;
} }
// The harness names a test by its module path below the crate root.
let test = module_path!().split_once("::").map_or(name.to_string(), |(_, m)| format!("{m}::{name}"));
let mut child = std::process::Command::new(std::env::current_exe().unwrap()) let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", name, "--nocapture"]) .args(["--exact", &test, "--nocapture"])
.env("PHOTOCRAFT_COMPOUND_CASE", name) .env("PHOTOCRAFT_COMPOUND_CASE", name)
.spawn() .spawn()
.unwrap(); .unwrap();
+4
View File
@@ -0,0 +1,4 @@
//! The crate's integration tests, linked into one test binary.
mod compound_limits;
mod real_files;
+5
View File
@@ -0,0 +1,5 @@
//! The crate's integration tests, linked into one test binary.
mod liquify_source_footprint;
mod retouch_blur_exact;
mod warp_clipped;
+16
View File
@@ -0,0 +1,16 @@
//! The crate's integration tests, linked into one test binary.
mod agent_tasks;
mod agent_tasks_extended;
mod agent_tasks_more;
mod bridge;
mod inactive_save;
mod krita;
mod layered_save_dirty;
mod mcp;
mod mcp_wire;
mod missing_font_warnings;
mod native_save_name;
mod openraster;
mod raw_defaults;
mod smart_filter_authorization;
+1 -1
View File
@@ -75,7 +75,7 @@ sampled gamut boundary. `profiles/photocraft-coated-cmyk.icc` (237 KB, AToB 11
21³ lut16) is that output, dedicated to the public domain (CC0-1.0). Regenerate with 21³ lut16) is that output, dedicated to the public domain (CC0-1.0). Regenerate with
```sh ```sh
PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test regen PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test it -- regen::
``` ```
It is a plausible coated-offset profile, not a measured characterisation: use your printer's It is a plausible coated-offset profile, not a measured characterisation: use your printer's
@@ -1,4 +1,4 @@
//! Throughput check: `cargo test -p photocraft-cms --release --test bench -- --ignored --nocapture`. //! Throughput check: `cargo test -p photocraft-cms --release --test it -- bench:: --ignored --nocapture`.
use photocraft_cms::{Builtin, Intent, Transform}; use photocraft_cms::{Builtin, Intent, Transform};
+6
View File
@@ -0,0 +1,6 @@
//! The crate's integration tests, linked into one test binary.
mod bench;
mod cms;
mod gpu_matrix_shaper;
mod regen;
@@ -1,7 +1,7 @@
//! Regenerates the shipped synthetic CMYK profile when `PHOTOCRAFT_REGEN_PROFILES=1`: //! Regenerates the shipped synthetic CMYK profile when `PHOTOCRAFT_REGEN_PROFILES=1`:
//! //!
//! ```sh //! ```sh
//! PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test regen //! PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test it -- regen::
//! ``` //! ```
//! //!
//! Without the variable the test only checks that the generator's model still matches the //! Without the variable the test only checks that the generator's model still matches the
@@ -348,7 +348,7 @@ fn compression_names_round_trip() {
/// Encode timings on a synthetic ~30 MP texture (6336×4752). Ignored by default; run in release: /// Encode timings on a synthetic ~30 MP texture (6336×4752). Ignored by default; run in release:
/// ///
/// ```sh /// ```sh
/// cargo test --release -p photocraft-codecs --test dds_write -- --ignored --nocapture /// cargo test --release -p photocraft-codecs --test it -- dds_write:: --ignored --nocapture
/// ``` /// ```
#[test] #[test]
#[ignore] #[ignore]
@@ -1,7 +1,7 @@
//! Decode warnings: a file that holds more than was decoded (#523: animations, multi-page //! Decode warnings: a file that holds more than was decoded (#523: animations, multi-page
//! TIFF) or less than it should (#518: truncated JPEG) says so; complete single images don't. //! TIFF) or less than it should (#518: truncated JPEG) says so; complete single images don't.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -1,7 +1,7 @@
//! Magic-number detection, extensions, and the capability table //! Magic-number detection, extensions, and the capability table
//! (including the symmetric read/write guarantee). //! (including the symmetric read/write guarantee).
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -1,6 +1,6 @@
//! `fidelity_warnings` cases, and consistency with what `encode` does. //! `fidelity_warnings` cases, and consistency with what `encode` does.
mod common; use crate::common;
use FidelityWarning as W; use FidelityWarning as W;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -8,7 +8,7 @@
#![cfg(all(feature = "corpus", feature = "heif"))] #![cfg(all(feature = "corpus", feature = "heif"))]
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -1,6 +1,6 @@
//! Decompression-bomb guards. //! Decompression-bomb guards.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
+35
View File
@@ -0,0 +1,35 @@
//! The crate's integration tests, linked into one test binary.
mod camera_raw;
mod common;
mod dds;
mod dds_write;
mod decode_warnings;
mod deep_exr;
mod detect_caps;
mod dpx;
mod exr_cryptomatte;
mod exr_multipart;
mod fidelity;
mod gif_frames;
mod heif;
mod limits;
mod malformed;
mod metadata;
mod orientation;
mod pcx;
mod png_jpeg;
mod pnm_headers;
mod resolution;
mod roundtrip;
mod source_settings;
mod tga;
mod tiff_bench;
mod tiff_exif;
mod tiff_lab;
mod tiff_pages;
mod tiff_photoshop;
mod tiff_regressions;
mod webp_frames;
mod webp_lossy;
mod xpm;
@@ -1,6 +1,6 @@
//! Malformed input must produce errors, never panics or huge allocations. //! Malformed input must produce errors, never panics or huge allocations.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
use proptest::prelude::*; use proptest::prelude::*;
@@ -1,6 +1,6 @@
//! ICC / EXIF / XMP / DPI / text preservation, per format, per caps. //! ICC / EXIF / XMP / DPI / text preservation, per format, per caps.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -36,7 +36,7 @@ fn check_format(format: Format) {
} }
if c.exif && format == Format::Tiff { if c.exif && format == Format::Tiff {
// TIFF writes the block's directories as its own and rebuilds a block from them on // TIFF writes the block's directories as its own and rebuilds a block from them on
// reading (tests/tiff_exif.rs); IFD0's orientation here is the image's. // reading (tests/it/tiff_exif.rs); IFD0's orientation here is the image's.
assert!(!warnings.contains(&FidelityWarning::ExifDropped), "{format:?}"); assert!(!warnings.contains(&FidelityWarning::ExifDropped), "{format:?}");
} else if c.exif { } else if c.exif {
assert_eq!(back.meta.exif, img.meta.exif, "{format:?} exif"); assert_eq!(back.meta.exif, img.meta.exif, "{format:?} exif");
@@ -1,7 +1,7 @@
//! EXIF / TIFF orientation (#285): files are opened upright, and every export //! EXIF / TIFF orientation (#285): files are opened upright, and every export
//! writes Orientation = 1 so the upright pixels are never rotated twice. //! writes Orientation = 1 so the upright pixels are never rotated twice.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
use proptest::prelude::{any, prop_assert, prop_assert_eq, proptest}; use proptest::prelude::{any, prop_assert, prop_assert_eq, proptest};
@@ -372,7 +372,7 @@ fn every_layout_and_depth_orients_and_inverts() {
} }
/// #285 performance budget: turning a 24 MP photo upright on open adds ≤ 50 ms /// #285 performance budget: turning a 24 MP photo upright on open adds ≤ 50 ms
/// (release build: `cargo test --release -p photocraft-codecs --test orientation -- --ignored`). /// (release build: `cargo test --release -p photocraft-codecs --test it -- orientation:: --ignored`).
#[test] #[test]
#[ignore = "timing; run in release"] #[ignore = "timing; run in release"]
fn rotating_24_mp_is_fast() { fn rotating_24_mp_is_fast() {
@@ -1,7 +1,7 @@
//! Format-specific behaviour: Adam7 PNG, JPEG colour models, oracle //! Format-specific behaviour: Adam7 PNG, JPEG colour models, oracle
//! cross-checks against the `image` crate, and the optional PngSuite corpus. //! cross-checks against the `image` crate, and the optional PngSuite corpus.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -2,7 +2,7 @@
//! Lossless formats must reproduce the (planned) conversion of the input //! Lossless formats must reproduce the (planned) conversion of the input
//! exactly; lossy formats must stay above a PSNR threshold. //! exactly; lossy formats must stay above a PSNR threshold.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -1,7 +1,7 @@
//! Reading back the settings a lossy file was saved with: a JPEG's quality from its //! Reading back the settings a lossy file was saved with: a JPEG's quality from its
//! quantisation tables, a WebP's lossy or lossless encoding (and its quality). //! quantisation tables, a WebP's lossy or lossless encoding (and its quality).
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -2,10 +2,10 @@
//! release: //! release:
//! //!
//! ```sh //! ```sh
//! cargo test --release -p photocraft-codecs --test tiff_bench -- --ignored --nocapture //! cargo test --release -p photocraft-codecs --test it -- tiff_bench:: --ignored --nocapture
//! ``` //! ```
mod common; use crate::common;
use common::tiffgen::*; use common::tiffgen::*;
use photocraft_codecs::*; use photocraft_codecs::*;
use std::time::Instant; use std::time::Instant;
@@ -4,7 +4,7 @@
//! offsets past the end, absurd sizes) that must give `Ok` or a clean `Err`, never a panic. //! offsets past the end, absurd sizes) that must give `Ok` or a clean `Err`, never a panic.
//! Every file here is hand-built by `common::tiffgen`. //! Every file here is hand-built by `common::tiffgen`.
mod common; use crate::common;
use common::Rng; use common::Rng;
use common::tiffgen::*; use common::tiffgen::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -2,7 +2,7 @@
//! `Metadata` as opaque bytes: written when present, read from either byte order and from //! `Metadata` as opaque bytes: written when present, read from either byte order and from
//! BigTIFF, and never a panic or an allocation on malformed input. //! BigTIFF, and never a panic or an allocation on malformed input.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
@@ -1,4 +1,4 @@
mod common; use crate::common;
use common::tiffgen::*; use common::tiffgen::*;
use photocraft_codecs::{Image, decode, exif_orientation, upright_exif}; use photocraft_codecs::{Image, decode, exif_orientation, upright_exif};
@@ -3,7 +3,7 @@
//! smaller than lossless on photographic content; alpha and metadata travel in the container; //! smaller than lossless on photographic content; alpha and metadata travel in the container;
//! odd sizes and the limits behave. //! odd sizes and the limits behave.
mod common; use crate::common;
use common::*; use common::*;
use photocraft_codecs::*; use photocraft_codecs::*;
+1
View File
@@ -42,6 +42,7 @@ roxmltree = "0.20"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tempfile = "3" tempfile = "3"
chrono = { workspace = true }
# `file_cmds::same_file` on Windows: the volume serial number and file index, so a hard link to a # `file_cmds::same_file` on Windows: the volume serial number and file index, so a hard link to a
# file is that file (std's `MetadataExt::file_index` is unstable). # file is that file (std's `MetadataExt::file_index` is unstable).
+24 -12
View File
@@ -231,18 +231,7 @@ pub fn now_iso() -> String {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0); let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);
let (days, rem) = (secs.div_euclid(86_400), secs.rem_euclid(86_400)); iso_from_unix(secs)
// Civil date from days since 1970-01-01 (Howard Hinnant's algorithm).
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + i64::from(m <= 2);
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", rem / 3600, rem % 3600 / 60, rem % 60)
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
@@ -250,6 +239,29 @@ pub fn now_iso() -> String {
} }
} }
/// `YYYY-MM-DDTHH:MM:SSZ` for Unix seconds `secs` (UTC, proleptic Gregorian). A time outside
/// chrono's year range still converts (the calendar repeats every 400 years, so the instant is
/// moved into range by whole cycles and only the year is carried, matching the old hand-rolled
/// code), never a silent reset to the epoch. chrono is the only calendar logic.
#[cfg(not(target_arch = "wasm32"))]
fn iso_from_unix(secs: i64) -> String {
use chrono::Datelike;
let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097));
// `rest + 719_163` (1970-01-01 is CE day 719_163) is always a valid CE day, and `sod` < 86_400.
let date = i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
let time = u32::try_from(sod).ok().and_then(|s| chrono::NaiveTime::from_num_seconds_from_midnight_opt(s, 0));
match (date, time) {
(Some(d), Some(t)) => {
let y = i128::from(d.year()) + i128::from(cycles) * 400;
let y = i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX });
// `{:04}` rather than `%Y`, which writes a `+` before years past 9999.
format!("{y:04}-{}", d.and_time(t).format("%m-%dT%H:%M:%SZ"))
}
_ => "1970-01-01T00:00:00Z".to_string(), // unreachable
}
}
fn round4(v: f64) -> f64 { fn round4(v: f64) -> f64 {
(v * 10_000.0).round() / 10_000.0 (v * 10_000.0).round() / 10_000.0
} }
+17
View File
@@ -535,3 +535,20 @@ fn count_add_keeps_first_group_creation_and_targets_the_requested_group() {
let r = s.execute("count.add", json!({"x": 6, "y": 7})).unwrap(); let r = s.execute("count.add", json!({"x": 6, "y": 7})).unwrap();
assert_eq!(r["groups"][1]["points"], json!([[6.0, 7.0]])); assert_eq!(r["groups"][1]["points"], json!([[6.0, 7.0]]));
} }
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn iso_from_unix_matches_the_old_cycle_conversion_including_beyond_chrono() {
use super::iso_from_unix;
assert_eq!(iso_from_unix(0), "1970-01-01T00:00:00Z");
assert_eq!(iso_from_unix(951_782_400), "2000-02-29T00:00:00Z");
assert_eq!(iso_from_unix(1_791_446_357), "2026-10-08T07:59:17Z");
// Years past 9999 keep the plain zero-padded year (no `+`).
assert_eq!(iso_from_unix(253_402_300_800), "10000-01-01T00:00:00Z");
// Beyond chrono's year range the old hand-rolled code still converted; pin that output
// (never the epoch) in both directions and at the i64 extremes.
assert_eq!(iso_from_unix(10_000_000_000_000), "318857-05-20T17:46:40Z");
assert_eq!(iso_from_unix(-10_000_000_000_000), "-314918-08-13T06:13:20Z");
assert_eq!(iso_from_unix(i64::MAX), "292277026596-12-04T15:30:07Z");
assert_eq!(iso_from_unix(i64::MIN), "-292277022657-01-27T08:29:52Z");
}
@@ -1,7 +1,7 @@
//! Artboards recognized from current .af properties move with their children and undo together. //! Artboards recognized from current .af properties move with their children and undo together.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#[path = "../../io/tests/support/current_affinity.rs"] #[path = "../../../io/tests/support/current_affinity.rs"]
mod fixture; mod fixture;
use fixture::{Marker, document}; use fixture::{Marker, document};

Some files were not shown because too many files have changed in this diff Show More