Merge pull request #1309 from ghost/embed-print-fonts

This commit is contained in:
Hakan Seven committed 2026-09-17 06:57:42 +03:00
commit 03f4e7cb86
29 files changed
+1833 -68

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
// Embed a raster image inside the drawing itself as an OLE2FRAME entity.
//
// The IMAGE mechanism (RasterImage + ImageDefinition) stores only a file path,
// so a drawing copied to another machine loses its pictures. An OLE2FRAME
// instead carries the encoded raster inside an OLE compound file embedded in
// the entity data, so
// the .dwg is self-contained. cadcodec both builds the compound storage and
// reads it back, and the renderer already paints OLE2FRAMEs via
// ImageModel::from_ole2frame, so this module only packs and unpacks.
use std::io::Write as _;
use std::path::Path;
use acadrust::compound_file::{
BinaryRecord, CompoundEntry, CompoundFile, CompoundStorage, CompoundStream,
CompoundStreamContent, StructuredStoragePayload,
};
use acadrust::entities::{Ole2Frame, OleFrameEnvelope, OleObjectType};
use acadrust::types::Vector3;
/// OLE compound-file stream holding OLE 1.0 native data. cadcodec's
/// presentation extractor looks here first; the body is the encoded raster
/// behind a 4-byte little-endian length.
const OLE10NATIVE: &str = "\u{1}Ole10Native";
/// Leading header marker carried verbatim by the geometry envelope; readers
/// use it to recognise the envelope around the compound file.
const GEOMETRY_MARKER: u16 = 1;
/// Encoded formats the extractor recognises by magic and every DWG consumer
/// can decode. Anything else is re-encoded as PNG before embedding.
const PORTABLE_FORMATS: [image::ImageFormat; 3] = [
image::ImageFormat::Png,
image::ImageFormat::Jpeg,
image::ImageFormat::Bmp,
];
/// A raster image prepared for embedding: encoded bytes plus the metadata the
/// placement prompts need.
#[derive(Clone, Debug, PartialEq)]
pub struct EmbeddedImage {
/// Encoded raster bytes (PNG / JPEG / BMP as stored or re-encoded).
pub bytes: Vec<u8>,
pub pixel_width: u32,
pub pixel_height: u32,
/// File name the picture came from — surfaces in prompts and properties.
pub name: String,
}
impl EmbeddedImage {
/// Prepare an image file for embedding. Recognised raster formats are
/// stored as-is; anything else is decoded and re-encoded as PNG so the
/// payload always stays renderable.
pub fn from_file(path: &Path) -> Result<Self, String> {
let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("image")
.to_string();
Self::from_bytes(name, bytes)
}
/// Prepare already-read image bytes for embedding (web file handles).
pub fn from_bytes(name: String, bytes: Vec<u8>) -> Result<Self, String> {
let format = image::guess_format(&bytes).map_err(|e| e.to_string())?;
let encoded = if PORTABLE_FORMATS.contains(&format) {
bytes
} else {
encode_png(&bytes)?
};
let img = image::load_from_memory(&encoded).map_err(|e| e.to_string())?;
let (pixel_width, pixel_height) = image::GenericImageView::dimensions(&img);
Ok(Self {
bytes: encoded,
pixel_width,
pixel_height,
name,
})
}
/// Width : height aspect ratio; a degenerate height falls back to square.
pub fn aspect(&self) -> f64 {
if self.pixel_height == 0 {
1.0
} else {
self.pixel_width as f64 / self.pixel_height as f64
}
}
}
fn encode_png(bytes: &[u8]) -> Result<Vec<u8>, String> {
let img = image::load_from_memory(bytes).map_err(|e| e.to_string())?;
let mut png = std::io::Cursor::new(Vec::new());
img.write_to(&mut png, image::ImageFormat::Png)
.map_err(|e| e.to_string())?;
Ok(png.into_inner())
}
/// World-space corners for a picture placed with its bottom-left corner at
/// `origin`, extending `width` right and `width / aspect` up — the same
/// rectangle the IMAGE command previews.
pub fn corners_from_placement(origin: Vector3, width: f64, aspect: f64) -> (Vector3, Vector3) {
let width = width.max(1e-6);
let height = width / aspect.max(1e-6);
(
Vector3::new(origin.x, origin.y + height, origin.z),
Vector3::new(origin.x + width, origin.y, origin.z),
)
}
/// Pack `image` into an OLE2FRAME spanning `upper_left` → `lower_right` in
/// world space. The picture rides in an `\x01Ole10Native` stream as OLE 1.0
/// native data behind the geometry envelope cadcodec writes around it.
pub fn build_embedded_ole(
image: &EmbeddedImage,
upper_left: Vector3,
lower_right: Vector3,
) -> Ole2Frame {
// OLE 1.0 native: 4-byte little-endian payload length, then the raster.
let mut native = Vec::with_capacity(4 + image.bytes.len());
let _ = native.write(&(image.bytes.len() as u32).to_le_bytes());
native.extend_from_slice(&image.bytes);
let file = CompoundFile {
minor_version: 0x003E,
major_version: 3,
transaction_signature: 0,
root: CompoundStorage {
name: "Root Entry".to_string(),
class_id: [0; 16],
state_bits: 0,
creation_time: 0,
modified_time: 0,
entries: vec![CompoundEntry::Stream(CompoundStream {
name: OLE10NATIVE.to_string(),
class_id: [0; 16],
state_bits: 0,
creation_time: 0,
modified_time: 0,
content: CompoundStreamContent::Binary(BinaryRecord::split(&native, 4096)),
})],
},
};
let mut ole = Ole2Frame::new();
ole.ole_object_type = OleObjectType::Embedded;
ole.source_application = "OpenCADStudio".to_string();
ole.upper_left_corner = upper_left;
ole.lower_right_corner = lower_right;
ole.envelope = OleFrameEnvelope::Geometry {
marker: GEOMETRY_MARKER,
extension_records: Vec::new(),
};
ole.storage = StructuredStoragePayload {
leading_records: Vec::new(),
compound_file: Some(file),
trailing_records: Vec::new(),
};
ole
}
/// Non-interactive embed used by the control API: add the entity to `doc` and
/// return its handle. The undo snapshot is the caller's responsibility.
pub fn add_embedded_image(
doc: &mut acadrust::CadDocument,
image: &EmbeddedImage,
origin: Vector3,
width: f64,
) -> Result<acadrust::Handle, String> {
let (upper_left, lower_right) = corners_from_placement(origin, width, image.aspect());
let ole = build_embedded_ole(image, upper_left, lower_right);
doc.add_entity(acadrust::EntityType::Ole2Frame(ole))
.map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
/// Minimal valid PNG: a solid-colour 4×3 image through the `image` crate.
fn png_bytes(width: u32, height: u32) -> Vec<u8> {
let img = image::RgbaImage::from_pixel(width, height, image::Rgba([10, 20, 30, 255]));
let mut png = std::io::Cursor::new(Vec::new());
img.write_to(&mut png, image::ImageFormat::Png).unwrap();
png.into_inner()
}
#[test]
fn from_bytes_keeps_png_and_measures_pixels() {
let embedded = EmbeddedImage::from_bytes("t.png".into(), png_bytes(4, 3)).unwrap();
assert_eq!(embedded.pixel_width, 4);
assert_eq!(embedded.pixel_height, 3);
assert_eq!(embedded.bytes, png_bytes(4, 3));
}
#[test]
fn from_bytes_reencodes_foreign_formats_as_png() {
// TIFF starts with II*/MM* and is not in the portable set, so it must
// come back re-encoded as PNG.
let img = image::RgbaImage::from_pixel(2, 2, image::Rgba([1, 2, 3, 255]));
let mut tiff = std::io::Cursor::new(Vec::new());
img.write_to(&mut tiff, image::ImageFormat::Tiff).unwrap();
let embedded = EmbeddedImage::from_bytes("t.tif".into(), tiff.into_inner()).unwrap();
assert!(embedded.bytes.starts_with(&[0x89, b'P', b'N', b'G']));
}
#[test]
fn packed_payload_roundtrips_through_the_extractor() {
let png = png_bytes(4, 3);
let image = EmbeddedImage {
bytes: png.clone(),
pixel_width: 4,
pixel_height: 3,
name: "t.png".into(),
};
let ole = build_embedded_ole(
&image,
Vector3::new(0.0, 3.0, 0.0),
Vector3::new(4.0, 0.0, 0.0),
);
let payload = ole.encoded_payload();
match acadrust::entities::extract_presentation(&payload) {
Some(acadrust::entities::OlePresentation::Raster(bytes)) => assert_eq!(bytes, png),
other => panic!("expected the embedded raster back, got {other:?}"),
}
}
#[test]
fn dwg_roundtrip_preserves_the_embedded_picture() {
let png = png_bytes(4, 3);
let image = EmbeddedImage {
bytes: png.clone(),
pixel_width: 4,
pixel_height: 3,
name: "t.png".into(),
};
let mut doc = acadrust::CadDocument::new();
let handle =
add_embedded_image(&mut doc, &image, Vector3::new(10.0, 10.0, 0.0), 20.0).unwrap();
let dir = std::env::temp_dir().join("ocs_ole_embed_tests");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("embedded_roundtrip.dwg");
acadrust::DwgWriter::write_to_file(&path, &doc).unwrap();
let mut reader = acadrust::io::dwg::DwgReader::from_file(&path).unwrap();
let reopened = reader.read().unwrap();
std::fs::remove_file(&path).ok();
let ole = match reopened.get_entity(handle) {
Some(acadrust::EntityType::Ole2Frame(ole)) => ole,
other => panic!("expected the Ole2Frame back, got {other:?}"),
};
// Corners survive: 20 wide at aspect 4:3 → 15 tall, base at (10,10).
assert_eq!(ole.lower_right_corner, Vector3::new(30.0, 10.0, 0.0));
assert_eq!(ole.upper_left_corner, Vector3::new(10.0, 25.0, 0.0));
match acadrust::entities::extract_presentation(&ole.encoded_payload()) {
Some(acadrust::entities::OlePresentation::Raster(bytes)) => assert_eq!(bytes, png),
other => panic!("expected the embedded raster back, got {other:?}"),
}
}
}