forked from 1b-agent/opencadstudio-upstream
PDF underlays: attach dialog with pages, underlay tab, layers, import settings and file import, unload on the definition
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c50013ca7
commit
3f6bdf8c59
57 files changed
+5049
-365
No files matched your search
@@ -205,7 +205,7 @@ impl ImageModel {
|
||||
if !u.flags.contains(UnderlayDisplayFlags::ON) {
|
||||
return None;
|
||||
}
|
||||
if !matches!(def.underlay_type, UnderlayType::Pdf) {
|
||||
if !matches!(def.underlay_type, UnderlayType::Pdf) || def.unloaded {
|
||||
return None;
|
||||
}
|
||||
let page = if def.page_name.trim().is_empty() {
|
||||
@@ -214,10 +214,12 @@ impl ImageModel {
|
||||
def.page_name.trim()
|
||||
};
|
||||
let (page_w, page_h) = pdf_raster::page_size_inches(&def.file_path, page)?;
|
||||
let raster = pdf_raster::rasterize_page_display(&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)?;
|
||||
let bg_lum = 0.299 * background[0] + 0.587 * background[1] + 0.114 * background[2];
|
||||
let pixels = pdf_raster::adjusted_pixels(
|
||||
&def.file_path,
|
||||
&source,
|
||||
page,
|
||||
&raster,
|
||||
PageAdjust {
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod image_model;
|
||||
pub mod instance_model;
|
||||
pub mod material_model;
|
||||
pub mod ole_pres;
|
||||
pub mod pdf_layers;
|
||||
pub mod pdf_raster;
|
||||
pub mod pdf_vector;
|
||||
pub mod mesh_model;
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
//! PDF layers (optional-content groups): what a file declares, and a source
|
||||
//! whose layer visibility is overridden for an underlay.
|
||||
//!
|
||||
//! The override is an incremental update that replaces the catalog's default
|
||||
//! optional-content configuration, so the rasteriser and the vector reader
|
||||
//! both skip the hidden layers without any change to how they read a page.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use hayro::hayro_syntax::object::{Array, Dict, Name, ObjectIdentifier};
|
||||
use hayro::hayro_syntax::Pdf;
|
||||
|
||||
use super::pdf_raster::{register_derived_source, source_bytes};
|
||||
|
||||
/// An optional-content group (layer) of a PDF.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PdfLayer {
|
||||
pub name: String,
|
||||
/// Shown by the file's default configuration.
|
||||
pub on_by_default: bool,
|
||||
object: (i32, i32),
|
||||
}
|
||||
|
||||
fn text_string(bytes: &[u8]) -> String {
|
||||
if bytes.starts_with(&[0xFE, 0xFF]) {
|
||||
let units: Vec<u16> = bytes[2..]
|
||||
.chunks_exact(2)
|
||||
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||||
.collect();
|
||||
return String::from_utf16_lossy(&units);
|
||||
}
|
||||
bytes.iter().map(|&b| b as char).collect()
|
||||
}
|
||||
|
||||
/// The layers a PDF declares, in file order.
|
||||
pub fn layers(path: &str) -> Vec<PdfLayer> {
|
||||
let Some(bytes) = source_bytes(path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(pdf) = Pdf::new(bytes) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let xref = pdf.xref();
|
||||
let Some(catalog) = xref.get::<Dict<'_>>(xref.root_id()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(props) = catalog.get::<Dict<'_>>(b"OCProperties") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let config = props.get::<Dict<'_>>(b"D").unwrap_or_default();
|
||||
let ids = |key: &[u8]| -> Vec<(i32, i32)> {
|
||||
config
|
||||
.get::<Array<'_>>(key)
|
||||
.map(|a| {
|
||||
a.raw_iter()
|
||||
.filter_map(|item| item.as_obj_ref())
|
||||
.map(|r| (r.obj_number, r.gen_number))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let base_off = config
|
||||
.get::<Name<'_>>(b"BaseState")
|
||||
.is_some_and(|n| n.as_ref() == b"OFF");
|
||||
let (on, off) = (ids(b"ON"), ids(b"OFF"));
|
||||
let Some(ocgs) = props.get::<Array<'_>>(b"OCGs") else {
|
||||
return Vec::new();
|
||||
};
|
||||
ocgs.raw_iter()
|
||||
.filter_map(|item| item.as_obj_ref())
|
||||
.filter_map(|r| {
|
||||
let id = (r.obj_number, r.gen_number);
|
||||
let dict = xref.get::<Dict<'_>>(ObjectIdentifier::from(r))?;
|
||||
let name = dict
|
||||
.get::<hayro::hayro_syntax::object::String<'_>>(b"Name")
|
||||
.map(|s| text_string(s.as_bytes()))
|
||||
.unwrap_or_default();
|
||||
let on_by_default = if base_off {
|
||||
on.contains(&id)
|
||||
} else {
|
||||
!off.contains(&id)
|
||||
};
|
||||
Some(PdfLayer {
|
||||
name,
|
||||
on_by_default,
|
||||
object: id,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A source whose `hidden` layers are off and every other layer on: the
|
||||
/// original path when that is the file's own state, else a derived source
|
||||
/// name registered with the updated bytes. Falls back to the original path
|
||||
/// when the file cannot be updated (encrypted, unreadable catalog).
|
||||
pub fn source_with_layers(path: &str, hidden: &[String]) -> String {
|
||||
let all = layers(path);
|
||||
if all.is_empty() {
|
||||
return path.to_string();
|
||||
}
|
||||
let off: Vec<&PdfLayer> = all
|
||||
.iter()
|
||||
.filter(|l| hidden.iter().any(|h| *h == l.name))
|
||||
.collect();
|
||||
if off.is_empty() && all.iter().all(|l| l.on_by_default) {
|
||||
return path.to_string();
|
||||
}
|
||||
let mut names: Vec<&str> = off.iter().map(|l| l.name.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
let key = format!("{path}#layers-off={}", names.join("|"));
|
||||
if source_bytes(&key).is_some() {
|
||||
return key;
|
||||
}
|
||||
let Some(updated) = source_bytes(path).and_then(|bytes| with_layer_config(&bytes, &all, &off))
|
||||
else {
|
||||
return path.to_string();
|
||||
};
|
||||
register_derived_source(&key, Arc::new(updated));
|
||||
key
|
||||
}
|
||||
|
||||
/// The bytes with an incremental update whose catalog lists `off` layers
|
||||
/// as off in the default configuration and all others on.
|
||||
fn with_layer_config(bytes: &[u8], all: &[PdfLayer], off: &[&PdfLayer]) -> Option<Vec<u8>> {
|
||||
let pdf = Pdf::new(Arc::new(bytes.to_vec())).ok()?;
|
||||
let xref = pdf.xref();
|
||||
let root = xref.root_id();
|
||||
let catalog = xref.get::<Dict<'_>>(root)?;
|
||||
let raw = std::str::from_utf8(catalog.data()).ok()?.trim().to_string();
|
||||
let tail = String::from_utf8_lossy(&bytes[bytes.len().saturating_sub(4096)..]).into_owned();
|
||||
if tail.contains("/Encrypt") {
|
||||
return None;
|
||||
}
|
||||
let prev: usize = tail
|
||||
.rfind("startxref")
|
||||
.and_then(|i| tail[i + 9..].split_whitespace().next()?.parse().ok())?;
|
||||
let size: usize = {
|
||||
let region = String::from_utf8_lossy(&bytes[prev.min(bytes.len())..]).into_owned();
|
||||
let region = if region.contains("/Size") { region } else { tail.clone() };
|
||||
let i = region.find("/Size")?;
|
||||
region[i + 5..]
|
||||
.trim_start()
|
||||
.split(|c: char| !c.is_ascii_digit())
|
||||
.next()?
|
||||
.parse()
|
||||
.ok()?
|
||||
};
|
||||
let body = without_key(&raw, "/OCProperties")?;
|
||||
let refs = |layers: Vec<&PdfLayer>| {
|
||||
layers
|
||||
.iter()
|
||||
.map(|l| format!("{} {} R", l.object.0, l.object.1))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
};
|
||||
let props = format!(
|
||||
"/OCProperties << /OCGs [{}] /D << /BaseState /ON /OFF [{}] >> >>",
|
||||
refs(all.iter().collect()),
|
||||
refs(off.to_vec()),
|
||||
);
|
||||
let open = body.trim_end().strip_suffix(">>")?.trim_end();
|
||||
let catalog_text = format!("{open} {props} >>");
|
||||
let mut out = bytes.to_vec();
|
||||
if !out.ends_with(b"\n") {
|
||||
out.push(b'\n');
|
||||
}
|
||||
let object_offset = out.len();
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"{} {} obj\n{catalog_text}\nendobj\n",
|
||||
root.obj_number, root.gen_number
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
let xref_offset = out.len();
|
||||
out.extend_from_slice(
|
||||
format!(
|
||||
"xref\n0 1\n0000000000 65535 f \n{} 1\n{:010} {:05} n \ntrailer\n<< /Size {size} /Root {} {} R /Prev {prev} >>\nstartxref\n{xref_offset}\n%%EOF\n",
|
||||
root.obj_number, object_offset, root.gen_number, root.obj_number, root.gen_number,
|
||||
)
|
||||
.as_bytes(),
|
||||
);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A dictionary's text without one key and its value (a dictionary, an
|
||||
/// array, a reference or a single token).
|
||||
fn without_key(dict: &str, key: &str) -> Option<String> {
|
||||
let Some(start) = dict.find(key) else {
|
||||
return Some(dict.to_string());
|
||||
};
|
||||
let rest = &dict[start + key.len()..];
|
||||
let value = rest.trim_start();
|
||||
let lead = rest.len() - value.len();
|
||||
let length = if value.starts_with("<<") || value.starts_with('[') {
|
||||
let (open, close) = if value.starts_with('[') { ("[", "]") } else { ("<<", ">>") };
|
||||
let mut depth = 0i32;
|
||||
let mut i = 0;
|
||||
loop {
|
||||
if i >= value.len() {
|
||||
return None;
|
||||
}
|
||||
if value[i..].starts_with(open) {
|
||||
depth += 1;
|
||||
i += open.len();
|
||||
} else if value[i..].starts_with(close) {
|
||||
depth -= 1;
|
||||
i += close.len();
|
||||
if depth == 0 {
|
||||
break i;
|
||||
}
|
||||
} else {
|
||||
i += value[i..].chars().next()?.len_utf8();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let parts: Vec<&str> = value.split_whitespace().take(3).collect();
|
||||
if parts.len() == 3 && parts[2].starts_with('R') {
|
||||
value.find('R')? + 1
|
||||
} else {
|
||||
value
|
||||
.find(|c: char| c.is_whitespace() || c == '/' || c == '>')
|
||||
.unwrap_or(value.len())
|
||||
}
|
||||
};
|
||||
Some(format!("{}{}", &dict[..start], &rest[lead + length..]))
|
||||
}
|
||||
|
||||
/// Layer names an underlay has switched off: the strings of its
|
||||
/// `AdeskUnderlayLayerOverrideData` extended data, as the reference stores
|
||||
/// them.
|
||||
pub const LAYER_OVERRIDE_APP: &str = "AdeskUnderlayLayerOverrideData";
|
||||
|
||||
pub fn hidden_layers(u: &acadrust::entities::Underlay) -> Vec<String> {
|
||||
u.common
|
||||
.extended_data
|
||||
.records()
|
||||
.iter()
|
||||
.filter(|r| r.application_name.eq_ignore_ascii_case(LAYER_OVERRIDE_APP))
|
||||
.flat_map(|r| r.values.iter())
|
||||
.filter_map(|v| match v {
|
||||
acadrust::xdata::XDataValue::String(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The source an underlay's page is read from, with its layer overrides.
|
||||
pub fn underlay_source(u: &acadrust::entities::Underlay, file_path: &str) -> String {
|
||||
let hidden = hidden_layers(u);
|
||||
if hidden.is_empty() {
|
||||
return file_path.to_string();
|
||||
}
|
||||
source_with_layers(file_path, &hidden)
|
||||
}
|
||||
@@ -48,6 +48,15 @@ pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
||||
.retain(|key, _| key.0 != path);
|
||||
}
|
||||
|
||||
/// Register bytes under a derived source name (a layer override of a
|
||||
/// file), leaving the file's own caches alone.
|
||||
pub(crate) fn register_derived_source(key: &str, bytes: Arc<Vec<u8>>) {
|
||||
source_cache()
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
.insert(key.to_string(), bytes);
|
||||
}
|
||||
|
||||
/// Rasterise a 1-based PDF page, memoised by source path and page name.
|
||||
pub fn rasterize_page(path: &str, page: &str) -> Option<Arc<PdfPage>> {
|
||||
rasterize_page_at_dpi(path, page, RASTER_DPI)
|
||||
@@ -134,6 +143,46 @@ fn unpremultiply(pixels: &mut [u8]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_hsl([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let l = (max + min) / 2.0;
|
||||
let d = max - min;
|
||||
if d <= f32::EPSILON {
|
||||
return (0.0, 0.0, l);
|
||||
}
|
||||
let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
|
||||
let h = if max == r {
|
||||
((g - b) / d).rem_euclid(6.0)
|
||||
} else if max == g {
|
||||
(b - r) / d + 2.0
|
||||
} else {
|
||||
(r - g) / d + 4.0
|
||||
} / 6.0;
|
||||
(h, s, l)
|
||||
}
|
||||
|
||||
fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] {
|
||||
if s <= f32::EPSILON {
|
||||
return [l; 3];
|
||||
}
|
||||
let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
|
||||
let p = 2.0 * l - q;
|
||||
let channel = |t: f32| {
|
||||
let t = t.rem_euclid(1.0);
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 0.5 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
}
|
||||
};
|
||||
[channel(h + 1.0 / 3.0), channel(h), channel(h - 1.0 / 3.0)]
|
||||
}
|
||||
|
||||
/// Number of pages in a PDF source.
|
||||
pub fn page_count(path: &str) -> Option<usize> {
|
||||
let bytes = source_bytes(path)?;
|
||||
@@ -155,7 +204,8 @@ pub fn page_size_inches(path: &str, page: &str) -> Option<(f64, f64)> {
|
||||
/// Display adjustments of an underlay page, applied to its display raster.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct PageAdjust {
|
||||
/// 0..=100; 100 keeps the colours, lower values pull them to mid grey.
|
||||
/// 0..=100; 100 keeps the colours, lower values pull their lightness
|
||||
/// towards one third.
|
||||
pub contrast: u8,
|
||||
pub monochrome: bool,
|
||||
/// Lighten dark, unsaturated content on a dark background (black lines
|
||||
@@ -198,20 +248,27 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
||||
rgb = [l; 3];
|
||||
}
|
||||
if adjust.adjust_for_background {
|
||||
// Content that would vanish into the background — near-black on a
|
||||
// dark background, near-white on a light one, without much
|
||||
// colour — turns to its opposite; coloured and mid-tone content
|
||||
// stays as drawn.
|
||||
let max = rgb[0].max(rgb[1]).max(rgb[2]);
|
||||
let min = rgb[0].min(rgb[1]).min(rgb[2]);
|
||||
let saturation = if max > 0.0 { (max - min) / max } else { 0.0 };
|
||||
let l = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
||||
// ponytail: greyscale inversion of low-saturation content only;
|
||||
// hue-preserving remap if coloured content must adapt too.
|
||||
let clash = if adjust.dark_background { l < 0.5 } else { l > 0.5 };
|
||||
// ponytail: fixed 0.2 / 0.8 thresholds measured on black, white
|
||||
// and 30 % grey; a contrast-to-background test if mid tones clash.
|
||||
let clash = if adjust.dark_background { l < 0.2 } else { l > 0.8 };
|
||||
if saturation < 0.25 && clash {
|
||||
rgb = rgb.map(|c| 1.0 - c);
|
||||
}
|
||||
}
|
||||
if adjust.contrast < 100 {
|
||||
// Lightness is pulled towards one third, hue and saturation kept:
|
||||
// contrast 0 leaves every colour at a third of full lightness.
|
||||
let (h, s, l) = rgb_to_hsl(rgb);
|
||||
let k = adjust.contrast as f32 / 100.0;
|
||||
rgb = rgb.map(|c| 0.5 + (c - 0.5) * k);
|
||||
rgb = hsl_to_rgb(h, s, 1.0 / 3.0 + (l - 1.0 / 3.0) * k);
|
||||
}
|
||||
for (dst, c) in px[..3].iter_mut().zip(rgb) {
|
||||
*dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
|
||||
@@ -37,13 +37,13 @@ impl Segment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SubPath {
|
||||
pub segments: Vec<Segment>,
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PdfPath {
|
||||
pub subpaths: Vec<SubPath>,
|
||||
/// Stroke colour (RGB) and width in points, for stroked paths.
|
||||
@@ -53,7 +53,7 @@ pub struct PdfPath {
|
||||
}
|
||||
|
||||
/// A run of glyphs on one baseline in one font and size.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PdfText {
|
||||
pub text: String,
|
||||
/// Base font name without a subset prefix ("Helvetica").
|
||||
@@ -69,10 +69,26 @@ pub struct PdfText {
|
||||
pub color: [u8; 3],
|
||||
}
|
||||
|
||||
/// A raster image drawn on a page: its pixels and where they land.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PdfImage {
|
||||
/// Straight RGBA, rows from the top.
|
||||
pub rgba: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Lower-left corner, page inches.
|
||||
pub origin: [f64; 2],
|
||||
/// One pixel along the image's rows and up its columns, inches.
|
||||
pub u: [f64; 2],
|
||||
pub v: [f64; 2],
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct PageVectors {
|
||||
pub paths: Vec<PdfPath>,
|
||||
pub texts: Vec<PdfText>,
|
||||
/// Filled only when images are asked for (`page_content`).
|
||||
pub images: Vec<PdfImage>,
|
||||
}
|
||||
|
||||
type Key = (String, String);
|
||||
@@ -101,7 +117,66 @@ pub fn page_vectors(path: &str, page: &str) -> Option<Arc<PageVectors>> {
|
||||
built
|
||||
}
|
||||
|
||||
/// A page's paths, text and raster images (not cached; import only).
|
||||
pub fn page_content(path: &str, page: &str) -> Option<PageVectors> {
|
||||
read_page_with(path, page, true)
|
||||
}
|
||||
|
||||
/// A page's content split by PDF layer: `None` for what no layer holds,
|
||||
/// then each named layer that is not in `hidden`. Each layer's share is
|
||||
/// what the page shows with that layer alone on, less what it shows with
|
||||
/// every layer off.
|
||||
// ponytail: content in several layers at once (membership dictionaries) is
|
||||
// credited to the first layer that shows it.
|
||||
pub fn page_content_by_layer(
|
||||
path: &str,
|
||||
page: &str,
|
||||
hidden: &[String],
|
||||
) -> Option<Vec<(Option<String>, PageVectors)>> {
|
||||
let layers = super::pdf_layers::layers(path);
|
||||
if layers.is_empty() {
|
||||
return Some(vec![(None, page_content(path, page)?)]);
|
||||
}
|
||||
let names: Vec<String> = layers.iter().map(|l| l.name.clone()).collect();
|
||||
let base = page_content(&super::pdf_layers::source_with_layers(path, &names), page)?;
|
||||
let key = |s: &dyn std::fmt::Debug| format!("{s:?}");
|
||||
let mut seen: HashMap<String, usize> = HashMap::new();
|
||||
for item in base.paths.iter().map(|p| key(p)).chain(base.texts.iter().map(|t| key(t))) {
|
||||
*seen.entry(item).or_default() += 1;
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for layer in &layers {
|
||||
if hidden.contains(&layer.name) {
|
||||
continue;
|
||||
}
|
||||
let others: Vec<String> = names.iter().filter(|n| **n != layer.name).cloned().collect();
|
||||
let Some(mut only) =
|
||||
page_content(&super::pdf_layers::source_with_layers(path, &others), page)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut left = seen.clone();
|
||||
let mut fresh = |item: String| match left.get_mut(&item) {
|
||||
Some(n) if *n > 0 => {
|
||||
*n -= 1;
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
only.paths.retain(|p| fresh(key(p)));
|
||||
only.texts.retain(|t| fresh(key(t)));
|
||||
only.images.retain(|i| !base.images.contains(i));
|
||||
out.push((Some(layer.name.clone()), only));
|
||||
}
|
||||
out.insert(0, (None, base));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
||||
read_page_with(path, page, false)
|
||||
}
|
||||
|
||||
fn read_page_with(path: &str, page: &str, images: bool) -> Option<PageVectors> {
|
||||
let bytes = super::pdf_raster::source_bytes(path)?;
|
||||
let pdf = Pdf::new(bytes).ok()?;
|
||||
let page_no = page.trim().parse::<usize>().ok()?;
|
||||
@@ -117,7 +192,10 @@ fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
||||
page.xref(),
|
||||
InterpreterSettings::default(),
|
||||
);
|
||||
let mut device = Collector::default();
|
||||
let mut device = Collector {
|
||||
want_images: images,
|
||||
..Collector::default()
|
||||
};
|
||||
interpret_page(page, &mut context, &mut device);
|
||||
device.finish_text();
|
||||
|
||||
@@ -150,6 +228,7 @@ fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
||||
Some(PageVectors {
|
||||
paths: device.paths,
|
||||
texts: texts.into_iter().map(|t| t.text).collect(),
|
||||
images: device.images,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -169,6 +248,8 @@ struct Collector {
|
||||
texts: Vec<PendingText>,
|
||||
current: Option<PendingText>,
|
||||
font_order: Vec<u128>,
|
||||
want_images: bool,
|
||||
images: Vec<PdfImage>,
|
||||
}
|
||||
|
||||
impl Collector {
|
||||
@@ -355,11 +436,104 @@ impl<'a> Device<'a> for Collector {
|
||||
.sqrt();
|
||||
}
|
||||
}
|
||||
fn draw_image(&mut self, _: Image<'a, '_>, _: Affine) {}
|
||||
fn draw_image(&mut self, image: Image<'a, '_>, transform: Affine) {
|
||||
let Image::Raster(raster) = image else {
|
||||
return;
|
||||
};
|
||||
if !self.want_images {
|
||||
return;
|
||||
}
|
||||
let mut decoded: Option<PdfImage> = None;
|
||||
raster.with_rgba(
|
||||
|data, alpha| {
|
||||
use hayro::hayro_interpret::ImageData;
|
||||
let (w, h) = (data.width(), data.height());
|
||||
let n = (w * h) as usize;
|
||||
let mut rgba = vec![255u8; n * 4];
|
||||
match &data {
|
||||
ImageData::Rgb(rgb) => {
|
||||
for i in 0..n {
|
||||
rgba[i * 4..i * 4 + 3].copy_from_slice(&rgb.data[i * 3..i * 3 + 3]);
|
||||
}
|
||||
}
|
||||
ImageData::Luma(luma) => {
|
||||
for i in 0..n {
|
||||
rgba[i * 4..i * 4 + 3].fill(luma.data[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(a) = alpha.filter(|a| a.width == w && a.height == h) {
|
||||
for i in 0..n {
|
||||
rgba[i * 4 + 3] = a.data[i];
|
||||
}
|
||||
}
|
||||
// The transform maps pixel space (y down) onto the page.
|
||||
let at = |x: f64, y: f64| pt(transform * Point::new(x, y));
|
||||
let (wf, hf) = (w as f64, h as f64);
|
||||
let (o, x1, y1) = (at(0.0, hf), at(wf, hf), at(0.0, 0.0));
|
||||
decoded = Some(PdfImage {
|
||||
rgba,
|
||||
width: w,
|
||||
height: h,
|
||||
origin: o,
|
||||
u: [(x1[0] - o[0]) / w as f64, (x1[1] - o[1]) / w as f64],
|
||||
v: [(y1[0] - o[0]) / h as f64, (y1[1] - o[1]) / h as f64],
|
||||
});
|
||||
},
|
||||
None,
|
||||
);
|
||||
self.images.extend(decoded);
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
fn pop_transparency_group(&mut self) {}
|
||||
}
|
||||
|
||||
/// A point of a segment at parameter `t` (0..1).
|
||||
pub fn bezier(seg: &Segment, t: f64) -> [f64; 2] {
|
||||
match seg {
|
||||
Segment::Line(a, b) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
|
||||
Segment::Cubic(p0, p1, p2, p3) => {
|
||||
let u = 1.0 - t;
|
||||
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
|
||||
[
|
||||
a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0],
|
||||
a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1],
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Centre and radius when a closed loop of cubics traces a circle.
|
||||
pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> {
|
||||
if !sp.closed || sp.segments.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
let cubics: Vec<&Segment> = sp
|
||||
.segments
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Segment::Cubic(..)))
|
||||
.collect();
|
||||
if cubics.len() != sp.segments.len() && sp.segments.len() - cubics.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
let starts: Vec<[f64; 2]> = cubics.iter().map(|s| s.start()).collect();
|
||||
let n = starts.len() as f64;
|
||||
let c = [
|
||||
starts.iter().map(|p| p[0]).sum::<f64>() / n,
|
||||
starts.iter().map(|p| p[1]).sum::<f64>() / n,
|
||||
];
|
||||
let dist = |p: [f64; 2]| ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2)).sqrt();
|
||||
let r = starts.iter().map(|p| dist(*p)).sum::<f64>() / n;
|
||||
if r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let tol = r * 2e-3;
|
||||
let on_circle = cubics.iter().all(|s| {
|
||||
[0.0, 0.25, 0.5, 0.75].iter().all(|t| (dist(bezier(s, *t)) - r).abs() <= tol)
|
||||
});
|
||||
on_circle.then_some((c, r))
|
||||
}
|
||||
|
||||
// ── Snapping ────────────────────────────────────────────────────────────────
|
||||
|
||||
static PDF_OSNAP: AtomicBool = AtomicBool::new(true);
|
||||
@@ -391,7 +565,8 @@ pub fn underlay_snap_points(
|
||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(vectors) = page_vectors(&def.file_path, crate::entities::underlay::page_of(def)) else {
|
||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
||||
let Some(vectors) = page_vectors(&source, crate::entities::underlay::page_of(def)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let world = |p: [f64; 2]| {
|
||||
@@ -401,6 +576,13 @@ pub fn underlay_snap_points(
|
||||
let mut out = Vec::new();
|
||||
'paths: for path in &vectors.paths {
|
||||
for sp in &path.subpaths {
|
||||
// A PDF circle offers its centre and quadrants, as a circle does.
|
||||
if let Some((c, r)) = circle_of(sp) {
|
||||
out.push((world(c), SnapHint::Center));
|
||||
for (dx, dy) in [(r, 0.0), (0.0, r), (-r, 0.0), (0.0, -r)] {
|
||||
out.push((world([c[0] + dx, c[1] + dy]), SnapHint::Quadrant));
|
||||
}
|
||||
}
|
||||
for seg in &sp.segments {
|
||||
if out.len() >= MAX_SNAP_POINTS {
|
||||
break 'paths;
|
||||
@@ -418,3 +600,49 @@ pub fn underlay_snap_points(
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The underlay's PDF geometry as world polylines (NaN-separated, curves
|
||||
/// flattened) for nearest, intersection and perpendicular snaps. Empty when
|
||||
/// PDFOSNAP is off or the page is not shown.
|
||||
pub fn underlay_snap_geometry(
|
||||
u: &acadrust::entities::Underlay,
|
||||
document: &acadrust::CadDocument,
|
||||
) -> Vec<[f64; 3]> {
|
||||
if !pdf_osnap() || !u.flags.contains(acadrust::entities::UnderlayDisplayFlags::ON) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(def) = crate::entities::underlay::definition(u, document) else {
|
||||
return Vec::new();
|
||||
};
|
||||
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 {
|
||||
return Vec::new();
|
||||
};
|
||||
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
|
||||
let mut out: Vec<[f64; 3]> = Vec::new();
|
||||
for path in &vectors.paths {
|
||||
for sp in &path.subpaths {
|
||||
if out.len() >= MAX_GEOMETRY_POINTS {
|
||||
return out;
|
||||
}
|
||||
if !out.is_empty() {
|
||||
out.push([f64::NAN; 3]);
|
||||
}
|
||||
out.push(world(sp.segments[0].start()));
|
||||
for seg in &sp.segments {
|
||||
let steps = if matches!(seg, Segment::Cubic(..)) { 12 } else { 1 };
|
||||
for k in 1..=steps {
|
||||
out.push(world(bezier(seg, k as f64 / steps as f64)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in new issue
Block a user