mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
M4.69: Targa export at 16, 24 or 32 bits per pixel
document.export {format: "tga"} writes uncompressed true-colour Targa files
through the raster export path (resolution, background, anti-aliasing,
artboard ranges): depth 16 (5-5-5 with a one-bit alpha), 24 (flattened on
white) or 32 (8-bit alpha), rows bottom-up, with the version 2 footer.
Export As offers it with the tgaOptions dialog (Depth).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f2fc8782c7
commit
b578c32f4d
@@ -9,6 +9,7 @@ pub mod gif;
|
||||
pub mod jpeg;
|
||||
pub mod png;
|
||||
pub mod quantize;
|
||||
pub mod tga;
|
||||
pub mod tiff;
|
||||
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
@@ -27,6 +28,7 @@ pub enum RasterFormat {
|
||||
Gif,
|
||||
Tiff,
|
||||
Bmp,
|
||||
Tga,
|
||||
}
|
||||
|
||||
/// How a raster export renders and encodes.
|
||||
@@ -49,6 +51,8 @@ pub struct RasterExportOptions {
|
||||
pub tiff: tiff::TiffOptions,
|
||||
/// BMP layout, depth, compression and row order.
|
||||
pub bmp: bmp::BmpOptions,
|
||||
/// Targa depth.
|
||||
pub tga: tga::TgaOptions,
|
||||
}
|
||||
|
||||
impl Default for RasterExportOptions {
|
||||
@@ -63,6 +67,7 @@ impl Default for RasterExportOptions {
|
||||
palette: quantize::PaletteOptions::default(),
|
||||
tiff: tiff::TiffOptions::default(),
|
||||
bmp: bmp::BmpOptions::default(),
|
||||
tga: tga::TgaOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,6 +114,7 @@ impl RasterExportOptions {
|
||||
}
|
||||
RasterFormat::Tiff => tiff::encode(img, self.ppi, &self.tiff),
|
||||
RasterFormat::Bmp => bmp::encode(&img.to_straight(), img.width, img.height, self.ppi, &self.bmp, &self.palette),
|
||||
RasterFormat::Tga => tga::encode(&img.to_straight(), img.width, img.height, &self.tga),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,4 +197,6 @@ mod tests_jpeg;
|
||||
#[cfg(test)]
|
||||
mod tests_palette;
|
||||
#[cfg(test)]
|
||||
mod tests_tga;
|
||||
#[cfg(test)]
|
||||
mod tests_tiff;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Targa export: depths, alpha and the footer.
|
||||
|
||||
use super::on_white;
|
||||
use super::tests_bmp::{art, decode, u16_at};
|
||||
use super::tga::{self, TgaOptions};
|
||||
|
||||
#[test]
|
||||
fn targa_depths_alpha_and_footer() {
|
||||
let (w, h) = (7, 3);
|
||||
let px = art(w, h);
|
||||
for (depth, alpha_bits) in [(16, 1), (24, 0), (32, 8)] {
|
||||
let file = tga::encode(&px, w, h, &TgaOptions { depth }).unwrap();
|
||||
assert_eq!((file[2], file[16], file[17]), (2, depth, alpha_bits), "true colour, the depth byte, alpha bits");
|
||||
assert_eq!((u16_at(&file, 12), u16_at(&file, 14)), (7, 3));
|
||||
assert_eq!(file.len(), 18 + (w * h) as usize * usize::from(depth / 8) + 26);
|
||||
assert!(file.ends_with(b"TRUEVISION-XFILE.\0"));
|
||||
let img = decode(&file, image::ImageFormat::Tga);
|
||||
assert_eq!(img.dimensions(), (w, h));
|
||||
let want = on_white(px[(2 * w as usize + 3) * 4..][..4].try_into().unwrap());
|
||||
let got = img.get_pixel(3, 2).0;
|
||||
assert!(got[..3].iter().zip(want).all(|(a, b)| a.abs_diff(b) <= 8), "depth {depth}: {got:?} vs {want:?}");
|
||||
match depth {
|
||||
32 => assert_eq!((img.get_pixel(0, 0).0[3], img.get_pixel(1, 0).0[3]), (0, 128), "8-bit alpha"),
|
||||
24 => assert_eq!(img.get_pixel(0, 0).0, [255; 4], "flattened on white"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert!(tga::encode(&px, w, h, &TgaOptions { depth: 8 }).is_err());
|
||||
assert!(tga::encode(&px, 70_000, 1, &TgaOptions::default()).is_err(), "too wide");
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Targa export: uncompressed true colour at 16 bits per pixel (5-5-5 and a one-bit alpha), 24 or
|
||||
//! 32 (8-bit alpha), rows bottom-up, with the version 2 footer.
|
||||
|
||||
use super::on_white;
|
||||
|
||||
/// Bits per pixel a Targa export can have.
|
||||
pub const DEPTHS: [u8; 3] = [16, 24, 32];
|
||||
|
||||
/// The Targa options of a raster export.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TgaOptions {
|
||||
/// Bits per pixel ([`DEPTHS`]).
|
||||
pub depth: u8,
|
||||
}
|
||||
|
||||
impl Default for TgaOptions {
|
||||
fn default() -> Self {
|
||||
Self { depth: 24 }
|
||||
}
|
||||
}
|
||||
|
||||
impl TgaOptions {
|
||||
/// Why these options can't be written, if they can't.
|
||||
pub fn check(&self) -> Result<(), String> {
|
||||
if DEPTHS.contains(&self.depth) { Ok(()) } else { Err(format!("Targa depth {}: 16, 24 or 32 bits per pixel", self.depth)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Straight-alpha RGBA pixels as a Targa file. 24 bits blend partly transparent pixels over white;
|
||||
/// at 16 bits a pixel is opaque from half opacity up, else transparent.
|
||||
pub fn encode(rgba: &[u8], width: u32, height: u32, o: &TgaOptions) -> Result<Vec<u8>, String> {
|
||||
o.check()?;
|
||||
let (Ok(w16), Ok(h16)) = (u16::try_from(width), u16::try_from(height)) else {
|
||||
return Err(format!("{width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution"));
|
||||
};
|
||||
let (w, h) = (usize::from(w16), usize::from(h16));
|
||||
if w == 0 || h == 0 || w * h * 4 != rgba.len() {
|
||||
return Err("Targa encoding failed: the pixel buffer doesn't match the image size".into());
|
||||
}
|
||||
let px = rgba.as_chunks::<4>().0;
|
||||
let mut out = Vec::with_capacity(18 + w * h * usize::from(o.depth / 8) + 26);
|
||||
// No image id or colour map, uncompressed true colour, origin (0, 0).
|
||||
out.extend([0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
out.extend(w16.to_le_bytes());
|
||||
out.extend(h16.to_le_bytes());
|
||||
// Bits per pixel, then the alpha bits (rows bottom-up, left to right).
|
||||
let alpha_bits = match o.depth {
|
||||
16 => 1,
|
||||
32 => 8,
|
||||
_ => 0,
|
||||
};
|
||||
out.extend([o.depth, alpha_bits]);
|
||||
for row in px.chunks(w).rev() {
|
||||
for p in row {
|
||||
match o.depth {
|
||||
16 => {
|
||||
let v = if p[3] >= 128 {
|
||||
let [r, g, b] = [p[0], p[1], p[2]].map(|c| u16::from(c >> 3));
|
||||
0x8000 | r << 10 | g << 5 | b
|
||||
} else {
|
||||
0
|
||||
};
|
||||
out.extend(v.to_le_bytes());
|
||||
}
|
||||
24 => {
|
||||
let [r, g, b] = on_white(p);
|
||||
out.extend([b, g, r]);
|
||||
}
|
||||
_ => out.extend([p[2], p[1], p[0], p[3]]),
|
||||
}
|
||||
}
|
||||
}
|
||||
// No extension area or developer directory.
|
||||
out.extend([0; 8]);
|
||||
out.extend(b"TRUEVISION-XFILE.\0");
|
||||
Ok(out)
|
||||
}
|
||||
Reference in New Issue
Block a user