use crate::scaling::DucDataScaler;
use crate::streaming::pdf_linear::PdfLinearRenderer;
use crate::streaming::stream_resources::ResourceStreamer;
use crate::utils::freedraw_bounds::FreeDrawBounds;
use crate::utils::style_resolver::{ResolvedStyles, StyleResolver};
use crate::{ConversionError, ConversionResult};
use bigcolor::BigColor;
use duc::types::{
DucArrowElement, DucBlockDuplicationArray, DucBlockInstance, DucDocElement, DucElementEnum,
DucEllipseElement, DucFrameElement, DucFreeDrawElement, DucImageElement, DucLine,
DucLineReference, DucLinearElement, DucLinearElementBase, DucModelElement, DucPath,
DucPdfElement, DucPlotElement, DucPoint, DucPolygonElement, DucRectangleElement,
DucTableElement, DucTextElement, ElementBackground, ElementContentBase, ElementWrapper,
GeometricPoint, BEZIER_MIRRORING, ELEMENT_CONTENT_PREFERENCE, STROKE_CAP, STROKE_JOIN,
};
use hipdf::embed_pdf::PdfEmbedder;
use hipdf::fonts::Font;
use hipdf::hatching::HatchingManager;
use hipdf::images::{Image, ImageManager};
use hipdf::lopdf::content::Operation;
use hipdf::lopdf::{Dictionary, Document, Object};
use hipdf::ocg::OCGManager;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::f64::consts::PI;
use wasm_bindgen::JsValue;
const DUC_STANDARD_PRIMARY_COLOR: &str = "oklch(62% 0.15 281)";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OpacityKey {
stroke_thousandths: u16,
fill_thousandths: u16,
}
#[derive(Debug, Clone, Copy)]
struct StyleProfile {
use_background_fill: bool,
fill_from_stroke: bool,
apply_stroke_properties: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StreamMode {
Crop,
Plot,
}
pub struct ElementStreamer {
style_resolver: StyleResolver,
page_height: f64,
visible_scene_rect: (f64, f64, f64, f64),
page_origin: (f64, f64),
page_translation: (f64, f64),
resource_cache: HashMap<String, String>, images: HashMap<String, u32>, new_xobjects: Vec<(String, Object)>,
embedded_pdfs: HashMap<String, u32>, freedraw_bboxes: HashMap<String, FreeDrawBounds>, svg_dimensions: HashMap<String, (f64, f64)>, font_resource_name: String,
text_font: Font,
font_map: HashMap<String, (Font, String)>,
block_instances: HashMap<String, DucBlockInstance>,
group_cell_pitches: HashMap<String, (f64, f64)>,
render_only_plot_elements: bool,
ext_gstate_cache: HashMap<OpacityKey, String>,
ext_gstate_definitions: HashMap<String, Dictionary>,
current_page_ext_gstates: BTreeSet<String>,
current_mode: StreamMode,
current_plot_id: Option<String>,
allowed_element_ids: Option<HashSet<String>>,
}
impl ElementStreamer {
pub fn new(
style_resolver: StyleResolver,
page_height: f64,
font_resource_name: String,
text_font: Font,
block_instances: HashMap<String, DucBlockInstance>,
font_map: HashMap<String, (Font, String)>,
) -> Self {
Self {
style_resolver,
page_height,
visible_scene_rect: (0.0, 0.0, 0.0, 0.0),
page_origin: (0.0, 0.0),
page_translation: (0.0, 0.0),
resource_cache: HashMap::new(),
images: HashMap::new(),
new_xobjects: Vec::new(),
embedded_pdfs: HashMap::new(),
freedraw_bboxes: HashMap::new(),
svg_dimensions: HashMap::new(),
font_resource_name,
text_font,
font_map,
block_instances,
group_cell_pitches: HashMap::new(),
render_only_plot_elements: false,
ext_gstate_cache: HashMap::new(),
ext_gstate_definitions: HashMap::new(),
current_page_ext_gstates: BTreeSet::new(),
current_mode: StreamMode::Crop,
current_plot_id: None,
allowed_element_ids: None,
}
}
pub fn get_duplication_offsets(
duplication_array: &DucBlockDuplicationArray,
cell_width: f64,
cell_height: f64,
) -> Vec<(f64, f64)> {
if duplication_array.row_spacing.is_nan() || duplication_array.col_spacing.is_nan() {
log::warn!(
"Duplication array has NaN spacing! row_spacing: {}, col_spacing: {}",
duplication_array.row_spacing,
duplication_array.col_spacing
);
}
let rows = duplication_array.rows.max(1) as usize;
let cols = duplication_array.cols.max(1) as usize;
let row_spacing = duplication_array.row_spacing;
let col_spacing = duplication_array.col_spacing;
let stride_x = cell_width + col_spacing;
let stride_y = cell_height + row_spacing;
let mut offsets = Vec::with_capacity(rows * cols);
for row in 0..rows {
for col in 0..cols {
let x_offset = col as f64 * stride_x;
let y_offset = row as f64 * stride_y;
offsets.push((x_offset, y_offset));
}
}
offsets
}
fn compute_cell_dimensions(
total_width: f64,
total_height: f64,
dup_array: &DucBlockDuplicationArray,
) -> (f64, f64) {
let cols = dup_array.cols.max(1) as f64;
let rows = dup_array.rows.max(1) as f64;
let cell_width = (total_width - (cols - 1.0) * dup_array.col_spacing) / cols;
let cell_height = (total_height - (rows - 1.0) * dup_array.row_spacing) / rows;
(cell_width.max(0.0), cell_height.max(0.0))
}
fn rotate_point_around_center(point: (f64, f64), center: (f64, f64), angle: f64) -> (f64, f64) {
if angle == 0.0 {
return point;
}
let cos = angle.cos();
let sin = angle.sin();
let dx = point.0 - center.0;
let dy = point.1 - center.1;
(
center.0 + dx * cos - dy * sin,
center.1 + dx * sin + dy * cos,
)
}
fn compute_linear_absolute_visual_bounds(
linear_base: &duc::types::DucLinearElementBase,
) -> Option<(f64, f64, f64, f64)> {
if linear_base.points.is_empty() {
return None;
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for point in &linear_base.points {
min_x = min_x.min(point.x);
min_y = min_y.min(point.y);
max_x = max_x.max(point.x);
max_y = max_y.max(point.y);
}
for line in &linear_base.lines {
if let Some(handle) = &line.start.handle {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y);
}
if let Some(handle) = &line.end.handle {
min_x = min_x.min(handle.x);
min_y = min_y.min(handle.y);
max_x = max_x.max(handle.x);
max_y = max_y.max(handle.y);
}
}
let stroke_width = linear_base
.base
.styles
.stroke
.first()
.map(|stroke| stroke.width)
.unwrap_or(0.0);
let stroke_offset = stroke_width / 2.0;
Some((
linear_base.base.x + min_x - stroke_offset,
linear_base.base.y + min_y - stroke_offset,
linear_base.base.x + max_x + stroke_offset,
linear_base.base.y + max_y + stroke_offset,
))
}
fn get_duplication_footprint_coords(
&self,
element: &DucElementEnum,
_duplication_array: Option<&DucBlockDuplicationArray>,
) -> (f64, f64, f64, f64, f64, f64) {
let base = Self::get_element_base(element);
let bx1 = base.x.min(base.x + base.width.abs());
let by1 = base.y.min(base.y + base.height.abs());
let footprint_width = base.width.abs();
let footprint_height = base.height.abs();
let x1 = bx1;
let y1 = by1;
let x2 = bx1 + footprint_width;
let y2 = by1 + footprint_height;
let cx = (x1 + x2) / 2.0;
let cy = (y1 + y2) / 2.0;
(x1, y1, x2, y2, cx, cy)
}
fn compute_element_visual_bounds(element: &DucElementEnum) -> (f64, f64, f64, f64) {
if let DucElementEnum::DucLinearElement(l) = element {
if let Some(bounds) = Self::compute_linear_absolute_visual_bounds(&l.linear_base) {
return bounds;
}
}
let base = Self::get_element_base(element);
let x = base.x;
let y = base.y;
let w = base.width.abs();
let h = base.height.abs();
(x, y, x + w, y + h)
}
pub fn precompute_group_cell_pitches(&mut self, elements: &[ElementWrapper]) {
self.group_cell_pitches.clear();
let mut instance_elements: HashMap<String, Vec<&DucElementEnum>> = HashMap::new();
for ew in elements {
let base = Self::get_element_base(&ew.element);
if let Some(instance_id) = &base.instance_id {
if base.is_deleted || !base.is_visible {
continue;
}
instance_elements
.entry(instance_id.clone())
.or_default()
.push(&ew.element);
}
}
for (instance_id, elems) in &instance_elements {
if elems.len() <= 1 {
continue;
}
let Some(block_instance) = self.block_instances.get(instance_id) else {
continue;
};
let Some(dup_array) = &block_instance.duplication_array else {
continue;
};
if dup_array.rows <= 1 && dup_array.cols <= 1 {
continue;
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for elem in elems {
let renderable = self.get_renderable_duplication_element(elem);
let (bx1, by1, bx2, by2) = Self::compute_element_visual_bounds(&renderable);
min_x = min_x.min(bx1);
min_y = min_y.min(by1);
max_x = max_x.max(bx2);
max_y = max_y.max(by2);
}
let group_cell_width = max_x - min_x;
let group_cell_height = max_y - min_y;
if group_cell_width > 0.0 && group_cell_height > 0.0 {
self.group_cell_pitches
.insert(instance_id.clone(), (group_cell_width, group_cell_height));
}
}
}
pub fn get_element_duplication_offsets(
&self,
element: &DucElementEnum,
) -> Option<Vec<(f64, f64)>> {
let base = Self::get_element_base(element);
let instance_id = base.instance_id.as_ref()?;
if let Some(block_instance) = self.block_instances.get(instance_id) {
if let Some(dup_array) = &block_instance.duplication_array {
if dup_array.rows > 1 || dup_array.cols > 1 {
let (total_width, total_height) = Self::extract_element_dimensions(element);
let (elem_cell_width, elem_cell_height) =
Self::compute_cell_dimensions(total_width, total_height, dup_array);
let (pitch_w, pitch_h) = self
.group_cell_pitches
.get(instance_id.as_str())
.copied()
.unwrap_or((elem_cell_width, elem_cell_height));
let cols = dup_array.cols.max(1) as usize;
let rows = dup_array.rows.max(1) as usize;
let col_spacing = dup_array.col_spacing;
let row_spacing = dup_array.row_spacing;
let footprint_width = pitch_w * cols as f64 + (cols as f64 - 1.0) * col_spacing;
let footprint_height =
pitch_h * rows as f64 + (rows as f64 - 1.0) * row_spacing;
let (bx1, by1, _bx2, _by2, _fcx, _fcy) =
self.get_duplication_footprint_coords(element, Some(dup_array));
let fcx = bx1 + footprint_width / 2.0;
let fcy = by1 + footprint_height / 2.0;
let footprint_center = (fcx, fcy);
let c0 = (bx1 + pitch_w / 2.0, by1 + pitch_h / 2.0);
let mut offsets = Vec::with_capacity(rows * cols);
for row in 0..rows {
for col in 0..cols {
let c_copy = (
c0.0 + col as f64 * (pitch_w + col_spacing),
c0.1 + row as f64 * (pitch_h + row_spacing),
);
let c_rotated = Self::rotate_point_around_center(
c_copy,
footprint_center,
base.angle,
);
offsets.push((c_rotated.0 - c0.0, c_rotated.1 - c0.1));
}
}
return Some(offsets);
}
}
} else {
log::info!(
"Element refers to instance {} which is missing from block_instances!",
instance_id
);
}
None
}
pub fn get_renderable_duplication_element(&self, element: &DucElementEnum) -> DucElementEnum {
let base = Self::get_element_base(element);
let Some(instance_id) = base.instance_id.as_ref() else {
return element.clone();
};
let Some(block_instance) = self.block_instances.get(instance_id) else {
return element.clone();
};
let Some(dup_array) = block_instance.duplication_array.as_ref() else {
return element.clone();
};
if dup_array.rows <= 1 && dup_array.cols <= 1 {
return element.clone();
}
let (total_width, total_height) = Self::extract_element_dimensions(element);
let (cell_width, cell_height) =
Self::compute_cell_dimensions(total_width, total_height, dup_array);
Self::with_element_dimensions(element.clone(), cell_width, cell_height)
}
fn extract_element_dimensions(element: &DucElementEnum) -> (f64, f64) {
match element {
DucElementEnum::DucRectangleElement(r) => (r.base.width, r.base.height),
DucElementEnum::DucEllipseElement(e) => (e.base.width, e.base.height),
DucElementEnum::DucImageElement(i) => (i.base.width, i.base.height),
DucElementEnum::DucFrameElement(f) => (
f.stack_element_base.base.width,
f.stack_element_base.base.height,
),
DucElementEnum::DucPlotElement(p) => (
p.stack_element_base.base.width,
p.stack_element_base.base.height,
),
DucElementEnum::DucTableElement(t) => (t.base.width, t.base.height),
DucElementEnum::DucDocElement(d) => (d.base.width, d.base.height),
DucElementEnum::DucEmbeddableElement(e) => (e.base.width, e.base.height),
DucElementEnum::DucPolygonElement(p) => (p.base.width, p.base.height),
DucElementEnum::DucTextElement(t) => (t.base.width, t.base.height),
DucElementEnum::DucFreeDrawElement(f) => (f.base.width, f.base.height),
DucElementEnum::DucLinearElement(l) => {
(l.linear_base.base.width, l.linear_base.base.height)
}
DucElementEnum::DucArrowElement(a) => {
(a.linear_base.base.width, a.linear_base.base.height)
}
DucElementEnum::DucPdfElement(p) => (p.base.width, p.base.height),
DucElementEnum::DucModelElement(m) => (m.base.width, m.base.height),
}
}
fn with_element_dimensions(
mut element: DucElementEnum,
width: f64,
height: f64,
) -> DucElementEnum {
match &mut element {
DucElementEnum::DucRectangleElement(r) => {
r.base.width = width;
r.base.height = height;
}
DucElementEnum::DucPolygonElement(p) => {
p.base.width = width;
p.base.height = height;
}
DucElementEnum::DucEllipseElement(e) => {
e.base.width = width;
e.base.height = height;
}
DucElementEnum::DucEmbeddableElement(e) => {
e.base.width = width;
e.base.height = height;
}
DucElementEnum::DucPdfElement(p) => {
p.base.width = width;
p.base.height = height;
}
DucElementEnum::DucTableElement(t) => {
t.base.width = width;
t.base.height = height;
}
DucElementEnum::DucImageElement(i) => {
i.base.width = width;
i.base.height = height;
}
DucElementEnum::DucTextElement(t) => {
t.base.width = width;
t.base.height = height;
}
DucElementEnum::DucLinearElement(l) => {
l.linear_base.base.width = width;
l.linear_base.base.height = height;
}
DucElementEnum::DucArrowElement(a) => {
a.linear_base.base.width = width;
a.linear_base.base.height = height;
}
DucElementEnum::DucFreeDrawElement(f) => {
f.base.width = width;
f.base.height = height;
}
DucElementEnum::DucFrameElement(f) => {
f.stack_element_base.base.width = width;
f.stack_element_base.base.height = height;
}
DucElementEnum::DucPlotElement(p) => {
p.stack_element_base.base.width = width;
p.stack_element_base.base.height = height;
}
DucElementEnum::DucDocElement(d) => {
d.base.width = width;
d.base.height = height;
}
DucElementEnum::DucModelElement(m) => {
m.base.width = width;
m.base.height = height;
}
}
element
}
fn with_element_position(mut element: DucElementEnum, x: f64, y: f64) -> DucElementEnum {
match &mut element {
DucElementEnum::DucRectangleElement(r) => {
r.base.x = x;
r.base.y = y;
}
DucElementEnum::DucPolygonElement(p) => {
p.base.x = x;
p.base.y = y;
}
DucElementEnum::DucEllipseElement(e) => {
e.base.x = x;
e.base.y = y;
}
DucElementEnum::DucEmbeddableElement(e) => {
e.base.x = x;
e.base.y = y;
}
DucElementEnum::DucPdfElement(p) => {
p.base.x = x;
p.base.y = y;
}
DucElementEnum::DucTableElement(t) => {
t.base.x = x;
t.base.y = y;
}
DucElementEnum::DucImageElement(i) => {
i.base.x = x;
i.base.y = y;
}
DucElementEnum::DucTextElement(t) => {
t.base.x = x;
t.base.y = y;
}
DucElementEnum::DucLinearElement(l) => {
l.linear_base.base.x = x;
l.linear_base.base.y = y;
}
DucElementEnum::DucArrowElement(a) => {
a.linear_base.base.x = x;
a.linear_base.base.y = y;
}
DucElementEnum::DucFreeDrawElement(f) => {
f.base.x = x;
f.base.y = y;
}
DucElementEnum::DucFrameElement(f) => {
f.stack_element_base.base.x = x;
f.stack_element_base.base.y = y;
}
DucElementEnum::DucPlotElement(p) => {
p.stack_element_base.base.x = x;
p.stack_element_base.base.y = y;
}
DucElementEnum::DucDocElement(d) => {
d.base.x = x;
d.base.y = y;
}
DucElementEnum::DucModelElement(m) => {
m.base.x = x;
m.base.y = y;
}
}
element
}
pub fn set_text_font(&mut self, font_resource_name: String, font: Font) {
self.font_resource_name = font_resource_name;
self.text_font = font;
}
pub fn set_resource_cache(&mut self, cache: HashMap<String, String>) {
self.resource_cache = cache;
}
pub fn add_image(&mut self, file_id: String, image_id: u32) {
self.images.insert(file_id, image_id);
}
pub fn set_embedded_pdfs(&mut self, embedded_pdfs: HashMap<String, u32>) {
self.embedded_pdfs = embedded_pdfs;
}
pub fn set_images(&mut self, images: HashMap<String, u32>) {
self.images = images;
}
pub fn set_freedraw_bboxes(&mut self, freedraw_bboxes: HashMap<String, FreeDrawBounds>) {
self.freedraw_bboxes = freedraw_bboxes;
}
pub fn set_svg_dimensions(&mut self, svg_dimensions: HashMap<String, (f64, f64)>) {
self.svg_dimensions = svg_dimensions;
}
pub fn set_page_height(&mut self, page_height: f64) {
self.page_height = page_height;
}
pub fn set_visible_scene_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
self.visible_scene_rect = (x, y, w, h);
}
pub fn set_page_origin(&mut self, origin_x: f64, origin_y: f64) {
self.page_origin = (origin_x, origin_y);
}
pub fn set_page_translation(&mut self, tx: f64, ty: f64) {
self.page_translation = (tx, ty);
}
pub fn set_render_only_plot_elements(&mut self, value: bool) {
self.render_only_plot_elements = value;
}
pub fn set_page_context(
&mut self,
is_plot_mode: bool,
active_plot_id: Option<&str>,
allowed_element_ids: Option<HashSet<String>>,
) {
self.current_mode = if is_plot_mode {
StreamMode::Plot
} else {
StreamMode::Crop
};
self.current_plot_id = active_plot_id.map(|id| id.to_string());
self.allowed_element_ids = allowed_element_ids;
}
pub fn clear_page_context(&mut self) {
self.current_mode = StreamMode::Crop;
self.current_plot_id = None;
self.allowed_element_ids = None;
}
pub fn stream_elements_within_bounds(
&mut self,
elements: &[ElementWrapper],
all_elements: &[ElementWrapper],
bounds: (f64, f64, f64, f64),
local_state: Option<&duc::types::DucLocalState>,
resource_streamer: &mut ResourceStreamer,
hatching_manager: &mut HatchingManager,
pdf_embedder: &mut PdfEmbedder,
image_manager: &mut ImageManager,
ocg_manager: &OCGManager,
document: &mut Document,
) -> ConversionResult<Vec<Operation>> {
let mut all_operations = Vec::new();
let is_plot_mode = matches!(self.current_mode, StreamMode::Plot);
let (_bounds_x, _bounds_y, _bounds_width, _bounds_height) = bounds;
self.precompute_group_cell_pitches(elements);
let mut filtered_elements: Vec<_> = elements
.iter()
.filter(|element_wrapper| {
let base = Self::get_element_base(&element_wrapper.element);
if !self.should_render_element(base) {
return false;
}
if !is_plot_mode {
return true;
}
if let Some(allowed_ids) = &self.allowed_element_ids {
allowed_ids.contains(base.id.as_str())
} else {
true
}
})
.filter(|element_wrapper| {
if is_plot_mode {
return true;
}
let base = Self::get_element_base(&element_wrapper.element);
if base.layer_id.is_some() {
return true;
}
true
})
.collect();
filtered_elements.sort_by(|a, b| {
let base_a = Self::get_element_base(&a.element);
let base_b = Self::get_element_base(&b.element);
base_a
.z_index
.partial_cmp(&base_b.z_index)
.unwrap_or(std::cmp::Ordering::Equal)
});
for element_wrapper in filtered_elements {
let base = Self::get_element_base(&element_wrapper.element);
if let Some(layer_id) = &base.layer_id {
let is_layer_visible = self.is_layer_visible(ocg_manager, layer_id)?;
if !is_layer_visible {
continue; }
}
let mut clip_applied = false;
if let Some(frame_id) = &base.frame_id {
let (clipping_ops, clip_active) =
self.handle_frame_clipping(frame_id, all_elements, bounds, local_state)?;
if !clipping_ops.is_empty() {
all_operations.extend(clipping_ops);
}
clip_applied = clip_active;
}
let renderable_element =
self.get_renderable_duplication_element(&element_wrapper.element);
let offsets = self
.get_element_duplication_offsets(&element_wrapper.element)
.unwrap_or_else(|| vec![(0.0, 0.0)]);
let renderable_base = Self::get_element_base(&renderable_element);
for (x_off, y_off) in offsets {
let positioned_renderable_element = Self::with_element_position(
renderable_element.clone(),
renderable_base.x + x_off,
renderable_base.y + y_off,
);
let element_ops = self.stream_element_with_resources(
&positioned_renderable_element,
local_state,
all_elements,
document,
resource_streamer,
hatching_manager,
pdf_embedder,
image_manager,
None,
)?;
all_operations.extend(element_ops);
}
if clip_applied {
all_operations.push(Operation::new("Q", vec![])); all_operations.push(Operation::new("% Clipping state restored", vec![]));
}
}
Ok(all_operations)
}
fn stream_element_with_resources(
&mut self,
element: &DucElementEnum,
local_state: Option<&duc::types::DucLocalState>,
all_elements: &[ElementWrapper],
document: &mut Document,
resource_streamer: &mut ResourceStreamer,
hatching_manager: &mut HatchingManager,
pdf_embedder: &mut PdfEmbedder,
image_manager: &mut ImageManager,
duplication_offset: Option<(f64, f64)>,
) -> ConversionResult<Vec<Operation>> {
let mut operations = Vec::new();
let is_plot_mode = matches!(self.current_mode, StreamMode::Plot);
operations.push(Operation::new("q", vec![]));
let base = Self::get_element_base(element);
let center_override = Self::compute_element_center_override(element);
if base.x != 0.0 || base.y != 0.0 || base.angle != 0.0 {
let transform_ops = if is_plot_mode && base.frame_id.is_some() {
if let Some((
(parent_x, parent_y, parent_width, parent_height),
margins,
clip_active,
is_frame_parent,
)) = self.find_parent_plot_bounds(element, all_elements)
{
self.create_transformation_matrix_for_plot_child(
base,
parent_x,
parent_y,
parent_width,
parent_height,
margins,
clip_active,
is_frame_parent,
center_override,
)
} else {
self.create_transformation_matrix_with_scroll(
base,
if is_plot_mode { None } else { local_state },
center_override,
)
}
} else {
self.create_transformation_matrix_with_scroll(
base,
if is_plot_mode { None } else { local_state },
center_override,
)
};
operations.extend(transform_ops);
}
if let Some((x_off, y_off)) = duplication_offset {
if x_off != 0.0 || y_off != 0.0 {
operations.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(x_off as f32),
Object::Real((-y_off) as f32),
],
));
}
}
let styles = self.style_resolver.resolve_styles(element);
let is_pdf = matches!(
element,
DucElementEnum::DucPdfElement(_) | DucElementEnum::DucDocElement(_)
);
if !is_pdf {
let style_ops = self.apply_styles(element, &styles)?;
operations.extend(style_ops);
}
let element_ops = match element {
DucElementEnum::DucRectangleElement(rect) => {
self.stream_rectangle(rect, hatching_manager)?
}
DucElementEnum::DucPolygonElement(polygon) => self.stream_polygon(polygon)?,
DucElementEnum::DucEllipseElement(ellipse) => self.stream_ellipse(ellipse)?,
DucElementEnum::DucTextElement(text) => self.stream_text(text)?,
DucElementEnum::DucLinearElement(linear) => self.stream_linear(linear)?,
DucElementEnum::DucArrowElement(arrow) => self.stream_arrow(arrow)?,
DucElementEnum::DucTableElement(table) => self.stream_table(table)?,
DucElementEnum::DucFreeDrawElement(freedraw) => {
self.stream_freedraw(freedraw, &styles, document, pdf_embedder, resource_streamer)?
}
DucElementEnum::DucPdfElement(pdf) => {
self.stream_pdf_element(pdf, document, pdf_embedder)?
}
DucElementEnum::DucImageElement(image) => self.stream_image(
image,
document,
pdf_embedder,
image_manager,
resource_streamer,
)?,
DucElementEnum::DucFrameElement(frame) => self.stream_frame(frame)?,
DucElementEnum::DucPlotElement(plot) => self.stream_plot(plot)?,
DucElementEnum::DucEmbeddableElement(_) => vec![], DucElementEnum::DucDocElement(doc) => {
self.stream_doc_element(doc, document, pdf_embedder)?
}
DucElementEnum::DucModelElement(model) => {
self.stream_model(model, document, image_manager)?
}
};
operations.extend(element_ops);
operations.push(Operation::new("Q", vec![]));
Ok(operations)
}
fn get_element_base(element: &DucElementEnum) -> &duc::types::DucElementBase {
match element {
DucElementEnum::DucRectangleElement(elem) => &elem.base,
DucElementEnum::DucPolygonElement(elem) => &elem.base,
DucElementEnum::DucEllipseElement(elem) => &elem.base,
DucElementEnum::DucEmbeddableElement(elem) => &elem.base,
DucElementEnum::DucPdfElement(elem) => &elem.base,
DucElementEnum::DucTableElement(elem) => &elem.base,
DucElementEnum::DucImageElement(elem) => &elem.base,
DucElementEnum::DucTextElement(elem) => &elem.base,
DucElementEnum::DucLinearElement(elem) => &elem.linear_base.base,
DucElementEnum::DucArrowElement(elem) => &elem.linear_base.base,
DucElementEnum::DucFreeDrawElement(elem) => &elem.base,
DucElementEnum::DucFrameElement(elem) => &elem.stack_element_base.base,
DucElementEnum::DucPlotElement(elem) => &elem.stack_element_base.base,
DucElementEnum::DucDocElement(elem) => &elem.base,
DucElementEnum::DucModelElement(elem) => &elem.base,
}
}
pub fn should_render_element(&self, base: &duc::types::DucElementBase) -> bool {
if !base.is_visible || base.is_deleted {
return false;
}
if self.render_only_plot_elements {
base.is_plot
} else {
true
}
}
fn find_parent_plot_bounds(
&self,
element: &DucElementEnum,
all_elements: &[ElementWrapper],
) -> Option<(
(f64, f64, f64, f64),
Option<(f64, f64, f64, f64)>,
bool,
bool, // is_frame_parent
)> {
let base = Self::get_element_base(element);
if let Some(frame_id) = &base.frame_id {
for element_wrapper in all_elements {
let wrapper_base = Self::get_element_base(&element_wrapper.element);
if wrapper_base.id == *frame_id {
if let DucElementEnum::DucPlotElement(plot) = &element_wrapper.element {
let plot_base = &plot.stack_element_base.base;
let margins = Some((
plot.layout.margins.left,
plot.layout.margins.top,
plot.layout.margins.right,
plot.layout.margins.bottom,
));
return Some((
(plot_base.x, plot_base.y, plot_base.width, plot_base.height),
margins,
plot.stack_element_base.clip,
false, ));
}
else if let DucElementEnum::DucFrameElement(frame) = &element_wrapper.element
{
let frame_base = &frame.stack_element_base.base;
return Some((
(
frame_base.x,
frame_base.y,
frame_base.width,
frame_base.height,
),
None,
frame.stack_element_base.clip,
true, ));
}
}
}
}
None
}
fn is_layer_visible(&self, ocg_manager: &OCGManager, layer_id: &str) -> ConversionResult<bool> {
if ocg_manager.get_layer(layer_id).is_none() {
return Ok(true);
}
Ok(true)
}
fn handle_frame_clipping(
&self,
frame_id: &str,
all_elements: &[ElementWrapper],
_bounds: (f64, f64, f64, f64),
local_state: Option<&duc::types::DucLocalState>,
) -> ConversionResult<(Vec<Operation>, bool)> {
let mut ops = Vec::new();
let mut clip_applied = false;
let is_plot_mode = matches!(self.current_mode, StreamMode::Plot);
let (scroll_x, scroll_y) = if is_plot_mode {
(0.0, 0.0)
} else if let Some(state) = local_state {
(state.scroll_x, state.scroll_y)
} else {
(0.0, 0.0)
};
if let Some(frame_wrapper) = all_elements.iter().find(|wrapper| {
let base = Self::get_element_base(&wrapper.element);
base.id == frame_id
}) {
match &frame_wrapper.element {
DucElementEnum::DucFrameElement(frame) => {
if frame.stack_element_base.clip {
let base = &frame.stack_element_base.base;
let width = base.width;
let height = base.height;
let stroke_inset = if let Some(stroke) = base.styles.stroke.first() {
if stroke.content.visible {
stroke.width / 2.0 } else {
0.0
}
} else {
0.0
};
ops.push(Operation::new("q", vec![]));
clip_applied = true;
if is_plot_mode {
let x = base.x;
let y = base.y;
let plot_y = self.page_origin.1;
let pdf_x = x;
let pdf_y = self.page_height - y + (2.0 * plot_y);
ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(pdf_x as f32),
Object::Real(pdf_y as f32),
],
));
ops.push(Operation::new(
"re",
vec![
Object::Real(stroke_inset as f32),
Object::Real(-(stroke_inset as f32)),
Object::Real((width - 2.0 * stroke_inset) as f32),
Object::Real(-(height - 2.0 * stroke_inset) as f32),
],
));
ops.push(Operation::new("W", vec![]));
ops.push(Operation::new("n", vec![]));
ops.push(Operation::new(
&format!(
"% Clipping active for frame (PDF coords): {} at ({}, {}) (w={}, h={}, inset={})",
frame_id, pdf_x, pdf_y, width, height, stroke_inset
),
vec![],
));
} else {
let clip_x = base.x + scroll_x + stroke_inset;
let clip_y = base.y + scroll_y + stroke_inset;
let clip_width = width - 2.0 * stroke_inset;
let clip_height = height - 2.0 * stroke_inset;
let pdf_y = DucDataScaler::transform_y_coordinate_to_pdf_system(
clip_y,
clip_height,
self.page_height,
);
ops.push(Operation::new(
"re",
vec![
Object::Real(clip_x as f32),
Object::Real(pdf_y as f32),
Object::Real(clip_width as f32),
Object::Real(clip_height as f32),
],
));
ops.push(Operation::new("W", vec![]));
ops.push(Operation::new("n", vec![]));
ops.push(Operation::new(
&format!(
"% Clipping active for frame (absolute coords): {} (w={}, h={}, inset={})",
frame_id, width, height, stroke_inset
),
vec![],
));
}
}
}
DucElementEnum::DucPlotElement(plot) => {
if plot.stack_element_base.clip {
let base = &plot.stack_element_base.base;
let ml = plot.layout.margins.left;
let mt = plot.layout.margins.top;
let mr = plot.layout.margins.right;
let mb = plot.layout.margins.bottom;
let width = base.width - (ml + mr);
let height = base.height - (mt + mb);
ops.push(Operation::new("q", vec![]));
clip_applied = true;
if is_plot_mode {
let tx = base.x + ml;
let ty = base.y + mt;
ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(tx as f32),
Object::Real(ty as f32),
],
));
ops.push(Operation::new(
"re",
vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(width as f32),
Object::Real(height as f32),
],
));
ops.push(Operation::new("W", vec![]));
ops.push(Operation::new("n", vec![]));
ops.push(Operation::new(
&format!(
"% Clipping active for plot (local content): {} (w={}, h={})",
frame_id, width, height
),
vec![],
));
} else {
let clip_x = base.x + ml + scroll_x;
let clip_y = base.y + mt + scroll_y;
let pdf_y = DucDataScaler::transform_y_coordinate_to_pdf_system(
clip_y,
height,
self.page_height,
);
ops.push(Operation::new(
"re",
vec![
Object::Real(clip_x as f32),
Object::Real(pdf_y as f32),
Object::Real(width as f32),
Object::Real(height as f32),
],
));
ops.push(Operation::new("W", vec![]));
ops.push(Operation::new("n", vec![]));
ops.push(Operation::new(
&format!(
"% Clipping active for plot (absolute content): {} (w={}, h={})",
frame_id, width, height
),
vec![],
));
}
}
}
_ => {}
}
} else {
ops.push(Operation::new(
&format!("% Warning: Frame '{}' not found for clipping", frame_id),
vec![],
));
}
Ok((ops, clip_applied))
}
fn compute_element_center_override(element: &DucElementEnum) -> Option<(f64, f64)> {
match element {
DucElementEnum::DucLinearElement(linear) => {
Self::compute_linear_center(&linear.linear_base)
}
DucElementEnum::DucPolygonElement(polygon) => {
Some((polygon.base.width / 2.0, -(polygon.base.height / 2.0)))
}
DucElementEnum::DucEllipseElement(ellipse) => {
let linear = Self::convert_ellipse_to_linear_element(ellipse);
Self::compute_linear_center(&linear.linear_base)
}
_ => None,
}
}
fn compute_linear_center(linear_base: &duc::types::DucLinearElementBase) -> Option<(f64, f64)> {
if linear_base.points.is_empty() {
return None;
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for point in &linear_base.points {
let x = point.x;
let y = -point.y;
if !x.is_finite() || !y.is_finite() {
continue;
}
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
for line in &linear_base.lines {
if let Some(handle) = &line.start.handle {
let x = handle.x;
let y = -handle.y;
if x.is_finite() && y.is_finite() {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
if let Some(handle) = &line.end.handle {
let x = handle.x;
let y = -handle.y;
if x.is_finite() && y.is_finite() {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
}
if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
return None;
}
Some(((min_x + max_x) / 2.0, (min_y + max_y) / 2.0))
}
fn create_transformation_matrix(
&self,
x: f64,
y: f64,
width: f64,
height: f64,
angle: f64,
center_override: Option<(f64, f64)>,
) -> Vec<Operation> {
let mut ops = Vec::new();
ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(x as f32),
Object::Real(y as f32),
],
));
if angle != 0.0 {
let (center_x, center_y) = center_override.unwrap_or((width / 2.0, -height / 2.0));
ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(center_x as f32),
Object::Real(center_y as f32),
],
));
let negated_angle = -angle;
let cos_a = negated_angle.cos();
let sin_a = negated_angle.sin();
ops.push(Operation::new(
"cm",
vec![
Object::Real(cos_a as f32),
Object::Real(sin_a as f32),
Object::Real(-sin_a as f32),
Object::Real(cos_a as f32),
Object::Real(0.0),
Object::Real(0.0),
],
));
ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(-center_x as f32),
Object::Real(-center_y as f32),
],
));
}
ops
}
fn create_transformation_matrix_with_scroll(
&self,
base: &duc::types::DucElementBase,
local_state: Option<&duc::types::DucLocalState>,
center_override: Option<(f64, f64)>,
) -> Vec<Operation> {
let (scroll_x, scroll_y) = if let Some(state) = local_state {
(state.scroll_x, state.scroll_y)
} else {
(0.0, 0.0)
};
let adjusted_x = base.x + scroll_x;
let adjusted_y = base.y + scroll_y;
let transformed_y =
DucDataScaler::transform_point_y_to_pdf_system(adjusted_y, self.page_height);
self.create_transformation_matrix(
adjusted_x,
transformed_y,
base.width,
base.height,
base.angle,
center_override,
)
}
fn create_transformation_matrix_for_plot_child(
&self,
base: &duc::types::DucElementBase,
parent_x: f64,
parent_y: f64,
parent_width: f64,
parent_height: f64,
parent_margins: Option<(f64, f64, f64, f64)>,
parent_clip_active: bool,
is_frame_parent: bool,
center_override: Option<(f64, f64)>,
) -> Vec<Operation> {
let (translation_x, translation_y) = self.compute_plot_child_translation(
base,
parent_x,
parent_y,
parent_width,
parent_height,
parent_margins,
parent_clip_active,
is_frame_parent,
);
self.create_transformation_matrix(
translation_x,
translation_y,
base.width,
base.height,
base.angle,
center_override,
)
}
fn compute_plot_child_translation(
&self,
base: &duc::types::DucElementBase,
parent_x: f64,
parent_y: f64,
_parent_width: f64,
_parent_height: f64,
parent_margins: Option<(f64, f64, f64, f64)>,
parent_clip_active: bool,
is_frame_parent: bool,
) -> (f64, f64) {
let plot_y = self.page_origin.1;
if is_frame_parent && parent_clip_active {
let translation_x = base.x - parent_x;
let translation_y = -(base.y - parent_y);
(translation_x, translation_y)
} else {
let (ml, mt, _mr, _mb) = if parent_clip_active {
parent_margins.unwrap_or((0.0, 0.0, 0.0, 0.0))
} else {
(0.0, 0.0, 0.0, 0.0)
};
let clip_translation_x = if parent_clip_active {
parent_x + ml
} else {
0.0
};
let clip_translation_y = if parent_clip_active {
parent_y + mt
} else {
0.0
};
let translation_x = base.x - clip_translation_x;
let translation_y = self.page_height - base.y + (2.0 * plot_y) - clip_translation_y;
(translation_x, translation_y)
}
}
fn quantize_opacity(value: f64) -> u16 {
let clamped = value.clamp(0.0, 1.0);
let quantized = (clamped * 1000.0).round();
quantized.max(0.0).min(1000.0) as u16
}
fn ensure_ext_gstate(&mut self, stroke_alpha: f64, fill_alpha: f64) -> Option<String> {
let stroke_q = Self::quantize_opacity(stroke_alpha);
let fill_q = Self::quantize_opacity(fill_alpha);
if stroke_q == 1000 && fill_q == 1000 {
return None;
}
let key = OpacityKey {
stroke_thousandths: stroke_q,
fill_thousandths: fill_q,
};
if let Some(existing) = self.ext_gstate_cache.get(&key) {
self.current_page_ext_gstates.insert(existing.clone());
return Some(existing.clone());
}
let name = format!("GS{:02}", self.ext_gstate_cache.len() + 1);
let mut dict = Dictionary::new();
dict.set("Type", Object::Name(b"ExtGState".to_vec()));
if stroke_q < 1000 {
dict.set("CA", Object::Real((stroke_q as f32) / 1000.0));
}
if fill_q < 1000 {
dict.set("ca", Object::Real((fill_q as f32) / 1000.0));
}
self.ext_gstate_cache.insert(key, name.clone());
self.ext_gstate_definitions
.insert(name.clone(), dict.clone());
self.current_page_ext_gstates.insert(name.clone());
Some(name)
}
pub fn begin_page(&mut self) {
self.current_page_ext_gstates.clear();
}
pub fn take_page_ext_gstates(&mut self) -> Vec<(String, Dictionary)> {
let names: Vec<String> = self.current_page_ext_gstates.iter().cloned().collect();
self.current_page_ext_gstates.clear();
let mut result = Vec::new();
for name in names {
if let Some(dict) = self.ext_gstate_definitions.get(&name) {
result.push((name, dict.clone()));
}
}
result
}
fn apply_styles(
&mut self,
element: &DucElementEnum,
styles: &ResolvedStyles,
) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let profile = Self::determine_style_profile(element);
let maybe_background = styles.background.iter().find(|bg| bg.visible);
let maybe_stroke = styles.stroke.iter().find(|st| st.visible);
let element_opacity = styles.opacity.clamp(0.0, 1.0);
let fill_color = if profile.fill_from_stroke {
maybe_stroke
.map(|stroke| stroke.color.clone())
.or_else(|| maybe_background.map(|bg| bg.color.clone()))
} else if profile.use_background_fill {
maybe_background.map(|bg| bg.color.clone())
} else {
None
};
let fill_opacity = if profile.fill_from_stroke {
styles.get_combined_stroke_opacity()
} else if profile.use_background_fill {
styles.get_combined_fill_opacity()
} else {
element_opacity
};
let stroke_opacity = if profile.apply_stroke_properties {
styles.get_combined_stroke_opacity()
} else {
element_opacity
};
if let Some(gs_name) = self.ensure_ext_gstate(stroke_opacity, fill_opacity) {
ops.push(Operation::new(
"gs",
vec![Object::Name(gs_name.into_bytes())],
));
}
if let Some(color_str) = &fill_color {
if let Ok(color) = self.parse_color(color_str) {
ops.push(Operation::new(
"rg",
vec![
Object::Real(color.0),
Object::Real(color.1),
Object::Real(color.2),
],
));
}
}
if let Some(stroke) = maybe_stroke {
if profile.apply_stroke_properties || profile.fill_from_stroke {
if let Ok(color) = self.parse_color(&stroke.color) {
ops.push(Operation::new(
"RG",
vec![
Object::Real(color.0),
Object::Real(color.1),
Object::Real(color.2),
],
));
}
}
if profile.apply_stroke_properties {
ops.push(Operation::new("w", vec![Object::Real(stroke.width as f32)]));
let cap = match stroke.cap {
STROKE_CAP::ROUND => 1,
STROKE_CAP::SQUARE => 2,
_ => 0,
};
ops.push(Operation::new("J", vec![Object::Integer(i64::from(cap))]));
let join = match stroke.join {
STROKE_JOIN::ROUND => 1,
STROKE_JOIN::BEVEL => 2,
_ => 0,
};
ops.push(Operation::new("j", vec![Object::Integer(i64::from(join))]));
if let Some(dash) = &stroke.dash_pattern {
if !dash.is_empty() {
let dash_objects: Vec<Object> =
dash.iter().map(|&d| Object::Real(d as f32)).collect();
ops.push(Operation::new(
"d",
vec![Object::Array(dash_objects), Object::Real(0.0)],
));
}
}
}
}
Ok(ops)
}
fn parse_color(&self, color_str: &str) -> Result<(f32, f32, f32), ConversionError> {
let color = BigColor::new(color_str);
let rgb = color.to_rgb();
Ok((
rgb.r as f32 / 255.0,
rgb.g as f32 / 255.0,
rgb.b as f32 / 255.0,
))
}
fn append_rounded_rect_path(
ops: &mut Vec<Operation>,
x: f64,
top_y: f64,
width: f64,
height: f64,
roundness: f64,
) {
if width <= 0.0 || height <= 0.0 || !width.is_finite() || !height.is_finite() {
return;
}
let radius = roundness.max(0.0).min(width * 0.5).min(height * 0.5);
if radius <= 0.01 {
ops.push(Operation::new(
"re",
vec![
Object::Real(x as f32),
Object::Real((top_y - height) as f32),
Object::Real(width as f32),
Object::Real(height as f32),
],
));
return;
}
let right = x + width;
let bottom = top_y - height;
let kappa = 0.552_284_749_830_793_6;
let control = radius * kappa;
ops.push(Operation::new(
"m",
vec![
Object::Real((x + radius) as f32),
Object::Real(top_y as f32),
],
));
ops.push(Operation::new(
"l",
vec![
Object::Real((right - radius) as f32),
Object::Real(top_y as f32),
],
));
ops.push(Operation::new(
"c",
vec![
Object::Real((right - radius + control) as f32),
Object::Real(top_y as f32),
Object::Real(right as f32),
Object::Real((top_y - radius + control) as f32),
Object::Real(right as f32),
Object::Real((top_y - radius) as f32),
],
));
ops.push(Operation::new(
"l",
vec![
Object::Real(right as f32),
Object::Real((bottom + radius) as f32),
],
));
ops.push(Operation::new(
"c",
vec![
Object::Real(right as f32),
Object::Real((bottom + radius - control) as f32),
Object::Real((right - radius + control) as f32),
Object::Real(bottom as f32),
Object::Real((right - radius) as f32),
Object::Real(bottom as f32),
],
));
ops.push(Operation::new(
"l",
vec![
Object::Real((x + radius) as f32),
Object::Real(bottom as f32),
],
));
ops.push(Operation::new(
"c",
vec![
Object::Real((x + radius - control) as f32),
Object::Real(bottom as f32),
Object::Real(x as f32),
Object::Real((bottom + radius - control) as f32),
Object::Real(x as f32),
Object::Real((bottom + radius) as f32),
],
));
ops.push(Operation::new(
"l",
vec![
Object::Real(x as f32),
Object::Real((top_y - radius) as f32),
],
));
ops.push(Operation::new(
"c",
vec![
Object::Real(x as f32),
Object::Real((top_y - radius + control) as f32),
Object::Real((x + radius - control) as f32),
Object::Real(top_y as f32),
Object::Real((x + radius) as f32),
Object::Real(top_y as f32),
],
));
ops.push(Operation::new("h", vec![]));
}
fn begin_rounded_element_clip(
ops: &mut Vec<Operation>,
width: f64,
height: f64,
roundness: f64,
) -> bool {
if roundness <= 0.01
|| width <= 0.0
|| height <= 0.0
|| !width.is_finite()
|| !height.is_finite()
{
return false;
}
ops.push(Operation::new("q", vec![]));
Self::append_rounded_rect_path(ops, 0.0, 0.0, width, height, roundness);
ops.push(Operation::new("W", vec![]));
ops.push(Operation::new("n", vec![]));
true
}
fn stream_rectangle(
&self,
rect: &DucRectangleElement,
hatching_manager: &mut HatchingManager,
) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let styles = &rect.base.styles;
let has_background = styles
.background
.iter()
.any(|background| background.content.visible);
let has_stroke = styles.stroke.iter().any(|stroke| stroke.content.visible);
let has_hatching = self.style_resolver.has_hatching(&styles.background);
if has_hatching {
let has_rounded_clip = Self::begin_rounded_element_clip(
&mut ops,
rect.base.width,
rect.base.height,
styles.roundness,
);
self.style_resolver.apply_hatching_pattern_with_dims(
&styles.background,
hatching_manager,
&mut ops,
rect.base.width,
rect.base.height,
)?;
if has_rounded_clip {
ops.push(Operation::new("Q", vec![]));
}
if has_stroke {
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
rect.base.width,
rect.base.height,
styles.roundness,
);
ops.push(Operation::new("S", vec![])); }
} else {
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
rect.base.width,
rect.base.height,
styles.roundness,
);
if has_background && has_stroke {
ops.push(Operation::new("B", vec![])); } else if has_background {
ops.push(Operation::new("f", vec![])); } else if has_stroke {
ops.push(Operation::new("S", vec![])); }
}
Ok(ops)
}
fn determine_style_profile(element: &DucElementEnum) -> StyleProfile {
match element {
DucElementEnum::DucRectangleElement(_)
| DucElementEnum::DucPolygonElement(_)
| DucElementEnum::DucEllipseElement(_)
| DucElementEnum::DucLinearElement(_)
| DucElementEnum::DucArrowElement(_)
| DucElementEnum::DucTableElement(_) => StyleProfile {
use_background_fill: true,
fill_from_stroke: false,
apply_stroke_properties: true,
},
DucElementEnum::DucFrameElement(_) => StyleProfile {
use_background_fill: true,
fill_from_stroke: false,
apply_stroke_properties: true,
},
DucElementEnum::DucPlotElement(_) => StyleProfile {
use_background_fill: false,
fill_from_stroke: false,
apply_stroke_properties: true,
},
DucElementEnum::DucTextElement(_) => StyleProfile {
use_background_fill: false,
fill_from_stroke: true,
apply_stroke_properties: false,
},
DucElementEnum::DucFreeDrawElement(_)
| DucElementEnum::DucImageElement(_)
| DucElementEnum::DucPdfElement(_)
| DucElementEnum::DucEmbeddableElement(_)
| DucElementEnum::DucDocElement(_) => StyleProfile {
use_background_fill: false,
fill_from_stroke: false,
apply_stroke_properties: false,
},
DucElementEnum::DucModelElement(_) => StyleProfile {
use_background_fill: false,
fill_from_stroke: false,
apply_stroke_properties: false,
},
}
}
fn stream_text(&self, text: &DucTextElement) -> ConversionResult<Vec<Operation>> {
use duc::types::{TEXT_ALIGN, VERTICAL_ALIGN};
use hipdf::fonts::utils::{create_text_block, TextAlign, WrapStrategy};
let resolved_text = self
.style_resolver
.resolve_dynamic_fields(&text.text, &DucElementEnum::DucTextElement(text.clone()));
let (active_font, active_resource_name) = self
.font_map
.get(&text.style.font_family)
.map(|(f, r)| (f, r.as_str()))
.unwrap_or((&self.text_font, &self.font_resource_name));
let align = match text.style.text_align {
TEXT_ALIGN::LEFT => TextAlign::Left,
TEXT_ALIGN::CENTER => TextAlign::Center,
TEXT_ALIGN::RIGHT => TextAlign::Right,
};
let line_height = text.style.font_size as f32 * text.style.line_height;
let wrap_strategy = if text.auto_resize {
WrapStrategy::Word
} else {
WrapStrategy::Hybrid
};
let font_size = text.style.font_size as f32;
let element_height = text.base.height as f32;
let line_count = {
let max_w = if text.auto_resize {
None
} else {
Some(text.base.width as f32)
};
let paragraphs: Vec<&str> = resolved_text.split('\n').collect();
let mut count = 0usize;
for para in ¶graphs {
if para.is_empty() {
count += 1;
} else if let Some(w) = max_w {
let wrapped = hipdf::fonts::utils::wrap_text(
active_font,
para,
w,
font_size,
wrap_strategy,
);
count += wrapped.len().max(1);
} else {
count += 1;
}
}
count
};
let total_text_height = font_size + (line_count.saturating_sub(1) as f32) * line_height;
let text_start_y = match text.style.vertical_align {
VERTICAL_ALIGN::MIDDLE => -(font_size + (element_height - total_text_height) / 2.0),
VERTICAL_ALIGN::BOTTOM => -(element_height),
_ => -font_size,
};
let max_width = if text.auto_resize {
None
} else {
Some(text.base.width as f32)
};
let max_height = Some(text.base.height as f32);
let operations = create_text_block(
active_resource_name,
active_font,
&resolved_text,
0.0,
text_start_y,
font_size,
max_width,
max_height,
line_height,
align,
wrap_strategy,
);
Ok(operations)
}
fn stream_table(&self, table: &DucTableElement) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
ops.push(Operation::new("% Table placeholder", vec![]));
ops.push(Operation::new(
"re",
vec![
Object::Real(0.0), Object::Real(-(table.base.height as f32)), Object::Real(table.base.width as f32),
Object::Real(table.base.height as f32),
],
));
ops.push(Operation::new("S", vec![]));
ops.push(Operation::new(
"% TODO: Implement full table rendering",
vec![],
));
Ok(ops)
}
fn stream_polygon(&self, polygon: &DucPolygonElement) -> ConversionResult<Vec<Operation>> {
let linear = Self::convert_polygon_to_linear_element(polygon);
PdfLinearRenderer::stream_linear(&linear)
}
fn stream_arrow(&self, arrow: &DucArrowElement) -> ConversionResult<Vec<Operation>> {
PdfLinearRenderer::stream_linear(&DucLinearElement {
linear_base: arrow.linear_base.clone(),
wipeout_below: false,
})
}
fn convert_polygon_to_linear_element(polygon: &DucPolygonElement) -> DucLinearElement {
let sides = polygon.sides.max(3);
let base_points =
Self::generate_polygon_points(sides, polygon.base.width, polygon.base.height);
let roundness = polygon.base.styles.roundness.max(0.0);
let (points, lines) = if roundness > 0.01 {
Self::generate_rounded_polygon_path(&base_points, roundness)
} else {
let mut lines: Vec<DucLine> = Vec::with_capacity(base_points.len());
for i in 0..base_points.len() {
let next_i = (i + 1) % base_points.len();
lines.push(DucLine {
start: DucLineReference {
index: i as i32,
handle: None,
},
end: DucLineReference {
index: next_i as i32,
handle: None,
},
});
}
(base_points, lines)
};
DucLinearElement {
linear_base: DucLinearElementBase {
base: polygon.base.clone(),
points,
lines,
path_overrides: Vec::new(),
last_committed_point: None,
start_binding: None,
end_binding: None,
},
wipeout_below: false,
}
}
fn generate_rounded_polygon_path(
vertices: &[DucPoint],
roundness: f64,
) -> (Vec<DucPoint>, Vec<DucLine>) {
if vertices.len() < 3 {
return (vertices.to_vec(), Vec::new());
}
let mut points = Vec::with_capacity(vertices.len() * 2);
let mut corner_controls = Vec::with_capacity(vertices.len());
for (index, point) in vertices.iter().enumerate() {
let prev = &vertices[(index + vertices.len() - 1) % vertices.len()];
let next = &vertices[(index + 1) % vertices.len()];
let prev_dx = prev.x - point.x;
let prev_dy = prev.y - point.y;
let next_dx = next.x - point.x;
let next_dy = next.y - point.y;
let prev_len = (prev_dx * prev_dx + prev_dy * prev_dy).sqrt();
let next_len = (next_dx * next_dx + next_dy * next_dy).sqrt();
let tangent = roundness.min(prev_len * 0.45).min(next_len * 0.45);
if tangent <= 0.01 || prev_len <= 0.001 || next_len <= 0.001 {
points.push(point.clone());
points.push(point.clone());
corner_controls.push((point.x, point.y));
continue;
}
points.push(DucPoint {
x: point.x + (prev_dx / prev_len) * tangent,
y: point.y + (prev_dy / prev_len) * tangent,
mirroring: None,
});
points.push(DucPoint {
x: point.x + (next_dx / next_len) * tangent,
y: point.y + (next_dy / next_len) * tangent,
mirroring: None,
});
corner_controls.push((point.x, point.y));
}
let mut lines = Vec::with_capacity(vertices.len() * 2);
for index in 0..vertices.len() {
let start_idx = index * 2;
let end_idx = start_idx + 1;
let next_start_idx = ((index + 1) % vertices.len()) * 2;
let control = corner_controls[index];
lines.push(DucLine {
start: DucLineReference {
index: start_idx as i32,
handle: Some(GeometricPoint {
x: control.0,
y: control.1,
}),
},
end: DucLineReference {
index: end_idx as i32,
handle: None,
},
});
lines.push(DucLine {
start: DucLineReference {
index: end_idx as i32,
handle: None,
},
end: DucLineReference {
index: next_start_idx as i32,
handle: None,
},
});
}
(points, lines)
}
fn generate_polygon_points(sides: i32, width: f64, height: f64) -> Vec<DucPoint> {
let valid_sides = sides.max(3);
let cx = width / 2.0;
let cy = height / 2.0;
let rx = width / 2.0;
let ry = height / 2.0;
(0..valid_sides)
.map(|i| {
let t = (i as f64) * 2.0 * PI / (valid_sides as f64) - PI / 2.0;
DucPoint {
x: cx + rx * t.cos(),
y: cy + ry * t.sin(),
mirroring: None,
}
})
.collect()
}
pub fn stream_ellipse(&self, ellipse: &DucEllipseElement) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let linear = Self::convert_ellipse_to_linear_element(ellipse);
ops.extend(PdfLinearRenderer::stream_linear(&linear)?);
if ellipse.show_aux_crosshair {
ops.extend(self.stream_ellipse_crosshair(ellipse)?);
}
Ok(ops)
}
fn stream_ellipse_crosshair(
&self,
ellipse: &DucEllipseElement,
) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let base = &ellipse.base;
let cx = base.width / 2.0;
let cy = base.height / 2.0;
let cross_width = base.width * 1.2;
let cross_height = base.height * 1.2;
let x1 = cx - cross_width / 2.0;
let x2 = cx + cross_width / 2.0;
let y1 = cy - cross_height / 2.0;
let y2 = cy + cross_height / 2.0;
let (r, g, b) = self.parse_color(DUC_STANDARD_PRIMARY_COLOR)?;
ops.push(Operation::new(
"RG",
vec![Object::Real(r), Object::Real(g), Object::Real(b)],
));
ops.push(Operation::new("w", vec![Object::Real(0.5)]));
ops.push(Operation::new("J", vec![Object::Integer(1)]));
ops.push(Operation::new("j", vec![Object::Integer(1)]));
ops.push(Operation::new("% Aux crosshair horizontal", vec![]));
let (dash_array_h, dash_offset_h) = Self::crosshair_dash_params(cross_width);
ops.push(Operation::new(
"d",
vec![Object::Array(dash_array_h), Object::Real(dash_offset_h)],
));
ops.push(Operation::new(
"m",
vec![Object::Real(x1 as f32), Object::Real(-(cy) as f32)],
));
ops.push(Operation::new(
"l",
vec![Object::Real(x2 as f32), Object::Real(-(cy) as f32)],
));
ops.push(Operation::new("S", vec![]));
ops.push(Operation::new("% Aux crosshair vertical", vec![]));
let (dash_array_v, dash_offset_v) = Self::crosshair_dash_params(cross_height);
ops.push(Operation::new(
"d",
vec![Object::Array(dash_array_v), Object::Real(dash_offset_v)],
));
ops.push(Operation::new(
"m",
vec![Object::Real(cx as f32), Object::Real(-y1 as f32)],
));
ops.push(Operation::new(
"l",
vec![Object::Real(cx as f32), Object::Real(-y2 as f32)],
));
ops.push(Operation::new("S", vec![]));
Ok(ops)
}
fn crosshair_dash_params(line_length: f64) -> (Vec<Object>, f32) {
const PATTERN: [f64; 4] = [26.0, 6.0, 0.6, 6.0];
let dash_array: Vec<Object> = PATTERN
.iter()
.map(|&value| Object::Real(value as f32))
.collect();
let total: f64 = PATTERN.iter().sum();
if line_length <= f64::EPSILON || total <= f64::EPSILON {
return (dash_array, 0.0);
}
let main_dash = PATTERN[0];
let mut offset = (main_dash / 2.0 - line_length / 2.0) % total;
if offset < 0.0 {
offset += total;
}
(dash_array, offset as f32)
}
pub fn convert_ellipse_to_linear_element(element: &DucEllipseElement) -> DucLinearElement {
let base = &element.base;
let width = base.width;
let height = base.height;
let ratio_f64 = element.ratio as f64;
let start_angle = element.start_angle;
let end_angle = element.end_angle;
let rx = width / 2.0;
let ry = height / 2.0;
let cx = width / 2.0;
let cy = height / 2.0;
let epsilon: f64 = 1e-6;
let sweep_angle = end_angle - start_angle;
let is_full_shape = sweep_angle.abs() >= 2.0 * PI - epsilon;
let has_hole = ratio_f64 > epsilon && ratio_f64 < 1.0_f64 - epsilon;
let mut all_points: Vec<DucPoint> = Vec::new();
let mut all_lines: Vec<DucLine> = Vec::new();
let mut path_overrides: Vec<DucPath> = Vec::new();
let create_arc = |radius_x: f64, radius_y: f64, s_angle: f64, e_angle: f64| {
let mut arc_points = Vec::new();
let mut arc_lines = Vec::new();
let sweep = e_angle - s_angle;
if sweep.abs() < epsilon {
return (arc_points, arc_lines);
}
let n_segments = (sweep.abs() / (PI / 2.0)).ceil() as usize;
let segment_sweep = sweep / n_segments as f64;
let n_points = if is_full_shape {
n_segments
} else {
n_segments + 1
};
for i in 0..n_points {
let angle = s_angle + (i as f64) * segment_sweep;
arc_points.push(DucPoint {
x: cx + radius_x * angle.cos(),
y: cy + radius_y * angle.sin(),
mirroring: Some(BEZIER_MIRRORING::ANGLE_LENGTH),
});
}
for i in 0..n_segments {
let p0_idx = i;
let p3_idx = (i + 1) % n_points;
let angle0 = s_angle + (i as f64) * segment_sweep;
let angle1 = s_angle + ((i + 1) as f64) * segment_sweep;
let p0_x = cx + radius_x * angle0.cos();
let p0_y = cy + radius_y * angle0.sin();
let p3_x = cx + radius_x * angle1.cos();
let p3_y = cy + radius_y * angle1.sin();
let k = (4.0 / 3.0) * (segment_sweep / 4.0).tan();
let t0_x = -radius_x * angle0.sin();
let t0_y = radius_y * angle0.cos();
let t1_x = -radius_x * angle1.sin();
let t1_y = radius_y * angle1.cos();
let cp1_x = p0_x + t0_x * k;
let cp1_y = p0_y + t0_y * k;
let cp2_x = p3_x - t1_x * k;
let cp2_y = p3_y - t1_y * k;
arc_lines.push(DucLine {
start: DucLineReference {
index: p0_idx as i32,
handle: Some(GeometricPoint { x: cp1_x, y: cp1_y }),
},
end: DucLineReference {
index: p3_idx as i32,
handle: Some(GeometricPoint { x: cp2_x, y: cp2_y }),
},
});
}
(arc_points, arc_lines)
};
let add_path_to_element = |points_to_add: &Vec<DucPoint>,
lines_to_add: &Vec<DucLine>,
all_points: &mut Vec<DucPoint>,
all_lines: &mut Vec<DucLine>|
-> (Vec<i32>, Vec<i32>) {
let point_offset = all_points.len() as i32;
let line_offset = all_lines.len() as i32;
let point_indices: Vec<i32> = (0..points_to_add.len())
.map(|i| point_offset + i as i32)
.collect();
let line_indices: Vec<i32> = (0..lines_to_add.len())
.map(|i| line_offset + i as i32)
.collect();
all_points.extend_from_slice(points_to_add);
for line in lines_to_add {
let mut new_line = line.clone();
let start_idx = line.start.index as usize;
let end_idx = line.end.index as usize;
new_line.start.index = point_indices[start_idx];
new_line.end.index = point_indices[end_idx];
all_lines.push(new_line);
}
(point_indices, line_indices)
};
let (outer_points, outer_lines) = create_arc(rx, ry, start_angle, end_angle);
let (outer_indices, _outer_line_indices) =
add_path_to_element(&outer_points, &outer_lines, &mut all_points, &mut all_lines);
if has_hole && !outer_indices.is_empty() {
let rx_inner = rx * (1.0_f64 - ratio_f64);
let ry_inner = ry * (1.0_f64 - ratio_f64);
let (inner_points_orig, inner_lines_orig) =
create_arc(rx_inner, ry_inner, start_angle, end_angle);
let inner_points: Vec<DucPoint> = inner_points_orig.into_iter().rev().collect();
let inner_lines: Vec<DucLine> = inner_lines_orig
.into_iter()
.rev()
.map(|line| {
let num_pts = inner_points.len();
DucLine {
start: DucLineReference {
index: (num_pts as i32 - 1) - line.end.index,
handle: line.end.handle.clone(),
},
end: DucLineReference {
index: (num_pts as i32 - 1) - line.start.index,
handle: line.start.handle.clone(),
},
}
})
.collect();
let (inner_indices, inner_line_indices) =
add_path_to_element(&inner_points, &inner_lines, &mut all_points, &mut all_lines);
if is_full_shape {
path_overrides.push(DucPath {
line_indices: inner_line_indices,
background: Some(ElementBackground {
content: ElementContentBase {
visible: false,
..element.base.styles.background.get(0).map_or_else(
|| ElementContentBase {
visible: false,
preference: Some(ELEMENT_CONTENT_PREFERENCE::SOLID),
src: String::new(),
opacity: 0.0,
tiling: None,
hatch: None,
image_filter: None,
},
|bg| bg.content.clone(),
)
},
}),
stroke: None,
});
} else if !inner_indices.is_empty() {
let outer_start_idx = outer_indices[0];
let outer_end_idx = *outer_indices.last().unwrap_or(&outer_start_idx);
let inner_start_idx = inner_indices[0];
let inner_end_idx = *inner_indices.last().unwrap_or(&inner_start_idx);
all_points[outer_start_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_points[outer_end_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_points[inner_start_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_points[inner_end_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_lines.push(DucLine {
start: DucLineReference {
index: outer_end_idx,
handle: None,
},
end: DucLineReference {
index: inner_start_idx,
handle: None,
},
});
all_lines.push(DucLine {
start: DucLineReference {
index: inner_end_idx,
handle: None,
},
end: DucLineReference {
index: outer_start_idx,
handle: None,
},
});
}
} else if !is_full_shape && !outer_indices.is_empty() {
let center_point = DucPoint {
x: cx,
y: cy,
mirroring: Some(BEZIER_MIRRORING::NONE),
};
let center_index = all_points.len() as i32;
all_points.push(center_point);
let outer_start_idx = outer_indices[0];
let outer_end_idx = *outer_indices.last().unwrap_or(&outer_start_idx);
all_points[outer_start_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_points[outer_end_idx as usize].mirroring = Some(BEZIER_MIRRORING::NONE);
all_lines.push(DucLine {
start: DucLineReference {
index: outer_end_idx,
handle: None,
},
end: DucLineReference {
index: center_index,
handle: None,
},
});
all_lines.push(DucLine {
start: DucLineReference {
index: center_index,
handle: None,
},
end: DucLineReference {
index: outer_start_idx,
handle: None,
},
});
}
DucLinearElement {
linear_base: DucLinearElementBase {
base: base.clone(),
points: all_points,
lines: all_lines,
path_overrides,
last_committed_point: None,
start_binding: None,
end_binding: None,
},
wipeout_below: false,
}
}
fn stream_linear(&self, linear: &DucLinearElement) -> ConversionResult<Vec<Operation>> {
PdfLinearRenderer::stream_linear(linear)
}
fn stream_freedraw(
&mut self,
freedraw: &DucFreeDrawElement,
_styles: &ResolvedStyles,
document: &mut Document,
pdf_embedder: &mut PdfEmbedder,
_resource_streamer: &mut ResourceStreamer,
) -> ConversionResult<Vec<Operation>> {
use crate::utils::freedraw_bounds::calculate_freedraw_bbox;
use hipdf::embed_pdf::{EmbedOptions, PageRange};
let mut ops = Vec::new();
let has_embedded_pdf = self.context_has_embedded_pdf(&freedraw.base.id);
if has_embedded_pdf {
let embed_id = format!("freedraw_{}", freedraw.base.id);
let bbox_offset = if let Some(bounds) = self.freedraw_bboxes.get(&freedraw.base.id) {
(bounds.min_x as f32, bounds.min_y as f32)
} else {
web_sys::console::log_1(&JsValue::from_str(&format!(
"Warning: No cached bounding box found for freedraw {}, calculating fallback",
freedraw.base.id
)));
if let Some(bounds) = calculate_freedraw_bbox(freedraw) {
(bounds.min_x as f32, bounds.min_y as f32)
} else {
(0.0, 0.0)
}
};
let options = EmbedOptions {
page_range: Some(PageRange::Single(0)), position: (bbox_offset.0, -bbox_offset.1),
max_width: Some(freedraw.base.width as f32),
max_height: Some(freedraw.base.height as f32),
preserve_aspect_ratio: true,
..Default::default()
};
match pdf_embedder.embed_pdf(document, &embed_id, &options) {
Ok(result) => {
for (name, obj_ref) in result.xobject_resources.iter() {
self.resource_cache
.insert(freedraw.base.id.clone(), name.clone());
self.new_xobjects.push((name.clone(), obj_ref.clone()));
}
ops.push(Operation::new("q", vec![])); ops.push(Operation::new(
"cm",
vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
Object::Real(1.0),
Object::Real(0.0),
Object::Real(-(freedraw.base.height as f32)),
],
));
ops.extend(result.operations);
ops.push(Operation::new("Q", vec![])); }
Err(e) => {
web_sys::console::log_1(&JsValue::from_str(&format!(
"Failed to embed Freedraw SVG-PDF {}: {}",
embed_id, e
)));
ops.push(Operation::new(
&format!("% Failed to embed Freedraw SVG-PDF {}: {}", embed_id, e),
vec![],
));
}
}
} else {
ops.push(Operation::new(
&format!(
"% No embedded PDF for Freedraw element {}",
freedraw.base.id
),
vec![],
));
}
Ok(ops)
}
fn stream_pdf_element(
&mut self,
pdf: &DucPdfElement,
document: &mut Document,
pdf_embedder: &mut PdfEmbedder,
) -> ConversionResult<Vec<Operation>> {
let file_id = match &pdf.file_id {
Some(fid) => fid.clone(),
None => {
let mut ops = Vec::new();
ops.push(Operation::new("% PDF element without file_id", vec![]));
ops.push(Operation::new(
"re",
vec![
Object::Real(0.0),
Object::Real(-(pdf.base.height as f32)),
Object::Real(pdf.base.width as f32),
Object::Real(pdf.base.height as f32),
],
));
ops.push(Operation::new("S", vec![]));
return Ok(ops);
}
};
self.stream_embedded_pdf_with_grid(
&file_id,
pdf.base.width,
pdf.base.height,
pdf.base.x,
pdf.base.y,
&pdf.grid_config,
document,
pdf_embedder,
)
}
fn stream_embedded_pdf_with_grid(
&mut self,
file_id: &str,
el_width: f64,
el_height: f64,
el_scene_x: f64,
el_scene_y: f64,
grid_config: &duc::types::DocumentGridConfig,
document: &mut Document,
pdf_embedder: &mut PdfEmbedder,
) -> ConversionResult<Vec<Operation>> {
use hipdf::embed_pdf::{EmbedOptions, MultiPageLayout, PageRange};
let embed_id = format!("pdf_{}", file_id);
let el_w = el_width as f32;
let el_h = el_height as f32;
let info = match pdf_embedder.get_pdf_info(&embed_id) {
Some(info) => info.clone(),
None => {
log::info!(
"[duc2pdf] PDF not loaded for embed_id={}, skipping",
embed_id
);
return Ok(vec![Operation::new(
&format!("% PDF not loaded: {}", embed_id),
vec![],
)]);
}
};
if info.page_count == 0 {
return Ok(vec![]);
}
let columns = grid_config.columns.max(1) as usize;
let grid_scale = if grid_config.scale == 0.0 {
1.0
} else {
grid_config.scale as f32
};
let gap_x = grid_config.gap_x as f32 * grid_scale;
let gap_y = grid_config.gap_y as f32 * grid_scale;
let first_page_alone = grid_config.first_page_alone;
let mut total_rows: usize = 0;
{
let mut col: usize = 0;
for i in 0..info.page_count {
if first_page_alone && i == 0 {
total_rows += 1;
col = 0;
continue;
}
if col == 0 {
total_rows += 1;
}
col += 1;
if col >= columns {
col = 0;
}
}
}
let total_gap_x = if columns > 1 {
gap_x * (columns as f32 - 1.0)
} else {
0.0
};
let total_gap_y = if total_rows > 1 {
gap_y * (total_rows as f32 - 1.0)
} else {
0.0
};
let content_w = (el_w - total_gap_x).max(0.0);
let content_h = (el_h - total_gap_y).max(0.0);
let cell_w = content_w / columns as f32;
let cell_h = content_h / total_rows as f32;
struct PageLayout {
page_index: usize,
local_x: f32,
local_y: f32,
scaled_w: f32,
scaled_h: f32,
page_scale: f32,
}
let mut layouts: Vec<PageLayout> = Vec::with_capacity(info.page_count);
let mut row: usize = 0;
let mut col: usize = 0;
for page_idx in 0..info.page_count {
let (page_w, page_h) = if page_idx < info.page_dimensions.len() {
info.page_dimensions[page_idx]
} else {
(595.0, 842.0)
};
let (cell_x, cell_y, used_w, used_h);
if first_page_alone && page_idx == 0 {
cell_x = 0.0;
cell_y = 0.0;
used_w = el_w;
used_h = cell_h;
row += 1;
col = 0;
} else {
cell_x = col as f32 * (cell_w + gap_x);
cell_y = row as f32 * (cell_h + gap_y);
used_w = cell_w;
used_h = cell_h;
col += 1;
if col >= columns {
col = 0;
row += 1;
}
}
let scale_x = used_w / page_w;
let scale_y = used_h / page_h;
let page_scale = scale_x.min(scale_y);
let scaled_w = page_w * page_scale;
let scaled_h = page_h * page_scale;
let offset_x = (used_w - scaled_w) / 2.0;
layouts.push(PageLayout {
page_index: page_idx,
local_x: cell_x + offset_x,
local_y: cell_y,
scaled_w,
scaled_h,
page_scale,
});
}
let mut ops = Vec::new();
let (export_x, export_y, export_w, export_h) = self.visible_scene_rect;
for layout in &layouts {
if export_w > 0.0 && export_h > 0.0 {
let margin_x = export_w * 0.1;
let margin_y = export_h * 0.1;
let page_scene_x = el_scene_x + layout.local_x as f64;
let page_scene_y = el_scene_y + layout.local_y as f64;
let page_scene_w = layout.scaled_w as f64;
let page_scene_h = layout.scaled_h as f64;
let no_overlap = page_scene_x + page_scene_w < export_x - margin_x
|| page_scene_x > export_x + export_w + margin_x
|| page_scene_y + page_scene_h < export_y - margin_y
|| page_scene_y > export_y + export_h + margin_y;
if no_overlap {
continue;
}
}
let pdf_x = layout.local_x;
let pdf_y = -(layout.local_y + layout.scaled_h);
ops.push(Operation::new("q", vec![]));
ops.push(Operation::new(
"rg",
vec![Object::Real(1.0), Object::Real(1.0), Object::Real(1.0)],
));
ops.push(Operation::new(
"re",
vec![
Object::Real(layout.local_x),
Object::Real(-(layout.local_y + layout.scaled_h)),
Object::Real(layout.scaled_w),
Object::Real(layout.scaled_h),
],
));
ops.push(Operation::new("f", vec![]));
ops.push(Operation::new("Q", vec![]));
let page_opts = EmbedOptions {
page_range: Some(PageRange::Single(layout.page_index)),
position: (pdf_x, pdf_y),
scale: (layout.page_scale, layout.page_scale),
layout: MultiPageLayout::FirstPageOnly,
preserve_aspect_ratio: false,
..Default::default()
};
match pdf_embedder.embed_pdf(document, &embed_id, &page_opts) {
Ok(result) => {
for (name, obj_ref) in result.xobject_resources.iter() {
self.resource_cache
.insert(file_id.to_string(), name.clone());
self.new_xobjects.push((name.clone(), obj_ref.clone()));
}
ops.extend(result.operations);
}
Err(e) => {
log::warn!(
"Failed to embed page {} of PDF {}: {}",
layout.page_index,
embed_id,
e
);
}
}
}
Ok(ops)
}
fn stream_doc_element(
&mut self,
doc: &DucDocElement,
document: &mut Document,
pdf_embedder: &mut PdfEmbedder,
) -> ConversionResult<Vec<Operation>> {
let file_id = match &doc.file_id {
Some(fid) => fid.clone(),
None => {
return Ok(vec![Operation::new(
"% DucDocElement without file_id",
vec![],
)]);
}
};
self.stream_embedded_pdf_with_grid(
&file_id,
doc.base.width,
doc.base.height,
doc.base.x,
doc.base.y,
&doc.grid_config,
document,
pdf_embedder,
)
}
pub fn drain_new_xobjects(&mut self) -> Vec<(String, Object)> {
let mut taken = Vec::new();
std::mem::swap(&mut self.new_xobjects, &mut taken);
taken
}
fn stream_model(
&mut self,
model: &DucModelElement,
document: &mut Document,
image_manager: &mut ImageManager,
) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let thumbnail = match &model.thumbnail {
Some(bytes) if !bytes.is_empty() => bytes,
_ => {
ops.push(Operation::new(
"% DucModelElement without thumbnail",
vec![],
));
return Ok(ops);
}
};
if model.base.width <= 0.0 || model.base.height <= 0.0 {
ops.push(Operation::new(
"% DucModelElement with invalid thumbnail bounds",
vec![],
));
return Ok(ops);
}
let cache_key = format!("model-thumbnail:{}", model.base.id);
let image_id = if let Some(&cached_id) = self.images.get(&cache_key) {
cached_id
} else {
let image = match Image::from_bytes(thumbnail.clone(), Some(cache_key.clone())) {
Ok(image) => image,
Err(error) => {
log::warn!(
"[duc2pdf] Failed to decode model thumbnail for {}: {}",
model.base.id,
error,
);
ops.push(Operation::new(
&format!(
"% Failed to decode DucModelElement thumbnail: {}",
model.base.id
),
vec![],
));
return Ok(ops);
}
};
let embedded_image_id = match image_manager.embed_image(document, image) {
Ok(image_id) => image_id.0,
Err(error) => {
log::warn!(
"[duc2pdf] Failed to embed model thumbnail for {}: {}",
model.base.id,
error,
);
ops.push(Operation::new(
&format!(
"% Failed to embed DucModelElement thumbnail: {}",
model.base.id
),
vec![],
));
return Ok(ops);
}
};
self.images.insert(cache_key.clone(), embedded_image_id);
embedded_image_id
};
let mut temp_resources = Dictionary::new();
let resource_name = image_manager.add_to_resources(&mut temp_resources, (image_id, 0));
self.new_xobjects
.push((resource_name.clone(), Object::Reference((image_id, 0))));
let y_offset = -(model.base.height as f32);
ops.extend(hipdf::images::ImageManager::draw_image(
&resource_name,
0.0,
y_offset,
model.base.width as f32,
model.base.height as f32,
));
Ok(ops)
}
fn stream_image(
&mut self,
image: &DucImageElement,
document: &mut Document,
pdf_embedder: &mut PdfEmbedder,
image_manager: &mut ImageManager,
_resource_streamer: &mut ResourceStreamer,
) -> ConversionResult<Vec<Operation>> {
use hipdf::embed_pdf::{EmbedOptions, PageRange};
let mut ops = Vec::new();
if let Some(file_id) = &image.file_id {
if self.context_has_embedded_pdf(file_id) {
let embed_id = format!("svg_{}", file_id);
let options = EmbedOptions {
page_range: Some(PageRange::Single(0)), position: (0.0, 0.0),
..Default::default()
};
match pdf_embedder.embed_pdf(document, &embed_id, &options) {
Ok(result) => {
for (name, obj_ref) in result.xobject_resources.iter() {
self.resource_cache.insert(file_id.clone(), name.clone());
self.new_xobjects.push((name.clone(), obj_ref.clone()));
}
let (mut scale_x, mut scale_y) = (1.0_f64, 1.0_f64);
if let Some(&(svg_width, svg_height)) = self.svg_dimensions.get(file_id) {
if svg_width > 0.0 && svg_height > 0.0 {
scale_x = image.base.width / svg_width;
scale_y = image.base.height / svg_height;
}
} else if let Some(crop) = &image.crop {
if crop.natural_width > 0.0 && crop.natural_height > 0.0 {
scale_x = image.base.width / crop.natural_width;
scale_y = image.base.height / crop.natural_height;
}
}
let has_rounded_clip = Self::begin_rounded_element_clip(
&mut ops,
image.base.width,
image.base.height,
image.base.styles.roundness,
);
ops.push(Operation::new("q", vec![]));
ops.push(Operation::new(
"cm",
vec![
Object::Real(scale_x as f32), Object::Real(0.0),
Object::Real(0.0),
Object::Real(scale_y as f32), Object::Real(0.0),
Object::Real(-(image.base.height as f32)), ],
));
ops.extend(result.operations);
ops.push(Operation::new("Q", vec![]));
if has_rounded_clip {
ops.push(Operation::new("Q", vec![]));
}
}
Err(e) => {
ops.push(Operation::new(
&format!("% Failed to embed SVG-PDF {}: {}", embed_id, e),
vec![],
));
println!("❌ Failed to embed SVG-PDF {}: {}", embed_id, e);
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
image.base.width,
image.base.height,
image.base.styles.roundness,
);
ops.push(Operation::new("S", vec![]));
}
}
} else {
let mut found_image_id = None;
if let Some(&image_id) = self.images.get(file_id) {
found_image_id = Some(image_id);
} else {
for (cache_key, &cache_image_id) in &self.images {
if cache_key.contains(file_id) || file_id.contains(cache_key) {
found_image_id = Some(cache_image_id);
break;
}
}
}
if let Some(image_id) = found_image_id {
let mut temp_resources = hipdf::lopdf::Dictionary::new();
let resource_name =
image_manager.add_to_resources(&mut temp_resources, (image_id, 0));
self.new_xobjects
.push((resource_name.clone(), Object::Reference((image_id, 0))));
let has_rounded_clip = Self::begin_rounded_element_clip(
&mut ops,
image.base.width,
image.base.height,
image.base.styles.roundness,
);
let y_offset = -(image.base.height as f32);
ops.extend(hipdf::images::ImageManager::draw_image(
&resource_name,
0.0,
y_offset,
image.base.width as f32,
image.base.height as f32,
));
if has_rounded_clip {
ops.push(Operation::new("Q", vec![]));
}
} else {
log::warn!("[duc2pdf] Image file_id {} NOT FOUND in cache", file_id);
ops.push(Operation::new(
&format!("% Image not found: {}", file_id),
vec![],
));
ops.push(Operation::new(
"RG",
vec![
Object::Real(1.0), Object::Real(0.0), Object::Real(0.0), ],
));
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
image.base.width,
image.base.height,
image.base.styles.roundness,
);
ops.push(Operation::new("S", vec![]));
ops.push(Operation::new("BT", vec![])); ops.push(Operation::new(
"Tf",
vec![
Object::Name("F1".as_bytes().to_vec()), Object::Real(12.0), ],
));
ops.push(Operation::new(
"Td",
vec![
Object::Real(5.0),
Object::Real(image.base.height as f32 - 20.0),
],
));
let error_msg = format!("Image not found: {}", file_id);
ops.push(Operation::new(
"Tj",
vec![Object::string_literal(error_msg.as_str())],
));
ops.push(Operation::new("ET", vec![])); }
}
} else {
ops.push(Operation::new("% Image element without file_id", vec![]));
ops.push(Operation::new(
"RG",
vec![
Object::Real(0.0), Object::Real(0.0), Object::Real(1.0), ],
));
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
image.base.width,
image.base.height,
image.base.styles.roundness,
);
ops.push(Operation::new("S", vec![]));
}
Ok(ops)
}
fn context_has_embedded_pdf(&self, file_id: &str) -> bool {
self.embedded_pdfs.contains_key(file_id)
}
fn stream_frame(&self, frame: &DucFrameElement) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
let styles = &frame.stack_element_base.base.styles;
let has_background = styles
.background
.iter()
.any(|background| background.content.visible);
let has_stroke = styles.stroke.iter().any(|stroke| stroke.content.visible);
if has_background || has_stroke {
Self::append_rounded_rect_path(
&mut ops,
0.0,
0.0,
frame.stack_element_base.base.width,
frame.stack_element_base.base.height,
styles.roundness,
);
if has_background && has_stroke {
ops.push(Operation::new("B", vec![]));
} else if has_background {
ops.push(Operation::new("f", vec![]));
} else {
ops.push(Operation::new("S", vec![]));
}
}
ops.push(Operation::new(
"% Frame element - child elements will be streamed with frame-relative positioning",
vec![],
));
Ok(ops)
}
fn stream_plot(&self, plot: &DucPlotElement) -> ConversionResult<Vec<Operation>> {
let mut ops = Vec::new();
if plot.stack_element_base.stack_base.is_plot {
ops.push(Operation::new(
"% Plot page - handled at page level",
vec![],
));
} else {
ops.push(Operation::new("q", vec![]));
ops.push(Operation::new(
"re",
vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(plot.stack_element_base.base.width as f32),
Object::Real(plot.stack_element_base.base.height as f32),
],
));
ops.push(Operation::new("W", vec![])); ops.push(Operation::new("n", vec![]));
ops.push(Operation::new("% TODO: Stream plot child elements", vec![]));
ops.push(Operation::new("Q", vec![])); }
Ok(ops)
}
}