From e802807355277bc2a0da6b4b57ef52a42a3c63d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sch=C3=BCtt?= Date: Sat, 7 Dec 2024 15:27:59 +0100 Subject: [PATCH] Fix images not appearing in PDF --- Cargo.lock | 13 ++- Cargo.toml | 12 +-- examples/html.rs | 62 ++++++++----- examples/subset.rs | 31 +++---- src/font.rs | 34 ++++--- src/html.rs | 226 ++++++++++++++++++++++++++++++--------------- src/image.rs | 215 +++++++++++++++++++++++++++++------------- src/lib.rs | 10 +- src/serialize.rs | 16 ++-- src/svg.rs | 3 +- src/utils.rs | 2 - src/wasm.rs | 33 +++++-- 12 files changed, 427 insertions(+), 230 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe57108..6bde5f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,21 +160,20 @@ dependencies = [ [[package]] name = "azul-core" version = "0.0.2" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "azul-css", "azul-css-parser", "gl-context-loader", "highway", "libm", - "rayon", "rust-fontconfig", ] [[package]] name = "azul-css" version = "0.0.1" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "libm", ] @@ -182,7 +181,7 @@ dependencies = [ [[package]] name = "azul-css-parser" version = "0.0.1" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "azul-css", "azul-simplecss", @@ -191,7 +190,7 @@ dependencies = [ [[package]] name = "azul-layout" version = "0.0.4" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "azul-core", "azul-css", @@ -209,7 +208,7 @@ checksum = "c303bfdf857413adbd19d9b15dddd9b4adb8e2be7e493c8a38d9fc6179a074ac" [[package]] name = "azul-text-layout" version = "0.0.5" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "allsorts", "azul-core", @@ -223,7 +222,7 @@ dependencies = [ [[package]] name = "azulc" version = "0.0.3" -source = "git+https://github.com/fschutt/azul?rev=8b71bef5439b8f8d4542a2849e66499173562235#8b71bef5439b8f8d4542a2849e66499173562235" +source = "git+https://github.com/fschutt/azul?rev=7e31b697b8e61779b88790aa9a74a95d8295a51a#7e31b697b8e61779b88790aa9a74a95d8295a51a" dependencies = [ "azul-core", "azul-css", diff --git a/Cargo.toml b/Cargo.toml index 7f6c8ba..8e9e2db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ pdf-writer = { version = "0.12.0" } image = { version = "0.25", default-features = false } svg2pdf = { version = "0.12.0" } # dependencies for wasm demo -azul-css = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false } -azul-css-parser = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false } -azul-core = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false, features = ["std"] } -azul-layout = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false, features = ["std", "text_layout"] } -azul-text-layout = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false } -azulc = { git = "https://github.com/fschutt/azul", rev = "8b71bef5439b8f8d4542a2849e66499173562235", default-features = false, features = ["std", "xml", "text_layout", "font_loading"] } +azul-css = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false } +azul-css-parser = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false } +azul-core = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false, features = ["std"] } +azul-layout = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false, features = ["std", "text_layout"] } +azul-text-layout = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false } +azulc = { git = "https://github.com/fschutt/azul", rev = "7e31b697b8e61779b88790aa9a74a95d8295a51a", default-features = false, features = ["std", "xml", "text_layout", "font_loading"] } rust-fontconfig = { version = "0.1.13", default-features = false } xmlparser = { version = "0.13.6", default-features = false } serde = { version = "1" } diff --git a/examples/html.rs b/examples/html.rs index e8d091e..e52cc8d 100644 --- a/examples/html.rs +++ b/examples/html.rs @@ -1,20 +1,14 @@ use printpdf::*; -const HTML_STRINGS: &[&str; 2] = &[ - "

hello

", - "
-

Very long text that breaks into multiple lines. asdfasd asdfasdf adsfasdf ladsjfplasdjf asdlfkjasdfl lasdkjfasdölkjf

-
", -]; +const HTML_STRINGS: &[&str; 1] = &[""]; -pub struct ImgComponent { } +pub struct ImgComponent {} impl XmlComponentTrait for ImgComponent { - fn get_available_arguments(&self) -> ComponentArguments { ComponentArguments { accepts_text: false, - args: vec![("src".to_string(), "String".to_string())] + args: vec![("src".to_string(), "String".to_string())], } } @@ -24,35 +18,55 @@ impl XmlComponentTrait for ImgComponent { arguments: &FilteredComponentArguments, content: &XmlTextContent, ) -> Result { - // TODO: parse image from arguments["src"] - Ok(Dom::image( - InternalImageRef::new_rawimage( - translate_to_internal_rawimage( - &RawImage::decode_from_bytes(include_bytes!("./assets/img/dog_alpha.png")).unwrap() - ) - ).unwrap() - ).style(CssApiWrapper::empty())) + use printpdf::html::ImageInfo; + + let im_info = arguments + .values + .get("src") + .map(|s| s.as_bytes().to_vec()) + .unwrap_or_default(); + + let image_info = serde_json::from_slice::(&im_info) + .ok() + .unwrap_or_default(); + let data_format = RawImageFormat::RGB8; + + let image = RawImage { + width: image_info.width, + height: image_info.height, + data_format, + pixels: RawImageData::empty(data_format), + tag: im_info, + }; + + let im = Dom::image(image.to_internal()).style(CssApiWrapper::empty()); + + Ok(im) } } fn main() -> Result<(), String> { - for (i, h) in HTML_STRINGS.iter().enumerate() { - let components = vec![XmlComponent { id: "img".to_string(), - renderer: Box::new(ImgComponent { }), + renderer: Box::new(ImgComponent {}), inherit_vars: false, }]; let config = XmlRenderOptions { components, - .. Default::default() + images: vec![( + "test.bmp".to_string(), + include_bytes!("./assets/img/BMP_test.bmp").to_vec(), + )] + .into_iter() + .collect(), + ..Default::default() }; - let doc = PdfDocument::new("HTML rendering demo") - .with_html(h, config)? - .save(&PdfSaveOptions::default()); + let mut doc = PdfDocument::new("HTML rendering demo"); + let pages = doc.html2pages(h, config)?; + let doc = doc.with_pages(pages).save(&PdfSaveOptions::default()); std::fs::write(format!("html{i}.pdf"), doc).unwrap(); } diff --git a/examples/subset.rs b/examples/subset.rs index d564c28..c387aca 100644 --- a/examples/subset.rs +++ b/examples/subset.rs @@ -1,25 +1,18 @@ -use allsorts::subset; use printpdf::*; const WIN_1252: &[char; 214] = &[ - '!', '"','#', '$', '%','&', '\'', - '(', ')', '*', '+', ',', '-', '.', '/', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', - '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', - 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', - 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', - 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', - 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', - '~', '€', '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', - 'Ž', '‘', '’', '“', '•', '–', '—', '˜', '™', 'š', '›', 'œ', 'ž', - 'Ÿ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', - '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', - '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', - 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', - 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', - 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', - 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', - 'ü', 'ý', 'þ', 'ÿ' + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '€', + '‚', 'ƒ', '„', '…', '†', '‡', 'ˆ', '‰', 'Š', '‹', 'Œ', 'Ž', '‘', '’', '“', '•', '–', '—', '˜', + '™', 'š', '›', 'œ', 'ž', 'Ÿ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '®', + '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', + 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', + 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', + 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', + 'û', 'ü', 'ý', 'þ', 'ÿ', ]; const FONTS: &[(BuiltinFont, &[u8])] = &[ diff --git a/src/font.rs b/src/font.rs index 6988a6f..dd663a6 100644 --- a/src/font.rs +++ b/src/font.rs @@ -320,10 +320,8 @@ impl ParsedFont { } pub fn subset_simple(&self, chars: &BTreeSet) -> Result { - let scope = ReadScope::new(&self.original_bytes); - let font_file = scope.read::>() - .map_err(|e| e.to_string())?; + let font_file = scope.read::>().map_err(|e| e.to_string())?; let provider = font_file .table_provider(self.original_index) .map_err(|e| e.to_string())?; @@ -344,9 +342,8 @@ impl ParsedFont { let mut gids = p.iter().map(|s| s.0).collect::>(); gids.sort(); gids.dedup(); - - let bytes = allsorts::subset::subset(&provider, &gids) - .map_err(|e| e.to_string())?; + + let bytes = allsorts::subset::subset(&provider, &gids).map_err(|e| e.to_string())?; Ok(SubsetFont { bytes, @@ -372,9 +369,11 @@ impl ParsedFont { .table_provider(self.original_index) .map_err(|e| e.to_string())?; - let font = - allsorts::subset::subset(&provider, &glyph_ids.iter().map(|s| s.0).collect::>()) - .map_err(|e| e.to_string())?; + let font = allsorts::subset::subset( + &provider, + &glyph_ids.iter().map(|s| s.0).collect::>(), + ) + .map_err(|e| e.to_string())?; Ok(SubsetFont { bytes: font, @@ -633,7 +632,7 @@ impl ParsedFont { let second_font_file = second_scope.read::>().ok()?; let second_provider = second_font_file.table_provider(font_index).ok()?; - let mut font_data_impl = allsorts::font::Font::new(second_provider).ok()?; + let font_data_impl = allsorts::font::Font::new(second_provider).ok()?; // required for font layout: gsub_cache, gpos_cache and gdef_table let gsub_cache = None; // font_data_impl.gsub_cache().ok().and_then(|s| s); @@ -651,7 +650,6 @@ impl ParsedFont { println!("warning: no cmap subtable"); } - let hmtx_data = provider .table_data(tag::HMTX) .ok() @@ -668,7 +666,19 @@ impl ParsedFont { .table_data(tag::HHEA) .ok() .and_then(|hhea_data| ReadScope::new(&hhea_data?).read::().ok()) - .unwrap_or(unsafe { std::mem::zeroed() }); + .unwrap_or(HheaTable { + ascender: 0, + descender: 0, + line_gap: 0, + advance_width_max: 0, + min_left_side_bearing: 0, + min_right_side_bearing: 0, + x_max_extent: 0, + caret_slope_rise: 0, + caret_slope_run: 0, + caret_offset: 0, + num_h_metrics: 0, + }); let font_metrics = FontMetrics::from_bytes(font_bytes, font_index); diff --git a/src/html.rs b/src/html.rs index c68114f..db3fe16 100644 --- a/src/html.rs +++ b/src/html.rs @@ -1,8 +1,13 @@ use crate::{BuiltinFont, Mm, Op, PdfDocument, PdfPage, PdfResources, Pt}; +pub use azul_core::dom::Dom; +pub use azul_core::styled_dom::StyledDom; +pub use azul_core::xml::{ + CompileError, ComponentArguments, FilteredComponentArguments, RenderDomError, XmlComponent, + XmlComponentMap, XmlComponentTrait, XmlNode, XmlTextContent, +}; use azul_core::{ app_resources::{ - DpiScaleFactor, Epoch, IdNamespace, ImageCache, - ImageDescriptor, ImageRef, ImageRefHash, RendererResources + DecodedImage, DpiScaleFactor, Epoch, IdNamespace, ImageCache, ImageRef, RendererResources, }, callbacks::DocumentId, display_list::{ @@ -10,29 +15,16 @@ use azul_core::{ StyleBorderStyles, StyleBorderWidths, }, dom::{NodeData, NodeId}, - styled_dom::{ContentGroup, DomId, StyledNode}, + styled_dom::{ContentGroup, StyledNode}, ui_solver::LayoutResult, - window::{FullWindowState, LogicalSize}, xml::Xml, + window::{FullWindowState, LogicalSize}, }; -use azul_css::{CssPropertyValue, FloatValue, LayoutDisplay, StyleTextColor, U8Vec}; -use base64::Engine; +use azul_css::{CssPropertyValue, FloatValue, LayoutDisplay, StyleTextColor}; +pub use azul_css_parser::CssApiWrapper; use rust_fontconfig::{FcFont, FcFontCache, FcPattern}; +use serde_derive::{Deserialize, Serialize}; use std::collections::BTreeMap; -pub use azul_core::xml::{ - XmlComponent, - XmlComponentTrait, - ComponentArguments, - XmlComponentMap, - FilteredComponentArguments, - XmlTextContent, - RenderDomError, - CompileError, - XmlNode -}; -pub use azul_core::styled_dom::StyledDom; -pub use azul_core::dom::Dom; -pub use azul_core::app_resources::ImageRef as InternalImageRef; -pub use azul_css_parser::CssApiWrapper; +use svg2pdf::usvg::tiny_skia_path::Scalar; const DPI_SCALE: DpiScaleFactor = DpiScaleFactor { inner: FloatValue::const_new(1), @@ -44,12 +36,10 @@ const DOCUMENT_ID: DocumentId = DocumentId { id: 0, }; -pub type Base64String = String; - #[derive(Debug)] pub struct XmlRenderOptions { - pub images: BTreeMap, - pub fonts: BTreeMap, + pub images: BTreeMap>, + pub fonts: BTreeMap>, pub page_width: Mm, pub page_height: Mm, pub components: Vec, @@ -72,14 +62,15 @@ pub(crate) fn xml_to_pages( config: XmlRenderOptions, document: &mut PdfDocument, ) -> Result, String> { - let size = LogicalSize { width: config.page_width.into_pt().0, height: config.page_height.into_pt().0, }; - let root_nodes = azulc_lib::xml::parse_xml_string(&fixup_xml(file_contents)) - .map_err(|e| format!("Error parsing XML: {}", e))?; + // inserts images into the PDF resources and changes the src="..." + let xml = fixup_xml(file_contents, document, &config); + let root_nodes = + azulc_lib::xml::parse_xml_string(&xml).map_err(|e| format!("Error parsing XML: {}", e))?; let fixup = fixup_xml_nodes(&root_nodes); @@ -89,21 +80,22 @@ pub(crate) fn xml_to_pages( } let styled_dom = azul_core::xml::str_to_dom( - fixup.as_ref(), - &mut components, - Some(config.page_width.into_pt().0) - ).map_err(|e| format!("Error constructing DOM: {}", e.to_string()))?; + fixup.as_ref(), + &mut components, + Some(config.page_width.into_pt().0), + ) + .map_err(|e| format!("Error constructing DOM: {}", e.to_string()))?; - let dom_id = DomId { inner: 0 }; let mut fake_window_state = FullWindowState::default(); fake_window_state.size.dimensions = size; let mut renderer_resources = RendererResources::default(); + let image_cache = ImageCache { image_id_map: config .images .iter() .filter_map(|(id, bytes)| { - let bytes = base64::prelude::BASE64_STANDARD.decode(bytes).ok()?; + // let bytes = base64::prelude::BASE64_STANDARD.decode(bytes).ok()?; let decoded = crate::image::RawImage::decode_from_bytes(&bytes).ok()?; let raw_image = crate::image::translate_to_internal_rawimage(&decoded); Some((id.clone().into(), ImageRef::new_rawimage(raw_image)?)) @@ -138,14 +130,14 @@ pub(crate) fn xml_to_pages( &config .fonts .iter() - .filter_map(|(id, font_base64)| { - let bytes = base64::prelude::BASE64_STANDARD.decode(font_base64).ok()?; + .filter_map(|(id, bytes)| { + // let bytes = base64::prelude::BASE64_STANDARD.decode(font_base64).ok()?; let pat = FcPattern { name: Some(id.split(".").next().unwrap_or("").to_string()), ..Default::default() }; let font = FcFont { - bytes, + bytes: bytes.clone(), font_index: 0, }; Some((pat, font)) @@ -243,7 +235,28 @@ fn get_fcpat(b: BuiltinFont) -> (FcPattern, FcFont) { ) } -fn fixup_xml(s: &str) -> String { +#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ImageInfo { + pub original_id: String, + pub xobject_id: String, + pub image_type: ImageTypeInfo, + pub width: usize, + pub height: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum ImageTypeInfo { + Image, + Svg, +} + +impl Default for ImageTypeInfo { + fn default() -> Self { + ImageTypeInfo::Image + } +} + +fn fixup_xml(s: &str, doc: &mut PdfDocument, config: &XmlRenderOptions) -> String { let s = if !s.contains("") { format!("{s}") } else { @@ -254,7 +267,60 @@ fn fixup_xml(s: &str) -> String { } else { s.trim().to_string() }; - s.trim().to_string() + + let mut s = s.trim().to_string(); + + for (k, image_bytes) in config.images.iter() { + let opt_svg = std::str::from_utf8(&image_bytes) + .ok() + .and_then(|s| crate::Svg::parse(s).ok()); + + let img_info = match opt_svg { + Some(o) => { + let width = o.width.map(|s| s.0).unwrap_or(0); + let height = o.height.map(|s| s.0).unwrap_or(0); + let image_xobject_id = doc.add_xobject(&o); + ImageInfo { + original_id: k.clone(), + xobject_id: image_xobject_id.0, + image_type: ImageTypeInfo::Svg, + width, + height, + } + } + None => { + let raw_image = match crate::image::RawImage::decode_from_bytes(&image_bytes) { + Ok(o) => o, + Err(e) => { + #[cfg(not(target_family = "wasm"))] + { + println!("{e}"); + } + continue; + } + }; + + let width = raw_image.width; + let height = raw_image.height; + let image_xobject_id = doc.add_image(&raw_image); + ImageInfo { + original_id: k.clone(), + xobject_id: image_xobject_id.0, + image_type: ImageTypeInfo::Image, + width, + height, + } + } + }; + + let json = serde_json::to_string(&img_info).unwrap_or_default(); + + s = s + .replace(&format!("src='{k}'"), &format!("src='{json}'")) + .replace(&format!("src=\"{k}\""), &format!("src='{json}'")); + } + + s } fn fixup_xml_nodes(nodes: &[XmlNode]) -> Vec { @@ -460,18 +526,34 @@ fn displaylist_handle_rect( }); } - if let Some((desc, data, hash, size)) = opt_image { - let image_id = format!("im_azul_layout_{h:032}", h = hash.0); - let xobject_id = crate::XObjectId(image_id.clone()); - let image = crate::XObject::Image(crate::image::translate_from_internal_rawimage( - desc, - data.as_slice(), - )); - doc.resources.xobjects.map.insert(xobject_id, image); + if let Some(image_info) = opt_image { + let source_width = image_info.width; + let source_height = image_info.width; + let target_width = positioned_rect.size.width; + let target_height = positioned_rect.size.height; + let pos = positioned_rect.position.get_static_offset(); + + let is_zero = target_width.is_nearly_zero() + || target_height.is_nearly_zero() + || source_height == 0 + || source_width == 0; + + if !is_zero { + ops.push(Op::UseXObject { + id: crate::XObjectId(image_info.xobject_id.clone()), + transform: crate::XObjectTransform { + translate_x: Some(Pt(pos.x)), + translate_y: Some(Pt(page_height.0 - pos.y)), + rotate: None, // todo + scale_x: Some(target_width / source_width as f32), + scale_y: Some(target_height / source_height as f32), + dpi: None, + }, + }); + } } if let Some((text, id, color, space_index)) = opt_text { - ops.push(Op::StartTextSection); ops.push(Op::SetFillColor { col: crate::Color::Rgb(crate::Rgb { @@ -485,22 +567,29 @@ fn displaylist_handle_rect( mode: crate::TextRenderingMode::Fill, }); ops.push(Op::SetWordSpacing { percent: 100.0 }); - ops.push(Op::SetLineHeight { lh: Pt(text.font_size_px) }); - + ops.push(Op::SetLineHeight { + lh: Pt(text.font_size_px), + }); + let glyphs = text.get_layouted_glyphs(); let static_bounds = positioned_rect.get_approximate_static_bounds(); for gi in glyphs.glyphs { - ops.push(Op::SetTextCursor { pos: crate::Point { x: Pt(0.0), y: Pt(0.0) } }); - ops.push(Op::SetTextMatrix { + ops.push(Op::SetTextCursor { + pos: crate::Point { + x: Pt(0.0), + y: Pt(0.0), + }, + }); + ops.push(Op::SetTextMatrix { matrix: crate::TextMatrix::Translate( - Pt(static_bounds.min_x() as f32 + (gi.point.x * 2.0)), - Pt(page_height.0 - static_bounds.min_y() as f32 - gi.point.y), - ) + Pt(static_bounds.min_x() as f32 + (gi.point.x * 2.0)), + Pt(page_height.0 - static_bounds.min_y() as f32 - gi.point.y), + ), }); ops.push(Op::WriteCodepoints { - font: id.clone(), + font: id.clone(), size: Pt(text.font_size_px * 2.0), cp: vec![(gi.index as u16, ' ')], }); @@ -510,6 +599,7 @@ fn displaylist_handle_rect( } if !newops.is_empty() { + println!("{newops:?}"); ops.push(Op::SaveGraphicsState); ops.append(&mut newops); ops.push(Op::RestoreGraphicsState); @@ -683,27 +773,19 @@ fn get_background_content( v } -fn get_image_node( - html_node: &NodeData, -) -> Option<(&ImageDescriptor, &U8Vec, ImageRefHash, LogicalSize)> { - use azul_core::app_resources::DecodedImage; - use azul_core::app_resources::ImageData; +fn get_image_node(html_node: &NodeData) -> Option { use azul_core::dom::NodeType; - let image_ref = match html_node.get_node_type() { - NodeType::Image(image_ref) => image_ref, + let data = match html_node.get_node_type() { + NodeType::Image(image_ref) => image_ref.get_data(), _ => return None, }; - let image_hash = image_ref.get_hash(); - let image_size = image_ref.get_size(); - - let (descriptor, data) = match image_ref.get_data() { - DecodedImage::Raw((descriptor, ImageData::Raw(vec))) => (descriptor, vec), - _ => return None, - }; - - Some((descriptor, data, image_hash, image_size)) + if let DecodedImage::NullImage { tag, .. } = data { + serde_json::from_slice(tag).ok() + } else { + None + } } fn get_text_node( diff --git a/src/image.rs b/src/image.rs index a5ba1f2..27b93dc 100644 --- a/src/image.rs +++ b/src/image.rs @@ -10,6 +10,7 @@ pub struct RawImage { pub width: usize, pub height: usize, pub data_format: RawImageFormat, + pub tag: Vec, } struct RawImageU8 { @@ -59,6 +60,41 @@ impl RawImageFormat { } } + fn from_internal(f: &azul_core::app_resources::RawImageFormat) -> Self { + use azul_core::app_resources::RawImageFormat; + match f { + RawImageFormat::R8 => crate::RawImageFormat::R8, + RawImageFormat::RG8 => crate::RawImageFormat::RG8, + RawImageFormat::RGB8 => crate::RawImageFormat::RGB8, + RawImageFormat::RGBA8 => crate::RawImageFormat::RGBA8, + RawImageFormat::R16 => crate::RawImageFormat::R16, + RawImageFormat::RG16 => crate::RawImageFormat::RG16, + RawImageFormat::RGB16 => crate::RawImageFormat::RGB16, + RawImageFormat::RGBA16 => crate::RawImageFormat::RGBA16, + RawImageFormat::BGR8 => crate::RawImageFormat::BGR8, + RawImageFormat::BGRA8 => crate::RawImageFormat::BGRA8, + RawImageFormat::RGBF32 => crate::RawImageFormat::RGBF32, + RawImageFormat::RGBAF32 => crate::RawImageFormat::RGBAF32, + } + } + + fn into_internal(&self) -> azul_core::app_resources::RawImageFormat { + match self { + RawImageFormat::R8 => azul_core::app_resources::RawImageFormat::R8, + RawImageFormat::RG8 => azul_core::app_resources::RawImageFormat::RG8, + RawImageFormat::RGB8 => azul_core::app_resources::RawImageFormat::RGB8, + RawImageFormat::RGBA8 => azul_core::app_resources::RawImageFormat::RGBA8, + RawImageFormat::R16 => azul_core::app_resources::RawImageFormat::R16, + RawImageFormat::RG16 => azul_core::app_resources::RawImageFormat::RG16, + RawImageFormat::RGB16 => azul_core::app_resources::RawImageFormat::RGB16, + RawImageFormat::RGBA16 => azul_core::app_resources::RawImageFormat::RGBA16, + RawImageFormat::BGR8 => azul_core::app_resources::RawImageFormat::BGR8, + RawImageFormat::BGRA8 => azul_core::app_resources::RawImageFormat::BGRA8, + RawImageFormat::RGBF32 => azul_core::app_resources::RawImageFormat::RGBF32, + RawImageFormat::RGBAF32 => azul_core::app_resources::RawImageFormat::RGBAF32, + } + } + pub fn has_alpha(&self) -> bool { use self::RawImageFormat::*; matches!(self, RGBA8 | RGBA16 | RGBAF32) @@ -94,7 +130,39 @@ pub enum RawImageData { F32(Vec), } +impl RawImageData { + pub fn empty(format: RawImageFormat) -> Self { + use self::RawImageFormat::*; + match format { + R8 | RG8 | RGB8 | RGBA8 | BGR8 | BGRA8 => Self::U8(Vec::new()), + + R16 | RG16 | RGB16 | RGBA16 => Self::U16(Vec::new()), + + RGBF32 | RGBAF32 => Self::F32(Vec::new()), + } + } + + pub fn is_empty(&self) -> bool { + match self { + RawImageData::U8(vec) => vec.is_empty(), + RawImageData::U16(vec) => vec.is_empty(), + RawImageData::F32(vec) => vec.is_empty(), + } + } +} + impl RawImage { + /// Creates an empty `RawImage` + pub fn empty(width: usize, height: usize, format: crate::RawImageFormat) -> Self { + Self { + width, + height, + data_format: format, + pixels: RawImageData::empty(format), + tag: Vec::new(), + } + } + /// NOTE: depends on the enabled image formats! pub fn decode_from_bytes(bytes: &[u8]) -> Result { use image::DynamicImage::*; @@ -102,64 +170,100 @@ impl RawImage { let im = image::guess_format(bytes).map_err(|e| e.to_string())?; let b_len = bytes.len(); - #[cfg(not(feature = "gif"))] { + #[cfg(not(feature = "gif"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'gif' to decode GIF files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Gif { return Err(err); } + if im == image::ImageFormat::Gif { + return Err(err); + } } - #[cfg(not(feature = "jpeg"))] { + #[cfg(not(feature = "jpeg"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'jpeg' to decode JPEG files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Gif { return Err(err); } + if im == image::ImageFormat::Gif { + return Err(err); + } } - #[cfg(not(feature = "png"))] { + #[cfg(not(feature = "png"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'png' to decode PNG files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Png { return Err(err); } + if im == image::ImageFormat::Png { + return Err(err); + } } - #[cfg(not(feature = "pnm"))] { + #[cfg(not(feature = "pnm"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'pnm' to decode PNM files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Pnm { return Err(err); } + if im == image::ImageFormat::Pnm { + return Err(err); + } } - #[cfg(not(feature = "tiff"))] { + #[cfg(not(feature = "tiff"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'tiff' to decode TIFF files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Tiff { return Err(err); } + if im == image::ImageFormat::Tiff { + return Err(err); + } } - - #[cfg(not(feature = "tiff"))] { + + #[cfg(not(feature = "tiff"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'tiff' to decode TIFF files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Tiff { return Err(err); } + if im == image::ImageFormat::Tiff { + return Err(err); + } } - #[cfg(not(feature = "bmp"))] { + #[cfg(not(feature = "bmp"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'bmp' to decode BMP files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Bmp { return Err(err); } + if im == image::ImageFormat::Bmp { + return Err(err); + } } - #[cfg(not(feature = "ico"))] { + #[cfg(not(feature = "ico"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'ico' to decode ICO files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Ico { return Err(err); } + if im == image::ImageFormat::Ico { + return Err(err); + } } - #[cfg(not(feature = "tga"))] { + #[cfg(not(feature = "tga"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'tga' to decode TGA files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Tga { return Err(err); } + if im == image::ImageFormat::Tga { + return Err(err); + } } - #[cfg(not(feature = "hdr"))] { + #[cfg(not(feature = "hdr"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'hdr' to decode HDR files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Hdr { return Err(err); } + if im == image::ImageFormat::Hdr { + return Err(err); + } } - #[cfg(not(feature = "dds"))] { + #[cfg(not(feature = "dds"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'dds' to decode DDS files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::Dds { return Err(err); } + if im == image::ImageFormat::Dds { + return Err(err); + } } - #[cfg(not(feature = "webp"))] { + #[cfg(not(feature = "webp"))] + { let err = format!("cannot decode image (len = {b_len} bytes): printpdf is missing feature 'webp' to decode WEBP files. Please enable it or construct the RawImage manually."); - if im == image::ImageFormat::WebP { return Err(err); } + if im == image::ImageFormat::WebP { + return Err(err); + } } let im = image::ImageReader::new(Cursor::new(bytes)) @@ -202,8 +306,26 @@ impl RawImage { width: w as usize, height: h as usize, data_format: ct, + tag: Vec::new(), }) } + + /// Translates to an internal `RawImage`, necessary for the `` component + pub fn to_internal(&self) -> azul_core::app_resources::ImageRef { + let invalid = azul_core::app_resources::ImageRef::null_image( + self.width, + self.height, + self.data_format.into_internal(), + self.tag.clone(), + ); + + if self.pixels.is_empty() { + invalid + } else { + azul_core::app_resources::ImageRef::new_rawimage(translate_to_internal_rawimage(self)) + .unwrap_or(invalid) + } + } } pub(crate) fn image_to_stream(im: RawImage, doc: &mut lopdf::Document) -> lopdf::Stream { @@ -305,33 +427,6 @@ fn split_rawimage_into_rgb_plus_alpha(im: RawImage) -> (RawImageU8, Option RawImage { - use azul_core::app_resources::RawImageFormat; - - RawImage { - pixels: crate::RawImageData::U8(data.to_vec()), - width: im.width, - height: im.height, - data_format: match im.format { - RawImageFormat::R8 => crate::RawImageFormat::R8, - RawImageFormat::RG8 => crate::RawImageFormat::RG8, - RawImageFormat::RGB8 => crate::RawImageFormat::RGB8, - RawImageFormat::RGBA8 => crate::RawImageFormat::RGBA8, - RawImageFormat::R16 => crate::RawImageFormat::R16, - RawImageFormat::RG16 => crate::RawImageFormat::RG16, - RawImageFormat::RGB16 => crate::RawImageFormat::RGB16, - RawImageFormat::RGBA16 => crate::RawImageFormat::RGBA16, - RawImageFormat::BGR8 => crate::RawImageFormat::BGR8, - RawImageFormat::BGRA8 => crate::RawImageFormat::BGRA8, - RawImageFormat::RGBF32 => crate::RawImageFormat::RGBF32, - RawImageFormat::RGBAF32 => crate::RawImageFormat::RGBAF32, - }, - } -} - pub fn translate_to_internal_rawimage(im: &RawImage) -> azul_core::app_resources::RawImage { azul_core::app_resources::RawImage { pixels: match &im.pixels { @@ -346,19 +441,7 @@ pub fn translate_to_internal_rawimage(im: &RawImage) -> azul_core::app_resources width: im.width, height: im.height, premultiplied_alpha: false, - data_format: match &im.data_format { - RawImageFormat::R8 => azul_core::app_resources::RawImageFormat::R8, - RawImageFormat::RG8 => azul_core::app_resources::RawImageFormat::RG8, - RawImageFormat::RGB8 => azul_core::app_resources::RawImageFormat::RGB8, - RawImageFormat::RGBA8 => azul_core::app_resources::RawImageFormat::RGBA8, - RawImageFormat::R16 => azul_core::app_resources::RawImageFormat::R16, - RawImageFormat::RG16 => azul_core::app_resources::RawImageFormat::RG16, - RawImageFormat::RGB16 => azul_core::app_resources::RawImageFormat::RGB16, - RawImageFormat::RGBA16 => azul_core::app_resources::RawImageFormat::RGBA16, - RawImageFormat::BGR8 => azul_core::app_resources::RawImageFormat::BGR8, - RawImageFormat::BGRA8 => azul_core::app_resources::RawImageFormat::BGRA8, - RawImageFormat::RGBF32 => azul_core::app_resources::RawImageFormat::RGBF32, - RawImageFormat::RGBAF32 => azul_core::app_resources::RawImageFormat::RGBAF32, - }, + data_format: im.data_format.into_internal(), + tag: im.tag.clone().into(), } } diff --git a/src/lib.rs b/src/lib.rs index 485e8e8..f62e174 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,15 +194,13 @@ impl PdfDocument { id } - /// Returns the `Pages` rendered by the HTML, adds resources automatically - pub fn with_html( + /// Renders HTML to pages + pub fn html2pages( &mut self, html: &str, config: XmlRenderOptions, - ) -> Result<&mut Self, String> { - let mut pages = crate::html::xml_to_pages(html, config, self)?; - self.pages.append(&mut pages); - Ok(self) + ) -> Result, String> { + crate::html::xml_to_pages(html, config, self) } /// Replaces `document.pages` with the new pages diff --git a/src/serialize.rs b/src/serialize.rs index 8d1d45d..ba48a0c 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -49,6 +49,7 @@ impl Default for PdfSaveOptions { pub fn serialize_pdf_into_bytes(pdf: &PdfDocument, opts: &PdfSaveOptions) -> Vec { let mut doc = lopdf::Document::with_version("1.3"); + doc.reference_table.cross_reference_type = lopdf::xref::XrefType::CrossReferenceTable; let pages_id = doc.new_object_id(); let mut catalog = LoDictionary::from_iter(vec![ ("Type", "Catalog".into()), @@ -889,13 +890,14 @@ fn prepare_fonts(resources: &PdfResources, pages: &[PdfPage]) -> BTreeMap>()) { - Ok(o) => o, - Err(e) => { - println!("{e}"); - continue; - } - }; + let subset_font = + match font.subset(&glyph_ids.iter().map(|s| (*s.0, *s.1)).collect::>()) { + Ok(o) => o, + Err(e) => { + println!("{e}"); + continue; + } + }; let font = match ParsedFont::from_bytes(&subset_font.bytes, 0) { Some(s) => s, None => continue, diff --git a/src/svg.rs b/src/svg.rs index 078a156..bee5f5e 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -46,7 +46,8 @@ impl Svg { // Let's first convert the SVG into an independent chunk. let mut options = usvg::Options::default(); - #[cfg(not(target_arch = "wasm32"))] { + #[cfg(not(target_arch = "wasm32"))] + { options.fontdb_mut().load_system_fonts(); } let tree = usvg::Tree::from_str(svg_string, &options) diff --git a/src/utils.rs b/src/utils.rs index ad6244f..31de1bd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,3 @@ -use base64::Engine; - use crate::date::OffsetDateTime; use std::io::Read; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/src/wasm.rs b/src/wasm.rs index 1ed7a0c..ab9a5ef 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -4,14 +4,16 @@ use std::collections::BTreeMap; use crate::{serialize::PdfSaveOptions, XmlRenderOptions}; +pub type Base64String = String; + #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct PrintPdfApiInput { #[serde(default, skip_serializing_if = "String::is_empty")] pub html: String, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub images: BTreeMap>, + pub images: BTreeMap, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub fonts: BTreeMap>, + pub fonts: BTreeMap, #[serde(default, skip_serializing_if = "PdfGenerationOptions::is_default")] pub options: PdfGenerationOptions, } @@ -73,19 +75,34 @@ fn printpdf_from_xml_internal( let opts = XmlRenderOptions { page_width: Mm(input.options.page_width_mm.unwrap_or(210.0)), page_height: Mm(input.options.page_height_mm.unwrap_or(297.0)), - images: BTreeMap::new(), - fonts: BTreeMap::new(), + images: input + .images + .iter() + .filter_map(|(k, v)| { + Some((k.clone(), base64::prelude::BASE64_STANDARD.decode(v).ok()?)) + }) + .collect(), + fonts: input + .fonts + .iter() + .filter_map(|(k, v)| { + Some((k.clone(), base64::prelude::BASE64_STANDARD.decode(v).ok()?)) + }) + .collect(), components: Vec::new(), }; - let pdf = crate::PdfDocument::new("HTML rendering demo") - .with_html(&input.html, opts) + let mut pdf = crate::PdfDocument::new("HTML rendering demo"); + + let pages = pdf + .html2pages(&input.html, opts) .map_err(|e| PrintPdfApiReturn { pdf: String::new(), status: 2, error: e, - })? - .save(&PdfSaveOptions::default()); + })?; + + let pdf = pdf.with_pages(pages).save(&PdfSaveOptions::default()); Ok(PrintPdfApiReturn { pdf: BASE64_STANDARD.encode(pdf),