use pdfrum_object::{Dict, Name, Object, Resolve, names as obj_names};
use crate::ap::emit::{Content, Float, PaintOp, color_op};
use crate::ap::freetext;
use crate::ap::markup::Generated;
use crate::color::Color;
use crate::geom;
use crate::vt;
const FONT_SIZE: f32 = 12.0;
const BORDER_WIDTH: f32 = 1.0;
const TEXT_OFFSET: (f32, f32) = (3.0, -3.0);
const FONT_ALIAS: &[u8] = b"FONT";
#[must_use]
pub fn popup<R: Resolve>(
dict: &Dict,
metrics: &vt::Metrics<'_>,
encode: &dyn Fn(u32) -> Vec<u8>,
r: &R,
) -> Option<Generated> {
let rect = geom::normalize(dict.rect(obj_names::RECT, r));
let mut out = Content::new();
out.raw("/GS gs\n");
out.raw(&color_op(Color::Rgb(1.0, 1.0, 0.0), PaintOp::Fill));
out.raw(&color_op(Color::Rgb(0.0, 0.0, 0.0), PaintOp::Stroke));
out.raw("1 w\n");
let stroked = geom::deflate(rect, BORDER_WIDTH / 2.0, BORDER_WIDTH / 2.0);
out.rect(stroked, Float::G6);
out.raw("re b\n");
let written = body(dict, metrics, encode, rect, r);
if written.is_empty() {
return None;
}
out.raw("BT\n");
out.raw(&color_op(Color::Rgb(0.0, 0.0, 0.0), PaintOp::Fill));
out.raw(&written);
out.raw("ET\n");
out.raw("Q\n");
Some(Generated {
stream: out.into_bytes(),
rect_override: None,
is_text_markup: false,
blend_multiply: false,
font_resources: Some(Dict::from_pairs([(
Name::new(FONT_ALIAS.to_vec()),
Object::Dict(freetext::fallback_font()),
)])),
})
}
fn body<R: Resolve>(
dict: &Dict,
metrics: &vt::Metrics<'_>,
encode: &dyn Fn(u32) -> Vec<u8>,
rect: kurbo::Rect,
r: &R,
) -> String {
let title = dict.text(obj_names::T, r).unwrap_or_default();
let contents = dict.text(obj_names::CONTENTS, r).unwrap_or_default();
let text = format!("{title}\n{contents}");
let config = vt::Config {
plate: rect,
alignment: vt::Alignment::Left,
font_size: FONT_SIZE,
multi_line: true,
auto_return: true,
..vt::Config::default()
};
let layout = vt::layout(&text, &config, metrics);
vt::edit_ap::generate(
&layout,
&config,
metrics,
TEXT_OFFSET,
vt::edit_ap::Grouping::Continuous,
|code| vt::edit_ap::Face::single(FONT_ALIAS, encode(code)),
)
}
#[cfg(test)]
mod tests {
use super::{FONT_ALIAS, popup};
use crate::vt::Metrics;
use pdfrum_object::{Array, Dict, NoResolve, Object, PdfString};
const LEFT: f32 = 115.750_62;
const TOP: f32 = 706.328_57;
fn card(title: &str, contents: &str) -> Dict {
Dict::from_pairs([
(
pdfrum_object::names::RECT.clone(),
Object::Array(Array::of([
Object::Real(LEFT),
Object::Real(TOP - 200.0),
Object::Real(LEFT + 200.0),
Object::Real(TOP),
])),
),
(
pdfrum_object::names::T.clone(),
Object::Str(PdfString::literal(title.as_bytes())),
),
(
pdfrum_object::names::CONTENTS.clone(),
Object::Str(PdfString::literal(contents.as_bytes())),
),
])
}
fn helvetica(width: &dyn Fn(u32) -> i32) -> Metrics<'_> {
Metrics {
width,
ascent: 718,
descent: -207,
}
}
#[test]
fn the_card_opens_with_the_chrome_upstream_writes_byte_for_byte() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
let made =
popup(&card("A", "b"), &metrics, &encode, &NoResolve).expect("a card with text draws");
let stream = String::from_utf8(made.stream).expect("ascii");
assert!(
stream.starts_with("/GS gs\n1 1 0 rg\n0 0 0 RG\n1 w\n116.251 506.829 199 199 re b\n"),
"{stream}"
);
}
#[test]
fn the_text_is_black_and_set_in_the_fixed_twelve_point_face() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
let made =
popup(&card("A", "b"), &metrics, &encode, &NoResolve).expect("a card with text draws");
let stream = String::from_utf8(made.stream).expect("ascii");
assert!(stream.contains("BT\n0 0 0 rg\n"), "{stream}");
assert!(stream.contains("/FONT 12 Tf\n"), "{stream}");
assert!(stream.ends_with("ET\nQ\n"), "{stream}");
}
#[test]
fn the_title_and_the_note_are_one_string_joined_by_a_newline() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
let made =
popup(&card("A", "b"), &metrics, &encode, &NoResolve).expect("a card with text draws");
let stream = String::from_utf8(made.stream).expect("ascii");
let title = stream.find("(A) Tj").expect("the title is set");
let note = stream.find("(b) Tj").expect("the note is set");
assert!(title < note, "{stream}");
}
#[test]
fn a_title_less_card_still_spends_its_first_line_on_the_empty_title() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
let titled = popup(&card("A", "b"), &metrics, &encode, &NoResolve).expect("draws");
let bare = popup(&card("", "b"), &metrics, &encode, &NoResolve).expect("draws");
let titled = String::from_utf8(titled.stream).expect("ascii");
let bare = String::from_utf8(bare.stream).expect("ascii");
assert!(titled.contains("118.75062 694.7125 Td"), "{titled}");
assert!(
titled.contains("(A) Tj\n0 -11.099976 Td\n(b) Tj"),
"{titled}"
);
assert!(!bare.contains("(A) Tj"), "{bare}");
assert!(bare.contains("118.75062 683.61255 Td"), "{bare}");
}
#[test]
fn a_card_with_neither_a_title_nor_a_note_draws_nothing_at_all() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
assert_eq!(popup(&card("", ""), &metrics, &encode, &NoResolve), None);
}
#[test]
fn the_stream_names_the_helvetica_it_sets_the_text_in() {
let width = |_: u32| 500;
let metrics = helvetica(&width);
let encode = |code: u32| vec![u8::try_from(code).unwrap_or(b'?')];
let made = popup(&card("A", "b"), &metrics, &encode, &NoResolve).expect("draws");
let fonts = made
.font_resources
.expect("a stream with a Tf carries a font");
let named = fonts
.dict(&pdfrum_object::Name::new(FONT_ALIAS.to_vec()), &NoResolve)
.expect("under the alias the Tf names");
assert_eq!(
named
.name(&pdfrum_object::Name::from("BaseFont"))
.map(pdfrum_object::Name::as_bytes),
Some(b"Helvetica".as_slice())
);
}
}