Download engine/crates/vision/src/decode.rs from respanai/span-02: direct link, hf CLI and curl.
- Browser
- Download file 11.4 kB
-
https://huggingface.co/respanai/span-02/resolve/main/engine/crates/vision/src/decode.rs
- Command line
-
hf download hf://respanai/span-02/engine/crates/vision/src/decode.rs
-
curl -L -o decode.rs https://huggingface.co/respanai/span-02/resolve/main/engine/crates/vision/src/decode.rs
11.4 kB
| //! Image decoding to 8-bit RGB with exactly PIL's `Image.open(...).convert('RGB')` semantics. | |
| //! | |
| //! * PNG: `png` crate (fdeflate). Lossless, bit exact. Alpha is dropped (not composited), | |
| //! palette/tRNS ignored for RGB conversion, 16-bit RGB(A)/LA keep the high byte, 16-bit | |
| //! gray ("I;16" in PIL) is clipped to 255 like PIL's `I16L_L`. | |
| //! * JPEG: libjpeg-turbo 3.x (vendored via `turbojpeg-sys`), the same library PIL uses: ISLOW IDCT, | |
| //! fancy upsampling, default libjpeg YCbCr->RGB. CMYK/YCCK: PIL's inverted-Adobe convention | |
| //! and `cmyk2rgb`. | |
| //! * GIF/BMP/TIFF/WebP: `image` crate (best effort; lossy WebP is not guaranteed bit-exact | |
| //! with libwebp). | |
| use crate::resize::{Rgb, PAD}; | |
| use crate::{Result, VisionError}; | |
| use std::io::Cursor; | |
| pub enum Format { | |
| Png, | |
| Jpeg, | |
| Gif, | |
| Bmp, | |
| WebP, | |
| Tiff, | |
| Unknown, | |
| } | |
| pub fn sniff(b: &[u8]) -> Format { | |
| if b.len() >= 8 && b[..8] == [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a] { | |
| Format::Png | |
| } else if b.len() >= 3 && b[0] == 0xff && b[1] == 0xd8 && b[2] == 0xff { | |
| Format::Jpeg | |
| } else if b.len() >= 6 && (&b[..6] == b"GIF87a" || &b[..6] == b"GIF89a") { | |
| Format::Gif | |
| } else if b.len() >= 2 && &b[..2] == b"BM" { | |
| Format::Bmp | |
| } else if b.len() >= 12 && &b[..4] == b"RIFF" && &b[8..12] == b"WEBP" { | |
| Format::WebP | |
| } else if b.len() >= 4 && (b[..4] == [b'I', b'I', 42, 0] || b[..4] == [b'M', b'M', 0, 42]) { | |
| Format::Tiff | |
| } else { | |
| Format::Unknown | |
| } | |
| } | |
| fn err<T: std::fmt::Display>(e: T) -> VisionError { | |
| VisionError::Decode(e.to_string()) | |
| } | |
| // ----------------------------------------------------------------------------- probe | |
| /// Header-only dimension probe (width, height). | |
| pub fn probe(bytes: &[u8]) -> Result<(usize, usize)> { | |
| match sniff(bytes) { | |
| Format::Png => { | |
| if bytes.len() < 24 || &bytes[12..16] != b"IHDR" { | |
| return Err(err("bad PNG header")); | |
| } | |
| let w = u32::from_be_bytes(bytes[16..20].try_into().unwrap()) as usize; | |
| let h = u32::from_be_bytes(bytes[20..24].try_into().unwrap()) as usize; | |
| Ok((w, h)) | |
| } | |
| Format::Jpeg => probe_jpeg(bytes), | |
| Format::Gif => { | |
| if bytes.len() < 10 { | |
| return Err(err("bad GIF header")); | |
| } | |
| Ok(( | |
| u16::from_le_bytes([bytes[6], bytes[7]]) as usize, | |
| u16::from_le_bytes([bytes[8], bytes[9]]) as usize, | |
| )) | |
| } | |
| Format::Unknown => Err(VisionError::Unsupported), | |
| _ => { | |
| let r = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().map_err(err)?; | |
| let (w, h) = r.into_dimensions().map_err(err)?; | |
| Ok((w as usize, h as usize)) | |
| } | |
| } | |
| } | |
| fn probe_jpeg(b: &[u8]) -> Result<(usize, usize)> { | |
| let mut i = 2; | |
| while i + 4 <= b.len() { | |
| if b[i] != 0xff { | |
| i += 1; | |
| continue; | |
| } | |
| let m = b[i + 1]; | |
| if m == 0xff { | |
| i += 1; | |
| continue; | |
| } | |
| if m == 0xd8 || (0xd0..=0xd7).contains(&m) || m == 0x01 || m == 0x00 { | |
| i += 2; | |
| continue; | |
| } | |
| let len = u16::from_be_bytes([b[i + 2], b[i + 3]]) as usize; | |
| // SOF0..SOF15 except DHT(c4), JPG(c8), DAC(cc) | |
| if (0xc0..=0xcf).contains(&m) && m != 0xc4 && m != 0xc8 && m != 0xcc { | |
| if i + 9 > b.len() { | |
| break; | |
| } | |
| let h = u16::from_be_bytes([b[i + 5], b[i + 6]]) as usize; | |
| let w = u16::from_be_bytes([b[i + 7], b[i + 8]]) as usize; | |
| return Ok((w, h)); | |
| } | |
| if m == 0xda { | |
| break; | |
| } | |
| i += 2 + len; | |
| } | |
| Err(err("JPEG: no SOF marker")) | |
| } | |
| // ----------------------------------------------------------------------------- entry | |
| /// Decode to packed RGB8 (PIL `convert('RGB')` semantics). `max_pixels` guards against | |
| /// decompression bombs (checked from the header before any large allocation). | |
| pub fn decode_rgb(bytes: &[u8], max_pixels: usize) -> Result<Rgb> { | |
| let fmt = sniff(bytes); | |
| if fmt == Format::Unknown { | |
| return Err(VisionError::Unsupported); | |
| } | |
| let (w, h) = probe(bytes)?; | |
| if w == 0 || h == 0 { | |
| return Err(err("zero-sized image")); | |
| } | |
| if w.checked_mul(h).map_or(true, |p| p > max_pixels) { | |
| return Err(VisionError::TooLarge { w, h, max_pixels }); | |
| } | |
| match fmt { | |
| Format::Png => decode_png(bytes), | |
| Format::Jpeg => decode_jpeg(bytes), | |
| _ => decode_other(bytes), | |
| } | |
| } | |
| /// `(w, h, rgb)` with `rgb.len() == w*h*3`. | |
| pub fn decode_to_rgb(bytes: &[u8]) -> Result<(usize, usize, Vec<u8>)> { | |
| let mut r = decode_rgb(bytes, crate::DEFAULT_MAX_PIXELS)?; | |
| let mut v = std::mem::take(&mut r.data); | |
| v.truncate(r.w * r.h * 3); | |
| Ok((r.w, r.h, v)) | |
| } | |
| // ----------------------------------------------------------------------------- PNG | |
| fn decode_png(bytes: &[u8]) -> Result<Rgb> { | |
| if let Some(p) = crate::png_fast::parse(bytes).filter(crate::png_fast::worthwhile) { | |
| let mut out = Rgb::scratch(p.w, p.h); | |
| let prog = std::sync::atomic::AtomicUsize::new(0); | |
| crate::png_fast::decode_into(&p, out.data.as_mut_ptr(), out.data.len(), &prog)?; | |
| return Ok(out); | |
| } | |
| decode_png_slow(bytes) | |
| } | |
| fn decode_png_slow(bytes: &[u8]) -> Result<Rgb> { | |
| use png::{BitDepth, ColorType, Transformations}; | |
| let limits = png::Limits { bytes: 1usize << 40 }; | |
| // first look at the header to special-case 16-bit gray (PIL "I;16") | |
| let probe_dec = png::Decoder::new_with_limits(Cursor::new(bytes), limits.clone()); | |
| let probe_reader = probe_dec.read_info().map_err(err)?; | |
| let (w, h) = { | |
| let i = probe_reader.info(); | |
| (i.width as usize, i.height as usize) | |
| }; | |
| let gray16 = probe_reader.info().color_type == ColorType::Grayscale | |
| && probe_reader.info().bit_depth == BitDepth::Sixteen; | |
| drop(probe_reader); | |
| let mut dec = png::Decoder::new_with_limits(Cursor::new(bytes), limits); | |
| dec.set_transformations(if gray16 { Transformations::IDENTITY } else { Transformations::EXPAND | Transformations::STRIP_16 }); | |
| let mut reader = dec.read_info().map_err(err)?; | |
| let (ct, bd) = reader.output_color_type(); | |
| let size = reader.output_buffer_size().ok_or_else(|| err("PNG buffer size overflow"))?; | |
| // RGB output decodes straight into the final (padded) buffer | |
| let direct = ct == ColorType::Rgb && bd == BitDepth::Eight; | |
| let mut buf = vec![0u8; if direct { size + PAD } else { size }]; | |
| reader.next_frame(&mut buf[..size]).map_err(err)?; | |
| if direct { | |
| return Ok(Rgb { w, h, data: buf }); | |
| } | |
| let mut out = vec![0u8; w * h * 3 + PAD]; | |
| let n = w * h; | |
| match (ct, bd) { | |
| (ColorType::Rgba, BitDepth::Eight) => rgba_to_rgb(&buf, &mut out[..n * 3]), | |
| (ColorType::Grayscale, BitDepth::Eight) => { | |
| for (i, &g) in buf[..n].iter().enumerate() { | |
| out[i * 3] = g; | |
| out[i * 3 + 1] = g; | |
| out[i * 3 + 2] = g; | |
| } | |
| } | |
| (ColorType::GrayscaleAlpha, BitDepth::Eight) => { | |
| for i in 0..n { | |
| let g = buf[i * 2]; | |
| out[i * 3] = g; | |
| out[i * 3 + 1] = g; | |
| out[i * 3 + 2] = g; | |
| } | |
| } | |
| (ColorType::Grayscale, BitDepth::Sixteen) => { | |
| for i in 0..n { | |
| let (hi, lo) = (buf[i * 2], buf[i * 2 + 1]); | |
| let g = if hi != 0 { 255 } else { lo }; | |
| out[i * 3] = g; | |
| out[i * 3 + 1] = g; | |
| out[i * 3 + 2] = g; | |
| } | |
| } | |
| _ => return Err(err(format!("unsupported PNG output {:?}/{:?}", ct, bd))), | |
| } | |
| Ok(Rgb { w, h, data: out }) | |
| } | |
| pub fn rgba_to_rgb(src: &[u8], dst: &mut [u8]) { | |
| use rayon::prelude::*; | |
| dst.par_chunks_mut(3 * 4096).zip(src.par_chunks(4 * 4096)).for_each(|(d, s)| { | |
| for (dp, sp) in d.chunks_exact_mut(3).zip(s.chunks_exact(4)) { | |
| dp.copy_from_slice(&sp[..3]); | |
| } | |
| }); | |
| } | |
| // ----------------------------------------------------------------------------- JPEG | |
| fn decode_jpeg(bytes: &[u8]) -> Result<Rgb> { | |
| use turbojpeg_sys as tj; | |
| unsafe { | |
| let h = tj::tj3Init(tj::TJINIT_TJINIT_DECOMPRESS as i32); | |
| if h.is_null() { | |
| return Err(err("tj3Init failed")); | |
| } | |
| struct Guard(tj::tjhandle); | |
| impl Drop for Guard { | |
| fn drop(&mut self) { | |
| unsafe { tj::tj3Destroy(self.0) } | |
| } | |
| } | |
| let _g = Guard(h); | |
| let msg = |h: tj::tjhandle| -> VisionError { | |
| let p = tj::tj3GetErrorStr(h); | |
| let s = if p.is_null() { "jpeg error".to_string() } else { std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned() }; | |
| VisionError::Decode(s) | |
| }; | |
| // defaults == PIL: accurate DCT (ISLOW), fancy upsampling; keep going on warnings | |
| tj::tj3Set(h, tj::TJPARAM_TJPARAM_FASTDCT as i32, 0); | |
| tj::tj3Set(h, tj::TJPARAM_TJPARAM_FASTUPSAMPLE as i32, 0); | |
| if tj::tj3DecompressHeader(h, bytes.as_ptr(), bytes.len() as _) != 0 { | |
| return Err(msg(h)); | |
| } | |
| let w = tj::tj3Get(h, tj::TJPARAM_TJPARAM_JPEGWIDTH as i32) as usize; | |
| let ht = tj::tj3Get(h, tj::TJPARAM_TJPARAM_JPEGHEIGHT as i32) as usize; | |
| let cs = tj::tj3Get(h, tj::TJPARAM_TJPARAM_COLORSPACE as i32); | |
| let prec = tj::tj3Get(h, tj::TJPARAM_TJPARAM_PRECISION as i32); | |
| if prec != 8 { | |
| return Err(err(format!("unsupported JPEG precision {prec}"))); | |
| } | |
| if cs == tj::TJCS_TJCS_CMYK as i32 || cs == tj::TJCS_TJCS_YCCK as i32 { | |
| let mut cmyk = vec![0u8; w * ht * 4]; | |
| if tj::tj3Decompress8(h, bytes.as_ptr(), bytes.len() as _, cmyk.as_mut_ptr(), (w * 4) as i32, tj::TJPF_TJPF_CMYK as i32) != 0 { | |
| return Err(msg(h)); | |
| } | |
| let mut out = vec![0u8; w * ht * 3 + PAD]; | |
| // PIL: rawmode "CMYK;I" (inverted, Adobe) then Convert.c cmyk2rgb | |
| for (o, p) in out.chunks_exact_mut(3).zip(cmyk.chunks_exact(4)) { | |
| let (c, m, y, k) = (255 - p[0] as u32, 255 - p[1] as u32, 255 - p[2] as u32, 255 - p[3] as u32); | |
| let nk = 255 - k; | |
| let md = |a: u32| -> u8 { | |
| let t = a * nk + 128; | |
| let v = ((t >> 8) + t) >> 8; | |
| nk.saturating_sub(v).min(255) as u8 | |
| }; | |
| o[0] = md(c); | |
| o[1] = md(m); | |
| o[2] = md(y); | |
| } | |
| return Ok(Rgb { w, h: ht, data: out }); | |
| } | |
| let mut out = vec![0u8; w * ht * 3 + PAD]; | |
| if tj::tj3Decompress8(h, bytes.as_ptr(), bytes.len() as _, out.as_mut_ptr(), (w * 3) as i32, tj::TJPF_TJPF_RGB as i32) != 0 { | |
| return Err(msg(h)); | |
| } | |
| Ok(Rgb { w, h: ht, data: out }) | |
| } | |
| } | |
| // ----------------------------------------------------------------------------- others | |
| fn decode_other(bytes: &[u8]) -> Result<Rgb> { | |
| let img = image::load_from_memory(bytes).map_err(err)?; | |
| let rgb = img.to_rgb8(); | |
| let (w, h) = (rgb.width() as usize, rgb.height() as usize); | |
| Ok(Rgb::from_vec(w, h, rgb.into_raw())) | |
| } | |