M4.68: BMP export at 1 to 32 bits, Windows or OS/2, with RLE and flipped rows

document.export {format: "bmp"} writes BMP through the raster export path:
depth 1 (black and white), 4 or 8 (a palette by reduction and dither,
optionally RLE4/RLE8), 16 (5-5-5), 24 or 32 (with alpha, a version 4
header); colorModel rgb or gray; fileFormat windows or os2 (a core header);
flipRows writes top-down rows (a negative height). Impossible combinations
are refused as bad params. Export As offers it with the bmpOptions dialog,
which keeps its choices writable together; BMP moves from read-only to read
and write (tests that used .bmp as the unwritable format now use .dwg).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 15:24:35 +07:00
co-authored by Claude Opus 5.5
parent d01b2fbd8a
commit f2fc8782c7
19 changed files with 668 additions and 44 deletions
+38 -2
View File
@@ -12,7 +12,7 @@ use vectorcraft_render::AntiAlias;
use vectorcraft_render::encode::jpeg::{self, JpegOptions};
use vectorcraft_render::encode::quantize::{Dither, PaletteOptions, Reduction};
use vectorcraft_render::encode::tiff::{ByteOrder, TiffOptions};
use vectorcraft_render::encode::{RasterExportOptions, RasterFormat};
use vectorcraft_render::encode::{RasterExportOptions, RasterFormat, bmp};
use super::super::*;
use super::Format;
@@ -165,6 +165,12 @@ struct RasterOptions {
lossless: Option<bool>,
lzw: Option<bool>,
byte_order: Option<String>,
/// BMP bits per pixel.
depth: Option<u8>,
/// BMP layout: `windows` or `os2`.
file_format: Option<String>,
rle: Option<bool>,
flip_rows: Option<bool>,
}
impl RasterOptions {
@@ -214,10 +220,38 @@ impl RasterOptions {
byte_order: parse(self.byte_order.as_deref(), ByteOrder::from_id, "byteOrder", "little or big")?,
embed_icc,
},
bmp: bmp::BmpOptions {
os2: parse(self.file_format.as_deref(), bmp_layout, "fileFormat", "windows or os2")?,
depth: self.depth.unwrap_or(24),
rle: self.rle.unwrap_or(false),
top_down: self.flip_rows.unwrap_or(false),
gray: color_model == jpeg::ColorModel::Gray,
},
})
}
}
/// The BMP layout by name: `true` for OS/2.
fn bmp_layout(s: &str) -> Option<bool> {
match s.to_ascii_lowercase().as_str() {
"windows" => Some(false),
"os2" | "os/2" => Some(true),
_ => None,
}
}
/// Refuse options `format` can't write together: BMP depths and layouts, and colour models (BMP
/// is RGB or grey).
fn check_options(format: RasterFormat, o: &RasterExportOptions) -> Result<()> {
let model = o.jpeg.color_model;
let r = match format {
RasterFormat::Bmp if model == jpeg::ColorModel::Cmyk => Err("BMP files are RGB or grayscale: colorModel rgb or gray".to_string()),
RasterFormat::Bmp => o.bmp.check(),
_ => Ok(()),
};
r.map_err(|e| bad(C, e))
}
/// An enum option by id (its default when absent).
fn parse<T: Default>(v: Option<&str>, from_id: fn(&str) -> Option<T>, name: &str, ids: &str) -> Result<T> {
v.map(|s| from_id(s).ok_or_else(|| bad(C, format!("{name} `{s}`: {ids}")))).transpose().map(Option::unwrap_or_default)
@@ -342,7 +376,7 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
let (bytes, warnings) = super::pdf::encode(C, doc, p)?;
return Ok(Encoded { warnings, ..Encoded::one(bytes) });
}
"png" | "jpg" | "webp" | "gif" | "png8" | "tiff" => {
"png" | "jpg" | "webp" | "gif" | "png8" | "tiff" | "bmp" => {
let o: RasterOptions = options(f, p)?;
// New Document → Background Contents: White makes the export opaque, unless `background`
// says otherwise (JPEG has no alpha: white either way).
@@ -355,8 +389,10 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
"gif" => RasterFormat::Gif,
"png8" => RasterFormat::Png8,
"tiff" => RasterFormat::Tiff,
"bmp" => RasterFormat::Bmp,
_ => RasterFormat::WebP,
};
check_options(format, &settings)?;
// Use Artboards: one file per chosen artboard (default all); else the one chosen.
let chosen = match use_artboards {
Some(true) => boards(o.boards.resolve(n))?.unwrap_or_else(|| (0..n).collect()),