app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
//! `swx_render_image` — a high-resolution image of the active document,
//! rendered by SOLIDWORKS's own viewport (0.9.18, track A).
//!
//! Drew, 2026-10-02: *"Does SW have rendering like fusion so we can get a really
//! gorgeous shot of this assembly?"* — and, if it works, *"add a bunch of intel
//! into the bridge to handle rendering."* The Fusion bridge has no render verb
//! at all (docs/FUSION-PARITY-REPORT.md).
//!
//! MEASURED: PhotoView 360 is ABSENT on this edition, so the renderer is the
//! viewport itself — RealView, ambient occlusion, shadows and perspective on the
//! laptop's GPU. `swx_screenshot` (ab's PrintWindow) works but is a screen grab
//! capped at the window (~1568 px wide).
//!
//! HOW — MEASURED 2026-10-05 on the laptop (SOLIDWORKS 2026 SP3, the 110-document
//! Moveo assembly, window MINIMIZED): 1920x1080 in 10.4 s and 3840x2160 in
//! 11.6 s with isometric + shaded + perspective + RealView + AO + zoomToFit, the
//! PNG header matching, every setting read back restored. See docs/RENDERING.md:
//! * the export is `IModelDoc2::SaveBMP(FileName, Width, Height)` — "Saves the
//! current view as a bitmap (BMP) file … If WidthIn or HeightIn is less than
//! or equal to 0, then the view size is based on the current window size."
//! <https://help.solidworks.com/2024/english/api/sldworksapi/SOLIDWORKS.Interop.sldworks~SOLIDWORKS.Interop.sldworks.IModelDoc2~SaveBMP.html>
//! It takes a pixel size independent of the window, which the TIF/PNG
//! "print capture" options do NOT for a model: swTiffPrintDPI, the paper
//! size and the custom width/height are documented "For drawings only"
//! (System Options > Export > TIF/PSD/JPG/PNG,
//! <https://help.solidworks.com/2017/english/api/swconst/filesaveastifpsdoptions.htm>);
//! * the bridge then converts the BMP to a PNG itself and verifies it BY
//! EFFECT: the file exists, starts with the PNG signature, and its IHDR
//! width × height is read back — never echoed from the arguments. If the
//! export came back smaller than asked (window-capped), the reply says
//! `capped:true` with the real size;
//! * every display setting the render changes is CAPTURED BEFORE, RESTORED
//! AFTER (on error too), read back, and listed in `restoredSettings`. The
//! document is never saved. The user's settings are never changed silently.
use serde_json::{json, Value};
pub const MAX_SIDE: u64 = 8192;
pub const DEFAULT_W: u64 = 1920;
pub const DEFAULT_H: u64 = 1080;
/// Standard views, `swStandardViews_e` (published:
/// <https://help.solidworks.com/2024/english/api/swconst/SOLIDWORKS.Interop.swconst~SOLIDWORKS.Interop.swconst.swStandardViews_e.html>).
pub const VIEWS: &[(&str, i32)] = &[
("front", 1),
("back", 2),
("left", 3),
("right", 4),
("top", 5),
("bottom", 6),
("isometric", 7),
("trimetric", 8),
("dimetric", 9),
];
/// Display styles, `swViewDisplayMode_e` (published:
/// <https://help.solidworks.com/2024/english/api/swconst/SOLIDWORKS.Interop.swconst~SOLIDWORKS.Interop.swconst.swViewDisplayMode_e.html>),
/// set through `IModelView::DisplayMode`.
pub const DISPLAY_STYLES: &[(&str, i32)] = &[
("wireframe", 1),
("hiddenLinesRemoved", 2),
("shaded", 4),
("shadedWithEdges", 5),
];
/// Backgrounds this bridge can set: a PLAIN viewport colour
/// (`swColorsBackgroundAppearance_e.swColorsBackgroundAppearance_Plain` = 0,
/// <https://help.solidworks.com/2024/english/api/swconst/SOLIDWORKS.Interop.swconst~SOLIDWORKS.Interop.swconst.swColorsBackgroundAppearance_e.html>)
/// with `swSystemColorsViewportBackground` as a Win32 COLORREF.
pub const BACKGROUNDS: &[(&str, i64)] = &[("white", 0x00FF_FFFF), ("black", 0)];
/// Does this request change anything ON SCREEN? Anything but a plain export of
/// the current view does (orientation, display style, RealView, AO, shadows,
/// perspective, background, zoom) — and that must respect the user-presence
/// guard exactly as a sweep does.
pub fn changes_display(args: &Value) -> bool {
let set = |k: &str| args.get(k).map(|v| !v.is_null()).unwrap_or(false);
let view = args.get("view").and_then(|v| v.as_str()).unwrap_or("current");
let bg = args.get("background").and_then(|v| v.as_str()).unwrap_or("current");
view != "current"
|| bg != "current"
|| ["displayStyle", "perspective", "realView", "ambientOcclusion", "shadows"].iter().any(|k| set(k))
|| args.get("zoomToFit").and_then(|v| v.as_bool()).unwrap_or(false)
|| args.get("zoomFactor").and_then(|v| v.as_f64()).map(|z| (z - 1.0).abs() > 1e-9).unwrap_or(false)
}
/// The validated request, as the com host receives it.
pub struct Request {
pub png: std::path::PathBuf,
pub bmp: std::path::PathBuf,
pub width: u64,
pub height: u64,
pub host_args: Value,
}
fn sanitize(s: &str) -> String {
let s: String = s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '_' }).collect();
let s = s.trim_matches('_').to_string();
if s.is_empty() { "document".into() } else { s }
}
/// VALIDATE. Never creates a directory outside the data root's `exports\`.
pub fn request(args: &Value, doc_title: &str) -> Result<Request, Value> {
let bad = |code: &str, msg: String, hint: &str| Err(crate::verbs::err_code(code, msg, hint));
let dim = |k: &str, d: u64| -> Result<u64, Value> {
match args.get(k) {
None | Some(Value::Null) => Ok(d),
Some(v) => match v.as_u64() {
Some(n) if (16..=MAX_SIDE).contains(&n) => Ok(n),
_ => Err(crate::verbs::err_code("bad_size", format!("{k} must be a whole number of pixels from 16 to {MAX_SIDE}"), "e.g. width:3840, height:2160")),
},
}
};
let width = dim("width", DEFAULT_W)?;
let height = dim("height", DEFAULT_H)?;
let view = args.get("view").and_then(|v| v.as_str()).unwrap_or("current").to_string();
if view != "current" && !VIEWS.iter().any(|(n, _)| *n == view) {
return bad("bad_view", format!("view {view:?} is not one of current, {}", VIEWS.iter().map(|v| v.0).collect::<Vec<_>>().join(", ")), "view:\"isometric\" is the usual hero angle");
}
let style = args.get("displayStyle").and_then(|v| v.as_str());
if let Some(s) = style {
if !DISPLAY_STYLES.iter().any(|(n, _)| *n == s) {
return bad("bad_display_style", format!("displayStyle {s:?} is not one of {}", DISPLAY_STYLES.iter().map(|v| v.0).collect::<Vec<_>>().join(", ")), "displayStyle:\"shaded\" for a render");
}
}
// FRAMING (0.9.19): MEASURED 2026-10-05, zoomToFit leaves the Moveo
// assembly at ~25% of a 16:9 frame (the fit box is the window's aspect).
// `zoomFactor` multiplies IModelView::Scale2 AFTER any fit; the camera is
// restored as before. MEASURED 2026-10-05: >1 zooms IN (Scale2 0.176 -> 0.352
// at 2.0, the model twice as large) — and 2.0 clipped the tall Moveo arm;
// 1.4–1.6 is the suggestion after a fit.
let zoom_factor = match args.get("zoomFactor") {
None | Some(Value::Null) => 1.0,
Some(v) => match v.as_f64() {
Some(z) if (0.1..=20.0).contains(&z) => z,
_ => return bad("bad_zoom_factor", "zoomFactor must be a number from 0.1 to 20 (1.0 = as framed)".into(), "e.g. zoomToFit:true, zoomFactor:2.5 to fill a 16:9 frame"),
},
};
let transparent = args.get("transparentBackground").and_then(|v| v.as_bool()).unwrap_or(false);
let background = args.get("background").and_then(|v| v.as_str()).unwrap_or(if transparent { "transparent" } else { "current" }).to_string();
if background == "transparent" {
return bad(
"render_option_unsupported",
"a transparent background is not available: SaveBMP writes a 24-bit image with no alpha channel".into(),
"Use background:\"white\" (or \"black\") and remove it afterwards, or leave it \"current\". Nothing was changed.",
);
}
if background != "current" && !BACKGROUNDS.iter().any(|(n, _)| *n == background) {
return bad("bad_background", format!("background {background:?} is not one of current, white, black, transparent"), "background:\"white\" is the usual product shot");
}
let exports = crate::scratch::root().join("exports");
let png = match args.get("path").and_then(|p| p.as_str()).filter(|p| !p.trim().is_empty()) {
Some(p) => {
let p = crate::verbs::safe_out(p)?;
let pb = std::path::PathBuf::from(&p);
if !p.to_lowercase().ends_with(".png") {
return bad("bad_path", format!("path must end in .png: {p}"), "The render is written as a PNG.");
}
match pb.parent() {
Some(dir) if dir.is_dir() => {}
_ => {
return bad(
"output_dir_missing",
format!("the folder for {p} does not exist"),
"Create it first, or omit `path` to write into the bridge's exports folder. The bridge never creates folders outside its own data root.",
)
}
}
if pb.exists() {
return bad("output_exists", format!("{p} already exists"), "Choose a new file name; the bridge never writes over an existing file.");
}
pb
}
None => {
if let Err(e) = std::fs::create_dir_all(&exports) {
return bad("output_dir_missing", format!("the exports folder {} could not be created: {e}", exports.display()), "Point SWX_SCRATCH_DIR somewhere writable, or pass `path`.");
}
let ts = crate::launchos::epoch_ms();
exports.join(format!("{}-{}-{}x{}-{}.png", sanitize(doc_title.rsplit(['\\', '/']).next().unwrap_or(doc_title).split('.').next().unwrap_or("document")), sanitize(&view), width, height, ts))
}
};
let _ = std::fs::create_dir_all(&exports);
let bmp = exports.join(format!(".render-{}.bmp", crate::launchos::epoch_ms()));
let host_args = json!({
"bmpPath": bmp.to_string_lossy(),
"width": width,
"height": height,
"view": view,
"viewId": VIEWS.iter().find(|(n, _)| *n == view).map(|v| v.1),
"displayStyle": style,
"displayModeId": style.and_then(|s| DISPLAY_STYLES.iter().find(|(n, _)| *n == s).map(|v| v.1)),
"perspective": args.get("perspective").and_then(|v| v.as_bool()),
"realView": args.get("realView").and_then(|v| v.as_bool()),
"ambientOcclusion": args.get("ambientOcclusion").and_then(|v| v.as_bool()),
"shadows": args.get("shadows").and_then(|v| v.as_bool()),
"background": background,
"backgroundColor": BACKGROUNDS.iter().find(|(n, _)| *n == background).map(|v| v.1),
"zoomToFit": args.get("zoomToFit").and_then(|v| v.as_bool()).unwrap_or(false),
"zoomFactor": zoom_factor,
});
Ok(Request { png, bmp, width, height, host_args })
}
// ── BMP in, PNG out ─────────────────────────────────────────────────────────
/// Decode a 24- or 32-bit uncompressed BMP into (width, height, RGB rows top-down).
pub fn decode_bmp(b: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
if b.len() < 54 || &b[0..2] != b"BM" {
return Err("not a BMP file".into());
}
let le32 = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
let off = le32(10) as usize;
let w = le32(18) as i32;
let h = le32(22) as i32;
let bpp = u16::from_le_bytes([b[28], b[29]]);
let comp = le32(30);
if w <= 0 || h == 0 || !(bpp == 24 || bpp == 32) || !(comp == 0 || comp == 3) {
return Err(format!("unsupported BMP: {w}x{h}, {bpp} bpp, compression {comp}"));
}
let (w, top_down, h) = (w as u32, h < 0, h.unsigned_abs());
let bytes_pp = (bpp / 8) as usize;
let stride = ((w as usize * bytes_pp) + 3) & !3;
if b.len() < off + stride * h as usize {
return Err("the BMP is shorter than its header says".into());
}
let mut rgb = Vec::with_capacity(w as usize * h as usize * 3);
for y in 0..h as usize {
let src_row = if top_down { y } else { h as usize - 1 - y };
let row = &b[off + src_row * stride..off + src_row * stride + w as usize * bytes_pp];
for px in row.chunks(bytes_pp) {
rgb.extend_from_slice(&[px[2], px[1], px[0]]);
}
}
Ok((w, h, rgb))
}
fn crc32(data: &[u8]) -> u32 {
let mut table = [0u32; 256];
for (i, t) in table.iter_mut().enumerate() {
let mut c = i as u32;
for _ in 0..8 {
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
}
*t = c;
}
let mut c = 0xFFFF_FFFFu32;
for &x in data {
c = table[((c ^ x as u32) & 0xFF) as usize] ^ (c >> 8);
}
c ^ 0xFFFF_FFFF
}
/// Encode RGB8 as a PNG.
pub fn encode_png(w: u32, h: u32, rgb: &[u8]) -> Vec<u8> {
let mut raw = Vec::with_capacity((w as usize * 3 + 1) * h as usize);
for row in rgb.chunks(w as usize * 3) {
raw.push(0); // filter: none
raw.extend_from_slice(row);
}
let z = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6);
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
let mut chunk = |kind: &[u8], data: &[u8]| {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
let mut c = kind.to_vec();
c.extend_from_slice(data);
out.extend_from_slice(&c);
out.extend_from_slice(&crc32(&c).to_be_bytes());
};
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&w.to_be_bytes());
ihdr.extend_from_slice(&h.to_be_bytes());
ihdr.extend_from_slice(&[8, 2, 0, 0, 0]); // 8-bit, truecolour, deflate, no filter, no interlace
chunk(b"IHDR", &ihdr);
chunk(b"IDAT", &z);
chunk(b"IEND", &[]);
out
}
/// READ BACK: (width, height) from a PNG's IHDR, or why it is not a PNG.
pub fn png_dims(b: &[u8]) -> Result<(u32, u32), String> {
if b.len() < 24 || &b[0..8] != b"\x89PNG\r\n\x1a\n" {
return Err("the file does not start with the PNG signature".into());
}
if &b[12..16] != b"IHDR" {
return Err("the PNG's first chunk is not IHDR".into());
}
let be32 = |o: usize| u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
Ok((be32(16), be32(20)))
}
// ── the verb ────────────────────────────────────────────────────────────────
pub fn render_image(ctx: &crate::verbs::Ctx, args: &Value) -> Value {
let started = std::time::Instant::now();
if args.get("reason").and_then(|r| r.as_str()).map(|r| r.trim().is_empty()).unwrap_or(true) {
return crate::verbs::err_code("reason_required", "swx_render_image needs a reason", "Say what the image is for; it is ledgered.");
}
let state = ctx.call("app_state", json!({}), 30).ok();
let title = state
.as_ref()
.and_then(|s| s.get("activeDocument").and_then(|d| d.as_str()))
.unwrap_or("document")
.to_string();
let req = match request(args, &title) {
Ok(r) => r,
Err(e) => return e,
};
let job = ctx.jobs.start("render_image", format!("Rendering {}x{}", req.width, req.height), 60);
job.update("capture, apply, export (SaveBMP), restore", json!({"stage": "render", "width": req.width, "height": req.height}));
let host = ctx.call("render_image", req.host_args.clone(), crate::verbs3::SWEEP_MAX_SECS);
let _cleanup = Bmp(req.bmp.clone());
let r = match host {
Ok(r) => r,
Err(e) => {
// The host restores before it errors; what it restored rides on the error.
let mut out = crate::verbs::err_code(
e.code.as_deref().unwrap_or("render_failed"),
format!("the render failed: {}", e.message),
"Every display setting the render changed was restored (see restoredSettings); nothing was saved.",
);
if let (Value::Object(m), Some(d)) = (&mut out, e.data.as_ref()) {
for k in ["appliedSettings", "restoredSettings", "restoreFailures"] {
if let Some(v) = d.get(k) {
m.insert(k.into(), v.clone());
}
}
}
crate::ledger::record("render_image", json!({"ok": false, "error": e.message, "reason": args.get("reason")}));
return out;
}
};
let restored = r.get("restoredSettings").cloned().unwrap_or(Value::Null);
let applied = r.get("appliedSettings").cloned().unwrap_or(Value::Null);
let fail = |code: &str, msg: String| {
let mut out = crate::verbs::err_code(code, msg, "Settings were restored; nothing was saved.");
if let Value::Object(m) = &mut out {
m.insert("appliedSettings".into(), applied.clone());
m.insert("restoredSettings".into(), restored.clone());
}
out
};
job.update("converting to PNG", json!({"stage": "convert"}));
let bmp = match std::fs::read(&req.bmp) {
Ok(b) => b,
Err(e) => return fail("render_no_file", format!("SOLIDWORKS reported the export but no image is on disk at {} ({e})", req.bmp.display())),
};
let (w, h, rgb) = match decode_bmp(&bmp) {
Ok(x) => x,
Err(e) => return fail("render_bad_file", format!("the exported image could not be read: {e}")),
};
let png = encode_png(w, h, &rgb);
if let Err(e) = std::fs::write(&req.png, &png) {
return fail("render_write_failed", format!("the PNG could not be written to {}: {e}", req.png.display()));
}
// VERIFY BY EFFECT: read the file back.
let back = std::fs::read(&req.png).unwrap_or_default();
let (pw, ph) = match png_dims(&back) {
Ok(d) => d,
Err(e) => return fail("render_bad_file", format!("the written file is not a valid PNG: {e}")),
};
let capped = (pw as u64, ph as u64) != (req.width, req.height);
let restore_failures = r.get("restoreFailures").cloned().unwrap_or(json!([]));
let restore_incomplete = restore_failures.as_array().map(|a| !a.is_empty()).unwrap_or(false);
// NOTES (0.9.20): a requested setting SOLIDWORKS ignored is said plainly.
let settings = r.get("settings").cloned().unwrap_or(json!({}));
let mut notes: Vec<String> = Vec::new();
if let Some(m) = settings.as_object() {
for (name, row) in m {
if row.get("why").and_then(|w| w.as_str()) == Some("setIgnored") {
notes.push(if name == "realView" {
"RealView was requested but SOLIDWORKS left it off — RealView needs a certified graphics \
card (this laptop has an RTX 4090 Laptop GPU, not on the certified list) and may also be \
unavailable while the window is minimized; the image was rendered shaded with shadows and \
ambient occlusion."
.to_string()
} else {
format!(
"{name} was requested ({}) but SOLIDWORKS left it at {} — the image was rendered without it.",
row.get("requested").cloned().unwrap_or(Value::Null),
row.get("after").cloned().unwrap_or(Value::Null)
)
});
}
}
}
crate::ledger::record(
"render_image",
json!({"ok": true, "path": req.png.to_string_lossy(), "width": pw, "height": ph, "capped": capped,
"document": r.get("document"), "reason": args.get("reason")}),
);
crate::verbs::ok(json!({
"path": std::fs::canonicalize(&req.png).unwrap_or(req.png.clone()).to_string_lossy(),
"bytes": back.len(),
"width": pw,
"height": ph,
"requested": {"width": req.width, "height": req.height},
"capped": capped,
"document": r.get("document"),
"appliedSettings": applied,
"restoredSettings": restored,
"restoreFailures": restore_failures,
// EVERY REQUESTED SETTING (0.9.19): {requested, before, after, changed,
// restored, why} — "already as asked" is never confused with "not applied".
"settings": settings,
"notes": notes,
// The swconst ids the host read from the installed interop DLL, for the record.
"enumIds": r.get("enumIds").cloned().unwrap_or(Value::Null),
// LOUD: a setting the render changed did not read back as restored.
"restoreIncomplete": restore_incomplete,
"renderer": "SOLIDWORKS viewport (IModelDoc2::SaveBMP), converted to PNG by the bridge",
"durationMs": started.elapsed().as_millis() as u64,
"progress": job.finished(),
"measured": "2026-10-05 (window minimized; visible-window flicker, 8192 px and background changes still unmeasured)",
"_hint": if restore_incomplete {
"THE IMAGE WAS WRITTEN, BUT NOT EVERY DISPLAY SETTING READ BACK AS RESTORED — see restoreFailures and restoredSettings (value = what it was before). Tell the user which of their view settings may have changed; the document was not saved."
} else if capped {
"The image came back SMALLER than asked (capped at the window's size) — width/height are what is actually in the file. Settings were restored; the document was not saved."
} else {
"The image is at the requested size; width/height were read back from the PNG header. Every display setting the render changed was restored (restoredSettings; `settings` accounts for every one you asked for); the document was not saved. For hero-grade output ask for width:3840, height:2160 — measured 2026-10-05 it took only ~1.2 s longer than 1080p. zoomFactor >1 zooms in; after a fit, 1.4–1.6 usually fills the frame without clipping — 2.0 clipped a tall assembly. Read `notes`: a setting SOLIDWORKS ignored (RealView on an uncertified GPU) is listed there."
},
}))
}
/// The temporary BMP is removed on every path.
struct Bmp(std::path::PathBuf);
impl Drop for Bmp {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bmp(w: u32, h: u32, top_down: bool) -> Vec<u8> {
let stride = ((w * 3 + 3) & !3) as usize;
let mut b = vec![0u8; 54 + stride * h as usize];
b[0..2].copy_from_slice(b"BM");
b[10..14].copy_from_slice(&54u32.to_le_bytes());
b[14..18].copy_from_slice(&40u32.to_le_bytes());
b[18..22].copy_from_slice(&(w as i32).to_le_bytes());
b[22..26].copy_from_slice(&(if top_down { -(h as i32) } else { h as i32 }).to_le_bytes());
b[26..28].copy_from_slice(&1u16.to_le_bytes());
b[28..30].copy_from_slice(&24u16.to_le_bytes());
// bottom-up: the LAST row in the file is the top row. Mark the top-left pixel red.
let top_row = if top_down { 0 } else { h as usize - 1 };
let o = 54 + top_row * stride;
b[o..o + 3].copy_from_slice(&[0, 0, 255]); // BGR red
b
}
#[test]
fn a_bmp_becomes_a_png_whose_header_says_its_size() {
for td in [false, true] {
let (w, h, rgb) = decode_bmp(&bmp(37, 21, td)).unwrap();
assert_eq!((w, h), (37, 21));
assert_eq!(&rgb[0..3], &[255, 0, 0], "the top-left pixel is red (orientation kept)");
let png = encode_png(w, h, &rgb);
assert_eq!(png_dims(&png).unwrap(), (37, 21));
// A real decoder agrees: inflate the IDAT back to the rows.
let idat_len = u32::from_be_bytes([png[33], png[34], png[35], png[36]]) as usize;
let raw = miniz_oxide::inflate::decompress_to_vec_zlib(&png[41..41 + idat_len]).unwrap();
assert_eq!(raw.len(), (37 * 3 + 1) * 21);
}
assert!(png_dims(b"not a png at all, really not").is_err());
assert!(decode_bmp(b"BMxx").is_err());
}
#[test]
fn only_a_current_view_export_leaves_the_screen_alone() {
assert!(!changes_display(&json!({"width": 3840, "height": 2160, "reason": "x"})));
assert!(!changes_display(&json!({"view": "current", "background": "current"})));
for a in [json!({"view": "isometric"}), json!({"realView": true}), json!({"shadows": false}), json!({"zoomToFit": true}), json!({"background": "white"}), json!({"displayStyle": "shaded"})] {
assert!(changes_display(&a), "{a}");
}
}
}