Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
032342a
feat(tiff): carry ICC, XMP, IPTC-IIM and an Exif sub-IFD
justin13888 Sep 9, 2026
e5d31d0
feat(tiff): carry the C2PA manifest store over the shared placement r…
justin13888 Sep 9, 2026
cacea4b
docs(tiff): record the metadata seam and the C2PA manifest store
justin13888 Sep 9, 2026
d4e55f6
style(tiff): read the multi-page store reservation as a let-chain
justin13888 Sep 9, 2026
d933c95
test(tiff): fuzz the metadata entry points and pin a BigTIFF store
justin13888 Sep 9, 2026
00a3fa1
Merge remote-tracking branch 'origin/feat/442-dng-c2pa-manifest-store…
justin13888 Sep 9, 2026
a8cd376
Merge remote-tracking branch 'origin/feat/442-dng-c2pa-manifest-store…
justin13888 Sep 9, 2026
c26c851
fix(tiff): make the metadata seam agree with its own contracts
justin13888 Sep 9, 2026
98c734a
test(tiff): pin the palette path's C2PA manifest store
justin13888 Sep 9, 2026
20268b8
docs(tiff): record the seam's normalisations and its IFD-0 placement
justin13888 Sep 9, 2026
80f4085
fix(tiff): stop a pointer the metadata never returns from failing the…
justin13888 Sep 9, 2026
65c48cb
fix(tiff): refuse a bad C2PA configuration before every pixel path
justin13888 Sep 9, 2026
f55d10d
docs(tiff): stop claiming more than the seam delivers
justin13888 Sep 9, 2026
01668a8
refactor(tiff): make the C2PA check load-bearing instead of discarded
justin13888 Sep 10, 2026
3102857
fix(tiff): stop a pointer on a discarded page from failing the read
justin13888 Sep 10, 2026
568c195
test(tiff): pin the store on the pixel paths nothing else encoded
justin13888 Sep 10, 2026
1126489
fix(tiff): refuse a C2PA reservation no buffer could hold
justin13888 Sep 10, 2026
175b0ff
fix(tiff): kill the diff mutants the round-4 repairs left alive
justin13888 Sep 10, 2026
d84f179
fix(tiff): refuse an Exif tree deeper than the reader walks back
justin13888 Sep 10, 2026
27a8bea
perf(tiff): find a visited sub-IFD offset in log time, not linear
justin13888 Sep 10, 2026
55739ee
fix(tiff): refuse a store classic TIFF's count word cannot describe
justin13888 Sep 10, 2026
e3d81f1
test(tiff): narrow the discarded-page test to the rule it names
justin13888 Sep 10, 2026
91d7911
docs(tiff): say why the restated pointer match carries no feature guard
justin13888 Sep 10, 2026
b6dd0d2
fix(tiff): keep the classic-count guard out of a mutant's reach
justin13888 Sep 10, 2026
a31a47d
fix(tiff): resolve every standard pointer inside the Exif subtree
justin13888 Sep 10, 2026
6c63188
fix(tiff): keep an Exif directory whose only content is a group
justin13888 Sep 10, 2026
e1e888b
test(tiff): pin the configuration refusal on every public encode surface
justin13888 Sep 10, 2026
85d8277
refactor(tiff): make the classic count bound assertable at its boundary
justin13888 Sep 10, 2026
1aa39fb
docs(tiff): correct what the seam claims about pointers and offsets
justin13888 Sep 10, 2026
81cab24
test(tiff): assert the group a written Exif directory carries, not it…
justin13888 Sep 10, 2026
a908afa
fix(tiff): refuse an Exif pointer tag carried as a plain field
justin13888 Sep 10, 2026
ef93840
docs(tiff): restate the Exif subtree's refusals where each is claimed
justin13888 Sep 10, 2026
31c08a6
test(tiff): fail the IFD-0 scoping test under the regression it names
justin13888 Sep 10, 2026
23271ab
fix(tiff): decide an Exif pointer field by its on-disk type code
justin13888 Sep 10, 2026
bd05a43
fix(tiff): refuse a field and a group under one Exif tag
justin13888 Sep 10, 2026
f18fdb0
fix(tiff): grade a directory that repeats a tag
justin13888 Sep 10, 2026
26a3579
test(tiff): pin the IFD-0 pointer set against the constant it names
justin13888 Sep 10, 2026
1ad07f2
test(tiff): ask libtiff what a directory repeating a tag means
justin13888 Sep 10, 2026
dc49d23
docs(tiff): state the repeated-tag refusal and the type-code rule whe…
justin13888 Sep 10, 2026
b73c8fe
test(tiff): repeat the tag the oracle's readout actually depends on
justin13888 Sep 10, 2026
cc13be1
fix(tiff): give the duplicate-tag anomaly the severity its sibling ca…
justin13888 Sep 10, 2026
2cac68d
test(tiff): pin what the duplicate-tag pass's silent skip promises
justin13888 Sep 10, 2026
d2f45bf
test(tiff): pin the one direction in which the writer is stricter tha…
justin13888 Sep 10, 2026
8c09576
docs(tiff): name the constant the IFD-0 sweep is actually derived from
justin13888 Sep 10, 2026
4117815
test(tiff): derive the second strip's offset from the strip length
justin13888 Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion crates/gamut-tiff/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,54 @@ compression schemes land additively on this frozen surface (see Status).
- **Compression** — uncompressed, PackBits, LZW (+ strip predictor), and Adobe Deflate
(+ horizontal differencing on strips or tiles), plus the bilevel CCITT schemes Modified Huffman
(Group 3 1-D) and Group 4 (T.6).
- **Metadata** — `TiffEncoder::with_metadata` / `TiffDecoder::metadata` carry an Exif sub-IFD
(`ExifIFD`, 34665, as a `gamut_ifd::Ifd`) plus opaque XMP (700), IPTC-IIM (33723), ICC (34675)
and C2PA (52545) payloads — the raw blocks the workspace's metadata facade consumes. Byte
payloads are verbatim; the Exif directory's *entries* are carried unchanged but its ordering is
normalised (ascending tag, duplicate tags collapsed, a child's next-IFD pointer ignored), and a
tag the caller gave both a field and a sub-IFD group is refused rather than normalised — that
would be two entries under one tag, and no reader keeps both. The
blocks live in **IFD 0 only**, so a reader decoding page 3 of a multi-page document alone must
look at IFD 0 for them.
Which pointers are resolved depends on the level, because the two levels answer opposite
questions. **Inside the returned Exif directory all four standard pointer tags** — `SubIFDs`
(330), `ExifIFD` (34665), `GPSInfo` (34853), `InteroperabilityIFD` (40965) — come back as child
directories rather than as stale offsets, since anything left unresolved there is handed to the
caller as an absolute offset into the source file. **At IFD 0 only `ExifIFD` is followed**,
since no other target feeds the seam and a broken one would otherwise hide the blocks. The price
is stated: inside the Exif directory an unreadable target under any of the four fails the read.
A *vendor-private* tag whose value happens to be an offset is not a pointer to anything this
crate can see, so it is carried through and re-encoded verbatim, still holding the source file's
offset — a round trip through such a field is **not** proof the result is pointer-safe.
What the encoder writes the decoder reads back, and the writer is bounded by exactly what the
reader would misread. The Exif directory may nest one further directory
(`InteroperabilityIFD`, EXIF 2.3 §4.6.3, is the one a camera writes), which is as deep as the
reader walks; it may hang a group only off a standard pointer tag; it may not give one tag both
a field and a group; and it may not carry a *plain field* under one of those four tags whose
**on-disk type code** is a pointer's own (`LONG` 4, `IFD` 13, `LONG8` 16, `IFD8` 18), because
the reader decides "pointer" from the entry it parses and would follow that integer as a file
offset. It is the code and not the in-memory `Value` variant that is checked, since a
`Value::Unknown` carries an arbitrary code beside its word and the writer emits that code
verbatim. A value of any other type under those tags is not a pointer to either side and
round-trips unchanged. A caller's directory nested deeper, hung off any other tag, repeating a
tag, or carrying such a field is refused by the encode — with its own message per case — rather
than written into a file this crate could not read back unchanged.
The C2PA manifest store follows C2PA 2.4 §A.3.6 through the shared `gamut_ifd::c2pa` helper it
and `gamut-dng` both call: the entry in the last IFD of the main
chain, the store at the end of the file, and the two §18.5.5 exclusion ranges reported by
`TiffEncoder::encode_with_report` or recovered from any file by `gamut_tiff::c2pa_exclusions`.
`with_c2pa_reserved` writes a zero-filled reservation for an external signer to overwrite in
place; because it is an infallible builder, a length no buffer could hold — or, in a classic
TIFF, none its 32-bit `count` word could describe — is refused by the encode that follows
rather than panicking or costing a whole image's compression first.
- The decoder is hardened against hostile input (`#![forbid(unsafe_code)]`, a size cap, and a
byte-flip fuzz corpus).

**Deferred — planned, additive** (see the [STATUS.md](STATUS.md) scope ledger): YCbCr (§21),
CIE L\*a\*b\* / RGB colorimetry (§20, §23), new-style JPEG-in-TIFF (§22, `Compression = 7`), and
smaller items (CCITT Group 3 2-D, planar config, IEEE-float and 32-bit samples, 4-bit grayscale,
halftone hints).
halftone hints). The metadata payloads are carried as raw bytes rather than parsed here; wiring
them to the typed [`gamut-metadata`](../gamut-metadata) facade is tracked separately.
**Permanently out of scope:** old-style JPEG (§22, `Compression = 6`), deprecated and
unimplementable-as-specified per TIFF Technical Note 2.

Expand Down
107 changes: 106 additions & 1 deletion crates/gamut-tiff/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,102 @@ Cross-depth requests resolve rather than fail: 8-bit widens to 16-bit by `×257`
narrows to 8-bit by truncation (lossy, documented). Evidence: `tests/high_bit_depth.rs`, pixel-exact
against libtiff in both directions.

**Added since v1.0 (semver-minor) — the metadata seam and the C2PA manifest store (issue #446).**
Until now the crate had no metadata surface at all: `tags.rs` named XMP (700), IPTC/NAA (33723),
ICC (34675) and the Exif/GPS/Interop pointers only so `deconstruct` would not flag them unknown,
and a caller wanting any of them dropped to the re-exported `gamut-ifd` spine. `TiffMetadata`
(`#[non_exhaustive]`, built through `new` + `with_*`) is now written by
`TiffEncoder::with_metadata` on the strip, tile and multi-page paths alike and read back by
`TiffDecoder::metadata`. XMP, IPTC-IIM, ICC and C2PA are **opaque bytes carried verbatim** — the
raw blocks the workspace's metadata facade consumes, as `gamut-png` and `gamut-webp` hand them
over — so this crate parses, validates and reconciles none of them; the `ExifIFD` is handed over
as a `gamut_ifd::Ifd`, because it *is* a directory the decoder has already walked.

Three consequences are contractual rather than incidental, and are documented where they are made.
**(a)** The Exif directory's *entries* are carried unchanged but its **ordering is normalised** —
ascending tag (TIFF 6.0 §2 requires it on disk), duplicate tags collapsed to the last, a child's
next-IFD pointer ignored — so "verbatim" is claimed for byte payloads, not for a directory model.
The one shape the model can express and a file cannot — a tag holding both a field and a sub-IFD
group, which is two entries under one tag — is refused by the encode rather than normalised,
because normalising it means silently choosing which of the two the caller meant.
**(b)** Which pointer tags the reader resolves depends on the **level**, and one rule decides it
at both: a pointer is followed exactly when its target belongs to a directory `TiffMetadata` hands
back. At **IFD 0** that is `ExifIFD` alone. `SubIFDs` and `GPSInfo` are out, because their targets
feed no field of `TiffMetadata` and are never re-encoded, so following them there could only add
failure modes — and did: a single dangling `SubIFDs` offset made XMP, IPTC, ICC and C2PA
unreachable on a file whose pixels decode perfectly, and two pages sharing one thumbnail directory
tripped the reader's cross-chain loop guard. **Inside the Exif subtree all four**
`gamut_ifd::tags::STANDARD_POINTER_TAGS` are resolved — `SubIFDs`, `ExifIFD`, `GPSInfo`,
`InteroperabilityIFD` — because that directory *is* returned to the caller and may be written
back, so a pointer left unresolved there is handed over as a raw absolute offset into the source
file; re-encoding it writes a dangling pointer into a file laid out differently, which the crate's
own `deconstruct` grades `Severity::Error`. `InteroperabilityIFD` is the one EXIF 2.3 §4.6.3 puts
there, but a `GPSInfo` or `SubIFDs` group under `ExifIFD` re-encodes just as badly and the reader
cannot tell a hand-built directory from a camera's. The cost is stated rather than hidden: inside
the Exif subtree an unreadable target under any of the four fails the whole read, the same trade
`ExifIFD` itself already makes. Only *standard* pointer tags are recognised anywhere; a **vendor
private tag holding an offset** is indistinguishable from an integer field, so it is carried
through and re-encoded verbatim, still holding the source file's offset — neither this crate nor
`deconstruct` can grade that, and a round trip through such a field is not proof the result is
pointer-safe. The same rule scopes *where* the walk runs: **IFD 0's subtree and no other page's**.
Every later page of a multi-page document feeds one field — the C2PA manifest store, whose entry
holds the store's bytes rather than an offset — so a *pointer* there is a discarded target too,
and a dangling `ExifIFD` on page 1, or two pages naming one Exif directory, used to fail the whole
read. `gamut_ifd::read_tree` can be scoped neither way: it resolves the one flat list it is given
at every node of every page. **(c)** The blocks live in **IFD 0 only**, so a reader decoding
page 3 of a multi-page document alone sees none of them; duplicating an ICC profile onto every
page is the worse outcome, and IFD 0 is where a reader conventionally looks. **(d)** The **writer
is bounded by what the reader accepts**, and it is bounded by inspecting *what the reader
inspects*, with a distinct message per case since they are distinct mistakes. *Depth*: the walk
stops two levels under IFD 0 — the deepest tree the followed tags legitimately reach (EXIF 2.3
§4.6.3) — and an Exif directory a caller nested deeper is refused. *Tag*: a group hung off
anything outside the standard pointer tags comes back as a raw offset, so it is refused too.
*Field*: the reader decides "pointer" from a directory's **fields**, while a caller builds one
from **groups**, so checking only the groups left the writer blind to the one shape the reader
misreads — a standard pointer tag carried as a plain `LONG`, which encoded cleanly and then failed
this crate's own reader with `read out of bounds` or `sub-IFD pointer loop`. A field under one of
the four tags whose **on-disk type code** is a pointer's own (`LONG` 4, `IFD` 13, `LONG8` 16,
`IFD8` 18) is therefore refused; any other type under those tags is not a pointer to either side
and round-trips unchanged. The discriminator is the *code*, not the in-memory `Value` variant,
because the two disagree for exactly one shape: `Value::Unknown` carries an arbitrary code beside
its value word, the writer emits the code verbatim, and the reader classifies by it — so an
`Unknown` built at 4, 13, 16 or 18 was a plain field to a variant-shaped check and a pointer to
the reader. *Pair*: a tag given both a field and a group is two entries under one tag, which
TIFF 6.0 §2 does not allow and which readers resolve in opposite directions — this crate keeps the
last occurrence, libtiff ignores everything after the first — so it is refused too. All four are
`Error::InvalidInput` from `with_metadata`'s encode before any pixel work, on **every** public
encode surface, rather than a well-formed file this crate cannot read back unchanged. The bound is
the spec's; that a narrower one is also easier to assert is not on its own a reason to narrow a
contract.

The C2PA manifest store is the one carrier with a placement rule of its own, and that rule is not
restated here: `gamut_ifd::c2pa` owns C2PA 2.4 §A.3.6 (tag 52545 / `0xCD41`, type `UNDEFINED`, one
store per asset, its entry in the **last IFD of the main chain**, its bytes at the **end of the
file**) and §18.5.5 (the two disjoint exclusion ranges — the store, and the `count` field of its
entry — that a `c2pa.hash.data` binding excludes; §18.7.3.3 leaves that the only binding a TIFF
asset has), and `gamut-dng` calls the same helper, so the two formats cannot drift.
`with_c2pa_reserved` writes a zero-filled reservation for an external signer to overwrite in place.
It is an infallible builder, so every bound on `len` is enforced by the **encode** that follows, as
`Error::InvalidInput`, on every entry point: below the store's minimum (8 bytes, 9 in BigTIFF — the
JUMBF box header, and one more than the variant's inline threshold, since a value that packs inline
is not the run at the end of the file §A.3.6 wants), above what a buffer can hold, since past
`isize::MAX` a `Vec<u8>` cannot exist and `vec![0; len]` said so by panicking with a capacity
overflow, and — in a classic TIFF — above the 4 GiB its entry's 32-bit `LONG` `count` could
describe, which the encode would otherwise discover only after compressing the image and
zero-filling the reservation (BigTIFF's count is 64-bit and has no such bound). The reservation is
taken fallibly instead, so a caller's number cannot panic a library path. A store whose *offset*
would pass classic TIFF's 4 GiB limit depends on the size of the file it lands after, so that one
stays `gamut_ifd::c2pa::append_store`'s, refused once the file exists. What stays outside this
crate's reach is the allocator's: a reservation the machine has no memory for aborts, as any
oversized allocation in Rust does. `encode_with_report` reports the ranges, and `c2pa_exclusions`
recovers them from any TIFF's bytes — including files written through `encode_palette8` or
`encode_pages_rgb8`, which the object-safe `EncodeImage` seam cannot report through. The store's
bytes are never byte-swapped: the header's `ByteOrder` does not govern them (§A.3.6). Tag 52545
joins `is_known_tag`, so the v1 zero-tolerance byte accounting claims the store as its entry's typed
value span rather than reporting an unknown private tag and an unaccounted trailer. Evidence:
`tests/c2pa.rs`, `tests/metadata.rs`, and libtiff decoding a store-carrying file pixel-exact
(`tests/oracle_metadata.rs`).

**Deferred (planned, additive).** Each plugs into the existing strip/tile pipeline and libtiff
oracle the way every codec above did:

Expand All @@ -84,7 +180,9 @@ oracle the way every codec above did:
4-bit grayscale; 16-bit palette (`ColorMap` indices stay 8-bit); `Cmyk16`/`GrayAlpha16`
presentation (no such `gamut-core` pixel type — a 16-bit CMYK page decodes through `Cmyk8` by
narrowing, or `Rgb16` with the fourth sample dropped); halftone hints (§17); document-storage
metadata tags (§12 beyond `PageNumber`).
metadata tags (§12 beyond `PageNumber`); **typed metadata** — the seam above carries raw
payloads only, and wiring them to the `gamut-metadata` facade's models is deliberately left out
(adding that dependency edge is the metadata epic's job, not this crate's).

**Additivity guarantee:** each deferred row lands semver-minor — a new variant on a
`#[non_exhaustive]` enum (`Compression`, `PhotometricInterpretation`, `Predictor`), a new builder
Expand Down Expand Up @@ -127,6 +225,13 @@ The API was frozen after a full-surface review; the additions and breaks:
`Rgba16`. All new items; nothing existing was reshaped. The one behavioural change is that a
16-bit page requested as an 8-bit pixel type now returns `Ok` (narrowed) where it previously
returned `Err(Unsupported)`.
- **Additions since the freeze (#446)** — `TiffMetadata`, `TiffEncodeReport`, `c2pa_exclusions`,
`tags::C2PA_MANIFEST_STORE`, `TiffEncoder::{with_metadata, with_c2pa_reserved,
encode_with_report}`, `TiffDecoder::metadata`, and the `C2paExclusions` re-export that keeps the
closure complete. All new items; nothing existing was reshaped. `TiffMetadata` is
`#[non_exhaustive]` from the start — a sixth carrier must not cost a major, which is exactly
what an exhaustive struct cost `gamut-dng`. The one behavioural change is that tag 52545 is no
longer reported as an unknown tag by `deconstruct`, since the crate now reads and writes it.
- **Documented freeze rationales** — `UnknownTag.field_type` stays a raw `u16` (unrecognised
on-disk type codes must be representable); `Anomaly`'s `detail` strings are human-readable
diagnostics whose wording is not contractual.
Expand Down
57 changes: 57 additions & 0 deletions crates/gamut-tiff/src/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use gamut_ifd::{ByteOrder, Ifd, read};
use crate::compression::{Compression, ccitt, deflate, lzw, packbits, predictor};
use crate::ifd::{PhotometricInterpretation, Predictor, SampleFormat};
use crate::info::{self, TiffInfo};
use crate::metadata::{self, TiffMetadata};
use crate::palette::Palette8;
use crate::tags;

Expand Down Expand Up @@ -185,6 +186,62 @@ impl TiffDecoder {
info::page_info(ifd, file.order)
}

/// Reads the metadata a TIFF carries, without decoding pixels.
///
/// IFD 0 supplies the XMP, IPTC-IIM and ICC payloads and the `ExifIFD` sub-IFD; the last IFD
/// of the main chain supplies the C2PA manifest store (C2PA 2.4 §A.3.6). Every byte-carried
/// payload comes back **verbatim** — this crate parses none of them — so a block written by
/// [`TiffEncoder::with_metadata`](crate::TiffEncoder::with_metadata) reads back identical.
/// The Exif directory is a directory model rather than a byte range, so what it promises is
/// narrower and is stated on [`TiffMetadata::exif`](crate::TiffMetadata::exif); the shapes it
/// could not promise for are refused by the encode rather than written.
/// Use [`c2pa_exclusions`](crate::c2pa_exclusions) for *where* the store sits.
///
/// ```
/// use gamut_core::{Dimensions, EncodeImage, Gray8, ImageRef};
/// use gamut_tiff::{TiffDecoder, TiffEncoder, TiffMetadata};
///
/// let dims = Dimensions { width: 2, height: 1 };
/// let tiff = TiffEncoder::new()
/// .with_metadata(TiffMetadata::new().with_xmp(b"<x:xmpmeta/>".to_vec()))
/// .encode_to_vec(ImageRef::<Gray8>::new(&[7, 9], dims)?)?;
///
/// let meta = TiffDecoder::new().metadata(&tiff)?;
/// assert_eq!(meta.xmp.as_deref(), Some(&b"<x:xmpmeta/>"[..]));
/// # Ok::<(), gamut_core::Error>(())
/// ```
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] for a malformed header or IFD chain, or for a **followed**
/// pointer that does not resolve into a tree: an out-of-bounds or unparseable target, two
/// pointers naming one directory, or nesting below the `ExifIFD` → `InteroperabilityIFD` pair
/// — two levels under IFD 0, the deepest tree those two tags legitimately reach (EXIF 2.3
/// §4.6.3), and the same bound
/// [`TiffEncoder::with_metadata`](crate::TiffEncoder::with_metadata) writes within.
///
/// Which pointers are followed depends on the **level**, so which ones can fail this call does
/// too. At **IFD 0** only `ExifIFD` (34665) is followed: a `SubIFDs` (330), `GPSInfo` (34853)
/// or `InteroperabilityIFD` (40965) field on the page itself is left as the integer it was
/// read as, so however broken it is it cannot fail this call. **Inside the returned Exif
/// directory all four** are followed, because that directory is handed back and an unresolved
/// pointer in it would be a raw offset into the source file — so there, unlike at IFD 0, an
/// unreadable target under any of the four *does* fail the call. And only IFD 0's subtree is
/// walked at all: a pointer on any later page of a multi-page document is never resolved, not
/// even one naming a directory IFD 0's own subtree also names.
///
/// **This can fail on a file [`decode_image`](DecodeImage::decode_image) decodes happily**,
/// and that is deliberate. Pixel decoding never follows a metadata pointer, so a broken
/// `ExifIFD` offset cannot stop it; this method does follow one, and the alternative to
/// failing is reporting `exif: None` for a directory the file plainly declares — silent loss
/// a caller cannot tell apart from "there is no EXIF here". A caller that wants a partial
/// answer can walk the re-exported [`read`](crate::read) / [`gamut_ifd::read_tree`] spine
/// itself and decide per pointer. (`gamut-dng` degrades instead of failing, because there the
/// metadata is incidental to a raw *image* decode that must still succeed.)
pub fn metadata(&self, data: &[u8]) -> Result<TiffMetadata> {
metadata::read_metadata(data)
}

/// Selects which lossy conversions a typed decode may perform.
///
/// Defaults to [`ConvertPolicy::lossless`], under which a layout that cannot hold the page
Expand Down
Loading
Loading