use std::collections::HashMap;
use pdf_writer::Content;
use crate::fonts::FontEntry;
use crate::model::{Paragraph, SectionProperties, TextAnchor, Textbox, VRelativeFrom};
use super::color::{fill_rgb, stroke_rgb};
use super::layout::{
LinkAnnotation, build_paragraph_lines, build_tabbed_line, render_paragraph_lines,
tallest_run_metrics,
};
use super::list_label::render_list_label;
use super::positioning::resolve_h_position;
use super::smartart::draw_shape_path;
use super::wordart;
use super::{GradientSpec, RenderContext, render_shape_fill, resolve_line_h};
pub(super) fn render_single_textbox(
tb: &Textbox,
sp: &SectionProperties,
col_x: f32,
col_w: f32,
text_width: f32,
slot_top: f32,
content: &mut Content,
gradient_specs: &mut Vec<GradientSpec>,
ctx: &RenderContext,
page_links: &mut Vec<LinkAnnotation>,
) {
let tb_x = resolve_h_position(
tb.h_relative_from,
&tb.h_position,
tb.width_pt,
sp,
col_x,
col_w,
text_width,
);
let tb_y_top = match tb.v_relative_from {
VRelativeFrom::Page => sp.page_height - tb.v_offset_pt,
VRelativeFrom::Margin | VRelativeFrom::TopMargin => {
sp.page_height - sp.margin_top - tb.v_offset_pt
}
VRelativeFrom::Paragraph => slot_top - tb.v_offset_pt,
};
let tb_height = if matches!(tb.auto_fit, crate::model::AutoFit::Shape) {
let tmp_w = if tb.no_text_wrap {
10000.0
} else {
(tb.width_pt - tb.margin_left - tb.margin_right).max(0.0)
};
let empty_imgs: HashMap<usize, String> = HashMap::new();
let mut h = 0.0f32;
for tp in &tb.paragraphs {
let tp_ls = tp.line_spacing.unwrap_or(ctx.doc_line_spacing);
let tw = (tmp_w - tp.indent_left - tp.indent_right).max(1.0);
let hang = if !tp.list_label.is_empty() {
if tp.indent_first_line > 0.0 && tp.indent_hanging == 0.0 {
-tp.indent_first_line
} else {
0.0
}
} else if tp.indent_hanging > 0.0 {
tp.indent_hanging
} else {
-tp.indent_first_line
};
let lines = if tp.runs.iter().any(|r| r.is_tab) {
build_tabbed_line(
&tp.runs, ctx.fonts, &tp.tab_stops, tp.indent_left,
tw, hang, &empty_imgs, ctx.default_tab_stop,
)
} else {
build_paragraph_lines(
&tp.runs, ctx.fonts, tw, hang, &empty_imgs, None, None, None,
)
};
let (fs, lhr, _) = tallest_run_metrics(&tp.runs, ctx.fonts);
let lh = resolve_line_h(tp_ls, fs, lhr);
h += tp.space_before + lines.len().max(1) as f32 * lh + tp.space_after;
}
h + tb.margin_top + tb.margin_bottom
} else {
tb.height_pt
};
if let Some(ref fill) = tb.fill {
render_shape_fill(
content,
fill,
tb_x,
tb_y_top - tb_height,
tb.width_pt,
tb_height,
&tb.shape_type,
gradient_specs,
);
}
if let Some(stroke) = tb.stroke_color {
if tb.stroke_width > 0.0 {
content.save_state();
content.set_line_width(tb.stroke_width);
stroke_rgb(content, stroke);
draw_shape_path(
content,
tb_x,
tb_y_top - tb_height,
tb.width_pt,
tb_height,
&tb.shape_type,
);
content.stroke();
content.restore_state();
}
}
let content_x = tb_x + tb.margin_left;
let natural_w = (tb.width_pt - tb.margin_left - tb.margin_right).max(0.0);
let content_w = if tb.no_text_wrap { 10000.0 } else { natural_w };
let align_w = natural_w;
let needs_clip = !matches!(tb.auto_fit, crate::model::AutoFit::Shape);
if needs_clip {
content.save_state();
content.rect(tb_x, tb_y_top - tb_height, tb.width_pt, tb_height);
content.clip_nonzero();
content.end_path();
}
if tb
.text_warp
.as_ref()
.is_some_and(|w| w.preset != "textNoShape")
{
if wordart::render_warped_textbox(tb, content, ctx.fonts, tb_x, tb_y_top, align_w) {
if needs_clip { content.restore_state(); }
return;
}
if wordart::render_text_on_path(tb, content, ctx.fonts, tb_x, tb_y_top, align_w) {
if needs_clip { content.restore_state(); }
return;
}
}
let anchor_offset = match tb.text_anchor {
TextAnchor::Top => 0.0,
TextAnchor::Middle | TextAnchor::Bottom => {
let empty_inline_imgs_pre: HashMap<usize, String> = HashMap::new();
let mut total_h = 0.0f32;
for tp in &tb.paragraphs {
let tp_ls = tp.line_spacing.unwrap_or(ctx.doc_line_spacing);
let tp_text_w = (content_w - tp.indent_left - tp.indent_right).max(1.0);
let text_hanging = if !tp.list_label.is_empty() {
if let Some(nts) = tp.num_level_tab_stop {
if nts < tp.indent_left && (tp.indent_left - tp.indent_hanging).abs() < 0.5 {
(tp.indent_left - nts).max(0.0)
} else if tp.indent_first_line > 0.0 && tp.indent_hanging == 0.0 {
-tp.indent_first_line
} else {
0.0
}
} else if tp.indent_first_line > 0.0 && tp.indent_hanging == 0.0 {
-tp.indent_first_line
} else {
0.0
}
} else if tp.indent_hanging > 0.0 {
tp.indent_hanging
} else {
-tp.indent_first_line
};
let has_tabs = tp.runs.iter().any(|r| r.is_tab);
let lines = if has_tabs {
build_tabbed_line(
&tp.runs, ctx.fonts, &tp.tab_stops, tp.indent_left,
tp_text_w, text_hanging, &empty_inline_imgs_pre,
ctx.default_tab_stop,
)
} else {
build_paragraph_lines(
&tp.runs, ctx.fonts, tp_text_w, text_hanging, &empty_inline_imgs_pre, None, None, None,
)
};
let (fs, lhr, _) = tallest_run_metrics(&tp.runs, ctx.fonts);
let lh = resolve_line_h(tp_ls, fs, lhr);
let n = lines.len().max(1) as f32;
total_h += tp.space_before + n * lh + tp.space_after;
}
let available = tb_height - tb.margin_top - tb.margin_bottom;
let gap = (available - total_h).max(0.0);
match tb.text_anchor {
TextAnchor::Middle => gap / 2.0,
TextAnchor::Bottom => gap,
TextAnchor::Top => 0.0,
}
}
};
let clip_bottom = if needs_clip {
Some(tb_y_top - tb_height)
} else {
None
};
if let Some(glow) = wordart::find_text_glow(tb) {
content.save_state();
stroke_rgb(content, glow.color);
content.set_line_width(glow.radius_pt * 2.0);
content.set_line_join(pdf_writer::types::LineJoinStyle::RoundJoin);
content.set_text_rendering_mode(pdf_writer::types::TextRenderingMode::Stroke);
let mut discard_links: Vec<LinkAnnotation> = Vec::new();
render_textbox_paragraphs(
&tb.paragraphs, content, content_x, content_w, align_w,
tb_y_top - tb.margin_top - anchor_offset,
0.0, 0.0, None, false, &mut discard_links, ctx, clip_bottom,
);
content.restore_state();
}
if let Some(shadow) = wordart::find_text_shadow(tb) {
content.save_state();
let [sr, sg, sb] = shadow.color;
let shadow_color = [
(sr as f32 * shadow.alpha) as u8,
(sg as f32 * shadow.alpha) as u8,
(sb as f32 * shadow.alpha) as u8,
];
let mut discard_links: Vec<LinkAnnotation> = Vec::new();
render_textbox_paragraphs(
&tb.paragraphs, content, content_x, content_w, align_w,
tb_y_top - tb.margin_top - anchor_offset,
shadow.offset_x, shadow.offset_y, Some(shadow_color),
false, &mut discard_links, ctx, clip_bottom,
);
content.restore_state();
}
render_textbox_paragraphs(
&tb.paragraphs, content, content_x, content_w, align_w,
tb_y_top - tb.margin_top - anchor_offset,
0.0, 0.0, None, true, page_links, ctx, clip_bottom,
);
if needs_clip {
content.restore_state();
}
}
pub(super) fn render_textbox_paragraphs(
paragraphs: &[Paragraph],
content: &mut Content,
content_x: f32,
content_w: f32,
align_w: f32,
start_y: f32,
x_offset: f32,
y_offset: f32,
force_color: Option<[u8; 3]>,
render_labels: bool,
links: &mut Vec<LinkAnnotation>,
ctx: &RenderContext,
clip_bottom: Option<f32>,
) {
let mut cursor_y = start_y;
let mut prev_space_after = 0.0f32;
let empty_imgs: HashMap<usize, String> = HashMap::new();
for (tp_idx, tp) in paragraphs.iter().enumerate() {
let inter_gap = if tp_idx == 0 {
tp.space_before
} else {
prev_space_after.max(tp.space_before)
};
if let Some(bottom) = clip_bottom {
if cursor_y - inter_gap < bottom {
break;
}
}
let tp_ls = tp.line_spacing.unwrap_or(ctx.doc_line_spacing);
let tp_text_w = (content_w - tp.indent_left - tp.indent_right).max(1.0);
let tp_align_w = (align_w - tp.indent_left - tp.indent_right).max(1.0);
let text_hanging = if !tp.list_label.is_empty() {
if let Some(nts) = tp.num_level_tab_stop {
(tp.indent_left - nts).max(0.0)
} else if tp.indent_first_line > 0.0 && tp.indent_hanging == 0.0 {
-tp.indent_first_line
} else {
0.0
}
} else if tp.indent_hanging > 0.0 {
tp.indent_hanging
} else {
-tp.indent_first_line
};
let tb_lines = if tp.runs.iter().any(|r| r.is_tab) {
build_tabbed_line(
&tp.runs, ctx.fonts, &tp.tab_stops, tp.indent_left,
tp_text_w, text_hanging, &empty_imgs, ctx.default_tab_stop,
)
} else {
build_paragraph_lines(
&tp.runs, ctx.fonts, tp_text_w, text_hanging, &empty_imgs, None, None, None,
)
};
if tb_lines.is_empty() {
let (fs, lhr, _) = tallest_run_metrics(&tp.runs, ctx.fonts);
let lh = resolve_line_h(tp_ls, fs, lhr);
cursor_y -= inter_gap + lh;
prev_space_after = tp.space_after;
continue;
}
let (tb_fs, tb_lhr, tb_ar) = tallest_run_metrics(&tp.runs, ctx.fonts);
let tb_line_h = resolve_line_h(tp_ls, tb_fs, tb_lhr);
let tb_baseline = cursor_y - inter_gap - tb_fs * tb_ar.unwrap_or(0.75) - y_offset;
let tp_text_x = content_x + tp.indent_left + x_offset;
if let Some(c) = force_color {
fill_rgb(content, c);
}
if render_labels {
render_list_label(
content,
tp,
ctx.fonts,
content_x + tp.indent_left - tp.indent_hanging,
tb_baseline,
tb_fs,
);
}
render_paragraph_lines(
content, &tb_lines, &tp.alignment, tp_text_x, tp_align_w,
tb_baseline, tb_line_h, tb_lines.len(), 0,
links, 0.0, ctx.fonts, None,
);
cursor_y -= inter_gap + (tb_lines.len() as f32) * tb_line_h;
prev_space_after = tp.space_after;
}
}