WMF: arcs on whole units, text by its font's ascent, no chords

Arcs and pies are cut at whole multiples of 1/99 turn round the
ellipse's whole-unit centre and radii, rounded down, from the start ray
to the end ray. Text sits the font's ascent below its reference point
and is four fifths of it tall; its style is WMF-<face><n> on the
installed file of that face. CHORD records are not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-10-16 19:12:47 +03:00
1 parent 8e9bd7db69
commit 9ddc103bcc
3 files changed
+215 -50

No files matched your search

+14 -15
View File
@@ -145,22 +145,21 @@ impl OpenCADStudio {
};
self.push_undo_snapshot(i, if prompt { "WMFIN" } else { "IMPORT" });
let scene = &mut self.tabs[i].scene;
// A text style per font, named WMF-<n> after the first free number.
// A text style per face, `WMF-<face><n>` with `n` counting the
// face's bold and italic variants; the style's font is the
// installed file of that face, none when it is not installed. A
// style the drawing already has is used as it is.
let mut styles: Vec<String> = Vec::new();
for _ in &picture.fonts {
let mut n = 0;
let name = loop {
let name = format!("WMF-{n}");
if !scene.document.text_styles.contains(&name) && !styles.contains(&name) {
break name;
}
n += 1;
};
let mut style = codec::tables::TextStyle::new(name.as_str());
style.handle = scene.document.allocate_handle();
// No font of its own, as the metafile names the face only.
style.font_file = String::new();
let _ = scene.document.text_styles.add(style);
for (k, face) in picture.fonts.iter().enumerate() {
let n = picture.fonts[..k].iter().filter(|f| f.name == face.name).count();
let name = format!("WMF-{}{n}", face.name);
if !scene.document.text_styles.contains(&name) {
let mut style = codec::tables::TextStyle::new(name.as_str());
style.handle = scene.document.allocate_handle();
style.font_file =
crate::scene::text::sysfont::face_file(&face.name, face.bold, face.italic).unwrap_or_default();
let _ = scene.document.text_styles.add(style);
}
styles.push(name);
}
for (at, font) in &picture.text_fonts {
+185 -35
View File
@@ -1,7 +1,9 @@
//! Windows metafile (WMF) reading: the records a metafile plays, as the
//! drawing objects of a block — outlines as polylines in their pen's colour
//! and width, filled shapes as closed polylines in their brush's colour,
//! ellipses as 99-segment polylines, text as single-line text.
//! ellipses as 99-segment polylines, arcs and pies on whole units, text as
//! single-line text placed and sized by its font's own ascent. Chords are
//! not read.
//!
//! The picture is scaled to fit half the view it is imported into, with its
//! top-left corner at the block's origin and y running down the screen as
@@ -11,13 +13,21 @@ use codec::entities::{Polyline2D, Text, Vertex2D};
use codec::types::{Color, Vector3};
use codec::EntityType;
/// A font face as the text uses it: its name, and whether bold and italic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Face {
pub name: String,
pub bold: bool,
pub italic: bool,
}
/// A picture: its objects, in block coordinates, and the font of each
/// text (an index into `fonts`).
pub struct Picture {
pub entities: Vec<EntityType>,
/// The face name of each font the text uses; text styles are
/// `WMF-<n>` for each, in this order.
pub fonts: Vec<String>,
/// Each face the text uses, in the order first used; each becomes a
/// text style.
pub fonts: Vec<Face>,
/// For each text in `entities` (by position), its font index.
pub text_fonts: Vec<(usize, usize)>,
}
@@ -37,11 +47,62 @@ struct Brush {
#[derive(Clone)]
struct Font {
/// The logical height: negative for the character height, positive for
/// the cell height.
height: f64,
escapement: f64,
weight: i32,
italic: bool,
face: String,
}
/// A font's ascent, in metafile units, as the system lays the font out at
/// its logical height: the baseline sits this far below a text's reference
/// point. Without the system's font engine it is nine tenths of the height.
#[cfg(target_os = "windows")]
fn ascent(font: &Font) -> f64 {
use windows_sys::Win32::Graphics::Gdi::{
CreateCompatibleDC, CreateFontIndirectW, DeleteDC, DeleteObject, GetTextMetricsW,
SelectObject, LOGFONTW, TEXTMETRICW,
};
// SAFETY: a private memory device context and font, released before
// returning; the structures are plain data the calls fill.
unsafe {
let mut logfont: LOGFONTW = std::mem::zeroed();
logfont.lfHeight = font.height as i32;
logfont.lfWeight = font.weight;
logfont.lfItalic = u8::from(font.italic);
for (slot, unit) in logfont
.lfFaceName
.iter_mut()
.zip(font.face.encode_utf16().take(31))
{
*slot = unit;
}
let dc = CreateCompatibleDC(std::ptr::null_mut());
if dc.is_null() {
return 0.9 * font.height.abs();
}
let handle = CreateFontIndirectW(&logfont);
let previous = SelectObject(dc, handle);
let mut metrics: TEXTMETRICW = std::mem::zeroed();
let ok = GetTextMetricsW(dc, &mut metrics) != 0;
SelectObject(dc, previous);
DeleteObject(handle);
DeleteDC(dc);
if ok {
f64::from(metrics.tmAscent)
} else {
0.9 * font.height.abs()
}
}
}
#[cfg(not(target_os = "windows"))]
fn ascent(font: &Font) -> f64 {
0.9 * font.height.abs()
}
#[derive(Clone)]
enum Object {
Pen(Pen),
@@ -70,7 +131,7 @@ struct Player {
origin: [f64; 2],
scale: [f64; 2],
entities: Vec<EntityType>,
fonts: Vec<String>,
fonts: Vec<Face>,
text_fonts: Vec<(usize, usize)>,
}
@@ -129,12 +190,94 @@ fn ellipse_points(bounds: [f64; 4], start: f64, sweep: f64) -> Vec<[f64; 2]> {
.collect()
}
/// The angle of a radial point round an ellipse's centre, in drawing
/// orientation.
fn angle(bounds: [f64; 4], x: f64, y: f64) -> f64 {
/// Turns an arc is cut into: its points lie on whole multiples of this
/// share of a turn.
const ARC_STEPS: i64 = 99;
/// The angle of a radial point round an ellipse, in degrees in `(0, 360]`
/// counter-clockwise in drawing orientation — or `None` for a point inside
/// the ellipse, which marks no angle.
fn radial_angle(bounds: [f64; 4], x: f64, y: f64) -> Option<f64> {
let [left, top, right, bottom] = bounds;
let (cx, cy) = (0.5 * (left + right), 0.5 * (top + bottom));
(-(y - cy)).atan2(x - cx)
let (rx, ry) = (0.5 * (right - left).abs(), 0.5 * (bottom - top).abs());
let (u, v) = ((x - cx) / rx, -(y - cy) / ry);
if u.hypot(v) < 1.0 {
return None;
}
let degrees = v.atan2(u).to_degrees().rem_euclid(360.0);
Some(if degrees == 0.0 { 360.0 } else { degrees })
}
/// An arc as the reference reader plays it: points round the ellipse's
/// whole-unit centre and radii at whole multiples of 1/99 turn, rounded
/// down to whole units, from the start angle up to the end angle — past
/// 360 degrees back from zero when the end comes first. A start inside the
/// ellipse starts at zero; an end inside it runs on to a whole turn; with
/// both inside there is no arc.
fn arc_points(bounds: [f64; 4], start: [f64; 2], end: [f64; 2]) -> Vec<[f64; 2]> {
let [left, top, right, bottom] = bounds.map(|v| v as i64);
let (cx, cy) = ((left + right).div_euclid(2), (top + bottom).div_euclid(2));
let (rx, ry) = ((right - left).abs() / 2, (bottom - top).abs() / 2);
let (from, to) = (radial_angle(bounds, start[0], start[1]), radial_angle(bounds, end[0], end[1]));
let (start_angle, end_angle) = match (from, to) {
(None, None) => return Vec::new(),
(None, to) => (0.0, to),
(Some(from), to) => (from, to.filter(|to| *to != from)),
};
// The angles are stepped out from zero one step at a time in degrees,
// rounding and all, as the points come out.
let mut degrees = vec![0.0f64; ARC_STEPS as usize + 1];
for k in 1..degrees.len() {
degrees[k] = degrees[k - 1] + 360.0 / ARC_STEPS as f64;
}
let point = |k: i64| {
let a = degrees[k as usize] * (std::f64::consts::PI / 180.0);
[
(cx as f64 + rx as f64 * a.cos()).floor(),
(cy as f64 + ry as f64 * -a.sin()).floor(),
]
};
// `k` steps lie below an angle when k·360 < angle·99, kept exact.
let under = |k: &i64, angle: f64| ((k * 360) as f64) < angle * ARC_STEPS as f64;
let first = (start_angle * ARC_STEPS as f64 / 360.0).ceil() as i64;
let mut steps: Vec<i64> = match end_angle {
// Up to the end.
Some(to) if to > start_angle => (first..).take_while(|k| under(k, to)).collect(),
// Round to a whole turn, then from zero up to the end.
Some(to) => (first..=ARC_STEPS).chain((0..).take_while(|k| under(k, to))).collect(),
// Round to a whole turn.
None => (first..=ARC_STEPS).collect(),
};
// The whole-unit points next to the start and end rays are judged by
// which side of the ray they fall: a point on the start ray is left
// out, one on the end ray kept.
let (fx, fy) = (0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]));
let side = |ray: [f64; 2], k: i64| {
let p = point(k);
let (ux, uy) = (ray[0] - fx, -(ray[1] - fy));
let (vx, vy) = (p[0] - fx, -(p[1] - fy));
ux * vy - uy * vx
};
if from.is_some() {
if let Some(&k) = steps.first() {
if k < ARC_STEPS && side(start, k) <= 0.0 {
steps.remove(0);
} else if k > 0 && k < ARC_STEPS && side(start, k - 1) > 0.0 {
steps.insert(0, k - 1);
}
}
}
if end_angle.is_some() {
if let Some(&k) = steps.last() {
if k > 0 && side(end, k) > 0.0 {
steps.pop();
} else if k + 1 < ARC_STEPS && side(end, k + 1) <= 0.0 {
steps.push(k + 1);
}
}
}
steps.into_iter().map(point).collect()
}
/// Reads a metafile, scaled so the picture fits within `fit` (width and
@@ -206,6 +349,8 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option<Picture> {
let mut font = Font {
height: 12.0,
escapement: 0.0,
weight: 400,
italic: false,
face: String::new(),
};
let mut text_rgb = [0, 0, 0];
@@ -251,8 +396,10 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option<Picture> {
create(
&mut table,
Object::Font(Font {
height: s(0).abs(),
height: s(0),
escapement: s(2) / 10.0,
weight: s(4) as i32,
italic: p.get(5).is_some_and(|w| w & 0xff != 0),
face: String::from_utf8_lossy(&face[..end]).into_owned(),
}),
)
@@ -321,29 +468,26 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option<Picture> {
player.polyline(&ring, false, pen.width, pen.rgb);
}
}
// ARC, CHORD, PIE: (yEnd, xEnd, yStart, xStart, bottom,
// right, top, left), drawn counter-clockwise
0x0817 | 0x0830 | 0x081A => {
// ARC, PIE: (yEnd, xEnd, yStart, xStart, bottom, right, top,
// left), drawn counter-clockwise; a pie's outline starts at the
// centre. (CHORD records are not read.)
0x0817 | 0x081A => {
let bounds = [s(7), s(6), s(5), s(4)];
let start = angle(bounds, s(3), s(2));
let end = angle(bounds, s(1), s(0));
let mut sweep = end - start;
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
let arc = arc_points(bounds, [s(3), s(2)], [s(1), s(0)]);
if arc.is_empty() {
continue;
}
let mut ring = ellipse_points(bounds, start, sweep);
match function {
0x0817 => {
if !pen.null {
player.polyline(&ring, false, pen.width, pen.rgb);
}
}
_ => {
if *function == 0x081A {
ring.push([0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3])]);
}
player.shape(&ring, &pen, &brush);
if *function == 0x0817 {
if !pen.null {
player.polyline(&arc, false, pen.width, pen.rgb);
}
} else {
let centre = [
((bounds[0] + bounds[2]) as i64).div_euclid(2) as f64,
((bounds[1] + bounds[3]) as i64).div_euclid(2) as f64,
];
let ring: Vec<[f64; 2]> = std::iter::once(centre).chain(arc).collect();
player.shape(&ring, &pen, &brush);
}
}
// TEXTOUT (count, string, y, x) and EXTTEXTOUT (y, x, count,
@@ -368,21 +512,27 @@ pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option<Picture> {
continue;
}
// The reference point is the cell's top; the baseline sits
// nine tenths of the font height below it.
// the font's ascent below it, and the text is four fifths
// of that ascent tall.
let rotation = font.escapement.to_radians();
let drop = 0.9 * font.height;
let drop = ascent(&font);
let base = player.at(x + drop * rotation.sin(), y + drop * rotation.cos());
let index = match player.fonts.iter().position(|f| *f == font.face) {
let face = Face {
name: font.face.clone(),
bold: font.weight >= 600,
italic: font.italic,
};
let index = match player.fonts.iter().position(|f| *f == face) {
Some(k) => k,
None => {
player.fonts.push(font.face.clone());
player.fonts.push(face);
player.fonts.len() - 1
}
};
let mut text = Text::new();
text.value = value;
text.insertion_point = base;
text.height = 0.72 * font.height * scale;
text.height = 0.8 * drop * scale;
text.rotation = rotation;
text.common.color = color(text_rgb);
player.text_fonts.push((player.entities.len(), index));
+16
View File
@@ -292,6 +292,22 @@ fn face_id_weighted(family: &str, weight: fontdb::Weight) -> Option<fontdb::ID>
fonts().db.lock().unwrap().query(&query)
}
/// The file name (lower case) of the installed face of exactly `family` —
/// no stand-in family — in the bold and italic asked for, or `None`.
pub fn face_file(family: &str, bold: bool, italic: bool) -> Option<String> {
let db = fonts().db.lock().unwrap();
let id = db.query(&fontdb::Query {
families: &[fontdb::Family::Name(family)],
weight: if bold { fontdb::Weight::BOLD } else { fontdb::Weight::NORMAL },
style: if italic { fontdb::Style::Italic } else { fontdb::Style::Normal },
..Default::default()
})?;
match &db.face(id)?.source {
fontdb::Source::File(path) => Some(path.file_name()?.to_string_lossy().to_ascii_lowercase()),
_ => None,
}
}
/// Borrow the raw face bytes for `family` and run `f` over them. The byte slice
/// is only valid inside the closure, so callers extract everything they need
/// (e.g. flattened glyph outlines) before returning. `index` is the face index