DWF/DGN underlays: clipping, layers, text, 3D DGN, colours and zoom extents

- PDFOSNAP, DWFOSNAP and DGNOSNAP are separate; UOSNAP sets them all and
  reads 2 while they differ. Enable Snap switches the selected kind's.
- DWFCLIP / DGNCLIP clip their own kind with the kind's prompts and message;
  CLIP routes DWF and DGN underlays there; Enter at a corner asks again.
- The right-edge underlay tools show for DWF and DGN too (import is PDF
  only).
- Underlay layers: DGN levels (V8 level table, "Default" listed as "0"; V7
  used levels named from the level name table), hidden through the layer
  override data with the DGN placeholder string first; a plotted DWF has
  none.
- V8 colour read from its colour word (by level, index, or a true colour
  drawn as its nearest index); 3D V8 elements drawn in plan (lines, line
  strings, ellipses and arcs by their orientation, text); a model's extent
  includes its text.
- DWF 6 TrueType text (font, height, rotation from the font opcode) and DWFx
  glyph runs are drawn; the default W2D colour map is the measured one.
- Contrast pulls lightness towards a per-kind pivot as measured (PDF 0.5,
  DWF 0.243, DGN 0.65).
- Zoom extents includes raster images and underlays whose frames are hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-28 16:10:01 +03:00
1 parent 1cbe89ef62
commit 56ecf786f1
41 files changed
+593 -147

No files matched your search

+54 -2
View File
@@ -262,6 +262,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
width_factor: if height > 0.0 { width / height } else { 1.0 },
rotation: (rotation as f64 / 360000.0).to_radians(),
color,
font: "txt".to_string(),
});
}
27 => {
@@ -380,14 +381,65 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap<String, Raw>, depth: usize, out:
}
}
/// The model's geometry, in master units.
pub fn model(bytes: &[u8]) -> Option<Sheet> {
/// A level's number (0-63) from an element header.
fn level(e: &[u8]) -> u8 {
e[0] & 0x3f
}
/// Level names from the level name table (type 66 elements on level 6):
/// each entry is a level number followed by its name.
// ponytail: entries are found by that pattern rather than by the table's
// record layout; read the layout if a file lists a level wrongly.
fn level_names(all: &[&[u8]]) -> HashMap<u8, String> {
let mut names = HashMap::new();
for e in all.iter().filter(|e| e[1] & 0x7f == 66 && level(e) == 6) {
for j in 36..e.len().saturating_sub(3) {
let n = u16::from_le_bytes([e[j], e[j + 1]]);
if !(1..=63).contains(&n) || (j > 36 && e[j - 1] != 0) || names.contains_key(&(n as u8)) {
continue;
}
let rest = &e[j + 2..];
let len = rest.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'$'))).unwrap_or(rest.len());
if len > 0 && rest.get(len) == Some(&0) {
names.insert(n as u8, String::from_utf8_lossy(&rest[..len]).to_string());
}
}
}
names
}
/// The used levels' names (or numbers), for the underlay layers list.
pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
if !is_v7(bytes) {
return None;
}
let all = elements(bytes);
let names = level_names(&all);
let mut used: Vec<u8> = all
.iter()
.filter(|e| !matches!(e[1] & 0x7f, 9 | 8 | 10 | 5 | 66 | 34) && level(e) > 0)
.map(|e| level(e))
.collect();
used.sort_unstable();
used.dedup();
Some(used.into_iter().map(|l| names.get(&l).cloned().unwrap_or_else(|| l.to_string())).collect())
}
/// The model's geometry, in master units, without the `hidden` levels.
pub fn model(bytes: &[u8], hidden: &[String]) -> 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);
let names = level_names(&all);
let off = |e: &[u8]| {
let l = level(e);
let name = names.get(&l).cloned().unwrap_or_else(|| l.to_string());
hidden.iter().any(|h| h.eq_ignore_ascii_case(&name))
};
let all: Vec<&[u8]> = all.into_iter().filter(|e| !off(e)).collect();
// 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();
+121 -14
View File
@@ -210,15 +210,22 @@ struct Styles {
colors: Vec<[u8; 3]>,
/// Level id → colour index.
levels: HashMap<u32, u32>,
/// Levels the underlay turns off: their elements are left out.
hidden: std::collections::HashSet<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 {
// The colour word: an index into the colour table, 0xFFFFFFFF for
// the level's colour, or a true colour — n × 256 plus the index of
// its nearest table colour.
// ponytail: a true colour draws as that nearest index; the RGB it
// stands for is not read.
let explicit = u32_at(e, 52).unwrap_or(u32::MAX);
let index = if explicit == u32::MAX {
self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0)
} else {
explicit & 0xff
};
self.colors.get(index as usize).copied().unwrap_or([255, 255, 255])
}
@@ -239,6 +246,32 @@ struct Instance {
origin: [f64; 2],
}
/// A 3D element (flag 0x0800 of its properties word): three coordinates a
/// point, orientations as quaternions. Its plan view is drawn.
fn is_3d(e: &[u8]) -> bool {
u32_at(e, 40).is_some_and(|v| v & 0x0800 != 0)
}
/// The in-plane x and y axes (projected onto the plan) of a 3D element's
/// orientation quaternion w, x, y, z stored at `at` (the rows of its
/// rotation matrix).
fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 2], [f64; 2])> {
let (w, x, y, z) = (f64_at(e, at)?, f64_at(e, at + 8)?, f64_at(e, at + 16)?, f64_at(e, at + 24)?);
Some((
[1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z)],
[2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z)],
))
}
/// An elliptical arc on axes `u` and `v` (plan projections) as cubics.
fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec<Segment> {
let map = |p: [f64; 2]| [c[0] + a * p[0] * u[0] + b * p[1] * v[0], c[1] + a * p[0] * u[1] + b * p[1] * v[1]];
arc_cubics([0.0, 0.0], 1.0, 1.0, 0.0, start, sweep)
.into_iter()
.map(|[p0, p1, p2, p3]| Segment::Cubic(map(p0), map(p1), map(p2), map(p3)))
.collect()
}
fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
let mut raw = Raw::default();
// A complex shape or chain collects its components into one path.
@@ -272,12 +305,13 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
}
}
for e in run {
if e.len() < 104 {
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
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)?]) };
let d3 = is_3d(e);
match kind(e) {
12 | 14 => {
let n = u32_at(e, 104).unwrap_or(0) as usize;
@@ -287,13 +321,14 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
}
}
3 => {
if let (Some(a), Some(b)) = (p2(104), p2(120)) {
if let (Some(a), Some(b)) = (p2(104), p2(if d3 { 128 } else { 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();
let stride = if d3 { 24 } else { 16 };
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * stride)).collect();
if pts.len() >= 2 {
let mut pb = PathBuilder::default();
pb.move_to(pts[0]);
@@ -306,6 +341,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None });
}
}
15 | 16 if d3 => {
// Axes, orientation and centre: ellipse a 104, b 112,
// quaternion 120, centre 152; arc start 104, sweep 112, a 120,
// b 128, quaternion 136, centre 168.
let arc = if kind(e) == 15 {
(|| Some((f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p2(152)?, 0.0, std::f64::consts::TAU)))()
} else {
(|| Some((f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p2(168)?, f64_at(e, 104)?, f64_at(e, 112)?)))()
};
let Some((a, b, (u, v), c, start, sweep)) = arc else { continue };
let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep };
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
push(
&mut raw,
&mut complex,
Path { subpaths: vec![SubPath { segments: projected_arc(c, a, b, u, v, start, sweep), closed }], 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)))()
@@ -321,6 +374,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
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 if d3 => {
// Width 112, height 120, quaternion 144, origin 176; the
// characters follow the string marker.
let (Some(width), Some(height), Some((u, _)), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), quat_axes(e, 144), p2(176)) else {
continue;
};
let Some(text) = marked_string(e, 200) else { continue };
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: u[1].atan2(u[0]),
color,
font: "txt".to_string(),
});
}
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;
@@ -331,7 +402,7 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw {
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 });
raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color, font: "txt".to_string() });
}
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 {
@@ -457,23 +528,59 @@ pub fn model_names(bytes: &[u8]) -> Option<Vec<String>> {
(!names.is_empty()).then_some(names)
}
pub fn model(bytes: &[u8], name: &str) -> Option<Sheet> {
/// The level table (the table entries after a header of table 1): id,
/// name and colour index. The default level (64) is listed as "0", as the
/// reference names it.
fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
let mut out = Vec::new();
let mut in_levels = false;
for e in nm {
match kind(e) {
96 => in_levels = u32_at(e, 12) == Some(1),
95 if in_levels && e.len() >= 80 => {
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else { continue };
let mut name = marked_string(e, 32).unwrap_or_default();
if id == 64 && name.eq_ignore_ascii_case("Default") {
name = "0".to_string();
}
out.push((id, name, color));
}
_ => {}
}
}
out
}
fn non_model(cfb: &Cfb) -> Vec<u8> {
cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default()
}
/// Level names of the file, for the underlay layers list.
pub fn layer_names(bytes: &[u8]) -> Option<Vec<String>> {
let cfb = Cfb::open(bytes)?;
let nm = non_model(&cfb);
Some(level_table(&elements(&nm)).into_iter().map(|l| l.1).filter(|n| !n.is_empty()).collect())
}
pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> 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 non_model = non_model(&cfb);
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
let table = level_table(&nm);
let levels = table.iter().map(|l| (l.0, l.2)).collect();
let hidden = table
.iter()
.filter(|e| kind(e) == 95 && e.len() >= 80)
.filter_map(|e| Some((u32_at(e, 32)?, u32_at(e, 72)?)))
.filter(|l| hidden.iter().any(|h| h.eq_ignore_ascii_case(&l.1)))
.map(|l| l.0)
.collect();
let styles = Styles { colors, levels };
let styles = Styles { colors, levels, hidden };
// Shared cell definitions: a type-34 element and its components.
let mut defs = HashMap::new();
let mut i = 0;
+141 -29
View File
@@ -6,7 +6,7 @@
use std::collections::HashMap;
use std::io::{Cursor, Read};
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath};
use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM};
/// The package's files, with `\` separators turned into `/`.
fn unzip(bytes: &[u8]) -> Option<HashMap<String, Vec<u8>>> {
@@ -160,37 +160,58 @@ pub fn sheet(bytes: &[u8], name: &str) -> Option<Sheet> {
.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") {
let (paths, view, texts) = 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 })
Some(Sheet { rect, paths, texts, 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.
/// The default colour map of a W2D stream: ten system colours, a 6×6×6
/// colour cube from index 10 (red 36 steps, green 6, blue 1), a grey ramp
/// at 226-245 and ten more system colours (measured against plotted files:
/// the cube, the ramp, 7 and 248).
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.
const LOW: [[u8; 3]; 10] = [
[0, 0, 0],
[128, 0, 0],
[0, 128, 0],
[128, 128, 0],
[0, 0, 128],
[128, 0, 128],
[0, 128, 128],
[192, 192, 192],
[192, 220, 192],
[166, 202, 240],
];
const HIGH: [[u8; 3]; 10] = [
[255, 251, 240],
[160, 160, 164],
[128, 128, 128],
[255, 0, 0],
[0, 255, 0],
[255, 255, 0],
[0, 0, 255],
[255, 0, 255],
[0, 255, 255],
[255, 255, 255],
];
match i {
0..=9 => LOW[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;
226..=245 => {
let g = ((i - 226) * 255 / 19) as u8;
[g, g, g]
}
_ => HIGH[i - 246],
}
}
@@ -286,9 +307,13 @@ mod w2d {
(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]>)> {
/// The stream's geometry and text in model units, and its plotted view.
pub fn read(b: &[u8]) -> Option<(Vec<Path>, Option<[f64; 4]>, Vec<Text>)> {
let mut r = Reader { b, i: 0 };
// The current font: family, em height (graphics units), rotation
// (radians) and width scale.
let mut font = (String::from("Arial"), 0.0f64, 0.0f64, 1.0f64);
let mut texts: Vec<(String, [f64; 2], (String, f64, f64, f64), [u8; 3])> = Vec::new();
let mut units = Units::identity();
let mut view: Option<[f64; 4]> = None;
let mut color = [0u8, 0, 0];
@@ -322,6 +347,13 @@ mod w2d {
color = [v[0] as u8, v[1] as u8, v[2] as u8];
}
}
// "(FontExtension 'logfont name' 'canonical name')"
"FontExtension" => {
let body = String::from_utf8_lossy(body);
if let Some(name) = body.split('\'').nth(1).filter(|n| !n.is_empty()) {
font.0 = name.to_string();
}
}
_ => {}
}
}
@@ -426,12 +458,47 @@ mod w2d {
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).
0x06 => {
// Font: a field mask, then the fields it names in order.
r.i += 1;
rel(&mut at, r.i32()? as f64, r.i32()? as f64);
skip_string(&mut r)?;
let mask = r.u16()?;
if mask & !0x07fe != 0 {
// A font name field (or one unknown here) — its
// encoding is not read, so stop.
break;
}
for bit in 1..11 {
if mask & (1 << bit) == 0 {
continue;
}
match 1u16 << bit {
// charset, pitch, family, style
0x0002 | 0x0004 | 0x0008 | 0x0010 => {
r.u8()?;
}
0x0020 => font.1 = r.i32()? as f64,
0x0040 => font.2 = r.u16()? as f64 * std::f64::consts::TAU / 65536.0,
0x0080 => font.3 = r.u16()? as f64 / 1024.0,
// oblique, spacing
0x0100 | 0x0200 => {
r.u16()?;
}
_ => {
r.i32()?;
}
}
}
}
b'x' => {
// Text at a relative position. Stroke-font text is also
// plotted as outlines with the text itself hidden, so only
// visible text is kept.
r.i += 1;
let p = rel(&mut at, r.i32()? as f64, r.i32()? as f64);
let s = read_string(&mut r)?;
if visible && font.1 > 0.0 && !s.trim().is_empty() {
texts.push((s, p, font.clone(), color));
}
}
_ => break,
}
@@ -453,22 +520,42 @@ mod w2d {
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))
let texts = texts
.into_iter()
.map(|(text, p, (family, em, rotation, width), color)| Text {
text,
origin: units.model(p),
height: units.length(em) * CAP_PER_EM,
width_factor: width,
rotation,
color,
font: family,
})
.collect();
Some((paths, view, texts))
}
/// A W2D string: quoted ASCII, or a count followed by UTF-16 units.
fn skip_string(r: &mut Reader) -> Option<()> {
fn read_string(r: &mut Reader) -> Option<String> {
if r.b.get(r.i) == Some(&b'\'') {
r.i += 1;
let start = r.i;
while *r.b.get(r.i)? != b'\'' {
r.i += 1;
}
r.i += 1;
Some(String::from_utf8_lossy(&r.b[start..r.i - 1]).to_string())
} else {
let n = r.count()?;
let units: Vec<u16> = r
.b
.get(r.i..r.i + n * 2)?
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
r.i += n * 2;
Some(String::from_utf16_lossy(&units))
}
Some(())
}
}
@@ -529,8 +616,8 @@ mod xps {
})
}
/// 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]>)> {
/// The fixed page's paths and text in model units, and the plotted view.
pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec<Path>, Option<[f64; 4]>, Vec<Text>)> {
let mut units = Units::identity();
let mut view_log = None;
if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) {
@@ -565,12 +652,37 @@ mod xps {
[(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]]
};
let mut paths = Vec::new();
let mut texts = 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,
};
if node.tag_name().name() == "Glyphs" {
// Text: its baseline origin and em size in page units (y
// down), drawn in the plotted family.
// ponytail: the embedded font is obfuscated and not read;
// DWFx text is drawn in Arial.
let num = |name: &str| attr(node, name).and_then(|v| v.parse::<f64>().ok());
let (Some(em), Some(text)) = (num("FontRenderingEmSize"), attr(node, "UnicodeString")) else { return };
let o = [num("OriginX").unwrap_or(0.0), num("OriginY").unwrap_or(0.0)];
let map = |p: [f64; 2]| units.model(to_logical(apply(m, p)));
let (a, along, up) = (map(o), map([o[0] + em, o[1]]), map([o[0], o[1] - em]));
let len = |p: [f64; 2]| ((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt();
if !text.trim().is_empty() {
texts.push(Text {
text: text.to_string(),
origin: a,
height: len(up) * CAP_PER_EM,
width_factor: if len(up) > 0.0 { len(along) / len(up) } else { 1.0 },
rotation: (along[1] - a[1]).atan2(along[0] - a[0]),
color: attr(node, "Fill").and_then(parse_color).unwrap_or([0, 0, 0]),
font: "Arial".to_string(),
});
}
return;
}
let Some(data) = attr(node, "Data") else { return };
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() {
@@ -591,7 +703,7 @@ mod xps {
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))
Some((paths, view, texts))
}
fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) {
@@ -604,7 +716,7 @@ mod xps {
};
walk(child, m2, f);
}
"Path" => f(child, m),
"Path" | "Glyphs" => f(child, m),
_ => {}
}
}
+48 -10
View File
@@ -39,6 +39,15 @@ fn sheet_cache() -> &'static Mutex<HashMap<Key, Option<Arc<Sheet>>>> {
CACHE.get_or_init(Default::default)
}
/// The cache key of an item seen without some layers.
fn layered_item(item: &str, hidden: &[String]) -> String {
if hidden.is_empty() {
item.to_string()
} else {
format!("{item}#{}", hidden.join("|"))
}
}
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)
@@ -51,10 +60,28 @@ pub fn forget(path: &str) {
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path);
}
/// Layer (DWF) or level (DGN) names of a file; empty when it has none.
pub fn layer_names(kind: UnderlayType, path: &str) -> Vec<String> {
let Some(bytes) = source_bytes(path) else { return Vec::new() };
match kind {
// A plotted DWF carries no layers.
UnderlayType::Dwf | UnderlayType::Pdf => None,
UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::layer_names(&bytes),
UnderlayType::Dgn => dgn8::layer_names(&bytes),
}
.unwrap_or_default()
}
/// 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());
sheet_without(kind, path, item, &[])
}
/// The sheet without the `hidden` layers / levels, on the full sheet's
/// rectangle (turning layers off never moves the underlay), memoised.
pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<Sheet>> {
let key = (path.to_string(), layered_item(item, hidden));
if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) {
return hit.clone();
}
@@ -62,11 +89,22 @@ pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option<Arc<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::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes, hidden).unwrap_or_default(),
UnderlayType::Dgn => dgn8::model(&bytes, item, hidden).unwrap_or_default(),
UnderlayType::Pdf => return None,
};
outline_texts(&mut sheet);
// A DGN model's extent is its geometry, text included (a DWF sheet
// keeps its plotted view).
if kind == UnderlayType::Dgn {
if let Some(b) = model::paths_bounds(&sheet.paths) {
let r = sheet.rect;
sheet.rect = [r[0].min(b[0]), r[1].min(b[1]), r[2].max(b[2]), r[3].max(b[3])];
}
}
if !hidden.is_empty() {
sheet.rect = sheet_without(kind, path, item, &[])?.rect;
}
Some(sheet)
};
let value = read().map(Arc::new);
@@ -83,7 +121,7 @@ fn outline_texts(sheet: &mut Sheet) {
t.rotation as f32,
t.width_factor as f32,
0.0,
"txt",
&t.font,
&t.text,
);
let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64];
@@ -114,13 +152,13 @@ pub const RASTER_SIDE: f64 = 3072.0;
/// The sheet drawn on a transparent background with `side` pixels on its
/// longest side (at most [`RASTER_SIDE`]), straight alpha, rows from the
/// top, memoised per size.
pub fn display_raster(kind: UnderlayType, path: &str, item: &str, side: f64) -> Option<Arc<PdfPage>> {
pub fn display_raster(kind: UnderlayType, path: &str, item: &str, hidden: &[String], side: f64) -> Option<Arc<PdfPage>> {
let side = side.clamp(16.0, RASTER_SIDE).round();
let key = (path.to_string(), format!("{item}@{side}"));
let key = (path.to_string(), format!("{}@{side}", layered_item(item, hidden)));
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, side)).map(Arc::new);
let value = sheet_without(kind, path, item, hidden).and_then(|s| rasterize(&s, side)).map(Arc::new);
raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
value
}
@@ -198,12 +236,12 @@ fn vector_cache() -> &'static Mutex<HashMap<Key, Option<Arc<super::pdf_vector::P
/// 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());
pub fn page_vectors(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option<Arc<super::pdf_vector::PageVectors>> {
let key = (path.to_string(), layered_item(item, hidden));
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)));
let value = sheet_without(kind, path, item, hidden).map(|s| Arc::new(to_page_vectors(&s)));
vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone());
value
}
+9
View File
@@ -169,8 +169,17 @@ pub struct Text {
pub width_factor: f64,
pub rotation: f64,
pub color: [u8; 3],
/// The font the text is drawn in: a stroke font name or a TrueType
/// family.
pub font: String,
}
/// Height of a TrueType font's capitals per em (Arial and its kin), which
/// turns a plotted em size into a text height.
// ponytail: one ratio for every family; read the font's own metrics if a
// serif family plots noticeably off.
pub const CAP_PER_EM: f64 = 0.716;
/// 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]> {