mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
Expose shared raster source and tone inspection
This commit is contained in:
3 files changed
+199
-1
No files matched your search
@@ -83,7 +83,7 @@ pub(crate) fn encode_bmp_indexed(width: u32, height: u32, indices: &[u8], palett
|
||||
}
|
||||
|
||||
/// The `image` decoder for `f` with our limits, its header read and its size checked.
|
||||
fn open<'a>(f: Format, bytes: &'a [u8], limits: &Limits) -> Result<impl ImageDecoder + 'a, CodecError> {
|
||||
pub(crate) fn open<'a>(f: Format, bytes: &'a [u8], limits: &Limits) -> Result<impl ImageDecoder + 'a, CodecError> {
|
||||
let fmt = image_format(f).ok_or_else(|| CodecError::unsupported(f, "not handled by image backend"))?;
|
||||
let mut reader = image::ImageReader::with_format(Cursor::new(bytes), fmt);
|
||||
let mut il = image::Limits::default();
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Bounded source-header and tone inspection for raster documents.
|
||||
|
||||
use crate::error::CodecError;
|
||||
use crate::{ChannelLayout, DecodeOptions, Format, Limits, SampleType, decode_with};
|
||||
|
||||
/// The source pixel model reported by an image container, before decode normalizes it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RasterColorType {
|
||||
Bitmap,
|
||||
Gray,
|
||||
GrayAlpha,
|
||||
Indexed,
|
||||
Rgb,
|
||||
Rgba,
|
||||
Cmyk,
|
||||
CmykAlpha,
|
||||
Lab,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// Useful source facts, including palette entries when an indexed container exposes them.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RasterInspection {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub channels: u8,
|
||||
pub bits_per_sample: u8,
|
||||
pub color_type: RasterColorType,
|
||||
pub palette: Option<Vec<[u8; 3]>>,
|
||||
pub transparent_index: Option<u8>,
|
||||
}
|
||||
|
||||
/// A grayscale source suitable for single-swatch tinting.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ToneClass {
|
||||
Bitmap,
|
||||
Gray,
|
||||
}
|
||||
|
||||
/// Inspect PSD, PNG, JPEG, TIFF, GIF and BMP source headers without expanding pixel data.
|
||||
pub fn inspect_raster(bytes: &[u8]) -> Result<Option<RasterInspection>, CodecError> {
|
||||
if bytes.starts_with(b"8BPS") {
|
||||
if bytes.len() < 26 || !matches!(u16::from_be_bytes([bytes[4], bytes[5]]), 1 | 2) {
|
||||
return Ok(None);
|
||||
}
|
||||
let u16_at = |i| u16::from_be_bytes([bytes[i], bytes[i + 1]]);
|
||||
let height = u32::from_be_bytes(bytes[14..18].try_into().map_err(|_| CodecError::InvalidImage("short PSD header".into()))?);
|
||||
let width = u32::from_be_bytes(bytes[18..22].try_into().map_err(|_| CodecError::InvalidImage("short PSD header".into()))?);
|
||||
let color_type = match u16_at(24) {
|
||||
0 => RasterColorType::Bitmap,
|
||||
1 => RasterColorType::Gray,
|
||||
2 => RasterColorType::Indexed,
|
||||
3 => RasterColorType::Rgb,
|
||||
4 => RasterColorType::Cmyk,
|
||||
9 => RasterColorType::Lab,
|
||||
_ => RasterColorType::Other,
|
||||
};
|
||||
return Ok(Some(RasterInspection {
|
||||
width,
|
||||
height,
|
||||
channels: u16_at(12).min(255) as u8,
|
||||
bits_per_sample: u16_at(22).min(255) as u8,
|
||||
color_type,
|
||||
palette: None,
|
||||
transparent_index: None,
|
||||
}));
|
||||
}
|
||||
let Some(format) = crate::detect(bytes) else { return Ok(None) };
|
||||
let i = match format {
|
||||
Format::Png => {
|
||||
let rows = crate::PngDecodeRows::new(bytes, &Limits::default())?;
|
||||
let h = rows.header();
|
||||
let depth = *bytes.get(24).ok_or_else(|| CodecError::malformed(format, "short PNG IHDR"))?;
|
||||
let ct = *bytes.get(25).ok_or_else(|| CodecError::malformed(format, "short PNG IHDR"))?;
|
||||
let color = match (ct, depth) {
|
||||
(0, 1) => RasterColorType::Bitmap,
|
||||
(0, _) => RasterColorType::Gray,
|
||||
(2, _) => RasterColorType::Rgb,
|
||||
(3, _) => RasterColorType::Indexed,
|
||||
(4, _) => RasterColorType::GrayAlpha,
|
||||
(6, _) => RasterColorType::Rgba,
|
||||
_ => RasterColorType::Other,
|
||||
};
|
||||
let palette = crate::png_palette(bytes)?.map(|p| (p.colors, p.transparent));
|
||||
let (palette, transparent_index) = palette.map_or((None, None), |(c, t)| (Some(c), t));
|
||||
RasterInspection {
|
||||
width: h.width,
|
||||
height: h.height,
|
||||
channels: match ct {
|
||||
0 | 3 => 1,
|
||||
2 => 3,
|
||||
4 => 2,
|
||||
6 => 4,
|
||||
_ => h.layout.channels().min(255) as u8,
|
||||
},
|
||||
bits_per_sample: depth,
|
||||
color_type: color,
|
||||
palette,
|
||||
transparent_index,
|
||||
}
|
||||
}
|
||||
Format::Tiff => {
|
||||
let info = crate::tiff_info(bytes)?;
|
||||
let Some(p) = info.default_page().and_then(|n| info.pages.get(n)) else { return Ok(None) };
|
||||
let color = match (p.photometric, p.bits_per_sample) {
|
||||
(Some(0 | 1), 1) => RasterColorType::Bitmap,
|
||||
(Some(0 | 1), _) => RasterColorType::Gray,
|
||||
(Some(2 | 6), _) => RasterColorType::Rgb,
|
||||
(Some(3), _) => RasterColorType::Indexed,
|
||||
(Some(5), _) => RasterColorType::Cmyk,
|
||||
(Some(8), _) => RasterColorType::Lab,
|
||||
_ => RasterColorType::Other,
|
||||
};
|
||||
RasterInspection {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
channels: p.samples_per_pixel.min(255) as u8,
|
||||
bits_per_sample: p.bits_per_sample.min(255) as u8,
|
||||
color_type: color,
|
||||
palette: None,
|
||||
transparent_index: None,
|
||||
}
|
||||
}
|
||||
Format::Jpeg => {
|
||||
let Some((w, h, channels)) = crate::codecs::jpeg::scan_metadata(bytes).frame else { return Ok(None) };
|
||||
let depth = craft_jpeg::frame(bytes).map_or(8, |f| f.precision);
|
||||
let color = match channels {
|
||||
1 => RasterColorType::Gray,
|
||||
3 => RasterColorType::Rgb,
|
||||
4 => RasterColorType::Cmyk,
|
||||
_ => RasterColorType::Other,
|
||||
};
|
||||
RasterInspection { width: w, height: h, channels, bits_per_sample: depth, color_type: color, palette: None, transparent_index: None }
|
||||
}
|
||||
Format::Bmp | Format::Gif => {
|
||||
use image::ImageDecoder;
|
||||
let d = crate::codecs::via_image::open(format, bytes, &Limits::default())?;
|
||||
let (width, height) = d.dimensions();
|
||||
let (channels, bits, color) = match d.original_color_type() {
|
||||
image::ExtendedColorType::L1 => (1, 1, RasterColorType::Bitmap),
|
||||
image::ExtendedColorType::L2 => (1, 2, RasterColorType::Gray),
|
||||
image::ExtendedColorType::L4 => (1, 4, RasterColorType::Gray),
|
||||
image::ExtendedColorType::L8 => (1, 8, RasterColorType::Gray),
|
||||
image::ExtendedColorType::L16 => (1, 16, RasterColorType::Gray),
|
||||
image::ExtendedColorType::La8 => (2, 8, RasterColorType::GrayAlpha),
|
||||
image::ExtendedColorType::La16 => (2, 16, RasterColorType::GrayAlpha),
|
||||
image::ExtendedColorType::Rgb8 => (3, 8, RasterColorType::Rgb),
|
||||
image::ExtendedColorType::Rgb16 => (3, 16, RasterColorType::Rgb),
|
||||
image::ExtendedColorType::Rgba8 => (4, 8, RasterColorType::Rgba),
|
||||
image::ExtendedColorType::Rgba16 => (4, 16, RasterColorType::Rgba),
|
||||
_ => (4, 8, RasterColorType::Other),
|
||||
};
|
||||
let (palette, transparent_index) = if format == Format::Gif {
|
||||
crate::gif_palette(bytes).map_or((None, None), |(p, t)| (Some(p), t))
|
||||
} else {
|
||||
(crate::bmp_palette(bytes)?, None)
|
||||
};
|
||||
let (channels, color) = if format == Format::Gif { (1, RasterColorType::Indexed) } else { (channels, color) };
|
||||
RasterInspection { width, height, channels, bits_per_sample: bits, color_type: color, palette, transparent_index }
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(i))
|
||||
}
|
||||
|
||||
/// Classify grayscale sources for single-swatch tinting. GIF checks visible decoded pixels and
|
||||
/// exposes its source palette through [`inspect_raster`]; all other formats use source headers.
|
||||
pub fn classify_tone(bytes: &[u8]) -> Option<ToneClass> {
|
||||
let i = inspect_raster(bytes).ok()??;
|
||||
if i.color_type == RasterColorType::Indexed && bytes.starts_with(b"GIF8") {
|
||||
i.palette.as_ref()?;
|
||||
let decoded = decode_with(bytes, &DecodeOptions::default()).ok()?;
|
||||
let rgba = decoded.convert(ChannelLayout::Rgba, SampleType::U8);
|
||||
let mut opaque = true;
|
||||
let mut two_level = true;
|
||||
for p in rgba.data().chunks_exact(4) {
|
||||
match p[3] {
|
||||
0 => opaque = false,
|
||||
255 if p[0] == p[1] && p[1] == p[2] => two_level &= p[0] == 0 || p[0] == 255,
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
return if two_level {
|
||||
Some(ToneClass::Bitmap)
|
||||
} else if opaque {
|
||||
Some(ToneClass::Gray)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
match i.color_type {
|
||||
RasterColorType::Bitmap => Some(ToneClass::Bitmap),
|
||||
RasterColorType::Gray => Some(ToneClass::Gray),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub mod exr_stream;
|
||||
mod fidelity;
|
||||
mod format;
|
||||
mod image;
|
||||
mod inspection;
|
||||
mod lab;
|
||||
mod options;
|
||||
pub mod orientation;
|
||||
@@ -47,6 +48,7 @@ pub use crate::image::{
|
||||
ChannelLayout, CryptomatteBuffer, CryptomatteLayer, DecodeWarning, DeepChannel, DeepImage, ExrChannelInfo, ExrPartInfo, ExtraSample, Image,
|
||||
MAX_EXTRA_SAMPLES, Metadata, SampleType, convert_samples,
|
||||
};
|
||||
pub use crate::inspection::{RasterColorType, RasterInspection, ToneClass, classify_tone, inspect_raster};
|
||||
pub use crate::options::{
|
||||
DdsCompression, DdsOptions, DecodeOptions, DpxBits, EncodeOptions, ExrCompression, JpegFormat, Limits, PngCompression, PngStereo,
|
||||
TIFF_PYRAMID_MIN, TiffCompression,
|
||||
|
||||
Reference in new issue
Block a user