Repository navigation
Releases
v0.7.3
PeachImage 0.7.3
Release date: November 3, 2026
Full changelog: v0.7.2...v0.7.3
Release tag: v0.7.3
A fix for 0.7.2's pixel density.
Fixes
- Density survives
DecoderOptions.TargetPixelFormat.ImageMetadata.HorizontalResolutionandVerticalResolution(pixels per metre) were read by the decoders but lost when a target pixel format made the decoder build a new image, which is what a caller that always asks forRgba32gets. BMP, TIFF, WebP, AVIF, JPEG XL, GIF and PNG now keep it through the conversion, including the density a WebP, AVIF, JPEG XL or PNG carries only in its EXIF block.
Behaviour notes
- Only the density is carried across that conversion. Profiles (EXIF, ICC, XMP) are still not attached to an image the converter had to rebuild; that is unchanged.
v0.7.2
PeachImage 0.7.2
Release date: November 3, 2026
Full changelog: v0.7.1...v0.7.2
Release tag: v0.7.2
Pixel density is now reported for JPEG, TIFF, BMP, JPEG 2000 and (through EXIF) WebP, AVIF and JPEG XL, in one documented unit. Nothing that worked before changes.
Resolution metadata
| Member | What it does |
|---|---|
ImageMetadata.HorizontalResolution, VerticalResolution |
Now documented as pixels per metre, the unit PNG pHYs always used. JPEG and TIFF fill them too, with inch and centimetre densities converted. |
ImageMetadata.HorizontalDpi, VerticalDpi (new) |
The same values as pixels per inch, or null when the file records none. |
- JPEG: the JFIF density is used when its unit is dots per inch or dots per centimetre. A JFIF header that only gives a pixel aspect ratio (unit 0, which is what PeachImage itself writes) is not a resolution, so the EXIF
XResolution/YResolution/ResolutionUnitof IFD0 is used instead. JFIF wins when both carry a density. Before this release JPEG always reportednull. - TIFF:
XResolution,YResolutionandResolutionUnitof the first IFD; a missing unit means inch, as in the TIFF specification. Unit "none" and zero densities reportnull. - PNG is unchanged (
pHYs, pixels per metre). - BMP:
biXPelsPerMeter/biYPelsPerMeterof a Windows DIB header (zero means not recorded). - JPEG 2000: the JP2
resbox, capture resolution (resc) before display resolution (resd). - PNG
eXIf, WebP, AVIF, JPEG XL: when the format has no density field of its own, theXResolution/YResolution/ResolutionUnitof the EXIF block (as for JPEG). - GIF and the remaining formats record no absolute density and still report
null.
Behaviour notes
- A JPEG or TIFF that carries a density now has a non-null
HorizontalResolutionwhere it wasnullbefore. Code that treatednullas "unknown" keeps working; code that copied the value into a PNG now writes apHYschunk.
v0.7.1
PeachImage 0.7.1
Gaps found while replacing ImageSharp in a web API that decodes untrusted uploads. Every new option defaults to the previous behaviour, so existing callers are unaffected.
1. Bounded parallelism
| Option | Default | What it does |
|---|---|---|
DecoderOptions.MaxDegreeOfParallelism, EncoderOptions.MaxDegreeOfParallelism, ResizeOptions.MaxDegreeOfParallelism |
-1 (up to Environment.ProcessorCount, as before) |
The most threads one call may use. 1 runs the whole call on the calling thread. Output is identical for every value. |
DecoderOptions.TaskScheduler, EncoderOptions.TaskScheduler, ResizeOptions.TaskScheduler |
null (thread pool) |
Runs the call's parallel work on this scheduler, so a host can share one limit across concurrent calls. |
Image.ConvertTo(PixelFormat, int maxDegreeOfParallelism, TaskScheduler? = null) |
n/a | The same limit for pixel-format conversion, which has no options object. |
- The limit is per call, not global: ten concurrent calls with
MaxDegreeOfParallelism = 4can use 40 threads. To cap the total, give every call the same scheduler, for example theConcurrentSchedulerofnew ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, maxConcurrencyLevel: 4). - Applies to JPEG decode (IDCT, chroma upsampling, colour conversion), resize, pixel conversion, JPEG XL, JPEG 2000 and AVIF decode and encode.
AvifEncoderOptions.MaxDegreeOfParallelismmoved to theEncoderOptionsbase class (source and binary compatible) and now also bounds the AVIF loop-filter searches. - The JPEG XL JPEG-recompression and reconstruction APIs (
JxlJpegRecompression.RecompressJpeg/TryRecompressJpeg,JxlJpegReconstruction.ReconstructJpeg/TryReconstructJpeg) gain additive overloads takingmaxDegreeOfParallelismand an optionaltaskScheduler, and the AVIF reconstruction thread now honours the scheduler. - For a server: set
MaxDegreeOfParallelismto a low value (1 to 2) on the decoder and resize options.
2. Scaled decode and upright decode
| Option | Default | What it does |
|---|---|---|
DecoderOptions.ScaleDenominator |
1 |
Decode at 1/2, 1/4 or 1/8 size (ceil(size / N)). |
DecoderOptions.TargetSize |
null |
Pick the largest reduction that still leaves an image at least this big (a 6000x4000 JPEG with (600, 420) decodes at 750x500). |
DecoderOptions.ApplyOrientation |
false |
Return the image upright: the EXIF orientation is applied to the pixels (once, on the already reduced image) and reset in the EXIF profile. |
- JPEG scales in the DCT domain (baseline and progressive), so a reduced decode skips most of the IDCT, upsampling and colour-conversion work and never allocates the full-size planes. Formats without a native reduction decode at full size and are then box-averaged down, so the result size is the same for every format (no CPU saved there). JPEG 2000 and JPEG XL reduce natively too: JPEG 2000 drops resolution levels (its wavelet low-pass differs from a box average, so expect larger differences from a resize), and JPEG XL decodes 1/8 from the DC image of a plain VarDCT still. JPEG XL 1/2 and 1/4, and modular, lossless, alpha and animated files, decode at full size and are then averaged down.
MaxPixelsandMaxExpansionRatioare checked against the size the header declares, before anything is allocated.ImageInforeports the declared size.- With
ApplyOrientation = true,TargetSizeis compared against the upright size. Without it, orientation is not applied andTargetSizecompares against the stored size. - A reduced decode is close to, but not bit-identical to, resizing the full decode.
3. CMYK and YCCK without a colour profile
Image.ConvertTonow convertsCmyk32andYcck32toRgb24,Rgba32andGray8, usingR = 255 x (1 - C) x (1 - K)(likewise G and B; channels scaled to 0..1, 0 meaning no ink). It is an approximation of how the inks look, not a colour-managed conversion. Nothing converts into CMYK or YCCK.Image.ConvertToSrgb()falls back to the same formula when a CMYK image has no usable ICC profile (it used to return the CMYK image unchanged). The result isRgba32, as it is with a profile. New overloadImage.ConvertToSrgb(IccColorProfile? fallbackCmykProfile)andDecoderOptions.FallbackCmykProfilelet a host supply its own CMYK profile for images that have none; nothing is bundled.- The JPEG decoder already de-inverts Adobe CMYK (unless
KeepAdobeCmykInverted) and converts YCCK to standard CMYK (unlessDecodeRawYcck), so both reachConvertTo/ConvertToSrgbas ordinaryCmyk32. WithDecodeRawYcck = truethe image isYcck32, whichConvertToandConvertToSrgbalso accept (they undo the YCbCr transform first). DecoderOptions.TargetPixelFormat = Rgba32on a profile-less CMYK JPEG or TIFF keeps its existing additive conversion (R = 255 - min(255, C + K)); use the decode followed byConvertTofor the multiplicative one.
4. Decompression-bomb guard
- The ratio is now defined precisely: decoded pixel bytes (width x height x bytes per pixel of the decoded format) divided by compressed bytes (stream length from the image start). Realistic per-format guidance is in the
DecoderOptions.MaxExpansionRatiodocumentation: photographic JPEG expands 3 to 30 times (a ceiling of 100 to 200 is generous), while flat lossless graphics reach tens of thousands (this library writes a flat 400x400 RGBA PNG at about 5,500x and a flat 2000x2000 one at about 27,000x). - New
DecoderOptions.MaxExpansionRatios(IReadOnlyDictionary<string, double>, keyed by format name such as"jpeg","png", case-insensitive) overridesMaxExpansionRatioper format. A tight JPEG limit and a loose PNG limit can now coexist, so the global 20,000 workaround can go. - Cheap early refusal: a baseline JPEG whose header claims more blocks than its remaining bytes could hold (an 808-byte file edited to say 8000x8000) throws
JpegDecodingException(aJpegFormatException, the JPEG decoder's one failure type) before any coefficient buffer is allocated, even with no limits set (seekable streams). With a ratio set, the header check refuses it withImageTooLargeExceptionfirst. Truncated entropy-coded data now fails fast withJpegDecodingExceptioninstead of decoding zero blocks across the claimed canvas. AnimatedImage.Loadnow appliesMaxPixelsand the ratio limits too.
Full Changelog: v0.7.0...v0.7.1
v0.7.0
PeachImage 0.7.0 Release Notes
Release date: October 31, 2026
Full changelog: v0.6.2...v0.7.0
Release tag: v0.7.0
Safe ingestion of untrusted uploads. Every new option defaults to the previous behaviour, so existing callers are unaffected.
New options
| Option | Default | What it does |
|---|---|---|
DecoderOptions.AllowedFormats |
null (all formats) |
Names of the formats Image.Load / Image.Identify may accept. Anything else throws UnsupportedFormatException before its decoder runs. |
DecoderOptions.SkipMetadata |
false |
Return no profiles (EXIF, XMP, IPTC, ICC, text) and no location; the PNG decoder does not parse them. |
DecoderOptions.MaxPixels |
null (format ceilings only) |
Refuse an image whose header declares more pixels (ImageTooLargeException). |
DecoderOptions.MaxExpansionRatio |
null |
Refuse an image that would decode to more than this many bytes per byte of file (ImageTooLargeException). |
EncoderOptions.IncludeIccProfile |
null (follows IncludeMetadata) |
Write or drop the ICC profile independently of IncludeMetadata. true makes the JPEG encoder write APP2 ICC segments (it wrote none before). |
ResizeOptions.AlwaysCopy |
false |
Resize always returns a new, independently owned image, even when ResizeMode.Max would return the source. |
ResizeOptions.CopyMetadata |
false |
The resized image carries the source's resolution and profiles (a resize previously dropped them, ICC included). |
New members
ImageInfo.HasLocation,ImageInfo.ExifLocation(trailing optional record parameters) andImageMetadata.HasLocation,ImageMetadata.ExifLocation: whether EXIF GPS, XMPGPSLatitude/GPSLongitudeor IPTC City/Sub-location/Province-State name a place. Read from JPEG, PNG, WebP, TIFF, GIF, AVIF and JPEG XL without decoding pixels.ExifLocation,UnsupportedFormatException,ImageTooLargeException.MetadataProfileKind.Iptc: a JPEGAPP13Photoshop block (with itsPhotoshop 3.0signature) is now returned inImage.Metadata.Profiles. No encoder writes it, so it never reaches an output file.- A PNG
eXIfchunk is now returned as anExifprofile inImage.Metadata.Profiles. Consequence: converting such a PNG to JPEG XL (or saving it to WebP) with the defaultIncludeMetadata = truenow carries its EXIF, GPS included, into the output; setIncludeMetadata = falseto drop it.
Behaviour notes
- Encoders now write EXIF and ICC by default. With the default
IncludeMetadata = true, a JPEG encode writes the image's EXIF (APP1) and ICC profile (APP2), PNG writeseXIf, and AVIF writes anExifitem (with matchingirot/imirtransforms for the orientation) and acolrICC property; WebP and JPEG XL already did. Previously JPEG and AVIF wrote neither. To get the old output, setIncludeMetadata = false(andIncludeIccProfile = trueto keep just the colour profile). XMP is written too (below). - XMP is now decoded and encoded wherever the format has a slot. It is returned as
MetadataProfileKind.Xmpholding the bare packet from JPEG (APP1; previously filed underExif), PNG (iTXtXML:com.adobe.xmp, compressed or not; previously aTextentry) and TIFF (tag 700), as well as WebP, AVIF and JPEG XL as before. It is written by JPEG, PNG, WebP, AVIF (amimeitem) and JPEG XL, and dropped byIncludeMetadata = false. GIF is covered too: theXMP DataXMPapplication extension is read (before the first frame) and written (for a still image, after the colour table) with its magic trailer, so readers that do not know XMP skip it. JPEG extended XMP (packets over 64 KB) is neither read nor written. - GIF, BMP and animated WebP write no metadata (no usable slot, or no source for it).
Image.ApplyOrientationInPlacenow replaces an EXIF profile inImage.Metadatawith a copy whose orientation tag is 1, asApplyOrientationalready did.Image.Loadhas always decoded only the first frame of an animated GIF or WebP; this is now documented and tested. There is noMaxFrames; useAnimatedImage.Loadfor more frames.Image.Identifyreads the whole stream of a non-seekable JPEG/PNG/WebP/TIFF into memory to scan its metadata.
v0.6.2
PeachImage 0.6.2 Release Notes
Release date: October 30, 2026
Full changelog: v0.6.1...v0.6.2
Release tag: v0.6.2
PeachImage 0.6.2 speeds up AVIF decoding by roughly 2 to 6 times and fixes an AVIF edge-upsampling bug. It also adds TIFF Deflate and LSB-first FillOrder decoding and extended sequential (SOF1) JPEG decoding, and hardens the GIF, PNG and WebP decoders against truncated or oversized input.
Highlights
Faster AVIF decoding (#120)
Pinned measurements on an i9-14900K, with libavif running single-threaded for comparison:
| Scenario | 0.6.1 | 0.6.2 | libavif |
|---|---|---|---|
| Photo 4:2:0 | 126.7 ms | ~40 ms | 19.7 ms |
| Lossless 4:4:4 | 559 ms | ~250 ms | 136.8 ms |
| Loop restoration, 8-bit | 408 ms | ~63 ms | 23.4 ms |
| Loop restoration, 10-bit | 316 ms | ~61 ms | 42.0 ms |
- The entropy decoder uses a 64-bit window with bulk refill, unrolled 2- and 4-symbol paths, and a dav1d-style coefficient loop (packed level grid, branchless
read_bool). - Each tile decodes on two threads: one parses and emits per-transform-block reconstruction commands, the other predicts, dequantises, inverse-transforms and adds.
- Planes are typed (
byte[]for 8-bit,ushort[]for 10-bit) with kernels generic over the sample type. - An eight-lane int32 inverse transform runs ahead of the exact 64-bit path, which stays as the fallback and the test oracle.
- Deblocking, CDEF and YUV-to-RGB run in parallel, and loop restoration was rewritten to work by region.
- Bug fix:
EdgeUpsampledid not clip its output. Affected decodes now differ from 0.6.1, and 23 corpus hashes plus the lossless benchmark asset were regenerated. - Decoding stays bit-exact against the dav1d/ffmpeg reference hashes. The encoder is unchanged; for context, lossy 1080p encoding takes 1.76 s against libavif's 0.51 s, and lossless takes 29.6 s against 1.02 s.
TIFF: Deflate and FillOrder
- Deflate-compressed strips (compression 8 and the legacy 32946) now decode, including with the Predictor=2 horizontal-differencing variant. A corrupt Deflate strip throws
TiffDecodingException. FillOrder = 2(LSB-first) is now accepted for uncompressed, LZW, Deflate and PackBits data. Strip bytes are bit-reversed before decompression. CCITT already handled both orders.- The tiled, planar and BigTIFF
TiffUnsupportedFeatureExceptionmessages now say which subset is decoded and point to the README.
JPEG: extended sequential (SOF1)
- 8-bit Huffman-coded SOF1 files now decode like baseline files. 12-bit precision is still rejected.
Hardening against bad input
- GIF: the LZW decoder clears its output buffer first. Previously a truncated or corrupt GIF could expose pixel indices left in a pooled buffer by an earlier decode, so output was not deterministic.
- PNG and WebP: a chunk's declared size is checked against the bytes actually in the stream before the payload is allocated. A tiny file declaring a multi-gigabyte chunk now throws
PngDecodingExceptionorWebpDecodingExceptioninstead of attempting the allocation. Both formats share a newBoundedPayloadReaderunderFormats/Shared/IO.
Packaging, docs and tests
ContinuousIntegrationBuildis set on GitHub Actions so published packages and symbols carry normalized source paths, and the package now carries aPackageReleaseNoteslink to this releases page.LIBRARY_COMPARISON.mdwas refreshed against SkiaSharp 4.154.0 and native reference codecs (libjxl, OpenJPEG, libtiff, libavif), with new native-library benchmarks (#119). The README covers the new TIFF and JPEG support.- New tests cover TIFF FillOrder and Deflate (including the predictor and a corrupt stream), SOF1 decoding and 12-bit rejection, GIF stale-buffer clearing, and the chunk-size guard on seekable and non-seekable streams. The AVIF changes add large-asset hash tests, inverse-transform equivalence tests, an intra-prediction range test and a large-tile lossless round trip.
- The orientation no-allocation test now takes the lowest of five measurements, because it failed intermittently on macOS CI with a few hundred bytes of runtime overhead.
Breaking changes & migration notes
- No API removals or additions.
- AVIF decoded pixels can differ from 0.6.1 for images affected by the
EdgeUpsampleclipping fix. - Files that previously threw
TiffUnsupportedFeatureExceptionfor Deflate or non-CCITTFillOrder = 2now decode, and SOF1 JPEGs that threwJpegDecodingExceptionnow decode. - Truncated PNG and WebP chunks now throw
PngDecodingExceptionorWebpDecodingExceptionwith a "Truncated ... chunk" message.
All Merged Pull Requests
v0.6.1
PeachImage 0.6.1 Release Notes
Release date: October 27, 2026
Full changelog: v0.6.0...v0.6.1
Release tag: v0.6.1
PeachImage 0.6.1 makes lossy AVIF encoding about ten times faster. Encoding the 1920x1080 photo from #116 took 26 to 32 seconds in 0.6.0 and now takes about 1.5 seconds, with output within about 1.5% of the old size (slightly smaller on that image). Lossy file size depends on content, so expect small differences either way. Lossless encoding and AVIF decoding are unchanged.
Highlights
Faster lossy AVIF encoding (#118, fixes #116)
- The lossy encoder used to cost about 61 luma intra-mode candidates (13 modes plus angle deltas) at every partition level, then repeat the search when committing each block, with no pruning and one thread. It now ranks candidates by prediction-residual SATD and fully costs only the best few: angle delta 0 for every mode, with the other deltas refined only for the three best directional modes. Partition-decision estimates use a much smaller shortlist and 13 base chroma modes, and the committed chroma search reuses libaom's "prune chroma modes using the luma winner" table.
AvifEncoderOptions.Effortnow applies to lossy encoding. 0 and 1 skip the luma shortlist and cost every candidate (slowest), the default of 2 keeps the best 16 luma candidates, 3 to 4 keep 10, 5 to 6 keep 6 and 7 to 9 keep 4.- Lossy images 512 pixels or taller are split into tile rows that encode in parallel. New
AvifEncoderOptions.TileRowsLog2(null, the default, chooses from the image height alone, so the same input always produces the same bytes on any machine;0forces a single tile) andMaxDegreeOfParallelism(limits threads, never changes the output). - Unrolled the coefficient context derivation and replaced the per-symbol log2, divide and rounding in the entropy-cost estimate with a precomputed table. Output is byte-identical, and it speeds up lossless as well.
- The deblocking and CDEF candidate searches now run in parallel with the same results as before.
Measurements (1920x1080 photographic image, one encode, i9-14900K with affinity pinning)
| Configuration | Time | Output | PSNR |
|---|---|---|---|
| 0.6.0 | 26-28 s | 198,634 B | 40.40 dB |
Single tile (TileRowsLog2 = 0) |
3.6 s | 195,806 B | 40.37 dB |
| Automatic (4 tile rows), 16 threads | 1.5 s | 198,111 B | 40.39 dB |
| 8 tile rows, 16 threads | 0.8 s | 199,527 B | 40.36 dB |
- Tile boundaries are exact AV1 tile rows, so decoders see ordinary multi-tile streams. ffmpeg decodes them without errors, and its single-tile and tiled decodes agree at about 41 dB PSNR.
- Single-digit-percent differences between separate runs on this machine are noise even when pinned.
Docs, benchmarks and tests
- README and
LIBRARY_COMPARISON.mdno longer describe the lossy encoder as "fixed 8x8 leaves with DC chroma"; theEffortremarks cover the lossy behaviour. AvifEncodeBenchmarksgains effort (0, 5, 9) and tile (single versus 8 rows) scenarios; theavif-encode-profileharness now prints output size and round-trip PSNR and accepts a tile-rows argument.- New tests cover multi-tile round trips (odd sizes, alpha), identical output across thread counts, small images staying single-tile, tile-info parse-back through the decoder's parser, and the shortlist table. The AVIF suite (745 tests) passes on net8.0 and net10.0, including the lossless byte-identical test.
Breaking changes & migration notes
- No API removals. Lossy AVIF output is not byte-identical to 0.6.0: the default
Effortof 2 now prunes the mode search, and images 512 pixels or taller are written as multiple tiles. Round-trip quality is essentially unchanged. For the most exhaustive luma search setEffort = 0(or1) andTileRowsLog2 = 0; expect it to be several times slower. Even then the output is not guaranteed identical to 0.6.0, because the partition-decision chroma estimate was trimmed at every effort level. - Lossless AVIF output is unchanged byte for byte.
All Merged Pull Requests
- Speed up lossy AVIF encode ~10x (0.6.1) — #118
v0.6.0
PeachImage 0.6.0 Release Notes
Release date: October 21, 2026
Full changelog: v0.5.3...v0.6.0
Release tag: v0.6.0
PeachImage 0.6.0 adds decoding for JPEG 2000, JBIG2 and CCITT fax, the three image encodings PDF files use besides JPEG, Flate and DCT. All three load through Image.Load, standalone or taken straight out of a PDF stream. There are no breaking changes.
Highlights
JPEG 2000 (#117)
- JP2 files and raw codestreams (
.jp2,.j2k,.j2c), recognised by their signatures - Reversible 5/3 and irreversible 9/7 wavelets, tiles and tile-parts, precincts, quality layers, all five progression orders and progression order changes, every code-block style (bypass, reset, termination on each pass, vertically causal, predictable termination, segmentation symbols), region-of-interest maxshift,
SOP/EPHmarkers, packed packet headers (PPM/PPT), image and tile offsets, sub-sampled components, and 1 to 16-bit signed or unsigned samples - JP2
colr(sRGB, gray, sYCC, CMYK, ICC profile),pclr/cmappalettes andcdefchannel definitions; opacity channels become alpha Jpeg2000DecoderOptions.ReduceResolutionLevelsdecodes at half, quarter, ... size by skipping the finest wavelet levelsJpeg2000DecoderOptions.IgnoreColorSpacereturns the components as stored, for PDFJPXDecodewhere the PDF supplies the colour space- Output matches OpenJPEG sample for sample on reversible files across about 110 OpenJPEG-written test files
- Not supported (throws
Jpeg2000UnsupportedFeatureException): Part 2 extensions and samples deeper than 16 bits
JBIG2 (#117)
- Standalone
.jb2/.jbig2files (sequential and random-access) and the embedded segment streams of PDF files, viaJbig2DecoderOptions { Embedded = true, Globals = ... } - Generic regions (all four templates, typical prediction, moved adaptive pixels, MMR), refinement regions, symbol dictionaries (arithmetic and Huffman, refinement/aggregate coding, retained contexts, custom tables), text regions, pattern dictionaries and halftone regions, striped pages of unknown height
- Test streams were checked against two independent decoders (jbig2dec through MuPDF, and PDFium)
- Only the first page is decoded. Not supported (throws
Jbig2UnsupportedFeatureException): the extended 12-pixel generic template
CCITT fax (#117)
- TIFF compressions 2 (Modified Huffman), 3 (Group 3, one- and two-dimensional) and 4 (Group 4), including
FillOrder2 - Bare fax streams, such as PDF
CCITTFaxDecodedata, viaCcittFaxDecoderOptions(K,Columns,Rows,EndOfLine,EncodedByteAlign,EndOfBlock,BlackIs1,DamagedRowsBeforeError) - Not supported: the uncompressed extension mode
Decoding images from PDF files
- A PDF stream has no signature to sniff, so the options classes above select the decoder directly and everything stays on
Image.Load. The README gains a "Decoding images from PDF files" section - CCITT and JBIG2 images decode to
Gray8with black as 0, the PDF default;BlackIs1andInvertOutputflip that
Performance (4 logical cores, no SkiaSharp baseline exists for these formats)
- JPEG 2000 1280x960 RGB: 88.5 ms lossless, 44.7 ms lossy
- CCITT Group 4 TIFF 1700x2200: 20.9 ms; JBIG2 850x1100 generic region: 5.9 ms arithmetic, 3.0 ms MMR
- Decoding runs on all cores where it can and pools its working buffers; see
LIBRARY_COMPARISON.md
Docs and tests
- README status entries,
LIBRARY_COMPARISON.mdsections andTHIRD-PARTY-LICENSES.md(OpenJPEG notice for the test files and reference behaviour) updated - New benchmarks for all three decoders
- Corrupted-input tests for all three formats; the full suite passes on net8.0 and net10.0
Breaking changes & migration notes
- None.
Image.SupportedFormatsnow also listsccitt,jbig2andjpeg2000; code that assumed exactly eight formats will see three more. - The TIFF decoder now accepts CCITT compression where it used to throw
TiffUnsupportedFeatureException.
All Merged Pull Requests
- Add JPEG 2000, JBIG2 and CCITT decoding (0.6.0) — #117
v0.5.3
PeachImage 0.5.3 Release Notes
Release date: October 17, 2026
Full changelog: v0.5.2...v0.5.3
Release tag: v0.5.3
PeachImage 0.5.3 adds lossless JPEG recompression to JPEG XL, the encoding counterpart of the JPEG reconstruction added in 0.5.0. A JPEG file goes in, a smaller JPEG XL file comes out, and JxlJpegReconstruction (or libjxl's djxl) turns it back into the identical JPEG. There are no breaking changes.
Highlights
JxlJpegRecompression (#115)
- New public API:
RecompressJpeg(Stream),RecompressJpeg(Stream, Stream)andTryRecompressJpeg(Stream, out byte[]?), mirroringJxlJpegReconstruction - The JPEG's quantized DCT coefficients are re-coded with JPEG XL's entropy coder as a coefficient-preserving VarDCT frame, with the data needed to rewrite the original bytes kept in a
jbrdbox. No pixels are decoded and nothing is lost. The file also decodes like any JPEG XL image, to the pixels the JPEG decodes to - Supported: baseline 8-bit JPEGs, gray or YCbCr, at 4:4:4, 4:2:2, 4:4:0 or 4:2:0, with restart intervals and any metadata. ICC profiles, Exif (including orientation) and XMP move into the JPEG XL file; comments, other markers, and stray bytes between markers or after the image are kept verbatim
- Every result is reconstructed and compared with the input before it is returned. A JPEG that would not come back byte for byte is declined (
TryRecompressJpegreturnsfalse,RecompressJpegthrowsJxlUnsupportedFeatureException) instead of being silently changed - Not supported yet: progressive, arithmetic-coded, 12-bit, RGB and CMYK JPEGs
Results against libjxl 0.12
- 40 photographs of 24 MP: 70.8% of the JPEG size (about 29% smaller), against 69.3% for
cjxlat its default effort and 67.0% at effort 9, in about 0.65 s per photo including the self-check djxlrebuilt the original JPEG byte for byte from every file we produced, and this library rebuilt the original from everycjxlfile, across 118 files with no failures- libjxl decodes the output to the same pixels as this library's decoder, within one level on a real photograph
- On the JPEG test corpora (mostly deliberately malformed inputs), 78 of 431 files are accepted and all reconstruct exactly;
cjxlaccepts 116, and everything we accept it accepts too
Docs and tests
- README gains a "Recompressing a JPEG into JPEG XL" section
- New tests cover the parser round trip, every sampling mode, gray, restart intervals, metadata and odd layouts, a libjxl oracle through ffmpeg, and every accepted JPEG in the corpora
Breaking changes & migration notes
- None. The change is additive; the encoder internals gained a few
internalmembers for the new frame encoder, with no change to existing behavior.
All Merged Pull Requests
- Add lossless JPEG to JPEG XL recompression (0.5.3) — #115
v0.5.2
PeachImage 0.5.2 Release Notes
Release date: October 14, 2026
Full changelog: v0.5.1...v0.5.2
Release tag: v0.5.2
PeachImage 0.5.2 adds JPEG XL encoding, both lossless and lossy, completing the format alongside the decoder added in 0.5.0. It also fixes Image.Clone dropping the alpha and animation flags. There are no breaking changes.
Highlights
JPEG XL encoding (#113)
- Still-image encoding is new; animation encoding remains unsupported
- Lossless: Modular coder with YCoCg, gradient prediction and rANS entropy coding, encoded in parallel 256x256 groups, with ICC profile embedding. Output is bit-exact through this repo's decoder and through libjxl
- Lossy: VarDCT in XYB with 8x8 to 32x32 transforms (
Effort1 to 9), per-tile chroma-from-luma, an edge-preserving filter, and an encoder-side Gaborish inverse at high qualities. Adaptive quantization is optional and off by default, because it made files larger at equal SSIMULACRA2 - Tuned against SSIMULACRA2: roughly level with libjxl effort 7, with geometric-mean size ratios of 0.96 / 1.01 / 1.01 at scores 70 / 80 / 88
- Supports 8-bit, 16-bit and 32-bit float Gray, RGB and RGBA, plus EXIF, XMP and orientation
Image.Savenow asks each codec whether it accepts float pixels through the newIImageCodec.CanEncodeFloatPixels- README and
LIBRARY_COMPARISON.mdupdated, and a newJxlEncodeBenchmarkscovers the encoder
Image.Clone keeps HasAlpha and IsAnimated (#114)
Clone()copied pixels, resolution and profiles but not the two flags, so a clone of an alpha-bearing image reportedHasAlpha == false- It now propagates both, matching
ApplyOrientationandResize
Breaking changes & migration notes
- None.
IImageCodec.CanEncodeFloatPixelsis a default interface member (defaultfalse), so existing custom codecs keep working.
All Merged Pull Requests
v0.5.1
PeachImage 0.5.1 Release Notes
Release date: October 9, 2026
Full changelog: v0.5.0...v0.5.1
Release tag: v0.5.1
PeachImage 0.5.1 is a follow-up to the JPEG XL work in 0.5.0. It reports lossless JPEG XL files as lossless, and it adds early-preview callbacks, image orientation reporting across the decoders, and a vectorized Image.ApplyOrientation. It also stops JPEG XL decode from rotating pixels itself, which is the one behavior change to be aware of.
Highlights
JPEG XL: lossless files are reported as lossless (#111)
ImageInfo.IsLosslessEncodingwas alwaysfalsefor JPEG XL; it is nowtruefor lossless files- A file counts as lossless when its colour channels are not XYB-encoded and its first real frame is Modular
- Lossy files, and files made by recompressing a JPEG (which are lossless only relative to the original JPEG bytes, not the pixels), still report
false - A preview frame, when present, is stepped over so the real image's first frame is the one inspected
Early previews while the full image decodes (#111)
ImageInfo.HasPreviewsays whether decoding can produce an early, lower-fidelity versionDecoderOptions.PreviewAvailableis anAction<Image>called duringLoad; the callback owns and disposes each image, and an exception thrown from it aborts the decode- JPEG XL: the file's preview frame, delivered before the main frame decodes
- PNG: one full-size preview per Adam7 pass except the last (six for a normal interlaced image), with undecoded pixels filled from their decoded neighbours
- JPEG: one preview per progressive scan except the last, starting once DC data for every component has arrived
- Previews honor
TargetPixelFormat; the final image is identical with or without a callback - Not covered yet: interlaced GIF, and AVIF thumbnails and TIFF reduced-resolution images, which are not parsed
Image orientation (#111)
ImageInfo.Orientationreports the orientation stored in the file, as a newImageOrientationenum with the EXIF values 1 to 8- Read from JPEG EXIF, PNG
eXIf, WebPEXIF, TIFF tag 274, AVIFirot/imir, and the JPEG XL header - A PNG
eXIfchunk after the image data is not seen byIdentifyand reportsNormal - Decoders never apply orientation: pixels and dimensions are always as stored
Image.ApplyOrientation (#111)
- Applies an orientation to an already-decoded image so it comes out upright
- Three forms: returns a new image (or the same instance for
Normal), writes into a destination you supply with no allocation, or works in place - In-place works for any size with
MirrorHorizontal,Rotate180andMirrorVertical, and for square images with the rest - Vectorized with
Vector128and cache-tiled for the transposing orientations; a 4000x3000 Rgb24Rotate90into a destination took about 9.5 ms median on a pinned core - The result's EXIF orientation tag is reset to 1 and its resolution values are swapped for rotations; orientation stored in XMP is not rewritten
Breaking changes & migration notes
- JPEG XL decode no longer applies orientation. In 0.5.0, a JPEG XL file with a non-default orientation was decoded already rotated, and
Identifyreported the rotated size. Now the pixels andImageInfo.Width/Heightare as stored, and the orientation is reported inImageInfo.Orientation. Callers that relied on pre-rotated JPEG XL output should callimage.ApplyOrientation(info.Orientation). - PNG
Identifyreads further. It now scans chunks up to the first image data chunk to findeXIf, so a truncated PNG that ends before the image data now throws fromIdentifywhere it could previously succeed. - Otherwise additive:
ImageInfogained two optional trailing parameters,HasPreviewandOrientation, with defaults that preserve existing behavior.
All Merged Pull Requests
- Report lossless JPEG XL as lossless; add previews, orientation and Image.ApplyOrientation (0.5.1) — #111