mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 12:24:19 +00:00
Metafile import: metric WMFs are read at 96 pixels an inch, and only round solid strokes join
A WMF without a usable placeable box that draws in a metric map mode (MM_LOMETRIC to MM_TWIPS) is read at 96 pixels an inch, the scale craft_metafile plays those modes at, so a one-inch MM_TWIPS picture opens at 72 pt instead of 4.8 pt. The mode that more of the drawing records the reader plays are drawn in decides, with SAVEDC and RESTOREDC followed as the reader follows them; a file that draws nothing goes by its last mode. Opaque strokes drawn one after another now join into one path only when the pen is solid with round caps and round joins. A dashed stroke keeps its own dash phase, and flat or square caps and miter or bevel joins stay as the separate strokes drew them.
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
//! picture, and the picture's items become document objects here.
|
||||
//!
|
||||
//! - A filled or stroked path becomes a path with a fill and a stroke (caps, joins, miter limit
|
||||
//! and dashes from the pen). Opaque strokes drawn one after another, each starting where the
|
||||
//! last one ended, with the same pen and clip, join into one path.
|
||||
//! and dashes from the pen). Opaque solid strokes with round caps and round joins drawn one after
|
||||
//! another, each starting where the last one ended, with the same pen and clip, join into one
|
||||
//! path. Other strokes stay apart, each with its own caps and its own dashes.
|
||||
//! - A path filled with a pattern or hatched brush is filled with the average color of the
|
||||
//! brush's tile.
|
||||
//! - A bitmap becomes an image (its pixels as PNG) placed where the picture draws it, turned or
|
||||
@@ -14,8 +15,10 @@
|
||||
//! the whole picture clips nothing and is left out.
|
||||
//!
|
||||
//! A WMF without a placeable header is read at 1440 units an inch, and one whose placeable box is
|
||||
//! empty or inverted at the units an inch the header gives. An EMF without a usable frame is read
|
||||
//! at its device's pixels a millimetre (96 pixels an inch when the header doesn't say).
|
||||
//! empty or inverted at the units an inch the header gives. Either one that draws in a metric map
|
||||
//! mode (MM_LOMETRIC to MM_TWIPS) is read at 96 pixels an inch, as `craft_metafile` plays those
|
||||
//! modes. An EMF without a usable frame is read at its device's pixels a millimetre (96
|
||||
//! pixels an inch when the header doesn't say).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
@@ -46,11 +49,79 @@ const READER_ITEMS: usize = 200_000;
|
||||
|
||||
/// Points a unit of a WMF without a placeable header (1440 units an inch).
|
||||
const WMF_UNIT: f64 = 72.0 / 1440.0;
|
||||
/// The SAVEDC levels `craft_metafile` keeps; it stops reading at a SAVEDC past them.
|
||||
const SAVED_LEVELS: usize = 1024;
|
||||
/// Points a pixel at 96 pixels an inch.
|
||||
const PT_PER_PX: f64 = 72.0 / 96.0;
|
||||
/// Points a millimetre.
|
||||
const PT_PER_MM: f64 = 72.0 / 25.4;
|
||||
|
||||
/// WMF records that draw and that `craft_metafile` plays: lines, shapes, text, PATBLT and bitmaps.
|
||||
const WMF_DRAWS: [u16; 16] =
|
||||
[0x0213, 0x0324, 0x0325, 0x0418, 0x041B, 0x0521, 0x0538, 0x061C, 0x061D, 0x0817, 0x081A, 0x0830, 0x0940, 0x0A32, 0x0B41, 0x0F43];
|
||||
|
||||
/// Does a WMF draw in a metric map mode (MM_LOMETRIC to MM_TWIPS)? `craft_metafile` plays those
|
||||
/// at 96 pixels an inch, whatever the window. The mode that more drawing records are played in
|
||||
/// decides; a file that draws nothing goes by its last mode. `start` is where the standard header
|
||||
/// begins.
|
||||
fn wmf_metric(bytes: &[u8], start: usize) -> bool {
|
||||
let u16_at = |at: usize| bytes.get(at..at.checked_add(2)?).map(|b| u16::from_le_bytes([b[0], b[1]]));
|
||||
let u32_at = |at: usize| bytes.get(at..at.checked_add(4)?).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
let Some(words) = start.checked_add(2).and_then(u16_at) else { return false };
|
||||
let mut at = start.saturating_add(usize::from(words) * 2);
|
||||
let mut mode = 1;
|
||||
let metric = |mode: u16| (2..=6).contains(&mode);
|
||||
// Drawing records played in a metric mode, and in any other.
|
||||
let (mut in_metric, mut in_other) = (0usize, 0usize);
|
||||
// The modes SAVEDC kept, which RESTOREDC brings back as the reader does.
|
||||
let mut saved: Vec<u16> = Vec::new();
|
||||
// The records as the reader walks them: it stops at the end record, a record shorter than its
|
||||
// header, one that runs past the file and a SAVEDC past its 1,024 levels.
|
||||
while let (Some(words), Some(function)) = (u32_at(at), u16_at(at.saturating_add(4))) {
|
||||
let size = (words as usize).saturating_mul(2);
|
||||
let Some(next) = at.checked_add(size) else { break };
|
||||
if function == 0 || size < 6 || next > bytes.len() {
|
||||
break;
|
||||
}
|
||||
match function {
|
||||
// SETMAPMODE, when the record holds its mode.
|
||||
0x0103 if size >= 8 => mode = u16_at(at + 6).unwrap_or(mode),
|
||||
// SAVEDC.
|
||||
0x001E => {
|
||||
if saved.len() >= SAVED_LEVELS {
|
||||
break;
|
||||
}
|
||||
saved.push(mode);
|
||||
}
|
||||
// RESTOREDC: a negative count pops that many levels, a positive one goes back to that
|
||||
// level. One that names no saved level changes nothing.
|
||||
0x0127 if size >= 8 => {
|
||||
let n = u16_at(at + 6).map_or(0, |v| i32::from(i16::from_le_bytes(v.to_le_bytes())));
|
||||
let level = if n < 0 { saved.len().checked_sub(n.unsigned_abs() as usize) } else { (n as usize).checked_sub(1) };
|
||||
if let Some(level) = level
|
||||
&& let Some(m) = saved.get(level).copied()
|
||||
{
|
||||
mode = m;
|
||||
saved.truncate(level);
|
||||
}
|
||||
}
|
||||
f if WMF_DRAWS.contains(&f) => {
|
||||
if metric(mode) {
|
||||
in_metric += 1;
|
||||
} else {
|
||||
in_other += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
if in_metric == in_other { metric(mode) } else { in_metric > in_other }
|
||||
}
|
||||
|
||||
/// Points a picture unit across and down. `craft_metafile` gives a placeable WMF with a box and an
|
||||
/// EMF with a frame in points, and other files in their logical (WMF) or device (EMF) units.
|
||||
/// EMF with a frame in points, and other files in their logical units (WMF; pixels in a metric map
|
||||
/// mode) or device units (EMF).
|
||||
fn unit(bytes: &[u8], kind: Kind) -> (f64, f64) {
|
||||
let u16_at = |at: usize| bytes.get(at..at.checked_add(2)?).map(|b| u16::from_le_bytes([b[0], b[1]]));
|
||||
let i16_at = |at: usize| bytes.get(at..at.checked_add(2)?).map(|b| i16::from_le_bytes([b[0], b[1]]));
|
||||
@@ -59,15 +130,20 @@ fn unit(bytes: &[u8], kind: Kind) -> (f64, f64) {
|
||||
let positive = |v: Option<i32>| v.filter(|v| *v > 0).map(f64::from);
|
||||
match kind {
|
||||
Kind::Wmf => {
|
||||
if bytes.get(..4) != Some(&PLACEABLE_KEY.to_le_bytes()[..]) {
|
||||
return (WMF_UNIT, WMF_UNIT);
|
||||
}
|
||||
if let (Some(l), Some(t), Some(r), Some(b)) = (i16_at(6), i16_at(8), i16_at(10), i16_at(12))
|
||||
let placeable = bytes.get(..4) == Some(&PLACEABLE_KEY.to_le_bytes()[..]);
|
||||
if placeable
|
||||
&& let (Some(l), Some(t), Some(r), Some(b)) = (i16_at(6), i16_at(8), i16_at(10), i16_at(12))
|
||||
&& r > l
|
||||
&& b > t
|
||||
{
|
||||
return (1.0, 1.0);
|
||||
}
|
||||
if wmf_metric(bytes, if placeable { 22 } else { 0 }) {
|
||||
return (PT_PER_PX, PT_PER_PX);
|
||||
}
|
||||
if !placeable {
|
||||
return (WMF_UNIT, WMF_UNIT);
|
||||
}
|
||||
let k = 72.0 / u16_at(14).filter(|v| *v > 0).map_or(1440.0, f64::from);
|
||||
(k, k)
|
||||
}
|
||||
@@ -85,7 +161,7 @@ fn unit(bytes: &[u8], kind: Kind) -> (f64, f64) {
|
||||
match px_mm {
|
||||
Some(_) if frame => (1.0, 1.0),
|
||||
Some((x, y)) if ok(x) && ok(y) => (PT_PER_MM / x, PT_PER_MM / y),
|
||||
_ => (0.75, 0.75),
|
||||
_ => (PT_PER_PX, PT_PER_PX),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,10 +406,14 @@ impl Mapper {
|
||||
PathEl::MoveTo(p) => Some(*p),
|
||||
_ => None,
|
||||
});
|
||||
// A translucent stroke stays apart, so where strokes overlap each one shows.
|
||||
// A translucent stroke stays apart, so where strokes overlap each one shows. So
|
||||
// does a dashed one, whose dashes start at its own start, and one whose caps
|
||||
// or joins aren't round: a join where two strokes meet is not their two caps.
|
||||
if let Some(p) = &mut self.pending
|
||||
&& p.pen == pen
|
||||
&& pen.color[3] == u8::MAX
|
||||
&& pen.line.dash.is_empty()
|
||||
&& matches!((&pen.line.cap, &pen.line.join), (Cap::Round, Join::Round))
|
||||
&& start.is_some()
|
||||
&& p.path.elements().last().and_then(|e| e.end_point()) == start
|
||||
{
|
||||
|
||||
@@ -349,6 +349,66 @@ fn wmf_without_a_placeable_header_is_read_at_1440_units_an_inch() {
|
||||
assert!(near(p[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 72.0, 72.0), 1e-6), "{:?}", p[0].geometric_bounds());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wmf_without_a_placeable_box_in_a_metric_map_mode_is_read_at_96_pixels_an_inch() {
|
||||
// MM_TWIPS (y up) and a window of 1440 twips square: one inch. The rectangle is the whole window.
|
||||
let records = vec![(0x0103, le16(&[6])), (0x020B, le16(&[0, 0])), (0x020C, le16(&[1440, 1440])), (0x041B, le16(&[1440, 1440, 0, 0]))];
|
||||
let boxed = hand_wmf(1440, 1440, &records);
|
||||
let plain = boxed[22..].to_vec();
|
||||
// The box runs from right to left, so it gives no size.
|
||||
let mut inverted = boxed;
|
||||
inverted[6..8].copy_from_slice(&1440i16.to_le_bytes());
|
||||
inverted[10..12].copy_from_slice(&0i16.to_le_bytes());
|
||||
for bytes in [plain, inverted] {
|
||||
let back = import(&bytes).unwrap();
|
||||
assert!(near(back.document.artboards[0].rect, Rect::new(0.0, 0.0, 72.0, 72.0), 1e-4), "{:?}", back.document.artboards[0].rect);
|
||||
let p = paths(&back.document);
|
||||
assert_eq!(p.len(), 1);
|
||||
assert!(near(p[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 72.0, 72.0), 1e-4), "{:?}", p[0].geometric_bounds());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wmf_whose_metric_map_mode_is_restored_away_is_read_at_1440_units_an_inch() {
|
||||
// SAVEDC, MM_TWIPS, then RESTOREDC back to MM_TEXT, by a count of levels and by a level; the
|
||||
// drawing is then the one in `wmf_without_a_placeable_header_is_read_at_1440_units_an_inch`.
|
||||
for restore in [-1i16, 1] {
|
||||
let records = vec![
|
||||
(0x001E, Vec::new()),
|
||||
(0x0103, le16(&[6])),
|
||||
(0x0127, le16(&[restore])),
|
||||
(0x020B, le16(&[0, 0])),
|
||||
(0x020C, le16(&[1440, 2880])),
|
||||
(0x041B, le16(&[1440, 1440, 0, 0])),
|
||||
];
|
||||
let plain = hand_wmf(2880, 1440, &records)[22..].to_vec();
|
||||
let back = import(&plain).unwrap();
|
||||
assert!(near(back.document.artboards[0].rect, Rect::new(0.0, 0.0, 144.0, 72.0), 1e-6), "{restore}: {:?}", back.document.artboards[0].rect);
|
||||
let p = paths(&back.document);
|
||||
assert_eq!(p.len(), 1);
|
||||
assert!(near(p[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 72.0, 72.0), 1e-6), "{restore}: {:?}", p[0].geometric_bounds());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wmf_that_draws_in_a_metric_map_mode_and_restores_is_read_at_96_pixels_an_inch() {
|
||||
// SAVEDC, MM_TWIPS, a one-inch rectangle, then RESTOREDC back to MM_TEXT with nothing more drawn.
|
||||
let records = vec![
|
||||
(0x001E, Vec::new()),
|
||||
(0x0103, le16(&[6])),
|
||||
(0x020B, le16(&[0, 0])),
|
||||
(0x020C, le16(&[1440, 1440])),
|
||||
(0x041B, le16(&[1440, 1440, 0, 0])),
|
||||
(0x0127, le16(&[-1])),
|
||||
];
|
||||
let plain = hand_wmf(1440, 1440, &records)[22..].to_vec();
|
||||
let back = import(&plain).unwrap();
|
||||
let p = paths(&back.document);
|
||||
assert_eq!(p.len(), 1);
|
||||
let b = p[0].geometric_bounds().unwrap();
|
||||
assert!((b.width() - 72.0).abs() < 1e-4 && (b.height() - 72.0).abs() < 1e-4, "{b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_files_fail_or_import_partly() {
|
||||
let good = emf(&sample()).bytes;
|
||||
@@ -590,6 +650,33 @@ fn translucent_strokes_drawn_one_after_another_stay_apart() {
|
||||
assert_eq!(lines(0xFFFF_0000).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes_join_only_with_round_caps_round_joins_and_no_dashes() {
|
||||
// CREATEPENINDIRECT: a 20-unit black pen of `style`, selected; then two lines one after the
|
||||
// other that meet at a right angle.
|
||||
let lines = |style: i16| {
|
||||
let records = vec![
|
||||
(0x02FA, [le16(&[style, 20, 0]), 0u32.to_le_bytes().to_vec()].concat()),
|
||||
(0x012D, le16(&[0])),
|
||||
(0x0214, le16(&[100, 100])),
|
||||
(0x0213, le16(&[100, 700])),
|
||||
(0x0213, le16(&[700, 700])),
|
||||
];
|
||||
paths(&import(&hand_wmf(1440, 1440, &records)).unwrap().document)
|
||||
};
|
||||
// Round caps and round joins, a solid line: one path, as a round join is the two round caps.
|
||||
assert_eq!(lines(0).len(), 1);
|
||||
// Flat caps and miter joins, flat caps alone and miter joins alone: each line keeps its caps.
|
||||
for style in [0x2200, 0x0200, 0x2000] {
|
||||
let p = lines(style);
|
||||
assert_eq!(p.len(), 2, "style {style:#x}: {p:?}");
|
||||
}
|
||||
// PS_DASH: each line's dashes start at its own start.
|
||||
let p = lines(1);
|
||||
assert_eq!(p.len(), 2, "{p:?}");
|
||||
assert!(p.iter().all(|n| n.appearance.stroke().is_some_and(|st| st.dash.is_some())), "{p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_brushes_fill_with_their_average_color() {
|
||||
// CREATEDIBPATTERNBRUSHPT: a 2 × 2 tile of 24-bit pixels, two red and two blue (bottom-up rows
|
||||
|
||||
Reference in New Issue
Block a user