diff --git a/crates/jpeg/src/tests.rs b/crates/jpeg/src/tests.rs index 02b6657c..3929e643 100644 --- a/crates/jpeg/src/tests.rs +++ b/crates/jpeg/src/tests.rs @@ -19,29 +19,6 @@ fn u16_samples(img: &Decoded) -> &[u16] { } } -/// Local check against the conformance samples (never committed): set `CRAFT_JPEG_SAMPLES` to a -/// directory holding testorig.jpg, testorig12.jpg and testimgari.jpg. -#[test] -#[ignore] -fn conformance_samples() { - let dir = std::env::var("CRAFT_JPEG_SAMPLES").unwrap(); - let read = |n: &str| std::fs::read(format!("{dir}/{n}")).unwrap(); - let (ref8, dimensions) = zune_decode(&read("testorig.jpg")); - for (name, sixteen) in [("testorig12.jpg", true), ("testimgari.jpg", false)] { - let img = decode_default(&read(name)).unwrap(); - assert_eq!((img.width, img.height), dimensions, "{name}"); - assert_eq!(matches!(img.samples, Samples::U16(_)), sixteen, "{name}"); - assert!(!img.damaged, "{name}"); - let px: Vec = match &img.samples { - Samples::U16(v) => v.iter().map(|&v| v as f32 / 257.0).collect(), - Samples::U8(v) => v.iter().map(|&v| v as f32).collect(), - }; - let mad = px.iter().zip(&ref8).map(|(a, &b)| (a - b as f32).abs()).sum::() / px.len() as f32; - eprintln!("{name}: mean abs diff vs testorig.jpg {mad}"); - assert!(mad < 3.0, "{name}: {mad}"); - } -} - // --- A test-only arithmetic encoder (T.81 D.1, F.1.4 and G.1.3), mirroring the decoder --- struct Enc { diff --git a/crates/jpeg/tests/conformance.rs b/crates/jpeg/tests/conformance.rs new file mode 100644 index 00000000..2e81620f --- /dev/null +++ b/crates/jpeg/tests/conformance.rs @@ -0,0 +1,98 @@ +//! Decodes of the libjpeg-turbo test images (tests/data, see SOURCE.md there). + +use craft_jpeg::{Decoded, Limits, Samples, decode}; + +fn fixture(name: &str) -> Vec { + let path = format!("{}/tests/data/{name}", env!("CARGO_MANIFEST_DIR")); + std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")) +} + +fn decode_fixture(name: &str) -> Decoded { + let img = decode(&fixture(name), &Limits::default(), None).unwrap_or_else(|e| panic!("{name}: {e}")); + assert_eq!((img.width, img.height, img.components), (227, 149, 3), "{name}"); + assert!(!img.damaged, "{name}"); + img +} + +/// Mean and maximum absolute difference. +fn diff(a: impl Iterator, b: impl Iterator) -> (f64, f64) { + let (mut sum, mut max, mut n) = (0.0, 0.0f64, 0usize); + for (a, b) in a.zip(b) { + let d = (a - b).abs(); + sum += d; + max = max.max(d); + n += 1; + } + (sum / n.max(1) as f64, max) +} + +/// testimgari-huffman.jpg is testimgari.jpg transcoded by jpegtran: the same coefficients, only +/// the entropy coding differs, so the samples must be identical. +#[test] +fn arithmetic_coded_image_matches_its_huffman_transcode() { + let huffman = decode_fixture("testimgari-huffman.jpg"); + let arithmetic = decode_fixture("testimgari.jpg"); + assert!(matches!(huffman.samples, Samples::U8(_))); + assert!(huffman.samples == arithmetic.samples, "testimgari.jpg decodes differently from testimgari-huffman.jpg"); +} + +/// The 8-bit path against zune-jpeg. Both use centered linear chroma upsampling; the residual is +/// float against integer IDCT and color conversion. +#[test] +fn baseline_image_matches_zune_jpeg() { + let bytes = fixture("testorig.jpg"); + let mut zune = zune_jpeg::JpegDecoder::new(zune_jpeg::zune_core::bytestream::ZCursor::new(&bytes[..])); + let reference = zune.decode().unwrap_or_else(|e| panic!("zune-jpeg: {e:?}")); + let img = decode_fixture("testorig.jpg"); + let Samples::U8(px) = &img.samples else { panic!("testorig.jpg: expected 8-bit samples") }; + assert_eq!(px.len(), reference.len()); + let (mean, max) = diff(px.iter().map(|&v| f64::from(v)), reference.iter().map(|&v| f64::from(v))); + eprintln!("testorig.jpg vs zune-jpeg: mean {mean:.3}, max {max}"); + assert!(mean < MAX_MEAN_8 && max <= MAX_ABS_8, "mean {mean}, max {max}"); +} + +// Observed: mean 0.52, max 3. +const MAX_MEAN_8: f64 = 0.75; +const MAX_ABS_8: f64 = 4.0; + +/// Parse a binary PPM with 16-bit big-endian samples. +fn ppm16(bytes: &[u8]) -> (u32, u32, u32, Vec) { + let mut fields = Vec::new(); + let mut at = 0; + while fields.len() < 4 { + while bytes.get(at).is_some_and(u8::is_ascii_whitespace) { + at += 1; + } + let start = at; + while bytes.get(at).is_some_and(|b| !b.is_ascii_whitespace()) { + at += 1; + } + fields.push(std::str::from_utf8(&bytes[start..at]).unwrap_or_default().to_owned()); + } + assert_eq!(fields[0], "P6"); + let num = |s: &str| s.parse::().unwrap_or_else(|e| panic!("PPM header {s}: {e}")); + let (w, h, maxval) = (num(&fields[1]), num(&fields[2]), num(&fields[3])); + let data = &bytes[at + 1..]; + let px: Vec = data.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect(); + assert_eq!(px.len(), (w * h * 3) as usize); + (w, h, maxval, px) +} + +/// testorig12.jpg against libjpeg-turbo's decode (testorig12.ppm, 12-bit RGB; SOURCE.md says how +/// it was made). libjpeg-turbo uses an integer IDCT and integer color conversion, craft-jpeg +/// floats; the comparison is on the 12-bit scale. +#[test] +fn twelve_bit_image_matches_libjpeg_turbo() { + let (w, h, maxval, reference) = ppm16(&fixture("testorig12.ppm")); + assert_eq!((w, h, maxval), (227, 149, 4095)); + let img = decode_fixture("testorig12.jpg"); + let Samples::U16(px) = &img.samples else { panic!("testorig12.jpg: expected 16-bit samples") }; + assert_eq!(px.len(), reference.len()); + let (mean, max) = diff(px.iter().map(|&v| f64::from(v) * 4095.0 / 65535.0), reference.iter().map(|&v| f64::from(v))); + eprintln!("testorig12.jpg vs libjpeg-turbo (12-bit units): mean {mean:.3}, max {max:.2}"); + assert!(mean < MAX_MEAN_12 && max <= MAX_ABS_12, "mean {mean}, max {max}"); +} + +// Observed: mean 0.50, max 2.46 (of 4095). +const MAX_MEAN_12: f64 = 0.75; +const MAX_ABS_12: f64 = 4.0; diff --git a/crates/jpeg/tests/data/README.ijg b/crates/jpeg/tests/data/README.ijg new file mode 100644 index 00000000..8f376826 --- /dev/null +++ b/crates/jpeg/tests/data/README.ijg @@ -0,0 +1,258 @@ +libjpeg-turbo note: This file has been modified by The libjpeg-turbo Project +to include only information relevant to libjpeg-turbo, to wordsmith certain +sections, and to remove impolitic language that existed in the libjpeg v8 +README. It is included only for reference. Please see README.md for +information specific to libjpeg-turbo. + + +The Independent JPEG Group's JPEG software +========================================== + +This distribution contains a release of the Independent JPEG Group's free JPEG +software. You are welcome to redistribute this software and to use it for any +purpose, subject to the conditions under LEGAL ISSUES, below. + +This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone, +Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson, +Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers, +and other members of the Independent JPEG Group. + +IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee +(also known as JPEG, together with ITU-T SG16). + + +DOCUMENTATION ROADMAP +===================== + +This file contains the following sections: + +OVERVIEW General description of JPEG and the IJG software. +LEGAL ISSUES Copyright, lack of warranty, terms of distribution. +REFERENCES Where to learn more about JPEG. +ARCHIVE LOCATIONS Where to find newer versions of this software. +FILE FORMAT WARS Software *not* to get. +TO DO Plans for future IJG releases. + +Other documentation files in the distribution are: + +User documentation: + usage.txt Usage instructions for cjpeg, djpeg, jpegtran, + rdjpgcom, and wrjpgcom. + *.1 Unix-style man pages for programs (same info as usage.txt). + wizard.txt Advanced usage instructions for JPEG wizards only. + change.log Version-to-version change highlights. +Programmer and internal documentation: + libjpeg.txt How to use the JPEG library in your own programs. + example.c Sample code for calling the JPEG library. + structure.txt Overview of the JPEG library's internal structure. + coderules.txt Coding style rules --- please read if you contribute code. + +Please read at least usage.txt. Some information can also be found in the JPEG +FAQ (Frequently Asked Questions) article. See ARCHIVE LOCATIONS below to find +out where to obtain the FAQ article. + +If you want to understand how the JPEG code works, we suggest reading one or +more of the REFERENCES, then looking at the documentation files (in roughly +the order listed) before diving into the code. + + +OVERVIEW +======== + +This package contains C software to implement JPEG image encoding, decoding, +and transcoding. JPEG (pronounced "jay-peg") is a standardized compression +method for full-color and grayscale images. JPEG's strong suit is compressing +photographic images or other types of images that have smooth color and +brightness transitions between neighboring pixels. Images with sharp lines or +other abrupt features may not compress well with JPEG, and a higher JPEG +quality may have to be used to avoid visible compression artifacts with such +images. + +JPEG is normally lossy, meaning that the output pixels are not necessarily +identical to the input pixels. However, on photographic content and other +"smooth" images, very good compression ratios can be obtained with no visible +compression artifacts, and extremely high compression ratios are possible if +you are willing to sacrifice image quality (by reducing the "quality" setting +in the compressor.) + +This software implements JPEG baseline, extended-sequential, progressive, and +lossless compression processes. Provision is made for supporting all variants +of these processes, although some uncommon parameter settings aren't +implemented yet. We have made no provision for supporting the hierarchical +processes defined in the standard. + +We provide a set of library routines for reading and writing JPEG image files, +plus two sample applications "cjpeg" and "djpeg", which use the library to +perform conversion between JPEG and some other popular image file formats. +The library is intended to be reused in other applications. + +In order to support file conversion and viewing software, we have included +considerable functionality beyond the bare JPEG coding/decoding capability; +for example, the color quantization modules are not strictly part of JPEG +decoding, but they are essential for output to colormapped file formats or +colormapped displays. These extra functions can be compiled out of the +library if not required for a particular application. + +We have also included "jpegtran", a utility for lossless transcoding between +different JPEG processes, and "rdjpgcom" and "wrjpgcom", two simple +applications for inserting and extracting textual comments in JFIF files. + +The emphasis in designing this software has been on achieving portability and +flexibility, while also making it fast enough to be useful. In particular, +the software is not intended to be read as a tutorial on JPEG. (See the +REFERENCES section for introductory material.) Rather, it is intended to +be reliable, portable, industrial-strength code. We do not claim to have +achieved that goal in every aspect of the software, but we strive for it. + +We welcome the use of this software as a component of commercial products. +No royalty is required, but we do ask for an acknowledgement in product +documentation, as described under LEGAL ISSUES. + + +LEGAL ISSUES +============ + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +REFERENCES +========== + +We recommend reading one or more of these references before trying to +understand the innards of the JPEG software. + +The best short technical introduction to the JPEG compression algorithm is + Wallace, Gregory K. "The JPEG Still Picture Compression Standard", + Communications of the ACM, April 1991 (vol. 34 no. 4), pp. 30-44. +(Adjacent articles in that issue discuss MPEG motion picture compression, +applications of JPEG, and related topics.) If you don't have the CACM issue +handy, a PDF file containing a revised version of Wallace's article is +available at http://www.ijg.org/files/Wallace.JPEG.pdf. The file (actually +a preprint for an article that appeared in IEEE Trans. Consumer Electronics) +omits the sample images that appeared in CACM, but it includes corrections +and some added material. Note: the Wallace article is copyright ACM and IEEE, +and it may not be used for commercial purposes. + +A somewhat less technical, more leisurely introduction to JPEG can be found in +"The Data Compression Book" by Mark Nelson and Jean-loup Gailly, published by +M&T Books (New York), 2nd ed. 1996, ISBN 1-55851-434-1. This book provides +good explanations and example C code for a multitude of compression methods +including JPEG. It is an excellent source if you are comfortable reading C +code but don't know much about data compression in general. The book's JPEG +sample code is far from industrial-strength, but when you are ready to look +at a full implementation, you've got one here... + +The best currently available description of JPEG is the textbook "JPEG Still +Image Data Compression Standard" by William B. Pennebaker and Joan L. +Mitchell, published by Van Nostrand Reinhold, 1993, ISBN 0-442-01272-1. +Price US$59.95, 638 pp. The book includes the complete text of the ISO JPEG +standards (DIS 10918-1 and draft DIS 10918-2). + +The original JPEG standard is divided into two parts, Part 1 being the actual +specification, while Part 2 covers compliance testing methods. Part 1 is +titled "Digital Compression and Coding of Continuous-tone Still Images, +Part 1: Requirements and guidelines" and has document numbers ISO/IEC IS +10918-1, ITU-T T.81. Part 2 is titled "Digital Compression and Coding of +Continuous-tone Still Images, Part 2: Compliance testing" and has document +numbers ISO/IEC IS 10918-2, ITU-T T.83. + +The JPEG standard does not specify all details of an interchangeable file +format. For the omitted details, we follow the "JFIF" conventions, revision +1.02. JFIF version 1 has been adopted as ISO/IEC 10918-5 (05/2013) and +Recommendation ITU-T T.871 (05/2011): Information technology - Digital +compression and coding of continuous-tone still images: JPEG File Interchange +Format (JFIF). It is available as a free download in PDF file format from +https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871. +A PDF file of the older JFIF 1.02 specification is available at +http://www.w3.org/Graphics/JPEG/jfif3.pdf. + +The TIFF 6.0 file format specification can be obtained from +http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation +scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious +problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression +tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note +#2 (Compression tag 7). Copies of this Note can be obtained from +http://www.ijg.org/files/. It is expected that the next revision +of the TIFF spec will replace the 6.0 JPEG design with the Note's design. +Although IJG's own code does not support TIFF/JPEG, the free libtiff library +uses our library to implement TIFF/JPEG per the Note. + + +ARCHIVE LOCATIONS +================= + +The "official" archive site for this software is www.ijg.org. +The most recent released version can always be found there in +directory "files". + +The JPEG FAQ (Frequently Asked Questions) article is a source of some +general information about JPEG. It is available at +http://www.faqs.org/faqs/jpeg-faq. + + +FILE FORMAT COMPATIBILITY +========================= + +This software implements ITU T.81 | ISO/IEC 10918 with some extensions from +ITU T.871 | ISO/IEC 10918-5 (JPEG File Interchange Format-- see REFERENCES). +Informally, the term "JPEG image" or "JPEG file" most often refers to JFIF or +a subset thereof, but there are other formats containing the name "JPEG" that +are incompatible with the original JPEG standard or with JFIF (for instance, +JPEG 2000 and JPEG XR). This software therefore does not support these +formats. Indeed, one of the original reasons for developing this free software +was to help force convergence on a common, interoperable format standard for +JPEG files. + +JFIF is a minimal or "low end" representation. TIFF/JPEG (TIFF revision 6.0 as +modified by TIFF Technical Note #2) can be used for "high end" applications +that need to record a lot of additional data about an image. + + +TO DO +===== + +Please send bug reports, offers of help, etc. to jpeg-info@jpegclub.org. diff --git a/crates/jpeg/tests/data/SHA256SUMS b/crates/jpeg/tests/data/SHA256SUMS new file mode 100644 index 00000000..551a5dba --- /dev/null +++ b/crates/jpeg/tests/data/SHA256SUMS @@ -0,0 +1,5 @@ +4672c7f08864cd0a8c73a4fa4b66ca32b635d38464551c1ecf06564ae8c89b38 testimgari.jpg +491679b8057739b3c8e5bacd1e918efb1691d271cbbd69820ff8d480dcb90963 testimgari-huffman.jpg +acc6ec555d41d15b368320edaa3b20958ee6fa97cb6e4a18d1213d5ae8bec73b testorig.jpg +34790770f76db8e60a7765b52ca4edf5f16bc21bcb8c6045ca2efef39a8a013e testorig12.jpg +62d182ed042e243544d3d753c440ada1a6992464717311fe6efd546632f44dc4 testorig12.ppm diff --git a/crates/jpeg/tests/data/SOURCE.md b/crates/jpeg/tests/data/SOURCE.md new file mode 100644 index 00000000..3876fe87 --- /dev/null +++ b/crates/jpeg/tests/data/SOURCE.md @@ -0,0 +1,46 @@ +# Test images + +The JPEG files come from libjpeg-turbo 3.0.0, `testimages/` +(https://github.com/libjpeg-turbo/libjpeg-turbo/tree/3.0.0/testimages), under the IJG License +(README.ijg in this directory). The other two files are derived from them as described below. +`SHA256SUMS` lists every file. + +| File | What it is | +|---|---| +| `testorig.jpg` | 227×149 YCbCr, 2×2 luma sampling, baseline Huffman (SOF0) | +| `testimgari.jpg` | the same image, arithmetic coded (SOF9), one interleaved scan | +| `testimgari-huffman.jpg` | `testimgari.jpg` losslessly transcoded to baseline Huffman (SOF0) | +| `testorig12.jpg` | 227×149 YCbCr, 12-bit extended sequential Huffman (SOF1) | +| `testorig12.ppm` | libjpeg-turbo's decode of `testorig12.jpg`: 12-bit RGB, binary PPM, maxval 4095 | + +`testimgari.jpg` and `testorig.jpg` share their quantization tables but not their coefficients: +libjpeg-turbo decodes them to different pixels. `testimgari-huffman.jpg` holds exactly the +coefficients of `testimgari.jpg`, so the two must decode to identical samples. + +## How the derived files were made + +`testimgari-huffman.jpg`, with jpegtran from libjpeg-turbo 3.1.0 (Alpine 3.22, +`libjpeg-turbo-utils-3.1.0-r0`): + +```sh +jpegtran -copy none testimgari.jpg > testimgari-huffman.jpg +``` + +`testorig12.ppm`, with libjpeg-turbo 3.2.0 through imagecodecs 2026.10.10 (default settings: +integer IDCT `JDCT_ISLOW`, fancy upsampling), run as +`uv run --with imagecodecs --with numpy python make.py testorig12.jpg testorig12.ppm`: + +```python +import sys, imagecodecs, numpy as np +src, dst = sys.argv[1], sys.argv[2] +a = imagecodecs.jpeg8_decode(open(src, "rb").read()) +h, w, c = a.shape +assert c == 3 and a.dtype == np.uint16 and a.max() <= 4095 +with open(dst, "wb") as f: + f.write(b"P6\n%d %d\n4095\n" % (w, h)) + f.write(a.astype(">u2").tobytes()) +``` + +`djpeg -dct int testorig12.jpg` from libjpeg-turbo 3.1.0 writes a byte-identical file. + +"This software is based in part on the work of the Independent JPEG Group." diff --git a/crates/jpeg/tests/data/testimgari-huffman.jpg b/crates/jpeg/tests/data/testimgari-huffman.jpg new file mode 100644 index 00000000..2501c615 Binary files /dev/null and b/crates/jpeg/tests/data/testimgari-huffman.jpg differ diff --git a/crates/jpeg/tests/data/testimgari.jpg b/crates/jpeg/tests/data/testimgari.jpg new file mode 100644 index 00000000..89664874 Binary files /dev/null and b/crates/jpeg/tests/data/testimgari.jpg differ diff --git a/crates/jpeg/tests/data/testorig.jpg b/crates/jpeg/tests/data/testorig.jpg new file mode 100644 index 00000000..9816a0c6 Binary files /dev/null and b/crates/jpeg/tests/data/testorig.jpg differ diff --git a/crates/jpeg/tests/data/testorig12.jpg b/crates/jpeg/tests/data/testorig12.jpg new file mode 100644 index 00000000..861aff98 Binary files /dev/null and b/crates/jpeg/tests/data/testorig12.jpg differ diff --git a/crates/jpeg/tests/data/testorig12.ppm b/crates/jpeg/tests/data/testorig12.ppm new file mode 100644 index 00000000..df60fe91 Binary files /dev/null and b/crates/jpeg/tests/data/testorig12.ppm differ