Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support all resvg options #12

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ repos:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
exclude: \.changes/.*\.md
exclude: server/.*
- id: trailing-whitespace
exclude: server/.*

- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.22.0
Expand All @@ -28,12 +29,14 @@ repos:
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
exclude: server/.*\.py

- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
language_version: python3.10
exclude: server/.*\.py

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.1.1
Expand All @@ -43,6 +46,7 @@ repos:
- types-docutils
- types-requests
- typing-extensions
exclude: server/.*\.py

- repo: https://github.com/pre-commit/pre-commit
rev: v3.2.2
Expand Down
11 changes: 10 additions & 1 deletion resvg_py.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ import os

from numpy import ndarray

class ShapeRendering:
OptimizeSpeed: ShapeRendering
CrispEdges: ShapeRendering
GeometricPrecision: ShapeRendering

class SVGOptions:
def __init__(
self: SVGOptions,
*,
dpi: float = 96.0,
font_family: str = "Times New Roman",
font_size: float = 12.0,
languages: list[str] | None = None,
shape_rendering: ShapeRendering,
resources_dir: os.PathLike | None = None,
default_width: float = 100.0,
default_height: float = 100.0,
resources_dir: os.PathLike | None = None,
) -> None: ...

class Resvg:
Expand Down
66 changes: 12 additions & 54 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,11 @@
mod options;

use pyo3::prelude::*;
use pyo3::types::PyBytes;
use resvg::tiny_skia::Pixmap;
use usvg::{Size, Tree, TreeParsing};

/// SVG parsing and rendering options.
///
/// TODO(edgarmondragon): Add more options.
#[derive(Clone)]
#[pyclass]
pub struct SVGOptions {
/// Target DPI.
///
/// Impacts units conversion.
///
/// Default: 96.0
dpi: f64,

/// Directory that will be used during relative paths resolving.
///
/// Expected to be the same as the directory that contains the SVG file,
/// but can be set to any.
///
/// Default: `None
resources_dir: Option<std::path::PathBuf>,

/// Default viewport width to assume if there is no `viewBox` attribute and
/// the `width` is relative.
///
/// Default: 100.0
default_width: f64,

/// Default viewport height to assume if there is no `viewBox` attribute and
/// the `height` is relative.
///
/// Default: 100.0
default_height: f64,
}

#[pymethods]
impl SVGOptions {
#[new]
#[pyo3(signature = (*, dpi = 96.0, default_width = 100.0, default_height = 100.0, resources_dir = None))]
fn new(
dpi: f64,
default_width: f64,
default_height: f64,
resources_dir: Option<std::path::PathBuf>,
) -> Self {
Self {
dpi,
default_width,
default_height,
resources_dir,
}
}
}
use crate::options::{SVGOptions, ShapeRendering};

/// A Python class for rendering SVGs.
#[pyclass]
Expand Down Expand Up @@ -86,9 +37,15 @@ impl Resvg {
let options = if let Some(options) = &self.options {
usvg::Options {
dpi: options.dpi,
default_size: Size::new(options.default_width, options.default_height).unwrap(),
font_family: options.font_family.clone(),
font_size: options.font_size,
languages: options.languages.clone().unwrap_or_default(),
shape_rendering: options.shape_rendering.clone().into(),
text_rendering: options.text_rendering.clone().into(),
image_rendering: options.image_rendering.clone().into(),
resources_dir: options.resources_dir.clone(),
..usvg::Options::default()
default_size: Size::new(options.default_width, options.default_height).unwrap(),
image_href_resolver: usvg::ImageHrefResolver::default(),
}
} else {
usvg::Options::default()
Expand Down Expand Up @@ -140,6 +97,7 @@ impl RenderedImage {
/// Python bindings for resvg.
#[pymodule]
fn resvg_py(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<ShapeRendering>()?;
m.add_class::<SVGOptions>()?;
m.add_class::<RenderedImage>()?;
m.add_class::<Resvg>()?;
Expand Down
186 changes: 186 additions & 0 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use pyo3::prelude::*;

/// A shape rendering method.
///
/// `shape-rendering` attribute in the SVG.
#[derive(Clone)]
#[pyclass]
pub enum ShapeRendering {
OptimizeSpeed,
CrispEdges,
GeometricPrecision,
}

impl From<ShapeRendering> for usvg::ShapeRendering {
fn from(shape_rendering: ShapeRendering) -> Self {
match shape_rendering {
ShapeRendering::OptimizeSpeed => Self::OptimizeSpeed,
ShapeRendering::CrispEdges => Self::CrispEdges,
ShapeRendering::GeometricPrecision => Self::GeometricPrecision,
}
}
}

/// Specifies the default text rendering method.
///
/// Will be used when an SVG element's `text-rendering` property is set to `auto`.
///
/// Default: OptimizeLegibility
#[derive(Clone)]
#[pyclass]
pub enum TextRendering {
OptimizeSpeed,
OptimizeLegibility,
GeometricPrecision,
}

impl From<TextRendering> for usvg::TextRendering {
fn from(text_rendering: TextRendering) -> Self {
match text_rendering {
TextRendering::OptimizeSpeed => Self::OptimizeSpeed,
TextRendering::OptimizeLegibility => Self::OptimizeLegibility,
TextRendering::GeometricPrecision => Self::GeometricPrecision,
}
}
}

/// An image rendering method.
///
/// `image-rendering` attribute in the SVG.
#[derive(Clone)]
#[pyclass]
pub enum ImageRendering {
OptimizeQuality,
OptimizeSpeed,
}

impl From<ImageRendering> for usvg::ImageRendering {
fn from(image_rendering: ImageRendering) -> Self {
match image_rendering {
ImageRendering::OptimizeQuality => Self::OptimizeQuality,
ImageRendering::OptimizeSpeed => Self::OptimizeSpeed,
}
}
}

/// SVG parsing and rendering options.
///
/// TODO(edgarmondragon): Add more options.
#[derive(Clone)]
#[pyclass]
pub struct SVGOptions {
/// Target DPI.
///
/// Impacts units conversion.
///
/// Default: 96.0
pub dpi: f64,

/// A default font family.
///
/// Will be used when no `font-family` attribute is set in the SVG.
///
/// Default: Times New Roman
pub font_family: String,

/// A default font size.
///
/// Will be used when no `font-size` attribute is set in the SVG.
///
/// Default: 12
pub font_size: f64,

/// Languages to use when resolving `systemLanguage` conditional attributes.
///
/// Format: en, en-US.
///
/// Default: `[en]`
pub languages: Option<Vec<String>>,

/// Specifies the default shape rendering method.
///
/// Will be used when an SVG element's `shape-rendering` property is set to `auto`.
///
/// Default: GeometricPrecision
pub shape_rendering: ShapeRendering,

/// Specifies the default text rendering method.
///
/// Will be used when an SVG element's `text-rendering` property is set to `auto`.
///
/// Default: OptimizeLegibility
pub text_rendering: TextRendering,

/// Specifies the default image rendering method.
///
/// Will be used when an SVG element's `image-rendering` property is set to `auto`.
///
/// Default: OptimizeQuality
pub image_rendering: ImageRendering,

/// Directory that will be used during relative paths resolving.
///
/// Expected to be the same as the directory that contains the SVG file,
/// but can be set to any.
///
/// Default: `None
pub resources_dir: Option<std::path::PathBuf>,

/// Default viewport width to assume if there is no `viewBox` attribute and
/// the `width` is relative.
///
/// Default: 100.0
pub default_width: f64,

/// Default viewport height to assume if there is no `viewBox` attribute and
/// the `height` is relative.
///
/// Default: 100.0
pub default_height: f64,
}


#[pymethods]
impl SVGOptions {
#[new]
#[pyo3(
signature = (
*,
dpi = 96.0,
font_family = "Times New Roman".to_string(),
font_size = 12.0,
languages = None,
shape_rendering = ShapeRendering::GeometricPrecision,
text_rendering = TextRendering::OptimizeLegibility,
image_rendering = ImageRendering::OptimizeQuality,
resources_dir = None,
default_width = 100.0,
default_height = 100.0,
)
)]
fn new(
dpi: f64,
font_family: String,
font_size: f64,
languages: Option<Vec<String>>,
shape_rendering: ShapeRendering,
text_rendering: TextRendering,
image_rendering: ImageRendering,
resources_dir: Option<std::path::PathBuf>,
default_width: f64,
default_height: f64,
) -> Self {
Self {
dpi,
font_family,
font_size,
languages,
shape_rendering,
text_rendering,
image_rendering,
resources_dir,
default_width,
default_height,
}
}
}
17 changes: 13 additions & 4 deletions tests/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@


@pytest.mark.parametrize(
"svg_file",
("svg_file", "options"),
[
pytest.param("resources/examples/svg/octocat.svg", id="octocat"),
pytest.param(
"resources/examples/svg/octocat.svg",
resvg_py.SVGOptions(
shape_rendering=resvg_py.ShapeRendering.GeometricPrecision,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jboarman Do you have handy examples of input/output pairs that might be worth testing here?

There's a bunch of examples in the upstream Rust repo, but it's probably overkill to check all of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until we implement the benchmark in issue #10, I think we can make use of a few random selections from the upstream repo that you identified in your comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I'll try to port a few of the test files to validate the options.

),
id="octocat",
),
],
)
def test_render(snapshot: Snapshot, svg_file: str) -> None:
options = resvg_py.SVGOptions()
def test_render(
snapshot: Snapshot,
svg_file: str,
options: resvg_py.SVGOptions,
) -> None:
r = resvg_py.Resvg(options)

with Path(svg_file).open("r") as input_file:
Expand Down