Skip to content

Releases

v0.7.3

Choose a tag to compare

@github-actions github-actions released this 03 Nov 15:17
43b1266

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.HorizontalResolution and VerticalResolution (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 for Rgba32 gets. 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

Choose a tag to compare

@github-actions github-actions released this 03 Nov 10:32
ef1835c

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/ResolutionUnit of IFD0 is used instead. JFIF wins when both carry a density. Before this release JPEG always reported null.
  • TIFF: XResolution, YResolution and ResolutionUnit of the first IFD; a missing unit means inch, as in the TIFF specification. Unit "none" and zero densities report null.
  • PNG is unchanged (pHYs, pixels per metre).
  • BMP: biXPelsPerMeter/biYPelsPerMeter of a Windows DIB header (zero means not recorded).
  • JPEG 2000: the JP2 res box, capture resolution (resc) before display resolution (resd).
  • PNG eXIf, WebP, AVIF, JPEG XL: when the format has no density field of its own, the XResolution/YResolution/ResolutionUnit of 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 HorizontalResolution where it was null before. Code that treated null as "unknown" keeps working; code that copied the value into a PNG now writes a pHYs chunk.

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 02 Nov 16:24
e3da902

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 = 4 can use 40 threads. To cap the total, give every call the same scheduler, for example the ConcurrentScheduler of new 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.MaxDegreeOfParallelism moved to the EncoderOptions base 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 taking maxDegreeOfParallelism and an optional taskScheduler, and the AVIF reconstruction thread now honours the scheduler.
  • For a server: set MaxDegreeOfParallelism to 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.
  • MaxPixels and MaxExpansionRatio are checked against the size the header declares, before anything is allocated. ImageInfo reports the declared size.
  • With ApplyOrientation = true, TargetSize is compared against the upright size. Without it, orientation is not applied and TargetSize compares 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.ConvertTo now converts Cmyk32 and Ycck32 to Rgb24, Rgba32 and Gray8, using R = 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 is Rgba32, as it is with a profile. New overload Image.ConvertToSrgb(IccColorProfile? fallbackCmykProfile) and DecoderOptions.FallbackCmykProfile let 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 (unless DecodeRawYcck), so both reach ConvertTo / ConvertToSrgb as ordinary Cmyk32. With DecodeRawYcck = true the image is Ycck32, which ConvertTo and ConvertToSrgb also accept (they undo the YCbCr transform first).
  • DecoderOptions.TargetPixelFormat = Rgba32 on a profile-less CMYK JPEG or TIFF keeps its existing additive conversion (R = 255 - min(255, C + K)); use the decode followed by ConvertTo for 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.MaxExpansionRatio documentation: 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) overrides MaxExpansionRatio per 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 (a JpegFormatException, 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 with ImageTooLargeException first. Truncated entropy-coded data now fails fast with JpegDecodingException instead of decoding zero blocks across the claimed canvas.
  • AnimatedImage.Load now applies MaxPixels and the ratio limits too.

Full Changelog: v0.7.0...v0.7.1

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 21:12
5a570b8

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) and ImageMetadata.HasLocation, ImageMetadata.ExifLocation: whether EXIF GPS, XMP GPSLatitude/GPSLongitude or 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 JPEG APP13 Photoshop block (with its Photoshop 3.0 signature) is now returned in Image.Metadata.Profiles. No encoder writes it, so it never reaches an output file.
  • A PNG eXIf chunk is now returned as an Exif profile in Image.Metadata.Profiles. Consequence: converting such a PNG to JPEG XL (or saving it to WebP) with the default IncludeMetadata = true now carries its EXIF, GPS included, into the output; set IncludeMetadata = false to 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 writes eXIf, and AVIF writes an Exif item (with matching irot/imir transforms for the orientation) and a colr ICC property; WebP and JPEG XL already did. Previously JPEG and AVIF wrote neither. To get the old output, set IncludeMetadata = false (and IncludeIccProfile = true to 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.Xmp holding the bare packet from JPEG (APP1; previously filed under Exif), PNG (iTXt XML:com.adobe.xmp, compressed or not; previously a Text entry) and TIFF (tag 700), as well as WebP, AVIF and JPEG XL as before. It is written by JPEG, PNG, WebP, AVIF (a mime item) and JPEG XL, and dropped by IncludeMetadata = false. GIF is covered too: the XMP DataXMP application 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.ApplyOrientationInPlace now replaces an EXIF profile in Image.Metadata with a copy whose orientation tag is 1, as ApplyOrientation already did.
  • Image.Load has always decoded only the first frame of an animated GIF or WebP; this is now documented and tested. There is no MaxFrames; use AnimatedImage.Load for more frames.
  • Image.Identify reads the whole stream of a non-seekable JPEG/PNG/WebP/TIFF into memory to scan its metadata.

v0.6.2

Choose a tag to compare

@github-actions github-actions released this 30 Oct 19:47
c320fae

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: EdgeUpsample did 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 TiffUnsupportedFeatureException messages 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 PngDecodingException or WebpDecodingException instead of attempting the allocation. Both formats share a new BoundedPayloadReader under Formats/Shared/IO.

Packaging, docs and tests

  • ContinuousIntegrationBuild is set on GitHub Actions so published packages and symbols carry normalized source paths, and the package now carries a PackageReleaseNotes link to this releases page.
  • LIBRARY_COMPARISON.md was 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 EdgeUpsample clipping fix.
  • Files that previously threw TiffUnsupportedFeatureException for Deflate or non-CCITT FillOrder = 2 now decode, and SOF1 JPEGs that threw JpegDecodingException now decode.
  • Truncated PNG and WebP chunks now throw PngDecodingException or WebpDecodingException with a "Truncated ... chunk" message.

All Merged Pull Requests

  • Refresh library comparison: SkiaSharp 4.154.0 and native reference codecs — #119
  • Speed up AVIF decode: typed planes, threaded recon, SIMD transform, faster entropy — #120
  • Add TIFF Deflate/FillOrder, JPEG SOF1, decoder hardening (0.6.2) — #121

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 27 Oct 20:57
cd88ad7

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.Effort now 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; 0 forces a single tile) and MaxDegreeOfParallelism (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.md no longer describe the lossy encoder as "fixed 8x8 leaves with DC chroma"; the Effort remarks cover the lossy behaviour.
  • AvifEncodeBenchmarks gains effort (0, 5, 9) and tile (single versus 8 rows) scenarios; the avif-encode-profile harness 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 Effort of 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 set Effort = 0 (or 1) and TileRowsLog2 = 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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 00:52
263c76a

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/EPH markers, 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/cmap palettes and cdef channel definitions; opacity channels become alpha
  • Jpeg2000DecoderOptions.ReduceResolutionLevels decodes at half, quarter, ... size by skipping the finest wavelet levels
  • Jpeg2000DecoderOptions.IgnoreColorSpace returns the components as stored, for PDF JPXDecode where 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/.jbig2 files (sequential and random-access) and the embedded segment streams of PDF files, via Jbig2DecoderOptions { 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 FillOrder 2
  • Bare fax streams, such as PDF CCITTFaxDecode data, via CcittFaxDecoderOptions (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 Gray8 with black as 0, the PDF default; BlackIs1 and InvertOutput flip 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.md sections and THIRD-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.SupportedFormats now also lists ccitt, jbig2 and jpeg2000; 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

Choose a tag to compare

@github-actions github-actions released this 17 Oct 00:58
fa34592

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) and TryRecompressJpeg(Stream, out byte[]?), mirroring JxlJpegReconstruction
  • 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 jbrd box. 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 (TryRecompressJpeg returns false, RecompressJpeg throws JxlUnsupportedFeatureException) 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 cjxl at its default effort and 67.0% at effort 9, in about 0.65 s per photo including the self-check
  • djxl rebuilt the original JPEG byte for byte from every file we produced, and this library rebuilt the original from every cjxl file, 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; cjxl accepts 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 internal members 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

Choose a tag to compare

@github-actions github-actions released this 14 Oct 04:41
1ce8593

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 (Effort 1 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.Save now asks each codec whether it accepts float pixels through the new IImageCodec.CanEncodeFloatPixels
  • README and LIBRARY_COMPARISON.md updated, and a new JxlEncodeBenchmarks covers 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 reported HasAlpha == false
  • It now propagates both, matching ApplyOrientation and Resize

Breaking changes & migration notes

  • None. IImageCodec.CanEncodeFloatPixels is a default interface member (default false), so existing custom codecs keep working.

All Merged Pull Requests

  • Add JPEG XL encoding: lossless Modular and lossy VarDCT (0.5.2) — #113
  • Preserve HasAlpha and IsAnimated in Image.Clone (0.5.2) — #114

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 17:32
2339696

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.IsLosslessEncoding was always false for JPEG XL; it is now true for 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.HasPreview says whether decoding can produce an early, lower-fidelity version
  • DecoderOptions.PreviewAvailable is an Action<Image> called during Load; 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.Orientation reports the orientation stored in the file, as a new ImageOrientation enum with the EXIF values 1 to 8
  • Read from JPEG EXIF, PNG eXIf, WebP EXIF, TIFF tag 274, AVIF irot/imir, and the JPEG XL header
  • A PNG eXIf chunk after the image data is not seen by Identify and reports Normal
  • 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, Rotate180 and MirrorVertical, and for square images with the rest
  • Vectorized with Vector128 and cache-tiled for the transposing orientations; a 4000x3000 Rgb24 Rotate90 into 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 Identify reported the rotated size. Now the pixels and ImageInfo.Width/Height are as stored, and the orientation is reported in ImageInfo.Orientation. Callers that relied on pre-rotated JPEG XL output should call image.ApplyOrientation(info.Orientation).
  • PNG Identify reads further. It now scans chunks up to the first image data chunk to find eXIf, so a truncated PNG that ends before the image data now throws from Identify where it could previously succeed.
  • Otherwise additive: ImageInfo gained two optional trailing parameters, HasPreview and Orientation, 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