Skip to content

Commit dfd0309

Browse files
authored
Readd option to configure DPI (#69)
1 parent f65ab64 commit dfd0309

11 files changed

Lines changed: 113 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@ 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.
10+
- Made the CLI more flexible in terms of which features you want to include.
11+
- Added `raster-scale` and `text-to-paths` as arguments for the CLI.
12+
- Removed the option to configure the view box from the API. This might be readded in
13+
a later update.
1114
- The `convert_str` method has been removed. You should now always convert your SVG string into a `usvg`
1215
tree yourself and then call either `to_pdf` or `to_chunk`.
1316
- The `convert_tree` method has been renamed into `to_pdf`, and now requires you to provide the fontdb
1417
used for the `usvg` tree, unless you have disabled the `text` feature.
1518
- `convert_tree_into` has been renamed into `to_chunk` and now returns an independent chunk as well
1619
as the object ID of the actual SVG in the chunk.
1720

18-
- TODO: The CLI options have been (temporarily) removed. They will be readded before the next release.
19-
- TODO: Add tests for CLI and svg options
20-
- TODO: Add CLI option to convert text to paths.
21-
2221
### Changed
2322
- Bumped resvg to v0.40.
2423
- `convert_str` now requires a `fontdb` as an argument as well.

Cargo.toml

Lines changed: 2 additions & 2 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 = "0.41"
28+
usvg = { version = "0.41", default-features = false }
2929
tiny-skia = "0.11.4"
3030
unicode-properties = "0.1.1"
31-
resvg = "0.41"
31+
resvg = { version = "0.41", default-features = false }
3232
subsetter = "0.1.1"
3333
ttf-parser = { version = "0.20.0" }
3434
siphasher = { version = "1.0.1"}

cli/src/args.rs

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ pub struct CliArguments {
1919
/// The number of SVG pixels per PDF points.
2020
#[clap(long, default_value = "72.0")]
2121
pub dpi: f32,
22+
/// Whether text should be converted to paths
23+
/// before embedding it into the PDF.
24+
#[clap(long, short, action=ArgAction::SetTrue)]
25+
pub text_to_paths: bool,
26+
/// How much raster images of rasterized effects should be scaled up.
27+
#[clap(long, default_value = "1.5")]
28+
pub raster_scale: f32,
2229
}
2330

2431
// What to do.
@@ -29,21 +36,6 @@ pub enum Command {
2936
Fonts(FontsCommand),
3037
}
3138

32-
/// Lists all discovered fonts in system.
33-
#[derive(Debug, Clone, Parser)]
34-
pub struct ConvertCommand {
35-
/// Path to read SVG file from.
36-
pub input: PathBuf,
37-
/// Path to write PDF file to.
38-
pub output: Option<PathBuf>,
39-
/// The number of SVG pixels per PDF points.
40-
#[clap(long, default_value = "72.0")]
41-
pub dpi: f32,
42-
// How much rasterized effects should be scaled up.
43-
#[clap(long, default_value = "1.0")]
44-
pub raster_scale: f32,
45-
}
46-
4739
/// Lists all discovered fonts in system.
4840
#[derive(Debug, Clone, Parser)]
4941
pub struct FontsCommand {

cli/src/convert.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
use crate::args::ConvertCommand;
21
use std::path::{Path, PathBuf};
3-
use svg2pdf::Options;
4-
5-
/// Execute a font listing command.
6-
pub fn _convert(command: ConvertCommand) -> Result<(), String> {
7-
convert_(&command.input, command.output)
8-
}
9-
10-
pub fn convert_(input: &PathBuf, output: Option<PathBuf>) -> Result<(), String> {
2+
use svg2pdf::{ConversionOptions, PageOptions};
3+
4+
pub fn convert_(
5+
input: &PathBuf,
6+
output: Option<PathBuf>,
7+
conversion_options: ConversionOptions,
8+
page_options: PageOptions,
9+
) -> Result<(), String> {
1110
if let Ok(()) = log::set_logger(&LOGGER) {
1211
log::set_max_level(log::LevelFilter::Warn);
1312
}
@@ -40,7 +39,8 @@ pub fn convert_(input: &PathBuf, output: Option<PathBuf>) -> Result<(), String>
4039

4140
let pdf = svg2pdf::to_pdf(
4241
&tree,
43-
Options::default(),
42+
conversion_options,
43+
page_options,
4444
#[cfg(feature = "text")]
4545
&fontdb,
4646
);

cli/src/main.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88
io::{self, Write},
99
process,
1010
};
11+
use svg2pdf::{ConversionOptions, PageOptions};
1112
use termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor};
1213

1314
fn main() {
@@ -22,7 +23,15 @@ fn run() -> Result<(), String> {
2223

2324
// If an input argument was provided, convert the svg file to pdf.
2425
if let Some(input) = args.input {
25-
return convert::convert_(&input, args.output);
26+
let conversion_options = ConversionOptions {
27+
compress: true,
28+
embed_text: !args.text_to_paths,
29+
raster_scale: args.raster_scale,
30+
};
31+
32+
let page_options = PageOptions { dpi: args.dpi };
33+
34+
return convert::convert_(&input, args.output, conversion_options, page_options);
2635
};
2736

2837
// Otherwise execute the command provided if any.

src/lib.rs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This example reads an SVG file and writes the corresponding PDF back to the disk
1515
```
1616
# fn main() -> Result<(), Box<dyn std::error::Error>> {
1717
use svg2pdf::usvg::fontdb;
18-
use svg2pdf::Options;
18+
use svg2pdf::{ConversionOptions, PageOptions};
1919
2020
let input = "tests/svg/custom/integration/matplotlib/stairs.svg";
2121
let output = "target/stairs.pdf";
@@ -26,7 +26,7 @@ let mut db = fontdb::Database::new();
2626
db.load_system_fonts();
2727
let tree = svg2pdf::usvg::Tree::from_str(&svg, &options, &db)?;
2828
29-
let pdf = svg2pdf::to_pdf(&tree, Options::default(), &db);
29+
let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default(), &db);
3030
std::fs::write(output, pdf)?;
3131
# Ok(()) }
3232
```
@@ -59,14 +59,14 @@ mod util;
5959
pub use usvg;
6060

6161
use once_cell::sync::Lazy;
62-
use pdf_writer::{Chunk, Content, Filter, Finish, Pdf, Rect, Ref, TextStr};
62+
use pdf_writer::{Chunk, Content, Filter, Finish, Pdf, Ref, TextStr};
6363
#[cfg(feature = "text")]
6464
use usvg::fontdb;
65-
use usvg::Tree;
65+
use usvg::{Size, Transform, Tree};
6666

6767
use crate::render::{tree_to_stream, tree_to_xobject};
6868
use crate::util::context::Context;
69-
use crate::util::helper::deflate;
69+
use crate::util::helper::{deflate, RectExt, TransformExt};
7070
use crate::util::resources::ResourceContainer;
7171

7272
// The ICC profiles.
@@ -75,9 +75,24 @@ static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> =
7575
static GRAY_ICC_DEFLATED: Lazy<Vec<u8>> =
7676
Lazy::new(|| deflate(include_bytes!("icc/sGrey-v4.icc")));
7777

78-
/// Preferences for the PDF conversion.
78+
/// Options for the resulting PDF file.
7979
#[derive(Copy, Clone)]
80-
pub struct Options {
80+
pub struct PageOptions {
81+
/// The DPI that should be assumed for the conversion to PDF.
82+
///
83+
/// _Default:_ 72.0
84+
pub dpi: f32,
85+
}
86+
87+
impl Default for PageOptions {
88+
fn default() -> Self {
89+
Self { dpi: 72.0 }
90+
}
91+
}
92+
93+
/// Options for the PDF conversion.
94+
#[derive(Copy, Clone)]
95+
pub struct ConversionOptions {
8196
/// Whether the content streams should be compressed.
8297
///
8398
/// The smaller PDFs generated by this are generally more practical, but it
@@ -102,7 +117,7 @@ pub struct Options {
102117
pub embed_text: bool,
103118
}
104119

105-
impl Default for Options {
120+
impl Default for ConversionOptions {
106121
fn default() -> Self {
107122
Self {
108123
compress: false,
@@ -124,7 +139,7 @@ impl Default for Options {
124139
/// ```
125140
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
126141
/// use svg2pdf::usvg::fontdb;
127-
/// use svg2pdf::Options;
142+
/// use svg2pdf::{ConversionOptions, PageOptions};
128143
///
129144
/// let input = "tests/svg/custom/integration/matplotlib/stairs.svg";
130145
/// let output = "target/stairs.pdf";
@@ -136,24 +151,31 @@ impl Default for Options {
136151
/// let mut tree = svg2pdf::usvg::Tree::from_str(&svg, &options, &db)?;
137152
///
138153
///
139-
/// let pdf = svg2pdf::to_pdf(&tree, Options::default(), &db);
154+
/// let pdf = svg2pdf::to_pdf(&tree, ConversionOptions::default(), PageOptions::default(), &db);
140155
/// std::fs::write(output, pdf)?;
141156
/// # Ok(()) }
142157
/// ```
143158
pub fn to_pdf(
144159
tree: &Tree,
145-
options: Options,
160+
conversion_options: ConversionOptions,
161+
page_options: PageOptions,
146162
#[cfg(feature = "text")] fontdb: &fontdb::Database,
147163
) -> Vec<u8> {
148164
let mut ctx = Context::new(
149165
#[cfg(feature = "text")]
150166
tree,
151-
options,
167+
conversion_options,
152168
#[cfg(feature = "text")]
153169
fontdb,
154170
);
155171
let mut pdf = Pdf::new();
156172

173+
let dpi_ratio = 72.0 / page_options.dpi;
174+
let dpi_transform = Transform::from_scale(dpi_ratio, dpi_ratio);
175+
let page_size =
176+
Size::from_wh(tree.size().width() * dpi_ratio, tree.size().height() * dpi_ratio)
177+
.unwrap();
178+
157179
let catalog_ref = ctx.alloc_ref();
158180
let page_tree_ref = ctx.alloc_ref();
159181
let page_ref = ctx.alloc_ref();
@@ -165,7 +187,10 @@ pub fn to_pdf(
165187
// Generate main content
166188
let mut rc = ResourceContainer::new();
167189
let mut content = Content::new();
190+
content.save_state();
191+
content.transform(dpi_transform.to_pdf_transform());
168192
tree_to_stream(tree, &mut pdf, &mut content, &mut ctx, &mut rc);
193+
content.restore_state();
169194
let content_stream = ctx.finish_content(content);
170195
let mut stream = pdf.stream(content_ref, &content_stream);
171196

@@ -179,7 +204,7 @@ pub fn to_pdf(
179204
rc.finish(&mut page_resources);
180205
page_resources.finish();
181206

182-
page.media_box(Rect::new(0.0, 0.0, tree.size().width(), tree.size().height()));
207+
page.media_box(page_size.to_non_zero_rect(0.0, 0.0).to_pdf_rect());
183208
page.parent(page_tree_ref);
184209
page.group()
185210
.transparency()
@@ -240,7 +265,7 @@ pub fn to_pdf(
240265
/// let mut db = fontdb::Database::new();
241266
/// db.load_system_fonts();
242267
/// let tree = svg2pdf::usvg::Tree::from_str(&svg, &svg2pdf::usvg::Options::default(), &db)?;
243-
/// let (mut svg_chunk, svg_id) = svg2pdf::to_chunk(&tree, svg2pdf::Options::default(), &db);
268+
/// let (mut svg_chunk, svg_id) = svg2pdf::to_chunk(&tree, svg2pdf::ConversionOptions::default(), &db);
244269
///
245270
/// // Renumber the chunk so that we can embed it into our existing workflow, and also make sure
246271
/// // to update `svg_id`.
@@ -297,15 +322,15 @@ pub fn to_pdf(
297322
/// ```
298323
pub fn to_chunk(
299324
tree: &Tree,
300-
options: Options,
325+
conversion_options: ConversionOptions,
301326
#[cfg(feature = "text")] fontdb: &fontdb::Database,
302327
) -> (Chunk, Ref) {
303328
let mut chunk = Chunk::new();
304329

305330
let mut ctx = Context::new(
306331
#[cfg(feature = "text")]
307332
tree,
308-
options,
333+
conversion_options,
309334
#[cfg(feature = "text")]
310335
fontdb,
311336
);

src/util/context.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ use {
1212

1313
use super::helper::deflate;
1414
use crate::util::allocate::RefAllocator;
15-
use crate::{Options, GRAY_ICC_DEFLATED, SRGB_ICC_DEFLATED};
15+
use crate::{ConversionOptions, GRAY_ICC_DEFLATED, SRGB_ICC_DEFLATED};
1616

1717
/// Holds all of the necessary information for the conversion process.
1818
pub struct Context {
1919
/// Options that where passed by the user.
20-
pub options: Options,
20+
pub options: ConversionOptions,
2121
/// The refs of the fonts
2222
#[cfg(feature = "text")]
2323
pub fonts: HashMap<ID, Option<Font>>,
@@ -29,7 +29,11 @@ pub struct Context {
2929
impl Context {
3030
/// Create a new context.
3131
#[cfg(feature = "text")]
32-
pub fn new(tree: &Tree, options: Options, fontdb: &fontdb::Database) -> Self {
32+
pub fn new(
33+
tree: &Tree,
34+
options: ConversionOptions,
35+
fontdb: &fontdb::Database,
36+
) -> Self {
3337
let mut ctx = Self {
3438
ref_allocator: RefAllocator::new(),
3539
options,
@@ -48,7 +52,7 @@ impl Context {
4852
// TODO: Make context less ugly with different features.
4953
/// Create a new context.
5054
#[cfg(not(feature = "text"))]
51-
pub fn new(options: Options) -> Self {
55+
pub fn new(options: ConversionOptions) -> Self {
5256
Self {
5357
ref_allocator: RefAllocator::new(),
5458
options,
@@ -109,7 +113,7 @@ impl Context {
109113
}
110114

111115
/// Just a helper method so that we don't have to manually compress the content if this was
112-
/// set in the [Options] struct.
116+
/// set in the [ConversionOptions] struct.
113117
pub fn finish_content(&self, content: Content) -> Vec<u8> {
114118
if self.options.compress {
115119
deflate(&content.finish())

tests/ref/api/dpi.png

972 Bytes
Loading

tests/src/api.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,33 @@ use {
55
pdf_writer::{Content, Finish, Name, Pdf, Rect, Ref, Str},
66
std::collections::HashMap,
77
std::path::Path,
8-
svg2pdf::Options,
8+
svg2pdf::ConversionOptions,
9+
svg2pdf::PageOptions,
910
};
1011

1112
#[test]
1213
fn text_to_paths() {
13-
let options = Options { embed_text: false, ..Options::default() };
14+
let options = ConversionOptions { embed_text: false, ..ConversionOptions::default() };
1415

1516
let svg_path = "svg/resvg/text/text/simple-case.svg";
16-
let (pdf, actual_image) = convert_svg(Path::new(svg_path), options);
17+
let (pdf, actual_image) =
18+
convert_svg(Path::new(svg_path), options, PageOptions::default());
1719
let res = run_test_impl(pdf, actual_image, "api/text_to_paths");
1820
assert_eq!(res, 0);
1921
}
2022

23+
#[test]
24+
fn dpi() {
25+
let conversion_options = ConversionOptions::default();
26+
let page_options = PageOptions { dpi: 140.0 };
27+
28+
let svg_path = "svg/resvg/text/text/simple-case.svg";
29+
let (pdf, actual_image) =
30+
convert_svg(Path::new(svg_path), conversion_options, page_options);
31+
let res = run_test_impl(pdf, actual_image, "api/dpi");
32+
assert_eq!(res, 0);
33+
}
34+
2135
#[test]
2236
fn to_chunk() {
2337
let mut alloc = Ref::new(1);
@@ -36,7 +50,8 @@ fn to_chunk() {
3650
let tree =
3751
svg2pdf::usvg::Tree::from_str(&svg, &svg2pdf::usvg::Options::default(), &db)
3852
.unwrap();
39-
let (svg_chunk, svg_id) = svg2pdf::to_chunk(&tree, svg2pdf::Options::default(), &db);
53+
let (svg_chunk, svg_id) =
54+
svg2pdf::to_chunk(&tree, svg2pdf::ConversionOptions::default(), &db);
4055

4156
let mut map = HashMap::new();
4257
let svg_chunk =

0 commit comments

Comments
 (0)