mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 18:27:02 -06:00
EPS files the reference app saves (its procsets in the prolog, a binary header with a TIFF preview) now open as editable art instead of failing with "undefinedresource in Generic". When their PostScript still can't be read, the palette preview they carry is placed with the usual warning instead of the file being refused. Root cause: five interpreter gaps and one preview format. The Generic and Category resource categories didn't exist, so categories couldn't be made from Generic (now Category holds the standard categories, defineresource in Category makes new ones, undefineresource is added); clipsave/cliprestore were unknown (now a clip stack in the graphics state); systemdict's languagelevel was the operator, not the integer 3; getinterval copied arrays and strings instead of sharing their storage, so astore into a subarray was lost (arrays and strings are now views into shared storage, and readstring/readline return the part they fill); and resourceforall was unknown (now enumerates matching names with * ? \ templates). The preview is an 8-bit palette TIFF with an alpha sample, which the image crate can't decode: a small bounds-checked reader handles that layout (either byte order, any strips). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
6.6 KiB
Rust
162 lines
6.6 KiB
Rust
//! The TIFF preview (little-endian, PackBits strips, written by the raster exports' TIFF writer):
|
|
//! 1-bit black and white, or 8-bit RGB with or without an alpha channel. Also the PNG thumbnail,
|
|
//! and a reader for the palette previews other apps write that the `image` crate can't decode.
|
|
|
|
use std::borrow::Cow;
|
|
|
|
use vectorcraft_render::encode::on_white;
|
|
use vectorcraft_render::encode::tiff::{self, ByteOrder, Compression, Image, Layout, Photometric};
|
|
|
|
use crate::Raster;
|
|
|
|
/// `img` as a TIFF: one bit a pixel (`bw`: black where darker than mid-grey on white), else RGB
|
|
/// over white or, with `alpha`, RGBA (unassociated alpha).
|
|
pub(crate) fn encode(img: &Raster, bw: bool, alpha: bool) -> Result<Vec<u8>, String> {
|
|
let w = img.width as usize;
|
|
if w == 0 || img.height == 0 || w.checked_mul(img.height as usize).and_then(|n| n.checked_mul(4)) != Some(img.rgba.len()) {
|
|
return Err("the preview image is empty".into());
|
|
}
|
|
let px = img.rgba.as_chunks::<4>().0;
|
|
let (data, photometric, samples, bits): (Cow<[u8]>, _, u16, u16) = if bw {
|
|
let mut data = Vec::with_capacity(w.div_ceil(8) * img.height as usize);
|
|
for line in px.chunks(w) {
|
|
let at = data.len();
|
|
data.resize(at + w.div_ceil(8), 0);
|
|
for (x, p) in line.iter().enumerate() {
|
|
let [r, g, b] = on_white(p);
|
|
let luma = 299 * u32::from(r) + 587 * u32::from(g) + 114 * u32::from(b);
|
|
// WhiteIsZero: a set bit is black.
|
|
if luma < 128_000
|
|
&& let Some(byte) = data.get_mut(at + x / 8)
|
|
{
|
|
*byte |= 0x80 >> (x % 8);
|
|
}
|
|
}
|
|
}
|
|
(Cow::Owned(data), Photometric::WhiteIsZero, 1, 1)
|
|
} else if alpha {
|
|
(Cow::Borrowed(&img.rgba), Photometric::Rgb, 4, 8)
|
|
} else {
|
|
(px.iter().flat_map(on_white).collect(), Photometric::Rgb, 3, 8)
|
|
};
|
|
let image = Image { width: img.width, height: img.height, data: &data, photometric, samples, bits, alpha: alpha && !bw };
|
|
tiff::write(&image, &Layout { byte_order: ByteOrder::Little, compression: Compression::PackBits, ppi: 72.0, icc: None })
|
|
.map_err(|e| format!("the preview: {e}"))
|
|
}
|
|
|
|
/// `img` as a PNG.
|
|
pub(crate) fn png(img: &Raster) -> Result<Vec<u8>, String> {
|
|
let rgba = image::RgbaImage::from_raw(img.width, img.height, img.rgba.clone()).ok_or("the thumbnail image is malformed")?;
|
|
let mut out = vec![];
|
|
rgba.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).map_err(|e| e.to_string())?;
|
|
Ok(out)
|
|
}
|
|
|
|
/// Most pixels a palette preview may have.
|
|
const MAX_PIXELS: usize = 1 << 26;
|
|
/// Most values a TIFF field may have.
|
|
const MAX_VALUES: usize = 1 << 20;
|
|
|
|
/// A TIFF's bytes in its byte order.
|
|
struct Tiff<'a> {
|
|
b: &'a [u8],
|
|
big: bool,
|
|
}
|
|
|
|
impl Tiff<'_> {
|
|
fn u16(&self, at: usize) -> Option<u16> {
|
|
let b: [u8; 2] = self.b.get(at..at.checked_add(2)?)?.try_into().ok()?;
|
|
Some(if self.big { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) })
|
|
}
|
|
|
|
fn u32(&self, at: usize) -> Option<u32> {
|
|
let b: [u8; 4] = self.b.get(at..at.checked_add(4)?)?.try_into().ok()?;
|
|
Some(if self.big { u32::from_be_bytes(b) } else { u32::from_le_bytes(b) })
|
|
}
|
|
|
|
/// The values of the directory entry at `at` (bytes, shorts or longs; in the entry when they
|
|
/// fit in four bytes).
|
|
fn values(&self, at: usize) -> Option<Vec<u32>> {
|
|
let (kind, n) = (self.u16(at.checked_add(2)?)?, self.u32(at.checked_add(4)?)? as usize);
|
|
let size = match kind {
|
|
1 => 1,
|
|
3 => 2,
|
|
4 => 4,
|
|
_ => return None,
|
|
};
|
|
if n > MAX_VALUES {
|
|
return None;
|
|
}
|
|
let field = at.checked_add(8)?;
|
|
let start = if n * size <= 4 { field } else { self.u32(field)? as usize };
|
|
(0..n)
|
|
.map(|i| {
|
|
let at = start.checked_add(i * size)?;
|
|
match size {
|
|
1 => self.b.get(at).map(|v| u32::from(*v)),
|
|
2 => self.u16(at).map(u32::from),
|
|
_ => self.u32(at),
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// An 8-bit palette TIFF, with or without an alpha sample (`ExtraSamples`), uncompressed and
|
|
/// interleaved, as RGBA: the layout of other apps' EPS previews. `None` for anything else.
|
|
pub(crate) fn palette_rgba(bytes: &[u8]) -> Option<image::RgbaImage> {
|
|
let big = match bytes.get(..4)? {
|
|
b"II*\0" => false,
|
|
b"MM\0*" => true,
|
|
_ => return None,
|
|
};
|
|
let t = Tiff { b: bytes, big };
|
|
let ifd = t.u32(4)? as usize;
|
|
let entries = usize::from(t.u16(ifd)?);
|
|
let mut fields = std::collections::HashMap::new();
|
|
for i in 0..entries {
|
|
let at = ifd.checked_add(2 + i * 12)?;
|
|
fields.insert(t.u16(at)?, at);
|
|
}
|
|
let get = |tag: u16| fields.get(&tag).and_then(|at| t.values(*at));
|
|
let one = |tag: u16| get(tag).and_then(|v| v.first().copied());
|
|
let (w, h) = (one(256)? as usize, one(257)? as usize);
|
|
let samples = one(277).unwrap_or(1) as usize;
|
|
let alpha = samples == 2 && matches!(get(338)?.as_slice(), [0..=2]);
|
|
let plain = one(259).unwrap_or(1) == 1 && one(262)? == 3 && one(284).unwrap_or(1) == 1;
|
|
if !plain || !(samples == 1 || alpha) || get(258)?.iter().any(|b| *b != 8) || w == 0 || h == 0 || w.checked_mul(h)? > MAX_PIXELS {
|
|
return None;
|
|
}
|
|
let map = get(320)?;
|
|
if map.len() != 3 * 256 {
|
|
return None;
|
|
}
|
|
// Strips of `per` rows each, in order.
|
|
let (offsets, counts) = (get(273)?, get(279)?);
|
|
let per = (one(278).unwrap_or(u32::MAX) as usize).min(h);
|
|
let row = w * samples;
|
|
// The strips are in the file: so is all that's allocated.
|
|
if per == 0 || offsets.len() != counts.len() || offsets.len() < h.div_ceil(per) || row * h > bytes.len() {
|
|
return None;
|
|
}
|
|
let mut data = Vec::with_capacity(row * h);
|
|
for (k, (at, n)) in offsets.iter().zip(&counts).take(h.div_ceil(per)).enumerate() {
|
|
let want = per.min(h - k * per) * row;
|
|
if (*n as usize) < want {
|
|
return None;
|
|
}
|
|
let at = *at as usize;
|
|
data.extend_from_slice(bytes.get(at..at.checked_add(want)?)?);
|
|
}
|
|
// Map entries are 16-bit.
|
|
let channel = |c: usize, i: usize| map.get(c * 256 + i).map_or(0, |v| ((u64::from(*v).min(65_535) * 255 + 32_767) / 65_535) as u8);
|
|
let rgba = data
|
|
.chunks_exact(samples)
|
|
.flat_map(|px| {
|
|
let i = usize::from(px.first().copied().unwrap_or(0));
|
|
[channel(0, i), channel(1, i), channel(2, i), px.get(1).copied().unwrap_or(255)]
|
|
})
|
|
.collect();
|
|
image::RgbaImage::from_raw(w as u32, h as u32, rgba)
|
|
}
|