diff --git a/.github/copyright.sh b/.github/copyright.sh index 383eb2b..32e5322 100755 --- a/.github/copyright.sh +++ b/.github/copyright.sh @@ -7,7 +7,7 @@ # -g "!src/special_directory" # Check all the standard Rust source files -output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Vello Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" -g "!src/geom.rs" .) +output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Vello Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" .) if [ -n "$output" ]; then echo -e "The following files lack the correct copyright header:\n" diff --git a/CHANGELOG.md b/CHANGELOG.md index eca8401..954faa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,19 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe ## Unreleased -### changed +### Changed - Updated `usvg` to 0.41 -### fixed +### Fixed - The image viewBox is now properly translated +- `vello_svg::render_tree_with` no longer takes a transform parameter. This is to make it consistent with the documentation and `vello_svg::render_tree`. + +### Removed + +- MPL 2.0 is no longer a license requirement +- The root image viewBox clipping was removed, to be added back at a later time ## 0.1 (2024-03-11) diff --git a/Cargo.toml b/Cargo.toml index ce781a7..d537aec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ description = "An SVG integration for vello." categories = ["rendering", "graphics"] keywords = ["2d", "vector-graphics", "vello", "svg"] version.workspace = true -license = "(Apache-2.0 OR MIT) AND MPL-2.0" +license.workspace = true edition.workspace = true repository.workspace = true diff --git a/LICENSE-MPL b/LICENSE-MPL deleted file mode 100644 index f4bbcd2..0000000 --- a/LICENSE-MPL +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/README.md b/README.md index 8bc4103..efa1adf 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Linebender Zulip](https://img.shields.io/badge/Linebender-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) [![dependency status](https://deps.rs/repo/github/linebender/vello_svg/status.svg)](https://deps.rs/repo/github/linebender/vello_svg) -[![(MIT/Apache 2.0)+MPL 2.0](https://img.shields.io/badge/license-(MIT%2FApache)+MPL2-blue.svg)](#license) +[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#license) [![vello version](https://img.shields.io/badge/vello-v0.1.0-purple.svg)](https://crates.io/crates/vello) [![Crates.io](https://img.shields.io/crates/v/vello_svg.svg)](https://crates.io/crates/vello_svg) @@ -67,10 +67,7 @@ Licensed under either of - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) -at your option, in addition to - -- Mozilla Public License 2.0 - ([LICENSE-MPL](LICENSE-MPL) or ). +at your option The files in subdirectories of the [`examples/assets`](/examples/assets) directory are licensed solely under their respective licenses, available in the `LICENSE` file in their directories. diff --git a/src/geom.rs b/src/geom.rs deleted file mode 100644 index 20f53be..0000000 --- a/src/geom.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018 Yevhenii Reizner -// SPDX-License-Identifier: MPL-2.0 - -// copied from https://github.com/RazrFalcon/resvg/blob/4d27b8c3be3ecfff3256c9be1b8362eb22533659/crates/resvg/src/geom.rs - -/// Converts `viewBox` to `Transform` with an optional clip rectangle. -/// -/// Unlike `view_box_to_transform`, returns an optional clip rectangle -/// that should be applied before rendering the image. -pub fn view_box_to_transform_with_clip( - view_box: &usvg::ViewBox, - img_size: usvg::tiny_skia_path::IntSize, -) -> (usvg::Transform, Option) { - let r = view_box.rect; - - let new_size = fit_view_box(img_size.to_size(), view_box); - - let (tx, ty, clip) = if view_box.aspect.slice { - let (dx, dy) = usvg::utils::aligned_pos( - view_box.aspect.align, - 0.0, - 0.0, - new_size.width() - r.width(), - new_size.height() - r.height(), - ); - - (r.x() - dx, r.y() - dy, Some(r)) - } else { - let (dx, dy) = usvg::utils::aligned_pos( - view_box.aspect.align, - r.x(), - r.y(), - r.width() - new_size.width(), - r.height() - new_size.height(), - ); - - (dx, dy, None) - }; - - let sx = new_size.width() / img_size.width() as f32; - let sy = new_size.height() / img_size.height() as f32; - let ts = usvg::Transform::from_row(sx, 0.0, 0.0, sy, tx, ty); - - (ts, clip) -} - -/// Fits size into a viewbox. -pub fn fit_view_box(size: usvg::Size, vb: &usvg::ViewBox) -> usvg::Size { - let s = vb.rect.size(); - - if vb.aspect.align == usvg::Align::None { - s - } else if vb.aspect.slice { - size.expand_to(s) - } else { - size.scale_to(s) - } -} diff --git a/src/lib.rs b/src/lib.rs index d2f2e51..ade9acf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,12 +32,10 @@ //! - path shape-rendering //! - patterns -mod geom; +mod util; use std::convert::Infallible; -use std::sync::Arc; -use vello::kurbo::{Affine, BezPath, Point, Rect, Stroke}; -use vello::peniko::{BlendMode, Blob, Brush, Color, Fill, Image}; +use vello::peniko::{BlendMode, Fill}; use vello::Scene; /// Re-export vello. @@ -53,13 +51,8 @@ pub use usvg; /// /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features pub fn render_tree(scene: &mut Scene, svg: &usvg::Tree) { - render_tree_with::<_, Infallible>( - scene, - svg, - &usvg::Transform::identity(), - &mut default_error_handler, - ) - .unwrap_or_else(|e| match e {}); + render_tree_with::<_, Infallible>(scene, svg, &mut util::default_error_handler) + .unwrap_or_else(|e| match e {}); } /// Append a [`usvg::Tree`] into a Vello [`Scene`]. @@ -71,21 +64,27 @@ pub fn render_tree(scene: &mut Scene, svg: &usvg::Tree) { pub fn render_tree_with Result<(), E>, E>( scene: &mut Scene, svg: &usvg::Tree, - ts: &usvg::Transform, error_handler: &mut F, ) -> Result<(), E> { - render_tree_impl(scene, svg, &svg.view_box(), ts, error_handler) + render_tree_impl( + scene, + svg, + &svg.view_box(), + &usvg::Transform::identity(), + error_handler, + ) } +// A helper function to render a tree with a given transform. fn render_tree_impl Result<(), E>, E>( scene: &mut Scene, - svg: &usvg::Tree, + tree: &usvg::Tree, view_box: &usvg::ViewBox, ts: &usvg::Transform, error_handler: &mut F, ) -> Result<(), E> { - let ts = &ts.pre_concat(view_box.to_transform(svg.size())); - let transform = to_affine(ts); + let ts = &ts.pre_concat(view_box.to_transform(tree.size())); + let transform = util::to_affine(ts); scene.push_layer( BlendMode { mix: vello::peniko::Mix::Clip, @@ -100,35 +99,12 @@ fn render_tree_impl Result<(), E>, E>( view_box.rect.bottom().into(), ), ); - let (view_box_transform, clip) = - geom::view_box_to_transform_with_clip(view_box, svg.size().to_int_size()); - let view_box_transform = view_box_transform.pre_concat(view_box.to_transform(svg.size())); - if let Some(clip) = clip { - scene.push_layer( - BlendMode { - mix: vello::peniko::Mix::Clip, - compose: vello::peniko::Compose::SrcOver, - }, - 1.0, - transform, - &vello::kurbo::Rect::new( - clip.left().into(), - clip.top().into(), - clip.right().into(), - clip.bottom().into(), - ), - ); - } render_group( scene, - svg.root(), - &ts.pre_concat(view_box_transform) - .pre_concat(svg.root().transform()), + tree.root(), + &ts.pre_concat(tree.root().transform()), error_handler, )?; - if clip.is_some() { - scene.pop_layer(); - } scene.pop_layer(); Ok(()) @@ -141,14 +117,14 @@ fn render_group Result<(), E>, E>( error_handler: &mut F, ) -> Result<(), E> { for node in group.children() { - let transform = to_affine(ts); + let transform = util::to_affine(ts); match node { usvg::Node::Group(g) => { let mut pushed_clip = false; if let Some(clip_path) = g.clip_path() { if let Some(usvg::Node::Path(clip_path)) = clip_path.root().children().first() { // support clip-path with a single path - let local_path = to_bez_path(clip_path); + let local_path = util::to_bez_path(clip_path); scene.push_layer( BlendMode { mix: vello::peniko::Mix::Clip, @@ -172,12 +148,12 @@ fn render_group Result<(), E>, E>( if path.visibility() != usvg::Visibility::Visible { continue; } - let local_path = to_bez_path(path); + let local_path = util::to_bez_path(path); let do_fill = |scene: &mut Scene, error_handler: &mut F| { if let Some(fill) = &path.fill() { if let Some((brush, brush_transform)) = - paint_to_brush(fill.paint(), fill.opacity()) + util::to_brush(fill.paint(), fill.opacity()) { scene.fill( match fill.rule() { @@ -198,28 +174,9 @@ fn render_group Result<(), E>, E>( let do_stroke = |scene: &mut Scene, error_handler: &mut F| { if let Some(stroke) = &path.stroke() { if let Some((brush, brush_transform)) = - paint_to_brush(stroke.paint(), stroke.opacity()) + util::to_brush(stroke.paint(), stroke.opacity()) { - let mut conv_stroke = Stroke::new(stroke.width().get() as f64) - .with_caps(match stroke.linecap() { - usvg::LineCap::Butt => vello::kurbo::Cap::Butt, - usvg::LineCap::Round => vello::kurbo::Cap::Round, - usvg::LineCap::Square => vello::kurbo::Cap::Square, - }) - .with_join(match stroke.linejoin() { - usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => { - vello::kurbo::Join::Miter - } - usvg::LineJoin::Round => vello::kurbo::Join::Round, - usvg::LineJoin::Bevel => vello::kurbo::Join::Bevel, - }) - .with_miter_limit(stroke.miterlimit().get() as f64); - if let Some(dash_array) = stroke.dasharray().as_ref() { - conv_stroke = conv_stroke.with_dashes( - stroke.dashoffset() as f64, - dash_array.iter().map(|x| *x as f64), - ); - } + let conv_stroke = util::to_stroke(stroke); scene.stroke( &conv_stroke, transform, @@ -252,19 +209,19 @@ fn render_group Result<(), E>, E>( usvg::ImageKind::JPEG(_) | usvg::ImageKind::PNG(_) | usvg::ImageKind::GIF(_) => { - let Ok(decoded_image) = decode_raw_raster_image(img.kind()) else { + let Ok(decoded_image) = util::decode_raw_raster_image(img.kind()) else { error_handler(scene, node)?; continue; }; - let Some(size) = usvg::Size::from_wh( - decoded_image.width() as f32, - decoded_image.height() as f32, - ) else { + let image = util::into_image(decoded_image); + let Some(size) = + usvg::Size::from_wh(image.width as f32, image.height as f32) + else { error_handler(scene, node)?; continue; }; let view_box = img.view_box(); - let new_size = geom::fit_view_box(size, &view_box); + let new_size = view_box.rect.size(); let (tx, ty) = usvg::utils::aligned_pos( view_box.aspect.align, view_box.rect.x(), @@ -278,7 +235,6 @@ fn render_group Result<(), E>, E>( ); let view_box_transform = usvg::Transform::from_row(sx, 0.0, 0.0, sy, tx, ty); - let (width, height) = (decoded_image.width(), decoded_image.height()); scene.push_layer( BlendMode { mix: vello::peniko::Mix::Clip, @@ -293,18 +249,8 @@ fn render_group Result<(), E>, E>( view_box.rect.bottom().into(), ), ); - - let image_ts = to_affine(&ts.pre_concat(view_box_transform)); - let image_data: Arc> = decoded_image.into_vec().into(); - scene.draw_image( - &Image::new( - Blob::new(image_data), - vello::peniko::Format::Rgba8, - width, - height, - ), - image_ts, - ); + let image_ts = util::to_affine(&ts.pre_concat(view_box_transform)); + scene.draw_image(&image, image_ts); scene.pop_layer(); } @@ -321,186 +267,3 @@ fn render_group Result<(), E>, E>( Ok(()) } - -fn decode_raw_raster_image(img: &usvg::ImageKind) -> Result { - let res = match img { - usvg::ImageKind::JPEG(data) => { - image::load_from_memory_with_format(data, image::ImageFormat::Jpeg) - } - usvg::ImageKind::PNG(data) => { - image::load_from_memory_with_format(data, image::ImageFormat::Png) - } - usvg::ImageKind::GIF(data) => { - image::load_from_memory_with_format(data, image::ImageFormat::Gif) - } - usvg::ImageKind::SVG(_) => unreachable!(), - }? - .into_rgba8(); - Ok(res) -} - -fn to_affine(ts: &usvg::Transform) -> Affine { - let usvg::Transform { - sx, - kx, - ky, - sy, - tx, - ty, - } = ts; - Affine::new([sx, kx, ky, sy, tx, ty].map(|&x| f64::from(x))) -} - -fn to_bez_path(path: &usvg::Path) -> BezPath { - let mut local_path = BezPath::new(); - // The semantics of SVG paths don't line up with `BezPath`; we - // must manually track initial points - let mut just_closed = false; - let mut most_recent_initial = (0., 0.); - for elt in path.data().segments() { - match elt { - usvg::tiny_skia_path::PathSegment::MoveTo(p) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - most_recent_initial = (p.x.into(), p.y.into()); - local_path.move_to(most_recent_initial) - } - usvg::tiny_skia_path::PathSegment::LineTo(p) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.line_to(Point::new(p.x as f64, p.y as f64)) - } - usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.quad_to( - Point::new(p1.x as f64, p1.y as f64), - Point::new(p2.x as f64, p2.y as f64), - ) - } - usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => { - if std::mem::take(&mut just_closed) { - local_path.move_to(most_recent_initial); - } - local_path.curve_to( - Point::new(p1.x as f64, p1.y as f64), - Point::new(p2.x as f64, p2.y as f64), - Point::new(p3.x as f64, p3.y as f64), - ) - } - usvg::tiny_skia_path::PathSegment::Close => { - just_closed = true; - local_path.close_path() - } - } - } - - local_path -} - -/// Error handler function for [`render_tree_with`] which draws a transparent red box -/// instead of unsupported SVG features -pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) -> Result<(), Infallible> { - let bb = node.bounding_box(); - let rect = Rect { - x0: bb.left() as f64, - y0: bb.top() as f64, - x1: bb.right() as f64, - y1: bb.bottom() as f64, - }; - scene.fill( - Fill::NonZero, - Affine::IDENTITY, - Color::RED.with_alpha_factor(0.5), - None, - &rect, - ); - - Ok(()) -} - -fn paint_to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Brush, Affine)> { - match paint { - usvg::Paint::Color(color) => Some(( - Brush::Solid(Color::rgba8( - color.red, - color.green, - color.blue, - opacity.to_u8(), - )), - Affine::IDENTITY, - )), - usvg::Paint::LinearGradient(gr) => { - let stops: Vec = gr - .stops() - .iter() - .map(|stop| { - let mut cstop = vello::peniko::ColorStop::default(); - cstop.color.r = stop.color().red; - cstop.color.g = stop.color().green; - cstop.color.b = stop.color().blue; - cstop.color.a = (stop.opacity() * opacity).to_u8(); - cstop.offset = stop.offset().get(); - cstop - }) - .collect(); - let start = Point::new(gr.x1() as f64, gr.y1() as f64); - let end = Point::new(gr.x2() as f64, gr.y2() as f64); - let arr = [ - gr.transform().sx, - gr.transform().ky, - gr.transform().kx, - gr.transform().sy, - gr.transform().tx, - gr.transform().ty, - ] - .map(f64::from); - let transform = Affine::new(arr); - let gradient = - vello::peniko::Gradient::new_linear(start, end).with_stops(stops.as_slice()); - Some((Brush::Gradient(gradient), transform)) - } - usvg::Paint::RadialGradient(gr) => { - let stops: Vec = gr - .stops() - .iter() - .map(|stop| { - let mut cstop = vello::peniko::ColorStop::default(); - cstop.color.r = stop.color().red; - cstop.color.g = stop.color().green; - cstop.color.b = stop.color().blue; - cstop.color.a = (stop.opacity() * opacity).to_u8(); - cstop.offset = stop.offset().get(); - cstop - }) - .collect(); - - let start_center = Point::new(gr.cx() as f64, gr.cy() as f64); - let end_center = Point::new(gr.fx() as f64, gr.fy() as f64); - let start_radius = 0_f32; - let end_radius = gr.r().get(); - let arr = [ - gr.transform().sx, - gr.transform().ky, - gr.transform().kx, - gr.transform().sy, - gr.transform().tx, - gr.transform().ty, - ] - .map(f64::from); - let transform = Affine::new(arr); - let gradient = vello::peniko::Gradient::new_two_point_radial( - start_center, - start_radius, - end_center, - end_radius, - ) - .with_stops(stops.as_slice()); - Some((Brush::Gradient(gradient), transform)) - } - usvg::Paint::Pattern(_) => None, - } -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6aa22e9 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,225 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::convert::Infallible; +use vello::kurbo::{Affine, BezPath, Point, Rect, Stroke}; +use vello::peniko::{Blob, Brush, Color, Fill, Image}; +use vello::Scene; + +pub fn to_affine(ts: &usvg::Transform) -> Affine { + let usvg::Transform { + sx, + kx, + ky, + sy, + tx, + ty, + } = ts; + Affine::new([sx, kx, ky, sy, tx, ty].map(|&x| f64::from(x))) +} + +pub fn to_stroke(stroke: &usvg::Stroke) -> Stroke { + let mut conv_stroke = Stroke::new(stroke.width().get() as f64) + .with_caps(match stroke.linecap() { + usvg::LineCap::Butt => vello::kurbo::Cap::Butt, + usvg::LineCap::Round => vello::kurbo::Cap::Round, + usvg::LineCap::Square => vello::kurbo::Cap::Square, + }) + .with_join(match stroke.linejoin() { + usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => vello::kurbo::Join::Miter, + usvg::LineJoin::Round => vello::kurbo::Join::Round, + usvg::LineJoin::Bevel => vello::kurbo::Join::Bevel, + }) + .with_miter_limit(stroke.miterlimit().get() as f64); + if let Some(dash_array) = stroke.dasharray().as_ref() { + conv_stroke = conv_stroke.with_dashes( + stroke.dashoffset() as f64, + dash_array.iter().map(|x| *x as f64), + ); + } + conv_stroke +} + +pub fn to_bez_path(path: &usvg::Path) -> BezPath { + let mut local_path = BezPath::new(); + // The semantics of SVG paths don't line up with `BezPath`; we + // must manually track initial points + let mut just_closed = false; + let mut most_recent_initial = (0., 0.); + for elt in path.data().segments() { + match elt { + usvg::tiny_skia_path::PathSegment::MoveTo(p) => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + most_recent_initial = (p.x.into(), p.y.into()); + local_path.move_to(most_recent_initial) + } + usvg::tiny_skia_path::PathSegment::LineTo(p) => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + local_path.line_to(Point::new(p.x as f64, p.y as f64)) + } + usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + local_path.quad_to( + Point::new(p1.x as f64, p1.y as f64), + Point::new(p2.x as f64, p2.y as f64), + ) + } + usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => { + if std::mem::take(&mut just_closed) { + local_path.move_to(most_recent_initial); + } + local_path.curve_to( + Point::new(p1.x as f64, p1.y as f64), + Point::new(p2.x as f64, p2.y as f64), + Point::new(p3.x as f64, p3.y as f64), + ) + } + usvg::tiny_skia_path::PathSegment::Close => { + just_closed = true; + local_path.close_path() + } + } + } + + local_path +} + +pub fn into_image(image: image::ImageBuffer, Vec>) -> Image { + let (width, height) = (image.width(), image.height()); + let image_data: Vec = image.into_vec(); + Image::new( + Blob::new(std::sync::Arc::new(image_data)), + vello::peniko::Format::Rgba8, + width, + height, + ) +} + +pub fn to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Brush, Affine)> { + match paint { + usvg::Paint::Color(color) => Some(( + Brush::Solid(Color::rgba8( + color.red, + color.green, + color.blue, + opacity.to_u8(), + )), + Affine::IDENTITY, + )), + usvg::Paint::LinearGradient(gr) => { + let stops: Vec = gr + .stops() + .iter() + .map(|stop| { + let mut cstop = vello::peniko::ColorStop::default(); + cstop.color.r = stop.color().red; + cstop.color.g = stop.color().green; + cstop.color.b = stop.color().blue; + cstop.color.a = (stop.opacity() * opacity).to_u8(); + cstop.offset = stop.offset().get(); + cstop + }) + .collect(); + let start = Point::new(gr.x1() as f64, gr.y1() as f64); + let end = Point::new(gr.x2() as f64, gr.y2() as f64); + let arr = [ + gr.transform().sx, + gr.transform().ky, + gr.transform().kx, + gr.transform().sy, + gr.transform().tx, + gr.transform().ty, + ] + .map(f64::from); + let transform = Affine::new(arr); + let gradient = + vello::peniko::Gradient::new_linear(start, end).with_stops(stops.as_slice()); + Some((Brush::Gradient(gradient), transform)) + } + usvg::Paint::RadialGradient(gr) => { + let stops: Vec = gr + .stops() + .iter() + .map(|stop| { + let mut cstop = vello::peniko::ColorStop::default(); + cstop.color.r = stop.color().red; + cstop.color.g = stop.color().green; + cstop.color.b = stop.color().blue; + cstop.color.a = (stop.opacity() * opacity).to_u8(); + cstop.offset = stop.offset().get(); + cstop + }) + .collect(); + + let start_center = Point::new(gr.cx() as f64, gr.cy() as f64); + let end_center = Point::new(gr.fx() as f64, gr.fy() as f64); + let start_radius = 0_f32; + let end_radius = gr.r().get(); + let arr = [ + gr.transform().sx, + gr.transform().ky, + gr.transform().kx, + gr.transform().sy, + gr.transform().tx, + gr.transform().ty, + ] + .map(f64::from); + let transform = Affine::new(arr); + let gradient = vello::peniko::Gradient::new_two_point_radial( + start_center, + start_radius, + end_center, + end_radius, + ) + .with_stops(stops.as_slice()); + Some((Brush::Gradient(gradient), transform)) + } + usvg::Paint::Pattern(_) => None, + } +} + +/// Error handler function for [`render_tree_with`] which draws a transparent red box +/// instead of unsupported SVG features +pub fn default_error_handler(scene: &mut Scene, node: &usvg::Node) -> Result<(), Infallible> { + let bb = node.bounding_box(); + let rect = Rect { + x0: bb.left() as f64, + y0: bb.top() as f64, + x1: bb.right() as f64, + y1: bb.bottom() as f64, + }; + scene.fill( + Fill::NonZero, + Affine::IDENTITY, + Color::RED.with_alpha_factor(0.5), + None, + &rect, + ); + + Ok(()) +} + +pub fn decode_raw_raster_image( + img: &usvg::ImageKind, +) -> Result { + let res = match img { + usvg::ImageKind::JPEG(data) => { + image::load_from_memory_with_format(data, image::ImageFormat::Jpeg) + } + usvg::ImageKind::PNG(data) => { + image::load_from_memory_with_format(data, image::ImageFormat::Png) + } + usvg::ImageKind::GIF(data) => { + image::load_from_memory_with_format(data, image::ImageFormat::Gif) + } + usvg::ImageKind::SVG(_) => unreachable!(), + }? + .into_rgba8(); + Ok(res) +}