DWF and DGN underlays: attach, display, snaps, frames and references

DWFATTACH / -DWFATTACH and DGNATTACH / -DGNATTACH attach a DWF / DWFx sheet
or a DGN (V7 or V8) model with the reference's prompts; ATTACH routes the
file types to them. The sheet or model is read into vector paths, drawn as a
transparent underlay raster (contrast, fade, monochrome and background
adjustment apply) and offers its geometry to object snaps. Definitions are
kept in ACAD_DWFDEFINITIONS / ACAD_DGNDEFINITIONS, the External References
palette lists and reloads them, and image textures get line-keeping mip
levels so thin underlay lines stay visible when zoomed out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-28 12:53:25 +03:00
1 parent 7f8f8457c7
commit cb7860e50e
46 files changed
+2723 -129

No files matched your search

+30 -17
View File
@@ -191,18 +191,23 @@ impl ImageModel {
if !u.flags.contains(UnderlayDisplayFlags::ON) {
return None;
}
if !matches!(def.underlay_type, UnderlayType::Pdf) || def.unloaded {
if def.unloaded {
return None;
}
let page = if def.page_name.trim().is_empty() {
"1"
} else {
def.page_name.trim()
let page = crate::entities::underlay::page_of(def);
let rect = crate::entities::underlay::definition_rect(def)?;
let (source, raster) = match def.underlay_type {
UnderlayType::Pdf => {
// Hidden PDF layers come from the underlay's layer overrides.
let source = super::pdf_layers::underlay_source(u, &def.file_path);
let raster = pdf_raster::rasterize_page_display(&source, page)?;
(source, raster)
}
kind => (
def.file_path.clone(),
super::underlay_vector::display_raster(kind, &def.file_path, page)?,
),
};
let (page_w, page_h) = pdf_raster::page_size_inches(&def.file_path, page)?;
// Hidden PDF layers come from the underlay's layer overrides.
let source = super::pdf_layers::underlay_source(u, &def.file_path);
let raster = pdf_raster::rasterize_page_display(&source, page)?;
// Dark means an HSL lightness under one half: pure blue counts as
// light, (0, 128, 0) as dark.
let bg_max = background[0].max(background[1]).max(background[2]);
@@ -216,11 +221,17 @@ impl ImageModel {
contrast: u.contrast.min(100),
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
dark_background: bg_lum < 0.5,
// A DGN model is drawn for a black background, so its
// colours turn over on a light one instead (white text stays
// light on a dark background, as the reference shows it).
dark_background: (bg_lum < 0.5) != (def.underlay_type == UnderlayType::Dgn),
},
);
// Page size in drawing units (1 unit per PDF inch), entity scale applied.
// The page rectangle in drawing units (1 unit per PDF inch, or per
// DWF/DGN sheet unit), entity scale applied, from its lower-left
// corner.
let (page_w, page_h) = (rect[2] - rect[0], rect[3] - rect[1]);
let w_du = page_w * u.x_scale;
let h_du = page_h * u.y_scale;
if w_du.abs() <= 0.0 || h_du.abs() <= 0.0 {
@@ -230,9 +241,7 @@ impl ImageModel {
let (uxv, uyv) = (c * w_du, s * w_du);
let (vxv, vyv) = (-s * h_du, c * h_du);
let oxv = u.insertion_point.x;
let oyv = u.insertion_point.y;
let ozv = u.insertion_point.z;
let [oxv, oyv, ozv] = crate::entities::underlay::local_to_world(u, [rect[0], rect[1]]);
let ox = oxv as f32;
let oy = oyv as f32;
let oz = ozv as f32;
@@ -249,7 +258,7 @@ impl ImageModel {
];
let corners_low = [[oxl, oyl, ozl]; 4];
let tris_uv = underlay_visible_uv(u, page_w, page_h);
let tris_uv = underlay_visible_uv(u, rect);
let verts: Vec<ImageQuadVertex> = tris_uv
.iter()
.map(|&[fu, fv]| {
@@ -281,7 +290,8 @@ impl ImageModel {
/// Visible region of an underlay page as triangles in page UV (0..1, y up):
/// the whole page, the clip polygon, or — for an inverted clip — the page
/// with the polygon cut out.
fn underlay_visible_uv(u: &codec::entities::Underlay, page_w: f64, page_h: f64) -> Vec<[f64; 2]> {
fn underlay_visible_uv(u: &codec::entities::Underlay, rect: [f64; 4]) -> Vec<[f64; 2]> {
let (page_w, page_h) = (rect[2] - rect[0], rect[3] - rect[1]);
use codec::entities::UnderlayDisplayFlags;
let page = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let whole = || vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
@@ -289,7 +299,10 @@ fn underlay_visible_uv(u: &codec::entities::Underlay, page_w: f64, page_h: f64)
if !u.flags.contains(UnderlayDisplayFlags::CLIPPING) || clip.len() < 3 {
return whole();
}
let ring: Vec<[f64; 2]> = clip.iter().map(|p| [p[0] / page_w, p[1] / page_h]).collect();
let ring: Vec<[f64; 2]> = clip
.iter()
.map(|p| [(p[0] - rect[0]) / page_w, (p[1] - rect[1]) / page_h])
.collect();
let (points, triangles) = if u.clip_inverted {
kernel::geom2d::triangulate(&page, &[ring])
} else {
+1
View File
@@ -9,6 +9,7 @@ pub mod ole_pres;
pub mod pdf_layers;
pub mod pdf_raster;
pub mod pdf_vector;
pub mod underlay_vector;
pub mod mesh_model;
pub mod solid_model;
pub mod solid_history;
+17 -4
View File
@@ -629,8 +629,7 @@ pub fn underlay_snap_points(
let Some(def) = crate::entities::underlay::definition(u, document) else {
return Vec::new();
};
let source = super::pdf_layers::underlay_source(u, &def.file_path);
let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else {
let Some(vectors) = underlay_vectors(u, def) else {
return Vec::new();
};
let world = |p: [f64; 2]| {
@@ -665,6 +664,21 @@ pub fn underlay_snap_points(
out
}
/// The page's vectors in page units: PDF through its layer overrides, DWF
/// and DGN from their sheet.
fn underlay_vectors(
u: &codec::entities::Underlay,
def: &codec::entities::UnderlayDefinition,
) -> Option<Arc<PageVectors>> {
let page = crate::entities::underlay::page_of(def);
match def.underlay_type {
codec::entities::UnderlayType::Pdf => {
page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page)
}
kind => super::underlay_vector::page_vectors(kind, &def.file_path, page),
}
}
/// Most points of the snap-only geometry wire of one underlay.
// ponytail: flat cap; a spatial index over page segments for huge drawings.
const MAX_GEOMETRY_POINTS: usize = 200_000;
@@ -693,8 +707,7 @@ pub fn underlay_snap_geometry(
if def.unloaded {
return Vec::new();
}
let source = super::pdf_layers::underlay_source(u, &def.file_path);
let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else {
let Some(vectors) = underlay_vectors(u, def) else {
return Vec::new();
};
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
+421
View File
@@ -0,0 +1,421 @@
//! DGN V7 (ISFF) design files: one model ("Default") of variable-length
//! element records. Coordinates are UORs; the design file header (TCB)
//! gives the global origin and the master unit the model is read in. Shared
//! cells are drawn where their instances place them.
use std::collections::HashMap;
use super::model::{arc_cubics, bspline_points, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
/// A 32-bit integer stored as two little-endian words, high word first.
fn int32(b: &[u8], at: usize) -> Option<i32> {
let p = b.get(at..at + 4)?;
Some(i32::from_le_bytes([p[2], p[3], p[0], p[1]]))
}
fn le_i32(b: &[u8], at: usize) -> Option<i32> {
Some(i32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?))
}
fn uint16(b: &[u8], at: usize) -> Option<u16> {
Some(u16::from_le_bytes(b.get(at..at + 2)?.try_into().ok()?))
}
/// A VAX D-floating value as an IEEE double.
fn vax_double(b: &[u8], at: usize) -> Option<f64> {
let s = b.get(at..at + 8)?;
let hi = u32::from_le_bytes([s[2], s[3], s[0], s[1]]);
let lo = u32::from_le_bytes([s[6], s[7], s[4], s[5]]);
let exponent = ((hi >> 23) & 0xff) as i64;
if exponent == 0 {
return Some(0.0);
}
let exponent = (exponent - 129 + 1023) as u32;
let rnd = lo & 7;
let mut lo2 = (lo >> 3) | (hi << 29);
if rnd != 0 {
lo2 |= 1;
}
let hi2 = ((hi >> 3) & 0x000f_ffff) | (exponent << 20) | (hi & 0x8000_0000);
Some(f64::from_bits(((hi2 as u64) << 32) | lo2 as u64))
}
/// The file is a V7 design file: it opens with a design file header.
pub fn is_v7(bytes: &[u8]) -> bool {
bytes.len() > 1256 && (bytes[1] & 0x7f) == 9 && (bytes[0] & 0x3f) == 8
}
struct Units {
uor_per_sub: f64,
sub_per_master: f64,
origin: [f64; 2],
}
fn units(b: &[u8]) -> Option<Units> {
let sub_per_master = int32(b, 1112)? as f64;
let uor_per_sub = int32(b, 1116)? as f64;
let origin = [vax_double(b, 1240)?, vax_double(b, 1248)?];
(sub_per_master > 0.0 && uor_per_sub > 0.0).then(|| Units {
uor_per_sub,
sub_per_master,
origin,
})
}
/// The element records, in file order.
fn elements(b: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut at = 0usize;
while at + 4 <= b.len() && !(b[at] == 0xff && b[at + 1] == 0xff) {
let Some(words) = uint16(b, at + 2) else { break };
let len = 4 + words as usize * 2;
let Some(e) = b.get(at..at + len) else { break };
out.push(e);
at += len;
}
out
}
/// The file's colour table (element type 5 on level 1): the colour of
/// index i is at 41 + 3i.
fn color_table(elements: &[&[u8]]) -> Option<Vec<[u8; 3]>> {
elements.iter().find_map(|e| {
(e[1] & 0x7f == 5 && e[0] & 0x3f == 1 && e.len() >= 41 + 765).then(|| {
(0..256)
.map(|i| {
let at = 41 + i * 3;
e.get(at..at + 3).map(|c| [c[0], c[1], c[2]]).unwrap_or([255, 255, 255])
})
.collect()
})
})
}
/// A usable default: the first eight DGN colours, then greys.
// ponytail: The full DGN default table is not reproduced; the files
// the reference writes carry their own colour table.
fn default_colors() -> Vec<[u8; 3]> {
let base: [[u8; 3]; 8] = [
[255, 255, 255],
[0, 0, 255],
[0, 255, 0],
[255, 0, 0],
[255, 255, 0],
[255, 0, 255],
[255, 127, 0],
[0, 255, 255],
];
(0..256).map(|i| if i < 8 { base[i] } else { [i as u8; 3] }).collect()
}
/// Model names of a V7 file (it has one).
pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
is_v7(bytes).then(|| vec!["Default".to_string()])
}
/// Fill colour index of an element's fill linkage (id 0x41), when it has one.
fn fill_index(e: &[u8]) -> Option<u8> {
// The linkages follow the attribute index's word; the first one starts
// at the first word flagged as user data.
let base = 32 + uint16(e, 30)? as usize * 2;
let mut at = (base..base + 16).step_by(2).find(|&at| {
uint16(e, at).is_some_and(|h| h & 0x1000 != 0 && (1..=64).contains(&(h & 0xff)))
})?;
while at + 4 <= e.len() {
let header = uint16(e, at)?;
let words = (header & 0x00ff) as usize;
if header & 0x1000 == 0 || words == 0 {
break;
}
if uint16(e, at + 2)? == 0x41 {
return e.get(at + 8).copied();
}
at += (words + 1) * 2;
}
None
}
/// Geometry in raw UORs, mapped to sheet units when placed.
#[derive(Default, Clone)]
struct Raw {
paths: Vec<Path>,
texts: Vec<Text>,
instances: Vec<Instance>,
}
#[derive(Clone)]
struct Instance {
name: String,
/// x' = m[0]·x + m[1]·y + origin.x, y' = m[2]·x + m[3]·y + origin.y.
m: [f64; 4],
origin: [f64; 2],
}
/// Reads a run of elements into raw geometry: plain elements, complex
/// chains and shapes (joined), B-splines, text and shared cell instances.
fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
let mut raw = Raw::default();
let mut spline: Option<(usize, usize, [u8; 3], f64)> = None;
let mut knots: Vec<f64> = Vec::new();
// A complex shape or chain collects its components into one path.
let mut complex: Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)> = None;
let push = |raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path| {
if let Some((left, pb, _, _, _)) = complex.as_mut() {
for sp in &path.subpaths {
for seg in &sp.segments {
match seg {
Segment::Line(a, b) => {
if pb.current() != Some(*a) {
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
}
pb.line_to(*b);
}
Segment::Cubic(a, c1, c2, b) => {
if pb.current() != Some(*a) {
if pb.current().is_none() { pb.move_to(*a) } else { pb.line_to(*a) }
}
pb.cubic_to(*c1, *c2, *b);
}
}
}
}
*left = left.saturating_sub(1);
if *left == 0 {
let (_, mut pb, stroke, fill, closed) = complex.take().unwrap();
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
}
} else {
raw.paths.push(path);
}
};
for e in run {
let kind = e[1] & 0x7f;
if e[1] & 0x80 != 0 || e.len() < 36 {
continue;
}
let symbology = uint16(e, 34).unwrap_or(0);
let color = colors[(symbology >> 8) as usize];
let weight = ((symbology & 0xf8) >> 3) as f64;
// Weights are screen pixels: a negative width asks for pixels.
let stroke = Some((color, -(weight * 2.0 + 1.0)));
let fill = fill_index(e).map(|i| colors[i as usize]);
let int_pt = |i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
match kind {
12 | 14 => {
let n = uint16(e, 38).unwrap_or(0) as usize;
if n > 0 {
complex = Some((n, PathBuilder::default(), stroke, if kind == 14 { fill } else { None }, kind == 14));
}
}
3 => {
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
}
}
4 | 6 | 11 => {
let n = uint16(e, 36).unwrap_or(0) as usize;
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
if pts.len() >= 2 {
push(&mut raw, &mut complex, polyline(&pts, kind == 6, stroke, if kind == 6 { fill } else { None }));
}
}
15 | 16 => {
let arc = if kind == 15 {
(|| Some((vax_double(e, 36)?, vax_double(e, 44)?, int32(e, 52)?, vax_double(e, 56)?, vax_double(e, 64)?, 0.0, 360.0)))()
} else {
(|| {
let sw = int32(e, 40)? as u32;
let mut sweep = if sw & 0x8000_0000 != 0 { -((sw & 0x7fff_ffff) as f64) } else { sw as f64 } / 360000.0;
if sweep == 0.0 {
sweep = 360.0;
}
Some((vax_double(e, 44)?, vax_double(e, 52)?, int32(e, 60)?, vax_double(e, 64)?, vax_double(e, 72)?, int32(e, 36)? as f64 / 360000.0, sweep))
})()
};
let Some((a, b, r, ox, oy, start, sweep)) = arc else { continue };
let pieces = arc_cubics([ox, oy], a, b, (r as f64 / 360000.0).to_radians(), start.to_radians(), sweep.to_radians());
let closed = sweep.abs() >= 360.0;
push(
&mut raw,
&mut complex,
Path {
subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }],
stroke,
fill: if closed { fill } else { None },
},
);
}
17 => {
let (Some(width), Some(height), Some(rotation), Some(origin)) = (int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50)) else {
continue;
};
let n = e.get(58).copied().unwrap_or(0) as usize;
let Some(chars) = e.get(60..60 + n) else { continue };
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
raw.texts.push(Text {
text: String::from_utf8_lossy(chars).trim_end_matches('\0').to_string(),
origin,
height,
width_factor: if height > 0.0 { width / height } else { 1.0 },
rotation: (rotation as f64 / 360000.0).to_radians(),
color,
});
}
27 => {
// B-spline curve header: order, pole count.
let order = ((e.get(40).copied().unwrap_or(0) & 0x0f) + 2) as usize;
let poles = uint16(e, 42).unwrap_or(0) as usize;
spline = Some((order, poles, color, -(weight * 2.0 + 1.0)));
knots.clear();
}
26 => {
// Interior knots as fractions of the parameter range.
let n = spline.map(|s| s.1.saturating_sub(s.0)).unwrap_or(0);
knots = (0..n).filter_map(|k| int32(e, 36 + k * 4)).map(|v| v as u32 as f64 / 2_147_483_648.0).collect();
}
21 => {
let n = uint16(e, 36).unwrap_or(0) as usize;
let poles: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
if let Some((order, count, color, width)) = spline.take() {
if poles.len() == count && count >= order {
let interior = if knots.len() == count - order {
knots.clone()
} else {
(1..=count - order).map(|k| k as f64 / (count - order + 1) as f64).collect()
};
let mut full = vec![0.0; order];
full.extend(interior);
full.extend(vec![1.0; order]);
let pts = bspline_points(order, &poles, &full, 24 * count);
push(&mut raw, &mut complex, polyline(&pts, false, Some((color, width)), None));
}
}
}
35 => {
// Shared cell instance: the definition's name, a 2×2 matrix
// and the origin.
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_string()).unwrap_or_default();
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) =
(vax_double(e, 76), vax_double(e, 84), vax_double(e, 92), vax_double(e, 100), le_i32(e, 148), le_i32(e, 152))
else {
continue;
};
raw.instances.push(Instance { name, m: [m0, m1, m2, m3], origin: [ox as f64, oy as f64] });
}
_ => {}
}
}
raw
}
fn polyline(pts: &[[f64; 2]], closed: bool, stroke: Option<([u8; 3], f64)>, fill: Option<[u8; 3]>) -> Path {
let mut pb = PathBuilder::default();
pb.move_to(pts[0]);
for p in &pts[1..] {
pb.line_to(*p);
}
if closed {
pb.close();
}
Path { subpaths: pb.finish(), stroke, fill }
}
/// x' = a·x + b·y + e, y' = c·x + d·y + f.
type Affine = [f64; 6];
fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] {
[m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5]]
}
fn compose(outer: &Affine, inner: &Affine) -> Affine {
[
outer[0] * inner[0] + outer[1] * inner[2],
outer[0] * inner[1] + outer[1] * inner[3],
outer[2] * inner[0] + outer[3] * inner[2],
outer[2] * inner[1] + outer[3] * inner[3],
outer[0] * inner[4] + outer[1] * inner[5] + outer[4],
outer[2] * inner[4] + outer[3] * inner[5] + outer[5],
]
}
/// Places raw geometry (and the shared cells it instances) through `m`.
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out: &mut Sheet) {
let map = |p: [f64; 2]| apply(m, p);
for path in &raw.paths {
out.paths.push(Path {
subpaths: path
.subpaths
.iter()
.map(|sp| SubPath {
segments: sp
.segments
.iter()
.map(|s| match s {
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
})
.collect(),
closed: sp.closed,
})
.collect(),
stroke: path.stroke,
fill: path.fill,
});
}
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
let turn = m[2].atan2(m[0]);
for t in &raw.texts {
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
}
if depth >= 8 {
return;
}
for inst in &raw.instances {
let Some(def) = defs.get(&inst.name.to_ascii_uppercase()) else { continue };
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
place(def, &compose(m, &local), defs, depth + 1, out);
}
}
/// The model's geometry, in master units.
pub fn model(bytes: &[u8]) -> Option<Sheet> {
if !is_v7(bytes) {
return None;
}
let u = units(bytes)?;
let all = elements(bytes);
let colors = color_table(&all).unwrap_or_else(default_colors);
// Shared cell definitions (type 34) and their components are drawn only
// where instances place them; everything else is the model itself.
let mut defs: HashMap<String, Raw> = HashMap::new();
let mut top: Vec<&[u8]> = Vec::new();
let mut i = 0;
while i < all.len() {
let e = all[i];
if e[1] & 0x7f == 34 && e[0] & 0x80 == 0 {
let name = e.get(164..180).map(|n| String::from_utf8_lossy(n).trim_end_matches('\0').trim().to_ascii_uppercase()).unwrap_or_default();
let mut j = i + 1;
while j < all.len() && all[j][0] & 0x80 != 0 {
j += 1;
}
defs.insert(name, read_run(&all[i + 1..j], &colors));
i = j;
continue;
}
top.push(e);
i += 1;
}
let raw = read_run(&top, &colors);
let per_unit = u.uor_per_sub * u.sub_per_master;
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, -u.origin[0] / per_unit, -u.origin[1] / per_unit];
let mut sheet = Sheet { sub_per_master: u.sub_per_master, ..Default::default() };
place(&raw, &m, &defs, 0, &mut sheet);
sheet.rect = paths_bounds(&sheet.paths).or_else(|| {
let t = sheet.texts.first()?;
Some([t.origin[0], t.origin[1], t.origin[0] + t.height, t.origin[1] + t.height])
})?;
Some(sheet)
}
+500
View File
@@ -0,0 +1,500 @@
//! DGN V8 design files: an OLE compound file whose models (Dgn-Md/#n)
//! keep their graphic elements in a compressed stream (Dgn^G/$1), and whose
//! non-model stream (Dgn^Nm/$1) holds the colour table, the levels and the
//! shared cell definitions. Element fields are little-endian; coordinates
//! are UORs.
use std::collections::HashMap;
use std::io::Read;
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text};
// ── Compound file ───────────────────────────────────────────────────────────
struct Cfb<'a> {
b: &'a [u8],
sector: usize,
fat: Vec<u32>,
mini_fat: Vec<u32>,
mini: Vec<u8>,
cutoff: u32,
entries: Vec<Entry>,
}
struct Entry {
name: String,
kind: u8,
left: u32,
right: u32,
child: u32,
start: u32,
size: u32,
}
const END: u32 = 0xFFFF_FFFA;
fn u32_at(b: &[u8], at: usize) -> Option<u32> {
Some(u32::from_le_bytes(b.get(at..at + 4)?.try_into().ok()?))
}
impl<'a> Cfb<'a> {
fn open(b: &'a [u8]) -> Option<Self> {
if !b.starts_with(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]) {
return None;
}
let sector = 1usize << u16::from_le_bytes([b[30], b[31]]);
let mini_sector_shift = u16::from_le_bytes([b[32], b[33]]);
if mini_sector_shift != 6 {
return None;
}
let mut difat: Vec<u32> = (0..109).filter_map(|i| u32_at(b, 76 + i * 4)).filter(|&v| v < END).collect();
let (mut next, mut count) = (u32_at(b, 68)?, u32_at(b, 72)?);
while count > 0 && next < END {
let at = (next as usize + 1) * sector;
for i in 0..sector / 4 - 1 {
if let Some(v) = u32_at(b, at + i * 4).filter(|&v| v < END) {
difat.push(v);
}
}
next = u32_at(b, at + sector - 4)?;
count -= 1;
}
let mut fat = Vec::new();
for s in difat {
let at = (s as usize + 1) * sector;
for i in 0..sector / 4 {
fat.push(u32_at(b, at + i * 4).unwrap_or(END));
}
}
let mut cfb = Cfb { b, sector, fat, mini_fat: Vec::new(), mini: Vec::new(), cutoff: u32_at(b, 56)?, entries: Vec::new() };
let dir = cfb.chain(u32_at(b, 48)?, None);
for e in dir.chunks_exact(128) {
let len = u16::from_le_bytes([e[64], e[65]]) as usize;
let name16: Vec<u16> = e[..len.saturating_sub(2).min(64)].chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
cfb.entries.push(Entry {
name: String::from_utf16_lossy(&name16),
kind: e[66],
left: u32_at(e, 68)?,
right: u32_at(e, 72)?,
child: u32_at(e, 76)?,
start: u32_at(e, 116)?,
size: u32_at(e, 120)?,
});
}
let root = cfb.entries.first()?;
let (root_start, root_size) = (root.start, root.size);
cfb.mini = cfb.chain(root_start, Some(root_size as usize));
let mini_fat = cfb.chain(u32_at(b, 60)?, None);
cfb.mini_fat = mini_fat.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
Some(cfb)
}
fn chain(&self, mut s: u32, size: Option<usize>) -> Vec<u8> {
let mut out = Vec::new();
let mut guard = 0;
while s < END && guard < 1_000_000 {
let at = (s as usize + 1) * self.sector;
let Some(block) = self.b.get(at..at + self.sector) else { break };
out.extend_from_slice(block);
s = self.fat.get(s as usize).copied().unwrap_or(END);
guard += 1;
}
if let Some(n) = size {
out.truncate(n);
}
out
}
fn stream(&self, e: &Entry) -> Vec<u8> {
if e.size >= self.cutoff {
return self.chain(e.start, Some(e.size as usize));
}
let mut out = Vec::new();
let mut s = e.start;
let mut guard = 0;
while s < END && guard < 1_000_000 {
let at = s as usize * 64;
let Some(block) = self.mini.get(at..at + 64) else { break };
out.extend_from_slice(block);
s = self.mini_fat.get(s as usize).copied().unwrap_or(END);
guard += 1;
}
out.truncate(e.size as usize);
out
}
/// Every stream with its full path ("Dgn-Md/#000000/Dgn^G/$1").
fn streams(&self) -> Vec<(String, &Entry)> {
let mut out = Vec::new();
fn walk<'b>(c: &'b Cfb, idx: u32, path: &str, out: &mut Vec<(String, &'b Entry)>, depth: usize) {
let Some(e) = c.entries.get(idx as usize) else { return };
if depth > 64 {
return;
}
walk(c, e.left, path, out, depth + 1);
let p = if path.is_empty() { e.name.clone() } else { format!("{path}/{}", e.name) };
if e.kind == 2 {
out.push((p.clone(), e));
} else if e.kind == 1 {
walk(c, e.child, &p, out, depth + 1);
}
walk(c, e.right, path, out, depth + 1);
}
if let Some(root) = self.entries.first() {
walk(self, root.child, "", &mut out, 0);
}
out
}
}
/// A DGN stream: raw, or zlib data after a 16-byte header.
fn inflate(data: &[u8]) -> Vec<u8> {
for skip in [16usize, 0] {
if let Some(z) = data.get(skip..) {
let mut out = Vec::new();
if flate2::read::ZlibDecoder::new(z).read_to_end(&mut out).is_ok() && !out.is_empty() {
return out;
}
}
}
data.to_vec()
}
// ── Elements ────────────────────────────────────────────────────────────────
/// Element records of an element stream (after its 4-byte count).
fn elements(d: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut at = 4;
while at + 8 <= d.len() {
let Some(words) = u32_at(d, at + 4) else { break };
let len = 4 + words as usize * 2;
if len < 8 {
break;
}
// The stream can end inside its last element (its trailing words are
// not stored); what is there is still read.
let e = &d[at..(at + len).min(d.len())];
out.push(e);
at += len;
}
out
}
fn kind(e: &[u8]) -> u8 {
e[0]
}
fn flags(e: &[u8]) -> u16 {
u16::from_le_bytes([e[2], e[3]])
}
/// A component of a complex element, cell or shared cell definition.
fn is_component(e: &[u8]) -> bool {
flags(e) & 0x4000 != 0
}
fn f64_at(e: &[u8], at: usize) -> Option<f64> {
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
}
/// A string stored after the "ff fe 01 00" marker, when the element has one.
fn marked_string(e: &[u8], from: usize) -> Option<String> {
let i = e.get(from..)?.windows(4).position(|w| w == [0xff, 0xfe, 0x01, 0x00])? + from + 4;
let end = e[i..].iter().position(|&c| c == 0 || c < 0x20).map(|n| i + n).unwrap_or(e.len());
Some(String::from_utf8_lossy(&e[i..end]).to_string())
}
struct Styles {
colors: Vec<[u8; 3]>,
/// Level id → colour index.
levels: HashMap<u32, u32>,
}
impl Styles {
fn color(&self, e: &[u8]) -> [u8; 3] {
let explicit = u32_at(e, 44).unwrap_or(u32::MAX);
let index = if explicit < 256 {
explicit
} else {
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
};
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
}
}
/// Geometry in raw UORs, placed through a transform.
#[derive(Default, Clone)]
struct Raw {
paths: Vec<Path>,
texts: Vec<Text>,
instances: Vec<Instance>,
}
#[derive(Clone)]
struct Instance {
name: String,
m: [f64; 4],
origin: [f64; 2],
}
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
let mut raw = Raw::default();
// A complex shape or chain collects its components into one path.
let mut complex: Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)> = None;
fn push(raw: &mut Raw, complex: &mut Option<(usize, PathBuilder, Option<([u8; 3], f64)>, Option<[u8; 3]>, bool)>, path: Path) {
if let Some((left, pb, _, _, _)) = complex.as_mut() {
for sp in &path.subpaths {
for seg in &sp.segments {
let (a, b) = match seg {
Segment::Line(a, b) | Segment::Cubic(a, _, _, b) => (*a, *b),
};
if pb.current() != Some(a) {
if pb.current().is_none() { pb.move_to(a) } else { pb.line_to(a) }
}
match seg {
Segment::Line(..) => pb.line_to(b),
Segment::Cubic(_, c1, c2, _) => pb.cubic_to(*c1, *c2, b),
}
}
}
*left = left.saturating_sub(1);
if *left == 0 {
let (_, mut pb, stroke, fill, closed) = complex.take().unwrap();
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
}
} else {
raw.paths.push(path);
}
}
for e in run {
if e.len() < 104 {
continue;
}
let color = styles.color(e);
let stroke = Some((color, -1.0));
let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) };
match kind(e) {
12 | 14 => {
let n = u32_at(e, 104).unwrap_or(0) as usize;
if n > 0 {
let fill = (kind(e) == 14 && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00])).then_some(color);
complex = Some((n, PathBuilder::default(), stroke, fill, kind(e) == 14));
}
}
3 => {
if let (Some(a), Some(b)) = (p2(104), p2(120)) {
push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None });
}
}
4 | 6 | 11 => {
let n = u32_at(e, 104).unwrap_or(0) as usize;
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * 16)).collect();
if pts.len() >= 2 {
let mut pb = PathBuilder::default();
pb.move_to(pts[0]);
for p in &pts[1..] {
pb.line_to(*p);
}
if kind(e) == 6 {
pb.close();
}
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
}
}
15 | 16 => {
let arc = if kind(e) == 15 {
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))()
} else {
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, f64_at(e, 136)?, p2(144)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
};
let Some((a, b, rot, c, start, sweep)) = arc else { continue };
let pieces = arc_cubics(c, a, b, rot, start, if sweep == 0.0 { std::f64::consts::TAU } else { sweep });
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9 || sweep == 0.0;
push(
&mut raw,
&mut complex,
Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None },
);
}
17 => {
let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else {
continue;
};
// The text: a byte count (with its 4-byte marker) and the
// characters after the marker.
let count = u16::from_le_bytes([e[110], e[111]]) as usize;
let Some(chars) = e.get(174..170 + count.max(4)) else { continue };
let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string();
let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0);
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color });
}
35 => {
let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else {
continue;
};
let Some(name) = marked_string(e, 248) else { continue };
raw.instances.push(Instance { name: name.to_ascii_uppercase(), m: [m0, m1, m3, m4], origin });
}
_ => {}
}
}
// A complex element cut short keeps the components it has.
if let Some((_, mut pb, stroke, fill, closed)) = complex.take() {
if closed {
pb.close();
}
raw.paths.push(Path { subpaths: pb.finish(), stroke, fill });
}
raw
}
type Affine = [f64; 6];
fn apply(m: &Affine, p: [f64; 2]) -> [f64; 2] {
[m[0] * p[0] + m[1] * p[1] + m[4], m[2] * p[0] + m[3] * p[1] + m[5]]
}
fn compose(o: &Affine, i: &Affine) -> Affine {
[
o[0] * i[0] + o[1] * i[2],
o[0] * i[1] + o[1] * i[3],
o[2] * i[0] + o[3] * i[2],
o[2] * i[1] + o[3] * i[3],
o[0] * i[4] + o[1] * i[5] + o[4],
o[2] * i[4] + o[3] * i[5] + o[5],
]
}
fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out: &mut Sheet) {
let map = |p: [f64; 2]| apply(m, p);
for path in &raw.paths {
out.paths.push(Path {
subpaths: path
.subpaths
.iter()
.map(|sp| SubPath {
segments: sp
.segments
.iter()
.map(|s| match s {
Segment::Line(a, b) => Segment::Line(map(*a), map(*b)),
Segment::Cubic(a, b, c, d) => Segment::Cubic(map(*a), map(*b), map(*c), map(*d)),
})
.collect(),
closed: sp.closed,
})
.collect(),
stroke: path.stroke,
fill: path.fill,
});
}
let scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt();
let turn = m[2].atan2(m[0]);
for t in &raw.texts {
out.texts.push(Text { origin: map(t.origin), height: t.height * scale, rotation: t.rotation + turn, ..t.clone() });
}
if depth >= 8 {
return;
}
for inst in &raw.instances {
let Some(def) = defs.get(&inst.name) else { continue };
let local: Affine = [inst.m[0], inst.m[1], inst.m[2], inst.m[3], inst.origin[0], inst.origin[1]];
place(def, &compose(m, &local), defs, depth + 1, out);
}
}
/// Units of a model header: UORs per master and per sub unit.
fn units(header: &[u8]) -> (f64, f64) {
// Unit definitions are numerator/denominator pairs per metre: master
// (4180/4188), sub (4196/4204), storage (4332/4340), and the UORs per
// storage unit (4324).
let get = |at: usize| f64_at(header, at).filter(|v| v.is_finite() && *v > 0.0);
let (mn, md, sn, sd) = (get(4180), get(4188), get(4196), get(4204));
let (uor, stn, std) = (get(4324), get(4332), get(4340));
let uor_per_metre = match (uor, stn, std) {
(Some(u), Some(n), Some(d)) => u * n / d,
_ => 1_000_000.0,
};
let master = match (mn, md) {
(Some(n), Some(d)) => uor_per_metre * d / n,
_ => uor_per_metre,
};
let sub = match (sn, sd) {
(Some(n), Some(d)) => uor_per_metre * d / n,
_ => master / 1000.0,
};
(master, sub)
}
/// Models of the file: (name, graphic stream, header stream).
fn models(cfb: &Cfb) -> Vec<(String, Vec<u8>, Vec<u8>)> {
let streams = cfb.streams();
let mut dirs: Vec<String> = streams
.iter()
.filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string()))
.collect();
dirs.sort();
dirs.into_iter()
.map(|dir| {
let get = |suffix: &str| streams.iter().find(|(p, _)| *p == format!("{dir}/{suffix}")).map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
let graphics = get("Dgn^G/$1");
let header = get("Dgn~Mh");
let name = marked_string(&header, 0).unwrap_or_else(|| "Default".to_string());
(name, graphics, header)
})
.collect()
}
pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
let cfb = Cfb::open(bytes)?;
let names: Vec<String> = models(&cfb).into_iter().map(|m| m.0).collect();
(!names.is_empty()).then_some(names)
}
pub fn model(bytes: &[u8], name: &str) -> Option<Sheet> {
let cfb = Cfb::open(bytes)?;
let all = models(&cfb);
let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?;
let non_model = cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default();
let nm = elements(&non_model);
let colors = nm
.iter()
.find(|e| kind(e) == 5 && e.len() >= 37 + 768)
.map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect())
.unwrap_or_else(|| vec![[255, 255, 255]; 256]);
let levels = nm
.iter()
.filter(|e| kind(e) == 95 && e.len() >= 80)
.filter_map(|e| Some((u32_at(e, 32)?, u32_at(e, 72)?)))
.collect();
let styles = Styles { colors, levels };
// Shared cell definitions: a type-34 element and its components.
let mut defs = HashMap::new();
let mut i = 0;
while i < nm.len() {
if kind(nm[i]) == 34 && !is_component(nm[i]) {
let name = marked_string(nm[i], 248).unwrap_or_default().to_ascii_uppercase();
let mut j = i + 1;
while j < nm.len() && is_component(nm[j]) {
j += 1;
}
defs.insert(name, read_run(&nm[i + 1..j], &styles));
i = j;
} else {
i += 1;
}
}
let raw = read_run(&elements(graphics), &styles);
let (per_unit, sub_uor) = units(header);
let m: Affine = [1.0 / per_unit, 0.0, 0.0, 1.0 / per_unit, 0.0, 0.0];
let mut sheet = Sheet { sub_per_master: per_unit / sub_uor, ..Default::default() };
place(&raw, &m, &defs, 0, &mut sheet);
sheet.rect = paths_bounds(&sheet.paths)?;
Some(sheet)
}
+782
View File
@@ -0,0 +1,782 @@
//! DWF (6.x, a zip after a "(DWF V06.00)" tag, sheets as W2D streams) and
//! DWFx (an XPS package, sheets as fixed pages) underlays: sheet names, and
//! each sheet's plotted geometry mapped back to the model units it was
//! plotted from, which is how an underlay places it.
use std::collections::HashMap;
use std::io::{Cursor, Read};
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath};
/// The package's files, with `\` separators turned into `/`.
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
// A DWF 6 package is a zip after a short tag; the archive reader finds
// the offset itself.
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).ok()?;
let mut files = HashMap::new();
for i in 0..archive.len() {
let mut file = archive.by_index(i).ok()?;
let name = file.name().replace('\\', "/").trim_start_matches('/').to_string();
let mut data = Vec::new();
file.read_to_end(&mut data).ok()?;
files.insert(name, data);
}
Some(files)
}
/// One plotted sheet of the package.
struct SheetEntry {
name: String,
order: i64,
/// The sheet's graphics: a W2D stream or an XPS fixed page.
graphics: String,
/// Paper millimetres per graphics unit and the paper offset (mm).
paper: ([f64; 2], [f64; 2]),
paper_height_mm: f64,
/// DWFx: the W2X resource holding the model units transform.
w2x: Option<String>,
}
fn attr<'a>(node: roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> {
node.attributes().find(|a| a.name() == name).map(|a| a.value())
}
fn numbers(text: &str) -> Vec<f64> {
text.split(|c: char| c == ',' || c.is_whitespace())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect()
}
/// Every sheet the package plots, in plot order.
fn sheets(files: &HashMap<String, Vec<u8>>) -> Vec<SheetEntry> {
let mut out = Vec::new();
for (name, data) in files {
if !name.ends_with("descriptor.xml") {
continue;
}
let Ok(text) = std::str::from_utf8(data) else { continue };
let Ok(doc) = roxmltree::Document::parse(text.trim_start_matches('\u{feff}')) else { continue };
let page = doc.root_element();
if page.tag_name().name() != "Page" {
continue;
}
let dir = name.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
let resolve = |href: &str| -> String {
let href = href.replace('\\', "/");
let href = href.split('?').next().unwrap_or("").to_string();
if let Some(abs) = href.strip_prefix('/') {
abs.to_string()
} else if files.contains_key(&href) {
href
} else {
// Relative to the descriptor's section folder or its parent.
let joined = format!("{dir}/{href}");
if files.contains_key(&joined) {
joined
} else {
let parent = dir.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
format!("{parent}/{href}").trim_start_matches('/').to_string()
}
}
};
let mut graphics = None;
let mut paper = ([0.0254 * 1000.0 / 1200.0; 2], [0.0; 2]);
let mut w2x = None;
let mut paper_height_mm = 0.0;
for node in page.descendants() {
match node.tag_name().name() {
"Paper" => {
let scale = match attr(node, "units") {
Some("in") => 25.4,
_ => 1.0,
};
paper_height_mm = attr(node, "height").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) * scale;
}
"GraphicResource" if attr(node, "role") == Some("2d streaming graphics") => {
graphics = attr(node, "href").map(resolve);
let t = attr(node, "transform").map(numbers).unwrap_or_default();
if t.len() == 16 {
paper = ([t[0], t[5]], [t[12], t[13]]);
}
}
"Resource" if attr(node, "role") == Some("2d graphics extension") => {
w2x = attr(node, "href").map(resolve);
}
_ => {}
}
}
let Some(graphics) = graphics else { continue };
out.push(SheetEntry {
name: attr(page, "name").unwrap_or("").to_string(),
order: attr(page, "plotOrder").and_then(|v| v.parse().ok()).unwrap_or(0),
graphics,
paper,
paper_height_mm,
w2x,
});
}
out.sort_by(|a, b| a.order.cmp(&b.order).then(a.name.cmp(&b.name)));
out
}
/// Names of the package's sheets, in plot order.
pub fn sheet_names(bytes: &[u8]) -> Option<Vec<String>> {
let files = unzip(bytes)?;
let names: Vec<String> = sheets(&files).into_iter().map(|s| s.name).collect();
(!names.is_empty()).then_some(names)
}
/// Graphics units → model units: model = (graphics − offset) / scale.
#[derive(Clone, Copy, Debug)]
struct Units {
scale: [f64; 2],
offset: [f64; 2],
}
impl Units {
fn identity() -> Self {
Self { scale: [1.0, 1.0], offset: [0.0, 0.0] }
}
fn model(&self, p: [f64; 2]) -> [f64; 2] {
[(p[0] - self.offset[0]) / self.scale[0], (p[1] - self.offset[1]) / self.scale[1]]
}
fn length(&self, v: f64) -> f64 {
v / self.scale[0].abs().max(1e-12)
}
/// A 4×4 matrix written row by row, translation in the last row.
fn from_matrix(m: &[f64]) -> Option<Self> {
(m.len() == 16 && m[0] != 0.0 && m[5] != 0.0)
.then(|| Self { scale: [m[0], m[5]], offset: [m[12], m[13]] })
}
}
/// The named sheet's geometry in model units.
pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
let files = unzip(bytes)?;
let entries = sheets(&files);
let entry = entries
.iter()
.find(|s| s.name.eq_ignore_ascii_case(name))
.or_else(|| (name.is_empty()).then(|| entries.first()).flatten())?;
let data = files.get(&entry.graphics)?;
let (paths, view) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") {
w2d::read(data)?
} else {
let w2x = entry.w2x.as_ref().and_then(|p| files.get(p));
xps::read(data, w2x.map(|v| v.as_slice()), entry)?
};
let rect = view.or_else(|| paths_bounds(&paths))?;
Some(Sheet { rect, paths, texts: Vec::new(), sub_per_master: 1.0 })
}
/// The default colour map of a W2D stream: black, a 6×6×6 colour cube from
/// index 10 (red 180 steps, green 30, blue 5), greys after it.
fn default_color(index: u8) -> [u8; 3] {
let i = index as usize;
const LEVELS: [u8; 6] = [0, 51, 102, 153, 204, 255];
// ponytail: only the cube is confirmed against plotted files; the entries
// around it are a grey ramp until a file uses them.
match i {
10..=225 => {
let c = i - 10;
[LEVELS[c / 36], LEVELS[(c / 6) % 6], LEVELS[c % 6]]
}
0 => [0, 0, 0],
1..=9 => {
let g = (i * 255 / 10) as u8;
[g, g, g]
}
_ => {
let g = ((i - 226) * 255 / 29) as u8;
[g, g, g]
}
}
}
mod w2d {
use super::*;
struct Reader<'a> {
b: &'a [u8],
i: usize,
}
impl<'a> Reader<'a> {
fn u8(&mut self) -> Option<u8> {
let v = *self.b.get(self.i)?;
self.i += 1;
Some(v)
}
fn i16(&mut self) -> Option<i16> {
let v = i16::from_le_bytes(self.b.get(self.i..self.i + 2)?.try_into().ok()?);
self.i += 2;
Some(v)
}
fn u16(&mut self) -> Option<u16> {
let v = u16::from_le_bytes(self.b.get(self.i..self.i + 2)?.try_into().ok()?);
self.i += 2;
Some(v)
}
fn i32(&mut self) -> Option<i32> {
let v = i32::from_le_bytes(self.b.get(self.i..self.i + 4)?.try_into().ok()?);
self.i += 4;
Some(v)
}
fn count(&mut self) -> Option<usize> {
match self.u8()? {
0 => Some(self.u16()? as usize + 256),
n => Some(n as usize),
}
}
/// An extended ASCII opcode "(Name ...)": its name and body.
fn extended(&mut self) -> Option<(String, &'a [u8])> {
let start = self.i;
let mut depth = 0i32;
let mut quote = false;
while self.i < self.b.len() {
let c = self.b[self.i];
self.i += 1;
match c {
b'\'' => quote = !quote,
b'(' if !quote => depth += 1,
b')' if !quote => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
let body = &self.b[start + 1..self.i.saturating_sub(1)];
let name_len = body.iter().position(|c| c.is_ascii_whitespace() || *c == b'(').unwrap_or(body.len());
Some((String::from_utf8_lossy(&body[..name_len]).to_string(), &body[name_len..]))
}
}
/// "((a b c d)(e f g h)(i j k l)(m n o p))" inside a Units opcode.
fn units_of(body: &[u8]) -> Option<Units> {
let text = String::from_utf8_lossy(body);
let text = text.split_once("((").map(|(_, rest)| rest).unwrap_or(&text);
let values: Vec<f64> = text
.split(|c: char| c == '(' || c == ')' || c.is_whitespace())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect();
Units::from_matrix(&values[..values.len().min(16)])
}
/// Integer points of "(View x,y x,y)" or a Contour's points.
fn points_of(body: &[u8]) -> Vec<[f64; 2]> {
let text = String::from_utf8_lossy(body);
text.split_whitespace()
.filter_map(|tok| {
let (x, y) = tok.split_once(',')?;
Some([x.trim_matches(')').parse().ok()?, y.trim_matches(')').parse().ok()?])
})
.collect()
}
fn rect_of(points: &[[f64; 2]]) -> Option<[f64; 4]> {
let mut r = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
for p in points {
r = [r[0].min(p[0]), r[1].min(p[1]), r[2].max(p[0]), r[3].max(p[1])];
}
(r[0] < r[2] && r[1] < r[3]).then_some(r)
}
/// The stream's geometry in model units, and its plotted view.
pub fn read(b: &[u8]) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
let mut r = Reader { b, i: 0 };
let mut units = Units::identity();
let mut view: Option<[f64; 4]> = None;
let mut color = [0u8, 0, 0];
let mut weight = 0.0f64;
let mut visible = true;
let mut at = [0f64; 2];
let mut raw: Vec<(Vec<SubPath>, [u8; 3], f64, bool)> = Vec::new();
let rel = |at: &mut [f64; 2], dx: f64, dy: f64| {
at[0] += dx;
at[1] += dy;
*at
};
while r.i < b.len() {
let op = b[r.i];
match op {
b' ' | b'\t' | b'\r' | b'\n' => r.i += 1,
b'(' => {
let (name, body) = r.extended()?;
match name.as_str() {
"Units" => units = units_of(body).unwrap_or(units),
"View" => view = rect_of(&points_of(body)).or(view),
"Viewport" if view.is_none() => {
view = rect_of(&points_of(body));
if let Some(u) = units_of(body) {
units = u;
}
}
"Color" => {
let v = super::numbers(&String::from_utf8_lossy(body));
if v.len() >= 3 {
color = [v[0] as u8, v[1] as u8, v[2] as u8];
}
}
_ => {}
}
}
b'{' => {
r.i += 1;
let size = r.i32()?;
r.i = r.i.checked_add(size.max(0) as usize)?;
}
b'V' => {
visible = true;
r.i += 1;
}
b'v' => {
visible = false;
r.i += 1;
}
b'c' => {
r.i += 1;
color = default_color(r.u8()?);
}
0x03 => {
r.i += 1;
let (cr, cg, cb, _ca) = (r.u8()?, r.u8()?, r.u8()?, r.u8()?);
color = [cr, cg, cb];
}
0x17 => {
r.i += 1;
weight = r.i32()? as f64;
}
0x0C | b'l' => {
r.i += 1;
let wide = op == b'l';
let mut pb = PathBuilder::default();
for k in 0..2 {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
let p = rel(&mut at, dx, dy);
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
}
raw.push((pb.finish(), color, weight, visible));
}
0x10 | b'p' => {
r.i += 1;
let wide = op == b'p';
let n = r.count()?;
let mut pb = PathBuilder::default();
for k in 0..n {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
let p = rel(&mut at, dx, dy);
if k == 0 { pb.move_to(p) } else { pb.line_to(p) }
}
raw.push((pb.finish(), color, weight, visible));
}
0x14 | b't' => {
r.i += 1;
let wide = op == b't';
let n = r.count()?;
let mut pts = Vec::with_capacity(n);
for _ in 0..n {
let (dx, dy) = if wide { (r.i32()? as f64, r.i32()? as f64) } else { (r.i16()? as f64, r.i16()? as f64) };
pts.push(rel(&mut at, dx, dy));
}
// A triangle strip: each point closes a triangle with the
// two before it.
let subpaths = pts
.windows(3)
.map(|t| SubPath {
segments: vec![Segment::Line(t[0], t[1]), Segment::Line(t[1], t[2]), Segment::Line(t[2], t[0])],
closed: true,
})
.collect();
raw.push((subpaths, color, -1.0, visible));
}
0x92 => {
// Circle or circular arc: relative centre, radius, start
// and end in 1/65536 turns (an end of 0 is a full turn).
r.i += 1;
let c = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
let radius = r.i32()? as f64;
let (s, e) = (r.u16()? as f64, r.u16()? as f64);
let turn = std::f64::consts::TAU / 65536.0;
let start = s * turn;
let mut sweep = (e - s) * turn;
if e == 0.0 && s == 0.0 || sweep == 0.0 {
sweep = std::f64::consts::TAU;
} else if sweep < 0.0 {
sweep += std::f64::consts::TAU;
}
raw.push((arc_path(c, radius, radius, 0.0, start, sweep), color, weight, visible));
}
b'e' | b'E' => {
// Ellipse: relative centre, major and minor radius, start
// and end (1/65536 turns) and tilt.
r.i += 1;
let c = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
let (major, minor) = (r.i32()? as f64, r.i32()? as f64);
let (s, e, tilt) = (r.u16()? as f64, r.u16()? as f64, r.u16()? as f64);
let turn = std::f64::consts::TAU / 65536.0;
let mut sweep = (e - s) * turn;
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
}
let fill = op == b'E';
raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible));
}
b'x' => {
// Text: relative position and a string (drawn as the
// outlines that follow it, so only the position counts).
r.i += 1;
rel(&mut at, r.i32()? as f64, r.i32()? as f64);
skip_string(&mut r)?;
}
_ => break,
}
}
let paths = raw
.into_iter()
.filter(|(_, _, _, visible)| *visible)
.map(|(subpaths, color, weight, _)| {
let subpaths = subpaths.into_iter().map(|sp| map_subpath(sp, |p| units.model(p))).collect();
if weight < 0.0 {
Path { subpaths, stroke: None, fill: Some(color) }
} else {
Path { subpaths, stroke: Some((color, units.length(weight))), fill: None }
}
})
.collect();
let view = view.map(|v| {
let a = units.model([v[0], v[1]]);
let b = units.model([v[2], v[3]]);
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
});
Some((paths, view))
}
/// A W2D string: quoted ASCII, or a count followed by UTF-16 units.
fn skip_string(r: &mut Reader) -> Option<()> {
if r.b.get(r.i) == Some(&b'\'') {
r.i += 1;
while *r.b.get(r.i)? != b'\'' {
r.i += 1;
}
r.i += 1;
} else {
let n = r.count()?;
r.i += n * 2;
}
Some(())
}
}
fn arc_path(c: [f64; 2], rx: f64, ry: f64, rot: f64, start: f64, sweep: f64) -> Vec<SubPath> {
let pieces = arc_cubics(c, rx, ry, rot, start, sweep);
let closed = (sweep.abs() - std::f64::consts::TAU).abs() < 1e-9;
vec![SubPath { segments: pieces.into_iter().map(|[a, b, cc, d]| Segment::Cubic(a, b, cc, d)).collect(), closed }]
}
fn map_subpath(sp: SubPath, f: impl Fn([f64; 2]) -> [f64; 2]) -> SubPath {
SubPath {
segments: sp
.segments
.into_iter()
.map(|s| match s {
Segment::Line(a, b) => Segment::Line(f(a), f(b)),
Segment::Cubic(a, b, c, d) => Segment::Cubic(f(a), f(b), f(c), f(d)),
})
.collect(),
closed: sp.closed,
}
}
mod xps {
use super::*;
/// Affine a, b, c, d, e, f: x' = a·x + c·y + e, y' = b·x + d·y + f.
type Affine = [f64; 6];
fn mul(outer: Affine, inner: Affine) -> Affine {
let [a, b, c, d, e, f] = outer;
let [a2, b2, c2, d2, e2, f2] = inner;
[
a * a2 + c * b2,
b * a2 + d * b2,
a * c2 + c * d2,
b * c2 + d * d2,
a * e2 + c * f2 + e,
b * e2 + d * f2 + f,
]
}
fn apply(m: Affine, p: [f64; 2]) -> [f64; 2] {
[m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]]
}
fn parse_affine(text: &str) -> Option<Affine> {
let v = numbers(text);
(v.len() == 6).then(|| [v[0], v[1], v[2], v[3], v[4], v[5]])
}
fn parse_color(text: &str) -> Option<[u8; 3]> {
let hex = text.trim().strip_prefix('#')?;
let hex = if hex.len() == 8 { &hex[2..] } else { hex };
(hex.len() == 6).then(|| {
let v = u32::from_str_radix(hex, 16).unwrap_or(0);
[(v >> 16) as u8, (v >> 8) as u8, v as u8]
})
}
/// The fixed page's paths in model units, and the plotted view.
pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec<Path>, Option<[f64; 4]>)> {
let mut units = Units::identity();
let mut view_log = None;
if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) {
if let Ok(doc) = roxmltree::Document::parse(w2x.trim_start_matches('\u{feff}')) {
for node in doc.descendants() {
match node.tag_name().name() {
"Units" => {
if let Some(u) = attr(node, "Transform").map(numbers).and_then(|m| Units::from_matrix(&m)) {
units = u;
}
}
"View" => {
let v = attr(node, "Area").map(numbers).unwrap_or_default();
if v.len() == 4 {
view_log = Some([v[0], v[1], v[2], v[3]]);
}
}
_ => {}
}
}
}
}
let text = std::str::from_utf8(page).ok()?;
let doc = roxmltree::Document::parse(text.trim_start_matches('\u{feff}')).ok()?;
// Page units (1/96 inch, y down) → graphics units: paper millimetres
// with y up, less the paper offset, over the graphics scale.
let (scale, offset) = entry.paper;
let px_mm = 25.4 / 96.0;
let to_logical = |p: [f64; 2]| {
let x = p[0] * px_mm;
let y = entry.paper_height_mm - p[1] * px_mm;
[(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]]
};
let mut paths = Vec::new();
walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| {
let Some(data) = attr(node, "Data") else { return };
let m = match attr(node, "RenderTransform").and_then(parse_affine) {
Some(local) => mul(m, local),
None => m,
};
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
let subpaths: Vec<SubPath> = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect();
if subpaths.is_empty() {
return;
}
let stroke_scale = (m[0] * m[3] - m[1] * m[2]).abs().sqrt() * px_mm / scale[0] / units.scale[0].abs();
let stroke = attr(node, "Stroke").and_then(parse_color).map(|c| {
let t = attr(node, "StrokeThickness").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0);
(c, t * stroke_scale)
});
let fill = attr(node, "Fill").and_then(parse_color);
if stroke.is_some() || fill.is_some() {
paths.push(Path { subpaths, stroke, fill });
}
});
let view = view_log.map(|v: [f64; 4]| {
let a = units.model([v[0], v[1]]);
let b = units.model([v[2], v[3]]);
[a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])]
});
Some((paths, view))
}
fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) {
for child in node.children().filter(|n| n.is_element()) {
match child.tag_name().name() {
"Canvas" => {
let m2 = match attr(child, "RenderTransform").and_then(parse_affine) {
Some(local) => mul(m, local),
None => m,
};
walk(child, m2, f);
}
"Path" => f(child, m),
_ => {}
}
}
}
/// The path mini-language: M L H V C Q S A Z and their relative forms.
fn parse_data(data: &str) -> Vec<SubPath> {
let mut toks: Vec<Tok> = Vec::new();
let bytes = data.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i] as char;
if c.is_ascii_alphabetic() && c != 'e' && c != 'E' {
if c == 'F' {
// Fill rule prefix "F0"/"F1".
i += 2;
continue;
}
toks.push(Tok::Cmd(c));
i += 1;
} else if c == '-' || c == '+' || c == '.' || c.is_ascii_digit() {
let start = i;
i += 1;
while i < bytes.len() {
let d = bytes[i] as char;
let prev = bytes[i - 1] as char;
if d.is_ascii_digit() || d == '.' || d == 'e' || d == 'E' || ((d == '-' || d == '+') && (prev == 'e' || prev == 'E')) {
i += 1;
} else {
break;
}
}
if let Ok(v) = data[start..i].parse() {
toks.push(Tok::Num(v));
}
} else {
i += 1;
}
}
let mut pb = PathBuilder::default();
let mut cmd = 'M';
let mut k = 0;
let mut last_ctrl: Option<[f64; 2]> = None;
let num = |toks: &Vec<Tok>, k: &mut usize| -> Option<f64> {
match toks.get(*k) {
Some(Tok::Num(v)) => {
*k += 1;
Some(*v)
}
_ => None,
}
};
while k < toks.len() {
if let Tok::Cmd(c) = toks[k] {
cmd = c;
k += 1;
if c == 'Z' || c == 'z' {
pb.close();
continue;
}
}
let cur = pb.current().unwrap_or([0.0, 0.0]);
let relative = cmd.is_ascii_lowercase();
let pt = |x: f64, y: f64| if relative { [cur[0] + x, cur[1] + y] } else { [x, y] };
match cmd.to_ascii_uppercase() {
'M' => {
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
pb.move_to(pt(x, y));
cmd = if relative { 'l' } else { 'L' };
}
'L' => {
let (Some(x), Some(y)) = (num(&toks, &mut k), num(&toks, &mut k)) else { break };
pb.line_to(pt(x, y));
}
'H' => {
let Some(x) = num(&toks, &mut k) else { break };
pb.line_to([if relative { cur[0] + x } else { x }, cur[1]]);
}
'V' => {
let Some(y) = num(&toks, &mut k) else { break };
pb.line_to([cur[0], if relative { cur[1] + y } else { y }]);
}
'C' => {
let v: Vec<f64> = (0..6).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 6 { break }
let (c1, c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]), pt(v[4], v[5]));
pb.cubic_to(c1, c2, p);
last_ctrl = Some(c2);
continue;
}
'S' => {
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 4 { break }
let c1 = last_ctrl.map(|c| [2.0 * cur[0] - c[0], 2.0 * cur[1] - c[1]]).unwrap_or(cur);
let (c2, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
pb.cubic_to(c1, c2, p);
last_ctrl = Some(c2);
continue;
}
'Q' => {
let v: Vec<f64> = (0..4).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 4 { break }
let (q, p) = (pt(v[0], v[1]), pt(v[2], v[3]));
let c1 = [cur[0] + 2.0 / 3.0 * (q[0] - cur[0]), cur[1] + 2.0 / 3.0 * (q[1] - cur[1])];
let c2 = [p[0] + 2.0 / 3.0 * (q[0] - p[0]), p[1] + 2.0 / 3.0 * (q[1] - p[1])];
pb.cubic_to(c1, c2, p);
}
'A' => {
let v: Vec<f64> = (0..7).filter_map(|_| num(&toks, &mut k)).collect();
if v.len() < 7 { break }
let end = pt(v[5], v[6]);
for [_, c1, c2, p] in svg_arc(cur, v[0], v[1], v[2].to_radians(), v[3] != 0.0, v[4] != 0.0, end) {
pb.cubic_to(c1, c2, p);
}
}
_ => k += 1,
}
last_ctrl = None;
}
pb.finish()
}
enum Tok {
Cmd(char),
Num(f64),
}
/// An SVG/XPS endpoint arc as cubic pieces.
fn svg_arc(p0: [f64; 2], rx: f64, ry: f64, phi: f64, large: bool, sweep: bool, p1: [f64; 2]) -> Vec<[[f64; 2]; 4]> {
let (mut rx, mut ry) = (rx.abs(), ry.abs());
if rx == 0.0 || ry == 0.0 || p0 == p1 {
return vec![[p0, p0, p1, p1]];
}
let (c, s) = (phi.cos(), phi.sin());
let dx = (p0[0] - p1[0]) / 2.0;
let dy = (p0[1] - p1[1]) / 2.0;
let x1 = c * dx + s * dy;
let y1 = -s * dx + c * dy;
let lambda = (x1 * x1) / (rx * rx) + (y1 * y1) / (ry * ry);
if lambda > 1.0 {
rx *= lambda.sqrt();
ry *= lambda.sqrt();
}
let num = (rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1).max(0.0);
let den = rx * rx * y1 * y1 + ry * ry * x1 * x1;
let mut coef = if den > 0.0 { (num / den).sqrt() } else { 0.0 };
if large == sweep {
coef = -coef;
}
let cx1 = coef * rx * y1 / ry;
let cy1 = -coef * ry * x1 / rx;
let cx = c * cx1 - s * cy1 + (p0[0] + p1[0]) / 2.0;
let cy = s * cx1 + c * cy1 + (p0[1] + p1[1]) / 2.0;
let ang = |ux: f64, uy: f64, vx: f64, vy: f64| {
let a = (ux * vy - uy * vx).atan2(ux * vx + uy * vy);
a
};
let t1 = ang(1.0, 0.0, (x1 - cx1) / rx, (y1 - cy1) / ry);
let mut dt = ang((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry);
if !sweep && dt > 0.0 {
dt -= std::f64::consts::TAU;
} else if sweep && dt < 0.0 {
dt += std::f64::consts::TAU;
}
let mut pieces = arc_cubics([cx, cy], rx, ry, phi, t1, dt);
// Pin the ends so the path joins exactly.
if let Some(first) = pieces.first_mut() {
first[0] = p0;
}
if let Some(last) = pieces.last_mut() {
last[3] = p1;
}
pieces
}
}
+235
View File
@@ -0,0 +1,235 @@
//! DWF and DGN underlay content: sheet (DWF) or model (DGN) names, each
//! sheet's geometry in its own units, a display raster of it and the
//! vectors object snaps use — the counterpart of `pdf_raster` and
//! `pdf_vector` for the other underlay kinds.
//!
//! A DWF sheet is in the model units it was plotted from, with the plot's
//! model origin at the underlay's insertion point; a DGN model is in its
//! master units with the global origin there.
mod dgn7;
mod dgn8;
mod dwf;
mod model;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use codec::entities::UnderlayType;
pub use model::{Path, Segment, Sheet, SubPath};
use super::pdf_raster::{source_bytes, PdfPage};
/// Sheet names (DWF) or model names (DGN) of a file.
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
let bytes = source_bytes(path)?;
match kind {
UnderlayType::Dwf => dwf::sheet_names(&bytes),
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model_names(&bytes),
UnderlayType::Dgn => dgn8::model_names(&bytes),
UnderlayType::Pdf => None,
}
}
type Key = (String, String);
fn sheet_cache() -> &'static Mutex<HashMap<Key, Option<Arc<Sheet>>>> {
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<Sheet>>>>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
fn raster_cache() -> &'static Mutex<HashMap<Key, Option<Arc<PdfPage>>>> {
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<PdfPage>>>>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// Forget what was read from `path` (its bytes changed).
pub fn forget(path: &str) {
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
}
/// The named sheet or model (the first when the name is empty or unknown),
/// memoised. Its text is drawn as strokes with the drawing's text engine.
pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<Sheet>> {
let key = (path.to_string(), item.to_string());
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
return hit.clone();
}
let read = || -> Option<Sheet> {
let bytes = source_bytes(path)?;
let mut sheet = match kind {
UnderlayType::Dwf => dwf::sheet(&bytes, item)?,
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes)?,
UnderlayType::Dgn => dgn8::model(&bytes, item)?,
UnderlayType::Pdf => return None,
};
outline_texts(&mut sheet);
Some(sheet)
};
let value = read().map(Arc::new);
sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
value
}
/// Text as strokes (and fills for outline fonts), in the text's colour.
fn outline_texts(sheet: &mut Sheet) {
for t in std::mem::take(&mut sheet.texts) {
let (strokes, fills) = crate::scene::text::lff::tessellate_text_ex(
[t.origin[0] as f32, t.origin[1] as f32],
t.height as f32,
t.rotation as f32,
t.width_factor as f32,
0.0,
"txt",
&t.text,
);
let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64];
let subpaths: Vec<SubPath> = strokes
.iter()
.filter(|s| s.len() >= 2)
.map(|s| SubPath { segments: s.windows(2).map(|w| Segment::Line(at(w[0]), at(w[1]))).collect(), closed: false })
.collect();
if !subpaths.is_empty() {
sheet.paths.push(Path { subpaths, stroke: Some((t.color, -1.0)), fill: None });
}
let triangles: Vec<SubPath> = fills
.chunks_exact(3)
.map(|t| SubPath {
segments: vec![Segment::Line(at(t[0]), at(t[1])), Segment::Line(at(t[1]), at(t[2])), Segment::Line(at(t[2]), at(t[0]))],
closed: true,
})
.collect();
if !triangles.is_empty() {
sheet.paths.push(Path { subpaths: triangles, stroke: None, fill: Some(t.color) });
}
}
}
/// Longest side of a sheet's display raster, pixels.
const RASTER_SIDE: f64 = 3072.0;
/// The sheet drawn on a transparent background, straight alpha, rows from
/// the top, memoised.
pub fn display_raster(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<PdfPage>> {
let key = (path.to_string(), item.to_string());
if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
return hit.clone();
}
let value = sheet(kind, path, item).and_then(|s| rasterize(&s)).map(Arc::new);
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
value
}
fn rasterize(sheet: &Sheet) -> Option<PdfPage> {
let [x0, y0, x1, y1] = sheet.rect;
let (w, h) = (x1 - x0, y1 - y0);
if !(w > 0.0 && h > 0.0) {
return None;
}
let scale = RASTER_SIDE / w.max(h);
let (pw, ph) = ((w * scale).ceil().max(1.0) as u32, (h * scale).ceil().max(1.0) as u32);
let mut pixmap = tiny_skia::Pixmap::new(pw, ph)?;
let to_px = |p: [f64; 2]| (((p[0] - x0) * scale) as f32, ((y1 - p[1]) * scale) as f32);
for path in &sheet.paths {
let mut pb = tiny_skia::PathBuilder::new();
for sp in &path.subpaths {
let Some(first) = sp.segments.first() else { continue };
let s = to_px(first.start());
pb.move_to(s.0, s.1);
for seg in &sp.segments {
match seg {
Segment::Line(_, b) => {
let b = to_px(*b);
pb.line_to(b.0, b.1);
}
Segment::Cubic(_, c1, c2, b) => {
let (c1, c2, b) = (to_px(*c1), to_px(*c2), to_px(*b));
pb.cubic_to(c1.0, c1.1, c2.0, c2.1, b.0, b.1);
}
}
}
if sp.closed {
pb.close();
}
}
let Some(p) = pb.finish() else { continue };
if let Some(c) = path.fill {
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(c[0], c[1], c[2], 255);
// Fills that share an edge (triangle strips) would show a seam.
paint.anti_alias = false;
pixmap.fill_path(&p, &paint, tiny_skia::FillRule::Winding, tiny_skia::Transform::identity(), None);
}
if let Some((c, width)) = path.stroke {
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(c[0], c[1], c[2], 255);
paint.anti_alias = true;
let stroke = tiny_skia::Stroke {
// A negative width is in pixels (DGN line weights).
width: if width < 0.0 { (-width) as f32 } else { ((width * scale) as f32).max(1.0) },
line_cap: tiny_skia::LineCap::Round,
line_join: tiny_skia::LineJoin::Round,
..Default::default()
};
pixmap.stroke_path(&p, &paint, &stroke, tiny_skia::Transform::identity(), None);
}
}
let mut pixels = pixmap.take();
for px in pixels.chunks_exact_mut(4) {
let a = px[3] as u32;
if a > 0 && a < 255 {
for c in &mut px[..3] {
*c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
}
}
}
Some(PdfPage { pixels: Arc::new(pixels), width: pw, height: ph, dpi: 0.0 })
}
fn vector_cache() -> &'static Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>> {
static CACHE: OnceLock<Mutex<HashMap<Key, Option<Arc<super::pdf_vector::PageVectors>>>>> = OnceLock::new();
CACHE.get_or_init(Default::default)
}
/// The sheet's geometry as page vectors for object snaps, in sheet units,
/// memoised.
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<super::pdf_vector::PageVectors>> {
let key = (path.to_string(), item.to_string());
if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
return hit.clone();
}
let value = sheet(kind, path, item).map(|s| Arc::new(to_page_vectors(&s)));
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
value
}
fn to_page_vectors(sheet: &Sheet) -> super::pdf_vector::PageVectors {
use super::pdf_vector as pv;
let paths = sheet
.paths
.iter()
.map(|p| pv::PdfPath {
subpaths: p
.subpaths
.iter()
.map(|sp| pv::SubPath {
segments: sp
.segments
.iter()
.map(|s| match s {
Segment::Line(a, b) => pv::Segment::Line(*a, *b),
Segment::Cubic(a, b, c, d) => pv::Segment::Cubic(*a, *b, *c, *d),
})
.collect(),
closed: sp.closed,
})
.collect(),
stroke: p.stroke.map(|(c, w)| (c, w.max(0.0))),
fill: p.fill,
})
.collect();
pv::PageVectors { paths, ..Default::default() }
}
+202
View File
@@ -0,0 +1,202 @@
//! Vector content of a DWF sheet or DGN model, in the underlay's own units
//! (the sheet's model units), and what an underlay needs from it.
/// One segment of a path.
#[derive(Clone, Debug, PartialEq)]
pub enum Segment {
Line([f64; 2], [f64; 2]),
Cubic([f64; 2], [f64; 2], [f64; 2], [f64; 2]),
}
impl Segment {
pub fn start(&self) -> [f64; 2] {
match self {
Segment::Line(a, _) | Segment::Cubic(a, ..) => *a,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SubPath {
pub segments: Vec<Segment>,
pub closed: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Path {
pub subpaths: Vec<SubPath>,
/// Stroke colour and width (sheet units; 0 draws one pixel wide).
pub stroke: Option<([u8; 3], f64)>,
pub fill: Option<[u8; 3]>,
}
/// A sheet (DWF) or model (DGN): its extent and what it draws.
#[derive(Clone, Debug, Default)]
pub struct Sheet {
/// min x, min y, max x, max y in sheet units.
pub rect: [f64; 4],
pub paths: Vec<Path>,
pub texts: Vec<Text>,
/// DGN: sub units per master unit (the scale a Sub conversion offers).
pub sub_per_master: f64,
}
/// Builds paths from pen moves.
#[derive(Default)]
pub struct PathBuilder {
pub subpaths: Vec<SubPath>,
current: Vec<Segment>,
start: Option<[f64; 2]>,
at: Option<[f64; 2]>,
}
impl PathBuilder {
pub fn move_to(&mut self, p: [f64; 2]) {
self.flush(false);
self.start = Some(p);
self.at = Some(p);
}
pub fn line_to(&mut self, p: [f64; 2]) {
let Some(a) = self.at else {
self.move_to(p);
return;
};
self.current.push(Segment::Line(a, p));
self.at = Some(p);
}
pub fn cubic_to(&mut self, c1: [f64; 2], c2: [f64; 2], p: [f64; 2]) {
let Some(a) = self.at else {
self.move_to(p);
return;
};
self.current.push(Segment::Cubic(a, c1, c2, p));
self.at = Some(p);
}
pub fn close(&mut self) {
if let (Some(s), Some(a)) = (self.start, self.at) {
if s != a {
self.current.push(Segment::Line(a, s));
}
}
self.flush(true);
self.at = self.start;
}
pub fn current(&self) -> Option<[f64; 2]> {
self.at
}
fn flush(&mut self, closed: bool) {
if !self.current.is_empty() {
self.subpaths.push(SubPath { segments: std::mem::take(&mut self.current), closed });
}
}
pub fn finish(mut self) -> Vec<SubPath> {
self.flush(false);
self.subpaths
}
}
/// Cubic pieces of an elliptical arc: centre, radii, axis rotation and the
/// start and sweep angles (radians, counter-clockwise positive).
pub fn arc_cubics(
center: [f64; 2],
rx: f64,
ry: f64,
rotation: f64,
start: f64,
sweep: f64,
) -> Vec<[[f64; 2]; 4]> {
let n = ((sweep.abs() / (std::f64::consts::FRAC_PI_2)).ceil() as usize).max(1);
let step = sweep / n as f64;
let k = 4.0 / 3.0 * (step / 4.0).tan();
let (cr, sr) = (rotation.cos(), rotation.sin());
let at = |x: f64, y: f64| [center[0] + x * cr - y * sr, center[1] + x * sr + y * cr];
let mut out = Vec::with_capacity(n);
for i in 0..n {
let a0 = start + step * i as f64;
let a1 = a0 + step;
let (c0, s0) = (a0.cos(), a0.sin());
let (c1, s1) = (a1.cos(), a1.sin());
let p0 = at(rx * c0, ry * s0);
let p3 = at(rx * c1, ry * s1);
let p1 = at(rx * (c0 - k * s0), ry * (s0 + k * c0));
let p2 = at(rx * (c1 + k * s1), ry * (s1 - k * c1));
out.push([p0, p1, p2, p3]);
}
out
}
/// Bounds of the paths, when there are any.
pub fn paths_bounds(paths: &[Path]) -> Option<[f64; 4]> {
let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
let mut add = |p: [f64; 2]| {
b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])];
};
for path in paths {
for sp in &path.subpaths {
for seg in &sp.segments {
match seg {
Segment::Line(a, c) => {
add(*a);
add(*c);
}
Segment::Cubic(a, c1, c2, c) => {
add(*a);
add(*c1);
add(*c2);
add(*c);
}
}
}
}
}
(b[0] <= b[2]).then_some(b)
}
/// A line of text the host draws with its own font.
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
pub text: String,
/// Baseline start, sheet units.
pub origin: [f64; 2],
pub height: f64,
/// Width of a character relative to its height.
pub width_factor: f64,
pub rotation: f64,
pub color: [u8; 3],
}
/// Points of a B-spline curve: its order, control points and knot vector
/// (clamped: `order` equal knots at each end).
pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> {
let n = poles.len();
if order < 2 || n < order || knots.len() != n + order {
return poles.to_vec();
}
let (t0, t1) = (knots[order - 1], knots[n]);
let mut out = Vec::with_capacity(samples + 1);
for s in 0..=samples {
let t = t0 + (t1 - t0) * s as f64 / samples as f64;
// de Boor
let mut k = order - 1;
while k < n - 1 && t >= knots[k + 1] {
k += 1;
}
let mut d: Vec<[f64; 2]> = (0..order).map(|j| poles[j + k + 1 - order]).collect();
for r in 1..order {
for j in (r..order).rev() {
let i = j + k + 1 - order;
let den = knots[i + order - r] - knots[i];
let a = if den.abs() < 1e-15 { 0.0 } else { (t - knots[i]) / den };
d[j] = [d[j - 1][0] * (1.0 - a) + d[j][0] * a, d[j - 1][1] * (1.0 - a) + d[j][1] * a];
}
}
out.push(d[order - 1]);
}
out
}
+69 -17
View File
@@ -220,6 +220,7 @@ impl ImageGpu {
"image.texture:{}:{}:{}",
model.file_path, x.content_start, y.content_start
);
let levels = mip_chain(&model.pixels, model.width, x.data_start, y.data_start, width, height);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(&tex_label),
size: wgpu::Extent3d {
@@ -227,7 +228,7 @@ impl ImageGpu {
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
mip_level_count: levels.len() as u32,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
@@ -235,22 +236,27 @@ impl ImageGpu {
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
texture.as_image_copy(),
&model.pixels[((y.data_start as usize * model.width as usize
+ x.data_start as usize)
* 4)..],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * model.width),
rows_per_image: Some(model.height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
for (level, (w, h, data)) in levels.iter().enumerate() {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: level as u32,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * w),
rows_per_image: Some(*h),
},
wgpu::Extent3d {
width: *w,
height: *h,
depth_or_array_layers: 1,
},
);
}
let tex_view =
texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -302,6 +308,52 @@ struct TileRange {
data_end: u32,
}
/// A tile's pixels and its smaller levels down to 1×1, so a far view
/// samples a level near its own size. A level keeps the most opaque of each
/// 2×2 block unless all four are equally opaque (then their average): thin
/// lines on a transparent page (PDF, DWF, DGN underlays) stay visible when
/// zoomed out instead of falling between samples, and opaque images are
/// averaged as usual.
fn mip_chain(pixels: &[u8], stride: u32, x0: u32, y0: u32, w: u32, h: u32) -> Vec<(u32, u32, Vec<u8>)> {
let (stride, x0, y0) = (stride as usize, x0 as usize, y0 as usize);
let mut base = Vec::with_capacity(w as usize * h as usize * 4);
for row in 0..h as usize {
let start = ((y0 + row) * stride + x0) * 4;
base.extend_from_slice(&pixels[start..start + w as usize * 4]);
}
let mut levels = vec![(w, h, base)];
loop {
let (pw, ph, prev) = levels.last().expect("base level");
if *pw == 1 && *ph == 1 {
break;
}
let (pw, ph) = (*pw as usize, *ph as usize);
let (nw, nh) = ((pw / 2).max(1), (ph / 2).max(1));
let mut next = Vec::with_capacity(nw * nh * 4);
for y in 0..nh {
for x in 0..nw {
let block = [(0, 0), (1, 0), (0, 1), (1, 1)].map(|(dx, dy)| {
let at = (((2 * y + dy).min(ph - 1)) * pw + (2 * x + dx).min(pw - 1)) * 4;
[prev[at], prev[at + 1], prev[at + 2], prev[at + 3]]
});
if block.iter().all(|p| p[3] == block[0][3]) {
let mut sum = [0u32; 4];
for p in &block {
for c in 0..4 {
sum[c] += p[c] as u32;
}
}
next.extend(sum.map(|v| ((v + 2) / 4) as u8));
} else {
next.extend(*block.iter().max_by_key(|p| p[3]).expect("four pixels"));
}
}
}
levels.push((nw as u32, nh as u32, next));
}
levels
}
fn tile_ranges(size: u32, limit: u32) -> Vec<TileRange> {
if size <= limit {
return vec![TileRange {