Skip to content

Commit efeafb7

Browse files
authored
Revamp feature selection (#67)
1 parent 948c590 commit efeafb7

13 files changed

Lines changed: 171 additions & 100 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
- uses: actions/checkout@v4
5050
- uses: dtolnay/rust-toolchain@stable
5151
- uses: Swatinem/rust-cache@v2
52+
- uses: taiki-e/install-action@cargo-hack
5253
- run: cargo clippy --all-targets
5354
- run: cargo fmt --check --all
5455
- run: cargo doc --workspace --no-deps
56+
- run: cargo hack check --each-feature -p svg2pdf
57+
- run: cargo hack check --each-feature -p svg2pdf-cli

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99
- Added support for text embedding.
10+
- Added a `text` feature flag.
1011
- The `convert_str` method has been removed. You should now always convert your SVG string into a `usvg`
1112
tree yourself.
1213
- 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
1718
- TODO: The CLI options have been (temporarily) removed. They will be readded before the next release.
1819
- TODO: Add tests for CLI and svg options
1920
- TODO: Add CLI option to convert text to paths.
20-
- TODO: Add CI test to test builds with different feature
21-
- TODO: Add text feature?
2221

2322
### Changed
2423
- Bumped resvg to v0.40.

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ oxipng = { version = "9", default-features = false, features = ["filetime", "par
2525
pdf-writer = "0.9"
2626
pdfium-render = "0.8.6"
2727
termcolor = "1.2"
28-
usvg = { git = "https://github.com/RazrFalcon/resvg", default-features = false, features = ["text"] }
28+
usvg = { git = "https://github.com/RazrFalcon/resvg", default-features = false}
2929
tiny-skia = "0.11.4"
3030
unicode-properties = "0.1.1"
31-
resvg = {git = "https://github.com/RazrFalcon/resvg"}
31+
resvg = {git = "https://github.com/RazrFalcon/resvg", default-features = false}
3232
subsetter = "0.1.1"
3333
ttf-parser = { version = "0.20.0" }
3434
siphasher = { version = "1.0.1"}
@@ -48,21 +48,25 @@ license = { workspace = true }
4848
bench = false
4949

5050
[features]
51-
default = ["image", "filters"]
51+
default = ["image", "filters", "text"]
52+
text = ["usvg/text", "resvg/text", "dep:siphasher",
53+
"dep:subsetter", "dep:ttf-parser", "dep:unicode-properties",
54+
"dep:fontdb"]
5255
image = ["dep:image"]
53-
filters = ["image", "dep:tiny-skia", "dep:resvg"]
56+
filters = ["image", "dep:tiny-skia", "resvg/raster-images"]
5457

5558
[dependencies]
56-
unicode-properties = { workspace = true }
59+
unicode-properties = { workspace = true, optional = true }
5760
miniz_oxide = { workspace = true }
5861
once_cell = { workspace = true }
5962
pdf-writer = { workspace = true }
63+
fontdb = { workspace = true, optional = true}
6064
usvg = { workspace = true }
6165
log = { workspace = true }
6266
image = { workspace = true, optional = true }
6367
tiny-skia = {workspace = true, optional = true }
6468
resvg = {workspace = true, optional = true }
65-
subsetter = { workspace = true }
66-
ttf-parser = { workspace = true }
67-
siphasher = { workspace = true }
69+
subsetter = { workspace = true, optional = true }
70+
ttf-parser = { workspace = true, optional = true }
71+
siphasher = { workspace = true, optional = true }
6872

cli/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ doc = false
2020

2121
[dependencies]
2222
clap = { workspace = true }
23-
fontdb = { workspace = true }
24-
image = { workspace = true }
23+
# TODO: Don't include if not build with text feature
24+
fontdb = { workspace = true}
2525
log = { workspace = true }
2626
miniz_oxide = { workspace = true }
2727
pdf-writer = { workspace = true }
@@ -30,9 +30,10 @@ termcolor = { workspace = true }
3030
usvg = { workspace = true }
3131

3232
[features]
33-
default = ["svg2pdf/default"]
33+
default = ["image", "filters", "text"]
3434
image = ["svg2pdf/image"]
3535
filters = ["svg2pdf/filters"]
36+
text = ["svg2pdf/text", "usvg/text"]
3637

3738
[build-dependencies]
3839
clap = { workspace = true, features = ["string"] }

cli/src/convert.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,20 @@ pub fn convert_(input: &PathBuf, output: Option<PathBuf>) -> Result<(), String>
3030

3131
let options = usvg::Options::default();
3232

33-
let tree =
34-
usvg::Tree::from_str(&svg, &options, &fontdb).map_err(|err| err.to_string())?;
35-
36-
let pdf = svg2pdf::to_pdf(&tree, Options::default(), &fontdb);
33+
let tree = usvg::Tree::from_str(
34+
&svg,
35+
&options,
36+
#[cfg(feature = "text")]
37+
&fontdb,
38+
)
39+
.map_err(|err| err.to_string())?;
40+
41+
let pdf = svg2pdf::to_pdf(
42+
&tree,
43+
Options::default(),
44+
#[cfg(feature = "text")]
45+
&fontdb,
46+
);
3747

3848
std::fs::write(output, pdf).map_err(|_| "Failed to write PDF file")?;
3949

src/lib.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ pub use usvg;
6060

6161
use once_cell::sync::Lazy;
6262
use pdf_writer::{Chunk, Content, Filter, Finish, Pdf, Rect, Ref, TextStr};
63-
use usvg::{fontdb, Tree};
63+
#[cfg(feature = "text")]
64+
use usvg::fontdb;
65+
use usvg::Tree;
6466

6567
use crate::render::{tree_to_stream, tree_to_xobject};
6668
use crate::util::context::Context;
@@ -127,8 +129,18 @@ impl Default for Options {
127129
/// std::fs::write(output, pdf)?;
128130
/// # Ok(()) }
129131
/// ```
130-
pub fn to_pdf(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Vec<u8> {
131-
let mut ctx = Context::new(tree, options, fontdb);
132+
pub fn to_pdf(
133+
tree: &Tree,
134+
options: Options,
135+
#[cfg(feature = "text")] fontdb: &fontdb::Database,
136+
) -> Vec<u8> {
137+
let mut ctx = Context::new(
138+
#[cfg(feature = "text")]
139+
tree,
140+
options,
141+
#[cfg(feature = "text")]
142+
fontdb,
143+
);
132144
let mut pdf = Pdf::new();
133145

134146
let catalog_ref = ctx.alloc_ref();
@@ -275,11 +287,17 @@ pub fn to_pdf(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Vec<u
275287
pub fn to_chunk(
276288
tree: &Tree,
277289
options: Options,
278-
fontdb: &fontdb::Database,
290+
#[cfg(feature = "text")] fontdb: &fontdb::Database,
279291
) -> (Chunk, Ref) {
280292
let mut chunk = Chunk::new();
281293

282-
let mut ctx = Context::new(tree, options, fontdb);
294+
let mut ctx = Context::new(
295+
#[cfg(feature = "text")]
296+
tree,
297+
options,
298+
#[cfg(feature = "text")]
299+
fontdb,
300+
);
283301
let x_ref = tree_to_xobject(tree, &mut chunk, &mut ctx);
284302
ctx.write_global_objects(&mut chunk);
285303
(chunk, x_ref)

src/render/group.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ pub fn render(
2727

2828
#[cfg(not(feature = "filters"))]
2929
if !group.filters().is_empty() {
30-
log::warn!("Filters have been disabled in this build of svg2pdf.")
30+
log::warn!(
31+
"Failed convert filter because the filters feature was disabled. Skipping."
32+
)
3133
}
3234

3335
let initial_opacity = initial_opacity.unwrap_or(Opacity::ONE);

src/render/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod image;
1515
pub mod mask;
1616
pub mod path;
1717
pub mod pattern;
18+
#[cfg(feature = "text")]
1819
pub mod text;
1920

2021
/// 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 {
107108
),
108109
#[cfg(not(feature = "image"))]
109110
Node::Image(_) => {
110-
log::warn!("Images have been disabled in this build of svg2pdf.")
111+
log::warn!("Failed convert image because the image feature was disabled. Skipping.")
111112
}
113+
#[cfg(feature = "text")]
112114
Node::Text(ref text) => {
113115
text::render(text, chunk, content, ctx, rc, accumulated_transform);
114116
// group::render(
@@ -120,6 +122,10 @@ impl Render for Node {
120122
// None,
121123
// );
122124
}
125+
#[cfg(not(feature = "text"))]
126+
Node::Text(_) => {
127+
log::warn!("Failed convert text because the text feature was disabled. Skipping.")
128+
}
123129
}
124130
}
125131
}

src/render/text.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
use crate::render::path;
22
use crate::util::allocate::RefAllocator;
3-
use crate::util::context::{Context, Font};
3+
use crate::util::context::Context;
44
use crate::util::helper::{deflate, TransformExt};
55
use crate::util::resources::ResourceContainer;
66
use pdf_writer::types::{
77
CidFontType, FontFlags, SystemInfo, TextRenderingMode, UnicodeCmap,
88
};
9-
use pdf_writer::{Chunk, Content, Filter, Finish, Name, Str};
9+
use pdf_writer::{Chunk, Content, Filter, Finish, Name, Ref, Str};
1010
use siphasher::sip128::{Hasher128, SipHasher13};
1111
use std::collections::{BTreeMap, HashMap};
1212
use std::hash::Hash;
1313
use ttf_parser::{name_id, Face, GlyphId, PlatformId, Tag};
1414
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
15-
use usvg::{Fill, PaintOrder, Stroke, Transform, Visibility};
15+
use usvg::{Fill, Group, ImageKind, Node, PaintOrder, Stroke, Transform, Visibility};
1616

1717
const CFF: Tag = Tag::from_bytes(b"CFF ");
1818
const CFF2: Tag = Tag::from_bytes(b"CFF2");
@@ -473,3 +473,63 @@ where
473473
Some((key, head))
474474
}
475475
}
476+
477+
#[derive(Clone)]
478+
pub struct Font {
479+
pub glyph_set: BTreeMap<u16, String>,
480+
pub reference: Ref,
481+
pub face_data: Vec<u8>,
482+
pub units_per_em: u16,
483+
pub face_index: u32,
484+
}
485+
486+
pub fn fill_fonts(group: &Group, ctx: &mut Context, fontdb: &fontdb::Database) {
487+
for child in group.children() {
488+
match child {
489+
Node::Text(t) => {
490+
let allocator = &mut ctx.ref_allocator;
491+
for span in t.layouted() {
492+
for g in &span.positioned_glyphs {
493+
let font = ctx.fonts.entry(g.font).or_insert_with(|| {
494+
fontdb
495+
.with_face_data(g.font, |data, face_index| {
496+
// TODO: Currently, we are parsing each font twice, once here
497+
// and once again when writing the fonts. We should probably
498+
// improve on that...
499+
if let Ok(ttf) =
500+
ttf_parser::Face::parse(data, face_index)
501+
{
502+
let reference = allocator.alloc_ref();
503+
let glyph_set = BTreeMap::new();
504+
return Some(Font {
505+
reference,
506+
face_data: Vec::from(data),
507+
units_per_em: ttf.units_per_em(),
508+
glyph_set,
509+
face_index,
510+
});
511+
}
512+
513+
None
514+
})
515+
.flatten()
516+
});
517+
518+
if let Some(ref mut font) = font {
519+
font.glyph_set.insert(g.glyph_id.0, g.text.clone());
520+
}
521+
}
522+
}
523+
}
524+
Node::Group(group) => fill_fonts(group, ctx, fontdb),
525+
Node::Image(image) => {
526+
if let ImageKind::SVG(svg) = image.kind() {
527+
fill_fonts(svg.root(), ctx, fontdb);
528+
}
529+
}
530+
_ => {}
531+
}
532+
533+
child.subroots(|subroot| fill_fonts(subroot, ctx, fontdb));
534+
}
535+
}

0 commit comments

Comments
 (0)