use std::io::Write as IoWrite;
use stet_core::object::EntityId;
use stet_graphics::device::TextParams;
use crate::content_stream::fmt_num;
use crate::font_tracker::FontTracker;
pub fn emit_text_batch(buf: &mut Vec<u8>, batch: &[&TextParams], font_tracker: &FontTracker) {
if batch.is_empty() {
return;
}
let first = batch[0];
let Some(pdf_font_name) = font_tracker.get_pdf_name(EntityId(first.font_entity)) else {
return;
};
if first.font_size.abs() < 0.001 {
return;
}
let has_widths = !font_tracker
.fonts()
.find(|u| u.font_entity == EntityId(first.font_entity))
.is_none_or(|u| u.widths.is_empty());
buf.extend(b"BT\n");
writeln!(buf, "/{} 1 Tf", pdf_font_name).unwrap();
let mut last_color: Option<ColorKey> = None;
let mut last_paint_type: i32 = -1;
let mut i = 0;
while i < batch.len() {
let text_obj = batch[i];
let (tm_a, tm_b, tm_c, tm_d) = compute_text_matrix(text_obj);
let mut run: Vec<usize> = vec![i];
if has_widths {
let tm_key = TmKey::new(tm_a, tm_b, tm_c, tm_d);
let adv_len_sq = tm_a * tm_a + tm_b * tm_b;
let mut j = i + 1;
while j < batch.len() {
let next = batch[j];
if color_key(&next.color, next.paint_type)
!= color_key(&text_obj.color, text_obj.paint_type)
{
break;
}
let (ntm_a, ntm_b, ntm_c, ntm_d) = compute_text_matrix(next);
if TmKey::new(ntm_a, ntm_b, ntm_c, ntm_d) != tm_key {
break;
}
let prev_idx = *run.last().unwrap();
let prev = batch[prev_idx];
let ndx = next.start_x - prev.start_x;
let ndy = next.start_y - prev.start_y;
if adv_len_sq > 1e-10 {
let adv_len = adv_len_sq.sqrt();
let perp_dist = (-tm_b * ndx + tm_a * ndy).abs() / adv_len;
if perp_dist > 0.5 {
break;
}
let along_dist = (ndx * tm_a + ndy * tm_b) / adv_len_sq;
let mut prev_text_width = 0.0;
for &byte_val in &prev.text {
if let Some(w) = font_tracker
.get_glyph_width(EntityId(prev.font_entity), byte_val as u16)
{
prev_text_width += w as f64;
}
}
let gap = (along_dist * 1000.0).abs() - prev_text_width;
if gap > 500.0 {
break;
}
} else if ndx.abs() > 0.5 || ndy.abs() > 0.5 {
break;
}
run.push(j);
j += 1;
}
}
let cur_color = color_key(&text_obj.color, text_obj.paint_type);
if last_color.as_ref() != Some(&cur_color) || last_paint_type != text_obj.paint_type {
if text_obj.paint_type == 2 {
emit_stroke_color(buf, text_obj);
fmt_num(buf, text_obj.stroke_width);
buf.extend(b" w\n");
buf.extend(b"1 Tr\n");
} else {
if last_paint_type == 2 {
buf.extend(b"0 Tr\n");
}
emit_text_color(buf, text_obj);
}
last_color = Some(cur_color);
last_paint_type = text_obj.paint_type;
}
emit_tm(
buf,
tm_a,
tm_b,
tm_c,
tm_d,
text_obj.start_x,
text_obj.start_y,
);
if run.len() == 1 {
emit_text_string(buf, &text_obj.text);
buf.extend(b" Tj\n");
} else {
let adv_len_sq = tm_a * tm_a + tm_b * tm_b;
let mut tj_parts: Vec<TjPart> = Vec::new();
for (k, &run_idx) in run.iter().enumerate() {
let tobj = batch[run_idx];
if k > 0 {
let prev_idx = run[k - 1];
let prev = batch[prev_idx];
let dx = tobj.start_x - prev.start_x;
let dy = tobj.start_y - prev.start_y;
let mut can_kern = false;
if adv_len_sq > 1e-10 {
let text_advance = (dx * tm_a + dy * tm_b) / adv_len_sq;
let mut prev_total_width = 0.0;
let mut all_widths_found = true;
for &b in &prev.text {
if let Some(w) =
font_tracker.get_glyph_width(EntityId(prev.font_entity), b as u16)
{
prev_total_width += w as f64;
} else {
all_widths_found = false;
break;
}
}
if all_widths_found && !prev.text.is_empty() {
let kern = prev_total_width - text_advance * 1000.0;
let kern_rounded = kern.round() as i64;
if kern_rounded != 0 {
tj_parts.push(TjPart::Kern(kern_rounded));
}
can_kern = true;
}
}
if !can_kern {
if !tj_parts.is_empty() {
emit_tj_array(buf, &tj_parts);
tj_parts.clear();
}
emit_tm(buf, tm_a, tm_b, tm_c, tm_d, tobj.start_x, tobj.start_y);
}
}
tj_parts.push(TjPart::Text(tobj.text.as_slice()));
}
if !tj_parts.is_empty() {
emit_tj_array(buf, &tj_parts);
}
}
i = *run.last().unwrap() + 1;
}
buf.extend(b"ET\n");
}
enum TjPart<'a> {
Text(&'a [u8]),
Kern(i64),
}
fn emit_tj_array(buf: &mut Vec<u8>, parts: &[TjPart]) {
buf.push(b'[');
for part in parts {
match part {
TjPart::Text(text) => emit_text_string(buf, text),
TjPart::Kern(k) => write!(buf, "{}", k).unwrap(),
}
}
buf.extend(b"] TJ\n");
}
#[derive(PartialEq)]
struct TmKey {
a: i64,
b: i64,
c: i64,
d: i64,
}
impl TmKey {
fn new(a: f64, b: f64, c: f64, d: f64) -> Self {
Self {
a: (a * 100.0).round() as i64,
b: (b * 100.0).round() as i64,
c: (c * 100.0).round() as i64,
d: (d * 100.0).round() as i64,
}
}
}
#[derive(PartialEq)]
struct ColorKey {
r: u16,
g: u16,
b: u16,
cmyk: Option<(u16, u16, u16, u16)>,
paint_type: i32,
}
fn quantize_color(v: f64) -> u16 {
(v.clamp(0.0, 1.0) * 10000.0) as u16
}
fn color_key(c: &stet_graphics::color::DeviceColor, paint_type: i32) -> ColorKey {
ColorKey {
r: quantize_color(c.r),
g: quantize_color(c.g),
b: quantize_color(c.b),
cmyk: c.native_cmyk.map(|(c, m, y, k)| {
(
quantize_color(c),
quantize_color(m),
quantize_color(y),
quantize_color(k),
)
}),
paint_type,
}
}
fn compute_text_matrix(params: &TextParams) -> (f64, f64, f64, f64) {
let ctm = params.ctm;
let fm = params.font_matrix;
if fm[0] != 0.0 || fm[1] != 0.0 || fm[2] != 0.0 || fm[3] != 0.0 {
(
fm[0] * ctm[0] + fm[1] * ctm[2],
fm[0] * ctm[1] + fm[1] * ctm[3],
fm[2] * ctm[0] + fm[3] * ctm[2],
fm[2] * ctm[1] + fm[3] * ctm[3],
)
} else {
let scale_x = (ctm[0] * ctm[0] + ctm[1] * ctm[1]).sqrt();
let scale_y = (ctm[2] * ctm[2] + ctm[3] * ctm[3]).sqrt();
let eff = (scale_x * scale_y).sqrt();
if eff > 1e-10 {
let pt = params.font_size / eff;
(pt * ctm[0], pt * ctm[1], pt * ctm[2], pt * ctm[3])
} else {
(params.font_size, 0.0, 0.0, -params.font_size)
}
}
}
fn emit_tm(buf: &mut Vec<u8>, a: f64, b: f64, c: f64, d: f64, tx: f64, ty: f64) {
fmt_num(buf, a);
buf.push(b' ');
fmt_num(buf, b);
buf.push(b' ');
fmt_num(buf, c);
buf.push(b' ');
fmt_num(buf, d);
buf.push(b' ');
fmt_num(buf, tx);
buf.push(b' ');
fmt_num(buf, ty);
buf.extend(b" Tm\n");
}
fn emit_text_color(buf: &mut Vec<u8>, params: &TextParams) {
let c = ¶ms.color;
if let Some((c_val, m, y, k)) = c.native_cmyk {
fmt_num(buf, c_val);
buf.push(b' ');
fmt_num(buf, m);
buf.push(b' ');
fmt_num(buf, y);
buf.push(b' ');
fmt_num(buf, k);
buf.extend(b" k\n");
} else if c.r == c.g && c.g == c.b {
fmt_num(buf, c.r);
buf.extend(b" g\n");
} else {
fmt_num(buf, c.r);
buf.push(b' ');
fmt_num(buf, c.g);
buf.push(b' ');
fmt_num(buf, c.b);
buf.extend(b" rg\n");
}
}
fn emit_stroke_color(buf: &mut Vec<u8>, params: &TextParams) {
let c = ¶ms.color;
if let Some((c_val, m, y, k)) = c.native_cmyk {
fmt_num(buf, c_val);
buf.push(b' ');
fmt_num(buf, m);
buf.push(b' ');
fmt_num(buf, y);
buf.push(b' ');
fmt_num(buf, k);
buf.extend(b" K\n");
} else if c.r == c.g && c.g == c.b {
fmt_num(buf, c.r);
buf.extend(b" G\n");
} else {
fmt_num(buf, c.r);
buf.push(b' ');
fmt_num(buf, c.g);
buf.push(b' ');
fmt_num(buf, c.b);
buf.extend(b" RG\n");
}
}
fn emit_text_string(buf: &mut Vec<u8>, text: &[u8]) {
buf.push(b'<');
for &b in text {
write!(buf, "{:02X}", b).unwrap();
}
buf.push(b'>');
}