use azul_core::{
geom::{LogicalRect, LogicalSize, LogicalPosition},
};
use azul_css::props::basic::ColorU;
use azul_layout::{
solver3::display_list::{DisplayList, DisplayListItem},
text3::cache::{FontManager, ParsedFontTrait, UnifiedLayout},
};
use crate::{Color, Mm, Op, Pt, Rgb, FontId};
use super::border::{
BorderConfig, extract_border_widths, extract_border_colors,
extract_border_styles, extract_border_radii, render_border,
};
fn convert_color(color: &ColorU) -> Color {
Color::Rgb(Rgb {
r: color.r as f32 / 255.0,
g: color.g as f32 / 255.0,
b: color.b as f32 / 255.0,
icc_profile: None,
})
}
pub fn display_list_to_printpdf_ops_with_margins<T: ParsedFontTrait + 'static>(
display_list: &DisplayList,
page_size: LogicalSize,
margin_left_pt: f32,
margin_top_pt: f32,
font_manager: &FontManager<T>,
) -> Result<Vec<Op>, String> {
let mut ops = Vec::new();
let page_height = page_size.height;
let mut current_text_layout: Option<(&azul_layout::text3::cache::UnifiedLayout, LogicalRect)> = None;
let _text_layout_count = display_list.items.iter().filter(|item| matches!(item, DisplayListItem::TextLayout { .. })).count();
for (_idx, item) in display_list.items.iter().enumerate() {
convert_display_list_item_with_margins(
&mut ops,
item,
page_height,
margin_left_pt,
margin_top_pt,
&mut current_text_layout,
font_manager
);
}
Ok(ops)
}
pub fn display_list_to_printpdf_ops<T: ParsedFontTrait + 'static>(
display_list: &DisplayList,
page_size: LogicalSize,
font_manager: &FontManager<T>,
) -> Result<Vec<Op>, String> {
display_list_to_printpdf_ops_with_margins(display_list, page_size, 0.0, 0.0, font_manager)
}
const CSS_PX_TO_PT: f32 = 72.0 / 96.0;
#[inline]
fn pt_to_mm(pt: f32) -> Mm {
Mm::from(Pt(pt))
}
fn bounds_px_to_pt(b: &LogicalRect) -> LogicalRect {
LogicalRect {
origin: LogicalPosition::new(
b.origin.x * CSS_PX_TO_PT,
b.origin.y * CSS_PX_TO_PT,
),
size: LogicalSize::new(
b.size.width * CSS_PX_TO_PT,
b.size.height * CSS_PX_TO_PT,
),
}
}
fn make_rect_polygon_pt(x: f32, y: f32, w: f32, h: f32) -> crate::graphics::Polygon {
let lp = |px: f32, py: f32| crate::graphics::LinePoint {
p: crate::graphics::Point::new(pt_to_mm(px), pt_to_mm(py)),
bezier: false,
};
crate::graphics::Polygon {
rings: vec![crate::graphics::PolygonRing {
points: vec![
lp(x, y),
lp(x + w, y),
lp(x + w, y + h),
lp(x, y + h),
],
}],
mode: crate::graphics::PaintMode::Fill,
winding_order: crate::graphics::WindingOrder::NonZero,
}
}
#[derive(Clone, Copy)]
struct CoordTransform {
page_height: f32,
margin_left: f32,
margin_top: f32,
}
impl CoordTransform {
fn new(page_height: f32, margin_left: f32, margin_top: f32) -> Self {
Self { page_height, margin_left, margin_top }
}
#[inline]
fn x(&self, layout_x: f32) -> f32 {
layout_x * CSS_PX_TO_PT + self.margin_left
}
#[inline]
fn y(&self, layout_y: f32) -> f32 {
self.page_height - layout_y * CSS_PX_TO_PT - self.margin_top
}
#[inline]
fn rect_y(&self, layout_y: f32, height: f32) -> f32 {
self.page_height - (layout_y + height) * CSS_PX_TO_PT - self.margin_top
}
#[inline]
fn dim(&self, px_value: f32) -> f32 {
px_value * CSS_PX_TO_PT
}
}
fn convert_display_list_item_with_margins<'a, T: ParsedFontTrait + 'static>(
ops: &mut Vec<Op>,
item: &'a DisplayListItem,
page_height: f32,
margin_left: f32,
margin_top: f32,
current_text_layout: &mut Option<(&'a UnifiedLayout, LogicalRect)>,
font_manager: &FontManager<T>,
) {
let transform = CoordTransform::new(page_height, margin_left, margin_top);
match item {
DisplayListItem::Rect {
bounds,
color,
border_radius,
} => {
if bounds.size.width == 0.0 || bounds.size.height == 0.0 {
return;
}
ops.push(Op::SaveGraphicsState);
let radii = crate::html::border::BorderRadii {
top_left: (border_radius.top_left, border_radius.top_left),
top_right: (border_radius.top_right, border_radius.top_right),
bottom_right: (border_radius.bottom_right, border_radius.bottom_right),
bottom_left: (border_radius.bottom_left, border_radius.bottom_left),
};
let has_radius = radii.top_left.0 > 0.0 || radii.top_left.1 > 0.0
|| radii.top_right.0 > 0.0 || radii.top_right.1 > 0.0
|| radii.bottom_right.0 > 0.0 || radii.bottom_right.1 > 0.0
|| radii.bottom_left.0 > 0.0 || radii.bottom_left.1 > 0.0;
if has_radius {
let b = bounds_px_to_pt(bounds);
let points = crate::html::border::create_rounded_rect_path_with_margins(
b.origin.x,
b.origin.y,
b.size.width,
b.size.height,
&radii,
page_height,
margin_left,
margin_top,
);
let polygon = crate::graphics::Polygon {
rings: vec![crate::graphics::PolygonRing { points }],
mode: crate::graphics::PaintMode::Fill,
winding_order: crate::graphics::WindingOrder::NonZero,
};
ops.push(Op::SetFillColor {
col: convert_color(color),
});
ops.push(Op::DrawPolygon { polygon });
} else {
let x = transform.x(bounds.origin.x);
let y = transform.rect_y(bounds.origin.y, bounds.size.height);
let w = transform.dim(bounds.size.width);
let h = transform.dim(bounds.size.height);
ops.push(Op::SetFillColor {
col: convert_color(color),
});
ops.push(Op::DrawPolygon { polygon: make_rect_polygon_pt(x, y, w, h) });
}
ops.push(Op::RestoreGraphicsState);
}
DisplayListItem::TextLayout {
layout,
bounds,
font_hash: _,
font_size_px: _,
color,
} => {
if let Some(unified_layout) = layout.downcast_ref::<azul_layout::text3::cache::UnifiedLayout>() {
render_unified_layout_with_margins(ops, unified_layout, bounds, *color, &transform, font_manager);
*current_text_layout = Some((unified_layout, *bounds));
}
}
DisplayListItem::Text { glyphs, font_hash, font_size_px, color, clip_rect: _ } => {
if glyphs.is_empty() {
return;
}
let use_builtin_font = font_hash.font_hash == 0;
if !use_builtin_font {
return;
}
ops.push(Op::SaveGraphicsState);
ops.push(Op::SetFillColor {
col: convert_color(color),
});
if use_builtin_font {
ops.push(Op::SetFont {
font: crate::ops::PdfFontHandle::Builtin(crate::BuiltinFont::Helvetica),
size: Pt(transform.dim(*font_size_px)),
});
} else {
let font_id = FontId(format!("F{}", font_hash.font_hash));
ops.push(Op::SetFont {
font: crate::ops::PdfFontHandle::External(font_id),
size: Pt(transform.dim(*font_size_px)),
});
}
ops.push(Op::StartTextSection);
let mut text_string = String::new();
let mut first_glyph_pos: Option<(f32, f32)> = None;
for glyph in glyphs {
if first_glyph_pos.is_none() {
first_glyph_pos = Some((glyph.point.x, glyph.point.y));
}
if use_builtin_font {
if let Some(ch) = char::from_u32(glyph.index) {
text_string.push(ch);
}
}
}
if let Some((x, y)) = first_glyph_pos {
let pdf_x = transform.x(x);
let pdf_y = transform.y(y);
ops.push(Op::SetTextMatrix {
matrix: crate::matrix::TextMatrix::Raw([
1.0, 0.0, 0.0, 1.0,
pdf_x, pdf_y,
]),
});
if use_builtin_font && !text_string.is_empty() {
ops.push(Op::ShowText {
items: vec![crate::text::TextItem::Text(text_string)],
});
} else if !use_builtin_font {
let glyph_ids: Vec<crate::text::Codepoint> = glyphs.iter().map(|g| {
crate::text::Codepoint::new(g.index as u16, 0.0)
}).collect();
ops.push(Op::ShowText {
items: vec![crate::text::TextItem::GlyphIds(glyph_ids)],
});
}
}
ops.push(Op::EndTextSection);
ops.push(Op::RestoreGraphicsState);
}
DisplayListItem::Border {
bounds,
widths,
colors,
styles,
border_radius,
} => {
let bounds_pt = bounds_px_to_pt(bounds);
let config = BorderConfig {
bounds: bounds_pt,
widths: extract_border_widths(widths),
colors: extract_border_colors(colors),
styles: extract_border_styles(styles),
radii: extract_border_radii(border_radius),
page_height,
margin_left,
margin_top,
};
render_border(ops, &config);
}
DisplayListItem::Image { bounds: _, image: _ } => {
}
DisplayListItem::Underline { bounds, color, thickness: _ }
| DisplayListItem::Strikethrough { bounds, color, thickness: _ }
| DisplayListItem::Overline { bounds, color, thickness: _ } => {
if bounds.size.width > 0.0 && bounds.size.height > 0.0 {
let x = transform.x(bounds.origin.x);
let y = transform.rect_y(bounds.origin.y, bounds.size.height);
let w = transform.dim(bounds.size.width);
let h = transform.dim(bounds.size.height);
ops.push(Op::SaveGraphicsState);
ops.push(Op::SetFillColor { col: convert_color(color) });
ops.push(Op::DrawPolygon { polygon: make_rect_polygon_pt(x, y, w, h) });
ops.push(Op::RestoreGraphicsState);
}
}
_ => {
}
}
}
pub fn render_unified_layout_public<T: ParsedFontTrait + 'static>(
layout: &UnifiedLayout,
bounds_width: f32,
bounds_height: f32,
color: ColorU,
page_height: f32,
_font_manager: &FontManager<T>,
) -> Vec<Op> {
let mut ops = Vec::new();
let bounds = LogicalRect {
origin: LogicalPosition::new(0.0, 0.0),
size: LogicalSize::new(bounds_width, bounds_height),
};
render_unified_layout_impl(&mut ops, layout, &bounds, color, page_height, _font_manager);
ops
}
fn render_unified_layout_impl<T: ParsedFontTrait + 'static>(
ops: &mut Vec<Op>,
layout: &UnifiedLayout,
bounds: &LogicalRect,
_color: ColorU, page_height: f32,
font_manager: &FontManager<T>,
) {
use azul_layout::text3::glyphs::get_glyph_runs_pdf;
let loaded_fonts = font_manager.get_loaded_fonts();
let glyph_runs = get_glyph_runs_pdf(layout, &loaded_fonts);
if glyph_runs.is_empty() {
return;
}
let mut current_color: Option<ColorU> = None;
for run in glyph_runs.iter() {
if run.glyphs.is_empty() {
continue;
}
if current_color != Some(run.color) {
ops.push(Op::SetFillColor {
col: convert_color(&run.color),
});
current_color = Some(run.color);
}
let font_id = FontId(format!("F{}", run.font_hash));
ops.push(Op::SetFont {
font: crate::ops::PdfFontHandle::External(font_id.clone()),
size: Pt(run.font_size_px * CSS_PX_TO_PT),
});
ops.push(Op::StartTextSection);
for glyph in &run.glyphs {
let glyph_x_px = bounds.origin.x + glyph.position.x;
let glyph_y_px = bounds.origin.y + glyph.position.y;
let pdf_x = glyph_x_px * CSS_PX_TO_PT;
let pdf_y = page_height - glyph_y_px * CSS_PX_TO_PT;
ops.push(Op::SetTextMatrix {
matrix: crate::matrix::TextMatrix::Raw([
1.0, 0.0, 0.0, 1.0, pdf_x, pdf_y, ]),
});
ops.push(Op::ShowText {
items: vec![crate::text::TextItem::GlyphIds(vec![
crate::text::Codepoint {
gid: glyph.glyph_id,
offset: 0.0,
cid: Some(glyph.unicode_codepoint.clone()),
}
])],
});
}
ops.push(Op::EndTextSection);
}
}
fn render_unified_layout_with_margins<T: ParsedFontTrait + 'static>(
ops: &mut Vec<Op>,
layout: &UnifiedLayout,
bounds: &LogicalRect,
_color: ColorU, transform: &CoordTransform,
font_manager: &FontManager<T>,
) {
use azul_layout::text3::glyphs::get_glyph_runs_pdf;
let loaded_fonts = font_manager.get_loaded_fonts();
let glyph_runs = get_glyph_runs_pdf(layout, &loaded_fonts);
if glyph_runs.is_empty() {
return;
}
for run in glyph_runs.iter() {
if run.glyphs.is_empty() {
continue;
}
if let Some(bg_color) = run.background_color {
if bg_color.a > 0 {
if let (Some(first_glyph), Some(last_glyph)) =
(run.glyphs.first(), run.glyphs.last())
{
let font_size = run.font_size_px;
let ascent = font_size * 0.8;
let descent = font_size * 0.2;
let bg_start_x = bounds.origin.x + first_glyph.position.x;
let bg_end_x = bounds.origin.x + last_glyph.position.x + last_glyph.advance;
let bg_width = bg_end_x - bg_start_x;
let baseline_y = bounds.origin.y + first_glyph.position.y;
let bg_top_y = baseline_y - ascent;
let bg_height = ascent + descent;
let pdf_x = transform.x(bg_start_x);
let pdf_y = transform.rect_y(bg_top_y, bg_height);
let pdf_w = transform.dim(bg_width);
let pdf_h = transform.dim(bg_height);
ops.push(Op::SaveGraphicsState);
ops.push(Op::SetFillColor {
col: convert_color(&bg_color),
});
ops.push(Op::DrawPolygon { polygon: make_rect_polygon_pt(pdf_x, pdf_y, pdf_w, pdf_h) });
ops.push(Op::RestoreGraphicsState);
}
}
}
}
let mut current_color: Option<ColorU> = None;
for run in glyph_runs.iter() {
if run.glyphs.is_empty() {
continue;
}
if current_color != Some(run.color) {
ops.push(Op::SetFillColor {
col: convert_color(&run.color),
});
current_color = Some(run.color);
}
let font_id = FontId(format!("F{}", run.font_hash));
ops.push(Op::SetFont {
font: crate::ops::PdfFontHandle::External(font_id.clone()),
size: Pt(transform.dim(run.font_size_px)),
});
ops.push(Op::StartTextSection);
for glyph in &run.glyphs {
let glyph_x_layout = bounds.origin.x + glyph.position.x;
let glyph_y_layout = bounds.origin.y + glyph.position.y;
let pdf_x = transform.x(glyph_x_layout);
let pdf_y = transform.y(glyph_y_layout);
ops.push(Op::SetTextMatrix {
matrix: crate::matrix::TextMatrix::Raw([
1.0, 0.0, 0.0, 1.0, pdf_x, pdf_y, ]),
});
ops.push(Op::ShowText {
items: vec![crate::text::TextItem::GlyphIds(vec![
crate::text::Codepoint {
gid: glyph.glyph_id,
offset: 0.0,
cid: Some(glyph.unicode_codepoint.clone()),
}
])],
});
}
ops.push(Op::EndTextSection);
}
}
pub fn apply_margin_offset(ops: &mut [Op], offset_x: crate::Mm, offset_y: crate::Mm) {
if offset_x.0 == 0.0 && offset_y.0 == 0.0 {
return;
}
let offset_x_pt = crate::Pt(offset_x.0 * 2.83465);
let offset_y_pt = crate::Pt(offset_y.0 * 2.83465);
for op in ops.iter_mut() {
match op {
Op::DrawPolygon { polygon } => {
for ring in &mut polygon.rings {
for point in &mut ring.points {
point.p.x = crate::Pt(point.p.x.0 + offset_x_pt.0);
point.p.y = crate::Pt(point.p.y.0 + offset_y_pt.0);
}
}
}
Op::DrawLine { line } => {
for point in &mut line.points {
point.p.x = crate::Pt(point.p.x.0 + offset_x_pt.0);
point.p.y = crate::Pt(point.p.y.0 + offset_y_pt.0);
}
}
Op::SetTextCursor { pos } => {
pos.x = crate::Pt(pos.x.0 + offset_x_pt.0);
pos.y = crate::Pt(pos.y.0 + offset_y_pt.0);
}
Op::UseXobject { transform, .. } => {
transform.translate_x = Some(crate::Pt(
transform.translate_x.unwrap_or(crate::Pt(0.0)).0 + offset_x_pt.0
));
transform.translate_y = Some(crate::Pt(
transform.translate_y.unwrap_or(crate::Pt(0.0)).0 + offset_y_pt.0
));
}
Op::DrawRectangle { rectangle } => {
rectangle.x = crate::Pt(rectangle.x.0 + offset_x_pt.0);
rectangle.y = crate::Pt(rectangle.y.0 + offset_y_pt.0);
}
Op::LinkAnnotation { link } => {
link.rect.x = crate::Pt(link.rect.x.0 + offset_x_pt.0);
link.rect.y = crate::Pt(link.rect.y.0 + offset_y_pt.0);
}
Op::Marker { .. }
| Op::SetColorSpaceStroke { .. }
| Op::SetColorSpaceFill { .. }
| Op::BeginLayer { .. }
| Op::EndLayer
| Op::SaveGraphicsState
| Op::RestoreGraphicsState
| Op::LoadGraphicsState { .. }
| Op::StartTextSection
| Op::EndTextSection
| Op::SetFont { .. }
| Op::ShowText { .. }
| Op::AddLineBreak
| Op::SetLineHeight { .. }
| Op::SetWordSpacing { .. }
| Op::SetFillColor { .. }
| Op::SetOutlineColor { .. }
| Op::SetOutlineThickness { .. }
| Op::SetLineDashPattern { .. }
| Op::SetLineJoinStyle { .. }
| Op::SetLineCapStyle { .. }
| Op::SetMiterLimit { .. }
| Op::SetTextRenderingMode { .. }
| Op::SetCharacterSpacing { .. }
| Op::SetLineOffset { .. }
| Op::SetTransformationMatrix { .. }
| Op::SetTextMatrix { .. }
| Op::MoveTextCursorAndSetLeading { .. }
| Op::SetRenderingIntent { .. }
| Op::SetHorizontalScaling { .. }
| Op::BeginInlineImage
| Op::BeginInlineImageData
| Op::EndInlineImage
| Op::BeginMarkedContent { .. }
| Op::BeginMarkedContentWithProperties { .. }
| Op::BeginOptionalContent { .. }
| Op::DefineMarkedContentPoint { .. }
| Op::EndMarkedContent
| Op::EndMarkedContentWithProperties
| Op::EndOptionalContent
| Op::BeginCompatibilitySection
| Op::EndCompatibilitySection
| Op::MoveToNextLineShowText { .. }
| Op::SetSpacingMoveAndShowText { .. }
| Op::Unknown { .. } => {}
}
}
}