use crate::ConversionResult;
use duc::types::{
ELEMENT_CONTENT_PREFERENCE, HATCH_STYLE, STROKE_CAP, STROKE_JOIN, STROKE_PREFERENCE,
DucElementEnum, DucElementStylesBase, DucHatchStyle, ElementBackground, ElementStroke,
};
use hipdf::hatching::{HatchStyle, HatchingManager};
use hipdf::lopdf::{content::Operation, Object};
use std::collections::HashMap;
pub struct StyleResolver {
parent_overrides: HashMap<String, DucElementStylesBase>,
}
impl StyleResolver {
pub fn new() -> Self {
Self {
parent_overrides: HashMap::new(),
}
}
pub fn add_parent_override(&mut self, element_id: String, styles: DucElementStylesBase) {
self.parent_overrides.insert(element_id, styles);
}
pub fn resolve_styles(&self, element: &DucElementEnum) -> ResolvedStyles {
let base = crate::builder::DucToPdfBuilder::get_element_base(element);
let mut resolved = {
let mut r = ResolvedStyles::from_base_styles(&base.styles);
r.apply_base_styles(&base.styles);
r
};
if let Some(parent_styles) = self.parent_overrides.get(&base.id) {
resolved.apply_base_styles(parent_styles);
}
resolved
}
pub fn resolve_dynamic_fields(&self, text: &str, _element: &DucElementEnum) -> String {
text.to_string()
}
}
#[derive(Debug, Clone)]
pub struct ResolvedStyles {
pub background: Vec<ResolvedBackground>,
pub stroke: Vec<ResolvedStroke>,
pub opacity: f64,
pub roundness: f64,
}
#[derive(Debug, Clone)]
pub struct ResolvedBackground {
pub preference: ELEMENT_CONTENT_PREFERENCE,
pub color: String,
pub opacity: f64,
pub visible: bool,
pub hatch: Option<DucHatchStyle>,
}
#[derive(Debug, Clone)]
pub struct ResolvedStroke {
pub preference: STROKE_PREFERENCE,
pub color: String,
pub width: f64,
pub opacity: f64,
pub cap: STROKE_CAP,
pub join: STROKE_JOIN,
pub dash_pattern: Option<Vec<f64>>,
pub visible: bool,
}
impl ResolvedStyles {
pub fn from_base_styles(styles: &DucElementStylesBase) -> Self {
let mut resolved = Self {
background: Vec::new(),
stroke: Vec::new(),
opacity: styles.opacity,
roundness: styles.roundness,
};
for bg in &styles.background {
resolved
.background
.push(ResolvedBackground::from_element_background(bg));
}
for stroke in &styles.stroke {
resolved
.stroke
.push(ResolvedStroke::from_element_stroke(stroke));
}
resolved
}
pub fn get_combined_fill_opacity(&self) -> f64 {
let element_opacity = self.opacity.clamp(0.0, 1.0);
if let Some(bg) = self.background.iter().find(|b| b.visible) {
(element_opacity * bg.effective_opacity()).clamp(0.0, 1.0)
} else {
element_opacity
}
}
pub fn get_combined_stroke_opacity(&self) -> f64 {
let element_opacity = self.opacity.clamp(0.0, 1.0);
if let Some(stroke) = self.stroke.iter().find(|s| s.visible) {
(element_opacity * stroke.effective_opacity()).clamp(0.0, 1.0)
} else {
element_opacity
}
}
pub fn apply_base_styles(&mut self, styles: &DucElementStylesBase) {
self.opacity = styles.opacity;
self.roundness = styles.roundness;
for bg in &styles.background {
self.background
.push(ResolvedBackground::from_element_background(bg));
}
for stroke in &styles.stroke {
self.stroke
.push(ResolvedStroke::from_element_stroke(stroke));
}
}
pub fn resolve_dynamic_fields(&self, text: &str, element: &DucElementEnum) -> String {
let mut result = text.to_string();
let base = crate::builder::DucToPdfBuilder::get_element_base(element);
result = result.replace("{width}", &base.width.to_string());
result = result.replace("{height}", &base.height.to_string());
result = result.replace("{x}", &base.x.to_string());
result = result.replace("{y}", &base.y.to_string());
result = result.replace("{angle}", &base.angle.to_string());
result = result.replace("{id}", &base.id);
let now = chrono::Utc::now();
result = result.replace("{date}", &now.format("%Y-%m-%d").to_string());
result = result.replace("{time}", &now.format("%H:%M:%S").to_string());
result = result.replace("{datetime}", &now.format("%Y-%m-%d %H:%M:%S").to_string());
let _ = element;
result
}
}
impl ResolvedBackground {
fn from_element_background(bg: &ElementBackground) -> Self {
Self {
preference: bg
.content
.preference
.unwrap_or(ELEMENT_CONTENT_PREFERENCE::SOLID),
color: bg.content.src.clone(),
opacity: bg.content.opacity,
visible: bg.content.visible,
hatch: bg.content.hatch.clone(),
}
}
pub fn effective_opacity(&self) -> f64 {
self.opacity.clamp(0.0, 1.0)
}
}
impl StyleResolver {
pub fn resolve_background(&self, bg: &ElementBackground) -> Option<ResolvedBackground> {
Some(ResolvedBackground::from_element_background(bg))
}
}
impl ResolvedStroke {
fn from_element_stroke(stroke: &ElementStroke) -> Self {
Self {
preference: stroke.style.preference.unwrap_or(STROKE_PREFERENCE::SOLID),
color: stroke.content.src.clone(),
width: stroke.width,
opacity: stroke.content.opacity,
cap: stroke.style.cap.unwrap_or(STROKE_CAP::BUTT),
join: stroke.style.join.unwrap_or(STROKE_JOIN::MITER),
dash_pattern: stroke.style.dash.clone(),
visible: stroke.content.visible,
}
}
pub fn effective_opacity(&self) -> f64 {
self.opacity.clamp(0.0, 1.0)
}
}
impl StyleResolver {
pub fn apply_hatching_pattern_with_dims(
&self,
backgrounds: &[ElementBackground],
hatching_manager: &mut HatchingManager,
ops: &mut Vec<Operation>,
width: f64,
height: f64,
) -> ConversionResult<()> {
for background in backgrounds {
if let Some(resolved_bg) = self.resolve_background(background) {
if let Some(hatch_style) = resolved_bg.hatch {
let _pattern_id = self.create_custom_hatching_pattern(
&hatch_style,
hatching_manager,
width,
height,
)?;
ops.push(Operation::new("% Custom hatching pattern applied", vec![]));
ops.push(Operation::new("% Hatching pattern info", vec![]));
ops.push(Operation::new(
&format!(
"% Pattern: {}, Scale: {}, Angle: {}",
hatch_style.pattern_name,
hatch_style.pattern_scale,
hatch_style.pattern_angle
),
vec![],
));
ops.push(Operation::new(
"re",
vec![
Object::Real(0.0),
Object::Real(-(height as f32)),
Object::Real(width as f32),
Object::Real(height as f32),
],
));
ops.push(Operation::new("f", vec![])); }
}
}
Ok(())
}
fn create_custom_hatching_pattern(
&self,
hatch_style: &DucHatchStyle,
_hatching_manager: &mut HatchingManager,
width: f64,
height: f64,
) -> ConversionResult<String> {
if let Some(_custom_pattern) = &hatch_style.custom_pattern {
return self.create_predefined_hatch_pattern(hatch_style, width, height);
}
self.create_predefined_hatch_pattern(hatch_style, width, height)
}
fn create_predefined_hatch_pattern(
&self,
hatch_style: &DucHatchStyle,
width: f64,
height: f64,
) -> ConversionResult<String> {
use hipdf::hatching::HatchStyle as HipdfHatchStyle;
let _scaled_width = width * hatch_style.pattern_scale as f64;
let _scaled_height = height * hatch_style.pattern_scale as f64;
let hipdf_style = match hatch_style.hatch_style {
HATCH_STYLE::NORMAL => HipdfHatchStyle::DiagonalRight,
HATCH_STYLE::OUTER => HipdfHatchStyle::Cross,
HATCH_STYLE::IGNORE => {
return Ok("no_hatch".to_string());
}
};
let pattern_id = format!(
"{:?}_{}_{}",
hipdf_style, hatch_style.pattern_scale, hatch_style.pattern_angle
);
Ok(pattern_id)
}
pub fn convert_duc_hatch_to_hipdf(
&self,
duc_hatch: &DucHatchStyle,
) -> ConversionResult<HatchStyle> {
match duc_hatch.hatch_style {
HATCH_STYLE::NORMAL => Ok(HatchStyle::DiagonalRight), HATCH_STYLE::OUTER => Ok(HatchStyle::DiagonalLeft), HATCH_STYLE::IGNORE => {
if duc_hatch.custom_pattern.is_some() {
Ok(HatchStyle::Cross)
} else {
Ok(HatchStyle::DiagonalRight) }
}
}
}
pub fn has_hatching(&self, backgrounds: &[ElementBackground]) -> bool {
backgrounds.iter().any(|bg| {
if let Some(resolved_bg) = self.resolve_background(bg) {
resolved_bg.hatch.is_some()
} else {
false
}
})
}
}