From 9165af773403f31b037982efe466febd7bf9848f Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Sat, 30 Mar 2024 20:03:22 +0100 Subject: [PATCH] Add text feature --- CHANGELOG.md | 3 +- Cargo.lock | 6 +-- Cargo.toml | 20 ++++---- cli/Cargo.toml | 7 +-- cli/src/convert.rs | 18 ++++++-- src/lib.rs | 28 ++++++++++-- src/render/group.rs | 2 +- src/render/mod.rs | 8 +++- src/render/text.rs | 66 +++++++++++++++++++++++++-- src/util/context.rs | 103 ++++++++++++++---------------------------- src/util/resources.rs | 1 + 11 files changed, 163 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c59588..8ef369f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Added support for text embedding. +- Added a `text` feature flag. - The `convert_str` method has been removed. You should now always convert your SVG string into a `usvg` tree yourself. - The `convert_tree` method has been renamed into `to_pdf`, and now requires you to provide the fontdb @@ -17,8 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TODO: The CLI options have been (temporarily) removed. They will be readded before the next release. - TODO: Add tests for CLI and svg options - TODO: Add CLI option to convert text to paths. -- TODO: Add CI test to test builds with different feature -- TODO: Add text feature? ### Changed - Bumped resvg to v0.40. diff --git a/Cargo.lock b/Cargo.lock index 98504083..b388f72b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,6 +1171,7 @@ checksum = "09eab8a83bff89ba2200bd4c59be45c7c787f988431b936099a5a266c957f2f9" name = "svg2pdf" version = "0.9.1" dependencies = [ + "fontdb", "image", "log", "miniz_oxide", @@ -1193,7 +1194,6 @@ dependencies = [ "clap_complete", "clap_mangen", "fontdb", - "image", "log", "miniz_oxide", "pdf-writer", @@ -1348,9 +1348,9 @@ checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" [[package]] name = "unicode-script" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc" +checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" [[package]] name = "unicode-vo" diff --git a/Cargo.toml b/Cargo.toml index 4f2fd50a..36fe88d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,10 @@ oxipng = { version = "9", default-features = false, features = ["filetime", "par pdf-writer = "0.9" pdfium-render = "0.8.6" termcolor = "1.2" -usvg = { git = "https://github.com/RazrFalcon/resvg", default-features = false, features = ["text"] } +usvg = { git = "https://github.com/RazrFalcon/resvg", default-features = false} tiny-skia = "0.11.4" unicode-properties = "0.1.1" -resvg = {git = "https://github.com/RazrFalcon/resvg"} +resvg = {git = "https://github.com/RazrFalcon/resvg", default-features = false} subsetter = "0.1.1" ttf-parser = { version = "0.20.0" } siphasher = { version = "1.0.1"} @@ -48,21 +48,25 @@ license = { workspace = true } bench = false [features] -default = ["image", "filters"] +default = ["image", "filters", "text"] +text = ["usvg/text", "resvg/text", "dep:siphasher", + "dep:subsetter", "dep:ttf-parser", "dep:unicode-properties", + "dep:fontdb"] image = ["dep:image"] -filters = ["image", "dep:tiny-skia", "dep:resvg"] +filters = ["image", "dep:tiny-skia", "resvg/raster-images"] [dependencies] -unicode-properties = { workspace = true } +unicode-properties = { workspace = true, optional = true } miniz_oxide = { workspace = true } once_cell = { workspace = true } pdf-writer = { workspace = true } +fontdb = { workspace = true, optional = true} usvg = { workspace = true } log = { workspace = true } image = { workspace = true, optional = true } tiny-skia = {workspace = true, optional = true } resvg = {workspace = true, optional = true } -subsetter = { workspace = true } -ttf-parser = { workspace = true } -siphasher = { workspace = true } +subsetter = { workspace = true, optional = true } +ttf-parser = { workspace = true, optional = true } +siphasher = { workspace = true, optional = true } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index cc952409..5cb68411 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,8 +20,8 @@ doc = false [dependencies] clap = { workspace = true } -fontdb = { workspace = true } -image = { workspace = true } +# TODO: Don't include if not build with text feature +fontdb = { workspace = true} log = { workspace = true } miniz_oxide = { workspace = true } pdf-writer = { workspace = true } @@ -30,9 +30,10 @@ termcolor = { workspace = true } usvg = { workspace = true } [features] -default = ["svg2pdf/default"] +default = ["image", "filters", "text"] image = ["svg2pdf/image"] filters = ["svg2pdf/filters"] +text = ["svg2pdf/text", "usvg/text"] [build-dependencies] clap = { workspace = true, features = ["string"] } diff --git a/cli/src/convert.rs b/cli/src/convert.rs index 119acf8b..691af88b 100644 --- a/cli/src/convert.rs +++ b/cli/src/convert.rs @@ -30,10 +30,20 @@ pub fn convert_(input: &PathBuf, output: Option) -> Result<(), String> let options = usvg::Options::default(); - let tree = - usvg::Tree::from_str(&svg, &options, &fontdb).map_err(|err| err.to_string())?; - - let pdf = svg2pdf::to_pdf(&tree, Options::default(), &fontdb); + let tree = usvg::Tree::from_str( + &svg, + &options, + #[cfg(feature = "text")] + &fontdb, + ) + .map_err(|err| err.to_string())?; + + let pdf = svg2pdf::to_pdf( + &tree, + Options::default(), + #[cfg(feature = "text")] + &fontdb, + ); std::fs::write(output, pdf).map_err(|_| "Failed to write PDF file")?; diff --git a/src/lib.rs b/src/lib.rs index fb89dce7..85d43d15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,9 @@ pub use usvg; use once_cell::sync::Lazy; use pdf_writer::{Chunk, Content, Filter, Finish, Pdf, Rect, Ref, TextStr}; -use usvg::{fontdb, Tree}; +#[cfg(feature = "text")] +use usvg::fontdb; +use usvg::Tree; use crate::render::{tree_to_stream, tree_to_xobject}; use crate::util::context::Context; @@ -127,8 +129,18 @@ impl Default for Options { /// std::fs::write(output, pdf)?; /// # Ok(()) } /// ``` -pub fn to_pdf(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Vec { - let mut ctx = Context::new(tree, options, fontdb); +pub fn to_pdf( + tree: &Tree, + options: Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, +) -> Vec { + let mut ctx = Context::new( + #[cfg(feature = "text")] + tree, + options, + #[cfg(feature = "text")] + fontdb, + ); let mut pdf = Pdf::new(); let catalog_ref = ctx.alloc_ref(); @@ -275,11 +287,17 @@ pub fn to_pdf(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Vec (Chunk, Ref) { let mut chunk = Chunk::new(); - let mut ctx = Context::new(tree, options, fontdb); + let mut ctx = Context::new( + #[cfg(feature = "text")] + tree, + options, + #[cfg(feature = "text")] + fontdb, + ); let x_ref = tree_to_xobject(tree, &mut chunk, &mut ctx); ctx.write_global_objects(&mut chunk); (chunk, x_ref) diff --git a/src/render/group.rs b/src/render/group.rs index 321010d1..90aa519a 100644 --- a/src/render/group.rs +++ b/src/render/group.rs @@ -27,7 +27,7 @@ pub fn render( #[cfg(not(feature = "filters"))] if !group.filters().is_empty() { - log::warn!("Filters have been disabled in this build of svg2pdf.") + log::warn!("Failed convert filter because the filters feature was disabled. Skipping.") } let initial_opacity = initial_opacity.unwrap_or(Opacity::ONE); diff --git a/src/render/mod.rs b/src/render/mod.rs index fd420d56..bb9eed11 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -15,6 +15,7 @@ pub mod image; pub mod mask; pub mod path; pub mod pattern; +#[cfg(feature = "text")] pub mod text; /// Write a tree into a stream. Assumes that the stream belongs to transparency group and the object @@ -107,8 +108,9 @@ impl Render for Node { ), #[cfg(not(feature = "image"))] Node::Image(_) => { - log::warn!("Images have been disabled in this build of svg2pdf.") + log::warn!("Failed convert image because the image feature was disabled. Skipping.") } + #[cfg(feature = "text")] Node::Text(ref text) => { text::render(text, chunk, content, ctx, rc, accumulated_transform); // group::render( @@ -120,6 +122,10 @@ impl Render for Node { // None, // ); } + #[cfg(not(feature = "text"))] + Node::Text(_) => { + log::warn!("Failed convert text because the text feature was disabled. Skipping.") + } } } } diff --git a/src/render/text.rs b/src/render/text.rs index b7a483b7..6a0bf542 100644 --- a/src/render/text.rs +++ b/src/render/text.rs @@ -1,18 +1,18 @@ use crate::render::path; use crate::util::allocate::RefAllocator; -use crate::util::context::{Context, Font}; +use crate::util::context::Context; use crate::util::helper::{deflate, TransformExt}; use crate::util::resources::ResourceContainer; use pdf_writer::types::{ CidFontType, FontFlags, SystemInfo, TextRenderingMode, UnicodeCmap, }; -use pdf_writer::{Chunk, Content, Filter, Finish, Name, Str}; +use pdf_writer::{Chunk, Content, Filter, Finish, Name, Ref, Str}; use siphasher::sip128::{Hasher128, SipHasher13}; use std::collections::{BTreeMap, HashMap}; use std::hash::Hash; use ttf_parser::{name_id, Face, GlyphId, PlatformId, Tag}; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; -use usvg::{Fill, PaintOrder, Stroke, Transform, Visibility}; +use usvg::{Fill, Group, ImageKind, Node, PaintOrder, Stroke, Transform, Visibility}; const CFF: Tag = Tag::from_bytes(b"CFF "); const CFF2: Tag = Tag::from_bytes(b"CFF2"); @@ -473,3 +473,63 @@ where Some((key, head)) } } + +#[derive(Clone)] +pub struct Font { + pub glyph_set: BTreeMap, + pub reference: Ref, + pub face_data: Vec, + pub units_per_em: u16, + pub face_index: u32, +} + +pub fn fill_fonts(group: &Group, ctx: &mut Context, fontdb: &fontdb::Database) { + for child in group.children() { + match child { + Node::Text(t) => { + let allocator = &mut ctx.ref_allocator; + for span in t.layouted() { + for g in &span.positioned_glyphs { + let font = ctx.fonts.entry(g.font).or_insert_with(|| { + fontdb + .with_face_data(g.font, |data, face_index| { + // TODO: Currently, we are parsing each font twice, once here + // and once again when writing the fonts. We should probably + // improve on that... + if let Ok(ttf) = + ttf_parser::Face::parse(data, face_index) + { + let reference = allocator.alloc_ref(); + let glyph_set = BTreeMap::new(); + return Some(Font { + reference, + face_data: Vec::from(data), + units_per_em: ttf.units_per_em(), + glyph_set, + face_index, + }); + } + + None + }) + .flatten() + }); + + if let Some(ref mut font) = font { + font.glyph_set.insert(g.glyph_id.0, g.text.clone()); + } + } + } + } + Node::Group(group) => fill_fonts(group, ctx, fontdb), + Node::Image(image) => { + if let ImageKind::SVG(svg) = image.kind() { + fill_fonts(svg.root(), ctx, fontdb); + } + } + _ => {} + } + + child.subroots(|subroot| fill_fonts(subroot, ctx, fontdb)); + } +} diff --git a/src/util/context.rs b/src/util/context.rs index fb5ea48e..45a4ae9b 100644 --- a/src/util/context.rs +++ b/src/util/context.rs @@ -1,86 +1,34 @@ use pdf_writer::{Chunk, Content, Filter, Ref}; -use std::collections::{BTreeMap, HashMap}; -use usvg::fontdb::ID; -use usvg::{fontdb, Group, ImageKind, Node, Tree}; + +#[cfg(feature = "text")] +use { + crate::render::text, + crate::render::text::{write_font, Font}, + std::collections::HashMap, + usvg::fontdb, + usvg::fontdb::ID, + usvg::Tree, +}; use super::helper::deflate; -use crate::render::text::write_font; use crate::util::allocate::RefAllocator; use crate::{Options, GRAY_ICC_DEFLATED, SRGB_ICC_DEFLATED}; -#[derive(Clone)] -pub struct Font { - pub glyph_set: BTreeMap, - pub reference: Ref, - pub face_data: Vec, - pub units_per_em: u16, - pub face_index: u32, -} - /// Holds all of the necessary information for the conversion process. pub struct Context { /// Options that where passed by the user. pub options: Options, /// The refs of the fonts + #[cfg(feature = "text")] pub fonts: HashMap>, srgb_ref: Option, sgray_ref: Option, pub ref_allocator: RefAllocator, } -fn fill_fonts(group: &Group, ctx: &mut Context, fontdb: &fontdb::Database) { - for child in group.children() { - match child { - Node::Text(t) => { - let allocator = &mut ctx.ref_allocator; - for span in t.layouted() { - for g in &span.positioned_glyphs { - let font = ctx.fonts.entry(g.font).or_insert_with(|| { - fontdb - .with_face_data(g.font, |data, face_index| { - // TODO: Currently, we are parsing each font twice, once here - // and once again when writing the fonts. We should probably - // improve on that... - if let Ok(ttf) = - ttf_parser::Face::parse(data, face_index) - { - let reference = allocator.alloc_ref(); - let glyph_set = BTreeMap::new(); - return Some(Font { - reference, - face_data: Vec::from(data), - units_per_em: ttf.units_per_em(), - glyph_set, - face_index, - }); - } - - None - }) - .flatten() - }); - - if let Some(ref mut font) = font { - font.glyph_set.insert(g.glyph_id.0, g.text.clone()); - } - } - } - } - Node::Group(group) => fill_fonts(group, ctx, fontdb), - Node::Image(image) => { - if let ImageKind::SVG(svg) = image.kind() { - fill_fonts(svg.root(), ctx, fontdb); - } - } - _ => {} - } - - child.subroots(|subroot| fill_fonts(subroot, ctx, fontdb)); - } -} - impl Context { /// Create a new context. + #[cfg(feature = "text")] pub fn new(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Self { let mut ctx = Self { ref_allocator: RefAllocator::new(), @@ -90,11 +38,23 @@ impl Context { sgray_ref: None, }; - fill_fonts(tree.root(), &mut ctx, fontdb); + text::fill_fonts(tree.root(), &mut ctx, fontdb); ctx } + // TODO: Make context less ugly with different features. + /// Create a new context. + #[cfg(not(feature = "text"))] + pub fn new(options: Options) -> Self { + Self { + ref_allocator: RefAllocator::new(), + options, + srgb_ref: None, + sgray_ref: None, + } + } + /// Allocate a new reference. pub fn alloc_ref(&mut self) -> Ref { self.ref_allocator.alloc_ref() @@ -114,15 +74,20 @@ impl Context { *sgray_ref.get_or_insert_with(|| alloc.alloc_ref()) } + #[cfg(feature = "text")] pub fn font_ref(&self, id: ID) -> Option<&Font> { self.fonts.get(&id).and_then(|f| f.as_ref()) } pub fn write_global_objects(&mut self, pdf: &mut Chunk) { - let allocator = &mut self.ref_allocator; - for font in self.fonts.values_mut() { - if let Some(font) = font.as_mut() { - write_font(pdf, allocator, font); + #[cfg(feature = "text")] + { + let allocator = &mut self.ref_allocator; + + for font in self.fonts.values_mut() { + if let Some(font) = font.as_mut() { + write_font(pdf, allocator, font); + } } } diff --git a/src/util/resources.rs b/src/util/resources.rs index 19897f1b..6eee3543 100644 --- a/src/util/resources.rs +++ b/src/util/resources.rs @@ -141,6 +141,7 @@ impl ResourceContainer { } /// Add a new Font as a resource. Returns the name of the Font. + #[cfg(feature = "text")] pub fn add_font(&mut self, reference: Ref) -> Rc { self.add_resource_entry(reference, PendingResourceType::Font) }