mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 11:16:25 +00:00
TIFF: CMYK files with unnamed extra samples past their ExtraSamples values open opaque
A CMYK TIFF with two or more samples past its inks and its ExtraSamples values, or with no ExtraSamples tag, which the decoder gives no color type, now opens opaque when none of those values names an alpha. It failed to open before; one that names an alpha already read with it.
This commit is contained in:
+15
-5
@@ -715,7 +715,8 @@ pub fn turned_cmyk(bytes: &[u8], orientation: image::metadata::Orientation, ppi:
|
||||
/// divided back out, and a pixel with no associated alpha has no color. 16-bit samples are
|
||||
/// converted at 16 bits, and only the result is rounded to 8 bits. The samples may be stored
|
||||
/// together or one plane per channel. A CMYK file with two or more samples past its inks and its
|
||||
/// ExtraSamples values is read when one of those values names an alpha. `None` for other images,
|
||||
/// ExtraSamples values (or with no ExtraSamples tag) is read too, opaque when none of those values
|
||||
/// names an alpha. `None` for other images,
|
||||
/// CMYK ones without alpha (their inks are read by [`ImageBlob::cmyk`]), ones larger than
|
||||
/// [`MAX_CMYK_PIXELS`], ones whose SamplesPerPixel is more than [`MAX_EXTRA_SAMPLES_PIXEL`] and
|
||||
/// ones whose samples, with the copy a planar file needs, the row the decoder reads each strip or
|
||||
@@ -740,14 +741,14 @@ pub fn extra_samples_tiff_rgba(bytes: &[u8], rgb: impl Fn([f32; 4]) -> [f32; 3])
|
||||
Some(tiff::ColorType::CMYKA(8 | 16)) => (4, if extras.is_empty() { Some((0, false)) } else { named }),
|
||||
Some(tiff::ColorType::CMYK(8 | 16)) if named.is_some() => (4, named),
|
||||
Some(tiff::ColorType::Multiband { bit_depth: 8 | 16, num_samples: 2.. }) if matches!(photometric, Some(0 | 1)) => (1, named),
|
||||
None if photometric == Some(5) && named.is_some() && uncounted.is_some_and(|c| c > 5) => (4, named),
|
||||
None if photometric == Some(5) && uncounted.is_some_and(|c| c > 5) => (4, named),
|
||||
_ => return None,
|
||||
};
|
||||
let n = usize::try_from(pixels(w, h)?).ok()?;
|
||||
// The decoder gives a CMYK pixel its inks and at most one extra sample, and doesn't read
|
||||
// white-is-zero gray with extra samples: those are read as black-is-zero gray, every sample.
|
||||
let given = kind.map_or(0, |k| usize::from(k.num_samples()));
|
||||
let (data, samples) = if photometric == Some(0) || alpha.is_some_and(|(i, _)| colour + i >= given) {
|
||||
let (data, samples) = if photometric == Some(0) || kind.is_none() || alpha.is_some_and(|(i, _)| colour + i >= given) {
|
||||
let mut d = tiff::decoder::Decoder::new(as_black_is_zero(bytes)?).ok()?;
|
||||
let tiff::ColorType::Multiband { bit_depth: bits @ (8 | 16), num_samples } = d.colortype().ok()? else { return None };
|
||||
let samples = usize::from(num_samples);
|
||||
@@ -1169,7 +1170,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// A CMYK TIFF of six samples a pixel whose ExtraSamples has only one value: the sample after
|
||||
/// the inks is alpha only when that value says so, and the sample after it is never alpha.
|
||||
/// the inks is alpha only when that value says so, and the sample after it is never alpha. Files
|
||||
/// with more samples than the decoder gives a color type read the same way, opaque when no
|
||||
/// value names an alpha.
|
||||
#[test]
|
||||
fn a_cmyk_tiff_with_fewer_extra_sample_values_than_samples_reads_only_a_named_alpha() {
|
||||
let naive = |[c, m, y, k]: [f32; 4]| [(1.0 - c) * (1.0 - k), (1.0 - m) * (1.0 - k), (1.0 - y) * (1.0 - k)];
|
||||
@@ -1182,7 +1185,14 @@ mod tests {
|
||||
let wide = [255, 0, 0, 0, 9, 128, 17, 18];
|
||||
let px = extra_samples_tiff_rgba(&extras_tiff::<Cmyk8x8>(&[0, 2], &wide), naive).expect("reads");
|
||||
assert_eq!(px.into_raw(), [0, 255, 255, 128], "cyan at the alpha the second value names");
|
||||
assert!(extra_samples_tiff_rgba(&extras_tiff::<Cmyk8x8>(&[0, 0], &wide), naive).is_none(), "no value names an alpha: not read");
|
||||
let px = extra_samples_tiff_rgba(&extras_tiff::<Cmyk8x8>(&[0, 0], &wide), naive).expect("no value names an alpha: reads");
|
||||
assert_eq!(px.into_raw(), [0, 255, 255, 255], "no value names an alpha: opaque cyan");
|
||||
// Six samples and no ExtraSamples tag: the decoder gives the file no color type either.
|
||||
let mut file = Cursor::new(Vec::new());
|
||||
let mut enc = tiff::encoder::TiffEncoder::new(&mut file).unwrap();
|
||||
enc.write_image::<Cmyk6x8>(1, 1, &wide[..6]).unwrap();
|
||||
let px = extra_samples_tiff_rgba(&file.into_inner(), naive).expect("no ExtraSamples tag: reads");
|
||||
assert_eq!(px.into_raw(), [0, 255, 255, 255], "no ExtraSamples tag: opaque cyan");
|
||||
// Seven samples with two values: the decoder reads it as CMYK with alpha.
|
||||
let px = extra_samples_tiff_rgba(&extras_tiff::<Cmyk7x8>(&[0, 2], &wide[..7]), naive).expect("reads");
|
||||
assert_eq!(px.into_raw(), [0, 255, 255, 128], "seven samples: cyan at the alpha the second value names");
|
||||
|
||||
@@ -1783,7 +1783,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546).
|
||||
- TIFFs that store each channel as its own plane (PlanarConfiguration 2) place, relink and open as the same image stored pixel by pixel does, 8 and 16 bits: grayscale with an alpha channel or other extra channels, and CMYK with or without them. A planar CMYK TIFF keeps its ink amounts and is stored as a CMYK TIFF with each pixel's inks together, at the file's resolution. A TIFF whose header claims more strips or tiles than can be counted is refused as too large to draw (#1466 in part; `crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- A 16-bit CMYK or grayscale TIFF with an alpha channel is converted from its 16-bit samples, and only the resulting color and alpha are rounded to 8 bits; associated alpha is divided out at 16 bits, and a pixel whose associated alpha is zero has no color. [TIFF 6.0](https://printtechnologies.org/standards/files/tiff-v6.pdf) gives extra samples the depth in BitsPerSample, and its Associated Alpha Handling section states the arithmetic and the zero-alpha rule for RGB data; applying them to CMYK and gray samples is VectorCraft's own rule. Each sample was cut to its high byte first, so a faint ink or a low alpha could be lost (`crates/doc/src/cmyk.rs`, `crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- A CMYK or grayscale TIFF whose alpha channel comes after another extra channel places and opens with that alpha, 8 and 16 bits, chunky or planar, and the other extra channels are left out. A channel that the file does not mark as alpha is not read as transparency, even when its ExtraSamples tag has fewer values than the file has extra channels, and a white-is-zero grayscale TIFF with an alpha channel opens too. A document saved with such a CMYK TIFF as a CMYK image keeps it as one, with its ink amounts. Such a TIFF is read when its samples take more than 256 MiB, as long as its samples, the copy a planar file needs, the row the decoder reads each strip or tile into and the RGBA pixels together fit the decoding allowance; a TIFF whose SamplesPerPixel is more than 64 is not read, which is VectorCraft's own limit, and both checks run on the file's tags before any decoding. [TIFF 6.0](https://printtechnologies.org/standards/files/tiff-v6.pdf) gives each extra channel its own ExtraSamples value, in order (pp. 31–32), and tells readers to skip extra channels they do not use (p. 27); reading the first channel marked as alpha is VectorCraft's own rule (`crates/doc/src/cmyk.rs`, `crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- A CMYK or grayscale TIFF whose alpha channel comes after another extra channel places and opens with that alpha, 8 and 16 bits, chunky or planar, and the other extra channels are left out. A channel that the file does not mark as alpha is not read as transparency, even when its ExtraSamples tag has fewer values than the file has extra channels. A CMYK TIFF with two or more extra channels past its ExtraSamples values, or with no ExtraSamples tag, opens opaque when none of them is marked as alpha, and a white-is-zero grayscale TIFF with an alpha channel opens too. A document saved with such a CMYK TIFF as a CMYK image keeps it as one, with its ink amounts. Such a TIFF is read when its samples take more than 256 MiB, as long as its samples, the copy a planar file needs, the row the decoder reads each strip or tile into and the RGBA pixels together fit the decoding allowance; a TIFF whose SamplesPerPixel is more than 64 is not read, which is VectorCraft's own limit, and both checks run on the file's tags before any decoding. [TIFF 6.0](https://printtechnologies.org/standards/files/tiff-v6.pdf) gives each extra channel its own ExtraSamples value, in order (pp. 31–32), and tells readers to skip extra channels they do not use (p. 27); reading the first channel marked as alpha is VectorCraft's own rule (`crates/doc/src/cmyk.rs`, `crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- 1-bit TIFFs compressed with CCITT Group 3 (ITU-T T.4) place, relink and open: Compression 2 (modified Huffman) and Compression 3 with one-dimensional rows or, with T4Options bit 0, two-dimensional ones, with or without fill bits, in strips or tiles, with either FillOrder and either polarity. Rows in T.4's uncompressed mode are not read. A file whose pixels would take more than the decoding allowance as RGBA is refused as too large to draw (#1466 in part; `crates/engine/src/cmd/fileio/tests.rs`).
|
||||
- EPS and PostScript import keeps a CMYK image's ink amounts, as PDF import does: the samples of `image` and `colorimage` in a CMYK color space, and of a CMYK JPEG read through `DCTDecode`, come in as a CMYK TIFF, read through the image's Decode array, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask (`crates/eps/src/import/tests_images.rs`). Such images came in as RGB PNGs.
|
||||
- EPS and PostScript import reads image data through `CCITTFaxDecode`: Group 4 (ITU-T T.6) and Group 3 one-dimensional and mixed (ITU-T T.4) data, with the filter's `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1` parameters, decodes to 1-bit rows for `image` and `imagemask`. Damaged data keeps the rows decoded before it (damage in the first row is a PostScript `ioerror`), and parameters out of range or past the 256 MiB limit on decoded data are refused (`crates/eps/src/import/tests_images.rs`). Reading such a file stopped at an error naming the filter, and the file opened from a fallback, such as its preview or the art drawn before the image, or did not open.
|
||||
|
||||
Reference in New Issue
Block a user