mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-17 04:39:54 +02:00
* TIFF: read and write CIE L*a*b* (CIELab, ICCLab); Lab documents stay Lab Photoshop saves Lab documents as CIELab TIFFs (PhotometricInterpretation 8, a* and b* signed), which PhotoCraft could not open, and PhotoCraft saved Lab documents as sRGB TIFFs. - codecs: a Lab channel layout (Lab, LabA) holding a* and b* as PhotoCraft's Lab documents do ((a + 128) / 255; 16 bits like the PSD importer), with its own Lab <-> sRGB conversion (same formulas as photocraft-color; codecs stays standalone) for formats without Lab, which now warn LabConverted. - TIFF read: CIELab and ICCLab (8 and 16 bits, alpha) are handed to the tiff crate as RGB and labelled Lab; a* and b* are converted from the file's encoding (signed a / 256a, or offset). - TIFF write: Lab and LabA as CIELab, as Photoshop writes it; float Lab is written with 16 bits (DepthReduced). - io: a Lab TIFF opens as a Lab document; a Lab document with one plain layer is written to TIFF in Lab (other formats still use sRGB). Photoshop 2025's Lab TIFFs open with the colours Photoshop shows (its own RGB conversion matches within one level at 8 bits and exactly at 16 bits), and Photoshop reads PhotoCraft's Lab TIFFs back with the same L*a*b* values. Fixes #4936 * Raw: measure the Nikon D90 image area (4288x2848 at (10, 10)) Aligning the decoded D90 sensor plane with the camera's own JPEG puts the image at (10, 10) in the 4352x2868 sensor plane, giving the camera's 4288x2848 frame (10-pixel margins; the right border reads the camera's flat 255 from column 4310). Add the measured ImageArea so a D90 NEF opens on the camera's frame instead of the uncropped plane, and make a measured profile leave a file that carries its own colour calibration (a DNG) untouched. * Codecs: open Cineon (.cin) film frames read-only Cineon is the 10-bit logarithmic film-scan format DPX grew from. Read it from the same Kodak header family: the generic header (big-endian, orientation at 192, channel count at 193, the channel-1 bits/dimensions), one or three channels, three 10-bit samples packed into each 32-bit word on a word boundary. The 10-bit code values open as U16 (round(c x 65535 / 1023)); the file's log black/white points are a display choice and are not applied. A depth other than 10 bits or a channel count other than 1/3 is Unsupported; truncation errors rather than panicking. Read-only (no Cineon writer), so .cin joins the open filters and Cineon read closes part of #1049's G10 gap. * PSD: write tiff blocks without copies, inflate merged ZIP once - ImageSourceData::to_bytes writes the global mask and global blocks straight from the model with 4-byte alignment instead of cloning each block (data included) to set its padding. Same bytes. - The tiff transcoder's nested regions and blocks write into the parent's buffer (taken and handed back, error or not) instead of building their own output and copying it up per nesting level. Bodies that fail or are not transcoded are truncated before being copied as stored or dropped. Same bytes. - PsdFile::from_bytes_and_merged decodes the merged image with the parse when asked: a ZIP stream is inflated once (the decode's stream check is the parse's); the parse-only check runs only when the decode fails, to keep from_bytes' errors. PSD import uses it with merged_needed. - PsdBuilder::composite documents that Gray/CMYK composites stay straight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Content-Aware Fill: measure a thin hole by its thickness, not its bounds (#1366) Removing long, thin objects (coiled guitar strings, wires) left their outline as darker and lighter blotches and copied pieces of other strings into the fill. The pyramid shrank until the hole's bounding box was four patches across: for a string across the image that is a level where the hole has swallowed its surroundings, so the matches found there copy whatever looks like the blur, and the finer levels (refine-only for wide bounds) keep them. A hole at most a quarter as thick as its bounds are long now counts as thin: the pyramid stops while it is still about two patches thick, and that level starts from a membrane across it (over a few pixels, the surroundings themselves) instead of the onion peel, whose exhaustive search is what made a shallow pyramid unaffordable. Compact holes are unchanged bit for bit. Refs #1366 * perf(compose,gpu): effect caches reserve tile addresses instead of pinning layers Effect map cache entries kept a clone of their layer (a deep clone for groups) so the tile addresses the cache key hashes couldn't be reused. That kept an edited or deleted layer's pixels alive, uncounted, until the entry was evicted. Keep a Weak to each hashed tile instead: it reserves the address without keeping the pixels. The GPU compositor's styled-group pins and shape-source tile snapshots do the same. Part of #4530. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(history): count snapshot-only tiles against the history budget History panel snapshots outlived every budget: a Merged Layers snapshot is a full flattened copy that neither the history budget, the GPU budget's document bytes nor Purge accounted for. Trim now counts the tiles only snapshots hold as fixed cost, the GPU budget includes them, and Purge > Histories no longer reports tiles a kept snapshot still holds as freed. Part of #4530. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Content-Aware Fill: every part of a thin selection keeps its surroundings (#1366) Two more causes of ghosts when removing strings and the small knobs selected with them: * Auto sampling summarises the selection's surroundings by a few clusters. Around a long selection those come from the strings' surroundings, so a small part elsewhere (a tuning knob in a differently coloured corner) lost its own surroundings and was filled from far away in another colour. Around a thin selection the Auto area now always reaches as far as the selection is thick. * The membrane that starts a thin hole now starts a patch radius out, past the anti-aliased fringe or halo that a selection drawn close to the object leaves outside it. Refs #1366 * Content-Aware Fill: peel a thin hole where that stays affordable (#1366) Starting every thin hole from a membrane smoothed Spot Healing on film grain (spot_healing_keeps_film_grain_through_overlapping_strokes: grain 9.0 against 12.1 around). The onion peel copies the grain; it was only given up because its exhaustive search is too slow on the big level a long thin hole stops at. It now starts the thin hole whenever hole cells times sources stay within 2^28 comparisons, and the membrane only beyond. Refs #1366 * Select Subject: keep subject parts coloured like the backdrop, leave enclosed gaps open Users on #2788 reported Select Subject dropping clothes that are close in colour to a plain wall and bridging the gap between an arm and the torso. Both come from the same place: the saliency pass labels low-contrast parts of the subject as background, the background colour model then learns them and explains them away, and the final blind hole fill closes every enclosed gap up to 5 % of the mask. Two changes in crates/algo: - A second GrabCut pass whose background model only knows the backdrop. A colour model fitted to the image frame (minus a margin around the salient blob, in case the subject runs out of the picture) marks every pixel it explains as probable background; the blob and whatever the backdrop cannot account for start as probable foreground. Only components that touch the first pass's result survive. - Hole filling that consults the colour models: specks (<= 0.2 % of the mask) always fill, larger holes up to 5 % fill only when their pixels fit the foreground model better than the background's. Tests: two regressions (a cream-shouldered figure on a near-white wall keeps its shoulders; a ring keeps its backdrop-coloured hole open), plus ignored timing and picture-dump helpers. The 6 MP first run goes from about 100 ms to about 200 ms on one core; the P29 repeat path is untouched. * Keyboard shortcut sets: Set menu, New Set, Save, Delete, .kys Export and Summarize (K2) Edit › Keyboard Shortcuts gets Photoshop's set management. A set is a saved copy of the bindings (Preferences.shortcutSets, the one in use in shortcutSet; "" is PhotoCraft Defaults), and the Set menu shows "(modified)" when the bindings differ from it. New Set…, Save and Delete wait for OK like the rest of the dialog. Export… writes the dialog's bindings as a .kys file (commands then tools, Photoshop's macOS key spelling), each row carrying our id as pc-id so PhotoCraft imports its own sets back exactly, whatever the label; Summarize… writes Photoshop's HTML list of every bound command by menu. edit.keyboardShortcuts takes useSet (before the call's edits), saveSet, deleteSet, sets and activeSet (after them), exportKys and summarize; every reply reports the sets, the set in use and whether it is modified. Bad names, keys or counts are errors that change nothing. * .kys import: tool keys and sets saved on Windows (K3) Photoshop's <tool> rows now import: each matches our tool by name (our tool names are Photoshop's) or by pc-id in sets PhotoCraft exported, and a letter other than the tool's own becomes its override, while its own letter keeps the tool cycling with the others (#5036). A set saved on Windows spells the primary modifier Ctrl where a Mac set writes Cmd and keeps Control for the Control key. A set with no Cmd or Opt anywhere but some Ctrl is read as a Windows set, its Ctrl as our Cmd: Ctrl+L is the Cmd+L default, not a Control-key override that would also mean Control on a Mac. The import reports `windows`. * cargo fmt the extension lists Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Vikram Bhaskaran <vikram.bhaskaran@outlook.com> Co-authored-by: vangel76 <vangel1976@googlemail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Finn Newick <finnnk@gmail.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Diego Chagas <drochachagas@gmail.com> Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
264 lines
14 KiB
Rust
264 lines
14 KiB
Rust
//! `photocraft-codecs`: symmetric, depth- and metadata-preserving codecs for
|
||
//! flat raster image formats.
|
||
//!
|
||
//! * Every format we **write** we can also **read** (see
|
||
//! [`ASYMMETRIC_EXCEPTIONS`] for the documented, feature-gated exception).
|
||
//! * Bit depth (8/16-bit integer, 16/32-bit float), ICC profiles and basic
|
||
//! metadata (EXIF, XMP, DPI, text) are preserved where the format allows.
|
||
//! * [`caps`] declares per-format capabilities and [`fidelity_warnings`]
|
||
//! tells the UI exactly what an export will lose.
|
||
//! * The API is I/O free (`&[u8]` in, `Vec<u8>` out) and builds for
|
||
//! `wasm32-unknown-unknown`. Decoders enforce [`Limits`] before allocating.
|
||
|
||
#![forbid(unsafe_code)]
|
||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||
|
||
mod codecs;
|
||
mod error;
|
||
pub mod exif_color;
|
||
mod fidelity;
|
||
mod format;
|
||
mod image;
|
||
mod lab;
|
||
mod options;
|
||
pub mod orientation;
|
||
pub mod resolution;
|
||
pub mod source_settings;
|
||
pub mod web;
|
||
|
||
pub use crate::codecs::exr_cryptomatte::{cryptomatte_id, cryptomatte_key, cryptomatte_preview_color};
|
||
pub use crate::codecs::gif_frames::{AnimationFrame, MAX_GIF_FRAMES};
|
||
pub use crate::codecs::png::{PngPalette, encode_indexed as encode_png_indexed, palette as png_palette};
|
||
pub use crate::codecs::tiff::{PhotoshopTags as TiffPhotoshopTags, photoshop_tags as tiff_photoshop_tags, writes_little_endian as tiff_writes_little_endian};
|
||
pub use crate::codecs::tiff_ifd::{TiffInfo, TiffPage, TiffPageKind};
|
||
pub use crate::error::CodecError;
|
||
pub use crate::exif_color::{EXIF_COLOR_SPACE_SRGB, exif_color_space};
|
||
pub use crate::fidelity::{FidelityWarning, fidelity_warnings, fidelity_warnings_with};
|
||
pub use crate::format::{ASYMMETRIC_EXCEPTIONS, Format, FormatCaps, caps, detect, from_extension};
|
||
pub use crate::image::{
|
||
ChannelLayout, CryptomatteBuffer, CryptomatteLayer, DecodeWarning, DeepChannel, DeepImage, ExrChannelInfo, ExrPartInfo, Image, Metadata, SampleType,
|
||
};
|
||
pub use crate::options::{DecodeOptions, DpxBits, EncodeOptions, ExrCompression, Limits, PngCompression, TiffCompression};
|
||
pub use crate::orientation::{exif_orientation, upright_exif, upright_xmp};
|
||
pub use crate::resolution::{exif_resolution, export_exif, export_xmp, photoshop_resolution, xmp_resolution};
|
||
pub use crate::source_settings::{WebpEncoding, jpeg_quality, webp_encoding};
|
||
pub use half::f16;
|
||
|
||
use crate::codecs::{dds, dpx, exr, fits, heif, jpeg, pcx, png, pnm, tiff, via_image, webp, xpm};
|
||
|
||
/// Detect the format and decode with default [`Limits`].
|
||
pub fn decode(bytes: &[u8]) -> Result<Image, CodecError> {
|
||
decode_with(bytes, &DecodeOptions::default())
|
||
}
|
||
|
||
/// Detect the format and decode.
|
||
pub fn decode_with(bytes: &[u8], opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
let format = detect(bytes).ok_or(CodecError::UnknownFormat)?;
|
||
decode_as_with(format, bytes, opts)
|
||
}
|
||
|
||
/// Decode as a specific format with default [`Limits`].
|
||
pub fn decode_as(format: Format, bytes: &[u8]) -> Result<Image, CodecError> {
|
||
decode_as_with(format, bytes, &DecodeOptions::default())
|
||
}
|
||
|
||
/// Decode as a specific format.
|
||
pub fn decode_as_with(format: Format, bytes: &[u8], opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
// HEIF without the `heif` feature: its decoder stub says what is missing from this build.
|
||
if !caps(format).read && format != Format::Heif {
|
||
return Err(CodecError::unsupported(format, "decoding is not available for this format"));
|
||
}
|
||
let l = &opts.limits;
|
||
let img = match format {
|
||
Format::Png => png::decode(bytes, l),
|
||
Format::Jpeg => jpeg::decode(bytes, l),
|
||
Format::Tiff => tiff::decode(bytes, l),
|
||
Format::WebP => webp::decode(bytes, l),
|
||
Format::Pnm => pnm::decode(bytes, l),
|
||
Format::Pcx => pcx::decode(bytes, l),
|
||
Format::Xpm => xpm::decode(bytes, l),
|
||
Format::Dds => dds::decode(bytes, l),
|
||
Format::Fits => fits::decode(bytes, l),
|
||
Format::Dpx => dpx::decode(bytes, l),
|
||
Format::Cineon => dpx::decode_cineon(bytes, l),
|
||
Format::OpenExr => exr::decode(bytes, l),
|
||
Format::Heif => heif::decode(bytes, l, opts.keep_orientation),
|
||
Format::Gif | Format::Bmp | Format::Tga | Format::Ico | Format::Qoi | Format::Hdr | Format::Avif => via_image::decode(format, bytes, l),
|
||
}?;
|
||
// Without a resolution of the format's own (PNG pHYs, TIFF tags, JPEG's metadata), the
|
||
// XMP or EXIF one counts, like Photoshop: a WebP, HEIF or PNG from a camera or phone
|
||
// opens at its recorded resolution, not at the 72 ppi default.
|
||
let mut img = img;
|
||
if img.meta.dpi.is_none() && format != Format::Tiff {
|
||
img.meta.dpi = resolution::from_metadata(img.meta.exif.as_deref(), img.meta.xmp.as_deref());
|
||
}
|
||
// Turn the pixels upright, like Photoshop: a TIFF records it in the decoded page's own
|
||
// directory, the others in their EXIF block. The metadata is rewritten to Orientation = 1
|
||
// on the way. HEIF keeps it in its container, and its decoder has already applied it.
|
||
let o = match format {
|
||
Format::Tiff => tiff::orientation(bytes, None),
|
||
Format::Heif => 1,
|
||
Format::Dpx => dpx::orientation(bytes),
|
||
Format::Cineon => dpx::cineon_orientation(bytes),
|
||
_ => img.meta.exif.as_deref().map_or(1, exif_orientation),
|
||
};
|
||
finish_decode(img, o, opts)
|
||
}
|
||
|
||
/// The upright pixel size [`decode`] would return, read from the file's header without decoding
|
||
/// pixels, for WebP, OpenEXR (flat) and the GIF/BMP/TGA/ICO/QOI/HDR family. It runs the same
|
||
/// header reads and size checks as the decoder (default [`Limits`]) and swaps the size for an
|
||
/// EXIF orientation of 5–8, so wherever [`decode`] succeeds the two agree. Other formats (and
|
||
/// deep EXR or AVIF) return [`CodecError::Unsupported`]: decode those to learn their size.
|
||
pub fn probe_size(bytes: &[u8]) -> Result<(u32, u32), CodecError> {
|
||
let format = detect(bytes).ok_or(CodecError::UnknownFormat)?;
|
||
if !caps(format).read {
|
||
return Err(CodecError::unsupported(format, "decoding is not available for this format"));
|
||
}
|
||
let l = &Limits::default();
|
||
let (w, h, exif) = match format {
|
||
Format::WebP => webp::probe(bytes, l)?,
|
||
Format::OpenExr => exr::probe(bytes, l).map(|(w, h)| (w, h, None))?,
|
||
Format::Gif | Format::Bmp | Format::Tga | Format::Ico | Format::Qoi | Format::Hdr | Format::Avif => via_image::probe(format, bytes, l)?,
|
||
_ => return Err(CodecError::unsupported(format, "no header-only size probe")),
|
||
};
|
||
let o = exif.as_deref().map_or(1, exif_orientation);
|
||
Ok(if (5..=8).contains(&o) { (h, w) } else { (w, h) })
|
||
}
|
||
|
||
/// Returns the embedded colour table for an indexed BMP, if it has one.
|
||
pub fn bmp_palette(bytes: &[u8]) -> Result<Option<Vec<[u8; 3]>>, CodecError> {
|
||
via_image::bmp_palette(bytes)
|
||
}
|
||
|
||
/// Lists the pages (image directories) of a TIFF or BigTIFF file without decoding pixels: the
|
||
/// main IFD chain and its SubIFDs, each with its size, kind (page, reduced-resolution copy or
|
||
/// mask) and storage. [`TiffInfo::default_page`] is the page [`decode`] opens.
|
||
pub fn tiff_info(bytes: &[u8]) -> Result<TiffInfo, CodecError> {
|
||
crate::codecs::tiff_ifd::tiff_info(bytes)
|
||
}
|
||
|
||
/// Decodes page `page` (an index into [`tiff_info`]'s `pages`) of a TIFF or BigTIFF file,
|
||
/// applying that page's orientation unless `opts.keep_orientation`.
|
||
pub fn decode_tiff_page(bytes: &[u8], page: usize, opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
let img = tiff::decode_page(bytes, Some(page), &opts.limits)?;
|
||
finish_decode(img, tiff::orientation(bytes, Some(page)), opts)
|
||
}
|
||
|
||
/// Decodes every frame of a GIF (a still GIF yields one), each a full RGBA8 picture of the
|
||
/// logical screen with its delay. [`decode`] keeps the first frame only. See
|
||
/// [`MAX_GIF_FRAMES`] and `opts.limits` for what is refused.
|
||
pub fn decode_gif_frames(bytes: &[u8], opts: &DecodeOptions) -> Result<Vec<AnimationFrame>, CodecError> {
|
||
crate::codecs::gif_frames::decode(bytes, opts)
|
||
}
|
||
|
||
/// The Orientation (1–8) of the page [`decode`] opens from a TIFF or BigTIFF file; 1 when it is
|
||
/// absent, malformed or out of range.
|
||
pub fn tiff_orientation(bytes: &[u8]) -> u16 {
|
||
tiff::orientation(bytes, None)
|
||
}
|
||
|
||
/// The Orientation (1–8) of page `page` (an index into [`tiff_info`]'s `pages`); 1 when it is
|
||
/// absent, malformed or out of range, or the page does not exist.
|
||
pub fn tiff_page_orientation(bytes: &[u8], page: usize) -> u16 {
|
||
tiff::orientation(bytes, Some(page))
|
||
}
|
||
|
||
/// The limits check and the orientation, shared by every decoder.
|
||
fn finish_decode(img: Image, o: u16, opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
let l = &opts.limits;
|
||
// Final guard for decoders whose header we could not pre-inspect.
|
||
l.check(img.width(), img.height(), img.layout(), img.sample_type())?;
|
||
if opts.keep_orientation {
|
||
return Ok(img);
|
||
}
|
||
// Orientations 5–8 swap width and height: with asymmetric limits a stored landscape
|
||
// that fit can turn into a portrait that does not, so check the upright size too.
|
||
if (5..=8).contains(&o) {
|
||
l.check(img.height(), img.width(), img.layout(), img.sample_type())?;
|
||
}
|
||
img.oriented(o)
|
||
}
|
||
|
||
/// Lists the parts of an OpenEXR file without decoding pixels: header facts per part
|
||
/// (name, view, size, channels, deep/tiled), what a multi-part chooser — a Maya/Arnold
|
||
/// render writes one part per AOV — offers. The indices are what [`decode_exr_part`]
|
||
/// takes; [`decode_as_with`] opens the part with the highest [`ExrPartInfo::color_rank`].
|
||
pub fn exr_info(bytes: &[u8], limits: &Limits) -> Result<Vec<ExrPartInfo>, CodecError> {
|
||
exr::info(bytes, limits)
|
||
}
|
||
|
||
/// Decodes part `part` (an index into [`exr_info`]'s list) of an OpenEXR file.
|
||
/// Deep parts stay with [`decode_deep_exr`]; a file that has any is refused here.
|
||
pub fn decode_exr_part(bytes: &[u8], part: usize, opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
let img = exr::decode_part(bytes, part, &opts.limits)?;
|
||
finish_decode(img, 1, opts)
|
||
}
|
||
|
||
/// Lists every Cryptomatte layer of an EXR file (specification 1.2: object/material IDs
|
||
/// with coverage, as written by Arnold, V-Ray, Redshift, Mantra/Karma and Cycles) from the
|
||
/// headers alone: name, metadata key, channels and the parsed embedded manifest. The
|
||
/// samples come from [`decode_cryptomatte`]; names hash to IDs with [`cryptomatte_id`].
|
||
pub fn cryptomatte_layers(bytes: &[u8], limits: &Limits) -> Result<Vec<CryptomatteLayer>, CodecError> {
|
||
codecs::exr_cryptomatte::layers(bytes, limits)
|
||
}
|
||
|
||
/// Decodes the samples of the Cryptomatte layer called `layer_name` (a
|
||
/// [`CryptomatteLayer::name`]): per pixel its (ID, coverage) pairs, coverage above zero,
|
||
/// sorted by descending coverage.
|
||
pub fn decode_cryptomatte(bytes: &[u8], layer_name: &str, limits: &Limits) -> Result<CryptomatteBuffer, CodecError> {
|
||
codecs::exr_cryptomatte::decode(bytes, layer_name, limits)
|
||
}
|
||
|
||
/// Decodes the named channels of one part (exact names as the file stores them, e.g.
|
||
/// `diffuse.R`) into an image: R/G/B (plus A when named) give Rgba, a lone Y gives Gray.
|
||
/// This renders a channel group without the part's other channels in the way.
|
||
pub fn decode_exr_channels(bytes: &[u8], part: usize, names: &[&str], opts: &DecodeOptions) -> Result<Image, CodecError> {
|
||
let img = exr::decode_channels(bytes, part, names, &opts.limits)?;
|
||
finish_decode(img, 1, opts)
|
||
}
|
||
|
||
/// Deep OpenEXR samples (`deepscanline`/`deeptile`): the structured per-pixel sample lists
|
||
/// (colour, alpha and Z per sample), for callers that composite deep data themselves —
|
||
/// [`decode_as_with`] instead flattens a deep file into a normal [`Image`] with a
|
||
/// [`DecodeWarning::DeepFlattened`]. `Err(Unsupported)` when the file has no deep part.
|
||
pub fn decode_deep_exr(bytes: &[u8], limits: &Limits) -> Result<DeepImage, CodecError> {
|
||
let mut cur = std::io::Cursor::new(bytes);
|
||
let meta = ::exr::meta::MetaData::read_from_buffered(&mut cur, false).map_err(|e| CodecError::malformed(Format::OpenExr, e))?;
|
||
codecs::deep_exr::decode_deep(&meta, bytes, limits)
|
||
}
|
||
|
||
/// Encode `image` as `format`. Conversions follow the same plan that
|
||
/// [`fidelity_warnings_with`] reports; fatal conditions (unsupported
|
||
/// format, oversize image) return an error.
|
||
pub fn encode(image: &Image, format: Format, opts: &EncodeOptions) -> Result<Vec<u8>, CodecError> {
|
||
if !caps(format).write {
|
||
return Err(CodecError::unsupported(format, "encoding is not available in this build"));
|
||
}
|
||
if image.width() == 0 || image.height() == 0 {
|
||
return Err(CodecError::InvalidImage("cannot encode an empty image".into()));
|
||
}
|
||
if let Some((mw, mh)) = format.max_dimensions()
|
||
&& (image.width() > mw || image.height() > mh)
|
||
{
|
||
return Err(CodecError::encode(format, format!("dimensions exceed {mw}x{mh}")));
|
||
}
|
||
let plan = fidelity::plan(image, format, opts);
|
||
match format {
|
||
Format::Png => png::encode(image, plan, opts),
|
||
Format::Jpeg => jpeg::encode(image, plan, opts),
|
||
Format::Tiff => tiff::encode(image, plan, opts),
|
||
Format::WebP => webp::encode(image, plan, opts),
|
||
Format::Pnm => pnm::encode(image, plan, opts),
|
||
Format::Pcx => Err(CodecError::unsupported(format, "PCX is import-only; save as PNG instead")),
|
||
Format::Xpm => Err(CodecError::unsupported(format, "XPM is import-only; save as PNG instead")),
|
||
Format::Dds => Err(CodecError::unsupported(format, "DDS is import-only; save as PNG instead")),
|
||
Format::Fits => fits::encode(image, plan, opts),
|
||
Format::Dpx => dpx::encode(image, plan, opts),
|
||
Format::Cineon => Err(CodecError::unsupported(format, "Cineon is import-only; save as DPX, TIFF or EXR instead")),
|
||
Format::OpenExr => exr::encode(image, plan, opts),
|
||
Format::Heif => Err(CodecError::unsupported(format, "encoding is not available in this build")),
|
||
Format::Gif | Format::Bmp | Format::Tga | Format::Ico | Format::Qoi | Format::Hdr | Format::Avif => via_image::encode(format, image, plan, opts),
|
||
}
|
||
}
|