mod block;
mod css;
mod inline;
mod structure;
mod text;
use std::cell::{Cell, RefCell};
use std::collections::{BTreeSet, HashMap};
use std::fmt::Write as _;
use rustyfi_backend::{
AnnotAction, DecoId, DocExtras, FontKey, FrameDecoration, GraphicsElem, ImageResource,
PageGeometry, VertBox,
};
use rustyfi_pdf::TtfFontStore;
use crate::HtmlError;
pub(crate) use text::BodyStyle;
pub(crate) struct Ctx<'a> {
pub(crate) fonts: Option<&'a TtfFontStore>,
pub(crate) used_fonts: RefCell<BTreeSet<usize>>,
pub(crate) links: HashMap<DecoId, &'a AnnotAction>,
pub(crate) dests: HashMap<DecoId, &'a str>,
pub(crate) outline_by_dest: HashMap<String, i64>,
pub(crate) emph_stack: RefCell<Vec<bool>>,
pub(crate) bullet_suppress: RefCell<u32>,
pub(crate) iframe_stack: RefCell<Vec<(String, &'static str)>>,
pub(crate) images: &'a [ImageResource],
pub(crate) body: BodyStyle,
pub(crate) pending_glue: Cell<Option<f64>>,
pub(crate) last_char: Cell<Option<char>>,
pub(crate) mono_run: Cell<bool>,
pub(crate) tabular_rules: RefCell<Vec<(f64, f64, Vec<GraphicsElem>)>>,
pub(crate) frame_decos: HashMap<DecoId, &'a FrameDecoration>,
pub(crate) footnotes: RefCell<Vec<(usize, String)>>,
pub(crate) footnote_seq: Cell<usize>,
pub(crate) shared_images: RefCell<Vec<usize>>,
image_canon: HashMap<usize, (usize, usize)>,
pub(crate) open_run: RefCell<Option<String>>,
}
impl Ctx<'_> {
pub(crate) fn font_family_for(&self, font: FontKey) -> Option<String> {
let store = self.fonts?;
let file_idx = store.file_index(font);
self.used_fonts.borrow_mut().insert(file_idx);
let family = store.file_family_name(file_idx)?;
Some(crate::fonts::reflow_font_stack(&family))
}
pub(crate) fn is_monospace(&self, font: Option<FontKey>) -> bool {
let (Some(store), Some(font)) = (self.fonts, font) else {
return false;
};
store
.file_family_name(store.file_index(font))
.is_some_and(|f| crate::fonts::is_monospace_family(&f))
}
pub(crate) fn note_glue(&self, natural_pt: f64) {
let merged = match self.pending_glue.get() {
Some(prev) if prev >= natural_pt => prev,
_ => natural_pt,
};
self.pending_glue.set(Some(merged));
}
pub(crate) fn resolve_glue(&self, out: &mut String, next: Option<char>) {
if let Some(width) = self.pending_glue.take() {
if text::wants_space(self.last_char.get(), next, width) {
out.push(' ');
}
}
}
pub(crate) fn reset_flow(&self) {
self.pending_glue.set(None);
self.last_char.set(None);
}
pub(crate) fn image_sharing(&self, id: usize) -> (usize, bool) {
match self.image_canon.get(&id) {
Some(&(canon, uses)) => (canon, uses > 1),
None => (id, false),
}
}
}
fn canonical_images(
images: &[ImageResource],
uses: &HashMap<usize, usize>,
) -> HashMap<usize, (usize, usize)> {
let mut first_by_content: HashMap<(&[u8], u32, u32), usize> = HashMap::new();
let mut canon_of: HashMap<usize, usize> = HashMap::new();
for (idx, res) in images.iter().enumerate() {
let bytes: &[u8] = match &res.jpeg_dct {
Some(j) => &j.bytes,
None => &res.samples,
};
if bytes.is_empty() {
canon_of.insert(idx, idx);
continue;
}
let canon = *first_by_content
.entry((bytes, res.px_w, res.px_h))
.or_insert(idx);
canon_of.insert(idx, canon);
}
let mut total: HashMap<usize, usize> = HashMap::new();
for (id, n) in uses {
let canon = canon_of.get(id).copied().unwrap_or(*id);
*total.entry(canon).or_default() += n;
}
canon_of
.into_iter()
.map(|(id, canon)| (id, (canon, total.get(&canon).copied().unwrap_or(0))))
.collect()
}
#[allow(clippy::too_many_arguments)]
pub fn render_html_reflow(
source: Option<&[VertBox]>,
geometry: &PageGeometry,
images: &[ImageResource],
extras: &DocExtras,
links: &[(DecoId, AnnotAction)],
dests: &[(DecoId, String)],
) -> Result<String, HtmlError> {
render_html_reflow_impl(source, geometry, images, extras, links, dests, &[], None)
}
pub fn render_html_reflow_with_decos(
source: Option<&[VertBox]>,
geometry: &PageGeometry,
images: &[ImageResource],
extras: &DocExtras,
links: &[(DecoId, AnnotAction)],
dests: &[(DecoId, String)],
frame_decos: &[(DecoId, FrameDecoration)],
) -> Result<String, HtmlError> {
render_html_reflow_impl(source, geometry, images, extras, links, dests, frame_decos, None)
}
#[allow(clippy::too_many_arguments)]
pub fn render_html_reflow_ttf_with(
source: Option<&[VertBox]>,
geometry: &PageGeometry,
store: &TtfFontStore,
images: &[ImageResource],
extras: &DocExtras,
links: &[(DecoId, AnnotAction)],
dests: &[(DecoId, String)],
) -> Result<String, HtmlError> {
render_html_reflow_impl(source, geometry, images, extras, links, dests, &[], Some(store))
}
#[allow(clippy::too_many_arguments)]
pub fn render_html_reflow_ttf_with_decos(
source: Option<&[VertBox]>,
geometry: &PageGeometry,
store: &TtfFontStore,
images: &[ImageResource],
extras: &DocExtras,
links: &[(DecoId, AnnotAction)],
dests: &[(DecoId, String)],
frame_decos: &[(DecoId, FrameDecoration)],
) -> Result<String, HtmlError> {
render_html_reflow_impl(
source,
geometry,
images,
extras,
links,
dests,
frame_decos,
Some(store),
)
}
#[allow(clippy::too_many_arguments)]
fn render_html_reflow_impl(
source: Option<&[VertBox]>,
geometry: &PageGeometry,
images: &[ImageResource],
extras: &DocExtras,
links: &[(DecoId, AnnotAction)],
dests: &[(DecoId, String)],
frame_decos: &[(DecoId, FrameDecoration)],
font_store: Option<&TtfFontStore>,
) -> Result<String, HtmlError> {
let body_style = BodyStyle::dominant(source);
let image_canon = canonical_images(images, &body_style.image_uses);
let ctx = Ctx {
fonts: font_store,
used_fonts: RefCell::new(BTreeSet::new()),
links: links.iter().map(|(id, action)| (*id, action)).collect(),
dests: dests
.iter()
.map(|(id, name)| (*id, name.as_str()))
.collect(),
outline_by_dest: structure::outline_levels(&extras.outline),
emph_stack: RefCell::new(Vec::new()),
bullet_suppress: RefCell::new(0),
iframe_stack: RefCell::new(Vec::new()),
images,
body: body_style,
pending_glue: Cell::new(None),
last_char: Cell::new(None),
mono_run: Cell::new(false),
tabular_rules: RefCell::new(Vec::new()),
frame_decos: frame_decos.iter().map(|(id, d)| (*id, d)).collect(),
footnotes: RefCell::new(Vec::new()),
footnote_seq: Cell::new(0),
shared_images: RefCell::new(Vec::new()),
image_canon,
open_run: RefCell::new(None),
};
let mut body = String::new();
body.push_str("<div class=\"doc\">\n");
if let Some(vboxes) = source {
block::walk_vboxes(&mut body, vboxes, &ctx);
} else {
body.push_str("<p class=\"para reflow-empty\">(no reflow source captured)</p>\n");
}
body.push_str("</div>\n");
let mut out = String::new();
let lang = if ctx.body.cjk_ratio > 0.1 { "ja" } else { "en" };
let _ = write!(
out,
"<!doctype html>\n<html lang=\"{lang}\">\n<head>\n<meta charset=\"utf-8\">\n\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
);
out.push_str("<style>\n");
out.push_str(&css::stylesheet(geometry, &ctx));
out.push_str(&css::shared_image_rules(&ctx));
out.push_str("</style>\n</head>\n<body>\n");
out.push_str(&body);
out.push_str("</body>\n</html>\n");
Ok(out)
}