Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
154 changes: 116 additions & 38 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ homepage = "https://github.com/justin13888/rawshift"
# gamut pin" for the required procedure.
gamut-core = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }
gamut-color = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }
gamut-exif = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }
gamut-metadata = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }
gamut-xmp = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }
gamut-icc = { git = "https://github.com/justin13888/gamut", rev = "dde9f640ab02ec9c3437c3f1181164f6e7c60151" }

# Shared infrastructure dependencies (used across multiple workspace crates).
thiserror = "2.0"
Expand Down
21 changes: 15 additions & 6 deletions crates/rawshift-image/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
rawshift-core = { workspace = true }
gamut-core = { workspace = true }
# gamut-icc backs the sRGB profile construction and validation in
# `metadata/icc.rs`, which compiles unconditionally (the `IccProfile` type is
# part of the public `core` surface), so it is a required dependency.
gamut-icc = { workspace = true }
gamut-exif = { workspace = true, optional = true }
gamut-metadata = { workspace = true, optional = true }
gamut-xmp = { workspace = true, optional = true }
binrw = { version = "0.15", optional = true }
libheif-rs = { version = "2.7", optional = true }
libwebp-sys = { version = "0.14", optional = true }
img-parts = { version = "0.4", optional = true }
jpeg-encoder = { version = "0.7", optional = true }
jxl-oxide = { version = "0.12.5", optional = true }
little_exif = { version = "0.6", optional = true }
rayon = { workspace = true }
image = { version = "0.25", default-features = false, features = ["avif-native"], optional = true }
ravif = { version = "0.13", optional = true }
Expand Down Expand Up @@ -192,12 +198,15 @@ serde = ["dep:serde", "rawshift-core/serde"]
# `zune-core` codec primitives (bit depth, colorspace, decoder/encoder option
# builders). Pulled by every zune-backed decode/encode impl feature.
zune-runtime = ["dep:zune-core"]
# Typed EXIF read/write via `little_exif`. Pulled by format impl features that
# parse embedded EXIF on decode or write it on encode.
exif = ["dep:little_exif"]
# Container segment muxing via `img-parts` (JPEG APP segments, PNG chunks).
# Typed EXIF read/write via the gamut metadata stack (`gamut-exif` for the
# blob model, `gamut-metadata` + `gamut-xmp` for the unified-model bridge).
# Pulled by format impl features that parse embedded EXIF on decode or write it
# on encode.
exif = ["dep:gamut-exif", "dep:gamut-metadata", "dep:gamut-xmp"]
# Container segment muxing via `img-parts` (JPEG APP segments, PNG chunks),
# plus `gamut-xmp` for validating XMP packets before they are embedded.
# Pulled by encode impl features that embed EXIF/ICC/XMP metadata.
container-embed = ["dep:img-parts"]
container-embed = ["dep:img-parts", "dep:gamut-xmp"]
# HEIC/HEIF linking mode. `heic` links the system libheif (resolved by
# pkg-config); `heic-vendored` builds libheif from source via cmake and links
# it statically (needs a C/C++ toolchain + cmake). Enable exactly one.
Expand Down
7 changes: 4 additions & 3 deletions crates/rawshift-image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ crate.
- `tiff-parser` — internal TIFF structure parser plus the public `TiffParser` API.
- `serde` — `Serialize`/`Deserialize` for metadata and option types.
- `zune-runtime` — `zune-core` codec primitives; pulled by zune-backed impls.
- `exif` — typed EXIF read/write (`little_exif`); pulled by impls that touch EXIF.
- `container-embed` — container segment muxing (`img-parts`); pulled by encode
impls that embed EXIF/ICC/XMP.
- `exif` — typed EXIF read/write via the gamut metadata stack (`gamut-exif`,
`gamut-metadata`, `gamut-xmp`); pulled by impls that touch EXIF.
- `container-embed` — container segment muxing (`img-parts`) plus XMP packet
validation (`gamut-xmp`); pulled by encode impls that embed EXIF/ICC/XMP.
- `heic-vendored` — build libheif from source and link it statically, instead
of linking the system libheif (`heic`). Requires a C/C++ toolchain + cmake.
- `jxl-encode-libjxl-vendored` — build libjxl from source via cmake and link
Expand Down
4 changes: 2 additions & 2 deletions crates/rawshift-image/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ pub use rgb_image::RgbImage;

// Re-export IccProfile from the internal metadata module so it remains
// publicly accessible under `core` as before the workspace split.
// The type is replaced by `gamut_icc::IccProfile` in the metadata-stack
// migration (#19), which owns the icc.rs internals wholesale.
// Its internals are built on `gamut_icc` (metadata-stack migration, #19);
// the wrapper type stays so the container-append API keeps its home.
pub use crate::metadata::icc::IccProfile;
4 changes: 1 addition & 3 deletions crates/rawshift-image/src/formats/heic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,14 @@ impl HeicFile {
/// metadata or cannot be parsed. Used by both [`HeicFile::metadata`] and
/// [`read_standard_image_metadata`](crate::formats::read_standard_image_metadata).
pub fn read_heic_metadata(data: &[u8]) -> ImageMetadata {
use little_exif::filetype::FileExtension;

let blobs = match heic::extract_metadata_blobs(data) {
Ok(b) => b,
Err(_) => return ImageMetadata::default(),
};

// Parse the EXIF block (a raw TIFF stream) into typed + generic fields.
let mut md = match blobs.exif {
Some(ref exif) => ExifParser::parse_from_bytes(exif, FileExtension::TIFF),
Some(ref exif) => ExifParser::parse_exif_blob(exif),
None => ImageMetadata::default(),
};

Expand Down
23 changes: 10 additions & 13 deletions crates/rawshift-image/src/formats/standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1639,28 +1639,25 @@ pub fn read_standard_image_metadata(
data: &[u8],
format: StandardFormat,
) -> crate::core::metadata::ImageMetadata {
use crate::metadata::exif::ExifParser;
use little_exif::filetype::FileExtension;
use crate::metadata::exif::{ExifContainer, ExifParser};

// HEIC goes through libheif so that ICC and XMP are extracted alongside EXIF.
#[cfg(feature = "heic-decode")]
if format == StandardFormat::Heic {
return crate::formats::heic::read_heic_metadata(data);
}

let file_type = match format {
StandardFormat::Jpeg => FileExtension::JPEG,
StandardFormat::Tiff => FileExtension::TIFF,
StandardFormat::WebP => FileExtension::WEBP,
StandardFormat::Avif => FileExtension::HEIF,
StandardFormat::Png => FileExtension::PNG {
as_zTXt_chunk: false,
},
// Formats with no EXIF support in little_exif
let container = match format {
StandardFormat::Jpeg => ExifContainer::Jpeg,
StandardFormat::Tiff => ExifContainer::Tiff,
StandardFormat::WebP => ExifContainer::WebP,
StandardFormat::Avif => ExifContainer::Avif,
StandardFormat::Png => ExifContainer::Png,
// Formats without an EXIF extraction path (GIF, SVG, JXL, …)
_ => return crate::core::metadata::ImageMetadata::default(),
};

ExifParser::parse_from_bytes(data, file_type)
ExifParser::parse_from_bytes(data, container)
}

/// Extract EXIF metadata from a standard image without decoding pixel data.
Expand Down Expand Up @@ -2549,7 +2546,7 @@ mod tests {

#[test]
fn read_metadata_invalid_data_returns_default() {
// Garbage data → little_exif returns error → we return default
// Garbage data → no EXIF segment found → we return default
let junk = b"\x00\x01\x02\x03\x04\x05\x06\x07";
let md = read_standard_image_metadata(junk, StandardFormat::Jpeg);
assert!(md.camera.make.is_empty());
Expand Down
Loading