Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0aba843
Reduce HEIC decode peak RSS
laurenspriem Jul 9, 2026
7dc3604
Avoid copying no-op clean aperture transforms
laurenspriem Jul 9, 2026
49fd799
Apply HEIC clean aperture before RGBA conversion
laurenspriem Jul 9, 2026
7fa78cb
Reuse grid tile RGBA scratch buffer
laurenspriem Jul 9, 2026
9eef4d7
Write oriented HEIC grids directly to RGBA
laurenspriem Jul 9, 2026
8e21d51
Decode image hooks into caller buffers
laurenspriem Jul 9, 2026
dbf4a0f
Fix image hook grid buffer initialization
laurenspriem Jul 9, 2026
942b17f
Keep chroma-unaligned clean apertures out of YUV cropping
laurenspriem Jul 9, 2026
b7a644a
Reject unaligned grid tile origins and dedupe HEIC decode paths
laurenspriem Jul 9, 2026
7332af8
Document image hook input buffering trade-off
laurenspriem Jul 10, 2026
505f5fd
Lock the lazy image adapter's memory and coverage contract
laurenspriem Jul 10, 2026
53dc23a
Deduplicate lazy RGBA decode paths and grid decode plumbing
laurenspriem Jul 10, 2026
ef948d6
Pin the Rust toolchain in CI
laurenspriem Jul 10, 2026
c6552d4
Mark the 709-family sRGB transfer aliasing as a deliberate divergence
laurenspriem Jul 10, 2026
b3552a9
Document that the per-file image hook check is intentional
laurenspriem Jul 10, 2026
e53f619
Feature-gate heic_storage_bit_depth to its image-integration users
laurenspriem Jul 10, 2026
e4b260a
Decode image hooks directly into caller buffers
laurenspriem Jul 10, 2026
359609c
Fill uncovered grid pixels with the converted zero-YUV color
laurenspriem Jul 10, 2026
009c3ef
Cap the hook input pre-allocation at 64 MiB
laurenspriem Jul 10, 2026
fed350a
Derive the AVIF layout probe's bit depth from the sequence header
laurenspriem Jul 10, 2026
7fce2d7
Expose primary-item EXIF through the image hook decoder
laurenspriem Jul 10, 2026
67e3fe9
Add an identity fast path to the RGBA transform-plan mapping
laurenspriem Jul 10, 2026
5b051ef
Route the 16-bit HEIF hook decode through the shared dispatch
laurenspriem Jul 10, 2026
b0fb2bf
Raise the hook input pre-allocation cap to 128 MiB
laurenspriem Jul 10, 2026
251d57a
Read probe SPS metadata from hvcC without assembling the stream
laurenspriem Jul 10, 2026
0ae17c5
Suppress hook EXIF orientation when container transforms bake rotation
laurenspriem Jul 10, 2026
74d68c6
Cap the default hook registration's encoded input buffer at 128 MiB
laurenspriem Jul 10, 2026
9bc10f4
Remove the eager HeifImageDecoder adapter
laurenspriem Jul 10, 2026
8a738b3
Reuse the layout probe's payload extraction in read_image
laurenspriem Jul 10, 2026
ebca8be
Skip grid canvas seeding when tiles cover the descriptor
laurenspriem Jul 10, 2026
e6ddc32
Share uncC component resolution between decode and layout probe
laurenspriem Jul 10, 2026
15cf8e6
Share the per-tile grid driver across owned and slice paste paths
laurenspriem Jul 10, 2026
11ffadc
Compute the YUV clean-aperture crop from the shared bounds helper
laurenspriem Jul 10, 2026
629ff24
Gate the EXIF/irot test builders behind image-integration
laurenspriem Jul 10, 2026
191a8c1
Probe hev1 SPS metadata in place instead of assembling a stream
laurenspriem Jul 10, 2026
f084cbf
Merge the AVIF caller-buffer converters into one generic pipeline
laurenspriem Jul 10, 2026
25a5930
Bound hook input buffering by guardrail
laurenspriem Jul 10, 2026
0f3b729
Install libpng for CI validator
laurenspriem Jul 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
18 changes: 17 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
# Pinned so fmt/clippy verdicts and build behaviour do not drift with the
# runner image's preinstalled stable; bump deliberately, together with the
# local toolchain.
RUST_TOOLCHAIN: 1.90.0

jobs:
lint:
runs-on: ubuntu-latest
Expand All @@ -21,6 +27,11 @@ jobs:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Pin Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal --component clippy,rustfmt
rustup default "$RUST_TOOLCHAIN"

- run: cargo fmt --check

- run: cargo clippy --all-targets --all-features --locked
Expand All @@ -45,12 +56,17 @@ jobs:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Pin Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake pkg-config ffmpeg \
libde265-dev libx265-dev libaom-dev libdav1d-dev \
libopenjp2-7-dev libjpeg-dev libbrotli-dev zlib1g-dev
libopenjp2-7-dev libjpeg-dev libpng-dev libbrotli-dev zlib1g-dev

- name: Fetch pinned libheif (validator source and corpus)
run: |
Expand Down
26 changes: 17 additions & 9 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ fn orientation_to_apply(path: &Path) -> Result<Option<u8>, heic_decoder::DecodeE

Requires `image-integration` feature.

Hook decodes buffer the entire encoded input in memory before decoding; in
exchange, pixels decode straight into the buffer `image` hands over, without
an additional full-frame owned RGBA allocation. Codec-native planes and a
single grid-tile scratch buffer may still be allocated.
`register_image_decoder_hooks()` caps the encoded input buffer at
`DEFAULT_HOOK_MAX_INPUT_BYTES` (128 MiB); register with explicit guardrails
to choose a different `max_input_bytes` bound (or `None` for unbounded).

### 1) Register hooks once at startup

```rust
Expand Down Expand Up @@ -279,15 +287,15 @@ let img = if let Some(orientation) = hint.orientation_to_apply() {
};
```

### 3) Direct adapter usage (optional)

`HeifImageDecoder` constructors:

- `from_bytes[_with_guardrails]`
- `from_read[_with_guardrails]`
- `from_bufread[_with_guardrails]`
- `from_seekable[_with_guardrails]`
- `from_path[_with_guardrails]`
The registered hook decoder also exposes the primary item's EXIF block through
the standard `ImageDecoder::exif_metadata`/`ImageDecoder::orientation` methods,
so generic `image` code (`decoder.orientation()` +
`DynamicImage::apply_orientation`) works without crate-specific helpers. EXIF
orientation stays unapplied in the pixels; applying it remains the caller's
choice. When the primary item carries `irot`/`imir` container transforms —
which decode does bake into the pixels — `orientation()` reports
`NoTransforms` (matching `ExifOrientationHint::should_apply_exif_orientation`)
so callers do not rotate twice; `exif_metadata` still returns the full block.

## Conversion Helpers (image module)

Expand Down
10 changes: 7 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ into this repository.
CI runs this on every pull request (`.github/workflows/tests.yml`): a `lint`
job (`cargo fmt --check`, clippy, `cargo test`) and a `verify` job that
performs the full default-corpus correctness pass, including stress-corpus
generation. The workflow pins the libheif and ente test-fixtures commits it
fetches; bump those pins in the workflow to move the CI validator or corpus
forward deliberately.
generation. The workflow pins the Rust toolchain plus the libheif and ente
test-fixtures commits it fetches; bump those pins in the workflow to move the
CI toolchain, validator, or corpus forward deliberately.

- pixel-for-pixel PNG comparison against an external `heif-dec` validator
- pixel-for-pixel comparison of the `image` crate integration hook output
(`ImageReader`/`DynamicImage::from_decoder`) against the direct Rust decode
for every comparable verifier file, including exact ICC-profile equality
through the hook decoder's `ImageDecoder::icc_profile`
- embedded ICC colour-profile comparison against the validator's PNG output:
when `heif-dec` embeds a profile, the Rust PNG must carry byte-identical
profile data; a Rust-only profile is allowed (the Rust decoder synthesizes
Expand Down
107 changes: 102 additions & 5 deletions scripts/heic_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,74 @@ fn main() -> Result<(), Box<dyn Error>> {
println!("{value}");
Ok(())
}
RS

cat > "$HELPER_DIR/src/bin/heif-image-hook-check.rs" <<'RS'
use heic_decoder::image_integration::register_image_decoder_hooks;
use heic_decoder::{DecodedRgbaPixels, decode_path_to_rgba};
use image::{DynamicImage, ImageDecoder, ImageReader};
use std::error::Error;
use std::path::Path;

fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
if args.len() != 2 {
return Err("Usage: heif-image-hook-check <input.heic|.heif|.avif>".into());
}

let input = Path::new(&args[1]);
let direct = decode_path_to_rgba(input)?;

let _ = register_image_decoder_hooks();
let mut decoder = ImageReader::open(input)?.into_decoder()?;
let icc_profile = decoder.icc_profile()?;
if icc_profile != direct.icc_profile {
return Err("image hook ICC profile differs from direct decode".into());
}

let decoded = DynamicImage::from_decoder(decoder)?;
if decoded.width() != direct.width || decoded.height() != direct.height {
return Err(format!(
"image hook dimensions {}x{} differ from direct decode {}x{}",
decoded.width(),
decoded.height(),
direct.width,
direct.height
)
.into());
}

match (&direct.pixels, decoded) {
(DecodedRgbaPixels::U8(expected), DynamicImage::ImageRgba8(actual))
if expected == actual.as_raw() => {}
(DecodedRgbaPixels::U16(expected), DynamicImage::ImageRgba16(actual))
if expected == actual.as_raw() => {}
(DecodedRgbaPixels::U8(expected), DynamicImage::ImageRgba8(actual)) => {
return Err(format!(
"image hook RGBA8 pixel mismatch: direct_samples={} hook_samples={}",
expected.len(),
actual.as_raw().len()
)
.into());
}
(DecodedRgbaPixels::U16(expected), DynamicImage::ImageRgba16(actual)) => {
return Err(format!(
"image hook RGBA16 pixel mismatch: direct_samples={} hook_samples={}",
expected.len(),
actual.as_raw().len()
)
.into());
}
(DecodedRgbaPixels::U8(_), other) => {
return Err(format!("image hook color mismatch: expected RGBA8, got {:?}", other.color()).into());
}
(DecodedRgbaPixels::U16(_), other) => {
return Err(format!("image hook color mismatch: expected RGBA16, got {:?}", other.color()).into());
}
}

Ok(())
}
RS

cat > "$HELPER_DIR/src/bin/heif-stream-concurrency-bench.rs" <<'RS'
Expand Down Expand Up @@ -789,6 +857,10 @@ decode_with_helper() {
"$HELPER_BIN_DIR/heif-decode" --orientation preserve "$1" "$2"
}

check_with_image_hook_helper() {
"$HELPER_BIN_DIR/heif-image-hook-check" "$1"
}

failure_reason_from_log() {
tr '\n' ' ' < "$1" | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//'
}
Expand Down Expand Up @@ -980,6 +1052,7 @@ png_to_rgba() {
# Prints a failure description and returns 1 on mismatch.
compare_png_icc() {
local ref_png="$1" rust_png="$2" prefix="$3"
local ref_label="${4:-validator}" rust_label="${5:-rust}"
local ref_icc="$prefix.ref.icc" rust_icc="$prefix.rust.icc"
local ref_status=0 rust_status=0
"$HELPER_BIN_DIR/heif-png-icc" "$ref_png" "$ref_icc" 2>/dev/null || ref_status=$?
Expand All @@ -997,14 +1070,14 @@ compare_png_icc() {
return 0
fi
if [[ "$rust_status" -eq 3 ]]; then
echo "icc profile missing: validator PNG embeds one, rust PNG has none"
echo "icc profile missing: $ref_label PNG embeds one, $rust_label PNG has none"
return 1
fi
if ! cmp -s "$ref_icc" "$rust_icc"; then
local ref_hash rust_hash
ref_hash="$(shasum -a 256 "$ref_icc" | awk '{print $1}')"
rust_hash="$(shasum -a 256 "$rust_icc" | awk '{print $1}')"
echo "icc profile mismatch ref=$ref_hash rust=$rust_hash"
echo "icc profile mismatch $ref_label=$ref_hash $rust_label=$rust_hash"
return 1
fi
return 0
Expand Down Expand Up @@ -1140,6 +1213,7 @@ EOF

echo "mode=$mode files=${#files[@]}" > "$report_file"
local total=0 skipped=0 passed=0 failed=0
local image_hook_passed=0
local expected_validator_failed=0 expected_validator_rust_decoded=0 expected_validator_rust_errors=0
local expected_rust_failed=0
local comparable_heif=0 comparable_heic=0 comparable_avif=0
Expand Down Expand Up @@ -1274,8 +1348,31 @@ EOF
if cmp -s "$ref_raw" "$rust_raw"; then
local icc_failure
if icc_failure="$(compare_png_icc "$ref_actual" "$rust_actual" "$tmp_dir/$id")"; then
passed=$((passed + 1))
echo "PASS $rel_path" >> "$report_file"
# Deliberately run for EVERY passing file even though it re-decodes
# the file twice more (direct + hook): the image-crate hook is a
# separate decode path (lazy adapter, caller-buffer slices) whose
# parity with the direct decode is a hard correctness requirement,
# so it gets the same per-file coverage as the validator comparison.
# Do not sample or gate this to save CI time.
local hook_log hook_status
hook_log="$tmp_dir/$id.image-hook.check.stderr.log"
if check_with_image_hook_helper "$input_file" >/dev/null 2>"$hook_log"; then
hook_status=0
else
hook_status=$?
fi

if [[ "$hook_status" -ne 0 ]]; then
local hook_reason
hook_reason="$(failure_reason_from_log "$hook_log")"
failed=$((failed + 1))
failures+=("$rel_path :: image hook check failed status=$hook_status: ${hook_reason:-no stderr}")
echo "FAIL $rel_path (image hook check failed status=$hook_status)" >> "$report_file"
else
image_hook_passed=$((image_hook_passed + 1))
passed=$((passed + 1))
echo "PASS $rel_path (direct+image-hook)" >> "$report_file"
fi
else
failed=$((failed + 1))
failures+=("$rel_path :: $icc_failure")
Expand All @@ -1296,7 +1393,7 @@ EOF
done

[[ "$keep_artifacts" -eq 1 ]] || rm -rf "$tmp_dir"
log verify "Summary: total=$total skipped=$skipped expected_validator_fail=$expected_validator_failed expected_validator_rust_decoded=$expected_validator_rust_decoded expected_validator_rust_errors=$expected_validator_rust_errors expected_rust_fail=$expected_rust_failed passed=$passed failed=$failed"
log verify "Summary: total=$total skipped=$skipped expected_validator_fail=$expected_validator_failed expected_validator_rust_decoded=$expected_validator_rust_decoded expected_validator_rust_errors=$expected_validator_rust_errors expected_rust_fail=$expected_rust_failed passed=$passed image_hook_passed=$image_hook_passed failed=$failed"
log verify "Report: $report_file"

if [[ "$failed" -gt 0 ]]; then
Expand Down
Loading
Loading