use serde::{Deserialize, Serialize};
use super::{Element, RenderContext};
use crate::{compliance::ua::StructTag, styles::StyleResolver};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Section {
pub title: String,
pub level: u8,
#[serde(default)]
pub style_ref: Option<String>,
}
impl Section {
pub fn new(title: impl Into<String>, level: u8) -> Self {
Self {
title: title.into(),
level: level.clamp(1, 3),
style_ref: None,
}
}
pub fn style(mut self, name: impl Into<String>) -> Self {
self.style_ref = Some(name.into());
self
}
fn default_style_name(&self) -> &'static str {
match self.level {
1 => "heading_1",
2 => "heading_2",
_ => "heading_3",
}
}
}
impl Section {
pub fn level(&self) -> u8 {
self.level
}
pub fn heading_text(&self) -> &str {
&self.title
}
}
impl Element for Section {
fn as_section_info(&self) -> Option<(u8, &str)> {
Some((self.level, &self.title))
}
fn estimated_height_mm(&self) -> f64 {
let pre = match self.level { 1 => 8.0, 2 => 6.0, _ => 4.0 };
let post = match self.level { 1 => 4.0, 2 => 3.0, _ => 2.0 };
pre + 7.0 + post
}
fn render(&self, ctx: &mut RenderContext) -> crate::Result<super::RenderResult> {
let style_name = self.style_ref.as_deref()
.unwrap_or_else(|| self.default_style_name());
let resolver = StyleResolver::new(&ctx.style.named_styles, &ctx.style);
let resolved = resolver.resolve(style_name)?;
let fs = resolved.font_size;
if resolved.space_before_mm > 0.0 && !ctx.flow.is_top_of_page() {
ctx.flow.advance(resolved.space_before_mm);
}
let color = resolved.color.clone()
.unwrap_or_else(|| ctx.style.text_color.clone());
let Some(font_ref) = ctx.get_font_ref(resolved.bold, resolved.italic) else {
ctx.flow.advance(self.estimated_height_mm() - resolved.space_before_mm);
return Ok(super::RenderResult::done());
};
let ua_tag = match self.level {
1 => StructTag::H1,
2 => StructTag::H2,
3 => StructTag::H3,
4 => StructTag::H4,
5 => StructTag::H5,
_ => StructTag::H6,
};
if ctx.ua_enabled() {
if let Some(prev) = ctx.last_heading_level {
if self.level > prev + 1 {
eprintln!(
"PDF/UA-2 WARNING: heading level skipped from H{} to H{}",
prev, self.level
);
}
}
ctx.last_heading_level = Some(self.level);
}
let mcid_opt = if ctx.ua_enabled() {
let mcid = ctx.ua_tag_element(ua_tag, None);
ctx.backend.begin_tagged_content(
match self.level { 1 => b"H1", 2 => b"H2", 3 => b"H3", 4 => b"H4", 5 => b"H5", _ => b"H6" },
mcid,
);
Some(mcid)
} else {
None
};
let x = ctx.layout.content_x_mm;
let y = ctx.flow.cursor_y_mm;
ctx.draw_text(&self.title, x, y, fs, font_ref, &color)?;
let page_idx = ctx.backend.current_page_idx();
ctx.backend.add_outline_entry(&self.title, self.level, page_idx, y);
let line_h = ctx.layout_engine.line_height_mm(&ctx.fonts, fs);
ctx.flow.advance(line_h + resolved.space_after_mm);
if mcid_opt.is_some() {
ctx.backend.end_tagged_content();
}
Ok(super::RenderResult::done())
}
}