Skip to content
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ image operations using `Span<T>`, and supports independent-copy cropping for loa
- 🎨 **PNG Codec** - Load non-interlaced PNGs; save 8-bit RGB/RGBA
- 🖨️ **TIFF Codec** - Load/save 8-bit RGB/RGBA/Grayscale TIFF files
- 🗜️ **JPEG Codec** - Load baseline/progressive; save baseline JPEG
- 🎞️ **GIF Codec** - Decode-only load of first GIF frame
- 🎞️ **GIF Codec** - Decode-only load of first GIF frame; `GetInfo` reports the true frame count
- 📐 **SVG Codec** - Rasterize a common SVG subset to a surface
- 🔍 **Header-Only Probing** - `GetInfo` reads headers without decoding pixels
- 🖌️ **Path Filling** - Antialiased nonzero/even-odd fill of vector paths
Expand Down Expand Up @@ -85,6 +85,12 @@ using var reloadedJpeg = JpegCodec.Load("surface.jpg");
// Decode-only: load the first frame of a GIF
using var reloadedGif = GifCodec.Load("surface.gif");

// GetInfo also reports a GIF's true total frame count; it decodes the first frame's
// LZW-compressed pixel data to validate CanDecode, but never resolves those pixels into a
// rendered Surface
var gifInfo = GifCodec.GetInfo("surface.gif");
Console.WriteLine($"Frames: {gifInfo.FrameCount}");

// Decode/rasterize an SVG into a 256x256 surface
using var rasterized = SvgCodec.Load("icon.svg", 256, 256);

Expand Down
69 changes: 44 additions & 25 deletions docs/design/canvas-net/codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ in-memory pixel representation; `SvgCodec` additionally depends on the `Geometry
- **JpegCodec** — hand-rolled loader/saver for a common real-world subset of JPEG files; see
_JpegCodec Unit Design_ (`codecs/jpeg-codec.md`)
- **GifCodec** — hand-rolled, decode-only loader for a common real-world subset of GIF files
(first frame only); see _GifCodec Unit Design_ (`codecs/gif-codec.md`)
(first frame only); `GetInfo` additionally reports the file's true total frame count; see
_GifCodec Unit Design_ (`codecs/gif-codec.md`)
- **SvgCodec** — decode/rasterize-only loader for a common real-world subset of SVG documents; see
_SvgCodec Unit Design_ (`codecs/svg-codec.md`)

Expand All @@ -48,21 +49,29 @@ in-memory pixel representation; `SvgCodec` additionally depends on the `Geometry
public readonly record struct ImageInfo(int Width, int Height, int Channels, bool HasAlpha)
{
public bool CanDecode { get; init; } = true;
public int FrameCount { get; init; } = 1;
}
```

It reports a candidate image's declared width, height, channel count, and alpha presence without
requiring the caller to decode (or even fully read) the file. It is the return type of every
`{Codec}.GetInfo(Stream)` / `{Codec}.GetInfo(string)` method across `BmpCodec`, `PngCodec`,
`TiffCodec`, `JpegCodec`, `GifCodec`, and `SvgCodec`. `CanDecode` is declared as an `init`-only property
outside the primary constructor (rather than a fifth positional parameter) specifically to avoid
changing the compiler-emitted constructor/`Deconstruct` signature — a binary-compatibility
concern, since a fifth positional parameter would break any pre-compiled caller's IL even though
source would still compile unchanged. It defaults to `true` and is set to `false` only by
`PngCodec.GetInfo` when the probed file is well-formed per the PNG specification but declares
Adam7 interlacing, the one case (see below) where a codec's `Load` refuses a file `GetInfo`
otherwise accepts. Because `ImageInfo` is a record struct, `CanDecode` participates in its
generated value equality like every other member.
`TiffCodec`, `JpegCodec`, `GifCodec`, and `SvgCodec`. `CanDecode` and `FrameCount` are both
declared as `init`-only properties outside the primary constructor (rather than positional
parameters) specifically to avoid changing the compiler-emitted constructor/`Deconstruct`
signature — a binary-compatibility concern, since an additional positional parameter would break
any pre-compiled caller's IL even though source would still compile unchanged. `CanDecode`
defaults to `true` and is set to `false` by `PngCodec.GetInfo` when the probed file is well-formed
per the PNG specification but declares Adam7 interlacing (the one well-formed-but-unsupported
case), and by `GifCodec.GetInfo` when the first Image Descriptor's compressed data fails the same
LZW decode `Load` itself performs for that frame (a well-formed container with an undecodable
payload) — see below and _GifCodec Unit Design_ (`codecs/gif-codec.md`) for the exact rationale.
`FrameCount` defaults to `1` and is overridden only by `GifCodec.GetInfo`, the only codec whose
file format can legitimately declare more than one frame (an animation); it reports the file's
true total Image Descriptor count by walking the file's block structure, never resolving any
frame's decoded pixels into a `Surface` — see _GifCodec Unit Design_ (`codecs/gif-codec.md`) for
the exact walk. Because `ImageInfo` is a record struct, `CanDecode` and `FrameCount` both
participate in its generated value equality like every other member.

### Header-Only Probing (`GetInfo`)

Expand Down Expand Up @@ -94,12 +103,18 @@ Adam7-interlacing rejection (every other header-validity check is unconditional,
decodable color-type/bit-depth space now spans the PNG specification's entire legal space) — see
_PngCodec Unit Design_ (`codecs/png-codec.md`) for the exact rationale; the other three raster
codecs still use only the single `enforceMaxDimension` flag, since none of them has a
feature-based `Load` refusal that is independent of header well-formedness. `GifCodec` shares a
similarly-purposed internal header-parsing helper (`ReadLogicalScreenDescriptor`), but with no
boolean flag at all: unlike the other four raster codecs, `GifCodec.GetInfo` never enforces
`Surface.MaxDimension` under any circumstance, since it performs no arithmetic or allocation based
on the reported width/height beyond constructing the returned `ImageInfo` itself — see _GifCodec
Unit Design_ (`codecs/gif-codec.md`) for the exact rationale. `SvgCodec` does not
feature-based `Load` refusal that is independent of header well-formedness. `GifCodec` shares its
per-frame structural-validation helpers (region bounds, minimum code size range, color table
resolution) between `Load` and `GetInfo`'s frame-counting walk, but with no boolean flag at all:
unlike the other four raster codecs, `GifCodec.GetInfo` never enforces `Surface.MaxDimension`
under any circumstance and performs no arithmetic or allocation proportional to the reported
width/height itself (only bounded, per-frame bounds-check arithmetic and a budget-capped sub-block
buffer) — see _GifCodec Unit Design_ (`codecs/gif-codec.md`) for the exact rationale, including how
`GetInfo` never invokes the LZW decoder for any frame after the first, but does attempt an LZW
decode of the first frame's compressed data (reusing `Load`'s own decoder, discarding its decoded
output) purely to determine `CanDecode`, so a first-frame compressed-data corruption that makes
`Load` throw is reflected as `CanDecode == false` rather than making `GetInfo` throw. `SvgCodec`
does not
use this pattern, because
its `Load` overloads take the requested output raster's width/height as ordinary caller-supplied
parameters (not values decoded from the file) and delegate them directly to `Surface`'s own
Expand All @@ -114,15 +129,19 @@ rather than giving up) - see the
_ImageInfo_ section above for the cross-codec invariant these fallbacks exist to uphold: GetInfo
never throws for an input Load would successfully decode.

**Well-formed but unsupported: `UnsupportedImageFeatureException`.** Investigation across all
five raster codecs found exactly one case where a codec's `Load` refuses a file that is
well-formed per its own format specification — PNG's Adam7 interlacing (the other codecs conflate
"unsupported" and "malformed" at `GetInfo`-time already, so this exception type is not currently
thrown by them). `GifCodec` is deliberately not a second such case: a multi-frame GIF is
well-formed and `Load` never refuses it — it decodes only the first frame, by design, so
`GifCodec.GetInfo` always reports `CanDecode == true` (see _GifCodec Unit Design_,
`codecs/gif-codec.md`, for the rationale).
`PngCodec.Load` signals this specific case with `UnsupportedImageFeatureException`
**Well-formed but unsupported/undecodable: `UnsupportedImageFeatureException` and
`ImageInfo.CanDecode == false`.** Investigation across all five raster codecs found exactly one
case where a codec's `Load` refuses a file that is well-formed per its own format specification —
PNG's Adam7 interlacing (the other codecs conflate "unsupported" and "malformed" at `GetInfo`-time
already, so this exception type is not currently thrown by them). `GifCodec` is a related, but
distinct, second case: a multi-frame GIF is well-formed and `Load` never refuses it — it decodes
only the first frame, by design, so a well-formed multi-frame GIF is not, by itself, a
`CanDecode == false` case. However, `GifCodec.GetInfo` does report `CanDecode == false` when the
first frame's compressed data is corrupt enough that `Load`'s LZW decoder would reject it — a
well-formed container with an undecodable payload, detected by `GetInfo` attempting (and
discarding the result of) that same first-frame LZW decode — see _GifCodec Unit Design_,
`codecs/gif-codec.md`, for the full rationale.
`PngCodec.Load` signals its specific case with `UnsupportedImageFeatureException`
rather than `InvalidDataException`, so a caller can distinguish "well-formed but unsupported" from
"malformed" without string-matching `Exception.Message`. This type derives from `IOException`
rather than `InvalidDataException`, because `System.IO.InvalidDataException` is `sealed` in .NET.
Expand Down
Loading
Loading