From 2b94970077c8e7b907086b98d86f952f8f92f0cb Mon Sep 17 00:00:00 2001 From: Chanjung Kim Date: Mon, 8 Apr 2024 11:58:17 +0900 Subject: [PATCH] Merge tag 'tags/v0.41.0' into feat/class-support --- .github/chart-svg2.svg | 62 +- .github/chart.svg | 58 +- CHANGELOG.md | 194 +- Cargo.lock | 205 +- Cargo.toml | 5 +- NOTICE.txt | 1 + README.md | 7 +- crates/c-api/Cargo.toml | 4 +- crates/c-api/ResvgQt.h | 4 +- crates/c-api/lib.rs | 118 +- crates/c-api/resvg.h | 18 +- crates/resvg/Cargo.toml | 12 +- crates/resvg/examples/bboxes.svg | 7 +- crates/resvg/examples/custom_href_resolver.rs | 25 +- crates/resvg/examples/custom_usvg_tree.rs | 55 - crates/resvg/examples/draw_bboxes.rs | 84 +- crates/resvg/examples/minimal.rs | 14 +- crates/resvg/src/clip.rs | 75 +- crates/resvg/src/filter/component_transfer.rs | 16 +- crates/resvg/src/filter/convolve_matrix.rs | 27 +- crates/resvg/src/filter/displacement_map.rs | 8 +- crates/resvg/src/filter/lighting.rs | 24 +- crates/resvg/src/filter/mod.rs | 423 +-- crates/resvg/src/image.rs | 122 +- crates/resvg/src/lib.rs | 77 +- crates/resvg/src/main.rs | 125 +- crates/resvg/src/mask.rs | 72 +- crates/resvg/src/paint_server.rs | 189 -- crates/resvg/src/path.rs | 359 +-- crates/resvg/src/render.rs | 108 +- crates/resvg/src/tree.rs | 285 -- .../tests/extra/filter-on-empty-group.png | Bin 0 -> 1878 bytes .../tests/extra/filter-on-empty-group.svg | 11 + .../extra/filter-with-transform-on-shape.png | Bin 0 -> 17558 bytes .../extra/filter-with-transform-on-shape.svg | 13 + crates/resvg/tests/integration/extra.rs | 12 +- crates/resvg/tests/integration/main.rs | 108 +- crates/resvg/tests/integration/render.rs | 54 + .../with-x-y-and-protruding-subregion-1.png | Bin 0 -> 610 bytes .../with-x-y-and-protruding-subregion-1.svg | 16 + .../with-x-y-and-protruding-subregion-2.png | Bin 0 -> 609 bytes .../with-x-y-and-protruding-subregion-2.svg | 16 + .../tests/tests/filters/feImage/with-x-y.png | Bin 0 -> 419 bytes .../tests/tests/filters/feImage/with-x-y.svg | 16 + .../on-group-with-child-outside-of-canvas.png | Bin 0 -> 362 bytes .../on-group-with-child-outside-of-canvas.svg | 19 + .../tests/tests/filters/filter/path-bbox.png | Bin 0 -> 1195 bytes .../tests/tests/filters/filter/path-bbox.svg | 16 + .../with-transform-outside-of-canvas.png | Bin 0 -> 362 bytes .../with-transform-outside-of-canvas.svg | 19 + .../tests/masking/mask/mask-type-in-style.png | Bin 0 -> 1529 bytes .../tests/masking/mask/mask-type-in-style.svg | 15 + ...recursive-nested-context-without-color.png | Bin 0 -> 346 bytes ...recursive-nested-context-without-color.svg | 14 + .../color/recursive-nested-context.png | Bin 0 -> 432 bytes .../color/recursive-nested-context.svg | 15 + .../tests/painting/context/in-marker.png | Bin 0 -> 3799 bytes .../tests/painting/context/in-marker.svg | 13 + .../painting/context/in-nested-marker.png | Bin 0 -> 4105 bytes .../painting/context/in-nested-marker.svg | 17 + .../context/in-nested-use-and-marker.png | Bin 0 -> 3799 bytes .../context/in-nested-use-and-marker.svg | 18 + .../tests/painting/context/in-nested-use.png | Bin 0 -> 2928 bytes .../tests/painting/context/in-nested-use.svg | 15 + .../tests/tests/painting/context/in-use.png | Bin 0 -> 2949 bytes .../tests/tests/painting/context/in-use.svg | 14 + .../context/on-shape-with-zero-size-bbox.png | Bin 0 -> 705 bytes .../context/on-shape-with-zero-size-bbox.svg | 11 + .../with-gradient-and-gradient-transform.png | Bin 0 -> 5202 bytes .../with-gradient-and-gradient-transform.svg | 28 + .../painting/context/with-gradient-in-use.png | Bin 0 -> 5149 bytes .../painting/context/with-gradient-in-use.svg | 28 + .../context/with-gradient-on-marker.png | Bin 0 -> 5768 bytes .../context/with-gradient-on-marker.svg | 20 + .../with-pattern-and-transform-in-use.png | Bin 0 -> 12751 bytes .../with-pattern-and-transform-in-use.svg | 26 + .../painting/context/with-pattern-in-use.png | Bin 0 -> 5808 bytes .../painting/context/with-pattern-in-use.svg | 26 + .../with-pattern-objectBoundingBox-in-use.png | Bin 0 -> 10306 bytes .../with-pattern-objectBoundingBox-in-use.svg | 28 + .../context/with-pattern-on-marker.png | Bin 0 -> 2204 bytes .../context/with-pattern-on-marker.svg | 18 + .../tests/painting/context/with-text.png | Bin 0 -> 6611 bytes .../tests/painting/context/with-text.svg | 25 + .../context/without-context-element.png | Bin 0 -> 346 bytes .../context/without-context-element.svg | 8 + .../paint-order/fill-markers-stroke.png | Bin 655 -> 15252 bytes .../paint-order/stroke-markers-fill.png | Bin 0 -> 11977 bytes .../paint-order/stroke-markers-fill.svg | 16 + .../painting/paint-order/stroke-markers.png | Bin 463 -> 11977 bytes .../painting/visibility/bbox-impact-3.png | Bin 1422 -> 1426 bytes .../tests/tests/shapes/rect/ic-values.svg | 2 +- .../image/embedded-svg-with-text.png | Bin 0 -> 16816 bytes .../image/embedded-svg-with-text.svg | 8 + .../structure/image/no-height-non-square.png | Bin 0 -> 1291 bytes .../structure/image/no-height-non-square.svg | 10 + .../structure/image/no-height-on-svg.png | Bin 4956 -> 5495 bytes .../tests/tests/structure/image/no-height.png | Bin 53152 -> 49382 bytes .../tests/tests/structure/image/no-height.svg | 9 +- .../tests/structure/image/no-width-on-svg.png | Bin 5238 -> 5495 bytes .../tests/tests/structure/image/no-width.png | Bin 53152 -> 49382 bytes .../tests/tests/structure/image/no-width.svg | 9 +- .../structure/transform-origin/bottom.png | Bin 0 -> 9415 bytes .../structure/transform-origin/bottom.svg | 13 + .../structure/transform-origin/center.png | Bin 0 -> 9449 bytes .../structure/transform-origin/center.svg | 13 + .../transform-origin/keyword-length.png | Bin 0 -> 8762 bytes .../transform-origin/keyword-length.svg | 13 + .../tests/structure/transform-origin/left.png | Bin 0 -> 9430 bytes .../tests/structure/transform-origin/left.svg | 13 + .../transform-origin/length-percent.png | Bin 0 -> 9449 bytes .../transform-origin/length-percent.svg | 13 + .../structure/transform-origin/length-px.png | Bin 0 -> 9449 bytes .../structure/transform-origin/length-px.svg | 13 + .../transform-origin/no-transform.png | Bin 0 -> 7448 bytes .../transform-origin/no-transform.svg | 11 + .../on-clippath-objectBoundingBox.png | Bin 0 -> 9819 bytes .../on-clippath-objectBoundingBox.svg | 11 + .../transform-origin/on-clippath.png | Bin 0 -> 11440 bytes .../transform-origin/on-clippath.svg | 11 + .../on-gradient-object-bounding-box.png | Bin 0 -> 33631 bytes .../on-gradient-object-bounding-box.svg | 13 + .../on-gradient-user-space-on-use.png | Bin 0 -> 33631 bytes .../on-gradient-user-space-on-use.svg | 13 + .../structure/transform-origin/on-group.png | Bin 0 -> 9375 bytes .../structure/transform-origin/on-group.svg | 10 + .../structure/transform-origin/on-image.png | Bin 0 -> 31032 bytes .../structure/transform-origin/on-image.svg | 62 + .../on-pattern-object-bounding-box.png | Bin 0 -> 13595 bytes .../on-pattern-object-bounding-box.svg | 14 + .../on-pattern-user-space-on-use.png | Bin 0 -> 13595 bytes .../on-pattern-user-space-on-use.svg | 14 + .../structure/transform-origin/on-shape.png | Bin 0 -> 9375 bytes .../structure/transform-origin/on-shape.svg | 9 + .../transform-origin/on-text-path.png | Bin 0 -> 16250 bytes .../transform-origin/on-text-path.svg | 16 + .../structure/transform-origin/on-text.png | Bin 0 -> 13386 bytes .../structure/transform-origin/on-text.svg | 12 + .../transform-origin/right-bottom.png | Bin 0 -> 8196 bytes .../transform-origin/right-bottom.svg | 13 + .../structure/transform-origin/right.png | Bin 0 -> 9431 bytes .../structure/transform-origin/right.svg | 13 + .../structure/transform-origin/top-left.png | Bin 0 -> 8144 bytes .../structure/transform-origin/top-left.svg | 12 + .../tests/structure/transform-origin/top.png | Bin 0 -> 9408 bytes .../tests/structure/transform-origin/top.svg | 13 + .../transform-origin/transform-on-parent.png | Bin 0 -> 7448 bytes .../transform-origin/transform-on-parent.svg | 14 + .../tests/tests/text/font/font-shorthand.png | Bin 0 -> 13832 bytes .../tests/tests/text/font/font-shorthand.svg | 13 + .../letter-spacing/non-ASCII-character.svg | 2 +- .../indirect-with-multiple-colors.png | Bin 0 -> 19114 bytes .../indirect-with-multiple-colors.svg | 19 + .../tests/text/text-decoration/indirect.png | Bin 1519 -> 13555 bytes .../text-decoration/style-resolving-4.png | Bin 2106 -> 16055 bytes .../ligatures-handling-in-mixed-fonts-1.png | Bin 0 -> 1635 bytes .../ligatures-handling-in-mixed-fonts-1.svg | 19 + .../ligatures-handling-in-mixed-fonts-2.png | Bin 0 -> 1638 bytes .../ligatures-handling-in-mixed-fonts-2.svg | 19 + .../tests/tests/text/textPath/complex.svg | 2 +- .../tests/text/textPath/writing-mode=tb.svg | 2 +- .../text/writing-mode/japanese-with-tb.svg | 2 +- .../text/writing-mode/tb-and-punctuation.svg | 2 +- .../tb-with-dx-on-second-tspan.png | Bin 2136 -> 16922 bytes crates/usvg-parser/Cargo.toml | 29 - crates/usvg-parser/LICENSE.txt | 373 --- crates/usvg-parser/README.md | 13 - crates/usvg-parser/src/converter.rs | 725 ----- crates/usvg-parser/src/mask.rs | 84 - crates/usvg-parser/src/paint_server.rs | 527 ---- crates/usvg-parser/tests/test.rs | 88 - crates/usvg-text-layout/Cargo.toml | 30 - crates/usvg-text-layout/LICENSE.txt | 373 --- crates/usvg-tree/Cargo.toml | 22 - crates/usvg-tree/LICENSE.txt | 373 --- crates/usvg-tree/README.md | 13 - crates/usvg-tree/src/lib.rs | 1359 --------- crates/usvg-tree/src/text.rs | 342 --- crates/usvg/Cargo.toml | 42 +- crates/usvg/README.md | 6 +- .../{usvg-parser => usvg}/codegen/Cargo.toml | 2 +- .../{usvg-parser => usvg}/codegen/README.md | 2 +- .../codegen/attributes.txt | 0 .../codegen/elements.txt | 0 crates/{usvg-parser => usvg}/codegen/main.rs | 41 +- .../docs/post-processing.md | 6 +- crates/usvg/docs/spec.adoc | 24 +- crates/usvg/src/lib.rs | 34 +- crates/usvg/src/main.rs | 60 +- .../src => usvg/src/parser}/clippath.rs | 69 +- crates/usvg/src/parser/converter.rs | 953 ++++++ .../src => usvg/src/parser}/filter.rs | 305 +- .../src => usvg/src/parser}/image.rs | 220 +- .../src => usvg/src/parser}/marker.rs | 49 +- crates/usvg/src/parser/mask.rs | 151 + .../src/lib.rs => usvg/src/parser/mod.rs} | 92 +- .../src => usvg/src/parser}/options.rs | 4 +- crates/usvg/src/parser/paint_server.rs | 1065 +++++++ .../src => usvg/src/parser}/shapes.rs | 40 +- .../src => usvg/src/parser}/style.rs | 99 +- .../src => usvg/src/parser}/svgtree/mod.rs | 119 +- .../src => usvg/src/parser}/svgtree/names.rs | 29 +- .../src => usvg/src/parser}/svgtree/parse.rs | 74 +- .../src => usvg/src/parser}/svgtree/text.rs | 2 +- .../src => usvg/src/parser}/switch.rs | 21 +- .../src => usvg/src/parser}/text.rs | 294 +- .../src => usvg/src/parser}/units.rs | 8 +- .../src => usvg/src/parser}/use_node.rs | 197 +- crates/usvg/src/text/flatten.rs | 135 + .../src/lib.rs => usvg/src/text/layout.rs} | 2691 ++++++++--------- crates/usvg/src/text/mod.rs | 29 + .../src => usvg/src/tree}/filter.rs | 481 ++- .../{usvg-tree/src => usvg/src/tree}/geom.rs | 90 +- crates/usvg/src/tree/mod.rs | 1893 ++++++++++++ crates/usvg/src/tree/text.rs | 602 ++++ crates/usvg/src/writer.rs | 919 ++++-- .../clip-path-with-complex-text-expected.svg | 11 + .../files/clip-path-with-complex-text.svg | 13 + ...h-with-object-units-multi-use-expected.svg | 16 + .../clip-path-with-object-units-multi-use.svg | 7 + .../files/clip-path-with-text-expected.svg | 10 + .../usvg/tests/files/clip-path-with-text.svg | 9 + .../files/ellipse-simple-case-expected.svg | 4 + .../usvg/tests/files/ellipse-simple-case.svg | 4 + .../files/filter-id-with-prefix-expected.svg | 25 + .../tests/files/filter-id-with-prefix.svg | 9 + ...r-with-object-units-multi-use-expected.svg | 16 + .../filter-with-object-units-multi-use.svg | 7 + ...erate-id-clip-path-for-symbol-expected.svg | 12 + .../generate-id-clip-path-for-symbol.svg | 10 + ...enerate-id-filter-function-v1-expected.svg | 25 + .../files/generate-id-filter-function-v1.svg | 8 + ...enerate-id-filter-function-v2-expected.svg | 16 + .../files/generate-id-filter-function-v2.svg | 11 + ...k-with-object-units-multi-use-expected.svg | 20 + .../mask-with-object-units-multi-use.svg | 7 + .../tests/files/path-simple-case-expected.svg | 4 + crates/usvg/tests/files/path-simple-case.svg | 3 + .../preserve-id-clip-path-v1-expected.svg | 10 + .../tests/files/preserve-id-clip-path-v1.svg | 6 + .../preserve-id-clip-path-v2-expected.svg | 13 + .../tests/files/preserve-id-clip-path-v2.svg | 10 + .../files/preserve-id-fe-image-expected.svg | 12 + ...erve-id-fe-image-with-opacity-expected.svg | 14 + .../preserve-id-fe-image-with-opacity.svg | 13 + .../usvg/tests/files/preserve-id-fe-image.svg | 11 + .../files/preserve-id-filter-expected.svg | 10 + .../usvg/tests/files/preserve-id-filter.svg | 7 + ...e-id-for-clip-path-in-pattern-expected.svg | 13 + .../preserve-id-for-clip-path-in-pattern.svg | 9 + .../preserve-text-in-clip-path-expected.svg | 11 + .../files/preserve-text-in-clip-path.svg | 14 + .../files/preserve-text-in-mask-expected.svg | 12 + .../tests/files/preserve-text-in-mask.svg | 12 + .../preserve-text-in-pattern-expected.svg | 8 + .../tests/files/preserve-text-in-pattern.svg | 7 + ...e-text-multiple-font-families-expected.svg | 4 + .../preserve-text-multiple-font-families.svg | 4 + .../files/preserve-text-on-path-expected.svg | 7 + .../tests/files/preserve-text-on-path.svg | 15 + .../preserve-text-simple-case-expected.svg | 6 + .../tests/files/preserve-text-simple-case.svg | 4 + ...-with-complex-text-decoration-expected.svg | 4 + ...erve-text-with-complex-text-decoration.svg | 12 + .../preserve-text-with-dx-and-dy-expected.svg | 4 + .../files/preserve-text-with-dx-and-dy.svg | 6 + ...xt-with-nested-baseline-shift-expected.svg | 4 + ...eserve-text-with-nested-baseline-shift.svg | 25 + .../preserve-text-with-rotate-expected.svg | 4 + .../tests/files/preserve-text-with-rotate.svg | 7 + .../tests/files/text-simple-case-expected.svg | 6 + crates/usvg/tests/files/text-simple-case.svg | 4 + ...text-with-generated-gradients-expected.svg | 13 + .../files/text-with-generated-gradients.svg | 13 + crates/usvg/tests/parser.rs | 331 ++ crates/usvg/tests/write.rs | 196 ++ docs/svg2-changelog.md | 10 +- docs/unsupported.md | 1 - tools/explorer-thumbnailer/Cargo.toml | 4 +- .../install/installer.iss | 6 +- tools/explorer-thumbnailer/src/error.rs | 10 +- .../src/thumbnail_provider.rs | 19 +- tools/explorer-thumbnailer/src/utils.rs | 18 +- tools/viewsvg/README.md | 2 +- version-bump.md | 5 +- 285 files changed, 11553 insertions(+), 8724 deletions(-) create mode 100644 NOTICE.txt delete mode 100644 crates/resvg/examples/custom_usvg_tree.rs delete mode 100644 crates/resvg/src/paint_server.rs delete mode 100644 crates/resvg/src/tree.rs create mode 100644 crates/resvg/tests/extra/filter-on-empty-group.png create mode 100644 crates/resvg/tests/extra/filter-on-empty-group.svg create mode 100644 crates/resvg/tests/extra/filter-with-transform-on-shape.png create mode 100644 crates/resvg/tests/extra/filter-with-transform-on-shape.svg create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.png create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.svg create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.png create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.svg create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y.png create mode 100644 crates/resvg/tests/tests/filters/feImage/with-x-y.svg create mode 100644 crates/resvg/tests/tests/filters/filter/on-group-with-child-outside-of-canvas.png create mode 100644 crates/resvg/tests/tests/filters/filter/on-group-with-child-outside-of-canvas.svg create mode 100644 crates/resvg/tests/tests/filters/filter/path-bbox.png create mode 100644 crates/resvg/tests/tests/filters/filter/path-bbox.svg create mode 100644 crates/resvg/tests/tests/filters/filter/with-transform-outside-of-canvas.png create mode 100644 crates/resvg/tests/tests/filters/filter/with-transform-outside-of-canvas.svg create mode 100644 crates/resvg/tests/tests/masking/mask/mask-type-in-style.png create mode 100644 crates/resvg/tests/tests/masking/mask/mask-type-in-style.svg create mode 100644 crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.png create mode 100644 crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.svg create mode 100644 crates/resvg/tests/tests/painting/color/recursive-nested-context.png create mode 100644 crates/resvg/tests/tests/painting/color/recursive-nested-context.svg create mode 100644 crates/resvg/tests/tests/painting/context/in-marker.png create mode 100644 crates/resvg/tests/tests/painting/context/in-marker.svg create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-marker.png create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-marker.svg create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.png create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.svg create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-use.png create mode 100644 crates/resvg/tests/tests/painting/context/in-nested-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/in-use.png create mode 100644 crates/resvg/tests/tests/painting/context/in-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.png create mode 100644 crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-and-gradient-transform.png create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-and-gradient-transform.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-in-use.png create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-in-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-on-marker.png create mode 100644 crates/resvg/tests/tests/painting/context/with-gradient-on-marker.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-and-transform-in-use.png create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-and-transform-in-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-in-use.png create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-in-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-objectBoundingBox-in-use.png create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-objectBoundingBox-in-use.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-on-marker.png create mode 100644 crates/resvg/tests/tests/painting/context/with-pattern-on-marker.svg create mode 100644 crates/resvg/tests/tests/painting/context/with-text.png create mode 100644 crates/resvg/tests/tests/painting/context/with-text.svg create mode 100644 crates/resvg/tests/tests/painting/context/without-context-element.png create mode 100644 crates/resvg/tests/tests/painting/context/without-context-element.svg create mode 100644 crates/resvg/tests/tests/painting/paint-order/stroke-markers-fill.png create mode 100644 crates/resvg/tests/tests/painting/paint-order/stroke-markers-fill.svg create mode 100644 crates/resvg/tests/tests/structure/image/embedded-svg-with-text.png create mode 100644 crates/resvg/tests/tests/structure/image/embedded-svg-with-text.svg create mode 100644 crates/resvg/tests/tests/structure/image/no-height-non-square.png create mode 100644 crates/resvg/tests/tests/structure/image/no-height-non-square.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/bottom.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/bottom.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/center.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/center.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/keyword-length.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/keyword-length.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/left.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/left.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/length-percent.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/length-percent.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/length-px.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/length-px.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/no-transform.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/no-transform.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-clippath.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-clippath.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-group.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-group.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-image.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-image.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-shape.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-shape.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-text-path.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-text-path.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-text.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/on-text.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/right-bottom.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/right-bottom.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/right.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/right.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/top-left.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/top-left.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/top.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/top.svg create mode 100644 crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.png create mode 100644 crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.svg create mode 100644 crates/resvg/tests/tests/text/font/font-shorthand.png create mode 100644 crates/resvg/tests/tests/text/font/font-shorthand.svg create mode 100644 crates/resvg/tests/tests/text/text-decoration/indirect-with-multiple-colors.png create mode 100644 crates/resvg/tests/tests/text/text-decoration/indirect-with-multiple-colors.svg create mode 100644 crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png create mode 100644 crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.svg create mode 100644 crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png create mode 100644 crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg delete mode 100644 crates/usvg-parser/Cargo.toml delete mode 100644 crates/usvg-parser/LICENSE.txt delete mode 100644 crates/usvg-parser/README.md delete mode 100644 crates/usvg-parser/src/converter.rs delete mode 100644 crates/usvg-parser/src/mask.rs delete mode 100644 crates/usvg-parser/src/paint_server.rs delete mode 100644 crates/usvg-parser/tests/test.rs delete mode 100644 crates/usvg-text-layout/Cargo.toml delete mode 100644 crates/usvg-text-layout/LICENSE.txt delete mode 100644 crates/usvg-tree/Cargo.toml delete mode 100644 crates/usvg-tree/LICENSE.txt delete mode 100644 crates/usvg-tree/README.md delete mode 100644 crates/usvg-tree/src/lib.rs delete mode 100644 crates/usvg-tree/src/text.rs rename crates/{usvg-parser => usvg}/codegen/Cargo.toml (93%) rename crates/{usvg-parser => usvg}/codegen/README.md (55%) rename crates/{usvg-parser => usvg}/codegen/attributes.txt (100%) rename crates/{usvg-parser => usvg}/codegen/elements.txt (100%) rename crates/{usvg-parser => usvg}/codegen/main.rs (88%) rename crates/{usvg-parser => usvg}/docs/post-processing.md (95%) rename crates/{usvg-parser/src => usvg/src/parser}/clippath.rs (54%) create mode 100644 crates/usvg/src/parser/converter.rs rename crates/{usvg-parser/src => usvg/src/parser}/filter.rs (79%) rename crates/{usvg-parser/src => usvg/src/parser}/image.rs (58%) rename crates/{usvg-parser/src => usvg/src/parser}/marker.rs (94%) create mode 100644 crates/usvg/src/parser/mask.rs rename crates/{usvg-parser/src/lib.rs => usvg/src/parser/mod.rs} (68%) rename crates/{usvg-parser/src => usvg/src/parser}/options.rs (97%) create mode 100644 crates/usvg/src/parser/paint_server.rs rename crates/{usvg-parser/src => usvg/src/parser}/shapes.rs (92%) rename crates/{usvg-parser/src => usvg/src/parser}/style.rs (72%) rename crates/{usvg-parser/src => usvg/src/parser}/svgtree/mod.rs (89%) rename crates/{usvg-parser/src => usvg/src/parser}/svgtree/names.rs (97%) rename crates/{usvg-parser/src => usvg/src/parser}/svgtree/parse.rs (88%) rename crates/{usvg-parser/src => usvg/src/parser}/svgtree/text.rs (99%) rename crates/{usvg-parser/src => usvg/src/parser}/switch.rs (90%) rename crates/{usvg-parser/src => usvg/src/parser}/text.rs (76%) rename crates/{usvg-parser/src => usvg/src/parser}/units.rs (96%) rename crates/{usvg-parser/src => usvg/src/parser}/use_node.rs (63%) create mode 100644 crates/usvg/src/text/flatten.rs rename crates/{usvg-text-layout/src/lib.rs => usvg/src/text/layout.rs} (71%) create mode 100644 crates/usvg/src/text/mod.rs rename crates/{usvg-tree/src => usvg/src/tree}/filter.rs (65%) rename crates/{usvg-tree/src => usvg/src/tree}/geom.rs (88%) create mode 100644 crates/usvg/src/tree/mod.rs create mode 100644 crates/usvg/src/tree/text.rs create mode 100644 crates/usvg/tests/files/clip-path-with-complex-text-expected.svg create mode 100644 crates/usvg/tests/files/clip-path-with-complex-text.svg create mode 100644 crates/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg create mode 100644 crates/usvg/tests/files/clip-path-with-object-units-multi-use.svg create mode 100644 crates/usvg/tests/files/clip-path-with-text-expected.svg create mode 100644 crates/usvg/tests/files/clip-path-with-text.svg create mode 100644 crates/usvg/tests/files/ellipse-simple-case-expected.svg create mode 100644 crates/usvg/tests/files/ellipse-simple-case.svg create mode 100644 crates/usvg/tests/files/filter-id-with-prefix-expected.svg create mode 100644 crates/usvg/tests/files/filter-id-with-prefix.svg create mode 100644 crates/usvg/tests/files/filter-with-object-units-multi-use-expected.svg create mode 100644 crates/usvg/tests/files/filter-with-object-units-multi-use.svg create mode 100644 crates/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg create mode 100644 crates/usvg/tests/files/generate-id-clip-path-for-symbol.svg create mode 100644 crates/usvg/tests/files/generate-id-filter-function-v1-expected.svg create mode 100644 crates/usvg/tests/files/generate-id-filter-function-v1.svg create mode 100644 crates/usvg/tests/files/generate-id-filter-function-v2-expected.svg create mode 100644 crates/usvg/tests/files/generate-id-filter-function-v2.svg create mode 100644 crates/usvg/tests/files/mask-with-object-units-multi-use-expected.svg create mode 100644 crates/usvg/tests/files/mask-with-object-units-multi-use.svg create mode 100644 crates/usvg/tests/files/path-simple-case-expected.svg create mode 100644 crates/usvg/tests/files/path-simple-case.svg create mode 100644 crates/usvg/tests/files/preserve-id-clip-path-v1-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-clip-path-v1.svg create mode 100644 crates/usvg/tests/files/preserve-id-clip-path-v2-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-clip-path-v2.svg create mode 100644 crates/usvg/tests/files/preserve-id-fe-image-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-fe-image-with-opacity.svg create mode 100644 crates/usvg/tests/files/preserve-id-fe-image.svg create mode 100644 crates/usvg/tests/files/preserve-id-filter-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-filter.svg create mode 100644 crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg create mode 100644 crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-clip-path-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-clip-path.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-mask-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-mask.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-pattern-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-in-pattern.svg create mode 100644 crates/usvg/tests/files/preserve-text-multiple-font-families-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-multiple-font-families.svg create mode 100644 crates/usvg/tests/files/preserve-text-on-path-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-on-path.svg create mode 100644 crates/usvg/tests/files/preserve-text-simple-case-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-simple-case.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-complex-text-decoration.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-dx-and-dy.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-rotate-expected.svg create mode 100644 crates/usvg/tests/files/preserve-text-with-rotate.svg create mode 100644 crates/usvg/tests/files/text-simple-case-expected.svg create mode 100644 crates/usvg/tests/files/text-simple-case.svg create mode 100644 crates/usvg/tests/files/text-with-generated-gradients-expected.svg create mode 100644 crates/usvg/tests/files/text-with-generated-gradients.svg create mode 100644 crates/usvg/tests/parser.rs create mode 100644 crates/usvg/tests/write.rs diff --git a/.github/chart-svg2.svg b/.github/chart-svg2.svg index 258c084f4..180aa3758 100644 --- a/.github/chart-svg2.svg +++ b/.github/chart-svg2.svg @@ -2,44 +2,44 @@ - resvg 0.36.0 - Chrome 105 - Firefox 107 - Safari 16.1 - librsvg 2.55.1 - Inkscape 1.2.1 - Batik 1.16 + resvg 0.41.0 + Chrome 123 + Firefox 124 + Safari 17.3.1 + librsvg 2.58.0 + Inkscape 1.3.2 + Batik 1.17 SVG.NET 3.2.3 - QtSvg 6.4.0 + QtSvg 6.7.0 0 - 39 + 50 - 79 + 100 - 118 + 149 - 157 - - 133 - - 140 - - 140 - - 85 - - 102 - - 50 - - - 3 - - 7 - - 8 + 199 + + 179 + + 165 + + 167 + + 104 + + 123 + + 55 + + + 3 + + 7 + + 9 Tests passed diff --git a/.github/chart.svg b/.github/chart.svg index 60f62552f..edac7a900 100644 --- a/.github/chart.svg +++ b/.github/chart.svg @@ -2,43 +2,43 @@ - resvg 0.36.0 - Chrome 105 - Firefox 107 - Safari 16.1 - librsvg 2.55.1 - Inkscape 1.2.1 - Batik 1.16 + resvg 0.41.0 + Chrome 123 + Firefox 124 + Safari 17.3.1 + librsvg 2.58.0 + Inkscape 1.3.2 + Batik 1.17 SVG.NET 3.2.3 - QtSvg 6.4.0 + QtSvg 6.7.0 0 - 391 + 404 - 782 + 808 - 1172 + 1212 - 1563 - - 1459 - - 1399 - - 1350 - - 1303 + 1616 + + 1520 + + 1423 + + 1385 + + 1341 - 1128 - - 975 - - 970 - - 530 - - 413 + 1168 + + 985 + + 976 + + 530 + + 591 Tests passed diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a90afe3..e104133e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,193 @@ This changelog also contains important changes in dependencies. ## [Unreleased] +## [0.41.0] - 2024-04-03 +### Added +- `context-fill` and `context-stroke` support. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- `usvg::Text::layouted()`, which returns a list of glyph IDs. + It can be used to manually draw glyphs, unlike with `usvg::Text::flattened()`, which returns + just vector paths. + Thanks to [@LaurenzV](https://github.com/LaurenzV). + +### Fixed +- Missing text when a `text` element uses multiple fonts and one of them produces ligatures. +- Absolute transform propagation during `use` resolving. +- Absolute transform propagation during nested `svg` resolving. +- `Node::abs_transform` documentation. The current element's transform _is_ included. + +## [0.40.0] - 2024-02-17 +### Added +- `usvg::Tree` is `Send + Sync` compatible now. +- `usvg::WriteOptions::preserve_text` to control how `usvg` generates an SVG. +- `usvg::Image::abs_bounding_box` + +### Changed +- All types in `usvg` are immutable now. Meaning that `usvg::Tree` cannot be modified + after creation anymore. +- All struct fields in `usvg` are private now. Use getters instead. +- All `usvg::Tree` parsing methods require the `fontdb` argument now. +- All `defs` children like gradients, patterns, clipPaths, masks and filters are guarantee + to have a unique, non-empty ID. +- All `defs` children like gradients, patterns, clipPaths, masks and filters are guarantee + to have `userSpaceOnUse` units now. No `objectBoundingBox` units anymore. +- `usvg::Mask` is allowed to have no children now. +- Text nodes will not be parsed when the `text` build feature isn't enabled. +- `usvg::Tree::clip_paths`, `usvg::Tree::masks`, `usvg::Tree::filters` returns + a pre-collected slice of unique nodes now. + It's no longer a closure and you do not have to deduplicate nodes by yourself. +- `usvg::filter::Primitive::x`, `y`, `width` and `height` methods were replaced + with `usvg::filter::Primitive::rect`. +- Split `usvg::Tree::paint_servers` into `usvg::Tree::linear_gradients`, + `usvg::Tree::radial_gradients`, `usvg::Tree::patterns`. + All three returns pre-collected slices now. +- A `usvg::Path` no longer can have an invalid bbox. Paths with an invalid bbox will be + rejected during parsing. +- All `usvg` methods that return bounding boxes return non-optional `Rect` now. + No `NonZeroRect` as well. +- `usvg::Text::flattened` returns `&Group` and not `Option<&Group>` now. +- `usvg::ImageHrefDataResolverFn` and `usvg::ImageHrefStringResolverFn` + require `fontdb::Database` argument. +- All shared nodes are stored in `Arc` and not `Rc` now. +- `resvg::render_node` now includes filters bounding box. Meaning that a node with a blur filter + no longer be clipped. +- Replace `usvg::utils::view_box_to_transform` with `usvg::ViewBox::to_transform`. +- Rename `usvg::XmlOptions` into `usvg::WriteOptions` and embed `xmlwriter::Options`. + +### Removed +- `usvg::Tree::postprocess()` and `usvg::PostProcessingSteps`. No longer needed. +- `usvg::ClipPath::units()`, `usvg::Mask::units()`, `usvg::Mask::content_units()`, + `usvg::Filter::units()`, `usvg::Filter::content_units()`, `usvg::LinearGradient::units()`, + `usvg::RadialGradient::units()`, `usvg::Pattern::units()`, `usvg::Pattern::content_units()` + and `usvg::Paint::units()`. They are always `userSpaceOnUse` now. +- `usvg::Units`. No longer needed. + +### Fixed +- Text bounding box is accounted during SVG size resolving. + Previously, only paths and images were included. +- Font selection when an italic font isn't explicitly marked as one. +- Preserve `image` aspect ratio when only `width` or `height` are present. + Thanks to [@LaurenzV](https://github.com/LaurenzV). + +## [0.39.0] - 2024-02-06 +### Added +- `font` shorthand parsing. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- `usvg::Group::abs_bounding_box` +- `usvg::Group::abs_stroke_bounding_box` +- `usvg::Path::abs_bounding_box` +- `usvg::Path::abs_stroke_bounding_box` +- `usvg::Text::abs_bounding_box` +- `usvg::Text::abs_stroke_bounding_box` + +### Changed +- All `usvg-*` crates merged into one. There is just the `usvg` crate now, as before. + +### Removed +- `usvg::Group::abs_bounding_box()` method. It's a field now. +- `usvg::Group::abs_filters_bounding_box()` +- `usvg::TreeParsing`, `usvg::TreePostProc` and `usvg::TreeWriting` traits. + They are no longer needed. + +### Fixed +- `font-family` parsing. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- Absolute bounding box calculation for paths. + +## [0.38.0] - 2024-01-21 +### Added +- Each `usvg::Node` stores its absolute transform now. + `Node::abs_transform()` executes in constant time now. +- `usvg::Tree::calculate_bounding_boxes` to calculate all bounding boxes beforehand. +- `usvg::Node::bounding_box` which returns a precalculated node's bounding box in object coordinates. +- `usvg::Node::abs_bounding_box` which returns a precalculated node's bounding box in canvas coordinates. +- `usvg::Node::stroke_bounding_box` which returns a precalculated node's bounding box, + including stroke, in object coordinates. +- `usvg::Node::abs_stroke_bounding_box` which returns a precalculated node's bounding box, + including stroke, in canvas coordinates. +- (c-api) `resvg_get_node_stroke_bbox` +- `usvg::Node::filters_bounding_box` +- `usvg::Node::abs_filters_bounding_box` +- `usvg::Tree::postprocess` + +### Changed +- `resvg` renders `usvg::Tree` directly again. `resvg::Tree` is gone. +- `usvg` no longer uses `rctree` for the nodes tree implementation. + The tree is a regular `enum` now. + - A caller no longer need to use the awkward `*node.borrow()`. + - No more panics on incorrect mutable `Rc` access. + - Tree nodes respect tree's mutability rules. Before, one could mutate tree nodes when the tree + itself is not mutable. Because `Rc` provides a shared mutable access. +- Filters, clip paths, masks and patterns are stored as `Rc>` instead of `Rc`. + This is required for proper mutability since `Node` itself is no longer an `Rc`. +- Rename `usvg::NodeKind` into `usvg::Node`. +- Upgrade to Rust 2021 edition. + +### Removed +- `resvg::Tree`. No longer needed. `resvg` can render `usvg::Tree` directly once again. +- `rctree::Node` methods. The `Node` API is completely different now. +- `usvg::NodeExt`. No longer needed. +- `usvg::Node::calculate_bbox`. Use `usvg::Node::abs_bounding_box` instead. +- `usvg::Tree::convert_text`. Use `usvg::Tree::postprocess` instead. +- `usvg::TreeTextToPath` trait. No longer needed. + +### Fixed +- Mark `mask-type` as a presentation attribute. +- Do not show needless warnings when parsing some attributes. +- `feImage` rendering with a non-default position. + Thanks to [@LaurenzV](https://github.com/LaurenzV). + +## [0.37.0] - 2023-12-16 +### Added +- `usvg` can write text back to SVG now. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- `--preserve-text` flag to the `usvg` CLI tool. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- Support [`transform-origin`](https://drafts.csswg.org/css-transforms/#transform-origin-property) + property. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- Support non-default markers order via + [`paint-order`](https://svgwg.org/svg2-draft/painting.html#PaintOrder). + Previously, only fill and stroke could have been swapped. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- `usvg_tree::Text::flattened` that will contain a flattened/outlined text. +- `usvg_tree::Text::bounding_box`. Will be set only after text flattening. +- Optimize `usvg_tree::NodeExt::abs_transform` by storing absolute transforms in the tree + instead of calculating them each time. + +### Changed +- `usvg_tree::Text::positions` was replaced with `usvg_tree::Text::dx` and `usvg_tree::Text::dy`.
+ `usvg_tree::CharacterPosition::x` and `usvg_tree::CharacterPosition::y` are gone. + They were redundant and you should use `usvg_tree::TextChunk::x` + and `usvg_tree::TextChunk::y` instead. +- `usvg_tree::LinearGradient::id` and `usvg_tree::RadialGradient::id` are moved to + `usvg_tree::BaseGradient::id`. +- Do not generate element IDs during parsing. Previously, some elements like `clipPath`s + and `filter`s could have generated IDs, but it wasn't very reliable and mostly unnecessary. + Renderer doesn't rely on them and usvg writer would generate them anyway. +- Text-to-paths conversion via `usvg_text_layout::Tree::convert_text` no longer replaces + original text elements with paths, but instead puts them into `usvg_tree::Text::flattened`. + +### Removed +- The `transform` field from `usvg_tree::Path`, `usvg_tree::Image` and `usvg_tree::Text`. + Only `usvg_tree::Group` can have it.
+ It doesn't break anything, because those properties were never used before anyway.
+ Thanks to [@LaurenzV](https://github.com/LaurenzV). +- `usvg_tree::CharacterPosition` +- `usvg_tree::Path::text_bbox`. Use `usvg_tree::Text::bounding_box` instead. +- `usvg_text_layout::TextToPath` trait for `Text` nodes. + Only the whole tree can be converted at once. + +### Fixed +- Path object bounding box calculation. We were using point bounds instead of tight contour bounds. + Was broken since v0.34 +- Convert text-to-paths in embedded SVGs as well. The one inside the `Image` node. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- Indirect `text-decoration` resolving in some cases. + Thanks to [@LaurenzV](https://github.com/LaurenzV). +- (usvg) Clip paths writing to SVG. + Thanks to [@LaurenzV](https://github.com/LaurenzV). + ## [0.36.0] - 2023-10-01 ### Added - `stroke-linejoin=miter-clip` support. SVG2. @@ -945,7 +1132,12 @@ This changelog also contains important changes in dependencies. ### Fixed - `font-size` attribute inheritance during `use` resolving. -[Unreleased]: https://github.com/RazrFalcon/resvg/compare/v0.36.0...HEAD +[Unreleased]: https://github.com/RazrFalcon/resvg/compare/v0.41.0...HEAD +[0.41.0]: https://github.com/RazrFalcon/resvg/compare/v0.40.0...v0.41.0 +[0.40.0]: https://github.com/RazrFalcon/resvg/compare/v0.39.0...v0.40.0 +[0.39.0]: https://github.com/RazrFalcon/resvg/compare/v0.38.0...v0.39.0 +[0.38.0]: https://github.com/RazrFalcon/resvg/compare/v0.37.0...v0.38.0 +[0.37.0]: https://github.com/RazrFalcon/resvg/compare/v0.36.0...v0.37.0 [0.36.0]: https://github.com/RazrFalcon/resvg/compare/v0.35.0...v0.36.0 [0.35.0]: https://github.com/RazrFalcon/resvg/compare/v0.34.1...v0.35.0 [0.34.1]: https://github.com/RazrFalcon/resvg/compare/v0.34.0...v0.34.1 diff --git a/Cargo.lock b/Cargo.lock index 4fdfc4aa5..13721c6c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "base64" -version = "0.21.4" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" @@ -32,11 +32,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" [[package]] name = "cfg-if" @@ -52,33 +58,33 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "data-url" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -92,18 +98,18 @@ checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" [[package]] name = "fontconfig-parser" -version = "0.5.3" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "674e258f4b5d2dcd63888c01c68413c51f565e8af99d2f7701c7b81d79ef41c4" +checksum = "6a595cb550439a117696039dfc69830492058211b771a2a165379f2a1a53d84d" dependencies = [ "roxmltree", ] [[package]] name = "fontdb" -version = "0.15.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "fontconfig-parser", "log", @@ -115,9 +121,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" dependencies = [ "color_quant", "weezl", @@ -131,45 +137,46 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "jpeg-decoder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "kurbo" -version = "0.9.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" dependencies = [ "arrayvec", + "smallvec", ] [[package]] name = "libc" -version = "0.2.148" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memmap2" -version = "0.8.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", @@ -177,9 +184,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pico-args" @@ -189,26 +196,20 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "png" -version = "0.17.10" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] -[[package]] -name = "rctree" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" - [[package]] name = "resvg" -version = "0.36.0+class" +version = "0.41.0+class" dependencies = [ "gif", "jpeg-decoder", @@ -224,7 +225,7 @@ dependencies = [ [[package]] name = "resvg-capi" -version = "0.36.0+class" +version = "0.41.0+class" dependencies = [ "log", "resvg", @@ -232,29 +233,26 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" dependencies = [ "bytemuck", ] [[package]] name = "roxmltree" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" -dependencies = [ - "xmlparser", -] +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "rustybuzz" -version = "0.10.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cd15fef9112a1f94ac64b58d1e4628192631ad6af4dc69997f995459c874e7" +checksum = "88117946aa1bfb53c2ae0643ceac6506337f44887f8c9fbfb43587b1cc52ba49" dependencies = [ - "bitflags", + "bitflags 2.5.0", "bytemuck", "smallvec", "ttf-parser", @@ -281,24 +279,24 @@ dependencies = [ [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slotmap" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ "version_check", ] [[package]] name = "smallvec" -version = "1.11.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strict-num" @@ -311,9 +309,9 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.12.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71499ff2d42f59d26edb21369a308ede691421f79ebc0f001e2b1fd3a7c9e52" +checksum = "d97ca9a891c9c70da8139ac9d8e8ea36a210fa21bb50eccd75d4a9561c83e87f" dependencies = [ "kurbo", "siphasher", @@ -321,9 +319,9 @@ dependencies = [ [[package]] name = "tiny-skia" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b72a92a05db376db09fe6d50b7948d106011761c05a6a45e23e17ee9b556222" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" dependencies = [ "arrayref", "arrayvec", @@ -336,9 +334,9 @@ dependencies = [ [[package]] name = "tiny-skia-path" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac3865b9708fc7e1961a65c3a4fa55e984272f33092d3c859929f887fceb647" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ "arrayref", "bytemuck", @@ -362,39 +360,39 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "ttf-parser" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-bidi-mirroring" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" [[package]] name = "unicode-ccc" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-properties" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f91c8b21fbbaa18853c3d0801c78f4fc94cdb976699bb03e832e75f7fd22f0" +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" @@ -404,55 +402,28 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "usvg" -version = "0.36.0+class" +version = "0.41.0+class" dependencies = [ "base64", - "log", - "pico-args", - "usvg-parser", - "usvg-text-layout", - "usvg-tree", - "xmlwriter", -] - -[[package]] -name = "usvg-parser" -version = "0.36.0+class" -dependencies = [ "data-url", "flate2", + "fontdb", "imagesize", "kurbo", "log", + "once_cell", + "pico-args", "roxmltree", + "rustybuzz", "simplecss", "siphasher", + "strict-num", "svgtypes", - "usvg-tree", -] - -[[package]] -name = "usvg-text-layout" -version = "0.36.0+class" -dependencies = [ - "fontdb", - "kurbo", - "log", - "rustybuzz", + "tiny-skia-path", "unicode-bidi", "unicode-script", "unicode-vo", - "usvg-tree", -] - -[[package]] -name = "usvg-tree" -version = "0.36.0+class" -dependencies = [ - "rctree", - "strict-num", - "svgtypes", - "tiny-skia-path", + "xmlwriter", ] [[package]] @@ -463,15 +434,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - -[[package]] -name = "xmlparser" -version = "0.13.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "xmlwriter" diff --git a/Cargo.toml b/Cargo.toml index e5ff1b3b1..b2533dd0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,7 @@ members = [ "crates/resvg", "crates/usvg", - "crates/usvg-parser", - "crates/usvg-text-layout", - "crates/usvg-tree", "crates/c-api", ] - default-members = ["crates/resvg"] +resolver = "2" diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 000000000..636ec97db --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1 @@ +Copyright (c) 2017 Yevhenii Reizner diff --git a/README.md b/README.md index 54fcb6d4a..44470b791 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ## resvg -![Build Status](https://github.com/RazrFalcon/resvg/workflows/Rust/badge.svg) +![Build Status](https://github.com/RazrFalcon/resvg/workflows/Build/badge.svg) [![Crates.io](https://img.shields.io/crates/v/resvg.svg)](https://crates.io/crates/resvg) [![Documentation](https://docs.rs/resvg/badge.svg)](https://docs.rs/resvg) [![Rust 1.65+](https://img.shields.io/badge/rust-1.65+-orange.svg)](https://www.rust-lang.org) @@ -79,7 +79,7 @@ on ARM macOS - the produced image will be identical. Each pixel would have the s Nevertheless, native text rendering is optimized for small horizontal text, which is not that common is SVG. - Unicode-only
- It's the 21th century. Text files that aren't UTF-8 encoded are no longer relevant. + It's the 21st century. Text files that aren't UTF-8 encoded are no longer relevant. ## SVG support @@ -122,7 +122,7 @@ Here are some of them: - [rustybuzz] - a [harfbuzz](https://github.com/harfbuzz/harfbuzz) subset ported to Rust - [ttf-parser] - a TrueType/OpenType font parser - [fontdb] - a simple, in-memory font database with CSS-like queries -- [roxmltree] + [xmlparser] - an XML parsing libraries +- [roxmltree] - an XML parsing library - [simplecss] - a pretty decent CSS 2 parser and selector - [pico-args] - an absolutely minimal, but surprisingly popular command-line arguments parser @@ -139,7 +139,6 @@ It's definitely the smallest option out there. [tiny-skia]: https://github.com/RazrFalcon/tiny-skia [ttf-parser]: https://github.com/RazrFalcon/ttf-parser [roxmltree]: https://github.com/RazrFalcon/roxmltree -[xmlparser]: https://github.com/RazrFalcon/xmlparser [simplecss]: https://github.com/RazrFalcon/simplecss [fontdb]: https://github.com/RazrFalcon/fontdb [pico-args]: https://github.com/RazrFalcon/pico-args diff --git a/crates/c-api/Cargo.toml b/crates/c-api/Cargo.toml index bde027d81..ddfa3650a 100644 --- a/crates/c-api/Cargo.toml +++ b/crates/c-api/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "resvg-capi" -version = "0.36.0+class" +version = "0.41.0+class" authors = ["Yevhenii Reizner "] keywords = ["svg", "render", "raster", "c-api"] license = "MPL-2.0" -edition = "2018" +edition = "2021" workspace = "../.." [lib] diff --git a/crates/c-api/ResvgQt.h b/crates/c-api/ResvgQt.h index ca8430dc9..1a3f7d1c3 100644 --- a/crates/c-api/ResvgQt.h +++ b/crates/c-api/ResvgQt.h @@ -14,9 +14,9 @@ #define RESVG_QT_H #define RESVG_QT_MAJOR_VERSION 0 -#define RESVG_QT_MINOR_VERSION 36 +#define RESVG_QT_MINOR_VERSION 41 #define RESVG_QT_PATCH_VERSION 0 -#define RESVG_QT_VERSION "0.36.0" +#define RESVG_QT_VERSION "0.41.0" #include diff --git a/crates/c-api/lib.rs b/crates/c-api/lib.rs index 87dfe63fb..3bb64d3ad 100644 --- a/crates/c-api/lib.rs +++ b/crates/c-api/lib.rs @@ -13,9 +13,9 @@ use std::os::raw::c_char; use std::slice; use resvg::tiny_skia; -use resvg::usvg::{self, NodeExt, TreeParsing}; +use resvg::usvg; #[cfg(feature = "text")] -use resvg::usvg::{fontdb, TreeTextToPath}; +use resvg::usvg::fontdb; /// @brief List of possible errors. #[repr(C)] @@ -526,17 +526,18 @@ pub extern "C" fn resvg_parse_tree_from_file( Err(_) => return resvg_error::FILE_OPEN_FAILED as i32, }; - #[allow(unused_mut)] - let mut utree = match usvg::Tree::from_data(&file_data, &raw_opt.options) { + let utree = usvg::Tree::from_data( + &file_data, + &raw_opt.options, + #[cfg(feature = "text")] + &raw_opt.fontdb, + ); + + let utree = match utree { Ok(tree) => tree, Err(e) => return convert_error(e) as i32, }; - #[cfg(feature = "text")] - { - utree.convert_text(&raw_opt.fontdb); - } - let tree_box = Box::new(resvg_render_tree(utree)); unsafe { *tree = Box::into_raw(tree_box); @@ -568,17 +569,18 @@ pub extern "C" fn resvg_parse_tree_from_data( &*opt }; - #[allow(unused_mut)] - let mut utree = match usvg::Tree::from_data(data, &raw_opt.options) { + let utree = usvg::Tree::from_data( + data, + &raw_opt.options, + #[cfg(feature = "text")] + &raw_opt.fontdb, + ); + + let utree = match utree { Ok(tree) => tree, Err(e) => return convert_error(e) as i32, }; - #[cfg(feature = "text")] - { - utree.convert_text(&raw_opt.fontdb); - } - let tree_box = Box::new(resvg_render_tree(utree)); unsafe { *tree = Box::into_raw(tree_box); @@ -598,7 +600,7 @@ pub extern "C" fn resvg_is_image_empty(tree: *const resvg_render_tree) -> bool { &*tree }; - !tree.0.root.has_children() + !tree.0.root().has_children() } /// @brief Returns an image size. @@ -616,7 +618,7 @@ pub extern "C" fn resvg_get_image_size(tree: *const resvg_render_tree) -> resvg_ &*tree }; - let size = tree.0.size; + let size = tree.0.size(); resvg_size { width: size.width(), @@ -637,7 +639,7 @@ pub extern "C" fn resvg_get_image_viewbox(tree: *const resvg_render_tree) -> res &*tree }; - let r = tree.0.view_box.rect; + let r = tree.0.view_box().rect; resvg_rect { x: r.x(), @@ -664,12 +666,7 @@ pub extern "C" fn resvg_get_image_bbox( &*tree }; - if let Some(r) = tree - .0 - .root - .calculate_bbox() - .and_then(|r| r.to_non_zero_rect()) - { + if let Some(r) = tree.0.root().abs_bounding_box().to_non_zero_rect() { unsafe { *bbox = resvg_rect { x: r.x(), @@ -757,7 +754,7 @@ pub extern "C" fn resvg_get_node_transform( false } -/// @brief Returns node's bounding box by ID. +/// @brief Returns node's bounding box in canvas coordinates by ID. /// /// @param tree Render tree. /// @param id Node's ID. Must not be NULL. @@ -770,6 +767,32 @@ pub extern "C" fn resvg_get_node_bbox( tree: *const resvg_render_tree, id: *const c_char, bbox: *mut resvg_rect, +) -> bool { + get_node_bbox(tree, id, bbox, &|node| node.abs_bounding_box()) +} + +/// @brief Returns node's bounding box, including stroke, in canvas coordinates by ID. +/// +/// @param tree Render tree. +/// @param id Node's ID. Must not be NULL. +/// @param bbox Node's bounding box. +/// @return `false` if a node with such an ID does not exist +/// @return `false` if ID isn't a UTF-8 string. +/// @return `false` if ID is an empty string +#[no_mangle] +pub extern "C" fn resvg_get_node_stroke_bbox( + tree: *const resvg_render_tree, + id: *const c_char, + bbox: *mut resvg_rect, +) -> bool { + get_node_bbox(tree, id, bbox, &|node| node.abs_stroke_bounding_box()) +} + +fn get_node_bbox( + tree: *const resvg_render_tree, + id: *const c_char, + bbox: *mut resvg_rect, + f: &dyn Fn(&usvg::Node) -> usvg::Rect, ) -> bool { let id = match cstr_to_str(id) { Some(v) => v, @@ -791,20 +814,16 @@ pub extern "C" fn resvg_get_node_bbox( match tree.0.node_by_id(id) { Some(node) => { - if let Some(r) = node.calculate_bbox() { - unsafe { - *bbox = resvg_rect { - x: r.x(), - y: r.y(), - width: r.width(), - height: r.height(), - } + let r = f(node); + unsafe { + *bbox = resvg_rect { + x: r.x(), + y: r.y(), + width: r.width(), + height: r.height(), } - - true - } else { - false } + true } None => { log::warn!("No node with '{}' ID is in the tree.", id); @@ -867,8 +886,7 @@ pub extern "C" fn resvg_render( unsafe { std::slice::from_raw_parts_mut(pixmap as *mut u8, pixmap_len) }; let mut pixmap = tiny_skia::PixmapMut::from_bytes(pixmap, width, height).unwrap(); - let rtree = resvg::Tree::from_usvg(&tree.0); - rtree.render(transform.to_tiny_skia(), &mut pixmap); + resvg::render(&tree.0, transform.to_tiny_skia(), &mut pixmap) } /// @brief Renders a Node by ID onto the image. @@ -913,12 +931,7 @@ pub extern "C" fn resvg_render_node( unsafe { std::slice::from_raw_parts_mut(pixmap as *mut u8, pixmap_len) }; let mut pixmap = tiny_skia::PixmapMut::from_bytes(pixmap, width, height).unwrap(); - if let Some(rtree) = resvg::Tree::from_usvg_node(&node) { - rtree.render(transform.to_tiny_skia(), &mut pixmap); - true - } else { - false - } + resvg::render_node(node, transform.to_tiny_skia(), &mut pixmap).is_some() } else { log::warn!("A node with '{}' ID wasn't found.", id); false @@ -942,15 +955,14 @@ impl log::Log for SimpleLogger { }; let line = record.line().unwrap_or(0); + let args = record.args(); match record.level() { - log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, record.args()), - log::Level::Warn => { - eprintln!("Warning (in {}:{}): {}", target, line, record.args()) - } - log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, record.args()), - log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, record.args()), - log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, record.args()), + log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args), + log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args), + log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args), + log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args), + log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args), } } } diff --git a/crates/c-api/resvg.h b/crates/c-api/resvg.h index ed3c0593a..bcdffaea4 100644 --- a/crates/c-api/resvg.h +++ b/crates/c-api/resvg.h @@ -17,9 +17,9 @@ #include #define RESVG_MAJOR_VERSION 0 -#define RESVG_MINOR_VERSION 36 +#define RESVG_MINOR_VERSION 41 #define RESVG_PATCH_VERSION 0 -#define RESVG_VERSION "0.36.0" +#define RESVG_VERSION "0.41.0" /** * @brief List of possible errors. @@ -431,7 +431,7 @@ bool resvg_get_node_transform(const resvg_render_tree *tree, resvg_transform *transform); /** - * @brief Returns node's bounding box by ID. + * @brief Returns node's bounding box in canvas coordinates by ID. * * @param tree Render tree. * @param id Node's ID. Must not be NULL. @@ -442,6 +442,18 @@ bool resvg_get_node_transform(const resvg_render_tree *tree, */ bool resvg_get_node_bbox(const resvg_render_tree *tree, const char *id, resvg_rect *bbox); +/** + * @brief Returns node's bounding box, including stroke, in canvas coordinates by ID. + * + * @param tree Render tree. + * @param id Node's ID. Must not be NULL. + * @param bbox Node's bounding box. + * @return `false` if a node with such an ID does not exist + * @return `false` if ID isn't a UTF-8 string. + * @return `false` if ID is an empty string + */ +bool resvg_get_node_stroke_bbox(const resvg_render_tree *tree, const char *id, resvg_rect *bbox); + /** * @brief Destroys the #resvg_render_tree. */ diff --git a/crates/resvg/Cargo.toml b/crates/resvg/Cargo.toml index 65fe8c601..fef7849d4 100644 --- a/crates/resvg/Cargo.toml +++ b/crates/resvg/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "resvg" -version = "0.36.0+class" +version = "0.41.0+class" authors = ["Yevhenii Reizner "] keywords = ["svg", "render", "raster"] license = "MPL-2.0" -edition = "2018" +edition = "2021" description = "An SVG rendering library." repository = "https://github.com/RazrFalcon/resvg" exclude = ["tests"] @@ -15,15 +15,15 @@ name = "resvg" required-features = ["text", "system-fonts", "memmap-fonts"] [dependencies] -gif = { version = "0.12", optional = true } +gif = { version = "0.13", optional = true } jpeg-decoder = { version = "0.3", default-features = false, features = ["platform_independent"], optional = true } log = "0.4" pico-args = { version = "0.5", features = ["eq-separator"] } png = { version = "0.17", optional = true } rgb = "0.8" -svgtypes = "0.12" -tiny-skia = "0.11.2" -usvg = { path = "../usvg", version = "0.36.0+class", default-features = false } +svgtypes = "0.15.0" +tiny-skia = "0.11.4" +usvg = { path = "../usvg", version = "0.41.0+class", default-features = false } [dev-dependencies] once_cell = "1.5" diff --git a/crates/resvg/examples/bboxes.svg b/crates/resvg/examples/bboxes.svg index b88a255ac..30c9622b0 100644 --- a/crates/resvg/examples/bboxes.svg +++ b/crates/resvg/examples/bboxes.svg @@ -1,4 +1,4 @@ - + Simple rect @@ -14,7 +14,10 @@ Rect with transform - THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG the quick brown fox jumps over the lazy dog + Circle with transform + + + THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG the quick brown fox jumps over the lazy dog Simple text Text diff --git a/crates/resvg/examples/custom_href_resolver.rs b/crates/resvg/examples/custom_href_resolver.rs index 7e6544dbe..c7015ceb6 100644 --- a/crates/resvg/examples/custom_href_resolver.rs +++ b/crates/resvg/examples/custom_href_resolver.rs @@ -1,19 +1,21 @@ -use usvg::TreeParsing; - fn main() { let mut opt = usvg::Options::default(); let ferris_image = std::sync::Arc::new(std::fs::read("./examples/ferris.png").unwrap()); // We know that our SVG won't have DataUrl hrefs, just return None for such case. - let resolve_data = Box::new(|_: &str, _: std::sync::Arc>, _: &usvg::Options| None); + let resolve_data = Box::new( + |_: &str, _: std::sync::Arc>, _: &usvg::Options, _: &usvg::fontdb::Database| None, + ); // Here we handle xlink:href attribute as string, // let's use already loaded Ferris image to match that string. - let resolve_string = Box::new(move |href: &str, _: &usvg::Options| match href { - "ferris_image" => Some(usvg::ImageKind::PNG(ferris_image.clone())), - _ => None, - }); + let resolve_string = Box::new( + move |href: &str, _: &usvg::Options, _: &usvg::fontdb::Database| match href { + "ferris_image" => Some(usvg::ImageKind::PNG(ferris_image.clone())), + _ => None, + }, + ); // Assign new ImageHrefResolver option using our closures. opt.image_href_resolver = usvg::ImageHrefResolver { @@ -21,14 +23,15 @@ fn main() { resolve_string, }; + let fontdb = usvg::fontdb::Database::new(); + let svg_data = std::fs::read("./examples/custom_href_resolver.svg").unwrap(); - let tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); - let rtree = resvg::Tree::from_usvg(&tree); + let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap(); - let pixmap_size = rtree.size.to_int_size(); + let pixmap_size = tree.size().to_int_size(); let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); - rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); + resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut()); pixmap.save_png("custom_href_resolver.png").unwrap(); } diff --git a/crates/resvg/examples/custom_usvg_tree.rs b/crates/resvg/examples/custom_usvg_tree.rs deleted file mode 100644 index 2153f817f..000000000 --- a/crates/resvg/examples/custom_usvg_tree.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::rc::Rc; - -fn main() { - let size = usvg::Size::from_wh(200.0, 200.0).unwrap(); - let tree = usvg::Tree { - size, - view_box: usvg::ViewBox { - rect: size.to_non_zero_rect(0.0, 0.0), - aspect: usvg::AspectRatio::default(), - }, - root: usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default())), - }; - - let gradient = usvg::LinearGradient { - id: "lg1".into(), - x1: 0.0, - y1: 0.0, - x2: 1.0, - y2: 0.0, - base: usvg::BaseGradient { - units: usvg::Units::ObjectBoundingBox, - transform: usvg::Transform::default(), - spread_method: usvg::SpreadMethod::Pad, - stops: vec![ - usvg::Stop { - offset: usvg::StopOffset::ZERO, - color: usvg::Color::new_rgb(0, 255, 0), - opacity: usvg::Opacity::ONE, - }, - usvg::Stop { - offset: usvg::StopOffset::ONE, - color: usvg::Color::new_rgb(0, 255, 0), - opacity: usvg::Opacity::ZERO, - }, - ], - }, - }; - - let fill = Some(usvg::Fill { - paint: usvg::Paint::LinearGradient(Rc::new(gradient)), - ..usvg::Fill::default() - }); - - let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect( - tiny_skia::Rect::from_xywh(20.0, 20.0, 160.0, 160.0).unwrap(), - ))); - path.fill = fill; - - let rtree = resvg::Tree::from_usvg(&tree); - - let pixmap_size = rtree.size.to_int_size(); - let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); - rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); - pixmap.save_png("out.png").unwrap(); -} diff --git a/crates/resvg/examples/draw_bboxes.rs b/crates/resvg/examples/draw_bboxes.rs index 1e8dbb115..4a279f352 100644 --- a/crates/resvg/examples/draw_bboxes.rs +++ b/crates/resvg/examples/draw_bboxes.rs @@ -1,6 +1,4 @@ -use std::rc::Rc; - -use usvg::{fontdb, NodeExt, TreeParsing, TreeTextToPath}; +use usvg::fontdb; fn main() { let args: Vec = std::env::args().collect(); @@ -24,59 +22,63 @@ fn main() { opt.resources_dir = std::fs::canonicalize(&args[1]) .ok() .and_then(|p| p.parent().map(|p| p.to_path_buf())); - // let fit_to = resvg::FitTo::Zoom(zoom); let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); let svg_data = std::fs::read(&args[1]).unwrap(); - let mut tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); - tree.convert_text(&fontdb); + let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap(); let mut bboxes = Vec::new(); - let mut text_bboxes = Vec::new(); - for node in tree.root.descendants() { - if let Some(bbox) = node.calculate_bbox() { - bboxes.push(bbox); - } + let mut stroke_bboxes = Vec::new(); + collect_bboxes(tree.root(), &mut bboxes, &mut stroke_bboxes); - // Text bboxes are different from path bboxes. - if let usvg::NodeKind::Path(ref path) = *node.borrow() { - if let Some(ref bbox) = path.text_bbox { - text_bboxes.push(bbox.to_rect()); - } - } - } + let pixmap_size = tree.size().to_int_size().scale_by(zoom).unwrap(); + let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); + let render_ts = tiny_skia::Transform::from_scale(zoom, zoom); + resvg::render(&tree, render_ts, &mut pixmap.as_mut()); + + let mut stroke = tiny_skia::Stroke::default(); + stroke.width = 1.0 / zoom; // prevent stroke scaling as well - let stroke = Some(usvg::Stroke { - paint: usvg::Paint::Color(usvg::Color::new_rgb(255, 0, 0)), - opacity: usvg::Opacity::new_clamped(0.5), - ..usvg::Stroke::default() - }); + let mut paint1 = tiny_skia::Paint::default(); + paint1.set_color_rgba8(255, 0, 0, 127); - let stroke2 = Some(usvg::Stroke { - paint: usvg::Paint::Color(usvg::Color::new_rgb(0, 0, 200)), - opacity: usvg::Opacity::new_clamped(0.5), - ..usvg::Stroke::default() - }); + let mut paint2 = tiny_skia::Paint::default(); + paint2.set_color_rgba8(0, 200, 0, 127); + + let root_ts = tree.view_box().to_transform(tree.size()); + let bbox_ts = render_ts.pre_concat(root_ts); for bbox in bboxes { - let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox))); - path.stroke = stroke.clone(); - tree.root.append_kind(usvg::NodeKind::Path(path)); + let path = tiny_skia::PathBuilder::from_rect(bbox); + pixmap.stroke_path(&path, &paint1, &stroke, bbox_ts, None); } - for bbox in text_bboxes { - let mut path = usvg::Path::new(Rc::new(tiny_skia::PathBuilder::from_rect(bbox))); - path.stroke = stroke2.clone(); - tree.root.append_kind(usvg::NodeKind::Path(path)); + for bbox in stroke_bboxes { + let path = tiny_skia::PathBuilder::from_rect(bbox); + pixmap.stroke_path(&path, &paint2, &stroke, bbox_ts, None); } - let rtree = resvg::Tree::from_usvg(&tree); - - let pixmap_size = rtree.size.to_int_size().scale_by(zoom).unwrap(); - let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); - let render_ts = tiny_skia::Transform::from_scale(zoom, zoom); - rtree.render(render_ts, &mut pixmap.as_mut()); pixmap.save_png(&args[2]).unwrap(); } + +fn collect_bboxes( + parent: &usvg::Group, + bboxes: &mut Vec, + stroke_bboxes: &mut Vec, +) { + for node in parent.children() { + if let usvg::Node::Group(ref group) = node { + collect_bboxes(group, bboxes, stroke_bboxes); + } + + let bbox = node.abs_bounding_box(); + bboxes.push(bbox); + + let stroke_bbox = node.abs_stroke_bounding_box(); + if bbox != stroke_bbox { + stroke_bboxes.push(stroke_bbox); + } + } +} diff --git a/crates/resvg/examples/minimal.rs b/crates/resvg/examples/minimal.rs index 30c52f108..a3d64d944 100644 --- a/crates/resvg/examples/minimal.rs +++ b/crates/resvg/examples/minimal.rs @@ -1,4 +1,4 @@ -use usvg::{fontdb, TreeParsing, TreeTextToPath}; +use usvg::fontdb; fn main() { let args: Vec = std::env::args().collect(); @@ -7,9 +7,7 @@ fn main() { return; } - // resvg::Tree own all the required data and does not require - // the input file, usvg::Tree or anything else. - let rtree = { + let tree = { let mut opt = usvg::Options::default(); // Get file's absolute directory. opt.resources_dir = std::fs::canonicalize(&args[1]) @@ -20,13 +18,11 @@ fn main() { fontdb.load_system_fonts(); let svg_data = std::fs::read(&args[1]).unwrap(); - let mut tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); - tree.convert_text(&fontdb); - resvg::Tree::from_usvg(&tree) + usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap() }; - let pixmap_size = rtree.size.to_int_size(); + let pixmap_size = tree.size().to_int_size(); let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); - rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); + resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut()); pixmap.save_png(&args[2]).unwrap(); } diff --git a/crates/resvg/src/clip.rs b/crates/resvg/src/clip.rs index 69bd6ea64..2f36a6a8c 100644 --- a/crates/resvg/src/clip.rs +++ b/crates/resvg/src/clip.rs @@ -2,54 +2,24 @@ // 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/. -use std::rc::Rc; - use crate::render::Context; -use crate::tree::{Node, OptionLog}; - -pub struct ClipPath { - pub transform: tiny_skia::Transform, - pub clip_path: Option>, - pub children: Vec, -} - -pub fn convert( - upath: Option>, - object_bbox: tiny_skia::Rect, -) -> Option { - let upath = upath?; - - let mut transform = upath.transform; - if upath.units == usvg::Units::ObjectBoundingBox { - let object_bbox = object_bbox - .to_non_zero_rect() - .log_none(|| log::warn!("Clipping of zero-sized shapes is not allowed."))?; - - let ts = usvg::Transform::from_bbox(object_bbox); - transform = transform.pre_concat(ts); - } - - let (children, _) = crate::tree::convert_node(upath.root.clone()); - Some(ClipPath { - transform, - clip_path: convert(upath.clip_path.clone(), object_bbox).map(Box::new), - children, - }) -} - -pub fn apply(clip: &ClipPath, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::Pixmap) { +pub fn apply( + clip: &usvg::ClipPath, + transform: tiny_skia::Transform, + pixmap: &mut tiny_skia::Pixmap, +) { let mut clip_pixmap = tiny_skia::Pixmap::new(pixmap.width(), pixmap.height()).unwrap(); clip_pixmap.fill(tiny_skia::Color::BLACK); draw_children( - &clip.children, + clip.root(), tiny_skia::BlendMode::Clear, - transform.pre_concat(clip.transform), + transform.pre_concat(clip.transform()), &mut clip_pixmap.as_mut(), ); - if let Some(ref clip) = clip.clip_path { + if let Some(clip) = clip.clip_path() { apply(clip, transform, pixmap); } @@ -59,31 +29,38 @@ pub fn apply(clip: &ClipPath, transform: tiny_skia::Transform, pixmap: &mut tiny } fn draw_children( - children: &[Node], + parent: &usvg::Group, mode: tiny_skia::BlendMode, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) { - for child in children { + for child in parent.children() { match child { - Node::FillPath(ref path) => { + usvg::Node::Path(ref path) => { + if path.visibility() != usvg::Visibility::Visible { + continue; + } + // We could use any values here. They will not be used anyway. let ctx = Context { max_bbox: tiny_skia::IntRect::from_xywh(0, 0, 1, 1).unwrap(), }; - crate::path::render_fill_path(path, mode, &ctx, transform, pixmap); + crate::path::fill_path(path, mode, &ctx, transform, pixmap); + } + usvg::Node::Text(ref text) => { + draw_children(text.flattened(), mode, transform, pixmap); } - Node::Group(ref group) => { - let transform = transform.pre_concat(group.transform); + usvg::Node::Group(ref group) => { + let transform = transform.pre_concat(group.transform()); - if let Some(ref clip) = group.clip_path { + if let Some(clip) = group.clip_path() { // If a `clipPath` child also has a `clip-path` // then we should render this child on a new canvas, // clip it, and only then draw it to the `clipPath`. - clip_group(&group.children, clip, transform, pixmap); + clip_group(group, clip, transform, pixmap); } else { - draw_children(&group.children, mode, transform, pixmap); + draw_children(group, mode, transform, pixmap); } } _ => {} @@ -92,8 +69,8 @@ fn draw_children( } fn clip_group( - children: &[Node], - clip: &ClipPath, + children: &usvg::Group, + clip: &usvg::ClipPath, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) -> Option<()> { diff --git a/crates/resvg/src/filter/component_transfer.rs b/crates/resvg/src/filter/component_transfer.rs index b10a5335c..b56af6437 100644 --- a/crates/resvg/src/filter/component_transfer.rs +++ b/crates/resvg/src/filter/component_transfer.rs @@ -10,20 +10,20 @@ use usvg::filter::{ComponentTransfer, TransferFunction}; /// Input image pixels should have an **unpremultiplied alpha**. pub fn apply(fe: &ComponentTransfer, src: ImageRefMut) { for pixel in src.data { - if !is_dummy(&fe.func_r) { - pixel.r = transfer(&fe.func_r, pixel.r); + if !is_dummy(fe.func_r()) { + pixel.r = transfer(fe.func_r(), pixel.r); } - if !is_dummy(&fe.func_b) { - pixel.b = transfer(&fe.func_b, pixel.b); + if !is_dummy(fe.func_b()) { + pixel.b = transfer(fe.func_b(), pixel.b); } - if !is_dummy(&fe.func_g) { - pixel.g = transfer(&fe.func_g, pixel.g); + if !is_dummy(fe.func_g()) { + pixel.g = transfer(fe.func_g(), pixel.g); } - if !is_dummy(&fe.func_a) { - pixel.a = transfer(&fe.func_a, pixel.a); + if !is_dummy(fe.func_a()) { + pixel.a = transfer(fe.func_a(), pixel.a); } } } diff --git a/crates/resvg/src/filter/convolve_matrix.rs b/crates/resvg/src/filter/convolve_matrix.rs index 49695ecfc..fca9e9c5f 100644 --- a/crates/resvg/src/filter/convolve_matrix.rs +++ b/crates/resvg/src/filter/convolve_matrix.rs @@ -30,12 +30,12 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) { let mut new_g = 0.0; let mut new_b = 0.0; let mut new_a = 0.0; - for oy in 0..matrix.matrix.rows { - for ox in 0..matrix.matrix.columns { - let mut tx = x as i32 - matrix.matrix.target_x as i32 + ox as i32; - let mut ty = y as i32 - matrix.matrix.target_y as i32 + oy as i32; + for oy in 0..matrix.matrix().rows() { + for ox in 0..matrix.matrix().columns() { + let mut tx = x as i32 - matrix.matrix().target_x() as i32 + ox as i32; + let mut ty = y as i32 - matrix.matrix().target_y() as i32 + oy as i32; - match matrix.edge_mode { + match matrix.edge_mode() { EdgeMode::None => { if tx < 0 || tx > width_max || ty < 0 || ty > height_max { continue; @@ -58,33 +58,34 @@ pub fn apply(matrix: &ConvolveMatrix, src: ImageRefMut) { } } - let k = matrix - .matrix - .get(matrix.matrix.columns - ox - 1, matrix.matrix.rows - oy - 1); + let k = matrix.matrix().get( + matrix.matrix().columns() - ox - 1, + matrix.matrix().rows() - oy - 1, + ); let p = src.pixel_at(tx as u32, ty as u32); new_r += (p.r as f32) / 255.0 * k; new_g += (p.g as f32) / 255.0 * k; new_b += (p.b as f32) / 255.0 * k; - if !matrix.preserve_alpha { + if !matrix.preserve_alpha() { new_a += (p.a as f32) / 255.0 * k; } } } - if matrix.preserve_alpha { + if matrix.preserve_alpha() { new_a = in_p.a as f32 / 255.0; } else { - new_a = new_a / matrix.divisor.get() + matrix.bias; + new_a = new_a / matrix.divisor().get() + matrix.bias(); } let bounded_new_a = f32_bound(0.0, new_a, 1.0); let calc = |x| { - let x = x / matrix.divisor.get() + matrix.bias * new_a; + let x = x / matrix.divisor().get() + matrix.bias() * new_a; - let x = if matrix.preserve_alpha { + let x = if matrix.preserve_alpha() { f32_bound(0.0, x, 1.0) * bounded_new_a } else { f32_bound(0.0, x, bounded_new_a) diff --git a/crates/resvg/src/filter/displacement_map.rs b/crates/resvg/src/filter/displacement_map.rs index 64dce3195..545a54418 100644 --- a/crates/resvg/src/filter/displacement_map.rs +++ b/crates/resvg/src/filter/displacement_map.rs @@ -43,10 +43,10 @@ pub fn apply( c as f32 / 255.0 - 0.5 }; - let dx = calc_offset(fe.x_channel_selector); - let dy = calc_offset(fe.y_channel_selector); - let ox = (x as f32 + dx * sx * fe.scale).round() as i32; - let oy = (y as f32 + dy * sy * fe.scale).round() as i32; + let dx = calc_offset(fe.x_channel_selector()); + let dy = calc_offset(fe.y_channel_selector()); + let ox = (x as f32 + dx * sx * fe.scale()).round() as i32; + let oy = (y as f32 + dy * sy * fe.scale()).round() as i32; // TODO: we should use some kind of anti-aliasing when offset is on a pixel border diff --git a/crates/resvg/src/filter/lighting.rs b/crates/resvg/src/filter/lighting.rs index a55192ba3..d6be6b71a 100644 --- a/crates/resvg/src/filter/lighting.rs +++ b/crates/resvg/src/filter/lighting.rs @@ -144,7 +144,7 @@ pub fn diffuse_lighting( let k = if normal.normal.approx_zero() { light_vector.z } else { - let mut n = normal.normal * (fe.surface_scale / 255.0); + let mut n = normal.normal * (fe.surface_scale() / 255.0); n.x *= normal.factor.x; n.y *= normal.factor.y; @@ -153,13 +153,13 @@ pub fn diffuse_lighting( normal.dot(&light_vector) / normal.length() }; - fe.diffuse_constant * k + fe.diffuse_constant() * k }; apply( light_source, - fe.surface_scale, - fe.lighting_color, + fe.surface_scale(), + fe.lighting_color(), &light_factor, calc_diffuse_alpha, src, @@ -195,33 +195,33 @@ pub fn specular_lighting( let k = if normal.normal.approx_zero() { let n_dot_h = h.z / h_length; - if fe.specular_exponent.approx_eq_ulps(&1.0, 4) { + if fe.specular_exponent().approx_eq_ulps(&1.0, 4) { n_dot_h } else { - n_dot_h.powf(fe.specular_exponent) + n_dot_h.powf(fe.specular_exponent()) } } else { - let mut n = normal.normal * (fe.surface_scale / 255.0); + let mut n = normal.normal * (fe.surface_scale() / 255.0); n.x *= normal.factor.x; n.y *= normal.factor.y; let normal = Vector3::new(n.x, n.y, 1.0); let n_dot_h = normal.dot(&h) / normal.length() / h_length; - if fe.specular_exponent.approx_eq_ulps(&1.0, 4) { + if fe.specular_exponent().approx_eq_ulps(&1.0, 4) { n_dot_h } else { - n_dot_h.powf(fe.specular_exponent) + n_dot_h.powf(fe.specular_exponent()) } }; - fe.specular_constant * k + fe.specular_constant() * k }; apply( light_source, - fe.surface_scale, - fe.lighting_color, + fe.surface_scale(), + fe.lighting_color(), &light_factor, calc_specular_alpha, src, diff --git a/crates/resvg/src/filter/mod.rs b/crates/resvg/src/filter/mod.rs index 3a07c11a9..24b3310a0 100644 --- a/crates/resvg/src/filter/mod.rs +++ b/crates/resvg/src/filter/mod.rs @@ -8,8 +8,6 @@ use rgb::{FromSlice, RGBA8}; use tiny_skia::IntRect; use usvg::{ApproxEqUlps, ApproxZeroUlps}; -use crate::tree::Node; - mod box_blur; mod color_matrix; mod component_transfer; @@ -86,134 +84,6 @@ impl<'a> ImageRefMut<'a> { } } -pub struct Primitive { - pub region: tiny_skia::NonZeroRect, - pub color_interpolation: usvg::filter::ColorInterpolation, - pub result: String, - pub kind: usvg::filter::Kind, -} - -pub struct Filter { - pub region: tiny_skia::NonZeroRect, - pub primitives: Vec, -} - -pub fn convert( - ufilters: &[Rc], - object_bbox: Option, -) -> (Vec, Option) { - let object_bbox = object_bbox.and_then(|bbox| bbox.to_non_zero_rect()); - - let region = match calc_filters_region(ufilters, object_bbox) { - Some(v) => v, - None => return (Vec::new(), None), - }; - - let mut filters = Vec::new(); - for ufilter in ufilters { - let filter = match convert_filter(ufilter, object_bbox, region) { - Some(v) => v, - None => return (Vec::new(), None), - }; - filters.push(filter); - } - - (filters, Some(region.to_rect())) -} - -fn convert_filter( - ufilter: &usvg::filter::Filter, - object_bbox: Option, - region: tiny_skia::NonZeroRect, -) -> Option { - let mut primitives = Vec::with_capacity(ufilter.primitives.len()); - for uprimitive in &ufilter.primitives { - let subregion = match calc_subregion(ufilter, uprimitive, object_bbox, region) { - Some(v) => v, - None => { - log::warn!("Invalid filter primitive region."); - continue; - } - }; - - if let Some(kind) = convert_primitive(uprimitive, ufilter.primitive_units, object_bbox) { - primitives.push(Primitive { - region: subregion, - color_interpolation: uprimitive.color_interpolation, - result: uprimitive.result.clone(), - kind, - }); - } - } - - Some(Filter { region, primitives }) -} - -fn convert_primitive( - uprimitive: &usvg::filter::Primitive, - units: usvg::Units, - object_bbox: Option, -) -> Option { - match uprimitive.kind { - usvg::filter::Kind::DisplacementMap(ref fe) => { - let (sx, _) = scale_coordinates(fe.scale, fe.scale, units, object_bbox)?; - Some(usvg::filter::Kind::DisplacementMap( - usvg::filter::DisplacementMap { - input1: fe.input1.clone(), - input2: fe.input2.clone(), - scale: sx, - x_channel_selector: fe.x_channel_selector, - y_channel_selector: fe.y_channel_selector, - }, - )) - } - usvg::filter::Kind::DropShadow(ref fe) => { - let (dx, dy) = scale_coordinates(fe.dx, fe.dy, units, object_bbox)?; - let (std_dev_x, std_dev_y) = - scale_coordinates(fe.std_dev_x.get(), fe.std_dev_y.get(), units, object_bbox)?; - Some(usvg::filter::Kind::DropShadow(usvg::filter::DropShadow { - input: fe.input.clone(), - dx, - dy, - std_dev_x: usvg::PositiveF32::new(std_dev_x).unwrap_or_default(), - std_dev_y: usvg::PositiveF32::new(std_dev_y).unwrap_or_default(), - color: fe.color, - opacity: fe.opacity, - })) - } - usvg::filter::Kind::GaussianBlur(ref fe) => { - let (std_dev_x, std_dev_y) = - scale_coordinates(fe.std_dev_x.get(), fe.std_dev_y.get(), units, object_bbox)?; - Some(usvg::filter::Kind::GaussianBlur( - usvg::filter::GaussianBlur { - input: fe.input.clone(), - std_dev_x: usvg::PositiveF32::new(std_dev_x).unwrap_or_default(), - std_dev_y: usvg::PositiveF32::new(std_dev_y).unwrap_or_default(), - }, - )) - } - usvg::filter::Kind::Morphology(ref fe) => { - let (radius_x, radius_y) = - scale_coordinates(fe.radius_x.get(), fe.radius_y.get(), units, object_bbox)?; - Some(usvg::filter::Kind::Morphology(usvg::filter::Morphology { - input: fe.input.clone(), - operator: fe.operator, - radius_x: usvg::PositiveF32::new(radius_x).unwrap_or_default(), - radius_y: usvg::PositiveF32::new(radius_y).unwrap_or_default(), - })) - } - usvg::filter::Kind::Offset(ref fe) => { - let (dx, dy) = scale_coordinates(fe.dx, fe.dy, units, object_bbox)?; - Some(usvg::filter::Kind::Offset(usvg::filter::Offset { - input: fe.input.clone(), - dx, - dy, - })) - } - _ => Some(uprimitive.kind.clone()), - } -} - #[derive(Debug)] pub(crate) enum Error { InvalidRegion, @@ -463,7 +333,11 @@ struct FilterResult { image: Image, } -pub fn apply(filter: &Filter, ts: tiny_skia::Transform, source: &mut tiny_skia::Pixmap) { +pub fn apply( + filter: &usvg::filter::Filter, + ts: tiny_skia::Transform, + source: &mut tiny_skia::Pixmap, +) { let result = apply_inner(filter, ts, source); let result = result.and_then(|image| apply_to_canvas(image, source)); @@ -482,93 +356,94 @@ pub fn apply(filter: &Filter, ts: tiny_skia::Transform, source: &mut tiny_skia:: } fn apply_inner( - filter: &Filter, + filter: &usvg::filter::Filter, ts: usvg::Transform, source: &mut tiny_skia::Pixmap, ) -> Result { - let mut results: Vec = Vec::new(); - let region = filter - .region + .rect() .transform(ts) .map(|r| r.to_int_rect()) .ok_or(Error::InvalidRegion)?; - for primitive in &filter.primitives { - let cs = primitive.color_interpolation; + let mut results: Vec = Vec::new(); + + for primitive in filter.primitives() { let mut subregion = primitive - .region + .rect() .transform(ts) .map(|r| r.to_int_rect()) .ok_or(Error::InvalidRegion)?; // `feOffset` inherits its region from the input. - if let usvg::filter::Kind::Offset(ref fe) = primitive.kind { - if let usvg::filter::Input::Reference(ref name) = fe.input { + if let usvg::filter::Kind::Offset(ref fe) = primitive.kind() { + if let usvg::filter::Input::Reference(ref name) = fe.input() { if let Some(res) = results.iter().rev().find(|v| v.name == *name) { subregion = res.image.region; } } } - let mut result = match primitive.kind { + let cs = primitive.color_interpolation(); + + let mut result = match primitive.kind() { usvg::filter::Kind::Blend(ref fe) => { - let input1 = get_input(&fe.input1, region, source, &results)?; - let input2 = get_input(&fe.input2, region, source, &results)?; + let input1 = get_input(fe.input1(), region, source, &results)?; + let input2 = get_input(fe.input2(), region, source, &results)?; apply_blend(fe, cs, region, input1, input2) } usvg::filter::Kind::DropShadow(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_drop_shadow(fe, cs, ts, input) } usvg::filter::Kind::Flood(ref fe) => apply_flood(fe, region), usvg::filter::Kind::GaussianBlur(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_blur(fe, cs, ts, input) } usvg::filter::Kind::Offset(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_offset(fe, ts, input) } usvg::filter::Kind::Composite(ref fe) => { - let input1 = get_input(&fe.input1, region, source, &results)?; - let input2 = get_input(&fe.input2, region, source, &results)?; + let input1 = get_input(fe.input1(), region, source, &results)?; + let input2 = get_input(fe.input2(), region, source, &results)?; apply_composite(fe, cs, region, input1, input2) } usvg::filter::Kind::Merge(ref fe) => apply_merge(fe, cs, region, source, &results), usvg::filter::Kind::Tile(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_tile(input, region) } usvg::filter::Kind::Image(ref fe) => apply_image(fe, region, subregion, ts), usvg::filter::Kind::ComponentTransfer(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_component_transfer(fe, cs, input) } usvg::filter::Kind::ColorMatrix(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_color_matrix(fe, cs, input) } usvg::filter::Kind::ConvolveMatrix(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_convolve_matrix(fe, cs, input) } usvg::filter::Kind::Morphology(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_morphology(fe, cs, ts, input) } usvg::filter::Kind::DisplacementMap(ref fe) => { - let input1 = get_input(&fe.input1, region, source, &results)?; - let input2 = get_input(&fe.input2, region, source, &results)?; + let input1 = get_input(fe.input1(), region, source, &results)?; + let input2 = get_input(fe.input2(), region, source, &results)?; apply_displacement_map(fe, region, cs, ts, input1, input2) } usvg::filter::Kind::Turbulence(ref fe) => apply_turbulence(fe, region, cs, ts), usvg::filter::Kind::DiffuseLighting(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_diffuse_lighting(fe, region, cs, ts, input) } usvg::filter::Kind::SpecularLighting(ref fe) => { - let input = get_input(&fe.input, region, source, &results)?; + let input = get_input(fe.input(), region, source, &results)?; apply_specular_lighting(fe, region, cs, ts, input) } }?; @@ -577,7 +452,7 @@ fn apply_inner( // Clip result. // TODO: explain - let subregion2 = if let usvg::filter::Kind::Offset(..) = primitive.kind { + let subregion2 = if let usvg::filter::Kind::Offset(..) = primitive.kind() { // We do not support clipping on feOffset. region.translate_to(0, 0) } else { @@ -627,7 +502,7 @@ fn apply_inner( } results.push(FilterResult { - name: primitive.result.clone(), + name: primitive.result().to_string(), image: result, }); } @@ -639,94 +514,6 @@ fn apply_inner( } } -// TODO: merge with mask region logic -fn calc_region( - filter: &usvg::filter::Filter, - object_bbox: Option, -) -> Option { - if filter.units == usvg::Units::ObjectBoundingBox { - Some(filter.rect.bbox_transform(object_bbox?)) - } else { - Some(filter.rect) - } -} - -pub fn calc_filters_region( - filters: &[Rc], - object_bbox: Option, -) -> Option { - let mut global_region = usvg::BBox::default(); - - for filter in filters { - if let Some(region) = calc_region(filter, object_bbox) { - global_region = global_region.expand(usvg::BBox::from(region)); - } - } - - if !global_region.is_default() { - global_region.to_non_zero_rect() - } else { - None - } -} - -fn calc_subregion( - filter: &usvg::filter::Filter, - primitive: &usvg::filter::Primitive, - bbox: Option, - region: tiny_skia::NonZeroRect, -) -> Option { - // TODO: rewrite/simplify/explain/whatever - - let region = match primitive.kind { - usvg::filter::Kind::Flood(..) | usvg::filter::Kind::Image(..) => { - // `feImage` uses the object bbox. - if filter.primitive_units == usvg::Units::ObjectBoundingBox { - let bbox = bbox?; - - // TODO: wrong - // let ts_bbox = tiny_skia::Rect::new(ts.e, ts.f, ts.a, ts.d).unwrap(); - - let r = tiny_skia::NonZeroRect::from_xywh( - primitive.x.unwrap_or(0.0), - primitive.y.unwrap_or(0.0), - primitive.width.unwrap_or(1.0), - primitive.height.unwrap_or(1.0), - )?; - - let r = r.bbox_transform(bbox); - // .bbox_transform(ts_bbox); - - return Some(r); - } else { - region - } - } - _ => region, - }; - - // TODO: Wrong! Does not account rotate and skew. - let subregion = if filter.primitive_units == usvg::Units::ObjectBoundingBox { - let subregion_bbox = tiny_skia::NonZeroRect::from_xywh( - primitive.x.unwrap_or(0.0), - primitive.y.unwrap_or(0.0), - primitive.width.unwrap_or(1.0), - primitive.height.unwrap_or(1.0), - )?; - - region.bbox_transform(subregion_bbox) - } else { - tiny_skia::NonZeroRect::from_xywh( - primitive.x.unwrap_or(region.x()), - primitive.y.unwrap_or(region.y()), - primitive.width.unwrap_or(region.width()), - primitive.height.unwrap_or(region.height()), - )? - }; - - Some(subregion) -} - fn get_input( input: &usvg::filter::Input, region: IntRect, @@ -791,13 +578,17 @@ fn apply_drop_shadow( ts: usvg::Transform, input: Image, ) -> Result { + let (dx, dy) = match scale_coordinates(fe.dx(), fe.dy(), ts) { + Some(v) => v, + None => return Ok(input), + }; + let mut pixmap = tiny_skia::Pixmap::try_create(input.width(), input.height())?; let input_pixmap = input.into_color_space(cs)?.take()?; let mut shadow_pixmap = input_pixmap.clone(); - let (sx, sy) = ts.get_scale(); if let Some((std_dx, std_dy, use_box_blur)) = - resolve_std_dev(fe.std_dev_x.get() * sx, fe.std_dev_y.get() * sy) + resolve_std_dev(fe.std_dev_x().get(), fe.std_dev_y().get(), ts) { if use_box_blur { box_blur::apply(std_dx, std_dy, shadow_pixmap.as_image_ref_mut()); @@ -808,10 +599,10 @@ fn apply_drop_shadow( // flood let color = tiny_skia::Color::from_rgba8( - fe.color.red, - fe.color.green, - fe.color.blue, - fe.opacity.to_u8(), + fe.color().red, + fe.color().green, + fe.color().blue, + fe.opacity().to_u8(), ); for p in shadow_pixmap.pixels_mut() { let mut color = color; @@ -825,8 +616,8 @@ fn apply_drop_shadow( } pixmap.draw_pixmap( - (fe.dx * sx) as i32, - (fe.dy * sy) as i32, + dx as i32, + dy as i32, shadow_pixmap.as_ref(), &tiny_skia::PixmapPaint::default(), tiny_skia::Transform::identity(), @@ -851,9 +642,8 @@ fn apply_blur( ts: usvg::Transform, input: Image, ) -> Result { - let (sx, sy) = ts.get_scale(); let (std_dx, std_dy, use_box_blur) = - match resolve_std_dev(fe.std_dev_x.get() * sx, fe.std_dev_y.get() * sy) { + match resolve_std_dev(fe.std_dev_x().get(), fe.std_dev_y().get(), ts) { Some(v) => v, None => return Ok(input), }; @@ -874,9 +664,10 @@ fn apply_offset( ts: usvg::Transform, input: Image, ) -> Result { - let (sx, sy) = ts.get_scale(); - let dx = fe.dx * sx; - let dy = fe.dy * sy; + let (dx, dy) = match scale_coordinates(fe.dx(), fe.dy(), ts) { + Some(v) => v, + None => return Ok(input), + }; if dx.approx_zero_ulps(4) && dy.approx_zero_ulps(4) { return Ok(input); @@ -921,7 +712,7 @@ fn apply_blend( 0, input1.as_ref().as_ref(), &tiny_skia::PixmapPaint { - blend_mode: crate::tree::convert_blend_mode(fe.mode), + blend_mode: crate::render::convert_blend_mode(fe.mode()), ..tiny_skia::PixmapPaint::default() }, tiny_skia::Transform::identity(), @@ -945,7 +736,7 @@ fn apply_composite( let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - if let Operator::Arithmetic { k1, k2, k3, k4 } = fe.operator { + if let Operator::Arithmetic { k1, k2, k3, k4 } = fe.operator() { let pixmap1 = input1.take()?; let pixmap2 = input2.take()?; @@ -971,7 +762,7 @@ fn apply_composite( None, ); - let blend_mode = match fe.operator { + let blend_mode = match fe.operator() { Operator::Over => tiny_skia::BlendMode::SourceOver, Operator::In => tiny_skia::BlendMode::SourceIn, Operator::Out => tiny_skia::BlendMode::SourceOut, @@ -1004,7 +795,7 @@ fn apply_merge( ) -> Result { let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - for input in &fe.inputs { + for input in fe.inputs() { let input = get_input(input, region, source, results)?; let input = input.into_color_space(cs)?; pixmap.draw_pixmap( @@ -1021,14 +812,14 @@ fn apply_merge( } fn apply_flood(fe: &usvg::filter::Flood, region: IntRect) -> Result { - let c = fe.color; + let c = fe.color(); let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; pixmap.fill(tiny_skia::Color::from_rgba8( c.red, c.green, c.blue, - fe.opacity.to_u8(), + fe.opacity().to_u8(), )); Ok(Image::from_image( @@ -1069,7 +860,7 @@ fn apply_image( ) -> Result { let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - match fe.data { + match fe.data() { usvg::filter::ImageKind::Image(ref kind) => { let dx = (subregion.x() - region.x()) as f32; let dy = (subregion.y() - region.y()) as f32; @@ -1082,34 +873,35 @@ fn apply_image( .to_rect() .to_non_zero_rect() .unwrap(), - aspect: fe.aspect, + aspect: fe.aspect(), }; - let uimage = usvg::Image { - id: String::new(), - #[cfg(feature = "class")] - class: String::new(), - transform: usvg::Transform::default(), - visibility: usvg::Visibility::Visible, + crate::image::render_inner( + kind, view_box, - rendering_mode: fe.rendering_mode, - kind: kind.clone(), - }; - - let mut children = Vec::new(); - crate::image::convert(&uimage, &mut children); - if let Some(Node::Image(image)) = children.first() { - crate::image::render_image(image, transform, &mut pixmap.as_mut()); - } + transform, + fe.rendering_mode(), + &mut pixmap.as_mut(), + ); } usvg::filter::ImageKind::Use(ref node) => { let (sx, sy) = ts.get_scale(); - let transform = tiny_skia::Transform::from_scale(sx, sy); - if let Some(mut rtree) = crate::Tree::from_usvg_node(node) { - rtree.view_box.rect = rtree.view_box.rect.translate_to(0.0, 0.0).unwrap(); - rtree.render(transform, &mut pixmap.as_mut()); - } + let transform = tiny_skia::Transform::from_row( + sx, + 0.0, + 0.0, + sy, + subregion.x() as f32, + subregion.y() as f32, + ); + + let ctx = crate::render::Context { + max_bbox: tiny_skia::IntRect::from_xywh(0, 0, region.width(), region.height()) + .unwrap(), + }; + + crate::render::render_nodes(node, &ctx, transform, &mut pixmap.as_mut()); } } @@ -1141,7 +933,7 @@ fn apply_color_matrix( let mut pixmap = input.into_color_space(cs)?.take()?; demultiply_alpha(pixmap.data_mut().as_rgba_mut()); - color_matrix::apply(&fe.kind, pixmap.as_image_ref_mut()); + color_matrix::apply(fe.kind(), pixmap.as_image_ref_mut()); multiply_alpha(pixmap.data_mut().as_rgba_mut()); Ok(Image::from_image(pixmap, cs)) @@ -1154,7 +946,7 @@ fn apply_convolve_matrix( ) -> Result { let mut pixmap = input.into_color_space(cs)?.take()?; - if fe.preserve_alpha { + if fe.preserve_alpha() { demultiply_alpha(pixmap.data_mut().as_rgba_mut()); } @@ -1171,16 +963,17 @@ fn apply_morphology( ) -> Result { let mut pixmap = input.into_color_space(cs)?.take()?; - let (sx, sy) = ts.get_scale(); - let rx = fe.radius_x.get() * sx; - let ry = fe.radius_y.get() * sy; + let (rx, ry) = match scale_coordinates(fe.radius_x().get(), fe.radius_y().get(), ts) { + Some(v) => v, + None => return Ok(Image::from_image(pixmap, cs)), + }; if !(rx > 0.0 && ry > 0.0) { pixmap.clear(); return Ok(Image::from_image(pixmap, cs)); } - morphology::apply(fe.operator, rx, ry, pixmap.as_image_ref_mut()); + morphology::apply(fe.operator(), rx, ry, pixmap.as_image_ref_mut()); Ok(Image::from_image(pixmap, cs)) } @@ -1198,7 +991,10 @@ fn apply_displacement_map( let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - let (sx, sy) = ts.get_scale(); + let (sx, sy) = match scale_coordinates(fe.scale(), fe.scale(), ts) { + Some(v) => v, + None => return Ok(Image::from_image(pixmap1, cs)), + }; displacement_map::apply( fe, @@ -1230,12 +1026,12 @@ fn apply_turbulence( region.y() as f64 - ts.ty as f64, sx as f64, sy as f64, - fe.base_frequency_x.get() as f64, - fe.base_frequency_y.get() as f64, - fe.num_octaves, - fe.seed, - fe.stitch_tiles, - fe.kind == usvg::filter::TurbulenceKind::FractalNoise, + fe.base_frequency_x().get() as f64, + fe.base_frequency_y().get() as f64, + fe.num_octaves(), + fe.seed(), + fe.stitch_tiles(), + fe.kind() == usvg::filter::TurbulenceKind::FractalNoise, pixmap.as_image_ref_mut(), ); @@ -1253,7 +1049,7 @@ fn apply_diffuse_lighting( ) -> Result { let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - let light_source = transform_light_source(fe.light_source, region, ts); + let light_source = transform_light_source(fe.light_source(), region, ts); lighting::diffuse_lighting( fe, @@ -1274,7 +1070,7 @@ fn apply_specular_lighting( ) -> Result { let mut pixmap = tiny_skia::Pixmap::try_create(region.width(), region.height())?; - let light_source = transform_light_source(fe.light_source, region, ts); + let light_source = transform_light_source(fe.light_source(), region, ts); lighting::specular_lighting( fe, @@ -1286,6 +1082,7 @@ fn apply_specular_lighting( Ok(Image::from_image(pixmap, cs)) } +// TODO: do not modify LightSource fn transform_light_source( mut source: usvg::filter::LightSource, region: IntRect, @@ -1342,7 +1139,9 @@ fn apply_to_canvas(input: Image, pixmap: &mut tiny_skia::Pixmap) -> Result<(), E /// Calculates Gaussian blur sigmas for the current world transform. /// /// If the last flag is set, then a box blur should be used. Or IIR otherwise. -fn resolve_std_dev(mut std_dx: f32, mut std_dy: f32) -> Option<(f64, f64, bool)> { +fn resolve_std_dev(std_dx: f32, std_dy: f32, ts: usvg::Transform) -> Option<(f64, f64, bool)> { + let (mut std_dx, mut std_dy) = scale_coordinates(std_dx, std_dy, ts)?; + // 'A negative value or a value of zero disables the effect of the given filter primitive // (i.e., the result is the filter input image).' if std_dx.approx_eq_ulps(&0.0, 4) && std_dy.approx_eq_ulps(&0.0, 4) { @@ -1365,17 +1164,7 @@ fn resolve_std_dev(mut std_dx: f32, mut std_dy: f32) -> Option<(f64, f64, bool)> Some((std_dx as f64, std_dy as f64, box_blur)) } -/// Converts coordinates from `objectBoundingBox` to the `userSpaceOnUse`. -fn scale_coordinates( - x: f32, - y: f32, - units: usvg::Units, - bbox: Option, -) -> Option<(f32, f32)> { - if units == usvg::Units::ObjectBoundingBox { - let bbox = bbox?; - Some((x * bbox.width(), y * bbox.height())) - } else { - Some((x, y)) - } +fn scale_coordinates(x: f32, y: f32, ts: usvg::Transform) -> Option<(f32, f32)> { + let (sx, sy) = ts.get_scale(); + Some((x * sx, y * sy)) } diff --git a/crates/resvg/src/image.rs b/crates/resvg/src/image.rs index e3c50b5ab..759965910 100644 --- a/crates/resvg/src/image.rs +++ b/crates/resvg/src/image.rs @@ -3,90 +3,62 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use crate::render::TinySkiaPixmapMutExt; -use crate::tree::{BBoxes, Node, Tree}; -pub enum ImageKind { - #[cfg(feature = "raster-images")] - Raster(tiny_skia::Pixmap), - Vector(Tree), -} - -pub struct Image { - pub transform: tiny_skia::Transform, - pub view_box: usvg::ViewBox, - pub quality: tiny_skia::FilterQuality, - pub kind: ImageKind, -} - -pub fn convert(image: &usvg::Image, children: &mut Vec) -> Option { - let object_bbox = image.view_box.rect.to_rect(); - let bboxes = BBoxes { - object: usvg::BBox::from(object_bbox), - transformed_object: usvg::BBox::from(object_bbox.transform(image.transform)?), - layer: usvg::BBox::from(object_bbox), - }; - - if image.visibility != usvg::Visibility::Visible { - return Some(bboxes); - } - - let mut quality = tiny_skia::FilterQuality::Bicubic; - if image.rendering_mode == usvg::ImageRendering::OptimizeSpeed { - quality = tiny_skia::FilterQuality::Nearest; +pub fn render( + image: &usvg::Image, + transform: tiny_skia::Transform, + pixmap: &mut tiny_skia::PixmapMut, +) { + if image.visibility() != usvg::Visibility::Visible { + return; } - let kind = match image.kind { - usvg::ImageKind::SVG(ref utree) => ImageKind::Vector(Tree::from_usvg(utree)), - #[cfg(feature = "raster-images")] - _ => ImageKind::Raster(raster_images::decode_raster(image)?), - #[cfg(not(feature = "raster-images"))] - _ => { - log::warn!("Images decoding was disabled by a build feature."); - return None; - } - }; - - children.push(Node::Image(Image { - transform: image.transform, - view_box: image.view_box, - quality, - kind, - })); - - Some(bboxes) + render_inner( + image.kind(), + image.view_box(), + transform, + image.rendering_mode(), + pixmap, + ); } -pub fn render_image( - image: &Image, +pub fn render_inner( + image_kind: &usvg::ImageKind, + view_box: usvg::ViewBox, transform: tiny_skia::Transform, + #[allow(unused_variables)] rendering_mode: usvg::ImageRendering, pixmap: &mut tiny_skia::PixmapMut, ) { - match image.kind { + match image_kind { + usvg::ImageKind::SVG(ref tree) => { + render_vector(tree, &view_box, transform, pixmap); + } #[cfg(feature = "raster-images")] - ImageKind::Raster(ref raster) => { - raster_images::render_raster(image, raster, transform, pixmap); + _ => { + raster_images::render_raster(image_kind, view_box, transform, rendering_mode, pixmap); } - ImageKind::Vector(ref rtree) => { - render_vector(image, rtree, transform, pixmap); + #[cfg(not(feature = "raster-images"))] + _ => { + log::warn!("Images decoding was disabled by a build feature."); } } } fn render_vector( - image: &Image, - tree: &Tree, + tree: &usvg::Tree, + view_box: &usvg::ViewBox, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) -> Option<()> { - let img_size = tree.size.to_int_size(); - let (ts, clip) = crate::geom::view_box_to_transform_with_clip(&image.view_box, img_size); + let img_size = tree.size().to_int_size(); + let (ts, clip) = crate::geom::view_box_to_transform_with_clip(&view_box, img_size); let mut sub_pixmap = tiny_skia::Pixmap::new(pixmap.width(), pixmap.height()).unwrap(); let source_transform = transform; - let transform = transform.pre_concat(image.transform).pre_concat(ts); + let transform = transform.pre_concat(ts); - tree.render(transform, &mut sub_pixmap.as_mut()); + crate::render(tree, transform, &mut sub_pixmap.as_mut()); let mask = if let Some(clip) = clip { pixmap.create_rect_mask(source_transform, clip.to_rect()) @@ -108,12 +80,11 @@ fn render_vector( #[cfg(feature = "raster-images")] mod raster_images { - use super::Image; use crate::render::TinySkiaPixmapMutExt; - use crate::tree::OptionLog; + use crate::OptionLog; - pub fn decode_raster(image: &usvg::Image) -> Option { - match image.kind { + fn decode_raster(image: &usvg::ImageKind) -> Option { + match image { usvg::ImageKind::SVG(_) => None, usvg::ImageKind::JPEG(ref data) => { decode_jpeg(data).log_none(|| log::warn!("Failed to decode a JPEG image.")) @@ -208,13 +179,16 @@ mod raster_images { } pub(crate) fn render_raster( - image: &Image, - raster: &tiny_skia::Pixmap, + image: &usvg::ImageKind, + view_box: usvg::ViewBox, transform: tiny_skia::Transform, + rendering_mode: usvg::ImageRendering, pixmap: &mut tiny_skia::PixmapMut, ) -> Option<()> { + let raster = decode_raster(image)?; + let img_size = tiny_skia::IntSize::from_wh(raster.width(), raster.height())?; - let rect = image_rect(&image.view_box, img_size); + let rect = image_rect(&view_box, img_size); let ts = tiny_skia::Transform::from_row( rect.width() / raster.width() as f32, @@ -225,23 +199,27 @@ mod raster_images { rect.y(), ); + let mut quality = tiny_skia::FilterQuality::Bicubic; + if rendering_mode == usvg::ImageRendering::OptimizeSpeed { + quality = tiny_skia::FilterQuality::Nearest; + } + let pattern = tiny_skia::Pattern::new( raster.as_ref(), tiny_skia::SpreadMode::Pad, - image.quality, + quality, 1.0, ts, ); let mut paint = tiny_skia::Paint::default(); paint.shader = pattern; - let mask = if image.view_box.aspect.slice { - pixmap.create_rect_mask(transform, image.view_box.rect.to_rect()) + let mask = if view_box.aspect.slice { + pixmap.create_rect_mask(transform, view_box.rect.to_rect()) } else { None }; - let transform = transform.pre_concat(image.transform); pixmap.fill_rect(rect.to_rect(), &paint, transform, mask.as_ref()); Some(()) diff --git a/crates/resvg/src/lib.rs b/crates/resvg/src/lib.rs index 4a427b4ea..8094417d3 100644 --- a/crates/resvg/src/lib.rs +++ b/crates/resvg/src/lib.rs @@ -23,9 +23,80 @@ mod filter; mod geom; mod image; mod mask; -mod paint_server; mod path; mod render; -mod tree; -pub use crate::tree::Tree; +/// Renders a tree onto the pixmap. +/// +/// `transform` will be used as a root transform. +/// Can be used to position SVG inside the `pixmap`. +/// +/// The produced content is in the sRGB color space. +pub fn render( + tree: &usvg::Tree, + transform: tiny_skia::Transform, + pixmap: &mut tiny_skia::PixmapMut, +) { + let target_size = tiny_skia::IntSize::from_wh(pixmap.width(), pixmap.height()).unwrap(); + let max_bbox = tiny_skia::IntRect::from_xywh( + -(target_size.width() as i32) * 2, + -(target_size.height() as i32) * 2, + target_size.width() * 4, + target_size.height() * 4, + ) + .unwrap(); + + let ts = tree.view_box().to_transform(tree.size()); + let root_transform = transform.pre_concat(ts); + + let ctx = render::Context { max_bbox }; + render::render_nodes(tree.root(), &ctx, root_transform, pixmap); +} + +/// Renders a node onto the pixmap. +/// +/// `transform` will be used as a root transform. +/// Can be used to position SVG inside the `pixmap`. +/// +/// The expected pixmap size can be retrieved from `usvg::Node::abs_layer_bounding_box()`. +/// +/// Returns `None` when `node` has a zero size. +/// +/// The produced content is in the sRGB color space. +pub fn render_node( + node: &usvg::Node, + mut transform: tiny_skia::Transform, + pixmap: &mut tiny_skia::PixmapMut, +) -> Option<()> { + let bbox = node.abs_layer_bounding_box()?; + + let target_size = tiny_skia::IntSize::from_wh(pixmap.width(), pixmap.height()).unwrap(); + let max_bbox = tiny_skia::IntRect::from_xywh( + -(target_size.width() as i32) * 2, + -(target_size.height() as i32) * 2, + target_size.width() * 4, + target_size.height() * 4, + ) + .unwrap(); + + transform = transform.pre_translate(-bbox.x(), -bbox.y()); + + let ctx = render::Context { max_bbox }; + render::render_node(node, &ctx, transform, pixmap); + + Some(()) +} + +pub(crate) trait OptionLog { + fn log_none(self, f: F) -> Self; +} + +impl OptionLog for Option { + #[inline] + fn log_none(self, f: F) -> Self { + self.or_else(|| { + f(); + None + }) + } +} diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs index d048e1fd2..491a7d5de 100644 --- a/crates/resvg/src/main.rs +++ b/crates/resvg/src/main.rs @@ -6,7 +6,7 @@ use std::path; -use usvg::{fontdb, NodeExt, TreeParsing, TreeTextToPath}; +use usvg::fontdb; fn main() { if let Err(e) = process() { @@ -22,11 +22,8 @@ where let now = std::time::Instant::now(); let result = f(); if perf { - println!( - "{}: {:.2}ms", - name, - now.elapsed().as_micros() as f64 / 1000.0 - ); + let elapsed = now.elapsed().as_micros() as f64 / 1000.0; + println!("{}: {:.2}ms", name, elapsed); } result @@ -83,13 +80,14 @@ fn process() -> Result<(), String> { .map_err(|e| e.to_string()) })?; - let mut tree = timed(args.perf, "SVG Parsing", || { - usvg::Tree::from_xmltree(&xml_tree, &args.usvg).map_err(|e| e.to_string()) - })?; - // fontdb initialization is pretty expensive, so perform it only when needed. - if tree.has_text_nodes() { - let fontdb = timed(args.perf, "FontDB", || load_fonts(&mut args)); + let has_text_nodes = xml_tree + .descendants() + .any(|n| n.has_tag_name(("http://www.w3.org/2000/svg", "text"))); + + let mut fontdb = fontdb::Database::new(); + if has_text_nodes { + timed(args.perf, "FontDB", || load_fonts(&mut args, &mut fontdb)); if args.list_fonts { for face in fontdb.faces() { if let fontdb::Source::File(ref path) = &face.source { @@ -111,10 +109,12 @@ fn process() -> Result<(), String> { } } } - - timed(args.perf, "Text Conversion", || tree.convert_text(&fontdb)); } + let tree = timed(args.perf, "SVG Parsing", || { + usvg::Tree::from_xmltree(&xml_tree, &args.usvg, &fontdb).map_err(|e| e.to_string()) + })?; + if args.query_all { return query_all(&tree); } @@ -573,8 +573,7 @@ fn parse_args() -> Result { }) } -fn load_fonts(args: &mut Args) -> fontdb::Database { - let mut fontdb = fontdb::Database::new(); +fn load_fonts(args: &mut Args, fontdb: &mut fontdb::Database) { if !args.skip_system_fonts { fontdb.load_system_fonts(); } @@ -597,14 +596,25 @@ fn load_fonts(args: &mut Args) -> fontdb::Database { fontdb.set_cursive_family(take_or(args.cursive_family.take(), "Comic Sans MS")); fontdb.set_fantasy_family(take_or(args.fantasy_family.take(), "Impact")); fontdb.set_monospace_family(take_or(args.monospace_family.take(), "Courier New")); - - fontdb } fn query_all(tree: &usvg::Tree) -> Result<(), String> { + let count = query_all_impl(tree.root()); + + if count == 0 { + return Err("the file has no valid ID's".to_string()); + } + + Ok(()) +} + +fn query_all_impl(parent: &usvg::Group) -> usize { let mut count = 0; - for node in tree.root.descendants() { + for node in parent.children() { if node.id().is_empty() { + if let usvg::Node::Group(ref group) = node { + count += query_all_impl(group); + } continue; } @@ -614,37 +624,39 @@ fn query_all(tree: &usvg::Tree) -> Result<(), String> { (v * 1000.0).round() / 1000.0 } - if let Some(bbox) = node.calculate_bbox() { - println!( - "{},{},{},{},{}", - node.id(), - round_len(bbox.x()), - round_len(bbox.y()), - round_len(bbox.width()), - round_len(bbox.height()) - ); - } - } + let bbox = node + .abs_layer_bounding_box() + .map(|r| r.to_rect()) + .unwrap_or(node.abs_bounding_box()); - if count == 0 { - return Err("the file has no valid ID's".to_string()); + println!( + "{},{},{},{},{}", + node.id(), + round_len(bbox.x()), + round_len(bbox.y()), + round_len(bbox.width()), + round_len(bbox.height()) + ); + + if let usvg::Node::Group(ref group) = node { + count += query_all_impl(group); + } } - Ok(()) + count } fn render_svg(args: &Args, tree: &usvg::Tree) -> Result { let now = std::time::Instant::now(); let img = if let Some(ref id) = args.export_id { - let node = match tree.root.descendants().find(|n| &*n.id() == id) { + let node = match tree.node_by_id(id) { Some(node) => node, None => return Err(format!("SVG doesn't have '{}' ID", id)), }; let bbox = node - .calculate_bbox() - .and_then(|r| r.to_non_zero_rect()) + .abs_layer_bounding_box() .ok_or_else(|| "node has zero size".to_string())?; let size = args @@ -661,19 +673,16 @@ fn render_svg(args: &Args, tree: &usvg::Tree) -> Result Result Result Option { - let content_area = rtree.content_area?.to_non_zero_rect()?; + let content_area = tree.root().layer_bounding_box(); let limit = tiny_skia::IntRect::from_xywh(0, 0, pixmap.width(), pixmap.height()).unwrap(); @@ -797,15 +803,14 @@ impl log::Log for SimpleLogger { }; let line = record.line().unwrap_or(0); + let args = record.args(); match record.level() { - log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, record.args()), - log::Level::Warn => { - eprintln!("Warning (in {}:{}): {}", target, line, record.args()) - } - log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, record.args()), - log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, record.args()), - log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, record.args()), + log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args), + log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args), + log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args), + log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args), + log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args), } } } diff --git a/crates/resvg/src/mask.rs b/crates/resvg/src/mask.rs index ea15131b3..4cd9dd39d 100644 --- a/crates/resvg/src/mask.rs +++ b/crates/resvg/src/mask.rs @@ -2,69 +2,15 @@ // 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/. -use std::rc::Rc; - use crate::render::Context; -use crate::tree::{Node, OptionLog}; - -pub struct Mask { - pub mask_all: bool, - pub region: tiny_skia::Rect, - pub content_transform: tiny_skia::Transform, - pub kind: usvg::MaskType, - pub mask: Option>, - pub children: Vec, -} - -pub fn convert(umask: Option>, object_bbox: tiny_skia::Rect) -> Option { - let umask = umask?; - - let mut content_transform = tiny_skia::Transform::default(); - if umask.content_units == usvg::Units::ObjectBoundingBox { - let object_bbox = object_bbox - .to_non_zero_rect() - .log_none(|| log::warn!("Masking of zero-sized shapes is not allowed."))?; - - let ts = usvg::Transform::from_bbox(object_bbox); - content_transform = ts; - } - - let mut mask_all = false; - if umask.units == usvg::Units::ObjectBoundingBox && object_bbox.to_non_zero_rect().is_none() { - // `objectBoundingBox` units and zero-sized bbox? Clear the canvas and return. - // Technically a UB, but this is what Chrome and Firefox do. - mask_all = true; - } - - let region = if umask.units == usvg::Units::ObjectBoundingBox { - if let Some(bbox) = object_bbox.to_non_zero_rect() { - umask.rect.bbox_transform(bbox) - } else { - // The actual values does not matter. Will not be used anyway. - tiny_skia::NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap() - } - } else { - umask.rect - }; - - let (children, _) = crate::tree::convert_node(umask.root.clone()); - Some(Mask { - mask_all, - region: region.to_rect(), - content_transform, - kind: umask.kind, - mask: convert(umask.mask.clone(), object_bbox).map(Box::new), - children, - }) -} pub fn apply( - mask: &Mask, + mask: &usvg::Mask, ctx: &Context, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::Pixmap, ) { - if mask.mask_all { + if mask.root().children().is_empty() { pixmap.fill(tiny_skia::Color::TRANSPARENT); return; } @@ -76,28 +22,22 @@ pub fn apply( // Mask has to be clipped by mask.region let mut alpha_mask = tiny_skia::Mask::new(pixmap.width(), pixmap.height()).unwrap(); alpha_mask.fill_path( - &tiny_skia::PathBuilder::from_rect(mask.region), + &tiny_skia::PathBuilder::from_rect(mask.rect().to_rect()), tiny_skia::FillRule::Winding, true, transform, ); - let content_transform = transform.pre_concat(mask.content_transform); - crate::render::render_nodes( - &mask.children, - ctx, - content_transform, - &mut mask_pixmap.as_mut(), - ); + crate::render::render_nodes(mask.root(), ctx, transform, &mut mask_pixmap.as_mut()); mask_pixmap.apply_mask(&alpha_mask); } - if let Some(ref mask) = mask.mask { + if let Some(mask) = mask.mask() { self::apply(mask, ctx, transform, pixmap); } - let mask_type = match mask.kind { + let mask_type = match mask.kind() { usvg::MaskType::Luminance => tiny_skia::MaskType::Luminance, usvg::MaskType::Alpha => tiny_skia::MaskType::Alpha, }; diff --git a/crates/resvg/src/paint_server.rs b/crates/resvg/src/paint_server.rs deleted file mode 100644 index e91ddc3d9..000000000 --- a/crates/resvg/src/paint_server.rs +++ /dev/null @@ -1,189 +0,0 @@ -// 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/. - -use std::rc::Rc; - -use crate::render::Context; -use crate::tree::{Node, OptionLog}; - -pub struct Pattern { - pub rect: tiny_skia::NonZeroRect, - pub view_box: Option, - pub opacity: usvg::Opacity, - pub transform: tiny_skia::Transform, - pub content_transform: tiny_skia::Transform, - pub children: Vec, -} - -#[derive(Clone)] -pub enum Paint { - Shader(tiny_skia::Shader<'static>), - Pattern(Rc), -} - -pub fn convert( - paint: &usvg::Paint, - opacity: usvg::Opacity, - object_bbox: Option, -) -> Option { - match paint { - usvg::Paint::Color(c) => { - let c = tiny_skia::Color::from_rgba8(c.red, c.green, c.blue, opacity.to_u8()); - Some(Paint::Shader(tiny_skia::Shader::SolidColor(c))) - } - usvg::Paint::LinearGradient(ref lg) => convert_linear_gradient(lg, opacity, object_bbox), - usvg::Paint::RadialGradient(ref rg) => convert_radial_gradient(rg, opacity, object_bbox), - usvg::Paint::Pattern(ref patt) => convert_pattern(patt, opacity, object_bbox), - } -} - -fn convert_linear_gradient( - gradient: &usvg::LinearGradient, - opacity: usvg::Opacity, - object_bbox: Option, -) -> Option { - let (mode, transform, points) = convert_base_gradient(gradient, opacity, object_bbox)?; - - let shader = tiny_skia::LinearGradient::new( - (gradient.x1, gradient.y1).into(), - (gradient.x2, gradient.y2).into(), - points, - mode, - transform, - )?; - - Some(Paint::Shader(shader)) -} - -fn convert_radial_gradient( - gradient: &usvg::RadialGradient, - opacity: usvg::Opacity, - object_bbox: Option, -) -> Option { - let (mode, transform, points) = convert_base_gradient(gradient, opacity, object_bbox)?; - - let shader = tiny_skia::RadialGradient::new( - (gradient.fx, gradient.fy).into(), - (gradient.cx, gradient.cy).into(), - gradient.r.get(), - points, - mode, - transform, - )?; - - Some(Paint::Shader(shader)) -} - -fn convert_base_gradient( - gradient: &usvg::BaseGradient, - opacity: usvg::Opacity, - object_bbox: Option, -) -> Option<( - tiny_skia::SpreadMode, - tiny_skia::Transform, - Vec, -)> { - let mode = match gradient.spread_method { - usvg::SpreadMethod::Pad => tiny_skia::SpreadMode::Pad, - usvg::SpreadMethod::Reflect => tiny_skia::SpreadMode::Reflect, - usvg::SpreadMethod::Repeat => tiny_skia::SpreadMode::Repeat, - }; - - let transform = if gradient.units == usvg::Units::ObjectBoundingBox { - let bbox = - object_bbox.log_none(|| log::warn!("Gradient on zero-sized shapes is not allowed."))?; - let ts = tiny_skia::Transform::from_bbox(bbox); - ts.pre_concat(gradient.transform) - } else { - gradient.transform - }; - - let mut points = Vec::with_capacity(gradient.stops.len()); - for stop in &gradient.stops { - let alpha = stop.opacity * opacity; - let color = tiny_skia::Color::from_rgba8( - stop.color.red, - stop.color.green, - stop.color.blue, - alpha.to_u8(), - ); - points.push(tiny_skia::GradientStop::new(stop.offset.get(), color)) - } - - Some((mode, transform, points)) -} - -fn convert_pattern( - pattern: &usvg::Pattern, - opacity: usvg::Opacity, - object_bbox: Option, -) -> Option { - let content_transform = - if pattern.content_units == usvg::Units::ObjectBoundingBox && pattern.view_box.is_none() { - let bbox = object_bbox - .log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?; - - // No need to shift patterns. - tiny_skia::Transform::from_scale(bbox.width(), bbox.height()) - } else { - tiny_skia::Transform::default() - }; - - let (children, _) = crate::tree::convert_node(pattern.root.clone()); - if children.is_empty() { - return None; - } - - let rect = if pattern.units == usvg::Units::ObjectBoundingBox { - let bbox = - object_bbox.log_none(|| log::warn!("Pattern on zero-sized shapes is not allowed."))?; - - pattern.rect.bbox_transform(bbox) - } else { - pattern.rect - }; - - Some(Paint::Pattern(Rc::new(Pattern { - rect, - view_box: pattern.view_box, - opacity, - transform: pattern.transform, - content_transform, - children, - }))) -} - -pub fn prepare_pattern_pixmap( - pattern: &Pattern, - ctx: &Context, - transform: tiny_skia::Transform, -) -> Option<(tiny_skia::Pixmap, tiny_skia::Transform)> { - let (sx, sy) = { - let ts2 = transform.pre_concat(pattern.transform); - ts2.get_scale() - }; - - let img_size = tiny_skia::IntSize::from_wh( - (pattern.rect.width() * sx).round() as u32, - (pattern.rect.height() * sy).round() as u32, - )?; - let mut pixmap = tiny_skia::Pixmap::new(img_size.width(), img_size.height())?; - - let mut transform = tiny_skia::Transform::from_scale(sx, sy); - if let Some(vbox) = pattern.view_box { - let ts = usvg::utils::view_box_to_transform(vbox.rect, vbox.aspect, pattern.rect.size()); - transform = transform.pre_concat(ts); - } - - transform = transform.pre_concat(pattern.content_transform); - - crate::render::render_nodes(&pattern.children, ctx, transform, &mut pixmap.as_mut()); - - let mut ts = tiny_skia::Transform::default(); - ts = ts.pre_concat(pattern.transform); - ts = ts.pre_translate(pattern.rect.x(), pattern.rect.y()); - ts = ts.pre_scale(1.0 / sx, 1.0 / sy); - - Some((pixmap, ts)) -} diff --git a/crates/resvg/src/path.rs b/crates/resvg/src/path.rs index 27db194e0..6b739470e 100644 --- a/crates/resvg/src/path.rs +++ b/crates/resvg/src/path.rs @@ -2,272 +2,209 @@ // 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/. -use std::rc::Rc; - -use crate::paint_server::Paint; use crate::render::Context; -use crate::tree::{BBoxes, Node}; - -pub struct FillPath { - pub transform: tiny_skia::Transform, - pub paint: Paint, - pub rule: tiny_skia::FillRule, - pub anti_alias: bool, - pub path: Rc, -} - -pub struct StrokePath { - pub transform: tiny_skia::Transform, - pub paint: Paint, - pub stroke: tiny_skia::Stroke, - pub anti_alias: bool, - pub path: Rc, -} - -pub fn convert(upath: &usvg::Path, children: &mut Vec) -> Option { - let transform = upath.transform; - let anti_alias = upath.rendering_mode.use_shape_antialiasing(); - - let fill_path = upath.fill.as_ref().and_then(|ufill| { - convert_fill_path( - ufill, - upath.data.clone(), - transform, - upath.text_bbox, - anti_alias, - ) - }); - - let stroke_path = upath.stroke.as_ref().and_then(|ustroke| { - convert_stroke_path( - ustroke, - upath.data.clone(), - transform, - upath.text_bbox, - anti_alias, - ) - }); - - if fill_path.is_none() && stroke_path.is_none() { - return None; - } - - let mut bboxes = BBoxes::default(); - if let Some((_, l_bbox, o_bbox)) = fill_path { - bboxes.layer = bboxes.layer.expand(l_bbox); - bboxes.object = bboxes.object.expand(o_bbox); - } - if let Some((_, l_bbox, o_bbox)) = stroke_path { - bboxes.layer = bboxes.layer.expand(l_bbox); - bboxes.object = bboxes.object.expand(o_bbox); - } - - bboxes.transformed_object = bboxes.object.transform(upath.transform)?; - - // Do not add hidden paths, but preserve the bbox. - // visibility=hidden still affects the bbox calculation. - if upath.visibility != usvg::Visibility::Visible { - return Some(bboxes); +pub fn render( + path: &usvg::Path, + blend_mode: tiny_skia::BlendMode, + ctx: &Context, + transform: tiny_skia::Transform, + pixmap: &mut tiny_skia::PixmapMut, +) { + if path.visibility() != usvg::Visibility::Visible { + return; } - if upath.paint_order == usvg::PaintOrder::FillAndStroke { - if let Some((path, _, _)) = fill_path { - children.push(Node::FillPath(path)); - } - - if let Some((path, _, _)) = stroke_path { - children.push(Node::StrokePath(path)); - } + if path.paint_order() == usvg::PaintOrder::FillAndStroke { + fill_path(path, blend_mode, ctx, transform, pixmap); + stroke_path(path, blend_mode, ctx, transform, pixmap); } else { - if let Some((path, _, _)) = stroke_path { - children.push(Node::StrokePath(path)); - } - - if let Some((path, _, _)) = fill_path { - children.push(Node::FillPath(path)); - } + stroke_path(path, blend_mode, ctx, transform, pixmap); + fill_path(path, blend_mode, ctx, transform, pixmap); } - - Some(bboxes) } -fn convert_fill_path( - ufill: &usvg::Fill, - path: Rc, +pub fn fill_path( + path: &usvg::Path, + blend_mode: tiny_skia::BlendMode, + ctx: &Context, transform: tiny_skia::Transform, - text_bbox: Option, - anti_alias: bool, -) -> Option<(FillPath, usvg::BBox, usvg::BBox)> { + pixmap: &mut tiny_skia::PixmapMut, +) -> Option<()> { + let fill = path.fill()?; + // Horizontal and vertical lines cannot be filled. Skip. - if path.bounds().width() == 0.0 || path.bounds().height() == 0.0 { + if path.data().bounds().width() == 0.0 || path.data().bounds().height() == 0.0 { return None; } - let rule = match ufill.rule { + let rule = match fill.rule() { usvg::FillRule::NonZero => tiny_skia::FillRule::Winding, usvg::FillRule::EvenOdd => tiny_skia::FillRule::EvenOdd, }; - let mut object_bbox = usvg::BBox::from(path.bounds()); - if let Some(text_bbox) = text_bbox { - object_bbox = object_bbox.expand(usvg::BBox::from(text_bbox)); - } - - let paint = - crate::paint_server::convert(&ufill.paint, ufill.opacity, object_bbox.to_non_zero_rect())?; - - let path = FillPath { - transform, - paint, - rule, - anti_alias, - path, - }; - - Some((path, object_bbox, object_bbox)) -} - -fn convert_stroke_path( - ustroke: &usvg::Stroke, - path: Rc, - transform: tiny_skia::Transform, - text_bbox: Option, - anti_alias: bool, -) -> Option<(StrokePath, usvg::BBox, usvg::BBox)> { - let mut stroke = tiny_skia::Stroke { - width: ustroke.width.get(), - miter_limit: ustroke.miterlimit.get(), - line_cap: match ustroke.linecap { - usvg::LineCap::Butt => tiny_skia::LineCap::Butt, - usvg::LineCap::Round => tiny_skia::LineCap::Round, - usvg::LineCap::Square => tiny_skia::LineCap::Square, - }, - line_join: match ustroke.linejoin { - usvg::LineJoin::Miter => tiny_skia::LineJoin::Miter, - usvg::LineJoin::MiterClip => tiny_skia::LineJoin::MiterClip, - usvg::LineJoin::Round => tiny_skia::LineJoin::Round, - usvg::LineJoin::Bevel => tiny_skia::LineJoin::Bevel, - }, - dash: None, - }; - - // Zero-sized stroke path is not an error, because linecap round or square - // would produce the shape either way. - // TODO: Find a better way to handle it. - let object_bbox = usvg::BBox::from(path.bounds()); - - let mut complete_object_bbox = object_bbox; - if let Some(text_bbox) = text_bbox { - complete_object_bbox = complete_object_bbox.expand(usvg::BBox::from(text_bbox)); - } - let paint = crate::paint_server::convert( - &ustroke.paint, - ustroke.opacity, - complete_object_bbox.to_non_zero_rect(), - )?; - - if let Some(ref list) = ustroke.dasharray { - stroke.dash = tiny_skia::StrokeDash::new(list.clone(), ustroke.dashoffset); - } - - // TODO: explain - // TODO: expand by stroke width for round/bevel joins - let resolution_scale = tiny_skia::PathStroker::compute_resolution_scale(&transform); - let resolution_scale = resolution_scale.max(10.0); - let stroked_path = path.stroke(&stroke, resolution_scale)?; - - let mut layer_bbox = usvg::BBox::from(stroked_path.bounds()); - if let Some(text_bbox) = text_bbox { - layer_bbox = layer_bbox.expand(usvg::BBox::from(text_bbox)); - } - - // TODO: dash beforehand - // TODO: preserve stroked path - - let path = StrokePath { - transform, - paint, - stroke, - anti_alias, - path, - }; - - Some((path, layer_bbox, object_bbox)) -} - -pub fn render_fill_path( - path: &FillPath, - blend_mode: tiny_skia::BlendMode, - ctx: &Context, - transform: tiny_skia::Transform, - pixmap: &mut tiny_skia::PixmapMut, -) -> Option<()> { let pattern_pixmap; let mut paint = tiny_skia::Paint::default(); - match path.paint { - Paint::Shader(ref shader) => { - paint.shader = shader.clone(); // TODO: avoid clone + match fill.paint() { + usvg::Paint::Color(c) => { + paint.set_color_rgba8(c.red, c.green, c.blue, fill.opacity().to_u8()); + } + usvg::Paint::LinearGradient(ref lg) => { + paint.shader = convert_linear_gradient(lg, fill.opacity())?; + } + usvg::Paint::RadialGradient(ref rg) => { + paint.shader = convert_radial_gradient(rg, fill.opacity())?; } - Paint::Pattern(ref pattern) => { - let (patt_pix, patt_ts) = - crate::paint_server::prepare_pattern_pixmap(pattern, ctx, transform)?; + usvg::Paint::Pattern(ref pattern) => { + let (patt_pix, patt_ts) = render_pattern_pixmap(pattern, ctx, transform)?; pattern_pixmap = patt_pix; paint.shader = tiny_skia::Pattern::new( pattern_pixmap.as_ref(), tiny_skia::SpreadMode::Repeat, tiny_skia::FilterQuality::Bicubic, - pattern.opacity.get(), + fill.opacity().get(), patt_ts, ) } } - - paint.anti_alias = path.anti_alias; + paint.anti_alias = path.rendering_mode().use_shape_antialiasing(); paint.blend_mode = blend_mode; - let transform = transform.pre_concat(path.transform); - pixmap.fill_path(&path.path, &paint, path.rule, transform, None); - + pixmap.fill_path(path.data(), &paint, rule, transform, None); Some(()) } -pub fn render_stroke_path( - path: &StrokePath, +fn stroke_path( + path: &usvg::Path, blend_mode: tiny_skia::BlendMode, ctx: &Context, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) -> Option<()> { + let stroke = path.stroke()?; let pattern_pixmap; let mut paint = tiny_skia::Paint::default(); - match path.paint { - Paint::Shader(ref shader) => { - paint.shader = shader.clone(); // TODO: avoid clone + match stroke.paint() { + usvg::Paint::Color(c) => { + paint.set_color_rgba8(c.red, c.green, c.blue, stroke.opacity().to_u8()); + } + usvg::Paint::LinearGradient(ref lg) => { + paint.shader = convert_linear_gradient(lg, stroke.opacity())?; } - Paint::Pattern(ref pattern) => { - let (patt_pix, patt_ts) = - crate::paint_server::prepare_pattern_pixmap(pattern, ctx, transform)?; + usvg::Paint::RadialGradient(ref rg) => { + paint.shader = convert_radial_gradient(rg, stroke.opacity())?; + } + usvg::Paint::Pattern(ref pattern) => { + let (patt_pix, patt_ts) = render_pattern_pixmap(pattern, ctx, transform)?; pattern_pixmap = patt_pix; paint.shader = tiny_skia::Pattern::new( pattern_pixmap.as_ref(), tiny_skia::SpreadMode::Repeat, tiny_skia::FilterQuality::Bicubic, - pattern.opacity.get(), + stroke.opacity().get(), patt_ts, ) } } - - paint.anti_alias = path.anti_alias; + paint.anti_alias = path.rendering_mode().use_shape_antialiasing(); paint.blend_mode = blend_mode; - // TODO: fallback to a stroked path when possible - - let transform = transform.pre_concat(path.transform); - pixmap.stroke_path(&path.path, &paint, &path.stroke, transform, None); + pixmap.stroke_path(path.data(), &paint, &stroke.to_tiny_skia(), transform, None); Some(()) } + +fn convert_linear_gradient( + gradient: &usvg::LinearGradient, + opacity: usvg::Opacity, +) -> Option { + let (mode, points) = convert_base_gradient(gradient, opacity)?; + + let shader = tiny_skia::LinearGradient::new( + (gradient.x1(), gradient.y1()).into(), + (gradient.x2(), gradient.y2()).into(), + points, + mode, + gradient.transform(), + )?; + + Some(shader) +} + +fn convert_radial_gradient( + gradient: &usvg::RadialGradient, + opacity: usvg::Opacity, +) -> Option { + let (mode, points) = convert_base_gradient(gradient, opacity)?; + + let shader = tiny_skia::RadialGradient::new( + (gradient.fx(), gradient.fy()).into(), + (gradient.cx(), gradient.cy()).into(), + gradient.r().get(), + points, + mode, + gradient.transform(), + )?; + + Some(shader) +} + +fn convert_base_gradient( + gradient: &usvg::BaseGradient, + opacity: usvg::Opacity, +) -> Option<(tiny_skia::SpreadMode, Vec)> { + let mode = match gradient.spread_method() { + usvg::SpreadMethod::Pad => tiny_skia::SpreadMode::Pad, + usvg::SpreadMethod::Reflect => tiny_skia::SpreadMode::Reflect, + usvg::SpreadMethod::Repeat => tiny_skia::SpreadMode::Repeat, + }; + + let mut points = Vec::with_capacity(gradient.stops().len()); + for stop in gradient.stops() { + let alpha = stop.opacity() * opacity; + let color = tiny_skia::Color::from_rgba8( + stop.color().red, + stop.color().green, + stop.color().blue, + alpha.to_u8(), + ); + points.push(tiny_skia::GradientStop::new(stop.offset().get(), color)) + } + + Some((mode, points)) +} + +fn render_pattern_pixmap( + pattern: &usvg::Pattern, + ctx: &Context, + transform: tiny_skia::Transform, +) -> Option<(tiny_skia::Pixmap, tiny_skia::Transform)> { + let (sx, sy) = { + let ts2 = transform.pre_concat(pattern.transform()); + ts2.get_scale() + }; + + let rect = pattern.rect(); + let img_size = tiny_skia::IntSize::from_wh( + (rect.width() * sx).round() as u32, + (rect.height() * sy).round() as u32, + )?; + let mut pixmap = tiny_skia::Pixmap::new(img_size.width(), img_size.height())?; + + let mut transform = tiny_skia::Transform::from_scale(sx, sy); + if let Some(vbox) = pattern.view_box() { + let ts = vbox.to_transform(rect.size()); + transform = transform.pre_concat(ts); + } + + crate::render::render_nodes(pattern.root(), ctx, transform, &mut pixmap.as_mut()); + + let mut ts = tiny_skia::Transform::default(); + ts = ts.pre_concat(pattern.transform()); + ts = ts.pre_translate(rect.x(), rect.y()); + ts = ts.pre_scale(1.0 / sx, 1.0 / sy); + + Some((pixmap, ts)) +} diff --git a/crates/resvg/src/render.rs b/crates/resvg/src/render.rs index 9d46b3c55..e6601b6a3 100644 --- a/crates/resvg/src/render.rs +++ b/crates/resvg/src/render.rs @@ -2,62 +2,35 @@ // 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/. -use crate::tree::{Group, Node, OptionLog, Tree}; +use crate::OptionLog; pub struct Context { pub max_bbox: tiny_skia::IntRect, } -impl Tree { - /// Renders the tree onto the pixmap. - /// - /// `transform` will be used as a root transform. - /// Can be used to position SVG inside the `pixmap`. - /// - /// The produced content is in the sRGB color space. - pub fn render(&self, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut) { - let target_size = tiny_skia::IntSize::from_wh(pixmap.width(), pixmap.height()).unwrap(); - let max_bbox = tiny_skia::IntRect::from_xywh( - -(target_size.width() as i32) * 2, - -(target_size.height() as i32) * 2, - target_size.width() * 4, - target_size.height() * 4, - ) - .unwrap(); - - let ts = - usvg::utils::view_box_to_transform(self.view_box.rect, self.view_box.aspect, self.size); - - let root_transform = transform.pre_concat(ts); - - let ctx = Context { max_bbox }; - render_nodes(&self.children, &ctx, root_transform, pixmap); - } -} - pub fn render_nodes( - children: &[Node], + parent: &usvg::Group, ctx: &Context, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) { - for node in children { + for node in parent.children() { render_node(node, ctx, transform, pixmap); } } -fn render_node( - node: &Node, +pub fn render_node( + node: &usvg::Node, ctx: &Context, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) { match node { - Node::Group(ref group) => { + usvg::Node::Group(ref group) => { render_group(group, ctx, transform, pixmap); } - Node::FillPath(ref path) => { - crate::path::render_fill_path( + usvg::Node::Path(ref path) => { + crate::path::render( path, tiny_skia::BlendMode::SourceOver, ctx, @@ -65,37 +38,31 @@ fn render_node( pixmap, ); } - Node::StrokePath(ref path) => { - crate::path::render_stroke_path( - path, - tiny_skia::BlendMode::SourceOver, - ctx, - transform, - pixmap, - ); + usvg::Node::Image(ref image) => { + crate::image::render(image, transform, pixmap); } - Node::Image(ref image) => { - crate::image::render_image(image, transform, pixmap); + usvg::Node::Text(ref text) => { + render_group(text.flattened(), ctx, transform, pixmap); } } } fn render_group( - group: &Group, + group: &usvg::Group, ctx: &Context, transform: tiny_skia::Transform, pixmap: &mut tiny_skia::PixmapMut, ) -> Option<()> { - let transform = transform.pre_concat(group.transform); + let transform = transform.pre_concat(group.transform()); - if group.is_transform_only() { - render_nodes(&group.children, ctx, transform, pixmap); + if !group.should_isolate() { + render_nodes(group, ctx, transform, pixmap); return Some(()); } - let bbox = group.bbox.transform(transform)?; + let bbox = group.layer_bounding_box().transform(transform)?; - let mut ibbox = if group.filters.is_empty() { + let mut ibbox = if group.filters().is_empty() { // Convert group bbox into an integer one, expanding each side outwards by 2px // to make sure that anti-aliased pixels would not be clipped. tiny_skia::IntRect::from_xywh( @@ -107,7 +74,7 @@ fn render_group( } else { // The bounding box for groups with filters is special and should not be expanded by 2px, // because it's already acting as a clipping region. - let bbox = bbox.to_non_zero_rect()?.to_int_rect(); + let bbox = bbox.to_int_rect(); // Make sure our filter region is not bigger than 4x the canvas size. // This is required mainly to prevent huge filter regions that would tank the performance. // It should not affect the final result in any way. @@ -116,7 +83,7 @@ fn render_group( // Make sure our layer is not bigger than 4x the canvas size. // This is required to prevent huge layers. - if group.filters.is_empty() { + if group.filters().is_empty() { ibbox = crate::geom::fit_to_rect(ibbox, ctx.max_bbox)?; } @@ -137,25 +104,25 @@ fn render_group( let mut sub_pixmap = tiny_skia::Pixmap::new(ibbox.width(), ibbox.height()) .log_none(|| log::warn!("Failed to allocate a group layer for: {:?}.", ibbox))?; - render_nodes(&group.children, ctx, transform, &mut sub_pixmap.as_mut()); + render_nodes(group, ctx, transform, &mut sub_pixmap.as_mut()); - if !group.filters.is_empty() { - for filter in &group.filters { + if !group.filters().is_empty() { + for filter in group.filters() { crate::filter::apply(filter, transform, &mut sub_pixmap); } } - if let Some(ref clip_path) = group.clip_path { + if let Some(clip_path) = group.clip_path() { crate::clip::apply(clip_path, transform, &mut sub_pixmap); } - if let Some(ref mask) = group.mask { + if let Some(mask) = group.mask() { crate::mask::apply(mask, ctx, transform, &mut sub_pixmap); } let paint = tiny_skia::PixmapPaint { - opacity: group.opacity.get(), - blend_mode: group.blend_mode, + opacity: group.opacity().get(), + blend_mode: convert_blend_mode(group.blend_mode()), quality: tiny_skia::FilterQuality::Nearest, }; @@ -193,3 +160,24 @@ impl TinySkiaPixmapMutExt for tiny_skia::PixmapMut<'_> { Some(mask) } } + +pub fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode { + match mode { + usvg::BlendMode::Normal => tiny_skia::BlendMode::SourceOver, + usvg::BlendMode::Multiply => tiny_skia::BlendMode::Multiply, + usvg::BlendMode::Screen => tiny_skia::BlendMode::Screen, + usvg::BlendMode::Overlay => tiny_skia::BlendMode::Overlay, + usvg::BlendMode::Darken => tiny_skia::BlendMode::Darken, + usvg::BlendMode::Lighten => tiny_skia::BlendMode::Lighten, + usvg::BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge, + usvg::BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn, + usvg::BlendMode::HardLight => tiny_skia::BlendMode::HardLight, + usvg::BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight, + usvg::BlendMode::Difference => tiny_skia::BlendMode::Difference, + usvg::BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion, + usvg::BlendMode::Hue => tiny_skia::BlendMode::Hue, + usvg::BlendMode::Saturation => tiny_skia::BlendMode::Saturation, + usvg::BlendMode::Color => tiny_skia::BlendMode::Color, + usvg::BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity, + } +} diff --git a/crates/resvg/src/tree.rs b/crates/resvg/src/tree.rs deleted file mode 100644 index 970c83742..000000000 --- a/crates/resvg/src/tree.rs +++ /dev/null @@ -1,285 +0,0 @@ -// 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/. - -use usvg::NodeExt; - -use crate::clip::ClipPath; -use crate::image::Image; -use crate::mask::Mask; -use crate::path::{FillPath, StrokePath}; - -pub struct Group { - pub transform: tiny_skia::Transform, - pub opacity: usvg::Opacity, - pub blend_mode: tiny_skia::BlendMode, - pub clip_path: Option, - pub mask: Option, - pub filters: Vec, - pub isolate: bool, - /// Group's layer bounding box in canvas coordinates. - pub bbox: tiny_skia::Rect, - - pub children: Vec, -} - -impl Group { - pub fn is_transform_only(&self) -> bool { - self.opacity == usvg::Opacity::ONE - && self.blend_mode == tiny_skia::BlendMode::SourceOver - && self.clip_path.is_none() - && self.mask.is_none() - && self.filters.is_empty() - && !self.isolate - } -} - -pub enum Node { - Group(Group), // TODO: box - FillPath(FillPath), - StrokePath(StrokePath), - Image(Image), -} - -// - No hidden nodes. -// - No text. -// - Uses mostly tiny-skia types. -// - No paint-order. Already resolved. -// - PNG/JPEG/GIF bitmaps are already decoded and are stored as tiny_skia::Pixmap. -// SVG images will be rendered each time. -// - No `objectBoundingBox` units. - -/// A render tree. -pub struct Tree { - /// Image size. - /// - /// Size of an image that should be created to fit the SVG. - /// - /// `width` and `height` in SVG. - pub size: usvg::Size, - - /// SVG viewbox. - /// - /// Specifies which part of the SVG image should be rendered. - /// - /// `viewBox` and `preserveAspectRatio` in SVG. - pub view_box: usvg::ViewBox, - - /// Content area. - /// - /// A bounding box of all elements. Includes strokes and filter regions. - /// - /// Can be `None` when the tree has no children. - pub content_area: Option, - - pub(crate) children: Vec, -} - -impl Tree { - /// Creates a rendering tree from [`usvg::Tree`]. - /// - /// Text nodes should be already converted into paths using - /// [`usvg::TreeTextToPath::convert_text`]. - pub fn from_usvg(tree: &usvg::Tree) -> Self { - if tree.has_text_nodes() { - log::warn!("Text nodes should be already converted into paths."); - } - - let (children, layer_bbox) = convert_node(tree.root.clone()); - - Self { - size: tree.size, - view_box: tree.view_box, - content_area: layer_bbox, - children, - } - } - - /// Creates a rendering tree from [`usvg::Node`]. - /// - /// Text nodes should be already converted into paths using - /// [`usvg::TreeTextToPath::convert_text`]. - /// - /// Returns `None` when `node` has a zero size. - pub fn from_usvg_node(node: &usvg::Node) -> Option { - let node_bbox = if let Some(bbox) = node.calculate_bbox().and_then(|r| r.to_non_zero_rect()) - { - bbox - } else { - log::warn!("Node '{}' has zero size.", node.id()); - return None; - }; - - let view_box = usvg::ViewBox { - rect: node_bbox, - aspect: usvg::AspectRatio::default(), - }; - - let (children, layer_bbox) = convert_node(node.clone()); - - Some(Self { - size: node_bbox.size(), - view_box, - content_area: layer_bbox, - children, - }) - } -} - -pub fn convert_node(node: usvg::Node) -> (Vec, Option) { - let mut children = Vec::new(); - let bboxes = convert_node_inner(node, &mut children); - (children, bboxes.and_then(|b| b.layer.to_rect())) -} - -#[derive(Default, Debug)] -pub struct BBoxes { - /// The object bounding box. - /// - /// Just a shape/image bbox as per SVG spec. - pub object: usvg::BBox, - - /// The same as above, but transformed using object's transform. - pub transformed_object: usvg::BBox, - - /// Similar to `object`, but expanded to fit the stroke as well. - pub layer: usvg::BBox, -} - -fn convert_node_inner(node: usvg::Node, children: &mut Vec) -> Option { - match &*node.borrow() { - usvg::NodeKind::Group(ref ugroup) => convert_group(node.clone(), ugroup, children), - usvg::NodeKind::Path(ref upath) => crate::path::convert(upath, children), - usvg::NodeKind::Image(ref uimage) => crate::image::convert(uimage, children), - usvg::NodeKind::Text(_) => None, // should be already converted into paths - } -} - -fn convert_group( - node: usvg::Node, - ugroup: &usvg::Group, - children: &mut Vec, -) -> Option { - let mut group_children = Vec::new(); - let mut bboxes = match convert_children(node, &mut group_children) { - Some(v) => v, - None => return convert_empty_group(ugroup, children), - }; - - let (filters, filter_bbox) = - crate::filter::convert(&ugroup.filters, bboxes.transformed_object.to_rect()); - - // TODO: figure out a nicer solution - // Ignore groups with filters but invalid filter bboxes. - if !ugroup.filters.is_empty() && filter_bbox.is_none() { - return None; - } - - if let Some(filter_bbox) = filter_bbox { - bboxes.layer = usvg::BBox::from(filter_bbox); - } - - let group = Group { - transform: ugroup.transform, - opacity: ugroup.opacity, - blend_mode: convert_blend_mode(ugroup.blend_mode), - clip_path: crate::clip::convert(ugroup.clip_path.clone(), bboxes.object.to_rect()?), - mask: crate::mask::convert(ugroup.mask.clone(), bboxes.object.to_rect()?), - isolate: ugroup.isolate, - filters, - bbox: bboxes.layer.to_rect()?, - children: group_children, - }; - - bboxes.object = bboxes.object.transform(ugroup.transform)?; - bboxes.transformed_object = bboxes.transformed_object.transform(ugroup.transform)?; - bboxes.layer = bboxes.layer.transform(ugroup.transform)?; - - children.push(Node::Group(group)); - Some(bboxes) -} - -fn convert_empty_group(ugroup: &usvg::Group, children: &mut Vec) -> Option { - if ugroup.filters.is_empty() { - return None; - } - - let (filters, layer_bbox) = crate::filter::convert(&ugroup.filters, None); - let layer_bbox = layer_bbox?; - - let group = Group { - transform: ugroup.transform, - opacity: ugroup.opacity, - blend_mode: convert_blend_mode(ugroup.blend_mode), - clip_path: None, - mask: None, - isolate: ugroup.isolate, - filters, - bbox: layer_bbox, - children: Vec::new(), - }; - - let bboxes = BBoxes { - // TODO: find a better solution - object: usvg::BBox::default(), - transformed_object: usvg::BBox::default(), - layer: usvg::BBox::from(layer_bbox), - }; - - children.push(Node::Group(group)); - Some(bboxes) -} - -fn convert_children(parent: usvg::Node, children: &mut Vec) -> Option { - let mut bboxes = BBoxes::default(); - - for node in parent.children() { - if let Some(bboxes2) = convert_node_inner(node, children) { - bboxes.object = bboxes.object.expand(bboxes2.object); - bboxes.transformed_object = - bboxes.transformed_object.expand(bboxes2.transformed_object); - bboxes.layer = bboxes.layer.expand(bboxes2.layer); - } - } - - if !bboxes.layer.is_default() && !bboxes.object.is_default() { - Some(bboxes) - } else { - None - } -} - -pub fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode { - match mode { - usvg::BlendMode::Normal => tiny_skia::BlendMode::SourceOver, - usvg::BlendMode::Multiply => tiny_skia::BlendMode::Multiply, - usvg::BlendMode::Screen => tiny_skia::BlendMode::Screen, - usvg::BlendMode::Overlay => tiny_skia::BlendMode::Overlay, - usvg::BlendMode::Darken => tiny_skia::BlendMode::Darken, - usvg::BlendMode::Lighten => tiny_skia::BlendMode::Lighten, - usvg::BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge, - usvg::BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn, - usvg::BlendMode::HardLight => tiny_skia::BlendMode::HardLight, - usvg::BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight, - usvg::BlendMode::Difference => tiny_skia::BlendMode::Difference, - usvg::BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion, - usvg::BlendMode::Hue => tiny_skia::BlendMode::Hue, - usvg::BlendMode::Saturation => tiny_skia::BlendMode::Saturation, - usvg::BlendMode::Color => tiny_skia::BlendMode::Color, - usvg::BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity, - } -} - -pub trait OptionLog { - fn log_none(self, f: F) -> Self; -} - -impl OptionLog for Option { - #[inline] - fn log_none(self, f: F) -> Self { - self.or_else(|| { - f(); - None - }) - } -} diff --git a/crates/resvg/tests/extra/filter-on-empty-group.png b/crates/resvg/tests/extra/filter-on-empty-group.png new file mode 100644 index 0000000000000000000000000000000000000000..24e724ea9019adefccf15b51b3adef6bddb2a1ec GIT binary patch literal 1878 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&V3+lDaSW+oeEWcLf{N!P6;Gk= zRWom;0XwS?RMV z?$haeCu2&l*7JXv6#vcd*{aq5suxWEv#H!DJof8-=Dkm*@14B!)vEt;FI4xxnR_NQ z{O@-M{hz7tQ`YTzwV%D}q<*z}@zv1(wimqXZd%V=z3$ih#!(|j!*Vncjpm-wVq>(t f#8*)qv}aOT?2Y9n$-v<0>gTe~DWM4fjxlL* literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/extra/filter-on-empty-group.svg b/crates/resvg/tests/extra/filter-on-empty-group.svg new file mode 100644 index 000000000..f36b61d28 --- /dev/null +++ b/crates/resvg/tests/extra/filter-on-empty-group.svg @@ -0,0 +1,11 @@ + + On an empty group (1) + + + + + + + + + diff --git a/crates/resvg/tests/extra/filter-with-transform-on-shape.png b/crates/resvg/tests/extra/filter-with-transform-on-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..c8de330cdac699aa432aed12c8428d679485aea8 GIT binary patch literal 17558 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 AJ^%m! literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/extra/filter-with-transform-on-shape.svg b/crates/resvg/tests/extra/filter-with-transform-on-shape.svg new file mode 100644 index 000000000..619fef603 --- /dev/null +++ b/crates/resvg/tests/extra/filter-with-transform-on-shape.svg @@ -0,0 +1,13 @@ + + `transform` on shape + + + + + + + + + + + diff --git a/crates/resvg/tests/integration/extra.rs b/crates/resvg/tests/integration/extra.rs index 1c63df6f7..6fb86ce46 100644 --- a/crates/resvg/tests/integration/extra.rs +++ b/crates/resvg/tests/integration/extra.rs @@ -1,4 +1,4 @@ -use crate::{render_extra, render_extra_with_scale}; +use crate::{render_extra, render_extra_with_scale, render_node}; #[test] fn group_with_only_transform() { @@ -67,3 +67,13 @@ fn filter_region_precision() { fn translate_outside_viewbox() { assert_eq!(render_extra("extra/translate-outside-viewbox"), 0); } + +#[test] +fn render_node_filter_on_empty_group() { + assert_eq!(render_node("extra/filter-on-empty-group", "g1"), 0); +} + +#[test] +fn render_node_filter_with_transform_on_shape() { + assert_eq!(render_node("extra/filter-with-transform-on-shape", "g1"), 0); +} diff --git a/crates/resvg/tests/integration/main.rs b/crates/resvg/tests/integration/main.rs index 9cc73acdd..54f90d087 100644 --- a/crates/resvg/tests/integration/main.rs +++ b/crates/resvg/tests/integration/main.rs @@ -1,6 +1,6 @@ use once_cell::sync::Lazy; use rgb::{FromSlice, RGBA8}; -use usvg::{fontdb, TreeParsing, TreeTextToPath}; +use usvg::fontdb; #[rustfmt::skip] mod render; @@ -10,6 +10,10 @@ mod extra; const IMAGE_SIZE: u32 = 300; static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { + if let Ok(()) = log::set_logger(&LOGGER) { + log::set_max_level(log::LevelFilter::Warn); + } + let mut fontdb = fontdb::Database::new(); fontdb.load_fonts_dir("tests/fonts"); fontdb.set_serif_family("Noto Serif"); @@ -34,20 +38,21 @@ pub fn render(name: &str) -> usize { let tree = { let svg_data = std::fs::read(&svg_path).unwrap(); - let mut tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); let db = GLOBAL_FONTDB.lock().unwrap(); - tree.convert_text(&db); - tree + usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() }; - let rtree = resvg::Tree::from_usvg(&tree); - let size = rtree.size.to_int_size().scale_to_width(IMAGE_SIZE).unwrap(); + let size = tree + .size() + .to_int_size() + .scale_to_width(IMAGE_SIZE) + .unwrap(); let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); let render_ts = tiny_skia::Transform::from_scale( - size.width() as f32 / tree.size.width() as f32, - size.height() as f32 / tree.size.height() as f32, + size.width() as f32 / tree.size().width() as f32, + size.height() as f32 / tree.size().height() as f32, ); - rtree.render(render_ts, &mut pixmap.as_mut()); + resvg::render(&tree, render_ts, &mut pixmap.as_mut()); // pixmap.save_png(&format!("tests/{}.png", name)).unwrap(); @@ -85,15 +90,15 @@ pub fn render_extra_with_scale(name: &str, scale: f32) -> usize { let tree = { let svg_data = std::fs::read(&svg_path).unwrap(); - usvg::Tree::from_data(&svg_data, &opt).unwrap() + let db = GLOBAL_FONTDB.lock().unwrap(); + usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() }; - let rtree = resvg::Tree::from_usvg(&tree); - let size = rtree.size.to_int_size().scale_by(scale).unwrap(); + let size = tree.size().to_int_size().scale_by(scale).unwrap(); let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); let render_ts = tiny_skia::Transform::from_scale(scale, scale); - rtree.render(render_ts, &mut pixmap.as_mut()); + resvg::render(&tree, render_ts, &mut pixmap.as_mut()); // pixmap.save_png(&format!("tests/{}.png", name)).unwrap(); @@ -127,6 +132,51 @@ pub fn render_extra(name: &str) -> usize { render_extra_with_scale(name, 1.0) } +pub fn render_node(name: &str, id: &str) -> usize { + let svg_path = format!("tests/{}.svg", name); + let png_path = format!("tests/{}.png", name); + + let opt = usvg::Options::default(); + + let tree = { + let svg_data = std::fs::read(&svg_path).unwrap(); + let db = GLOBAL_FONTDB.lock().unwrap(); + usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() + }; + + let node = tree.node_by_id(id).unwrap(); + let size = node.abs_layer_bounding_box().unwrap().size().to_int_size(); + let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); + resvg::render_node(node, tiny_skia::Transform::identity(), &mut pixmap.as_mut()); + + // pixmap.save_png(&format!("tests/{}.png", name)).unwrap(); + + let mut rgba = pixmap.take(); + demultiply_alpha(rgba.as_mut_slice().as_rgba_mut()); + + let expected_data = load_png(&png_path); + assert_eq!(expected_data.len(), rgba.len()); + + let mut pixels_d = 0; + for (a, b) in expected_data + .as_slice() + .as_rgba() + .iter() + .zip(rgba.as_rgba()) + { + if is_pix_diff(*a, *b) { + pixels_d += 1; + } + } + + // Save diff if needed. + // if pixels_d != 0 { + // gen_diff(&name, &expected_data, rgba.as_slice()).unwrap(); + // } + + pixels_d +} + fn load_png(path: &str) -> Vec { let data = std::fs::read(path).unwrap(); let mut decoder = png::Decoder::new(data.as_slice()); @@ -215,3 +265,35 @@ fn demultiply_alpha(data: &mut [RGBA8]) { p.r = (p.r as f64 / a + 0.5) as u8; } } + +/// A simple stderr logger. +static LOGGER: SimpleLogger = SimpleLogger; +struct SimpleLogger; +impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::LevelFilter::Warn + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let target = if !record.target().is_empty() { + record.target() + } else { + record.module_path().unwrap_or_default() + }; + + let line = record.line().unwrap_or(0); + let args = record.args(); + + match record.level() { + log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args), + log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args), + log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args), + log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args), + log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args), + } + } + } + + fn flush(&self) {} +} diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index d9e98c334..c294a30d5 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -196,6 +196,9 @@ use crate::render; #[test] fn filters_feImage_with_subregion_3() { assert_eq!(render("tests/filters/feImage/with-subregion-3"), 0); } #[test] fn filters_feImage_with_subregion_4() { assert_eq!(render("tests/filters/feImage/with-subregion-4"), 0); } #[test] fn filters_feImage_with_subregion_5() { assert_eq!(render("tests/filters/feImage/with-subregion-5"), 0); } +#[test] fn filters_feImage_with_x_y_and_protruding_subregion_1() { assert_eq!(render("tests/filters/feImage/with-x-y-and-protruding-subregion-1"), 0); } +#[test] fn filters_feImage_with_x_y_and_protruding_subregion_2() { assert_eq!(render("tests/filters/feImage/with-x-y-and-protruding-subregion-2"), 0); } +#[test] fn filters_feImage_with_x_y() { assert_eq!(render("tests/filters/feImage/with-x-y"), 0); } #[test] fn filters_feMerge_color_interpolation_filters_eq_linearRGB() { assert_eq!(render("tests/filters/feMerge/color-interpolation-filters=linearRGB"), 0); } #[test] fn filters_feMerge_color_interpolation_filters_eq_sRGB() { assert_eq!(render("tests/filters/feMerge/color-interpolation-filters=sRGB"), 0); } #[test] fn filters_feMerge_complex_transform() { assert_eq!(render("tests/filters/feMerge/complex-transform"), 0); } @@ -311,8 +314,10 @@ use crate::render; #[test] fn filters_filter_on_a_vertical_line() { assert_eq!(render("tests/filters/filter/on-a-vertical-line"), 0); } #[test] fn filters_filter_on_an_empty_group_1() { assert_eq!(render("tests/filters/filter/on-an-empty-group-1"), 0); } #[test] fn filters_filter_on_an_empty_group_2() { assert_eq!(render("tests/filters/filter/on-an-empty-group-2"), 0); } +#[test] fn filters_filter_on_group_with_child_outside_of_canvas() { assert_eq!(render("tests/filters/filter/on-group-with-child-outside-of-canvas"), 0); } #[test] fn filters_filter_on_the_root_svg() { assert_eq!(render("tests/filters/filter/on-the-root-svg"), 0); } #[test] fn filters_filter_on_zero_sized_shape() { assert_eq!(render("tests/filters/filter/on-zero-sized-shape"), 0); } +#[test] fn filters_filter_path_bbox() { assert_eq!(render("tests/filters/filter/path-bbox"), 0); } #[test] fn filters_filter_primitiveUnits_eq_objectBoundingBox() { assert_eq!(render("tests/filters/filter/primitiveUnits=objectBoundingBox"), 0); } #[test] fn filters_filter_recursive_xlink_href() { assert_eq!(render("tests/filters/filter/recursive-xlink-href"), 0); } #[test] fn filters_filter_region_with_stroke() { assert_eq!(render("tests/filters/filter/region-with-stroke"), 0); } @@ -340,6 +345,7 @@ use crate::render; #[test] fn filters_filter_with_subregion_1() { assert_eq!(render("tests/filters/filter/with-subregion-1"), 0); } #[test] fn filters_filter_with_subregion_2() { assert_eq!(render("tests/filters/filter/with-subregion-2"), 0); } #[test] fn filters_filter_with_subregion_3() { assert_eq!(render("tests/filters/filter/with-subregion-3"), 0); } +#[test] fn filters_filter_with_transform_outside_of_canvas() { assert_eq!(render("tests/filters/filter/with-transform-outside-of-canvas"), 0); } #[test] fn filters_filter_without_region_and_filterUnits_eq_userSpaceOnUse() { assert_eq!(render("tests/filters/filter/without-region-and-filterUnits=userSpaceOnUse"), 0); } #[test] fn filters_filter_zero_sized_subregion() { assert_eq!(render("tests/filters/filter/zero-sized-subregion"), 0); } #[test] fn filters_filter_functions_blur_function_mm_value() { assert_eq!(render("tests/filters/filter-functions/blur-function-mm-value"), 0); } @@ -458,6 +464,7 @@ use crate::render; #[test] fn masking_mask_mask_on_self_with_mask_type_eq_alpha() { assert_eq!(render("tests/masking/mask/mask-on-self-with-mask-type=alpha"), 0); } #[test] fn masking_mask_mask_on_self_with_mixed_mask_type() { assert_eq!(render("tests/masking/mask/mask-on-self-with-mixed-mask-type"), 0); } #[test] fn masking_mask_mask_on_self() { assert_eq!(render("tests/masking/mask/mask-on-self"), 0); } +#[test] fn masking_mask_mask_type_in_style() { assert_eq!(render("tests/masking/mask/mask-type-in-style"), 0); } #[test] fn masking_mask_mask_type_eq_alpha() { assert_eq!(render("tests/masking/mask/mask-type=alpha"), 0); } #[test] fn masking_mask_mask_type_eq_invalid() { assert_eq!(render("tests/masking/mask/mask-type=invalid"), 0); } #[test] fn masking_mask_mask_type_eq_luminance() { assert_eq!(render("tests/masking/mask/mask-type=luminance"), 0); } @@ -635,7 +642,24 @@ use crate::render; #[test] fn paint_servers_stop_opacity_50percent() { assert_eq!(render("tests/paint-servers/stop-opacity/50percent"), 0); } #[test] fn paint_servers_stop_opacity_simple_case() { assert_eq!(render("tests/paint-servers/stop-opacity/simple-case"), 0); } #[test] fn painting_color_inherit() { assert_eq!(render("tests/painting/color/inherit"), 0); } +#[test] fn painting_color_recursive_nested_context_without_color() { assert_eq!(render("tests/painting/color/recursive-nested-context-without-color"), 0); } +#[test] fn painting_color_recursive_nested_context() { assert_eq!(render("tests/painting/color/recursive-nested-context"), 0); } #[test] fn painting_color_simple_case() { assert_eq!(render("tests/painting/color/simple-case"), 0); } +#[test] fn painting_context_in_marker() { assert_eq!(render("tests/painting/context/in-marker"), 0); } +#[test] fn painting_context_in_nested_marker() { assert_eq!(render("tests/painting/context/in-nested-marker"), 0); } +#[test] fn painting_context_in_nested_use_and_marker() { assert_eq!(render("tests/painting/context/in-nested-use-and-marker"), 0); } +#[test] fn painting_context_in_nested_use() { assert_eq!(render("tests/painting/context/in-nested-use"), 0); } +#[test] fn painting_context_in_use() { assert_eq!(render("tests/painting/context/in-use"), 0); } +#[test] fn painting_context_on_shape_with_zero_size_bbox() { assert_eq!(render("tests/painting/context/on-shape-with-zero-size-bbox"), 0); } +#[test] fn painting_context_with_gradient_and_gradient_transform() { assert_eq!(render("tests/painting/context/with-gradient-and-gradient-transform"), 0); } +#[test] fn painting_context_with_gradient_in_use() { assert_eq!(render("tests/painting/context/with-gradient-in-use"), 0); } +#[test] fn painting_context_with_gradient_on_marker() { assert_eq!(render("tests/painting/context/with-gradient-on-marker"), 0); } +#[test] fn painting_context_with_pattern_and_transform_in_use() { assert_eq!(render("tests/painting/context/with-pattern-and-transform-in-use"), 0); } +#[test] fn painting_context_with_pattern_in_use() { assert_eq!(render("tests/painting/context/with-pattern-in-use"), 0); } +#[test] fn painting_context_with_pattern_objectBoundingBox_in_use() { assert_eq!(render("tests/painting/context/with-pattern-objectBoundingBox-in-use"), 0); } +#[test] fn painting_context_with_pattern_on_marker() { assert_eq!(render("tests/painting/context/with-pattern-on-marker"), 0); } +#[test] fn painting_context_with_text() { assert_eq!(render("tests/painting/context/with-text"), 0); } +#[test] fn painting_context_without_context_element() { assert_eq!(render("tests/painting/context/without-context-element"), 0); } #[test] fn painting_display_bBox_impact() { assert_eq!(render("tests/painting/display/bBox-impact"), 0); } #[test] fn painting_display_none_on_clipPath() { assert_eq!(render("tests/painting/display/none-on-clipPath"), 0); } #[test] fn painting_display_none_on_defs() { assert_eq!(render("tests/painting/display/none-on-defs"), 0); } @@ -827,6 +851,7 @@ use crate::render; #[test] fn painting_paint_order_on_text() { assert_eq!(render("tests/painting/paint-order/on-text"), 0); } #[test] fn painting_paint_order_on_tspan() { assert_eq!(render("tests/painting/paint-order/on-tspan"), 0); } #[test] fn painting_paint_order_stroke_invalid() { assert_eq!(render("tests/painting/paint-order/stroke-invalid"), 0); } +#[test] fn painting_paint_order_stroke_markers_fill() { assert_eq!(render("tests/painting/paint-order/stroke-markers-fill"), 0); } #[test] fn painting_paint_order_stroke_markers() { assert_eq!(render("tests/painting/paint-order/stroke-markers"), 0); } #[test] fn painting_paint_order_stroke() { assert_eq!(render("tests/painting/paint-order/stroke"), 0); } #[test] fn painting_paint_order_trailing_data() { assert_eq!(render("tests/painting/paint-order/trailing-data"), 0); } @@ -1071,6 +1096,7 @@ use crate::render; #[test] fn structure_image_embedded_jpeg_as_image_jpg() { assert_eq!(render("tests/structure/image/embedded-jpeg-as-image-jpg"), 0); } #[test] fn structure_image_embedded_jpeg_without_mime() { assert_eq!(render("tests/structure/image/embedded-jpeg-without-mime"), 0); } #[test] fn structure_image_embedded_png() { assert_eq!(render("tests/structure/image/embedded-png"), 0); } +#[test] fn structure_image_embedded_svg_with_text() { assert_eq!(render("tests/structure/image/embedded-svg-with-text"), 0); } #[test] fn structure_image_embedded_svg_without_mime() { assert_eq!(render("tests/structure/image/embedded-svg-without-mime"), 0); } #[test] fn structure_image_embedded_svg() { assert_eq!(render("tests/structure/image/embedded-svg"), 0); } #[test] fn structure_image_embedded_svgz() { assert_eq!(render("tests/structure/image/embedded-svgz"), 0); } @@ -1082,6 +1108,7 @@ use crate::render; #[test] fn structure_image_external_svgz() { assert_eq!(render("tests/structure/image/external-svgz"), 0); } #[test] fn structure_image_float_size() { assert_eq!(render("tests/structure/image/float-size"), 0); } #[test] fn structure_image_image_with_float_size_scaling() { assert_eq!(render("tests/structure/image/image-with-float-size-scaling"), 0); } +#[test] fn structure_image_no_height_non_square() { assert_eq!(render("tests/structure/image/no-height-non-square"), 0); } #[test] fn structure_image_no_height_on_svg() { assert_eq!(render("tests/structure/image/no-height-on-svg"), 0); } #[test] fn structure_image_no_height() { assert_eq!(render("tests/structure/image/no-height"), 0); } #[test] fn structure_image_no_width_and_height_on_svg() { assert_eq!(render("tests/structure/image/no-width-and-height-on-svg"), 0); } @@ -1228,6 +1255,29 @@ use crate::render; #[test] fn structure_transform_translate_without_Y() { assert_eq!(render("tests/structure/transform/translate-without-Y"), 0); } #[test] fn structure_transform_translate() { assert_eq!(render("tests/structure/transform/translate"), 0); } #[test] fn structure_transform_zeroed_matrix() { assert_eq!(render("tests/structure/transform/zeroed-matrix"), 0); } +#[test] fn structure_transform_origin_bottom() { assert_eq!(render("tests/structure/transform-origin/bottom"), 0); } +#[test] fn structure_transform_origin_center() { assert_eq!(render("tests/structure/transform-origin/center"), 0); } +#[test] fn structure_transform_origin_keyword_length() { assert_eq!(render("tests/structure/transform-origin/keyword-length"), 0); } +#[test] fn structure_transform_origin_left() { assert_eq!(render("tests/structure/transform-origin/left"), 0); } +#[test] fn structure_transform_origin_length_percent() { assert_eq!(render("tests/structure/transform-origin/length-percent"), 0); } +#[test] fn structure_transform_origin_length_px() { assert_eq!(render("tests/structure/transform-origin/length-px"), 0); } +#[test] fn structure_transform_origin_no_transform() { assert_eq!(render("tests/structure/transform-origin/no-transform"), 0); } +#[test] fn structure_transform_origin_on_clippath_objectBoundingBox() { assert_eq!(render("tests/structure/transform-origin/on-clippath-objectBoundingBox"), 0); } +#[test] fn structure_transform_origin_on_clippath() { assert_eq!(render("tests/structure/transform-origin/on-clippath"), 0); } +#[test] fn structure_transform_origin_on_gradient_object_bounding_box() { assert_eq!(render("tests/structure/transform-origin/on-gradient-object-bounding-box"), 0); } +#[test] fn structure_transform_origin_on_gradient_user_space_on_use() { assert_eq!(render("tests/structure/transform-origin/on-gradient-user-space-on-use"), 0); } +#[test] fn structure_transform_origin_on_group() { assert_eq!(render("tests/structure/transform-origin/on-group"), 0); } +#[test] fn structure_transform_origin_on_image() { assert_eq!(render("tests/structure/transform-origin/on-image"), 0); } +#[test] fn structure_transform_origin_on_pattern_object_bounding_box() { assert_eq!(render("tests/structure/transform-origin/on-pattern-object-bounding-box"), 0); } +#[test] fn structure_transform_origin_on_pattern_user_space_on_use() { assert_eq!(render("tests/structure/transform-origin/on-pattern-user-space-on-use"), 0); } +#[test] fn structure_transform_origin_on_shape() { assert_eq!(render("tests/structure/transform-origin/on-shape"), 0); } +#[test] fn structure_transform_origin_on_text_path() { assert_eq!(render("tests/structure/transform-origin/on-text-path"), 0); } +#[test] fn structure_transform_origin_on_text() { assert_eq!(render("tests/structure/transform-origin/on-text"), 0); } +#[test] fn structure_transform_origin_right_bottom() { assert_eq!(render("tests/structure/transform-origin/right-bottom"), 0); } +#[test] fn structure_transform_origin_right() { assert_eq!(render("tests/structure/transform-origin/right"), 0); } +#[test] fn structure_transform_origin_top_left() { assert_eq!(render("tests/structure/transform-origin/top-left"), 0); } +#[test] fn structure_transform_origin_top() { assert_eq!(render("tests/structure/transform-origin/top"), 0); } +#[test] fn structure_transform_origin_transform_on_parent() { assert_eq!(render("tests/structure/transform-origin/transform-on-parent"), 0); } #[test] fn structure_use_cSS_rules() { assert_eq!(render("tests/structure/use/cSS-rules"), 0); } #[test] fn structure_use_complex_style_resolving_order() { assert_eq!(render("tests/structure/use/complex-style-resolving-order"), 0); } #[test] fn structure_use_display_inheritance() { assert_eq!(render("tests/structure/use/display-inheritance"), 0); } @@ -1333,6 +1383,7 @@ use crate::render; #[test] fn text_dominant_baseline_text_after_edge() { assert_eq!(render("tests/text/dominant-baseline/text-after-edge"), 0); } #[test] fn text_dominant_baseline_text_before_edge() { assert_eq!(render("tests/text/dominant-baseline/text-before-edge"), 0); } #[test] fn text_dominant_baseline_use_script() { assert_eq!(render("tests/text/dominant-baseline/use-script"), 0); } +#[test] fn text_font_font_shorthand() { assert_eq!(render("tests/text/font/font-shorthand"), 0); } #[test] fn text_font_simple_case() { assert_eq!(render("tests/text/font/simple-case"), 0); } #[test] fn text_font_family_bold_sans_serif() { assert_eq!(render("tests/text/font-family/bold-sans-serif"), 0); } #[test] fn text_font_family_cursive() { assert_eq!(render("tests/text/font-family/cursive"), 0); } @@ -1428,6 +1479,8 @@ use crate::render; #[test] fn text_text_escaped_text_4() { assert_eq!(render("tests/text/text/escaped-text-4"), 0); } #[test] fn text_text_fill_rule_eq_evenodd() { assert_eq!(render("tests/text/text/fill-rule=evenodd"), 0); } #[test] fn text_text_filter_bbox() { assert_eq!(render("tests/text/text/filter-bbox"), 0); } +#[test] fn text_text_ligatures_handling_in_mixed_fonts_1() { assert_eq!(render("tests/text/text/ligatures-handling-in-mixed-fonts-1"), 0); } +#[test] fn text_text_ligatures_handling_in_mixed_fonts_2() { assert_eq!(render("tests/text/text/ligatures-handling-in-mixed-fonts-2"), 0); } #[test] fn text_text_mm_coordinates() { assert_eq!(render("tests/text/text/mm-coordinates"), 0); } #[test] fn text_text_nested() { assert_eq!(render("tests/text/text/nested"), 0); } #[test] fn text_text_no_coordinates() { assert_eq!(render("tests/text/text/no-coordinates"), 0); } @@ -1471,6 +1524,7 @@ use crate::render; #[test] fn text_text_decoration_all_types_inline_no_spaces() { assert_eq!(render("tests/text/text-decoration/all-types-inline-no-spaces"), 0); } #[test] fn text_text_decoration_all_types_inline() { assert_eq!(render("tests/text/text-decoration/all-types-inline"), 0); } #[test] fn text_text_decoration_all_types_nested() { assert_eq!(render("tests/text/text-decoration/all-types-nested"), 0); } +#[test] fn text_text_decoration_indirect_with_multiple_colors() { assert_eq!(render("tests/text/text-decoration/indirect-with-multiple-colors"), 0); } #[test] fn text_text_decoration_indirect() { assert_eq!(render("tests/text/text-decoration/indirect"), 0); } #[test] fn text_text_decoration_line_through() { assert_eq!(render("tests/text/text-decoration/line-through"), 0); } #[test] fn text_text_decoration_outside_the_text_element() { assert_eq!(render("tests/text/text-decoration/outside-the-text-element"), 0); } diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.png b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.png new file mode 100644 index 0000000000000000000000000000000000000000..88e2c6fb13b6061e26533b5c49a7e0423e1e1268 GIT binary patch literal 610 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAY~lj6XFV_F~ENY1|-zLz+f?f zLE!*{!hQyh_dvzmeb?84sFEPRU;Lsv3m$J%Q_FdDMtIqk#syKgx)*#qIZKN1^2DoLjK0%!A~&U+{1pu( zCaHRhQG?2k3z`+2_I0cJmTz++_o`e;1(^)7heF0L)5uNRM84HpPo$E8aQFP02Mz}~ z9~vMsMk^u)2>+v7|2DkYz>lj a{K~tjLb=CFdf7HmO7V2{b6Mw<&;$TG%Jtp= literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.svg b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.svg new file mode 100644 index 000000000..95ef3047b --- /dev/null +++ b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-1.svg @@ -0,0 +1,16 @@ + + With x and y and protruding subregion (1) + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.png b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5f16e4bcade4689a1941651a1a51acc6a71b522e GIT binary patch literal 609 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAY~lj6XFV_F~ENY1|-zLz+f?f zLE!*{!hQyh4?xAtUnZmjDejUWzhDLddx!jj3HuMc|4=zcb2`w9OP(%{Ar-gYUSLdP zb`)W_@KadIr09S3F@er=Wx4?gVLyLXyysZ&9)BdRG~VU?+TP<#ZrRn>{zM+I*%YSe z9Tq0GDdprORc|t>*T0>)QWd?Y=|pZyfl{gyM5pUqH{O?(c66GG-;OVnB6HI!=NUwo ziTyu6sTHK~BvmP?`aSoRJ(uc$y)litl+dO}Q2|%Z%!(G-a&}VW`gG6rf2eB#(9xkP o+E(0;PVBlHS#W#vvu4-N;x!smRC0e`&H*J9Pgg&ebxsLQ095t#;s5{u literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.svg b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.svg new file mode 100644 index 000000000..de1223390 --- /dev/null +++ b/crates/resvg/tests/tests/filters/feImage/with-x-y-and-protruding-subregion-2.svg @@ -0,0 +1,16 @@ + + With x and y and protruding subregion (2) + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y.png b/crates/resvg/tests/tests/filters/feImage/with-x-y.png new file mode 100644 index 0000000000000000000000000000000000000000..e4a462a7459e0664a290ef67db2e72724c53a896 GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4Af+4N6XFV_F~ENY1~6)1U{E-~ zps=5T;{yZ7d!Vv;zN@AKDbA80zhDLddx!jj3HuLd-~O8gG+M^f#WAGf*4uM}XP6j8 z7#?i@t5?r=_tVw9myUUs)17y2xpr#0_i5exMW@c6-&pJ!_xj*>%bJh(MNE40G*bV^{ZhfchnxnHNwC4zqqvU2`H#NUHx3vIVCg!03vF83jhEB literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/filters/feImage/with-x-y.svg b/crates/resvg/tests/tests/filters/feImage/with-x-y.svg new file mode 100644 index 000000000..fdd7fa455 --- /dev/null +++ b/crates/resvg/tests/tests/filters/feImage/with-x-y.svg @@ -0,0 +1,16 @@ + + With x and y + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/filter/on-group-with-child-outside-of-canvas.png b/crates/resvg/tests/tests/filters/filter/on-group-with-child-outside-of-canvas.png new file mode 100644 index 0000000000000000000000000000000000000000..46c4b561d5f0fa652b2bbe76855bb4b7ddd3709b GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4ASDst6XFV_(STleI8fww?YS>N zimfEbFW7-Wz&^iV&4z!oK@y%Wjv*Dd-k#gY>0~Iub|BvIQdY9=j>bY}@d8%q*Cmnn zc^)>uKX{iV{JY$Xz+5(|tM^2oZ;5L6p0jxxk>HwL^!4}mYqsTP(gv`0 + On group with child outside of canvas + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/filter/path-bbox.png b/crates/resvg/tests/tests/filters/filter/path-bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..b1880cab2bb8e5645f560e530ef7b847799def37 GIT binary patch literal 1195 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAXOdU6XFV_Vc4a%Q7)Mx#PPk3vGXZ1^-O{{>Qh3&q`5i@UBCwcRRWyG_XSfRM=n zew|bN+NXJxuX0M?=ahQDCiIF;@D(%XS7y#HVDlM%tjX5{n$24h)m5wasw>GwcbBaD zSFz?6=QcBy)h``yZCjk+9TRI5#&X3fVp+!xzHF9=i4oK7{&0)3i~TySo)~(U=|6)O z6HmmhSH>GECLhWW{ab#juUubG?zCTn&9tuXvl>rM>scCe;FiH&mAo(=KhdtEwGA0n ziBSSodIH|}8ya68{mAfTmw}G%2i?okkFGhFI7UQmh;q&Mx+%Jd``{zFdzYfKZL0Jb zye)r(U7Q`fwmo=f^3I0F%=QY0g2?2VtcSJ)PfakqnY1zC+Lq+1wt{&P_6NDZl#gnJEr7i@jyK)nha#uA5;IyQYI%uHdN4MkT$91EQCY{Oa2AfHC}! zm(XkJwn?XdyjR&+=Nwq&IV)m+g8!Nq*FGNUdMD+#<^|80PEqNfuYpAGX&@09q8^sA z + Path bounding box + + Test that object bounding calculation for paths works as defined by the spec: + https://svgwg.org/svg2-draft/coords.html#BoundingBoxes + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/filters/filter/with-transform-outside-of-canvas.png b/crates/resvg/tests/tests/filters/filter/with-transform-outside-of-canvas.png new file mode 100644 index 0000000000000000000000000000000000000000..46c4b561d5f0fa652b2bbe76855bb4b7ddd3709b GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4ASDst6XFV_(STleI8fww?YS>N zimfEbFW7-Wz&^iV&4z!oK@y%Wjv*Dd-k#gY>0~Iub|BvIQdY9=j>bY}@d8%q*Cmnn zc^)>uKX{iV{JY$Xz+5(|tM^2oZ;5L6p0jxxk>HwL^!4}mYqsTP(gv`0 + With transform outside of canvas + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/masking/mask/mask-type-in-style.png b/crates/resvg/tests/tests/masking/mask/mask-type-in-style.png new file mode 100644 index 0000000000000000000000000000000000000000..1c8302e4455c15c0470239c4164d435361819ae3 GIT binary patch literal 1529 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxV1_q|50X`wFKrX|75W%pDfuVtc zVJ4WI1Sb2yWH*@X1d|fbK`;LfBXZ zfl^q-u?o^(F{UpDR)m3JK2uE<0|Udik|4ie2Sx^FCRP@9Hck$1E?ypfJ^=w?AyE-= zF-Zw&DOnkLIYk9!RTXtLO$}`=T^)TrLjxmI6LT|53u`M|8+$u@M+avoS9dp04{tAD zAAi5VfZ(8z@UX~;=&0D3__)M`Z;nB`nrb3 zrsmd`w)W1h?%tlh{s|K&O`bYs+VmN-X3w27Z~lUXixw|kvTXT^m8;jTS+{<}#!Z{I zZrQed$Ie}Q_w3t$;NYRdM~@ske&Xb*(`V0}zi{!=lE{Pg+D*Kgl{{QUL%&)QRST^vIyZoR#1$k*f`;BYZ< z!Pc9+t-5)K4N~^+mrp-t9Q!A- zPZ6Y%6R#ma1G&X?AY=rX9kGE0C^BFg|97$i{YGmdKI;Vst03i>zTL1t6 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/masking/mask/mask-type-in-style.svg b/crates/resvg/tests/tests/masking/mask/mask-type-in-style.svg new file mode 100644 index 000000000..95a10d07c --- /dev/null +++ b/crates/resvg/tests/tests/masking/mask/mask-type-in-style.svg @@ -0,0 +1,15 @@ + + `mask-type` in `style` (SVG 2) + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.png b/crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa749a9e49f5a032a899f85bf01c602ca52c295 GIT binary patch literal 346 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4ASD{$6XFV_F@X>x!xEqANY#`SnJY({=WH`;V?Kzq?2P6hfY^ KelF{r5}E)hI;`0M literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.svg b/crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.svg new file mode 100644 index 000000000..9bd0b5a31 --- /dev/null +++ b/crates/resvg/tests/tests/painting/color/recursive-nested-context-without-color.svg @@ -0,0 +1,14 @@ + + Recursive nested context without color (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/color/recursive-nested-context.png b/crates/resvg/tests/tests/painting/color/recursive-nested-context.png new file mode 100644 index 0000000000000000000000000000000000000000..038dede666be0a5497f32a156318c08e9e17ba3d GIT binary patch literal 432 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4AY~BX6XFV_F~ENY1~6)1U{E-~ zps=5T;{yZ7d$1rw>s$$tUe1yrzhDOj0sH)d3HuLt65ggFvZT{4U$okD_|De4j7(d9#&vtw9ydG?>HS)M%U}R;(;2q! z3lmwZue(q05a!om}&NtLB;Bh*@!XVonX#BOljU5#3p00i_ I>zopr0C+9HrvLx| literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/color/recursive-nested-context.svg b/crates/resvg/tests/tests/painting/color/recursive-nested-context.svg new file mode 100644 index 000000000..b75fa920d --- /dev/null +++ b/crates/resvg/tests/tests/painting/color/recursive-nested-context.svg @@ -0,0 +1,15 @@ + + Recursive nested context (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/in-marker.png b/crates/resvg/tests/tests/painting/context/in-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..302877f0d81f5fabac2f94c33cca10770abf2460 GIT binary patch literal 3799 zcmb7HX;c$w7A+7OBnmh}plLRB5b1 z0Ko3kw$`oyAO;uR(mO!Lu{_oWEKtr4zn%n3ARfFoL?CJ#Z!ud>k+99jZi9p^uw8-% zAB*ofaogm-!Il{;!71^Z&uA&&NbYwGGFy`Piiio)1jN>WSP1|uL0)Vfh}{8*uL0t# zK+F#S7-e)1unz#PssgeTK#T+sxf}TV6F}(=ASDE(<^X6fAU6)kjR8^uQDd+MApJ!Y z3kF6@1Cg>Ke}p^`qAW@X)&%?w0Dpbp;!yx=31E=`76G800kjj~j0J3i0ox$JG9Ivu z1CC_^$1(te62PDsI79&s)dHF=faY^Rxd%}02IM~g@*L41Y5vxi6z{nv`t%MVu8wYi zyu7@Irlx^`ITC5 zq*j6EzlW+iT&Ih#Dt$1m6EeCX_j~RLMH(-`8HRT&rmd;J$$k+e>|hxF=-4ss zOm%n#xq27{PxETb;SagPuPbvVj; z_M*}jW2c>pd&-AbBL%7R;Y?--34L?rGyIi^bo}-bmfygyoC9BD&1S+Zu&rUQsUUUo zbHhi+Mo~5P5(afWQn8m|II{R5#e+N$bM$@$;~_sPW?Zo|m@`O>V5DYk1avfMdsEXt z9z#SuJ2CxvntZ1{RWW!ug~`Qh+ftuJTTLc;F=S@0++^OiV91!(pX4oAQ;#3{mdb`5 zRe+VW+w`dqyOHBtO;l>6z{b4NeGRVg=W2-Zu|l)dNt35mov$saje3T47nM>cAHSB+ z-T6eZ^JC_se?7OfaZFPZ9aFMB${idfD;m{F%KNtljtbQQ^NHWh z@N$%2K1hfhhJ?`V<8IUaoAlNed6);=^p<MA3+c_%Az&&^y3a~OGL+jfayA8+;lmVHG5Gi*~Bx5KV>+AZH0^s1wi!;8tH!@LC#1|Eiw zz~H@$Xs@)0Y z`FJ5H+QH5o>*Ev?7{16wD)~{0yqZ6-akE}`TG!WmKd1lA3Eel`u(G(QU?AhBT|ZD- zbn>r7K_sUdGCMVqljlbz_D_t=INohN*d0}OkZg0Xm>i;$R7?5!7Y|I zCV%}R#qnqjO0p0H;Qaa*HO$RpMwen*%2=Q6cnKaCINcs7TMh>|fT4QG{bj*NU|l%v zt}CRNvlW!;%T+(T^FHGPYo^;oj)FbSPPV+ljAs5pcu$^0`m50A8&h3zMM^v1Gb=ax z)FXRqZZs+8|Fe}zdD0fJ@F{DgktKwS?b)bV0d(U;naSj3}pY0C&* zIz(elbzHtJ_QX(|0tJGMtII_4W3d*A+Ay9Sk9sul7ZdNnXUiPm>>o!nCc7K-(e@cw z`-TnXmok&VrFBh{krK{1HjlAm5uxwX&%Qo@=7ul-euZzG9~R{-VEop8cP1L2x89~x zv3BPZY@qb!yZY8qL8qYmQZ!d-h~nXRkIvqSeq z#xY&3b!{7<`5s)z?6e!K!fSWuKZSXU%eUn4gWwNj#VucxV8v!oR88ehWV2N&y#R=* zerQmv`iFur9r#qEChjm{snSaZWNk6GJ!A0&ZHX+1okAS!dEG45iYuv;=F+Lpb4B+= zS}}e5=Ik33pdsFwv~LOh+iV#P41oM602~W(g(6??DhLyU&qPNveOqo7gn{dbpeR8d z8ai4`TNUMA+uUK!M%S)E9d(Cggkx;M8?4^6@p5RTmmJKRiysku_bz?QWsns0H&=f^ z{zg*N)j6?-G+J9rrICx(VECQkuqM6dRi9%?p8cI9B81@a)&=B+^HhlUjCzzhj4?0L#~Nb^NzYMQ5(=s?cgxX1fn`zNiP!Vk8k z_C-E=dmA;3W}g+Cjo*#t#$3Q_3?-miJt=dvKt-KsM!wf4qUSaAn{ZsHRNLi<5M|kC zjc)dGu=N_U@7~bnv6^AOED0qB1S_Z^BW9zC)Vf9fscIu$f=Tm2lgh3()77N061(t) z_r1D_B5LF!aWQcCh|S0fpDr~pLFW_Q`y#n<)?PdZ(g1a!vMb45yu*#dr$ocDX{sKGIlrGo zv1gc9&n^9VFYqgv`&B2c!KzaY-6bpOEdSXDCM_**t%>zHt>uxq4vC0Q*0?=Vni0>=RUEzW!bK~?Q(<7C>l>c`T-bfer#{zYtjc1Ql}yswa%8ivlO}zt z2OEnQ9ta0#d{Q;Wx$G* z%LarW(VgM6x^SYBOJ|E|yD>hlRQT~L1@B!Neo|WDpv+g2)(VxJI-w% z1Gnw3Y??LC@RS6Xb!R7l8)#v9;RRvk!bLr7LT+A#nrMG9P{nMjlGs3}?64A{CP55c z+`R#|t4*yU!@A;)N)2|EdpTI0Gd$<}p+;KT>ivk%32)29@v?_|u5BQP7I}O8fJ==- zLP+|KbZw$%S*rFU16T-d&l}l^XawC_(Fuh|%`kn-)c#z`@Ni+|xF0=J4TIbY)8vK{ za1Y~YP_HLFd6L#+rC=poF& z&a@63)?D#`{gNL1U_pZ5*E{5b0h3LX37>4ZIbmo}B$HhkJ$UEbK*Z0M&>X%>Zs$wv z&vyB9UT%8AHWGlbYz|=JVwDv&A%BLSsqe}t7r!XBB>Rc>%FT)v@BHaNQI+pSvC=LZ zb0^6T@#BRV5k;w4k~a)B1m=qSAkvf0d%E5B(SB}-ULVpr{!;ksEwameK>726R<{J& z`eMJVS|0$sumlba=U8yh!*&`4i+k?O)3VQpV`!QG^%^4AOMJStXU6ReNnJ*HvPS0K TnGEoM6ma^aqjk-3Y~sHFUztHY literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/in-marker.svg b/crates/resvg/tests/tests/painting/context/in-marker.svg new file mode 100644 index 000000000..bf02fae54 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/in-marker.svg @@ -0,0 +1,13 @@ + + `context-fill` and `context-stroke` with marker (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/in-nested-marker.png b/crates/resvg/tests/tests/painting/context/in-nested-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..d68f2173b66efbedefaf41a875f918255fe923c8 GIT binary patch literal 4105 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 + `context-fill` and `context-stroke` with nested markers (SVG 2) + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.png b/crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..302877f0d81f5fabac2f94c33cca10770abf2460 GIT binary patch literal 3799 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 TnGEoM6ma^aqjk-3Y~sHFUztHY literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.svg b/crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.svg new file mode 100644 index 000000000..da37e8cab --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/in-nested-use-and-marker.svg @@ -0,0 +1,18 @@ + + `context-fill` and `context-stroke` with nested use and markers (SVG 2) + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/in-nested-use.png b/crates/resvg/tests/tests/painting/context/in-nested-use.png new file mode 100644 index 0000000000000000000000000000000000000000..fed662cb0b87d514b1728ec44342dddf6e541811 GIT binary patch literal 2928 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 M?e67v+l74kFUwS!i2wiq literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/in-nested-use.svg b/crates/resvg/tests/tests/painting/context/in-nested-use.svg new file mode 100644 index 000000000..2e78f46c8 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/in-nested-use.svg @@ -0,0 +1,15 @@ + + `context-fill` and `context-stroke` with nested use (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/in-use.png b/crates/resvg/tests/tests/painting/context/in-use.png new file mode 100644 index 0000000000000000000000000000000000000000..1df5a7510c2deccd74099f01f37be34f0a7eb9e3 GIT binary patch literal 2949 zcmb`JdsLEn8pmHq(!o@85>3rpTCjjRT~yS(B!cE8Kuw`iCzmuVGd0VybZpF9rWw&r zQ7Q~YCaEM{XYx``1vBCWwP@13g_&gOnv%DD(RR-6IXipKp0nrt-sktc-{0qXp6~nn z>&+*Jko4eYZ~y@G0{jV70DxdMkIphs3E`a}fm;JPD9jJs0bt>u5loiyCGijakJlyO zUm|n{fXo441PdYa02It;0sTS1qz%xX0JO&egMPrE577Py=zIXO0N@A$NU{dvoq!l` zAjS)bBm?9az&{D_KLB`V0p6K_YYE_53^g-)sVTybG(kr7CSxWPDJ4EbYoQ8CB zOdLKe;QAmQHg*T$-%f7hY=_)jXzqhU?%#1uMIc<7saVbWe112YjbejAv+^~eSrD1H zr+^=sY3&>whP|d0P|K;>A6%P(yzuJ1h>SH^6Bkswvt2cD&%QbNh2`s-aIAs*!`Fv4 zHYZ2+>au3&wHEsfYFwHtcU!%s6wL9Ny4RI3Pt|IgAmIzdm|KvQ1Y_2jtcu;`b#tyx~T@oU%+dR09mbk#Wv9j){FlXJ%Pu`f$ z*|$B0es3qQY_~_*t6@D3>Cmlw>eS6tSatp7b;}Tvwx%707OBr;lIO4bxI`vBP#!v$ zwW1r@Q}xFBoZz#%#jsvaRvZ+XrSncl3$c2+tzWF=<+oPtBM<&YH4cH~1Er&$qruIG zVT#7j=O<2nwJe8fuH|%J3+DWG&P3IB(ywenyaf^7K{_@a)BH@GouiM$4D&W;)Nltg z7(2A~?)S3-DPw!(J#XvA%+}qx&WL6w1Pr%oZvIAa>g&4|akuQTW(0qp99r0_ANS3oH)!1>TFVzKiJkJr8)s+{3j{IzpJG1l zUohSOfN81zty}bu*nIk#G*>CU-k)Y9w$PlOI4iPa;uUjP?Q|-!YaOhA(K26T5c13GEL|7jSJR+6_*Zs^$p?@ z6URduva}iAig~tE1`CjX1cwpeMB)O^oN*Q$f_2bcV3@Wt9@aX+% zG3ujZP)w&{s*E;_yC~}1mU;EYCVz(-rs~+mF{`ot{5j8cEcLumP)xw%qnEF_E%dfW zIjRgxb*rKi-vb?Io#5P>hz%F-ET|^o%fElokd;SKu8f|Jyb#xGqqI>R zSNncc7avMrI=XWZ6=lvE^4i_vTQ}oO4#cpAPI7YNr-bEfXOvVM?EC(u4{kr1t*2>p zJBO~|V#|~s1b1vPUBSbe4|)(D;QlO?gd$}rpqUFK+z`4@iI=s#m-5(M=y!9RYw^(} zoG`R&24CFvu!#N_a@R5Zwzep5TsW#QPb;dy_Hg(wIGHwuY1!_s?3E3#wCylpgmOjhTYv4x&>#9mQEebgJ0P1Cg9sL z)KLbMK2b2(jfd^cT+Csb&SjekIZ|hsoSHAnm`8LvysV=H8cW7^c@V@>=WuyD4)t!t zrgGQqw_J9j53SpPq7nsvElyn~$<7s|y|{XcJ`#$IDz4=ms9=9)`E|!)l?ejA8>4a3 zRI0XRUJs(BZvG|tVLp9?irfWa)m(O^2J=wf5b)PBK2Dm-)N7g7Ng%d7`C%5ll7ie> z{2iydoK0uJx?`=C?gIXD#=*MKmc!&gn4~{msW~%aHMh0kd%bZvs03?;jZ#;@U%@CM zxDU;`G10y>-!^@GeGy$k?h32(ZE@tWZCRU4RICYbqz)sP=sq~RmWkd%<7u!w`c`sR z64<}Gf^Edwv>L>A2>4oz-$)>4#6ANp zjWUiBc#9?0_`fCKQzUL(yw%C`rhd3!YS&>K@Q&)&Q3B7Y=fm^To~j-5rWH@zFyNTX zp7GeZ{@94~oP}=v)sf^Cz7XJ7lX;@gRIdIxJ=*}@*B<2thu8dGz4ll7arXCnqQAP8 z7QoCrn}el^{`#!+zirLWDl?%YmpUDwif3jnJ_C>wzmQNTTpe4%e^DU7FNAP&OHB4Z E03d2ni2wiq literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/in-use.svg b/crates/resvg/tests/tests/painting/context/in-use.svg new file mode 100644 index 000000000..0ebdf9a72 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/in-use.svg @@ -0,0 +1,14 @@ + + `context-fill` and `context-stroke` with use (SVG 2) + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.png b/crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.png new file mode 100644 index 0000000000000000000000000000000000000000..896cb24e47b79869f57537ae11245c406c7a04cb GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAmtR`6XFV_L7)MQAPi(KoDETc zNir}9oSP{ER3=gqtuj10|S$kr;B4q#jUru z7}F0sNF0B-``J4w(}ueL>mzS(uylE`!#?ZSy!l>wj32*9=|^P+N9aCYx8Y-NsWjKF zyYAXgKkwCe)x;pC6R{!TuZYgUCRT1B8$xbi0kJnEaDvH$LO}L6*|0kj8|w6dqF{c) z!6sD@yNT5sOma^Hv4PqM88kFCo(=!ezab&?{hA$XuknW6u(J=WYtyaMvwnTa`=xk9 zV6t3->(|N#(bBo>5gS97v96Y1F3!e%ZJX@#&<)>`6PwP@UvV??;14_Qwu*?yTUxfR zm_KcUSnPT4h>4*KSXbM}A87iz>!Yp2qr|uV)75@_DV#NTYR~)AigMQ??pZWloo?Q@ zXR}=YB(~R!|GfNTj}bS(AYB@E>Y4#duJCJKmnhl9Rq?%DChQ!RZ|i}nh{4m<&t;uc GLK6TCXa4a3 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.svg b/crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.svg new file mode 100644 index 000000000..897cfdb1f --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/on-shape-with-zero-size-bbox.svg @@ -0,0 +1,11 @@ + + `context-stroke` on shape with zero-size bbox (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-gradient-and-gradient-transform.png b/crates/resvg/tests/tests/painting/context/with-gradient-and-gradient-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..ed34c4c4936ed13f569b9c0631c57a1cece75ecc GIT binary patch literal 5202 zcmbtYS5#A7w@n~Gs7ebRqz8$JbdVw%1fuYPw9rEfO}g|>1f)nWN=HBhB1VuRJwZV_ zB1Jk17@B|ukS6!={r}UwkN4q>vG>|*&pqc{`%uc_`BMqi_tZfK;i@EV7LfF1&K*H%C89Clz`#Ix z6UIz}h%N*|4=>!@+~hJj9T{9X0M?w6OjZiq|L8H}?KBBp!8L|4dlAX>yTex2)>mrW z5Kh!FR8}@N!!I^cvUs7eM<$L~XQ8hrJ>~ctgeVc+JRupA4Kx75!NI{(ZISJqoSgh2 zA55ee9vN{U&$U1xo7OzpU#RU(bU(c*tsWi}8CifpVWVyh9oNZXr#&bh#6`r?@854~ zt3O3JL}WPNF2JBL@31f{zu}3&yEZMhz%iMbnW!hf`#yd&+<%UZ(IP~d6`Lh?AF*}o znDAf~Cj1CMj+|a+oUXv!!osA{E8kh)ca6P3~h4BF5&!gZ>F2bar;yVNE)(X-L># zLPx#Wu*E7ITJnl@hyFx`hK348ZARQ%0j_}IL}Z;0bf-^pYkS*t=6gW*ATRM|DUl)| zAi&;)?;vxqr+Z{2mZUd?Rha%2Tr-%AVMZ)H-7K?V!+-yNUR;iVh0C%R5xf4oJKL)a z8Pmu=PgmYFG?>WNOG|`7k}%->{CrvL{(6E=7+rDSuQNw4H!<`jG~Bp#zP4)Pl$D&a z-R-qE_VMekB7#9SK~NNZ>abT9=JhszsF zN&)Wp@M`<{`LSzO9UlK}Y5K@OmY%DQxG*2NICr)+9-Y9g$w{@;Ny%Z%W^Ar$$n0do z#-64{-~0fhA}v}@`HJ!ru>zM*K|y^l+KP%9M3RVHfJ6&Jm8Gs%UT>ojIIC+?lwI0| zCA~jb^?i%myOPZ@>~|qGa`ftne%@_z>t2w<_Ns}U_Pmwddmr9m@-ND@X?}$ufj)Me zET;j@qh#rI1R{U^!4dd(Oy%_b#@JTLg1#t~8}CRcKfCGV-sO?9k2+^@2fe>kF`n1V ze0XWTDt#UR60-J5_IA_1kHcj-8B(5v(Ei;&`Fd!l#9J$tsk^p;C}Fzznh*xZ!xLjX zn~v+JHu*xzC(^}|z~j-M zlE{4cr1$o4^3rtk=iCnqSAJYYAehs`)X%Qv4{cnZA^W~%4D)-f&b*z&+%rOAVWy2) zx+9ipC?v3?p1-&?PxV6Bj5qd2s-xJR0^1aIygjiH)`ar5q^)&to}8bbZF_m#GJAi1 zYJT3jtm(|IJ~brpV7fi?7crGr-%y4{lFBgp!?zMw?PmEa4p zsF`S~pT1ut?{Vt`a-1E{5uH6kmV9e99DDcOuun|?Z%fi&U+-ccq0;_aH=ln3W-8vc z40nWGc#t>CrC#!Kt#Q9#Te{2Ofzxpbfiq6}DDe|L#JnzklteJ*?36XA#_6h@JR^=_ zs6N^SNyWIvZ80%XYG51n!13g*22jo8-p&ygLvFn16seH8;Bot@XxpPPjQ=sXQCnJU z=SM0^D~r0Ovl!#XaG)LaAB;tRfsFl~UkK(3;!(V3917x`tX=G!aB>TUlV?w{t%xtz z!c`FIDyBnpEacGK__RE#5q3=8g<|VRYzEx;vnuEW)Uh0m*m$C!5Rg+Bb`;1>(M|!!wDVG*$1xBBI1gPT=@R$VSf|C_NqB}6*BbLk(*pbX?}S#~{iKx6;F4#wK21G}al&)6*~WUE$D-ti6y@O*E(Dpm{PX(( zZcSa#!BH9D5|ur;dlDceAJyvF0npSv=B$PIhHrQuepY4o$>+{Fp4@o;@2r1z9bWK| z1p!94e~2*V(1e3c{J!}CAutdU(_305qd}DJ1zr%B|h4YjAf0#RjKH zFLCd3x&vVoGOkaW=SGZNr|NU{R?{zcnv+m+oyz0dEO|lviT{;0PTXTn|0b|qDJPX| zri)wEyrMI+gj^A5IP>ltyzAXrA4rpyv+57(?d&`YzTUH7;iW6NbfxukZSHG zh2}mtHY*qsv$}25#KL>=+AY!Bf#oc_aiEnSIYKp-Uf6}4CB%Z5>R%}9eJWxo!f{l70+kkFCJ+BgZ`c6<@ zR0du9@TW^h24}c;KY#4C^BKRtT8tspUOcK*n+sufldFo?3hajY>FApNpJb+F;U~Kh z8{!8~??KN`DEvF7s(Ct|KkofiFA+nqqOG9B6tI+4p7ro+{TI+zu2XhTHquGp957o% zKf<*L^V3e)iSXCf`qC9G{gc`=4(bn>D=40>!t|UH&2y4Fw9yPXlBEneL|@S?M*wzJ z5%Dh{#QCAw{TMF1c=M|jMTF=!sxu-lA~&d-9^IpqauU~Yv2(pX%KwIJp%7uVxJPe-1Hcbl# zH)SxGe#zs?)%`bz6i%+c-}J|zo{sGN2@`(o$H2oW1Q6P?ygKG(VIwTO4$cVby3d66 zPzjhR33T@6UCdin#M_^qD$FXkS?W^PcrTs^@pxdPL~tf`UJPrBvW-xYv)oZYlS9{r z;Xp%yOEevjW~?aF3Q=ND+_ z0KERDo+U)QDHq_=e_iv|9^I~0At zUDiAuD8A=M4ZM}-_8;xo@B&^nD!p2|gs~F6-O2g7cls?0irT%Sbz&&Ry;6gz@Rtj} zx0?8ENrN-Dck0e$6r2lRf2fUBjIoJxD=G_y>`^@#lM=hn^dE!(Xf)jOP@6s3U;a<@Bs{??W6gLveE*(MJ@;w$^)-}go4<;v_-hJTi} zx4&=K(Tm&($_xn175&W;k|j~g`hZf2{)V&CyXNtyZ+cIkM|2eLAK9497jPmf-}31j zNvb|eAO<@!&$$}$*!26;ATtAIb4P;xcwqMUzH((_7tyG29{xvSbnRb9dYGE) zWJ<0ZPL0OxY>Cd-G9XA#eH))C_FG&ialuUMTWc^)KIlTad5A|0^eX-g3^tz+6zAQ~ z^xSNjD)t-Bk^P1*t9oyJ^J7=t(+z#4udk~0pAZ6w!kwFq&kv*!b+6v^chUK3lg$0T zrH_AM(xOSE&Kb%I?6zK@V(m}#47%L1TT@4q5_{U2#ee!tH=Y6UFiMuZ^%m#d(0m8Z zjaD$4#MxG;NfQy9vD#r~)2S+w$2;0ZzYnZK_L9UVa`xgi$OY|N>y&}a)v!QXEy@8? z)X|sA+qlXX99ZF`&@b#>zt$7J`(Zj=)1%qJbKpS15ZuI9scCvPMg&jaPOp75@6QwU zCyxd^8f-;4WKP9rmw|7c~hh=zS_o^+m@ zp2?e>pLW4WIL8sZ-8eh-8HolVA)LRIHFBB9ePC4?(d-BOX5-8Hhid2Yee59uNa2uw zQkKZ;hRvYZ0&Xo0=S;ePQhFnY-(KHNYh`=<2Ml<6<-=oRIW9>~wih$&H@$4x%`X@4+yob~B2uxg%wYb7d-{Trxx{C5cm8SnF+00Edyc zpsNg*>0R;=i8U6lKZ?Lmh?(;j=$|N)J>NP2j2h=ImLlJE=qiPZ+hvGRN}Ai{hwkjV zVI=yuB@o+rF}iKKP=prc9m9aZud69sfO4I{;Z5a{Z(TEVT75SZJbFaWG}=G}%Q=c? z@TW)6JBE#e-3_ixV(FfGGr|b1VwTJOs%J!zyNq}|6Mo1b>+S`_(reC9J9vKjAv}@( zY_ + `context-fill` with gradient and gradient transform (SVG 2) + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-gradient-in-use.png b/crates/resvg/tests/tests/painting/context/with-gradient-in-use.png new file mode 100644 index 0000000000000000000000000000000000000000..25b962384a645924e88a77acaedf51ca91ac3526 GIT binary patch literal 5149 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 XYE8F25xqKK?Z<&Va~4x^ijed_b>?^F literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/with-gradient-in-use.svg b/crates/resvg/tests/tests/painting/context/with-gradient-in-use.svg new file mode 100644 index 000000000..6b13d07a4 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/with-gradient-in-use.svg @@ -0,0 +1,28 @@ + + `context-fill` with gradient in use (SVG 2) + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-gradient-on-marker.png b/crates/resvg/tests/tests/painting/context/with-gradient-on-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..4816dcff795cbeae2406bdd5fbd0cd1f5ff1c420 GIT binary patch literal 5768 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/with-gradient-on-marker.svg b/crates/resvg/tests/tests/painting/context/with-gradient-on-marker.svg new file mode 100644 index 000000000..4e12ecbe9 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/with-gradient-on-marker.svg @@ -0,0 +1,20 @@ + + `context-fill` with gradient on marker (SVG 2) + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-pattern-and-transform-in-use.png b/crates/resvg/tests/tests/painting/context/with-pattern-and-transform-in-use.png new file mode 100644 index 0000000000000000000000000000000000000000..f52fd17b0edc059546dc79d7662e9ea88ef4a1eb GIT binary patch literal 12751 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 + `context-fill` with pattern and transform (SVG 2) + + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-pattern-in-use.png b/crates/resvg/tests/tests/painting/context/with-pattern-in-use.png new file mode 100644 index 0000000000000000000000000000000000000000..a6cf24ad6b78d2afec207de8629b9d2140327506 GIT binary patch literal 5808 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 r+cYebo9{9 + `context-fill` and `context-stroke` with pattern (SVG 2) + + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-pattern-objectBoundingBox-in-use.png b/crates/resvg/tests/tests/painting/context/with-pattern-objectBoundingBox-in-use.png new file mode 100644 index 0000000000000000000000000000000000000000..dd6bff93d7a595c5b1721bfbd41e48e4fdfcdabe GIT binary patch literal 10306 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 o(2$+&7sUF!S0634w?h!!=TW + `context-fill` with pattern with objectBoundingBox (SVG 2) + + + + + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-pattern-on-marker.png b/crates/resvg/tests/tests/painting/context/with-pattern-on-marker.png new file mode 100644 index 0000000000000000000000000000000000000000..9bebff6e46a75eaba599f6c20340fc9c083de6e0 GIT binary patch literal 2204 zcmah~3piA1A3t)*jJsJw6e3zoDUy4rC~D<%87Hxv6h&ofM{xGI+)Lv30pR6v(Ayb6hygJeK%l(5yuaymF~4G68JGR(5R?S% zj@=EhATb#$R#u?XMJJGF`A%GR7yJCBBqSs-KVJ}2YywE^!~C27ssjiSCj@0OfIv_J zNGBe}41oWYaMiS;tiu1uw#qY@jMXInI|Nkt`9>k2 zpfyrb(lWBLL^*kRMMY(0WwN@KmbSKzuI{#N=2mtN4vvnF9v*&20t17BPKJd=M9^ro z1O|hdlA4*7m78BsR9wudz_6dV^^YDs?(FR9>Ulo!%h1s9@YwkH%*@-j0>SJ%;rkCF z(fr57#gy2fO%No*cHQgbec|QwtE4DD*Y(YUBQNgf+Ia2>JEl0Y2R-`%|Hyb8VRp@va!yPsOmNuq4Y|%vsr@{}O zygYL~bbA1aJai4?Kcm8QHAKKoa#r_4x-(?iGdD01it)F~vhNsC9{M2N6Xh8HH9r6F z-;*IundYt9#OC|>%fay#Xa)2uw7nRf;I-0@N8i2d)d2h;t zHfY&kp=^ zPUqSW&<4wc1pCr|iz<3sf0reM;en*cFXv#DPY#>_>$Ij|p_OR{m$#lEu}OzB8J${}o)f^kwrup}-M!q3-Oqfq1C(7tj9^OQ< zoea@A`Bqjh(a{h!2G2I;L3qq4i@=dFLn7UDNxTa($M7&IT`(&&W_HGiUAP48+awXf z-IGO_(rjgMbJpfzhR=X~!!g`XO9Fr@0L-QGFk=wCn+&2;-l|Mp&xIMeaO)ic+thv| zTH{_9rb{9_oK*iysOyg5qk-D`C>1h8e3*Smpyv4{>Ll=jBFGjeHGxalEiN@M{YWzwj?$;hMok@#ffWOtJNi`^i z%eQpi43jkPBo0y@Hb5xdozJ-dsd&#P;9dE$@s) zrP*49$G2U6o^|`7(&pzAzE2G_!tK-qPq^bpd-VdNcb370o;uX0n@HhyJ(<~@w5Bkr zi-)6kq`?#MB#$M+KpMF29fpna{%x!BA zW@S$})VRVgb`HG@SvslC>-#m|y(I9fP1EV_7Rt^GHD%Y1M!NOIqvz}@Tg+*U-gE=1 zK0}T|YEtIN*EAS6D~9iq^o66wl6#56^6gFX9HLc|aSMU-{kLAI%O$MEi^k@v>Y$p# z8tvojgXaRv8W*z+P*YQVizyXeGo*dqS`@dg?2-d~AoI-p(25qpt zKiKfV*78ynxr)|5l2JD+87>=`yZF(?yZ)O@uB2Mspv8q=MTg2{L-jh-OabSgZAT{K znqrPl=MVVq?!>f@)Xh(3cUhv7_XZk^dcHdrBQFXK(*kI0}rG4})c}dNQOp%um=| mnZLz$E+uMg%_ + `context-fill` with pattern on marker (SVG 2) + + + + + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/with-text.png b/crates/resvg/tests/tests/painting/context/with-text.png new file mode 100644 index 0000000000000000000000000000000000000000..b44e1c29080a1c47426a54f051a57dafe49beeb0 GIT binary patch literal 6611 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/with-text.svg b/crates/resvg/tests/tests/painting/context/with-text.svg new file mode 100644 index 000000000..1b2f92418 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/with-text.svg @@ -0,0 +1,25 @@ + + `context-fill` on text with decoration (SVG 2) + + + + + + + + + + + + Text + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/context/without-context-element.png b/crates/resvg/tests/tests/painting/context/without-context-element.png new file mode 100644 index 0000000000000000000000000000000000000000..4fa749a9e49f5a032a899f85bf01c602ca52c295 GIT binary patch literal 346 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&7G|JGckpC4ASD{$6XFV_F@X>x!xEqANY#`SnJY({=WH`;V?Kzq?2P6hfY^ KelF{r5}E)hI;`0M literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/painting/context/without-context-element.svg b/crates/resvg/tests/tests/painting/context/without-context-element.svg new file mode 100644 index 000000000..b84335e47 --- /dev/null +++ b/crates/resvg/tests/tests/painting/context/without-context-element.svg @@ -0,0 +1,8 @@ + + `context-fill` and `context-stroke` without context element (SVG 2) + + + + + + diff --git a/crates/resvg/tests/tests/painting/paint-order/fill-markers-stroke.png b/crates/resvg/tests/tests/painting/paint-order/fill-markers-stroke.png index b345dea5a6bcdd9da3c8d40f3ec56886749258ac..1b229ad341a5c0ea651e644beec23f4ae9faa49b 100644 GIT binary patch literal 15252 zcmeHOe@qis9B)B^U<$HCNF+)oCN7(-bIC9POt)ENYzEvSl7SYJk(#Yz&BAJ1CU=D_ z>g+~0%|O-w3&!Xm+GGo<8+BK=5k>?nF6=T0w8AV1k`}H&i`To|do8u?=u(sYihGyz zpT6(Ae((GJeBZs#>-Y2FzM=v_;>JWCk0&U6N&Fg*$KMV<331#v>e>bckC$6sD1KqT zia%@Pf3mgk^wz@1v>nae1!A#ODm_rbT}p5*_5U5$d*IfkrQlbj(mi{`A9U-R6D@nC zrAmcTS!@A6xwl<2VSeS^p_0-=3!)Kgf~v}9yfWR&~xZXJlqHFE(&!T6-gO9Ied3K_1 zOaNfy?OvIMw}jz>GT8j#0NKLR{Ili^R9No+bm=NMl*Jdi0Uc5kL3!mhYw0gLO z?Ic56?aIw;8O)B+^vfIA2`l3w?fK`GyCcLt-!d zT>4f|gz2Q@K85S8YFY;KW}Pq7c{XA9f3DD{eKsbDkd?GS8P3S5TpgYTORiipvxE3S zjxWoo_FEBgzNKc?!d79EbA}#J7Wj49F1yk_TZ~sp_H(7OI_M>0gV#e0IP#b-71mI7 zaQu9Ri!@!tj`YFOa+R%1_Fnf$W4>L<536kzwJi^|n#u?c%W32Qg8Etr30|fJvxDG-&jwFZI$% zg!istK>KnrSi_f<^O>qoAK*2_!f`ypTBDun60t^4OD~aXu>>ddS-4W2$+)Jy7r`Q` zlm{d040w4kEp&pUG6)pG7SUjjqJkja+7Ds$TY+=S`vG)@SVU6q zG6}Vaq8dYi_eeui^f8{U%4%(kXOm1zGFXJk3!{X<_+6^d?9fQ`Y+m@=3r#^0Lx4f` zf|d)gWkIVYdRd@%CE6;Wof6t8g2T55XIyBXjP}WBpN#g&XrGMs$%rPPGbMDShE5dG zfhsyrhX2tcn&59>Kr9KdB*c;sOF}FOu_VNj5KBTV39%%^k`PNmED8OW6zFnCBoUEB zL=vMXi9lcw-2NWYPK+9kQNuA*DxhBYhmb!6ce?*Yz3|n?s+*dyn-x_1@@yvTTfab# zabgA1qI&aavW^N7NJJxlcr>LaP@W|ZmP|w=h=6jrtYGa%WMxLJYpM1*C}a_R55z9m z38)-_dLcs(u7OD!a?B0>iI@k9%wQ7#v#Kg2O#`aHKS-Va7ph2r%;?0Nx;W-@0DmCn u+~|JG=F_11HKL0-F%7XozpC?X+^ai%>WR9eUviM=uEP8x@#Pl}*Z&S$C=S#B literal 655 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAY~cg6XFV_F~ENYhX4PUGUR(q zU}#`)(AsaWd4NFyi1ssZd|=>s50nLJ;hXhu8<65C3GxeO5U_X1FW7%zQ}&%XKwJKL zx;TbZ+Fk zN8-SWkE(#xrj+Uyr|DV?g3N^IMrKUag($)%hpBp)iERC~_4LfGzk?N!!eGsX$mPyL z%b#zWH1Vn;B(Qh)rCRI;1-5NhL0BwuICf3D3XP_zZIJl68gggB&l8W_N&~OmSy98< Xbau0$-REt2pk(Fg>gTe~DWM4fFhUb6 diff --git a/crates/resvg/tests/tests/painting/paint-order/stroke-markers-fill.png b/crates/resvg/tests/tests/painting/paint-order/stroke-markers-fill.png new file mode 100644 index 0000000000000000000000000000000000000000..e0e56cb5529cd22d4e4b313cb4746f7ce9f524a5 GIT binary patch literal 11977 zcmeHNZAep57`}6Uv6vr$1xi^ML}VHz8b+;;=~_vKXrm9cA2EqqD{9Q7cBe!XBeM^x zG5R1XP(go$X=&UAg;ds}h(S^Jl4Plf(-}?O?(Lj&H>wLafAmH0Uf7?Vd(M5|=Y7w4 zpL5Q0V$(*QT9vAzC`zqgr!AoNd-AX2=tKKfQa_x7-VL!ChvQZT}H)&1qz1eO- zuZKLNuia)vYX(PJ;{ql$a8om1bqLvG_a^&Dt1_62?I;s&5yJ+FwRW$<=E8O~30p+N zM6(AeCStZ|*k3W5PYEh73b8eL4=|#`@6+AEyyu;Bd5hFErjDlk?5g2o7;U~h(u;W} zL2caNOWeVyeR`XOU1uEOUIDKZ89~J>dT8anwD&lA){$^8Az$i!<$aBP+=0Bj?rZwhI<7ErQp*s0@cUVzEk5g3Ux+$(U&B7;cw-_aNuas zGsC?`TK$sP|9cQ!G)XDs^i?l$?I%X!bPr7i;vUd(2z+@M@obs+GLm>)NI>Z!ALt&E zIlbvg!YqWn42o&}k4X47j3)j$G+&=jyvY8b^OIhTH)alTW_U`J=HaFX?7fu@dLDi2*@%@VTu zl-QyLqdama$99rM;6^Z>e+s~%sXH}r#l387g=6x_ZL$bmnAqc|7-fx#_e-a^ki(m} zwhp$+K_b@Z$xZYvM^i)NkuFy-Bwz_iglN-u>;z^HEE}+1{lfEoZb0BQi#0FLU>MHbvqz~v0l1fW}g zpawt + stroke markers fill (SVG 2) + + + + + + + + + + diff --git a/crates/resvg/tests/tests/painting/paint-order/stroke-markers.png b/crates/resvg/tests/tests/painting/paint-order/stroke-markers.png index 8e199b06fac463450465e6ef0a0739883c41c811..e0e56cb5529cd22d4e4b313cb4746f7ce9f524a5 100644 GIT binary patch literal 11977 zcmeHNZAep57`}6Uv6vr$1xi^ML}VHz8b+;;=~_vKXrm9cA2EqqD{9Q7cBe!XBeM^x zG5R1XP(go$X=&UAg;ds}h(S^Jl4Plf(-}?O?(Lj&H>wLafAmH0Uf7?Vd(M5|=Y7w4 zpL5Q0V$(*QT9vAzC`zqgr!AoNd-AX2=tKKfQa_x7-VL!ChvQZT}H)&1qz1eO- zuZKLNuia)vYX(PJ;{ql$a8om1bqLvG_a^&Dt1_62?I;s&5yJ+FwRW$<=E8O~30p+N zM6(AeCStZ|*k3W5PYEh73b8eL4=|#`@6+AEyyu;Bd5hFErjDlk?5g2o7;U~h(u;W} zL2caNOWeVyeR`XOU1uEOUIDKZ89~J>dT8anwD&lA){$^8Az$i!<$aBP+=0Bj?rZwhI<7ErQp*s0@cUVzEk5g3Ux+$(U&B7;cw-_aNuas zGsC?`TK$sP|9cQ!G)XDs^i?l$?I%X!bPr7i;vUd(2z+@M@obs+GLm>)NI>Z!ALt&E zIlbvg!YqWn42o&}k4X47j3)j$G+&=jyvY8b^OIhTH)alTW_U`J=HaFX?7fu@dLDi2*@%@VTu zl-QyLqdama$99rM;6^Z>e+s~%sXH}r#l387g=6x_ZL$bmnAqc|7-fx#_e-a^ki(m} zwhp$+K_b@Z$xZYvM^i)NkuFy-Bwz_iglN-u>;z^HEE}+1{lfEoZb0BQi#0FLU>MHbvqz~v0l1fW}g zpawtpK^R3>7+dKHl3C<*clW)QG<$S>G`U{m&;IY5&=JzX3_DsCkS1WC0x zOg#ARt^!NH31soX|0!7;0KFVyLAh&?2$mY1`#K}?T?oRr8 z_q+6r>j%#)UFWK!v*yX6Lx-Yv%e+$3JNP2+VFVb2pP&2AEGcXI?jIJ-Ut@jOaD`>8 zncV_Z2n1Z(I&1uvpWIltn@xvmV9kZY5YvezPyH6R5`%!Spw?rDcKv(xWtV)g`rV`7 zCD+7>7aLx^uPeh10@|U8iHWL0Vq#&neqPOdD;VZ}%@b%k&U5Hkvw#E>Lxj)`jmX>( QO;EIWy85}Sb4q9e0F551Jpcdz diff --git a/crates/resvg/tests/tests/painting/visibility/bbox-impact-3.png b/crates/resvg/tests/tests/painting/visibility/bbox-impact-3.png index fd312ef1b8bef1950fdda0074805b9102e636b68..0bfe7724629357a762454c7f62dbd790602a806b 100644 GIT binary patch literal 1426 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAhj;QC&U#<;|Gn6jX(tr4Glmt zH#avsI~$@tHa0dUCMF;t0K#9_EiFw=O=V?eAQwW) z%F05dBqSt+g@qw(K0ZDOgN=<1!e9miAY1ch#UUVNSrX(I?7$!(qo877?;j8nAD@tr znVnxyTvA)#(B9G0KXJmO*|X=&U$Au9`t=(&?A(9g@Y(YhuHS#~=KY7S-#X_PEM;I| ziSl%D45_&F=7KxpVFn(S4X^XQB_#hl;&kx8hrpEQ>pRnfOjK7I^Uj&NBxKq-hy+Aw0@++tik+dzQ5p!oEqT~nQ8HF7vMm`_-=c&^a# z+W-1yU$#8gJ3H@yoQ=(YhK3fW`j|_nZzf&c+m>o~1W8x~#dsF>LvMUXsnWz1HodQ+;Ot_0W0p z(;~hZ9Fdy%aphv)3aQyUPB(sXWWF}BdZSr8`?S~X>gF4F98kRJFtxkx`?gx0>yM27 z`9$=~t*t4l+!b_L;d9~Zh)(y3`7N%sjTVyZ0)(~@H2cyK9ArdN=SWN#)T}uxA_|0>9l#DtA%HbU24==TTp&?qM*w-Yok$5`-YaR#4kyr*&8#CwcmRau|C;yNy@8@VLemx zJwE#BhiwvC{UmZlnU45Dq0^`4Ei>LV`_gCahzBo&rU>?g>0B-jmOH*kd*R%wVE$G5 zm*oC+PW^aWW|vBv{46c0iz0?P|EjM~)$2_BU$C72LPP4V1&R|Qwm*$|CZ&^~p}hKl zRj~7eMG-c?9&fib@4VW?rL#)(i~44cIj@oqc}>t(ueg0!J!&CiH#^tc)>WtFK4nF? zMVNTJJM_KnQ)m>c)P|ndfo&VL7I7BXvTRvF`{q0O z^4%{I$`eZaP5NKbWSs>2jgqSat26VAiOgOvjvkg}g gxw*Z$Uu{@wBA&AIBSS(;2B`G%boFyt=akR{0G_Q$mH+?% delta 999 zcmbQl-N!vag>`j+Pl)S8L&=E_9233Nc^DWNT)+UxnAi}maXeiv2FSE13GxeO5Rg$& zF|c>=@DGTOPe{nj&Mzn~sjaJTXz%Es#3&Pf=HuI51_qWWPZ!6Kid%0k7z#Ex2(WDU z-KHxsE4;Zr)j;FMJQt@l*(uka=uI@wUVh-;CL<9g?`b-bn^OLEgUFMUS}~||lP5B| zPF}z$Uw`fJI@t}oIGGC>&g**Lo$R-wSu%p*T4M*FwW{RLfAe!@Ts}5;m-+^F{`2+> z0#2FrDj$o!-A%4tWuD}xGwVwJQp=4VOKdlmE&rC^U8>DhDYE9ye%)zb?x`QImSU6t zyMKP$&CadztF1PcB{$jwiKXh->eg60>--F=S$}%bjx+TepQLt(s3e9wRNK?0^sek+ z&4Y%oLfISFHK1>7+Ju?O$)55-o-yXi4n|3x zQzTTb94W4nYFm7_{^;Auec7BFpJ{969cq8j+3H@g>&CR7Ev)?oFAmS~Gd%ig+SztH z5k}!@0d*dhw_A1sl?W9*J@Vfuk@Ig%jYG!F#F;-{pWGwwuXE8ydgGB*joM-2?I-O1 zNN!wmxOYaMcaP?@CHD5~mc$EYhg@1%v6p*YL`A~>n16o08#YWh{-yqaOt$I%=MfkD z4|{Dmdg^bP-TIYT2gSYLA3Y>DfBU1-HNxJNKIi1J-J^GYm00~FN!5BI&&yV$V?nPY zN@f+_nENnxO4|B_#-rYWeFiZbTcWj3X^O>hK8=_il~6lz+Xw$OHv7&V^nUcoIoM5C zYHzFP|9hdn(_NzMZ9XttBy`qA)(Z%%DZ4j)rPyWZJ12BD80m0akrK=Ndvv?4?$K8b ztk-S?Y`FeYA|~55De-E7`dumK_0wlDhvjILn8(C^3Yv1*gW_c+fhPylNdN^MM(=6XLC7<{6sm9K-H8>!(NOtS1%Z%Ks+t_F3dfZ&krn@}t z-chqZ%6Y38H>ihh^g8)ksddW6Wh&;|=P;RXXPlwkwQSmb37bh;Jpuuy+b1lUrLD=O z6ZeZjP&2YrAYh^p + font-family="'Mplus 1p'" font-size="32"> `ic` values (SVG 2) diff --git a/crates/resvg/tests/tests/structure/image/embedded-svg-with-text.png b/crates/resvg/tests/tests/structure/image/embedded-svg-with-text.png new file mode 100644 index 0000000000000000000000000000000000000000..d34a8fdee46f25daa93ae179991d7cc54ad489a1 GIT binary patch literal 16816 zcmeHP4^R_V8c*0UR594GMS7s*I_*u{vzmJHPmJW+v!xC_x!#?nEs!whDD}_=OMyfJ zOY~;$Dn01*uEQvr+|{$r=|Svd1d+flZ7FvmZmFYys2kx(tYDL{LK3psz4vyZHFy=0 z_GUVBn;B;%vw45M_xrx@_x;|!ed`ySGo*{+7Kub6>89s5{!%0oKMDWcCjsB+Dr!nZ zqNmF?ZG8IITJbHL_?<^Lz5D26^z-G)|G?-50lH=S^V9(7-wHpOTC03uc6F z(k2&k-zzVSim+A2p4<}fXMnh!hHI(|))J))y@icuKnqN{;vwz*#N_n%lk4$9{)gnS z#Xt-Vs3N_)+&k?9m66(A(?8HByIxTY^35s+og#A&*DqR2NYbkd%2(K$J1qwa{3~py zTDT!P-2-@mdnZ3CYA1$Rli@hGup83jy-yl%l~j?-i0#H(WmSE4Fr_m$v91Ed)M;sp zb`63_Spw0n%lz;?-kmm!wy95CnWL!JHIZUfJ-q)on_!aamkE>jUakaX8a-v`8{)uf zFRLQ$%D%gnmv&6;v>h>EaQ&yqp`!+~B)y)^y=zrLTNXo~gkN^Fnv_(=dv`L$6?Y*L zZ6qYBcpkFsDsa~>o5e>=+yz9oB^}(wBb8|ecQqS`ZyX?)#`FN24WuwY-jxLl{vTnG z{WHOg5tYSN@WSXCr!f0GT!G2*fxE|2BE9fpavWz1=i`w=XD}Oo9r6S-a@^tTfFl4z z0LVRax&ROXAOb*yOs%SCb?tH5Czt~NLpHNdcEh6dTgc@$ctMRP7_)C~;*T0hiK?3A z3SuU5Ds)2QGdY?;9fZR2wVpMk`$6wLV(#K-xyc@!yO?1dV4SDCCRj1s(Z7U2#aLZs zyml4C73s6hz#gtaWSzxR%Nrx+Q3u`29OyBRq`lhl=sX*$lhja7>LQ-$rYjb^`e;9$ zbh4ZMsVOjE3fM*5g`%CeFW@=uL!WFBVNyETHNaJjv$OzMFj|vcmud3$AYSOEp@2Zk zCO|U-nrIiiC<%76UH8@r(cJvghkXH;%LnxgpYmo9V%Umnao#74b*C4BdU38wkgbO{ zH)7nf9|>A^AL(c{c!s`<0j-Yc(y4}iTs03AX{G>*vX1^mVcY$jV z*C3`ISVzwib+|IR5Ha_#KgXojWdsS7pdR+cS<5wk5zlbbpHoNm}mMV{34V={}Y5zTXbV=Z|B^Ads{w9t8^6us4>puSu z+b(Y{m2Wdw4J%L+qE9A9JMaRrvj_bLl%zuLlbE;+es1o;hz8stB73U&_(Z2v4yUMw zJe5vQ`#Z5?TdT}=<)<2OALU8GGpR$Wc<3ys|pk-amlRiTD*5Pv$Z0{m2SQ7PE<}9YKMG-3sjQR@1dqzN&9|>bE)fdyg_k{ z3xEo!-*GJs;ssoBGD{`oj>BsPsxy~rQgI`GwO8s}oB&;w2C5{dDA6rg`O zNUQa=8bmF%#>)GY6^XRcgFw_yllKpbls(14+QYiGB-gL3`Ep=s`4y-`^3J63a2X@?cvb6u)7GIt zH3*%^rjx+P1nChuXZNbBNOHZXKsh7#XCi24)Z|ZaH5GQRp7I?wvzCFPJQGM*tT^n@ z#HM!4nyPL^B(VJb1lcE&ftbPDc(LmuNU&krIM5#FecXuEi9uj}h@UJpG4IDS?hKqa z(Uon@hf#+30&XJs;9h{w1N}+Mgr33u8&+taB1V@M9b!{?-+&gL)#+YN14f%8etz^2DuOI^aV+%(*YP~aeR6=3VQ z3#*V{;hOg(;Ta%bVG#-EC{Hq;MahQ4^dBb$MQR#`3A#%McqzZk2^?AdinGRk*@3H( zE||r&g|;JFrae-tVTQw9*a|Lb@D$}`JBa?9UBMxW?uO~Yh~?Q*336>!out%PONHq& zo|&l7Kcc);NbP-#*9E_l2cM-N)iYDjLu8|2n-PvqxlhYUm0u~Aszk6zl7vsH?J)2t zuZ4{bb5)%kmAHegWWAw^_pV~Lp@=n+UB}Tr2@@FR>SnD9t03;ejTv< z>mx9Yh6m{`ns|5?lF-9ouVsJim)oi^EOec*C z?H`7{nB0^(j*R{(sC{OeOsbE8-UOz0D`JWGTe;M4VGy_`@$O*igUA*25g(1^h&2gp z+oE7N(d_Q9XT!%|m{4ZTMs=EJPSCCPV-gi$jnjlROsF~<3?>kL3vJuHn(;dc8xb|h z)fNG%>vP2*Or}(GlrrnSSDVGpgqN4lhVS| z872^}2`kl7Jz%>qg`xX`0AcLgZTkY>%`;I4&%gW-*)sqQx%TzXtUZ`mXYj;S$_3UW zf`q&GQInvQfd+sXn=R!53z_1U5KHZ@(=m{4s1Y;Z`08Mh#c`YCxOow(xYO^&etE=T zhDAm~#MrooPk69c&z%UZ>O|j$^-1g)D59@pp9SS*0!|9nXV87+KwN=o{g|QoI%(%A z3E-zKH$$x6QC|$07 zq7iJPcK|n8V%Nb5D(Zr;4XPhsOS=p?5a`?=X>utYBR`mDGq((N;QMsEE>{kW zF~_q4L3#nvH&b1%O#+8Ud^U!r1XRGekhe-=V94_5u9wYAw(B* ztc#`-U9ZxPJwTSyt>)7qP%)MMv#75Q_3hw!H@l`XPP-D)wTEF531Q2Ya_;R3v79qG z+YGfJoU<=uh(5)20bfmw9438-NoQB(SXcX*Q%#_P0%Vwy&EN_;833897tmM@1&e=_ zm(X>@j=F=vduH4;YJXY1l~(HsYfkYTfd# zLT^fH=~%zaw656hgg<0zWq}(RWOtOiMC~b2moc<2lG2}ZP{+{m$I$VNaoJdqu43y{ zjdiw9t+;l5Mdd#9sFE30R^E^WdSz|hM>FeG2V#5m>F87rHj#sN905(2`!qlNmucb^ z(?k^Qn1@iR?BL^4_J&&0`$tl{u&UMB?*HrP#Gc}oT$Sb3hQ{mSrluz2wUVpX53DUP zHnp_qykNR}&@Sx!$~QVq zW^G2N7R<;$Cis=2EC56R$UV0l1b_$t5dZ==PY5AX3mk;g)!_p!a649rjY4b`Vxtfn z!2>WtVIdS2_k2Q10Ehq(0U%JqVikgRbxeQWp+8?Q)?X^9xFj!{p9BaGaOTV{Cd0#| z0nL1rNO*{~b8exSJL0-*9vY=Ez*%Q&XC4C-Eci`Nv~mZjzpog~5Q80A&Pg&6uz~dr zPkM5dAiS#wxYBJ)iLu + Embedded SVG with text + + + + + diff --git a/crates/resvg/tests/tests/structure/image/no-height-non-square.png b/crates/resvg/tests/tests/structure/image/no-height-non-square.png new file mode 100644 index 0000000000000000000000000000000000000000..0bae96a258ef3c122e422e43b0b830700d2c1e8c GIT binary patch literal 1291 zcmbu9YfRf^6vm&nw4*JhKnrasv_-ec1c?%0TOBaBaw!hRwGLkFy|7iB#g@n5d%6pHpSpv%Ef*a_zjIA4ad8qRHSYK2n^1Q#GU2SGCgXW`fc$9g!{ zf&Vf1Rp6fi|1|ig!21xqO7P0TD+8|-4kfV912-4k9B|(SHw)Z%z#)_gjuM;34ucg6D;cZ;!8`!welY#P@B_ma z3<(%}Kqs^tbRW>YLHB~?OR)67HaBc@1#Ks2J3!kG+OwdFK^1{21l0*t0WA2iZ~%o1 z3I`NhP;5Y9gG`78vK7cokQpFZfnXqxKN%xW(|fIU~7mO=6BzklL0g2lq{@*zgq^3nD|5(f@U$N|RaHCtx2l8Nbo4btJ8dZZrY&`@6NwV=4#<(`DQ-iPi@U2y$lxWHF3_EXUd$|w1T ztA)DaW86GH3Z*{t!Zs_Yvzb-IGFn8A>ny_Y8|YLqzv$2~qOr)%yQTYKdr3(iW@ zThq7da?Uy`Qq}b~wn$?$%Cihfi z`-z?#=nhop^@mgwxejMdm#xu@C4cXFLUU(Z-0h~CxhBJdJ$lOfH(tv~9*n>3eOhtr zw*pE37Mm?#n-n)WZNb|ly2 + No `height` non square + + + + + diff --git a/crates/resvg/tests/tests/structure/image/no-height-on-svg.png b/crates/resvg/tests/tests/structure/image/no-height-on-svg.png index 51d66ff7e3fc9687fc99bb67eb7eb965070d79f1..21957d7297a6dc88d878a3992fe34f35b2df44fc 100644 GIT binary patch delta 5274 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 diff --git a/crates/resvg/tests/tests/structure/image/no-height.png b/crates/resvg/tests/tests/structure/image/no-height.png index 7e65130e9c04571b70279bb6c50dc0a536b986e4..5dd2f458cab7298f34dec3a6c84e41eb21f09571 100644 GIT binary patch literal 49382 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≫}@{J^JOBwpf#|m0#$cb%#sZX0{ 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 diff --git a/crates/resvg/tests/tests/structure/image/no-height.svg b/crates/resvg/tests/tests/structure/image/no-height.svg index 7122b2e5b..cb46069a9 100644 --- a/crates/resvg/tests/tests/structure/image/no-height.svg +++ b/crates/resvg/tests/tests/structure/image/no-height.svg @@ -1,9 +1,8 @@ - - No `height` (SVG 2) - Nothing should be rendered in a SVG 1 conforming renderer. + No `height` - + - + diff --git a/crates/resvg/tests/tests/structure/image/no-width-on-svg.png b/crates/resvg/tests/tests/structure/image/no-width-on-svg.png index 3b534d808f4a2efae7fbc37376d95fd9c21e9697..21957d7297a6dc88d878a3992fe34f35b2df44fc 100644 GIT binary patch delta 5405 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 aspu|C=)_C=%wGJ30eAI{Zx`P}1^*9ml~j8G delta 5123 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 diff --git a/crates/resvg/tests/tests/structure/image/no-width.png b/crates/resvg/tests/tests/structure/image/no-width.png index 7e65130e9c04571b70279bb6c50dc0a536b986e4..5dd2f458cab7298f34dec3a6c84e41eb21f09571 100644 GIT binary patch literal 49382 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≫}@{J^JOBwpf#|m0#$cb%#sZX0{ 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 diff --git a/crates/resvg/tests/tests/structure/image/no-width.svg b/crates/resvg/tests/tests/structure/image/no-width.svg index 971236e3d..8be9617d2 100644 --- a/crates/resvg/tests/tests/structure/image/no-width.svg +++ b/crates/resvg/tests/tests/structure/image/no-width.svg @@ -1,9 +1,8 @@ - - No `width` (SVG 2) - Nothing should be rendered in a SVG 1 conforming renderer. + No `width` - + - + diff --git a/crates/resvg/tests/tests/structure/transform-origin/bottom.png b/crates/resvg/tests/tests/structure/transform-origin/bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..264605c47f28e5bc3ffd2f16518dbdfd096b1515 GIT binary patch literal 9415 zcmeHNZA?>V6mD?>g4n20ro(nqH?qlW97d5;?%Y6RI#_gwMx|TGAVf_;tt|q*m5&(1 zq<&19xKJ~HxY22K%#R6r0q3fqGDon+X10J81~M+iN(*i8T8i3JPqJjm_Uks`5BHvX zp7WgNocBHNy?Z}NP7Di~8N%Uk!je|RujX*LQJ%lxK=*(03d^M&&fM~(_+_8UxqT+? z{<%p9=O#@lt3Fzr7$2XRnYlLA{gZiqLpT46-ww~C)6zU%GBcMik1we`b~L1ARc4w@ zDwCzOcwDafR@qs*6j<*lTBV+=IOf4o&8+tV^TO`sP|sj{fw8bzwpo#|CF$uiY$sv+HoM%o7k74* z^lWw5Jqf#&VmGere%mv@{BL6d6CT$m&HL>~OOx2HaRf6pmp#*#-%!KUrQ~noKbfjB z56C17NQ!#6>53)IKH1f^3z>|VZ1vMiqV7n3bEi6|a(w;SX^IyXS*XQ&V8P4TQfllM z=2d7NsfrRN4;iSem8zzR3Y=TQ)|uEHWw>8&WcWww%svo|8V%+I%#5G@BaoAWVL@+e zdPR`)myA5e)Z`iyxgQ|Yp)y+mk&TWxODcP^yX#6z&but z(i&wGJO%>D#W*8i%`gE#&8h)R%?qHK!b1@|gI)IX^$#XBH zyiNG>ns4m$yT8VLe+O~9%h|BTw#qbiEIT&2S^F)%rIpi08SWmU8+fz-`MzRTr8Qbn zIm2(yGb|hNt?!FH#>aQM)|C_2M?}^DVqzUU(RFMH2$8?6CLha;LV zu zcd9xUi0NaZV&+xUrVHAffi^Ly&7aWbIJ8-Z+W7CJpp6{0fz6RrLK}R?)(@~zcYqAY zDy(?4G?0$b1d4~yW75&Xf)j!+MXUR*F-(uK9Z*fRU{t3-sux4r@WCw_(l!lJFoIFQ zd<7^7Zve_+CSu_buzE@%G<7GYf=JMGL;}b(`nr?(0YD4#bAwJhCxIId^b(B{@Z$V| z^L&lAsnI=~&?Dv7m7{H+*@95RzH?yTIk4{>*zX|Fe#U;j@$WvCiJNKL{On_yY?EfU t_hXr7U-a|_$4shQIQG?y(@F&BR-BK_o>Mi${ecoEDIq!j^ar06{S8~{Z$SV6 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/bottom.svg b/crates/resvg/tests/tests/structure/transform-origin/bottom.svg new file mode 100644 index 000000000..5beadb466 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/bottom.svg @@ -0,0 +1,13 @@ + + `bottom` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/center.png b/crates/resvg/tests/tests/structure/transform-origin/center.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd83e6514be8b3273dbe5286878c0d160c10b9e GIT binary patch literal 9449 zcmeI2VN6q36vqn)C>`Qvw3)g#abzZrjVwBG4s0|CBXh7U!|Dcf8CjV#rWoyL0eL8x z&5+PV6P;rdqghDWgle483HXY-RfDEF(~wBCorpRaQVLIMM|=0$4%2f^Kbb7?!+Qx| zc)jnQ^Z%W5&$;j2y&o1Arl&1PlgVW1MepT*B$Fj%h5u%zi2pI|Y&FPana7Lr*L-YD z81W>0mszwgv*@K3%b~8q{Cu5Gx1m(LYQop#mH*?~5q`8r6P`t)VpVCwI3tM(=t~ z{~CSl(5YSS#EL?DJfu!j$JYJn$sipK0(^e1mCT{IqW;qZp<>L8oL~0-)H+jINU3? zPyf({D3BBQrGIFRdTgVTHQ&gRDR;IG4dk^t>U;IR7sPhS{fIVVd~Zk3n;C(<4xfL? zkGkHZ+`yu~({+o?PjtKt?lP5~X_&mVK1sE-k@=gT<8$-^A3mE<09sQ+9p52a(}w2!HA zd;GCt`)i1CanzQ3fkP?}Qm*1SZLFORK!`h1*k1YMYxCe+T5nr!leM!pL>qf=F)ZoQR)70GJlOEoV#LgQafR=jTkZWj2`52Zuz>UHo zy=X`;nS`B!{ue<1gWM#YWEf1sHA448>N>nH3Gy&J)t8`MOxqLQOvUb5LhdIZXjBhi z05CJ*!fYUMrZOHs*q-Hnv@Q3ZykUP;wTtBtEG!3xvjT-C+fSm5$=e^F&3&*$**Dl; z{X;Tqf$M<4exn+9Lyh;qqzSO-#x7>tCvZ1Gh_TNR7A?i1hFBQJl1;I+Lo6J|qM=xv z5Q`dPX`xs;AQlzI@;1e?3}R7XEc+;ySrCgEVc{qit_NcI24UeT79L@#L|F6`ixy&8 ziY9UYjVq17{{h&SI(;aPH&DS0K(NkQB{pn=q_K7gq_IVTQEsIuw*Zu7LUt{zGhCc} zNdsUsJ(sWocZdS&+fV`a6?P~3&LifHNI*3L-QiB6WvPS4zgdVaGSx1A0If^=X#zMv z=Wynsdju^^m#J*Wc20LD;fh`+=y3|{phU+Y(SW87N!>(q;2EUme@9ZIViw9FZ<&HN zACyf#fdX^|b>~?E6qZa&l?f?er3!okgKZ2MrlW?rfX*Qmmbr^gQVS+w8F`=+6u3B5 zwizxpg(wGjngbjJ!;(-6Xvs^#fEs2rJB~8IMp3qp$D;IdsK;OmngAsr0sQK_DE_T9 z{;NRzPpP~J%NG>O4Twd7u&}cfiwRjDAFljIyfp(4M!eY3e)1dCmqKLB-R$K?hwUGTBErHh1JfL>P zw{Sh{4D^%C5qHP1r>)0?_}ipEw{T4$;!?3gTpL+!N?8F=4)5oJ10+Q|12s>D%kakR zO9PW}{1}b+A)DUxkCM((`UXV9S^DB8eJG5)Nszv8J}+->rIeIXQc6kbfX8!sE@kxs z|CaFinDB9$%^vHn=y96qGe5&KSI+KU{@=NpprB3^hP#(sur&9HN0?+q>x%O`*M7G9 EUn=8YIsgCw literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/center.svg b/crates/resvg/tests/tests/structure/transform-origin/center.svg new file mode 100644 index 000000000..3af6b9940 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/center.svg @@ -0,0 +1,13 @@ + + `center` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/keyword-length.png b/crates/resvg/tests/tests/structure/transform-origin/keyword-length.png new file mode 100644 index 0000000000000000000000000000000000000000..90bdfb8e99d475a11e25c73eb3e5a443ff38e833 GIT binary patch literal 8762 zcmeI2e@qi+7{^-};9$rYQKHp0{+RqBlNp$x78)nYh|VtCSapu#LWUaWs=?Y;ejGn^ zV2ofiK?O&h{6R*i%(#UOTCOZ+)d52#*hM0DiXg&}@}tt$mRs)*O8Y+iu`Jtv*Mxt# zyZ4^w^E}V{e&6T4lOOCz2@PHx%;WJwQ{PMekjLXk(|_{<-T%?#l_`0=<^0s-O&_cI z!zBOA%G9$fQ{OmMcd;oYIXNpU>ytG1PeuPG+y0N=5qfBujDAa2*4C}b$D67y26t@F zlBtv`m9&F?<@PHDy~b^ayV7LoR!KK8PkoSV_<68Scg>+4R=%QNXW4Bu@vwIHWn~u? z9TS?nz_P(lF0j2tDhr5y_kE3nScN$-KSaHhUmxmRXxU&?WV55S9`MHl^koI?RkGn1 zcG$k33JE`XijlcG-_re=(tW$DW9)%Z!LMIIGFO9a2i*hL>8AE--1Zch!!68Jf9E>d zjx_29W7rU5ZGoAeUq#zdN9|$_W6d7o;WK8lhH++dw7DeVN|K&uxS52Tx4GrUy|{C$ zBrSC~P4eGLsc{k6DL)^?&uvkRy~ZlJJXD+EowbHFVUClgF?B&h&XRI_5%FXZD+ap_ zwC?nhcUXxuCtWxZQQkKyO=$Nc?>^nx{IFNu7U^8oK{6tg=tpFwQ;_#yXza399bJz7 zx__bASkrMmx5+emA;$K8bqk|s@g78>+T(3Mn)^0MMt2IS1KH8U^TXo)ko3~sN)Do8sbKAkM)5$Du(elvs4^g?C`FrT zKU&_qHp5aSE@s4RWIJMtFYAh~h(X)i|ftW=y{VUQ?7&MOm4#I+Uv0+gw+Eo4{q2dda z5+pozip|5DIhoA@HBF!%B%Ui{oK2!0!T3XCj(=D`b`@ej%VHnxxiAvQAkslZG89n; zM1%uGuR=seQA8aOQ9OzWgNSf|C>tU=jUv)QL<$rU^*um@okJ1TLPWb!L=uRI0Ej{$ zq9Z6G0wRh+5m6A41R#<@MBkx^aInC*q%V*xqE0|WjVPk05Rm{N8U}?%JZ5K%*+Sa| z5v@WINx|ek)iB-L@e~^N2PicBE?Q_GK}1(kL|TYQ0T3OBh?-DDLl99UiYNmj!U3XB zA)+@?L^6n|9z}#hL>NGn1`+8{MD-9+28sxSSww*>>V=3zETVlw>9>nT$;>>Pk^|;h zoNhH1nD6QmFLl_9Yb_U<8%+>`#H$B*(6Bm5z49@383d22VuMF_A@9>;x?VOsja}0o zP8EZ-n-cjhl7GfOc`%~hl|OKA$*2SfMJqL3r=CuxWPh1W0Yr-nn*?GalQ^Stl%4M@p?x?3LaLjdsXTpDvC-7LY-D0-Olw$r<%)l% zSXzA^i2$LLC3YkFLz9%`?YtH!ipIh9K5*U5*+|mImqO@EwHbZQGN0E#;nFy0hq|>q zZ*$MHjq(anV_b@JBM7_9u!pU=*-r2l%XP)Ajd`ZlvCZfEOR-{*R&|ljX9ZG#-rYgf zm-?7i4}zI)oskfpt~C|xodD|aIZWHfR_JW+8o%cq9C_&+rlI1{y?=41@;A-BU)aCh zmVI*^)OfnJetI=!v&Y?MpE~X14i8LJY1A>48u7?GprBxsT{yaVv8x;&%Z9>s8j#H! z%GepiII=JTb9(jyj!Cb1fa~CKCojm1$sHI)qjkz1&vB=UvkS`f87`M39?8TVJ##1K z|K|IIq{X%aGt13>Qdh`+pD^ + keyword + length (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/left.png b/crates/resvg/tests/tests/structure/transform-origin/left.png new file mode 100644 index 0000000000000000000000000000000000000000..aa213b80fc6f9d5deaaf11dd27cb61456bb99bcc GIT binary patch literal 9430 zcmeHNZA?>V6fPp-V2n6}L}ic&lT1;vX~b!ST+%vK$oyV+~&YJ)*>GlMMoS7 zT{Mb{vrMxY%SJ=oL_oP(|xPI<$j%Q_n9mZ)q{em>a_5ybeAh(yuVU zykbhmT-zdJUJ3U=_+Dc_bEtD#v9J0y551prmU%@>&JOo(ZJ&CwUgs-H+)s`54eo7C zzwY9uWW=>hJblI7m6j)beZ1XrwTTO5Y^E)(rY z=FdzRHZWmrNvB6~m1u{KPjwsSHhVzEJ~!KKm}Ituwj>fMB*i=(@k6+5ncg{>aBU6vQeJIDeJb2{! z?h3kl=s&wN7q`xmx14ORD+H&wr(ct1V~!l_R()G(@%=iv>!nkv=0dq6sI9&rNMqm4 z_0AS1YJ3rydGVXHPaEZZ;lXV^+T5dFk(cl>+*3S)bL5PGW9AWWny5F6Rt@Im-%OGp zGAO#2`nm#wiz-ZA(G`Zm)*Q>r!kCW#k&FwO29bf$nHtk@(()U-nbJ6Iru{2T&znkf zmAq7F(AkH0XgH?qpNzAS>;B{6=H`1n>g$2d`RzubmB}Ha!|_V{FP)~a!dy{B=8CQaK#tRUCnj_*h2Rl{U?yRwn6k4Gu*2`eurc)% zwxa+xeu`$5!7>BVbQbWKyy^Jo_1&FMdi zWiWC*iJwQN-+d1N8H)#fTlghDkva~W>mvt zyvZ>XH9I_pAz~{a`Qx|&lCu_*qZ}gf76d^HAzJ%@=q4M4&beblHTO zIrx+Xz^>Yz!Pplq_Qk0Hbt<>Rm&95F766|K!X{WxH%P;$;mUfgYBvdC}!E-^kz$n zeTGm&6~Chpurm)Joy?ich0bE;YEz*}ZwVDTf)`kJ6Iu3&VUum#wO?hwrZmNgosb$U zfIkg_rY7Sfyd;P$se?;gv3QA`EP?GnO-42Lr52&QBLET;t-&jr@rsU*(ULZVG5Z_B zxS>B42EhH>J}jrSELPfUoYRGJtHea6jp0yUdG_Lvweoyhife)5qR>Eg?!*->S_AOn z%PM|0KO292$a0XqrV)FTF|5ku_D(<~ABqtU#t7>I`+!{dT^KJd!OIs`bmlh3i#8j| z(aAbCGdJ-Pn#p5<^v%o_w1oMZ7*-B58DD~Y;fW@7%s~k9WehU?f!Wmo$ae*A`;`EGYlq%e?iYs37u&r&XD5gx(A){CeB!CM>h<}R1jv$2s6xK zXqgALjMhunT9B5VQP{E}dgby5MaIk-5zH*D2s1Cb2|;I825srKK1=x1w2(O%()}+NF4!_g zEk`lK(U{4BGSlaNwwD`<%&*itlZ*F=S3!C|;b85HB})cKi^ai)VJ|U^5`(YY*!`gb zoXW>FW)LzmoR>pLD(5r7b(5?B(}Orq;l&*Or2KJM=LY>%>oKmHIjt~_xQuT&=L_YC zZ%go0`ng}gDR;jhb4PgZX1no+zu&|e)blfroNWxhw + `left` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/length-percent.png b/crates/resvg/tests/tests/structure/transform-origin/length-percent.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd83e6514be8b3273dbe5286878c0d160c10b9e GIT binary patch literal 9449 zcmeI2VN6q36vqn)C>`Qvw3)g#abzZrjVwBG4s0|CBXh7U!|Dcf8CjV#rWoyL0eL8x z&5+PV6P;rdqghDWgle483HXY-RfDEF(~wBCorpRaQVLIMM|=0$4%2f^Kbb7?!+Qx| zc)jnQ^Z%W5&$;j2y&o1Arl&1PlgVW1MepT*B$Fj%h5u%zi2pI|Y&FPana7Lr*L-YD z81W>0mszwgv*@K3%b~8q{Cu5Gx1m(LYQop#mH*?~5q`8r6P`t)VpVCwI3tM(=t~ z{~CSl(5YSS#EL?DJfu!j$JYJn$sipK0(^e1mCT{IqW;qZp<>L8oL~0-)H+jINU3? zPyf({D3BBQrGIFRdTgVTHQ&gRDR;IG4dk^t>U;IR7sPhS{fIVVd~Zk3n;C(<4xfL? zkGkHZ+`yu~({+o?PjtKt?lP5~X_&mVK1sE-k@=gT<8$-^A3mE<09sQ+9p52a(}w2!HA zd;GCt`)i1CanzQ3fkP?}Qm*1SZLFORK!`h1*k1YMYxCe+T5nr!leM!pL>qf=F)ZoQR)70GJlOEoV#LgQafR=jTkZWj2`52Zuz>UHo zy=X`;nS`B!{ue<1gWM#YWEf1sHA448>N>nH3Gy&J)t8`MOxqLQOvUb5LhdIZXjBhi z05CJ*!fYUMrZOHs*q-Hnv@Q3ZykUP;wTtBtEG!3xvjT-C+fSm5$=e^F&3&*$**Dl; z{X;Tqf$M<4exn+9Lyh;qqzSO-#x7>tCvZ1Gh_TNR7A?i1hFBQJl1;I+Lo6J|qM=xv z5Q`dPX`xs;AQlzI@;1e?3}R7XEc+;ySrCgEVc{qit_NcI24UeT79L@#L|F6`ixy&8 ziY9UYjVq17{{h&SI(;aPH&DS0K(NkQB{pn=q_K7gq_IVTQEsIuw*Zu7LUt{zGhCc} zNdsUsJ(sWocZdS&+fV`a6?P~3&LifHNI*3L-QiB6WvPS4zgdVaGSx1A0If^=X#zMv z=Wynsdju^^m#J*Wc20LD;fh`+=y3|{phU+Y(SW87N!>(q;2EUme@9ZIViw9FZ<&HN zACyf#fdX^|b>~?E6qZa&l?f?er3!okgKZ2MrlW?rfX*Qmmbr^gQVS+w8F`=+6u3B5 zwizxpg(wGjngbjJ!;(-6Xvs^#fEs2rJB~8IMp3qp$D;IdsK;OmngAsr0sQK_DE_T9 z{;NRzPpP~J%NG>O4Twd7u&}cfiwRjDAFljIyfp(4M!eY3e)1dCmqKLB-R$K?hwUGTBErHh1JfL>P zw{Sh{4D^%C5qHP1r>)0?_}ipEw{T4$;!?3gTpL+!N?8F=4)5oJ10+Q|12s>D%kakR zO9PW}{1}b+A)DUxkCM((`UXV9S^DB8eJG5)Nszv8J}+->rIeIXQc6kbfX8!sE@kxs z|CaFinDB9$%^vHn=y96qGe5&KSI+KU{@=NpprB3^hP#(sur&9HN0?+q>x%O`*M7G9 EUn=8YIsgCw literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/length-percent.svg b/crates/resvg/tests/tests/structure/transform-origin/length-percent.svg new file mode 100644 index 000000000..de1d5be91 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/length-percent.svg @@ -0,0 +1,13 @@ + + length `percent` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/length-px.png b/crates/resvg/tests/tests/structure/transform-origin/length-px.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd83e6514be8b3273dbe5286878c0d160c10b9e GIT binary patch literal 9449 zcmeI2VN6q36vqn)C>`Qvw3)g#abzZrjVwBG4s0|CBXh7U!|Dcf8CjV#rWoyL0eL8x z&5+PV6P;rdqghDWgle483HXY-RfDEF(~wBCorpRaQVLIMM|=0$4%2f^Kbb7?!+Qx| zc)jnQ^Z%W5&$;j2y&o1Arl&1PlgVW1MepT*B$Fj%h5u%zi2pI|Y&FPana7Lr*L-YD z81W>0mszwgv*@K3%b~8q{Cu5Gx1m(LYQop#mH*?~5q`8r6P`t)VpVCwI3tM(=t~ z{~CSl(5YSS#EL?DJfu!j$JYJn$sipK0(^e1mCT{IqW;qZp<>L8oL~0-)H+jINU3? zPyf({D3BBQrGIFRdTgVTHQ&gRDR;IG4dk^t>U;IR7sPhS{fIVVd~Zk3n;C(<4xfL? zkGkHZ+`yu~({+o?PjtKt?lP5~X_&mVK1sE-k@=gT<8$-^A3mE<09sQ+9p52a(}w2!HA zd;GCt`)i1CanzQ3fkP?}Qm*1SZLFORK!`h1*k1YMYxCe+T5nr!leM!pL>qf=F)ZoQR)70GJlOEoV#LgQafR=jTkZWj2`52Zuz>UHo zy=X`;nS`B!{ue<1gWM#YWEf1sHA448>N>nH3Gy&J)t8`MOxqLQOvUb5LhdIZXjBhi z05CJ*!fYUMrZOHs*q-Hnv@Q3ZykUP;wTtBtEG!3xvjT-C+fSm5$=e^F&3&*$**Dl; z{X;Tqf$M<4exn+9Lyh;qqzSO-#x7>tCvZ1Gh_TNR7A?i1hFBQJl1;I+Lo6J|qM=xv z5Q`dPX`xs;AQlzI@;1e?3}R7XEc+;ySrCgEVc{qit_NcI24UeT79L@#L|F6`ixy&8 ziY9UYjVq17{{h&SI(;aPH&DS0K(NkQB{pn=q_K7gq_IVTQEsIuw*Zu7LUt{zGhCc} zNdsUsJ(sWocZdS&+fV`a6?P~3&LifHNI*3L-QiB6WvPS4zgdVaGSx1A0If^=X#zMv z=Wynsdju^^m#J*Wc20LD;fh`+=y3|{phU+Y(SW87N!>(q;2EUme@9ZIViw9FZ<&HN zACyf#fdX^|b>~?E6qZa&l?f?er3!okgKZ2MrlW?rfX*Qmmbr^gQVS+w8F`=+6u3B5 zwizxpg(wGjngbjJ!;(-6Xvs^#fEs2rJB~8IMp3qp$D;IdsK;OmngAsr0sQK_DE_T9 z{;NRzPpP~J%NG>O4Twd7u&}cfiwRjDAFljIyfp(4M!eY3e)1dCmqKLB-R$K?hwUGTBErHh1JfL>P zw{Sh{4D^%C5qHP1r>)0?_}ipEw{T4$;!?3gTpL+!N?8F=4)5oJ10+Q|12s>D%kakR zO9PW}{1}b+A)DUxkCM((`UXV9S^DB8eJG5)Nszv8J}+->rIeIXQc6kbfX8!sE@kxs z|CaFinDB9$%^vHn=y96qGe5&KSI+KU{@=NpprB3^hP#(sur&9HN0?+q>x%O`*M7G9 EUn=8YIsgCw literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/length-px.svg b/crates/resvg/tests/tests/structure/transform-origin/length-px.svg new file mode 100644 index 000000000..68891132f --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/length-px.svg @@ -0,0 +1,13 @@ + + length `px` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/no-transform.png b/crates/resvg/tests/tests/structure/transform-origin/no-transform.png new file mode 100644 index 0000000000000000000000000000000000000000..f4eec7c22dc644a742867be41f6dfabefb85cdf1 GIT binary patch literal 7448 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4mO}jWo=(61B1+cPZ!6K3dXk&7$>NBPEzp{ z>RvT-x4Vl=T3T9IC=ir`!1pt>03{%!!^1(gq@}sJxwPz_J(KIaPg;0+X?eN-d61Pp ziy9su@GiU>8Xo)A{`P&2irfd=cf4A)dfl)2Z%bMBn0<&oaGat3EKs`i>VF-5-Wt&l zR!niyKXf0IHr#&(RJZF@{j}?Bb=(!QjO%&-h<&g+kY5Zj?Q}Nte%3vG4G`G}+jl~Y zeY&;rK2x0g0kACJhublwK;?hWf4X%bpK-nN1F$Unk6fU^t5*L@uLQcM;kw`luq^W* zGmtw%|8E96>^0c64c8g#W`o?Z>VG7}VGw7(X4nsNHpF40&K?cz(L^$uB|+uPXh||! zZjV+sz{-8JmIO6*M%yGqs44aL{->KW{`=cW7he5uiqW+DA8jZ7`Txmu<#oUIKSOV7 zeyu;9p8U`LXU>t(|JO0vkpJ?3=A8MjKEHRztN*hw+O+?5=lA~n|75dZ_}}=?=#AB1 z`=4$${6D|O?9r9{aK7w@&9S(kNv6tbhBgizx5c+-BHiN z`x>J$3+hjS+VG>v7~Cra_4`KiEWFb_T0nsY7C=J>!>u$156+AZr;LsQjgGmE4i$oi zUPnhbWk<)8M~A^jCmo;-p3%7yjB$X`nJM%!3ZfH9Kij`qun9G(A6d}q20Bmo&5Egy hSsyyHmQ(+?znsB=GeF@>4RFSj!PC{xWt~$(699>%5FG#j literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/no-transform.svg b/crates/resvg/tests/tests/structure/transform-origin/no-transform.svg new file mode 100644 index 000000000..5bbaeaea1 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/no-transform.svg @@ -0,0 +1,11 @@ + + no transform (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.png b/crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.png new file mode 100644 index 0000000000000000000000000000000000000000..c572c8d37952bc1e181ee01d09fc4108c10df205 GIT binary patch literal 9819 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.svg b/crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.svg new file mode 100644 index 000000000..7510bc410 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-clippath-objectBoundingBox.svg @@ -0,0 +1,11 @@ + + on `clipPath` with `clipPathUnits=objectBoundingBox` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-clippath.png b/crates/resvg/tests/tests/structure/transform-origin/on-clippath.png new file mode 100644 index 0000000000000000000000000000000000000000..f31e00c9e3c7d58976982a16ef58fe17f1a8d486 GIT binary patch literal 11440 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-clippath.svg b/crates/resvg/tests/tests/structure/transform-origin/on-clippath.svg new file mode 100644 index 000000000..197feebf3 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-clippath.svg @@ -0,0 +1,11 @@ + + on `clipPath` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.png b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..4795fee32dba0f22a2a36ee373cc375f28835fe4 GIT binary patch literal 33631 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.svg b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.svg new file mode 100644 index 000000000..9fd858016 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-object-bounding-box.svg @@ -0,0 +1,13 @@ + + on `gradient` with `gradientUnits=objectBoundingBox` (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.png b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.png new file mode 100644 index 0000000000000000000000000000000000000000..4795fee32dba0f22a2a36ee373cc375f28835fe4 GIT binary patch literal 33631 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.svg b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.svg new file mode 100644 index 000000000..f121a1fc1 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-gradient-user-space-on-use.svg @@ -0,0 +1,13 @@ + + on `gradient` with `gradientUnits=userSpaceOnUse` (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-group.png b/crates/resvg/tests/tests/structure/transform-origin/on-group.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc77a5c3361dfeed53ccbe95dbef2495fefff01 GIT binary patch literal 9375 zcmeI2e{5S<6~~=+{h@Rz;zwrKX#2-HhS)j}sU(moeFA0cvX+LSWfnAXkPsBZ9#dcf=#kDr&BcC=Eq*xC}C{DE0twRmAs^3N#`tm&b(JUZu~CK zzUMs3JN_e@;2-2j{inXV_uS9@-tRf*+Q0bLfxQ*wca}RGj*9w6>%Q%9l--~Iw|S%a zKb^;WeGbRY*7~}yKhaUPG*kAgo%PS}tp8LX_`=BEy1LfZ)+ZayKY#u&{m%d5@0tA6 zZg+l_*49TJsr%_j{|n{O{jF}l&+l)D=9k=mre|(^-_x;1ce5^CQ8#zA&qU58f}wC$ zUh>^GT9rIFKH*@K=EuGmzu#5SU74&|=eaAhYsS~3&YRN?Nf@xrHDlUJI2=I~tf!+(r>%LebBDSWyq z({4^23>k+y&3Jm^pG+1$UB6nDkE4+f6#`oW zM7t!C(v{5n%>SX2WYgKU>AQNO^CPCwGKG3F-!4xcTd!WI)ZRRlnlf!yiBgAVq3HbB zv)a|KjeSC%^qWr3e@xxe*tMrWz47(Hf_6O9_PS}l*$DM%eNewVTAgej{gJiabdmL* z6YD*k+S6U7nueWQq~6~3`0*;OqT9)9aH_y{nneOvhdRA8Bbi9&jy?UFnVUSji-dlU zfS^QgCNFnan_16wjHDR-jc&2tm#2R&csh%$x8=ja{#GZtaJ{n>dNg_{H9hWMmgfLf zmqC+MZA3tj;uzFukwNc@nOdxw4$$pgPm7r17}LNK0aZCJCQ@;t;j3g!42CR1)F;W# zxL1JWw~I5;`4l-_J}CCZPNipuR-OuH`3Lrubbd)JC0!DG&oSLz31s<#I9Yi~gkwlu z_A|?II1Mc+gv3>%BZFtm8sw7I_PIOXe$o7Kn5vl!5yBC zv6sO@WYHnq(m&|xV!2-4yPn9a{MP|66sCW|1s zT*D2pnr&1mub~L4QBH@cQfg2H{U9gyJE9Z@2{Oo^134*wA($k@#9M)7%PetP88LCh z6s4G=h>0PlLln~jVp0$jZODu{#MFYAK1VUVikKwC^h1j2tB6TPOuwa=eu--8156Ud zq-r1(J_JJP6OH99ZD%&UQb-cv;c}QrT_h;IBF&mL)DZZfGC>YSorl$e3pxnpNwPUM z262ZxXCbCYz6bx0o;SA$zLT$3ggdcU2!g)8!3%|Z)84i*Qy}!ybiK2va#YJr)yU$ZmtT>$54p)anr&B2 zdYdKcIMgni>>;~rU$!u(K>0^W>0gUAN5~MIkH%6TQ?47VJ0$&81U~kl{pf>hx@}ZB6xevA^6wyZS>$JIcSy1DVN7 z3)qN->N0xn964i5;J!-m5ygH3&L`Gn@O{fr3Gj;$_g>Py<|cqT;Z z^B}`KI7sv0AYxJwlbd3SBPJCw-A6GUK}=r66r-4Q#B?uYIKGu)f)|WN{%OQ?nwYy) z?eluTq)<$Xh3PiHq*6?lniOol8Ym`t7F<_(6V%kOY;#nzoSef!f5SoKy*n$vhlAe0 zLBx8pvgq~BC9HnA!EWH@a__@Tk`)uZbXGYiSuxRaP+r2BteDbtcPJ-(Q822hr)WvI zb>3wthKjg_Re#5^9Q-C#$_NxgV_$^xW&K11d=FeG*E_({OEIa4X$2eJSLkZTK)ki@ z;zX2^3Y5xcz$+povE(#(nc-LsJE>WEMXoa#HoW)Ic>K_SE8v3s9(qj@c4Dk37 zkBj1AfM*2p+)MGa0G@4#r-*(ez|#hJco95MP%y;9DIN}ZB*eog9u9b9#G?>A&EhF_ zHI2`DqzU$|=~WM3X1*(Sm2Y2cRa+?B^&3$qDSr|t+N}SYV0N$V#S$R0UZJ1i#7dE! zorDv66;31QB(Gq;*cQ>2u1s29xz`VWI@Wld& zg70OpoD6xMAvy{fUlBrbP9mv6V%Wop7SVQh%;NsoSdq~05PC~NkKoQ-A^JC8*+peO z0O_b%_YyN6PNHA$^p7 zX<^pf|MlOs^lda=#NV~tnK?WoevSh_m$^~1u6Ml)e&Eu^a(y4{{^MsKoZolY{Na(K O{^0|4;ctBJ + on `group` (SVG 2) + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-image.png b/crates/resvg/tests/tests/structure/transform-origin/on-image.png new file mode 100644 index 0000000000000000000000000000000000000000..c38ad3be94a884c221f682e9da7e3da597f917ba GIT binary patch literal 31032 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-image.svg b/crates/resvg/tests/tests/structure/transform-origin/on-image.svg new file mode 100644 index 000000000..bfb15523e --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-image.svg @@ -0,0 +1,62 @@ + + on `image` (SVG 2) + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.png b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..32c531e6cb2677b165068bba0d8e6ce9eda0c714 GIT binary patch literal 13595 zcmeHOYe-XJ7(R2RW=zwtA4*7MM4(8a6AWDC%7%6!vQg?5NRu#c+1R}8%rZnoi+*I` z52-L>qYV<9=4I=qKUD03EibvFqSVZsdD)!qoX&TqFqZpK)Q{jjFb4a^`#s+WRk_tH`uahEbT6WY4x?`(1ulNGOLKjqcjFtm{#^G7vV^yniycy3|gg;rKm?ZRMa^qFemJ3 zOur7>2@GKW2XA{hdTvt8QS4>rvvDELG8f*5*SIaPdGUCUq9nnZogvG(GC&>IjhfX@alD0tKmAVByA(I^Dhz!dX1gnkbom=*0*086YOU1eiVR+TL&OwhgAq>GXIdb#Rw925v=cl zP34|d510a&0vBx;b^#nj8!D=B5W!vGk}(3; zFA-y2KzAp literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.svg b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.svg new file mode 100644 index 000000000..5664b6f12 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-object-bounding-box.svg @@ -0,0 +1,14 @@ + + on `pattern` with `patternUnits` and `patternContentUnits=objectBoundingBox` (SVG 2) + + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.png b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.png new file mode 100644 index 0000000000000000000000000000000000000000..32c531e6cb2677b165068bba0d8e6ce9eda0c714 GIT binary patch literal 13595 zcmeHOYe-XJ7(R2RW=zwtA4*7MM4(8a6AWDC%7%6!vQg?5NRu#c+1R}8%rZnoi+*I` z52-L>qYV<9=4I=qKUD03EibvFqSVZsdD)!qoX&TqFqZpK)Q{jjFb4a^`#s+WRk_tH`uahEbT6WY4x?`(1ulNGOLKjqcjFtm{#^G7vV^yniycy3|gg;rKm?ZRMa^qFemJ3 zOur7>2@GKW2XA{hdTvt8QS4>rvvDELG8f*5*SIaPdGUCUq9nnZogvG(GC&>IjhfX@alD0tKmAVByA(I^Dhz!dX1gnkbom=*0*086YOU1eiVR+TL&OwhgAq>GXIdb#Rw925v=cl zP34|d510a&0vBx;b^#nj8!D=B5W!vGk}(3; zFA-y2KzAp literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.svg b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.svg new file mode 100644 index 000000000..44d4530aa --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-pattern-user-space-on-use.svg @@ -0,0 +1,14 @@ + + on `pattern` with `userSpaceOnUse` (SVG 2) + + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-shape.png b/crates/resvg/tests/tests/structure/transform-origin/on-shape.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc77a5c3361dfeed53ccbe95dbef2495fefff01 GIT binary patch literal 9375 zcmeI2e{5S<6~~=+{h@Rz;zwrKX#2-HhS)j}sU(moeFA0cvX+LSWfnAXkPsBZ9#dcf=#kDr&BcC=Eq*xC}C{DE0twRmAs^3N#`tm&b(JUZu~CK zzUMs3JN_e@;2-2j{inXV_uS9@-tRf*+Q0bLfxQ*wca}RGj*9w6>%Q%9l--~Iw|S%a zKb^;WeGbRY*7~}yKhaUPG*kAgo%PS}tp8LX_`=BEy1LfZ)+ZayKY#u&{m%d5@0tA6 zZg+l_*49TJsr%_j{|n{O{jF}l&+l)D=9k=mre|(^-_x;1ce5^CQ8#zA&qU58f}wC$ zUh>^GT9rIFKH*@K=EuGmzu#5SU74&|=eaAhYsS~3&YRN?Nf@xrHDlUJI2=I~tf!+(r>%LebBDSWyq z({4^23>k+y&3Jm^pG+1$UB6nDkE4+f6#`oW zM7t!C(v{5n%>SX2WYgKU>AQNO^CPCwGKG3F-!4xcTd!WI)ZRRlnlf!yiBgAVq3HbB zv)a|KjeSC%^qWr3e@xxe*tMrWz47(Hf_6O9_PS}l*$DM%eNewVTAgej{gJiabdmL* z6YD*k+S6U7nueWQq~6~3`0*;OqT9)9aH_y{nneOvhdRA8Bbi9&jy?UFnVUSji-dlU zfS^QgCNFnan_16wjHDR-jc&2tm#2R&csh%$x8=ja{#GZtaJ{n>dNg_{H9hWMmgfLf zmqC+MZA3tj;uzFukwNc@nOdxw4$$pgPm7r17}LNK0aZCJCQ@;t;j3g!42CR1)F;W# zxL1JWw~I5;`4l-_J}CCZPNipuR-OuH`3Lrubbd)JC0!DG&oSLz31s<#I9Yi~gkwlu z_A|?II1Mc+gv3>%BZFtm8sw7I_PIOXe$o7Kn5vl!5yBC zv6sO@WYHnq(m&|xV!2-4yPn9a{MP|66sCW|1s zT*D2pnr&1mub~L4QBH@cQfg2H{U9gyJE9Z@2{Oo^134*wA($k@#9M)7%PetP88LCh z6s4G=h>0PlLln~jVp0$jZODu{#MFYAK1VUVikKwC^h1j2tB6TPOuwa=eu--8156Ud zq-r1(J_JJP6OH99ZD%&UQb-cv;c}QrT_h;IBF&mL)DZZfGC>YSorl$e3pxnpNwPUM z262ZxXCbCYz6bx0o;SA$zLT$3ggdcU2!g)8!3%|Z)84i*Qy}!ybiK2va#YJr)yU$ZmtT>$54p)anr&B2 zdYdKcIMgni>>;~rU$!u(K>0^W>0gUAN5~MIkH%6TQ?47VJ0$&81U~kl{pf>hx@}ZB6xevA^6wyZS>$JIcSy1DVN7 z3)qN->N0xn964i5;J!-m5ygH3&L`Gn@O{fr3Gj;$_g>Py<|cqT;Z z^B}`KI7sv0AYxJwlbd3SBPJCw-A6GUK}=r66r-4Q#B?uYIKGu)f)|WN{%OQ?nwYy) z?eluTq)<$Xh3PiHq*6?lniOol8Ym`t7F<_(6V%kOY;#nzoSef!f5SoKy*n$vhlAe0 zLBx8pvgq~BC9HnA!EWH@a__@Tk`)uZbXGYiSuxRaP+r2BteDbtcPJ-(Q822hr)WvI zb>3wthKjg_Re#5^9Q-C#$_NxgV_$^xW&K11d=FeG*E_({OEIa4X$2eJSLkZTK)ki@ z;zX2^3Y5xcz$+povE(#(nc-LsJE>WEMXoa#HoW)Ic>K_SE8v3s9(qj@c4Dk37 zkBj1AfM*2p+)MGa0G@4#r-*(ez|#hJco95MP%y;9DIN}ZB*eog9u9b9#G?>A&EhF_ zHI2`DqzU$|=~WM3X1*(Sm2Y2cRa+?B^&3$qDSr|t+N}SYV0N$V#S$R0UZJ1i#7dE! zorDv66;31QB(Gq;*cQ>2u1s29xz`VWI@Wld& zg70OpoD6xMAvy{fUlBrbP9mv6V%Wop7SVQh%;NsoSdq~05PC~NkKoQ-A^JC8*+peO z0O_b%_YyN6PNHA$^p7 zX<^pf|MlOs^lda=#NV~tnK?WoevSh_m$^~1u6Ml)e&Eu^a(y4{{^MsKoZolY{Na(K O{^0|4;ctBJ + on `shape` (SVG 2) + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-text-path.png b/crates/resvg/tests/tests/structure/transform-origin/on-text-path.png new file mode 100644 index 0000000000000000000000000000000000000000..b40c1f2996e86fec1f2a155ae95b2d8f7d7de053 GIT binary patch literal 16250 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 T2k+F!JpI(OPj-I4?A8AbknMi} literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-text-path.svg b/crates/resvg/tests/tests/structure/transform-origin/on-text-path.svg new file mode 100644 index 000000000..8965521df --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-text-path.svg @@ -0,0 +1,16 @@ + + on `text-path` (SVG 2) + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-text.png b/crates/resvg/tests/tests/structure/transform-origin/on-text.png new file mode 100644 index 0000000000000000000000000000000000000000..6df1fa985ee152ca85aae7d983cf3685a05de484 GIT binary patch literal 13386 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 literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/on-text.svg b/crates/resvg/tests/tests/structure/transform-origin/on-text.svg new file mode 100644 index 000000000..47c50ae32 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/on-text.svg @@ -0,0 +1,12 @@ + + on `text` (SVG 2) + + + + Text + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/right-bottom.png b/crates/resvg/tests/tests/structure/transform-origin/right-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..b061b7dc7cc519856107bee1a6cae913cab52841 GIT binary patch literal 8196 zcmeHMYfM{p6mO|K#?k>IBUuN`Z3&6g@j)Cq7!3}o1Oi57NRTo!Eg@vE3AHVSw!k7I z7+AIlV<^?>g6T4lO=LV<7G_B*vOri33~qV9Mp_t;QcJIWuz3HM!$;$ndy{_Qe>mrN ze&_LjobvY*xlGSPo-`Vb8TVey2Q(U;ZU1?=+5RFfsua*@LE5;O=#ND7Wj+1s@wjWp z<6f>%)^>1XV)%Ui*<{-%w11m_{1;!oU3E%|-3p%{8yj=EqpsF-Ac3DE6bOY$19p=M zx8zgm_)EjdDe3E+QOrYhLErjdQF*7)tXLMj)OpgFrqUzAnr+@$+O0rV!O<)HDDIycX$O`FY7xpr2{ovZrRHmBN|mSW6>$5lI4 z4;V*35!l4H2UbVax%8%EddI4}>4HtM@{ToAY{Mhh`4}Cm49iJ7j%5741K5%Stz1W! z*4c3=@l+=;C+sDf>IG~kFbX!sHkBlMm1IFn+4DB2M6#FCn@KesG(8;n=bWYILsP;y zi(GBqL4SYMIn-J|TXsO|y)u4;RMGCHzrv@#>z?|2>xcNwBi$@ft~aT;`=d6P;s%2# zRXRU-_)>^*Of4xT0mxlz&h3jR(r8!D)XaLw(~1plyMV~9R~4NZUx?sU2epTBpBm2U zJBl~mNKFdUpU18W&6PDXWheT5yz)V7p&JBiSLVKig@}-@%*}!3GNy^3-F)77(iSO22uGcLVnB9??5Xe2yzbWjT?`m0J(|OgZ|lXj1D9-ugEpdA${j zAaVsl6n?1H1!H!GlN<0kN9={2Y)*bwrAgNg+H*G(?WrDtByjqOBv|jl{?QTt3~;gt zQF}Vj9yURY4DJKli$(ma`U1>^a6*RX^~!z%8rhh^^eSJih(*q-qe1 zOdBdR0t%t#Fqo(WBzgugdM*g#*aCoiTeV5-2Y3!t3wR4cWfH`a6+e`0*I6^Re$~xHb5=eM#4iuAO^SKY|L0jv+EVFcbEI`&L#mO}4}`=l zTNvWixNrBK7h8N2@9SmPfV5o4pEPux@G_TcR!za*@_Y7Rpe0F?9=PUPHM5fRR(}b; z56WF!qS$DUoDUAvPAM+lbj8OYp=32;PSyZm5)8jlG_th%y+XvUl0D2iXc4wSRdAZ5 zsti<>htk;uZXE?tQOg4H3zcmEx1w1**!fQ~BmHv|m>#B!9XxbMuj7R_# z%RrRV*N>WzTo`~7rm?^WWh)b8YdfS6;WQK8i9%3aT(JLavrt`Jc$Z<3biueHNV;HL z5vVQ+7$7SH)g^ffw)9d|Ds2y4sKvBBbfHq&ur6QJVm7SnIwIBiGgv=(Ig+Y8uwyCl z&(Kivr!P-#uAU%Qsb6SV6wb$Wt zo`{{M2OY$o5rtqG4x!l`{slV6a(W%8tpW9>uNC~?cO966raui9H#G3{W#V1uZO3KW zt04Oc=^W~~*Du-rFOzg==1@0#>Oq2f{-Iv2?5}|2>pz=%Y$M&Tsi#dUNkjqE`#1G$ zPYp?IlOJlfW1pl^yCk(sQgch&%=v%5OL7^+;r5$5!|k2nvm}77DX!U>a{s2Xa=T;w WPcP7mYHZVeTHL#dF^%t>EBOcV#E8ZK literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/right-bottom.svg b/crates/resvg/tests/tests/structure/transform-origin/right-bottom.svg new file mode 100644 index 000000000..6d80a480f --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/right-bottom.svg @@ -0,0 +1,13 @@ + + `right` + `bottom` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/right.png b/crates/resvg/tests/tests/structure/transform-origin/right.png new file mode 100644 index 0000000000000000000000000000000000000000..aa475c7bcad0dfa02b9e3bf93e5fe10896406b87 GIT binary patch literal 9431 zcmeHNYfO`86mCH-f{Z9pvY~VvX8ahlTQrKJ`iY{I=wNfQ>L$g7txlamr>y1raWO;Z zMsX&1VN!>2ERaBuEi!Her>O=FDnW?MeU+OGMM}AqTKes4S+?)IJwHg6{rj5mhxGJ4 z&vTx0-uImMJ^pTt+|TDFACXAp7ri-Zn@A)MbNqRGa{o!m$WIoDmKH}xMQ+!Khm7Ka zrP1Fljb3n~>};JpDoUkNeGtce)Q->Ot^eb5#PQP#g<}&gsq4e2BYX2j z!5R5|U2F0U>CH*zd7MS>FWC%f;m)_vWXW}jQ(tO(-byeP$#To>r4GNcI_8@mG$n~E z-EAv5cj?~^jjs%kEs?hP$Da!+))FS6m`Nw!M0t!bscUfHec~h`t3#Yw#L++vCNc$T zYyRoJyk)oNOLGX52+U-FZ^HU$*BZ=2!G^>}3wn6c)}1u@_atY7Jt*g;V|ae zAz8m~=BYgg+v!Uko|(RyCE`jy+alB3O-YB_<5;isr$OczX5_N%&ZX7e)SZj1Rq1tx zkuy@uJH^d$Y@9O4@`mwmr92Vo9ZHc-Rwdnt$S|~+H=OQ2#?B$|E0DnGL163wYakdM z24))oBPGH7h=B3U;DgbVV0L0)MgbU40?Zx^%ncCCAOeg81G4~tkr?kj-qHB5M{_IC z_FAj4jNLpF61N!#vzY#}a(keqb5-y8#-2~7e%NyO8v8QgnH-TB1yaWDl{&V%FljP8 zc+^9Gb9v1mb=8@}`B^j68L#|0SjIN5o$yO39oFp39Jsf5gz6*&phP^&vjWfD2+z`- zWd)|01m?^MLd{9z&cp`^K%W5#ARU8h)?Y`!L;^5+63mAf7)Z020J9$hV*~{tj{sAW z>RJGpr@+LE&`b#;GX_$YN0>;F_%pp=6@S99s|c1P6VSgLR;JWQBxM4hgYa*FjIE&% zl3`-9Rj|qycOjvCm7g&+Uqg%F0oUqBXY=*4Ydj44^>7$5xeQHY5WO1`6&;9*qG26e z1=%iQ&3uN-?HhpwtH(@0P|B5`Koc4<@t=UJv;!!18qp-oNyT&#P?(J!b;v5c+;;b&3m=x-4{ z=R($`B%J$5o&6Z(fIX1VCe!VzPIjGcqEoW^aV(@Jp>6)0t^pLgvWjnv+0KGkR443;#)#5u+iUzNzL9x@=ZFr zx3VQSv%5-WpIf@q#1JxI=ONM|a+Ewd(S?X~w!lmXBuW9*hDhf+l+LV9^CArPHbV%K2vXXoXf;;(>?Bqj-wZz=wQ87r|LrQ UuZs=m9xREXH^xL&tlypU7s$?5qyPW_ literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/right.svg b/crates/resvg/tests/tests/structure/transform-origin/right.svg new file mode 100644 index 000000000..c60a339db --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/right.svg @@ -0,0 +1,13 @@ + + `right` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/top-left.png b/crates/resvg/tests/tests/structure/transform-origin/top-left.png new file mode 100644 index 0000000000000000000000000000000000000000..a80726cdd1ee3064f343862564a356611f610e3f GIT binary patch literal 8144 zcmeI1eN0nV6u^tj%5X?Db4)=<#BIscb!G`DD>hr0NN{f1#LhXWj#!tes~_vyTI7Mc zu}w!89fG*P-2O138BCpHO3N$mqe0l3Wn&PDeMP>M4*F=tQf&M7UZ2W(&ii+NEPF5M zKitc?zk9y!J?DP(^7`C__=WKji6kL!ZO$taNz5|t7bj)^VmMH#l}OU6^KzcsppO}K z#eBLX@7R*Oc}E&fw&dpIsMYF?1?;EeK7Z%G@x9B5Rw}tw)ao^Bat^ngIvMX+r&j8; zI^9bSZpn3Dmi626_jDB~3nvu4RGfa7tNHtp26N+7{iyZTE~u9a;BxSj30G1U*yFO zUMQz#6y@3?qjTuqH?-{Zki&b`rip1vcSS~LP3~d^H<-iQ4J=Pt_gp$M8XL^uI0~5a z5ioazwyZm*^c2U@z-;2dcx<r9w=IR%*O!!}x2{*PrVK{T9$W>8}>d9MQG^;0$)~lr!q#0+j?;8Ehaf|O& z8J!+o$wr3Q{|t>8P*%@?dbb$__fw+{rb+N;NuQgq`)Vmk>xeR z;}}T=k$e*ZliYxN_AP-)!*G(2q<%1oyn(?LRFtmeHA(FPDrf{Mke$n|FNQ{u^F4&? z))iEOC6nF~E=WC4IoO+BX)kHhcpu`s75n(M#E1T$OEPJv?NC5zytV3t#=kPvH(Dg;%Fp^~lvbut@pz{qML`Aq@xZBXa= z@~M=LW@CyeaF7~U5Xk}7-EdNez@#m3Qhqj)luf`%9Rib@Aoy|&UiUjxK?YWV<2LY3 zvI_^ye2CPGk=o#5a}g2afj(eDX(dEJAmTG*UB(Qz-;cD-q*=LErh7Ux({4fspJIkU zPGQ#CH%^Aa$8>=!s=-_?e8c43hUEP~QT%##ekn-~6t;Smfw_@3qzP?o@6PurkIYcr z6f7t`%w>7ZT%5AG&wnT1Ik=RlO6%hqu*_c&Cl2Gl5TV9;;z>&7eM%KD2je0K0pB}K zzn^c&E=X){)G5%iz@MPTcfMgnkODhwp+flf9L zFV+Wbapu|YE_?jdmJVh3NyX*JAxJ_+6Ai^ngQY}!OUYo9mp|66Zy`aekf0-Dz+%M5 zNKnWP^KqgMdPf6cg)V)%@@x7|=rNDCio9atZxR|G2v$WWGDvt7Vppt0*rWp}l4!=* zcVGbN@cxOT5L=BbGG&6;)By4|<}AcEV;@mCA$AqU9)_O17~7wy1K6@-0)qB~5Rqje zMG~!0&>z?$JAnNS<=C|7Q1fY#k1(%;II6Sb#oWMZhB??Eo^jwun=lYM`nL(lnTVG2 z0ajx4HKVUb8=YO*PE6l)1Z{N}j?b#=pH8gi@IS4ozfj@3NYD?jJ`(l#^X)|AKwIKs z{5)m7`a|Gzg+9LiL41uDU!BCqGO-QF-jDv%Z9tPWTzfAUES!AX#lK(Qt;%m_&8Uo( h-F*?>NrX(N&F3B+jz4N-U+N`!FRagLe16NpzX4ja#eo0- literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/top-left.svg b/crates/resvg/tests/tests/structure/transform-origin/top-left.svg new file mode 100644 index 000000000..02dae007e --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/top-left.svg @@ -0,0 +1,12 @@ + + `top` + `left` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/top.png b/crates/resvg/tests/tests/structure/transform-origin/top.png new file mode 100644 index 0000000000000000000000000000000000000000..337452d0ffa62a4193ed5cf7721fc50015e41826 GIT binary patch literal 9408 zcmeHNZA?>F7;f#L3d4r6**KME(T!wwl0_qafQ?#VL`RoxteQdVLRR9OL87d!eB3H- z&X7?Z4kk?NN0bC7#5scka*Il;Ljue({K#yj@@=J*4_hd`>uqJd=e+#z&wgyVH{lPr z?>W!=ywCf-=bU>Eevp(97V>%shr&2t%2)Fy*^4!a5 z9Ldy0AFu%*T3rV>uZ+e~L2dB^^TQtCkW;Yzz*yKU+pI{~l%(s-f1r|h_!GZ&bgktm z?Pam~bbX@=usS;|`J?>m62bUElS8Ac&YrJ#?K3}~OV?9MgcnF^?;2Z{wpA8eY?3@$ z7OG1yq`AJ*Ti*?gUIRwIJvF*B>L*nLFLr!>6FN)DRk}Yg$}qJSndP`1PE!G+yb$Y|RldbOXov3jn^~aMdFJKZA8?#7XvBKr+O|1vzGh@)amKS1k;6gct)+A~ z_EcwMeUNHInD+4EKy=DLbk?C1%!W21ITY7f6(-#^qLpN{6@!`@#G*3hoz%S6{U0KleG}IA`pl3 z=hcmTy)$W;WSk*$TjAw?^E9p@wX`SAd}upq{oHkOGT$OGp`wg|Ze0==ATEujC-kq{AFt%AnS_tL?FR z8B>oJmUJCh(i|>_E2DJWmuRD@{*A$r1<_4{)Y63>(Ft4n-riX0%+0>OnQ-74nSZFa1-(x;=W)HC{VR{fg;W@L+KX5My=k1uf2kqB6CXM zx?>RZ_V3n=g=xzNHCys}f1f`@-UUjQG+?#HrFPzpd3>z?dA?BVK)O(`;uCHHX7WOimQQ(Tyr zeYj?b?-2pSKx8rWp&0WJiaLlOd$EP*1d}rYwp*eID}KXDG-raKGdqAYhY@F@rf~+) z{59f?0Sds}j|)JEIs-c=aS?aMj5;$-WNg5hBE*^1P|ZT1=6EDEW8hAz3Iwwm@xKbL zhL!l)q<8u|g9N7{M^pWSWg+G~O{NUM{0A z&6tKTV6i@6G0Ok>A+lE|kdE&?$Mqg#&|J-lAl(1#W=T8-?5Qq$h8{Q%VUPa7;*tH( zg4bYHB&@5oV&I*1K@&8#$wHcLDS8X=(lwozg tt2PDxt1$PxH*7L3XP#F=TNM>9KXN&0;*#*1_hu3&aYK@{a^2Q_e*>cfOeg>V literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/top.svg b/crates/resvg/tests/tests/structure/transform-origin/top.svg new file mode 100644 index 000000000..9004d69dd --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/top.svg @@ -0,0 +1,13 @@ + + `top` (SVG 2) + + + + + + + + + diff --git a/crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.png b/crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.png new file mode 100644 index 0000000000000000000000000000000000000000..f4eec7c22dc644a742867be41f6dfabefb85cdf1 GIT binary patch literal 7448 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4mO}jWo=(61B1+cPZ!6K3dXk&7$>NBPEzp{ z>RvT-x4Vl=T3T9IC=ir`!1pt>03{%!!^1(gq@}sJxwPz_J(KIaPg;0+X?eN-d61Pp ziy9su@GiU>8Xo)A{`P&2irfd=cf4A)dfl)2Z%bMBn0<&oaGat3EKs`i>VF-5-Wt&l zR!niyKXf0IHr#&(RJZF@{j}?Bb=(!QjO%&-h<&g+kY5Zj?Q}Nte%3vG4G`G}+jl~Y zeY&;rK2x0g0kACJhublwK;?hWf4X%bpK-nN1F$Unk6fU^t5*L@uLQcM;kw`luq^W* zGmtw%|8E96>^0c64c8g#W`o?Z>VG7}VGw7(X4nsNHpF40&K?cz(L^$uB|+uPXh||! zZjV+sz{-8JmIO6*M%yGqs44aL{->KW{`=cW7he5uiqW+DA8jZ7`Txmu<#oUIKSOV7 zeyu;9p8U`LXU>t(|JO0vkpJ?3=A8MjKEHRztN*hw+O+?5=lA~n|75dZ_}}=?=#AB1 z`=4$${6D|O?9r9{aK7w@&9S(kNv6tbhBgizx5c+-BHiN z`x>J$3+hjS+VG>v7~Cra_4`KiEWFb_T0nsY7C=J>!>u$156+AZr;LsQjgGmE4i$oi zUPnhbWk<)8M~A^jCmo;-p3%7yjB$X`nJM%!3ZfH9Kij`qun9G(A6d}q20Bmo&5Egy hSsyyHmQ(+?znsB=GeF@>4RFSj!PC{xWt~$(699>%5FG#j literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.svg b/crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.svg new file mode 100644 index 000000000..028b98af5 --- /dev/null +++ b/crates/resvg/tests/tests/structure/transform-origin/transform-on-parent.svg @@ -0,0 +1,14 @@ + + no transform (SVG 2) + + + + + + + + + + + diff --git a/crates/resvg/tests/tests/text/font/font-shorthand.png b/crates/resvg/tests/tests/text/font/font-shorthand.png new file mode 100644 index 0000000000000000000000000000000000000000..1133e21cf504566480b382c372b4887e7caf1985 GIT binary patch literal 13832 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 q? literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/font/font-shorthand.svg b/crates/resvg/tests/tests/text/font/font-shorthand.svg new file mode 100644 index 000000000..b9d5838f8 --- /dev/null +++ b/crates/resvg/tests/tests/text/font/font-shorthand.svg @@ -0,0 +1,13 @@ + + `font` shorthand + + + + + AVA + + + + + diff --git a/crates/resvg/tests/tests/text/letter-spacing/non-ASCII-character.svg b/crates/resvg/tests/tests/text/letter-spacing/non-ASCII-character.svg index 6bbe1b59a..7ede9fb04 100644 --- a/crates/resvg/tests/tests/text/letter-spacing/non-ASCII-character.svg +++ b/crates/resvg/tests/tests/text/letter-spacing/non-ASCII-character.svg @@ -1,5 +1,5 @@ + font-family="'Mplus 1p'" font-size="48"> Non-ASCII character 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 + Indirect with multiple colors + + + + + + + Te + xt + + + + + + + diff --git a/crates/resvg/tests/tests/text/text-decoration/indirect.png b/crates/resvg/tests/tests/text/text-decoration/indirect.png index f9787ac05c3ebb387b2563dbee522b3e0af9feed..f609ec0101c89fa03ca9c0c8ed87864a29375fe8 100644 GIT binary patch literal 13555 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 literal 1519 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 Un;*gp>5}!YZd`lnhdKNH7nE>O^8f$< literal 2106 zcmb7F3pCW*9v&xo4VojyW11MnC4Y{`7}u-3Cfu2j$Mi>HDDUGvQMKdb=F;{v)0*b@BMx2`~BA5Ywfka^<8$bvz8E3 z5Q9J<61Fy0ju40du)9R|>?n4{@zy)w>wtI0?%*GeOP4O~=oAX&x2nCp{crqhiA*N{ zUilL)8iAkQDWjqs?VKPmCGBHo_70w&US6SL@nlLSJ^x1Gtx9%dQ+Lmsw~OyLUy(B) z5MggyD|6@QR|`Gn0%i~xL>od8|ClWJHkRz3s1jiAkarCS;}_^N1k?QpOqfGQmI{sm zxPDgN-K)-`{vTL(v%fS{oo)ZxskqbiU-~UVz$Blo!mPaudwc?!@*Vpytxmm!5x_CW z2t4_W8&y9raplp8{s5!LSJ$?p4v^N9Mr^1VjIk_7ezFQkRRJ3k?bN{M^2PD0fX5U3 zeg=A=bLK;v?pU^$%5cCyV}YMOHRs?DV0*7QJIe`5i1|omzi3X_KmevP)Kzpku^nwP zD+@MkXQe*qg6{=wEK`+f=9Hg)P|&|B9FMTo1e?KKckmW@g059RnsbQsJT%#oYpIia_a{4|6 zuMx2juhjP`@470@NN2$H_R|kEwSo<#*v<$>-93>)QXt2VQu1t|Lsx=aM+_JaVZc$x z0pZRcuOzpHqMIBsfT6}6RIQsjsIZ;-IR*;clR{R*$FMnxG^*YL0VgNQSb7K(exZ=c zoon|*=ONFEFqlIY{k6)$&bHt^u3Il1&Zl4(x#u;wXL1s-p|{k;8FQ5hw?s%M!I@Xp zsmtkY_8e`p98#X8#5#C5+$4c^l1`9G;y2IBU&1b0O&0B)*J^qdAvy%QC=k}20OM?z z8ZR9XJ?y8F5@AH<;ZeAM2wki+@RZPR=MLMOS)%Qg70 zbp%?i`v50*8fF4#*1z`8W(p-Ty>CleZo(Oda2Ad!#lFXbB3sL9P9etSo)E`^j(&XI z6W$=FiUX>!r20ngGpBnMU`%@9)CxW!LQcbJM;Va#M&v3hAiRs|(|dtO#|7zhf0Q&TAAk4)IH4GNx_UDm zROh%%xGly`&;3IWI=cc+$1Hj*hiX}W`fxF1_j~_^xytuhnwY+x+3rcaxShMTzOm}F z)CgG4y8eYT{^&yALg_)VAPO1*ozg*9PL-g|ai+U9No3Ot`7Ib&%z8GWq`iZ9z@ z&xIAG_9n^>|_#2;z4mf z*NXf)&JpUhF?qK0J+hnuqSc-^7EG-%yb3@|W?T(-%q9?9BdvR)J87y;|eNv$`c zNXmy8R6Z}V$;2PSnlW*VPz%!eox?0Msx^h6d;NIj3~edab?ZUNTzzTUfbb{E<1Ypa zpBye1LAXdUd^7fyXm$vsjne*2NPKccByHI2q#Q8dLl63Xu87_9$WLZk{z{=Y@sqZZ zE@%##+KfjFL5r2jkBE0B59v7q!DPeMem)dHEcrU_Ux67RsQU#1UErMmoBiD}V^Y32^at<>J0IIn}x0Hf08qR6z& zQU?#tirlf6vrHkrHlbigE{p9K`{#Pii~SgulSwOY6n>lLuiYQ!qVGz)y{RP9a1D}4O1o)X>~&!p}WS|&@?5|8d$c3 z>WVCu1so2Njptl{9>1aYtZ0#qx<2S#u!@XwYYkw!<{cfa^Zg*tarA!B_Gt7&*FcUh zyl* kv4nKN@pCFVxSO{960!5k4I)>ma=XcGi?y?=u<)Y(3Zc@cBLDyZ diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png new file mode 100644 index 0000000000000000000000000000000000000000..a5bd97ab94c4323609532aa80e71fe7408e7da53 GIT binary patch literal 1635 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAT=|htFTQe*fWv*Y7`kb;~(KC^7s`|`d`b;ti4!D;!yN6c?(o|?L~wdw82Q|(PBCaHQ)(}{eq1R^)3D56tq z>ZX7dY)Ux^QAtyV)y<8}SHo(}`X1XEt`u3w#989N+NIIu@ud2{`1fnU*DYRN+h)CQ z?(g~X4v*)ln97?af3woryg7B}{g?Yojj!GMGCj%FG}&n4jhgvq7_YuRa;w-==hord z_G`{hGH6-#D$}Ki;Vb{^$y*%e^XRU1)neFdGHs*#?!VWHCQnarWq8?M`fZa!ScTiK zbn%>Tx<|b?$Xsako_g8!bW-Yt9WoCT`qgc{cTZ0cbx{1Q(=pXaNog1RC3UeVQ-+J) z8?+AmmES0ILT~OKo5_#%H!Y2tRjgfCrCn$*t8F%a+LStm(~eTJ=Gsht;~Fu$nf=x$ z|0h$veEF%C?Dw{$;nr1#eRoz{K3wS{3`7wtdF_`4* z6;`s#m;Y{CIK^$^X~{`(eu{zhUc$Gm~QUL?(=R_=zK0Czpv!^Y@OAI&exfQ+1Y12DKgF9 zzOl`^#YSq`EtJtNyr@BNM{tXMgy!xZTBFVeI|2#ZjW({WWRjtm0L#&RT~5wOym}g)7oLjQ4`2s26uq z?y1n^of{78_b%n1mvl8k_kz#!NDlA!x9$9=tdaCKwEnrJJHb`=N!Hu*Yck@JIdyEe zh4qP^&&m^ABNXuZ_@w^&wpqJ3JYwH?=wN!+)OCETnf6R77OIyI`=St5|JD5T+Rs-P z-aV$27R>bX`-WGmBOX4}30N3v&-c-@k0 zmt1-6r#Us2>v8{tx3?Nu861K3aj;MLciMJa)BcD?b5$FL + Ligatures handling in mixed fonts (1) + + This is a very resvg-specific test to make sure we're corrently + handling ligatures in text with multiple fonts + + + + + + + + final final + + + + + diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png new file mode 100644 index 0000000000000000000000000000000000000000..43a48444a4528e6ba0c49668cb8303117efb1073 GIT binary patch literal 1638 zcmeAS@N?(olHy`uVBq!ia0y~yVAKI&4rZW;-{LxVAT=kzC&U#lF79uVoAt5X* z3}N%}@c|iJTp)LfiHWhZvqQw0nL+M6DPIPnj7x(2f*lwH6b$VB1LETo5;C*$3yMo> z>l@lT`X|huJ!k%cW$V{(*syc|fx~CdU$}n%;e%(d-+%a8+_du?0|Tp@r;B4q#jQ6N zLK#~bL|8X`X4!7=<$Rm!j{iA=)AEClnBUYqHFaxi)7z7$+M7;HQuUsu6Zu{VL~cq^ zM5or&O#v&|lyVZHlBNu+o12-hdK6mpJ@yQldob1J+M7_ZrAIH%yY@NkckTc3-I)qP zuXb{;y|(cUf5R5T)vnz?isn|lG@UPUdOd@%?ozHM(J;0TmK&3n z{kSBpFrE8n?^kuTQrk=3D|>Hl+_=Z!?B(w<%~sXi5&jGtEjuS{Vi0IQwu^@$B>zcJ zBHzS6_iN^^@exY+!q>A@N~4sUC;#H9UK4Jn`;CDoHCby_`nyusM5+2(CRQAtKJSY1 z>B;-qvn6vSQzW+bY3-j4lGe-wSohUo> ziD{?A|FjKB>C;zOOnaTw`qTJAsM@nbxsO$^KReBNbt$97syo%b6_qdUtlPWs)J^gI zQq!jM@0b~@9&r1)*kr}2e&28PJ=ro(>$sHpg*SbwW=B4KF|}`q zntf(t1NVh=t80<`4Q1lCTOxIUnPS45UeU7~w&{K_){)3vc!i;PlG()>KdZ0lgiK069MI-BU^ceQ4PZL_iAoNiv zmSM{>=ajVvvyQ#Fb@5SNdW9k-znI@fUCQ2=g zX4JOUm{zh}tDz{%U-s#jL#oVS2QKYD#MSg*&a%wX?1;79PvcH-G{+XalM;S^+^eol z^ck<&_O{UT8=grNrtn9wd+Q|6Wq)uX!fbZ(%2PsDXI|T7$1`o7uV3u0-i>uqnB zhUk>mC9K(gv}1+&B0eiT*funJhl z%J83NKE=ONdfy>0z7A2wU(Oq<6egSfE6~w7|L6PRu&}rP9Q35}_*VaxoVMY%!2K;! z4*Bh(j65sVm&%7ZlrcxNTF003P2A1v<>0gSp$yB<1&wMv$&2^cP17k1K2eyiW4}kb zL66r)v-vagfy0&)E^%MtlX&M;Z?$SC%c(sx4PUkvrhQq@Tx|S7XieABtYs5CBI{MI zWzJL2k@LSlqsZ!R^5-XVrS-C(pXNXK`S0}BulzRu3at#E-p-s?e|CrFGm~%qH)m8A zyUl)YZ%|qHC+k CL#tW< literal 0 HcmV?d00001 diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg new file mode 100644 index 000000000..1921c8495 --- /dev/null +++ b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg @@ -0,0 +1,19 @@ + + Ligatures handling in mixed fonts (2) + + This is a very resvg-specific test to make sure we're corrently + handling ligatures in text with multiple fonts + + + + + + + + final final + + + + + diff --git a/crates/resvg/tests/tests/text/textPath/complex.svg b/crates/resvg/tests/tests/text/textPath/complex.svg index 14e7a17d2..f481c168f 100644 --- a/crates/resvg/tests/tests/text/textPath/complex.svg +++ b/crates/resvg/tests/tests/text/textPath/complex.svg @@ -7,7 +7,7 @@ 60 60 0 0 1 40 100 Z"/> - + 非常に長いテキ diff --git a/crates/resvg/tests/tests/text/textPath/writing-mode=tb.svg b/crates/resvg/tests/tests/text/textPath/writing-mode=tb.svg index 429ccba0a..48e968488 100644 --- a/crates/resvg/tests/tests/text/textPath/writing-mode=tb.svg +++ b/crates/resvg/tests/tests/text/textPath/writing-mode=tb.svg @@ -5,7 +5,7 @@ - + 日本語の表現 diff --git a/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.svg b/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.svg index 336405644..a1f73480f 100644 --- a/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.svg +++ b/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.svg @@ -1,5 +1,5 @@ + font-family="'Mplus 1p'" font-size="64"> Japanese with `tb` + font-family="'Mplus 1p'" font-size="16"> `tb` and punctuation diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-with-dx-on-second-tspan.png b/crates/resvg/tests/tests/text/writing-mode/tb-with-dx-on-second-tspan.png index 51f48700f5340f63bcdea6ba6278eccd00cf1a12..74946f26460c572900fdeb4bf8ebce44ad14043d 100644 GIT binary patch literal 16922 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 G-uy3aI_QD` literal 2136 zcmaJ?2~<;88jeIj0;qsQb_RrDLV|260wN-yl0dLV;EC95qOvHJvPyse77+{}LI}I9 zO*lxZc??MFf&@up*hH3qTE;*NVlqfd4FP1C7ntd3&zaME&U^oV|L=a^{m=dXci;8n z_@lZA69fzf(+xQ0cLE06gi$?P;7|mAC*vq&QjQ0n^oQ*K9a&jfP@GPu|BgmPMEveU zj)#W_ggQGrXJ%&pG5$w?>-yM;AP}l#paF-ga!}?k--3)?!U;S9w*7BT-iLyZ6Nw~J z+!qO%^rGUMRaMn>x9{_ww!i2f8GS36n3d1#DsH5~V44>K{CrNPy#BX1OXIM!nP7bE zf%kRXw(YRAH8-v)rr3dccUB40wA>$su8G2Z=1Dlfo1Z@R$pBsAfdF_Uezt-LWUFf< zF#vD#`kSPs@=1Js=-sz-eaQw4tzCC;_DAb;_mZW_p!8)6If(UN_Wxj&W?A>KINp)^ zHa%IWFdF!hvw?9Hfcudvf=X9K6dtv{$DgQN+WA?0>CnDxr$Xh0ByQ|xX1nHev%gK| z>{>4QP;vVMqn~1<8%4{q1fTSKi!%;0H}6XyYW9m0SEHvd_La+l3Dj4B=o?|A!rJPf z1F!1_fv_ZRmVAPM%mm9@$U=Gy_#8jJB-5n(p z-6kSLR8jp7Dx3A%VP00_%pAuWck+1@-Ig^_;NMsNg`HXJApaDGUmB13 zduGR(L9X^xiVibHx{|g^l$wCnp`A^$*COjI8ebV9&M|eE30PO;6FO`-o;NnbkBiuW%yr8UNP7e zP|#4b%L2ELZCTKRB=}-KA>kZxu1c^MXiMlE>SE~=?ifTQZV7rXv?h@6nmdIU>yy+u zAE}GXUAs*PdQG-}VFe5+T6?gH}2uolOg5X(R$m&uDvQSTe9$ANYdqr+4gT%9ez|fI8uicmJ zw?RTDsjrZPppG+@6QCbK3dUF4kKzab4`@P_saSmj{+BvW)~HTW02t#h_AU?SWL8{d)iZda&D5M)4MXdlLZBtsIiY|01cqo|Z^6wx8 z)PJN@=qC?PF`@YT&L;SWdqh<+gMe(fnl9A9C-Opk^Pu7tmigqX%&=ti!7+2o`NT_I zbHF#KL_lMIE&ZG}F=r>f;aR+#k-HfxV7TJlQg8XX?y+Q7He; zkUCah)=@43X~rDGCx9}FF&Y*Cau5p|jYep0Vn!jX;8YZY!cA*-*@Pr$5*v(VTOBm( zQg?KlwJ<9|PUh`Btzb2*V5N9^54B=z>)RUEX+XBr7MfaHF4sL`CvIU%_BQh4!lng? z7`EJ>8{^=)hALqIVd3!lkdC8zvT*HJJ&(A%DbC@hzU3|Q@6Pj@CR)Fgg3n_wb2HhQ ztsPcM@26ybhGMGG`z7klM1P?*Qb~T{7CRU0=y*Ss(SV_xDQ8Gz|DZe(B}q(f8ju?}QnPBonsmDu9`+F?I0vDMyK`DY_@+@Oc*=ubr6+=HvJ8P=Wp Xbt%2wl1_wv)35-4yx+aUr0o9ygCfV6 diff --git a/crates/usvg-parser/Cargo.toml b/crates/usvg-parser/Cargo.toml deleted file mode 100644 index ef854e1aa..000000000 --- a/crates/usvg-parser/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "usvg-parser" -version = "0.36.0+class" -authors = ["Yevhenii Reizner "] -keywords = ["svg"] -license = "MPL-2.0" -edition = "2018" -description = "An SVG parser used by usvg." -categories = ["multimedia::images"] -repository = "https://github.com/RazrFalcon/resvg" -documentation = "https://docs.rs/usvg-parser/" -readme = "README.md" -exclude = ["tests"] -workspace = "../.." - -[dependencies] -data-url = "0.3" # for href parsing -flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } # SVGZ decoding -imagesize = "0.12" # raster images size detection -kurbo = "0.9" # Bezier curves utils -log = "0.4" -roxmltree = "0.18" -simplecss = "0.2" -siphasher = "0.3" # perfect hash implementation -svgtypes = "0.12" -usvg-tree = { path = "../usvg-tree", version = "0.36.0+class" } - -[features] -class = ["usvg-tree/class"] diff --git a/crates/usvg-parser/LICENSE.txt b/crates/usvg-parser/LICENSE.txt deleted file mode 100644 index 14e2f777f..000000000 --- a/crates/usvg-parser/LICENSE.txt +++ /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. diff --git a/crates/usvg-parser/README.md b/crates/usvg-parser/README.md deleted file mode 100644 index db05034d1..000000000 --- a/crates/usvg-parser/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# usvg -[![Crates.io](https://img.shields.io/crates/v/usvg-parser.svg)](https://crates.io/crates/usvg-parser) -[![Documentation](https://docs.rs/usvg/badge.svg)](https://docs.rs/usvg-parser) -[![Rust 1.65+](https://img.shields.io/badge/rust-1.65+-orange.svg)](https://www.rust-lang.org) - -`usvg-parser` is an [SVG] parser used by [usvg]. - -## License - -*usvg-parser* is licensed under the [MPLv2.0](https://www.mozilla.org/en-US/MPL/). - -[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -[usvg]: https://github.com/RazrFalcon/resvg/tree/master/crates/usvg diff --git a/crates/usvg-parser/src/converter.rs b/crates/usvg-parser/src/converter.rs deleted file mode 100644 index 90eac6533..000000000 --- a/crates/usvg-parser/src/converter.rs +++ /dev/null @@ -1,725 +0,0 @@ -// 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/. - -use std::collections::{HashMap, HashSet}; -use std::hash::{Hash, Hasher}; -use std::rc::Rc; -use std::str::FromStr; - -use svgtypes::{Length, LengthUnit as Unit}; -use usvg_tree::*; - -use crate::svgtree::{self, AId, EId, FromValue, SvgNode}; -use crate::units; -use crate::{Error, Options}; - -#[derive(Clone)] -pub struct State<'a> { - pub(crate) parent_clip_path: Option>, - pub(crate) parent_markers: Vec>, - pub(crate) fe_image_link: bool, - /// A viewBox of the parent SVG element. - pub(crate) view_box: NonZeroRect, - /// A size of the parent `use` element. - /// Used only during nested `svg` size resolving. - /// Width and height can be set independently. - pub(crate) use_size: (Option, Option), - pub(crate) opt: &'a Options, -} - -#[derive(Default)] -pub struct Cache { - pub clip_paths: HashMap>, - pub masks: HashMap>, - pub filters: HashMap>, - pub paint: HashMap, - - // used for ID generation - pub all_ids: HashSet, - pub clip_path_index: usize, - pub filter_index: usize, -} - -impl Cache { - pub fn gen_clip_path_id(&mut self) -> String { - loop { - self.clip_path_index += 1; - let new_id = format!("clipPath{}", self.clip_path_index); - let new_hash = string_hash(&new_id); - if !self.all_ids.contains(&new_hash) { - return new_id; - } - } - } - - pub fn gen_filter_id(&mut self) -> String { - loop { - self.filter_index += 1; - let new_id = format!("filter{}", self.filter_index); - let new_hash = string_hash(&new_id); - if !self.all_ids.contains(&new_hash) { - return new_id; - } - } - } -} - -// TODO: is there a simpler way? -fn string_hash(s: &str) -> u64 { - let mut h = std::collections::hash_map::DefaultHasher::new(); - s.hash(&mut h); - h.finish() -} - -impl<'a, 'input: 'a> SvgNode<'a, 'input> { - pub fn convert_length(&self, aid: AId, object_units: Units, state: &State, def: Length) -> f32 { - units::convert_length( - self.attribute(aid).unwrap_or(def), - *self, - aid, - object_units, - state, - ) - } - - pub fn convert_user_length(&self, aid: AId, state: &State, def: Length) -> f32 { - self.convert_length(aid, Units::UserSpaceOnUse, state, def) - } - - pub fn parse_viewbox(&self) -> Option { - let vb: svgtypes::ViewBox = self.attribute(AId::ViewBox)?; - NonZeroRect::from_xywh(vb.x as f32, vb.y as f32, vb.w as f32, vb.h as f32) - } - - pub fn resolve_length(&self, aid: AId, state: &State, def: f32) -> f32 { - debug_assert!( - !matches!(aid, AId::BaselineShift | AId::FontSize), - "{} cannot be resolved via this function", - aid - ); - - if let Some(n) = self.ancestors().find(|n| n.has_attribute(aid)) { - if let Some(length) = n.attribute(aid) { - return units::convert_user_length(length, n, aid, state); - } - } - - def - } - - pub fn resolve_valid_length( - &self, - aid: AId, - state: &State, - def: f32, - ) -> Option { - let n = self.resolve_length(aid, state, def); - NonZeroPositiveF32::new(n) - } - - pub fn try_convert_length(&self, aid: AId, object_units: Units, state: &State) -> Option { - Some(units::convert_length( - self.attribute(aid)?, - *self, - aid, - object_units, - state, - )) - } - - pub fn has_valid_transform(&self, aid: AId) -> bool { - // Do not use Node::attribute::, because it will always - // return a valid transform. - - let attr = match self.attribute(aid) { - Some(attr) => attr, - None => return true, - }; - - let ts = match svgtypes::Transform::from_str(attr) { - Ok(v) => v, - Err(_) => return true, - }; - - let ts = Transform::from_row( - ts.a as f32, - ts.b as f32, - ts.c as f32, - ts.d as f32, - ts.e as f32, - ts.f as f32, - ); - ts.is_valid() - } - - pub fn is_visible_element(&self, opt: &crate::Options) -> bool { - self.attribute(AId::Display) != Some("none") - && self.has_valid_transform(AId::Transform) - && crate::switch::is_condition_passed(*self, opt) - } -} - -pub trait SvgColorExt { - fn split_alpha(self) -> (usvg_tree::Color, Opacity); -} - -impl SvgColorExt for svgtypes::Color { - fn split_alpha(self) -> (usvg_tree::Color, Opacity) { - ( - usvg_tree::Color::new_rgb(self.red, self.green, self.blue), - Opacity::new_u8(self.alpha), - ) - } -} - -/// Converts an input `Document` into a `Tree`. -/// -/// # Errors -/// -/// - If `Document` doesn't have an SVG node - returns an empty tree. -/// - If `Document` doesn't have a valid size - returns `Error::InvalidSize`. -pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result { - let svg = svg_doc.root_element(); - let (size, restore_viewbox) = resolve_svg_size(&svg, opt); - let size = size?; - let view_box = ViewBox { - rect: svg - .parse_viewbox() - .unwrap_or_else(|| size.to_non_zero_rect(0.0, 0.0)), - aspect: svg.attribute(AId::PreserveAspectRatio).unwrap_or_default(), - }; - - let mut tree = Tree { - size, - view_box, - root: Node::new(NodeKind::Group(Group::default())), - }; - - if !svg.is_visible_element(opt) { - return Ok(tree); - } - - let state = State { - parent_clip_path: None, - parent_markers: Vec::new(), - fe_image_link: false, - view_box: view_box.rect, - use_size: (None, None), - opt, - }; - - let mut cache = Cache::default(); - for node in svg_doc.descendants() { - if let Some(tag) = node.tag_name() { - if matches!(tag, EId::Filter | EId::ClipPath) { - if !node.element_id().is_empty() { - cache.all_ids.insert(string_hash(node.element_id())); - } - } - } - } - - convert_children(svg_doc.root(), &state, &mut cache, &mut tree.root); - - remove_empty_groups(&mut tree); - - if restore_viewbox { - calculate_svg_bbox(&mut tree); - } - - Ok(tree) -} - -fn resolve_svg_size(svg: &SvgNode, opt: &Options) -> (Result, bool) { - let mut state = State { - parent_clip_path: None, - parent_markers: Vec::new(), - fe_image_link: false, - view_box: NonZeroRect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), - use_size: (None, None), - opt, - }; - - let def = Length::new(100.0, Unit::Percent); - let mut width: Length = svg.attribute(AId::Width).unwrap_or(def); - let mut height: Length = svg.attribute(AId::Height).unwrap_or(def); - - let view_box = svg.parse_viewbox(); - - let restore_viewbox = - if (width.unit == Unit::Percent || height.unit == Unit::Percent) && view_box.is_none() { - // Apply the percentages to the fallback size. - if width.unit == Unit::Percent { - width = Length::new( - (width.number / 100.0) * state.opt.default_size.width() as f64, - Unit::None, - ); - } - - if height.unit == Unit::Percent { - height = Length::new( - (height.number / 100.0) * state.opt.default_size.height() as f64, - Unit::None, - ); - } - - true - } else { - false - }; - - let size = if let Some(vbox) = view_box { - state.view_box = vbox; - - let w = if width.unit == Unit::Percent { - vbox.width() * (width.number as f32 / 100.0) - } else { - svg.convert_user_length(AId::Width, &state, def) - }; - - let h = if height.unit == Unit::Percent { - vbox.height() * (height.number as f32 / 100.0) - } else { - svg.convert_user_length(AId::Height, &state, def) - }; - - Size::from_wh(w, h) - } else { - Size::from_wh( - svg.convert_user_length(AId::Width, &state, def), - svg.convert_user_length(AId::Height, &state, def), - ) - }; - - (size.ok_or(Error::InvalidSize), restore_viewbox) -} - -/// Calculates SVG's size and viewBox in case there were not set. -/// -/// Simply iterates over all nodes and calculates a bounding box. -fn calculate_svg_bbox(tree: &mut Tree) { - let mut right = 0.0; - let mut bottom = 0.0; - - for node in tree.root.descendants() { - if let Some(bbox) = node.calculate_bbox() { - if bbox.right() > right { - right = bbox.right(); - } - if bbox.bottom() > bottom { - bottom = bbox.bottom(); - } - } - } - - if let Some(rect) = NonZeroRect::from_xywh(0.0, 0.0, right, bottom) { - tree.view_box.rect = rect; - } - - if let Some(size) = Size::from_wh(right, bottom) { - tree.size = size; - } -} - -#[inline(never)] -pub(crate) fn convert_children( - parent_node: SvgNode, - state: &State, - cache: &mut Cache, - parent: &mut Node, -) { - for node in parent_node.children() { - convert_element(node, state, cache, parent); - } -} - -#[inline(never)] -pub(crate) fn convert_element( - node: SvgNode, - state: &State, - cache: &mut Cache, - parent: &mut Node, -) -> Option { - let tag_name = node.tag_name()?; - - if !tag_name.is_graphic() && !matches!(tag_name, EId::G | EId::Switch | EId::Svg) { - return None; - } - - if !node.is_visible_element(state.opt) { - return None; - } - - if tag_name == EId::Use { - crate::use_node::convert(node, state, cache, parent); - return None; - } - - if tag_name == EId::Switch { - crate::switch::convert(node, state, cache, parent); - return None; - } - - let parent = &mut match convert_group(node, state, false, cache, parent) { - GroupKind::Create(g) => g, - GroupKind::Skip => parent.clone(), - GroupKind::Ignore => return None, - }; - - match tag_name { - EId::Rect - | EId::Circle - | EId::Ellipse - | EId::Line - | EId::Polyline - | EId::Polygon - | EId::Path => { - if let Some(path) = crate::shapes::convert(node, state) { - convert_path(node, path, state, cache, parent); - } - } - EId::Image => { - crate::image::convert(node, state, parent); - } - EId::Text => { - crate::text::convert(node, state, cache, parent); - } - EId::Svg => { - if node.parent_element().is_some() { - crate::use_node::convert_svg(node, state, cache, parent); - } else { - // Skip root `svg`. - convert_children(node, state, cache, parent); - } - } - EId::G => { - convert_children(node, state, cache, parent); - } - _ => {} - } - - Some(parent.clone()) -} - -// `clipPath` can have only shape and `text` children. -// -// `line` doesn't impact rendering because stroke is always disabled -// for `clipPath` children. -#[inline(never)] -pub(crate) fn convert_clip_path_elements( - clip_node: SvgNode, - state: &State, - cache: &mut Cache, - parent: &mut Node, -) { - for node in clip_node.children() { - let tag_name = match node.tag_name() { - Some(v) => v, - None => continue, - }; - - if !tag_name.is_graphic() { - continue; - } - - if !node.is_visible_element(state.opt) { - continue; - } - - if tag_name == EId::Use { - crate::use_node::convert(node, state, cache, parent); - continue; - } - - let parent = &mut match convert_group(node, state, false, cache, parent) { - GroupKind::Create(g) => g, - GroupKind::Skip => parent.clone(), - GroupKind::Ignore => continue, - }; - - match tag_name { - EId::Rect | EId::Circle | EId::Ellipse | EId::Polyline | EId::Polygon | EId::Path => { - if let Some(path) = crate::shapes::convert(node, state) { - convert_path(node, path, state, cache, parent); - } - } - EId::Text => { - crate::text::convert(node, state, cache, parent); - } - _ => { - log::warn!("'{}' is no a valid 'clip-path' child.", tag_name); - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Debug)] -enum Isolation { - Auto, - Isolate, -} - -impl Default for Isolation { - fn default() -> Self { - Self::Auto - } -} - -impl<'a, 'input: 'a> FromValue<'a, 'input> for Isolation { - fn parse(_: SvgNode, _: AId, value: &str) -> Option { - match value { - "auto" => Some(Isolation::Auto), - "isolate" => Some(Isolation::Isolate), - _ => None, - } - } -} - -#[derive(Debug)] -pub enum GroupKind { - /// Creates a new group. - Create(Node), - /// Skips an existing group, but processes its children. - Skip, - /// Skips an existing group and all its children. - Ignore, -} - -// TODO: explain -pub(crate) fn convert_group( - node: SvgNode, - state: &State, - force: bool, - cache: &mut Cache, - parent: &mut Node, -) -> GroupKind { - // A `clipPath` child cannot have an opacity. - let opacity = if state.parent_clip_path.is_none() { - node.attribute::(AId::Opacity) - .unwrap_or(Opacity::ONE) - } else { - Opacity::ONE - }; - - // TODO: remove macro - macro_rules! resolve_link { - ($aid:expr, $f:expr) => {{ - let mut v = None; - - if let Some(link) = node.attribute::($aid) { - v = $f(link, state, cache); - - // If `$aid` is linked to an invalid element - skip this group completely. - if v.is_none() { - return GroupKind::Ignore; - } - } - - v - }}; - } - - // `mask` and `filter` cannot be set on `clipPath` children. - // But `clip-path` can. - - let clip_path = resolve_link!(AId::ClipPath, crate::clippath::convert); - - let mask = if state.parent_clip_path.is_none() { - resolve_link!(AId::Mask, crate::mask::convert) - } else { - None - }; - - let filters = { - let mut filters = Vec::new(); - if state.parent_clip_path.is_none() { - if node.attribute(AId::Filter) == Some("none") { - // Do nothing. - } else if node.has_attribute(AId::Filter) { - if let Ok(f) = crate::filter::convert(node, state, cache) { - filters = f; - } else { - // A filter that not a link or a filter with a link to a non existing element. - // - // Unlike `clip-path` and `mask`, when a `filter` link is invalid - // then the whole element should be ignored. - // - // This is kinda an undefined behaviour. - // In most cases, Chrome, Firefox and rsvg will ignore such elements, - // but in some cases Chrome allows it. Not sure why. - // Inkscape (0.92) simply ignores such attributes, rendering element as is. - // Batik (1.12) crashes. - // - // Test file: e-filter-051.svg - return GroupKind::Ignore; - } - } - } - - filters - }; - - let transform: Transform = node.attribute(AId::Transform).unwrap_or_default(); - let blend_mode: BlendMode = node.attribute(AId::MixBlendMode).unwrap_or_default(); - let isolation: Isolation = node.attribute(AId::Isolation).unwrap_or_default(); - let isolate = isolation == Isolation::Isolate; - - // TODO: ignore just transform - let is_g_or_use = matches!(node.tag_name(), Some(EId::G) | Some(EId::Use)); - let required = opacity.get().approx_ne_ulps(&1.0, 4) - || clip_path.is_some() - || mask.is_some() - || !filters.is_empty() - || !transform.is_identity() - || blend_mode != BlendMode::Normal - || isolate - || is_g_or_use - || force; - - if required { - // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. - let id = if is_g_or_use && state.parent_markers.is_empty() { - node.element_id().to_string() - } else { - String::new() - }; - #[cfg(feature = "class")] - let class = node.class().to_string(); - - let g = parent.append_kind(NodeKind::Group(Group { - id, - #[cfg(feature = "class")] - class, - transform, - opacity, - blend_mode, - isolate, - clip_path, - mask, - filters, - })); - - GroupKind::Create(g) - } else { - GroupKind::Skip - } -} - -fn remove_empty_groups(tree: &mut Tree) { - fn rm(parent: Node) -> bool { - let mut changed = false; - - let mut curr_node = parent.first_child(); - while let Some(node) = curr_node { - curr_node = node.next_sibling(); - - let is_g = if let NodeKind::Group(ref g) = *node.borrow() { - // Skip empty groups when they do not have a `filter` property. - // The `filter` property can be set on empty groups. For example: - // - // - // - // - // - g.filters.is_empty() - } else { - false - }; - - if is_g && !node.has_children() { - node.detach(); - changed = true; - } else { - if rm(node) { - changed = true; - } - } - } - - changed - } - - while rm(tree.root.clone()) {} -} - -fn convert_path( - node: SvgNode, - path: Rc, - state: &State, - cache: &mut Cache, - parent: &mut Node, -) { - debug_assert!(path.len() >= 2); - if path.len() < 2 { - return; - } - - let has_bbox = path.bounds().width() > 0.0 && path.bounds().height() > 0.0; - let fill = crate::style::resolve_fill(node, has_bbox, state, cache); - let stroke = crate::style::resolve_stroke(node, has_bbox, state, cache); - let mut visibility: Visibility = node.find_attribute(AId::Visibility).unwrap_or_default(); - let rendering_mode: ShapeRendering = node - .find_attribute(AId::ShapeRendering) - .unwrap_or(state.opt.shape_rendering); - - // TODO: handle `markers` before `stroke` - let raw_paint_order: svgtypes::PaintOrder = - node.find_attribute(AId::PaintOrder).unwrap_or_default(); - let paint_order = svg_paint_order_to_usvg(raw_paint_order); - - // If a path doesn't have a fill or a stroke than it's invisible. - // By setting `visibility` to `hidden` we are disabling rendering of this path. - if fill.is_none() && stroke.is_none() { - visibility = Visibility::Hidden; - } - - let mut markers_group = None; - if crate::marker::is_valid(node) && visibility == Visibility::Visible { - let mut g = parent.append_kind(NodeKind::Group(Group::default())); - crate::marker::convert(node, &path, state, cache, &mut g); - markers_group = Some(g); - } - - // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. - let id = if state.parent_markers.is_empty() { - node.element_id().to_string() - } else { - String::new() - }; - #[cfg(feature = "class")] - let class = node.element_id().to_string(); - - parent.append_kind(NodeKind::Path(Path { - id, - #[cfg(feature = "class")] - class, - transform: Default::default(), - visibility, - fill, - stroke, - paint_order, - rendering_mode, - text_bbox: None, - data: path, - })); - - if raw_paint_order.order[2] == svgtypes::PaintOrderKind::Markers { - // Insert markers group after `path`. - if let Some(g) = markers_group { - g.detach(); - parent.append(g); - } - } -} - -pub fn svg_paint_order_to_usvg(order: svgtypes::PaintOrder) -> PaintOrder { - match (order.order[0], order.order[1]) { - (svgtypes::PaintOrderKind::Stroke, _) => PaintOrder::StrokeAndFill, - (svgtypes::PaintOrderKind::Markers, svgtypes::PaintOrderKind::Stroke) => { - PaintOrder::StrokeAndFill - } - _ => PaintOrder::FillAndStroke, - } -} diff --git a/crates/usvg-parser/src/mask.rs b/crates/usvg-parser/src/mask.rs deleted file mode 100644 index f944e0279..000000000 --- a/crates/usvg-parser/src/mask.rs +++ /dev/null @@ -1,84 +0,0 @@ -// 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/. - -use std::rc::Rc; - -use svgtypes::{Length, LengthUnit as Unit}; -use usvg_tree::{Group, Mask, MaskType, Node, NodeKind, NonZeroRect, Units}; - -use crate::svgtree::{AId, EId, SvgNode}; -use crate::{converter, OptionLog}; - -pub(crate) fn convert( - node: SvgNode, - state: &converter::State, - cache: &mut converter::Cache, -) -> Option> { - // A `mask` attribute must reference a `mask` element. - if node.tag_name() != Some(EId::Mask) { - return None; - } - - // Check if this element was already converted. - if let Some(mask) = cache.masks.get(node.element_id()) { - return Some(mask.clone()); - } - - let units = node - .attribute(AId::MaskUnits) - .unwrap_or(Units::ObjectBoundingBox); - - let content_units = node - .attribute(AId::MaskContentUnits) - .unwrap_or(Units::UserSpaceOnUse); - - let rect = NonZeroRect::from_xywh( - node.convert_length(AId::X, units, state, Length::new(-10.0, Unit::Percent)), - node.convert_length(AId::Y, units, state, Length::new(-10.0, Unit::Percent)), - node.convert_length(AId::Width, units, state, Length::new(120.0, Unit::Percent)), - node.convert_length(AId::Height, units, state, Length::new(120.0, Unit::Percent)), - ); - let rect = - rect.log_none(|| log::warn!("Mask '{}' has an invalid size. Skipped.", node.element_id()))?; - - // Resolve linked mask. - let mut mask = None; - if let Some(link) = node.attribute::(AId::Mask) { - mask = convert(link, state, cache); - - // Linked `mask` must be valid. - if mask.is_none() { - return None; - } - } - - let kind = if node.attribute(AId::MaskType) == Some("alpha") { - MaskType::Alpha - } else { - MaskType::Luminance - }; - - let mut mask = Mask { - id: node.element_id().to_string(), - units, - content_units, - rect, - kind, - mask, - root: Node::new(NodeKind::Group(Group::default())), - }; - - converter::convert_children(node, state, cache, &mut mask.root); - - if mask.root.has_children() { - let mask = Rc::new(mask); - cache - .masks - .insert(node.element_id().to_string(), mask.clone()); - Some(mask) - } else { - // A mask without children is invalid. - None - } -} diff --git a/crates/usvg-parser/src/paint_server.rs b/crates/usvg-parser/src/paint_server.rs deleted file mode 100644 index c09411593..000000000 --- a/crates/usvg-parser/src/paint_server.rs +++ /dev/null @@ -1,527 +0,0 @@ -// 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/. - -use std::rc::Rc; -use std::str::FromStr; - -use strict_num::PositiveF32; -use svgtypes::{Length, LengthUnit as Unit}; -use usvg_tree::*; - -use crate::converter::SvgColorExt; -use crate::svgtree::{AId, EId, SvgNode}; -use crate::{converter, OptionLog}; - -pub(crate) enum ServerOrColor { - Server(Paint), - Color { color: Color, opacity: Opacity }, -} - -pub(crate) fn convert( - node: SvgNode, - state: &converter::State, - cache: &mut converter::Cache, -) -> Option { - // Check for existing. - if let Some(paint) = cache.paint.get(node.element_id()) { - return Some(ServerOrColor::Server(paint.clone())); - } - - // Unwrap is safe, because we already checked for is_paint_server(). - let paint = match node.tag_name().unwrap() { - EId::LinearGradient => convert_linear(node, state), - EId::RadialGradient => convert_radial(node, state), - EId::Pattern => convert_pattern(node, state, cache), - _ => unreachable!(), - }; - - if let Some(ServerOrColor::Server(ref paint)) = paint { - cache - .paint - .insert(node.element_id().to_string(), paint.clone()); - } - - paint -} - -#[inline(never)] -fn convert_linear(node: SvgNode, state: &converter::State) -> Option { - let stops = convert_stops(find_gradient_with_stops(node)?); - if stops.len() < 2 { - return stops_to_color(&stops); - } - - let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); - let transform = resolve_attr(node, AId::GradientTransform) - .attribute(AId::GradientTransform) - .unwrap_or_default(); - - let gradient = LinearGradient { - id: node.element_id().to_string(), - x1: resolve_number(node, AId::X1, units, state, Length::zero()), - y1: resolve_number(node, AId::Y1, units, state, Length::zero()), - x2: resolve_number( - node, - AId::X2, - units, - state, - Length::new(100.0, Unit::Percent), - ), - y2: resolve_number(node, AId::Y2, units, state, Length::zero()), - base: BaseGradient { - units, - transform, - spread_method: convert_spread_method(node), - stops, - }, - }; - - Some(ServerOrColor::Server(Paint::LinearGradient(Rc::new( - gradient, - )))) -} - -#[inline(never)] -fn convert_radial(node: SvgNode, state: &converter::State) -> Option { - let stops = convert_stops(find_gradient_with_stops(node)?); - if stops.len() < 2 { - return stops_to_color(&stops); - } - - let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); - let r = resolve_number(node, AId::R, units, state, Length::new(50.0, Unit::Percent)); - - // 'A value of zero will cause the area to be painted as a single color - // using the color and opacity of the last gradient stop.' - // - // https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementRAttribute - if !r.is_valid_length() { - let stop = stops.last().unwrap(); - return Some(ServerOrColor::Color { - color: stop.color, - opacity: stop.opacity, - }); - } - - let spread_method = convert_spread_method(node); - let cx = resolve_number( - node, - AId::Cx, - units, - state, - Length::new(50.0, Unit::Percent), - ); - let cy = resolve_number( - node, - AId::Cy, - units, - state, - Length::new(50.0, Unit::Percent), - ); - let fx = resolve_number(node, AId::Fx, units, state, Length::new_number(cx as f64)); - let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy as f64)); - let transform = resolve_attr(node, AId::GradientTransform) - .attribute(AId::GradientTransform) - .unwrap_or_default(); - - let gradient = RadialGradient { - id: node.element_id().to_string(), - cx, - cy, - r: PositiveF32::new(r).unwrap(), - fx, - fy, - base: BaseGradient { - units, - transform, - spread_method, - stops, - }, - }; - - Some(ServerOrColor::Server(Paint::RadialGradient(Rc::new( - gradient, - )))) -} - -#[inline(never)] -fn convert_pattern( - node: SvgNode, - state: &converter::State, - cache: &mut converter::Cache, -) -> Option { - let node_with_children = find_pattern_with_children(node)?; - - let view_box = { - let n1 = resolve_attr(node, AId::ViewBox); - let n2 = resolve_attr(node, AId::PreserveAspectRatio); - n1.parse_viewbox().map(|vb| ViewBox { - rect: vb, - aspect: n2.attribute(AId::PreserveAspectRatio).unwrap_or_default(), - }) - }; - - let units = convert_units(node, AId::PatternUnits, Units::ObjectBoundingBox); - let content_units = convert_units(node, AId::PatternContentUnits, Units::UserSpaceOnUse); - - let transform = resolve_attr(node, AId::PatternTransform) - .attribute(AId::PatternTransform) - .unwrap_or_default(); - - let rect = NonZeroRect::from_xywh( - resolve_number(node, AId::X, units, state, Length::zero()), - resolve_number(node, AId::Y, units, state, Length::zero()), - resolve_number(node, AId::Width, units, state, Length::zero()), - resolve_number(node, AId::Height, units, state, Length::zero()), - ); - let rect = rect.log_none(|| { - log::warn!( - "Pattern '{}' has an invalid size. Skipped.", - node.element_id() - ) - })?; - - let mut patt = Pattern { - id: node.element_id().to_string(), - units, - content_units, - transform, - rect, - view_box, - root: Node::new(NodeKind::Group(Group::default())), - }; - - converter::convert_children(node_with_children, state, cache, &mut patt.root); - - if !patt.root.has_children() { - return None; - } - - Some(ServerOrColor::Server(Paint::Pattern(Rc::new(patt)))) -} - -fn convert_spread_method(node: SvgNode) -> SpreadMethod { - let node = resolve_attr(node, AId::SpreadMethod); - node.attribute(AId::SpreadMethod).unwrap_or_default() -} - -pub(crate) fn convert_units(node: SvgNode, name: AId, def: Units) -> Units { - let node = resolve_attr(node, name); - node.attribute(name).unwrap_or(def) -} - -fn find_gradient_with_stops<'a, 'input: 'a>( - node: SvgNode<'a, 'input>, -) -> Option> { - for link in node.href_iter() { - if !link.tag_name().unwrap().is_gradient() { - log::warn!( - "Gradient '{}' cannot reference '{}' via 'xlink:href'.", - node.element_id(), - link.tag_name().unwrap() - ); - return None; - } - - if link.children().any(|n| n.tag_name() == Some(EId::Stop)) { - return Some(link); - } - } - - None -} - -fn find_pattern_with_children<'a, 'input: 'a>( - node: SvgNode<'a, 'input>, -) -> Option> { - for link in node.href_iter() { - if link.tag_name() != Some(EId::Pattern) { - log::warn!( - "Pattern '{}' cannot reference '{}' via 'xlink:href'.", - node.element_id(), - link.tag_name().unwrap() - ); - return None; - } - - if link.has_children() { - return Some(link); - } - } - - None -} - -fn convert_stops(grad: SvgNode) -> Vec { - let mut stops = Vec::new(); - - { - let mut prev_offset = Length::zero(); - for stop in grad.children() { - if stop.tag_name() != Some(EId::Stop) { - log::warn!("Invalid gradient child: '{:?}'.", stop.tag_name().unwrap()); - continue; - } - - // `number` can be either a number or a percentage. - let offset = stop.attribute(AId::Offset).unwrap_or(prev_offset); - let offset = match offset.unit { - Unit::None => offset.number, - Unit::Percent => offset.number / 100.0, - _ => prev_offset.number, - }; - prev_offset = Length::new_number(offset); - let offset = crate::f32_bound(0.0, offset as f32, 1.0); - - let (color, opacity) = match stop.attribute(AId::StopColor) { - Some("currentColor") => stop - .find_attribute(AId::Color) - .unwrap_or_else(svgtypes::Color::black), - Some(value) => { - if let Ok(c) = svgtypes::Color::from_str(value) { - c - } else { - log::warn!("Failed to parse stop-color value: '{}'.", value); - svgtypes::Color::black() - } - } - _ => svgtypes::Color::black(), - } - .split_alpha(); - - let stop_opacity = stop - .attribute::(AId::StopOpacity) - .unwrap_or(Opacity::ONE); - stops.push(Stop { - offset: StopOffset::new_clamped(offset), - color, - opacity: opacity * stop_opacity, - }); - } - } - - // Remove stops with equal offset. - // - // Example: - // offset="0.5" - // offset="0.7" - // offset="0.7" <-- this one should be removed - // offset="0.7" - // offset="0.9" - if stops.len() >= 3 { - let mut i = 0; - while i < stops.len() - 2 { - let offset1 = stops[i + 0].offset.get(); - let offset2 = stops[i + 1].offset.get(); - let offset3 = stops[i + 2].offset.get(); - - if offset1.approx_eq_ulps(&offset2, 4) && offset2.approx_eq_ulps(&offset3, 4) { - // Remove offset in the middle. - stops.remove(i + 1); - } else { - i += 1; - } - } - } - - // Remove zeros. - // - // From: - // offset="0.0" - // offset="0.0" - // offset="0.7" - // - // To: - // offset="0.0" - // offset="0.00000001" - // offset="0.7" - if stops.len() >= 2 { - let mut i = 0; - while i < stops.len() - 1 { - let offset1 = stops[i + 0].offset.get(); - let offset2 = stops[i + 1].offset.get(); - - if offset1.approx_eq_ulps(&0.0, 4) && offset2.approx_eq_ulps(&0.0, 4) { - stops[i + 1].offset = StopOffset::new_clamped(offset1 + f32::EPSILON); - } - - i += 1; - } - } - - // Shift equal offsets. - // - // From: - // offset="0.5" - // offset="0.7" - // offset="0.7" - // - // To: - // offset="0.5" - // offset="0.699999999" - // offset="0.7" - { - let mut i = 1; - while i < stops.len() { - let offset1 = stops[i - 1].offset.get(); - let offset2 = stops[i - 0].offset.get(); - - // Next offset must be smaller then previous. - if offset1 > offset2 || offset1.approx_eq_ulps(&offset2, 4) { - // Make previous offset a bit smaller. - let new_offset = offset1 - f32::EPSILON; - stops[i - 1].offset = StopOffset::new_clamped(new_offset); - stops[i - 0].offset = StopOffset::new_clamped(offset1); - } - - i += 1; - } - } - - stops -} - -#[inline(never)] -pub(crate) fn resolve_number( - node: SvgNode, - name: AId, - units: Units, - state: &converter::State, - def: Length, -) -> f32 { - resolve_attr(node, name).convert_length(name, units, state, def) -} - -fn resolve_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { - if node.has_attribute(name) { - return node; - } - - match node.tag_name().unwrap() { - EId::LinearGradient => resolve_lg_attr(node, name), - EId::RadialGradient => resolve_rg_attr(node, name), - EId::Pattern => resolve_pattern_attr(node, name), - EId::Filter => resolve_filter_attr(node, name), - _ => node, - } -} - -fn resolve_lg_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { - for link in node.href_iter() { - let tag_name = match link.tag_name() { - Some(v) => v, - None => return node, - }; - - match (name, tag_name) { - // Coordinates can be resolved only from - // ref element with the same type. - (AId::X1, EId::LinearGradient) - | (AId::Y1, EId::LinearGradient) - | (AId::X2, EId::LinearGradient) - | (AId::Y2, EId::LinearGradient) - // Other attributes can be resolved - // from any kind of gradient. - | (AId::GradientUnits, EId::LinearGradient) - | (AId::GradientUnits, EId::RadialGradient) - | (AId::SpreadMethod, EId::LinearGradient) - | (AId::SpreadMethod, EId::RadialGradient) - | (AId::GradientTransform, EId::LinearGradient) - | (AId::GradientTransform, EId::RadialGradient) => { - if link.has_attribute(name) { - return link; - } - } - _ => break, - } - } - - node -} - -fn resolve_rg_attr<'a, 'input>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { - for link in node.href_iter() { - let tag_name = match link.tag_name() { - Some(v) => v, - None => return node, - }; - - match (name, tag_name) { - // Coordinates can be resolved only from - // ref element with the same type. - (AId::Cx, EId::RadialGradient) - | (AId::Cy, EId::RadialGradient) - | (AId::R, EId::RadialGradient) - | (AId::Fx, EId::RadialGradient) - | (AId::Fy, EId::RadialGradient) - // Other attributes can be resolved - // from any kind of gradient. - | (AId::GradientUnits, EId::LinearGradient) - | (AId::GradientUnits, EId::RadialGradient) - | (AId::SpreadMethod, EId::LinearGradient) - | (AId::SpreadMethod, EId::RadialGradient) - | (AId::GradientTransform, EId::LinearGradient) - | (AId::GradientTransform, EId::RadialGradient) => { - if link.has_attribute(name) { - return link; - } - } - _ => break, - } - } - - node -} - -fn resolve_pattern_attr<'a, 'input: 'a>( - node: SvgNode<'a, 'input>, - name: AId, -) -> SvgNode<'a, 'input> { - for link in node.href_iter() { - let tag_name = match link.tag_name() { - Some(v) => v, - None => return node, - }; - - if tag_name != EId::Pattern { - break; - } - - if link.has_attribute(name) { - return link; - } - } - - node -} - -fn resolve_filter_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, aid: AId) -> SvgNode<'a, 'input> { - for link in node.href_iter() { - let tag_name = match link.tag_name() { - Some(v) => v, - None => return node, - }; - - if tag_name != EId::Filter { - break; - } - - if link.has_attribute(aid) { - return link; - } - } - - node -} - -fn stops_to_color(stops: &[Stop]) -> Option { - if stops.is_empty() { - None - } else { - Some(ServerOrColor::Color { - color: stops[0].color, - opacity: stops[0].opacity, - }) - } -} diff --git a/crates/usvg-parser/tests/test.rs b/crates/usvg-parser/tests/test.rs deleted file mode 100644 index f0fce0a5b..000000000 --- a/crates/usvg-parser/tests/test.rs +++ /dev/null @@ -1,88 +0,0 @@ -use usvg_parser::TreeParsing; - -#[test] -fn clippath_with_invalid_child() { - let svg = " - - - - - - - "; - - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - // clipPath is invalid and should be removed together with rect. - assert_eq!(tree.root.has_children(), false); -} - -#[test] -fn simplify_paths() { - let svg = " - - - - "; - - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - let path = tree.root.first_child().unwrap(); - match *path.borrow() { - usvg_tree::NodeKind::Path(ref path) => { - // Make sure we have MLZ and not MLZZZ - assert_eq!(path.data.verbs().len(), 3); - } - _ => unreachable!(), - }; -} - -#[test] -fn size_detection_1() { - let svg = ""; - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - assert_eq!(tree.size, usvg_tree::Size::from_wh(10.0, 20.0).unwrap()); -} - -#[test] -fn size_detection_2() { - let svg = - ""; - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - assert_eq!(tree.size, usvg_tree::Size::from_wh(30.0, 40.0).unwrap()); -} - -#[test] -fn size_detection_3() { - let svg = - ""; - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - assert_eq!(tree.size, usvg_tree::Size::from_wh(5.0, 20.0).unwrap()); -} - -#[test] -fn size_detection_4() { - let svg = " - - - - "; - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - assert_eq!(tree.size, usvg_tree::Size::from_wh(36.0, 36.0).unwrap()); - assert_eq!( - tree.view_box.rect, - usvg_tree::NonZeroRect::from_xywh(0.0, 0.0, 36.0, 36.0).unwrap() - ); -} - -#[test] -fn size_detection_5() { - let svg = ""; - let tree = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()).unwrap(); - assert_eq!(tree.size, usvg_tree::Size::from_wh(100.0, 100.0).unwrap()); -} - -#[test] -fn invalid_size_1() { - let svg = ""; - let result = usvg_tree::Tree::from_str(&svg, &usvg_parser::Options::default()); - assert!(result.is_err()); -} diff --git a/crates/usvg-text-layout/Cargo.toml b/crates/usvg-text-layout/Cargo.toml deleted file mode 100644 index 4452739ea..000000000 --- a/crates/usvg-text-layout/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "usvg-text-layout" -version = "0.36.0+class" -authors = ["Yevhenii Reizner "] -keywords = ["svg"] -license = "MPL-2.0" -edition = "2018" -description = "An SVG text layout implementation." -categories = ["multimedia::images"] -repository = "https://github.com/RazrFalcon/resvg" -documentation = "https://docs.rs/usvg-text-layout/" -workspace = "../.." - -[dependencies] -fontdb = { version = "0.15", default-features = false } -kurbo = "0.9" # Bezier curves utils for text-on-path -log = "0.4" -rustybuzz = "0.10" -unicode-bidi = "0.3" -unicode-script = "0.5" -unicode-vo = "0.1" -usvg-tree = { path = "../usvg-tree", version = "0.36.0+class" } - -[features] -default = ["system-fonts", "memmap-fonts"] -# Enables system fonts loading. -system-fonts = ["fontdb/fs", "fontdb/fontconfig"] -# Enables font files memmaping for faster loading. -memmap-fonts = ["fontdb/memmap"] -class = ["usvg-tree/class"] \ No newline at end of file diff --git a/crates/usvg-text-layout/LICENSE.txt b/crates/usvg-text-layout/LICENSE.txt deleted file mode 100644 index 14e2f777f..000000000 --- a/crates/usvg-text-layout/LICENSE.txt +++ /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. diff --git a/crates/usvg-tree/Cargo.toml b/crates/usvg-tree/Cargo.toml deleted file mode 100644 index 3d05102c6..000000000 --- a/crates/usvg-tree/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "usvg-tree" -version = "0.36.0+class" -authors = ["Yevhenii Reizner "] -keywords = ["svg"] -license = "MPL-2.0" -edition = "2018" -description = "An SVG tree representation used by usvg." -categories = ["multimedia::images"] -repository = "https://github.com/RazrFalcon/resvg" -documentation = "https://docs.rs/usvg-tree/" -readme = "README.md" -workspace = "../.." - -[dependencies] -rctree = "0.5" -strict-num = "0.1.1" -svgtypes = "0.12" -tiny-skia-path = "0.11.2" - -[features] -class = [] diff --git a/crates/usvg-tree/LICENSE.txt b/crates/usvg-tree/LICENSE.txt deleted file mode 100644 index 14e2f777f..000000000 --- a/crates/usvg-tree/LICENSE.txt +++ /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. diff --git a/crates/usvg-tree/README.md b/crates/usvg-tree/README.md deleted file mode 100644 index 7e6cee4b5..000000000 --- a/crates/usvg-tree/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# usvg-tree -[![Crates.io](https://img.shields.io/crates/v/usvg-tree.svg)](https://crates.io/crates/usvg-tree) -[![Documentation](https://docs.rs/usvg/badge.svg)](https://docs.rs/usvg-tree) -[![Rust 1.65+](https://img.shields.io/badge/rust-1.65+-orange.svg)](https://www.rust-lang.org) - -`usvg-tree` is an [SVG] tree representation used by [usvg]. - -## License - -*usvg-tree* is licensed under the [MPLv2.0](https://www.mozilla.org/en-US/MPL/). - -[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -[usvg]: https://github.com/RazrFalcon/resvg/tree/master/crates/usvg diff --git a/crates/usvg-tree/src/lib.rs b/crates/usvg-tree/src/lib.rs deleted file mode 100644 index 418c886ee..000000000 --- a/crates/usvg-tree/src/lib.rs +++ /dev/null @@ -1,1359 +0,0 @@ -// 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/. - -/*! -`usvg-tree` is an [SVG] tree representation used by [usvg]. - -[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -[usvg]: https://github.com/RazrFalcon/resvg/tree/master/crates/usvg -*/ - -#![forbid(unsafe_code)] -#![warn(missing_docs)] -#![warn(missing_debug_implementations)] -#![warn(missing_copy_implementations)] -#![allow(clippy::collapsible_else_if)] -#![allow(clippy::neg_cmp_op_on_partial_ord)] -#![allow(clippy::too_many_arguments)] -#![allow(clippy::derivable_impls)] - -pub mod filter; -mod geom; -mod text; - -use std::rc::Rc; -use std::sync::Arc; - -pub use strict_num::{self, ApproxEqUlps, NonZeroPositiveF32, NormalizedF32, PositiveF32}; -pub use svgtypes::{Align, AspectRatio}; - -pub use tiny_skia_path; - -pub use crate::geom::*; -pub use crate::text::*; - -/// An alias to `NormalizedF32`. -pub type Opacity = NormalizedF32; - -/// A non-zero `f32`. -/// -/// Just like `f32` but immutable and guarantee to never be zero. -#[derive(Clone, Copy, Debug)] -pub struct NonZeroF32(f32); - -impl NonZeroF32 { - /// Creates a new `NonZeroF32` value. - #[inline] - pub fn new(n: f32) -> Option { - if n.approx_eq_ulps(&0.0, 4) { - None - } else { - Some(NonZeroF32(n)) - } - } - - /// Returns an underlying value. - #[inline] - pub fn get(&self) -> f32 { - self.0 - } -} - -/// An element units. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Units { - UserSpaceOnUse, - ObjectBoundingBox, -} - -// `Units` cannot have a default value, because it changes depending on an element. - -/// A visibility property. -/// -/// `visibility` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Visibility { - Visible, - Hidden, - Collapse, -} - -impl Default for Visibility { - fn default() -> Self { - Self::Visible - } -} - -/// A shape rendering method. -/// -/// `shape-rendering` attribute in the SVG. -#[derive(Clone, Copy, PartialEq, Debug)] -#[allow(missing_docs)] -pub enum ShapeRendering { - OptimizeSpeed, - CrispEdges, - GeometricPrecision, -} - -impl ShapeRendering { - /// Checks if anti-aliasing should be enabled. - pub fn use_shape_antialiasing(self) -> bool { - match self { - ShapeRendering::OptimizeSpeed => false, - ShapeRendering::CrispEdges => false, - ShapeRendering::GeometricPrecision => true, - } - } -} - -impl Default for ShapeRendering { - fn default() -> Self { - Self::GeometricPrecision - } -} - -// TODO: remove? -impl std::str::FromStr for ShapeRendering { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "optimizeSpeed" => Ok(ShapeRendering::OptimizeSpeed), - "crispEdges" => Ok(ShapeRendering::CrispEdges), - "geometricPrecision" => Ok(ShapeRendering::GeometricPrecision), - _ => Err("invalid"), - } - } -} - -/// A text rendering method. -/// -/// `text-rendering` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum TextRendering { - OptimizeSpeed, - OptimizeLegibility, - GeometricPrecision, -} - -impl Default for TextRendering { - fn default() -> Self { - Self::OptimizeLegibility - } -} - -impl std::str::FromStr for TextRendering { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "optimizeSpeed" => Ok(TextRendering::OptimizeSpeed), - "optimizeLegibility" => Ok(TextRendering::OptimizeLegibility), - "geometricPrecision" => Ok(TextRendering::GeometricPrecision), - _ => Err("invalid"), - } - } -} - -/// An image rendering method. -/// -/// `image-rendering` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum ImageRendering { - OptimizeQuality, - OptimizeSpeed, -} - -impl Default for ImageRendering { - fn default() -> Self { - Self::OptimizeQuality - } -} - -impl std::str::FromStr for ImageRendering { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "optimizeQuality" => Ok(ImageRendering::OptimizeQuality), - "optimizeSpeed" => Ok(ImageRendering::OptimizeSpeed), - _ => Err("invalid"), - } - } -} - -/// A blending mode property. -/// -/// `mix-blend-mode` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum BlendMode { - Normal, - Multiply, - Screen, - Overlay, - Darken, - Lighten, - ColorDodge, - ColorBurn, - HardLight, - SoftLight, - Difference, - Exclusion, - Hue, - Saturation, - Color, - Luminosity, -} - -impl Default for BlendMode { - fn default() -> Self { - Self::Normal - } -} - -/// A spread method. -/// -/// `spreadMethod` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum SpreadMethod { - Pad, - Reflect, - Repeat, -} - -impl Default for SpreadMethod { - fn default() -> Self { - Self::Pad - } -} - -/// A generic gradient. -#[derive(Clone, Debug)] -pub struct BaseGradient { - /// Coordinate system units. - /// - /// `gradientUnits` in SVG. - pub units: Units, - - /// Gradient transform. - /// - /// `gradientTransform` in SVG. - pub transform: Transform, - - /// Gradient spreading method. - /// - /// `spreadMethod` in SVG. - pub spread_method: SpreadMethod, - - /// A list of `stop` elements. - pub stops: Vec, -} - -/// A linear gradient. -/// -/// `linearGradient` element in SVG. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub struct LinearGradient { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Can't be empty. - pub id: String, - - pub x1: f32, - pub y1: f32, - pub x2: f32, - pub y2: f32, - - /// Base gradient data. - pub base: BaseGradient, -} - -impl std::ops::Deref for LinearGradient { - type Target = BaseGradient; - - fn deref(&self) -> &Self::Target { - &self.base - } -} - -/// A radial gradient. -/// -/// `radialGradient` element in SVG. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub struct RadialGradient { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Can't be empty. - pub id: String, - - pub cx: f32, - pub cy: f32, - pub r: PositiveF32, - pub fx: f32, - pub fy: f32, - - /// Base gradient data. - pub base: BaseGradient, -} - -impl std::ops::Deref for RadialGradient { - type Target = BaseGradient; - - fn deref(&self) -> &Self::Target { - &self.base - } -} - -/// An alias to `NormalizedF32`. -pub type StopOffset = NormalizedF32; - -/// Gradient's stop element. -/// -/// `stop` element in SVG. -#[derive(Clone, Copy, Debug)] -pub struct Stop { - /// Gradient stop offset. - /// - /// `offset` in SVG. - pub offset: StopOffset, - - /// Gradient stop color. - /// - /// `stop-color` in SVG. - pub color: Color, - - /// Gradient stop opacity. - /// - /// `stop-opacity` in SVG. - pub opacity: Opacity, -} - -/// A pattern element. -/// -/// `pattern` element in SVG. -#[derive(Clone, Debug)] -pub struct Pattern { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Can't be empty. - pub id: String, - - /// Coordinate system units. - /// - /// `patternUnits` in SVG. - pub units: Units, - - // TODO: should not be accessible when `viewBox` is present. - /// Content coordinate system units. - /// - /// `patternContentUnits` in SVG. - pub content_units: Units, - - /// Pattern transform. - /// - /// `patternTransform` in SVG. - pub transform: Transform, - - /// Pattern rectangle. - /// - /// `x`, `y`, `width` and `height` in SVG. - pub rect: NonZeroRect, - - /// Pattern viewbox. - pub view_box: Option, - - /// Pattern children. - /// - /// The root node is always `Group`. - pub root: Node, -} - -/// An alias to `NonZeroPositiveF32`. -pub type StrokeWidth = NonZeroPositiveF32; - -/// A `stroke-miterlimit` value. -/// -/// Just like `f32` but immutable and guarantee to be >=1.0. -#[derive(Clone, Copy, Debug)] -pub struct StrokeMiterlimit(f32); - -impl StrokeMiterlimit { - /// Creates a new `StrokeMiterlimit` value. - #[inline] - pub fn new(n: f32) -> Self { - debug_assert!(n.is_finite()); - debug_assert!(n >= 1.0); - - let n = if !(n >= 1.0) { 1.0 } else { n }; - - StrokeMiterlimit(n) - } - - /// Returns an underlying value. - #[inline] - pub fn get(&self) -> f32 { - self.0 - } -} - -impl Default for StrokeMiterlimit { - #[inline] - fn default() -> Self { - StrokeMiterlimit::new(4.0) - } -} - -impl From for StrokeMiterlimit { - #[inline] - fn from(n: f32) -> Self { - Self::new(n) - } -} - -impl PartialEq for StrokeMiterlimit { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.0.approx_eq_ulps(&other.0, 4) - } -} - -/// A line cap. -/// -/// `stroke-linecap` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum LineCap { - Butt, - Round, - Square, -} - -impl Default for LineCap { - fn default() -> Self { - Self::Butt - } -} - -/// A line join. -/// -/// `stroke-linejoin` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum LineJoin { - Miter, - MiterClip, - Round, - Bevel, -} - -impl Default for LineJoin { - fn default() -> Self { - Self::Miter - } -} - -/// A stroke style. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub struct Stroke { - pub paint: Paint, - pub dasharray: Option>, - pub dashoffset: f32, - pub miterlimit: StrokeMiterlimit, - pub opacity: Opacity, - pub width: StrokeWidth, - pub linecap: LineCap, - pub linejoin: LineJoin, -} - -impl Default for Stroke { - fn default() -> Self { - Stroke { - // The actual default color is `none`, - // but to simplify the `Stroke` object creation we use `black`. - paint: Paint::Color(Color::black()), - dasharray: None, - dashoffset: 0.0, - miterlimit: StrokeMiterlimit::default(), - opacity: Opacity::ONE, - width: StrokeWidth::new(1.0).unwrap(), - linecap: LineCap::default(), - linejoin: LineJoin::default(), - } - } -} - -/// A fill rule. -/// -/// `fill-rule` attribute in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum FillRule { - NonZero, - EvenOdd, -} - -impl Default for FillRule { - fn default() -> Self { - Self::NonZero - } -} - -/// A fill style. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub struct Fill { - pub paint: Paint, - pub opacity: Opacity, - pub rule: FillRule, -} - -impl Fill { - /// Creates a `Fill` from `Paint`. - /// - /// `opacity` and `rule` will be set to default values. - pub fn from_paint(paint: Paint) -> Self { - Fill { - paint, - ..Fill::default() - } - } -} - -impl Default for Fill { - fn default() -> Self { - Fill { - paint: Paint::Color(Color::black()), - opacity: Opacity::ONE, - rule: FillRule::default(), - } - } -} - -/// A 8-bit RGB color. -#[derive(Clone, Copy, PartialEq, Debug)] -#[allow(missing_docs)] -pub struct Color { - pub red: u8, - pub green: u8, - pub blue: u8, -} - -impl Color { - /// Constructs a new `Color` from RGB values. - #[inline] - pub fn new_rgb(red: u8, green: u8, blue: u8) -> Color { - Color { red, green, blue } - } - - /// Constructs a new `Color` set to black. - #[inline] - pub fn black() -> Color { - Color::new_rgb(0, 0, 0) - } - - /// Constructs a new `Color` set to white. - #[inline] - pub fn white() -> Color { - Color::new_rgb(255, 255, 255) - } -} - -/// A paint style. -/// -/// `paint` value type in the SVG. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub enum Paint { - Color(Color), - LinearGradient(Rc), - RadialGradient(Rc), - Pattern(Rc), -} - -impl Paint { - /// Returns paint server units. - /// - /// Returns `None` for `Color`. - #[inline] - pub fn units(&self) -> Option { - match self { - Self::Color(_) => None, - Self::LinearGradient(ref lg) => Some(lg.units), - Self::RadialGradient(ref rg) => Some(rg.units), - Self::Pattern(ref patt) => Some(patt.units), - } - } -} - -impl PartialEq for Paint { - #[inline] - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Color(lc), Self::Color(rc)) => lc == rc, - (Self::LinearGradient(ref lg1), Self::LinearGradient(ref lg2)) => Rc::ptr_eq(lg1, lg2), - (Self::RadialGradient(ref rg1), Self::RadialGradient(ref rg2)) => Rc::ptr_eq(rg1, rg2), - (Self::Pattern(ref p1), Self::Pattern(ref p2)) => Rc::ptr_eq(p1, p2), - _ => false, - } - } -} - -/// A clip-path element. -/// -/// `clipPath` element in SVG. -#[derive(Clone, Debug)] -pub struct ClipPath { - /// Element's ID. - /// - /// Taken from the SVG itself or generated by the parser. - /// Used only during SVG writing. `resvg` doesn't rely on this property. - pub id: String, - - /// Coordinate system units. - /// - /// `clipPathUnits` in SVG. - pub units: Units, - - /// Clip path transform. - /// - /// `transform` in SVG. - pub transform: Transform, - - /// Additional clip path. - /// - /// `clip-path` in SVG. - pub clip_path: Option>, - - /// Clip path children. - /// - /// The root node is always `Group`. - pub root: Node, -} - -impl Default for ClipPath { - fn default() -> Self { - ClipPath { - id: String::new(), - units: Units::UserSpaceOnUse, - transform: Transform::default(), - clip_path: None, - root: Node::new(NodeKind::Group(Group::default())), - } - } -} - -/// A mask type. -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum MaskType { - /// Indicates that the luminance values of the mask should be used. - Luminance, - /// Indicates that the alpha values of the mask should be used. - Alpha, -} - -impl Default for MaskType { - fn default() -> Self { - Self::Luminance - } -} - -/// A mask element. -/// -/// `mask` element in SVG. -#[derive(Clone, Debug)] -pub struct Mask { - /// Element's ID. - /// - /// Taken from the SVG itself or generated by the parser. - /// Used only during SVG writing. `resvg` doesn't rely on this property. - pub id: String, - - /// Coordinate system units. - /// - /// `maskUnits` in SVG. - pub units: Units, - - /// Content coordinate system units. - /// - /// `maskContentUnits` in SVG. - pub content_units: Units, - - /// Mask rectangle. - /// - /// `x`, `y`, `width` and `height` in SVG. - pub rect: NonZeroRect, - - /// Mask type. - /// - /// `mask-type` in SVG. - pub kind: MaskType, - - /// Additional mask. - /// - /// `mask` in SVG. - pub mask: Option>, - - /// Clip path children. - /// - /// The root node is always `Group`. - pub root: Node, -} - -/// Node's kind. -#[allow(missing_docs)] -#[derive(Clone, Debug)] -pub enum NodeKind { - Group(Group), - Path(Path), - Image(Image), - Text(Text), -} - -impl NodeKind { - /// Returns node's ID. - pub fn id(&self) -> &str { - match self { - NodeKind::Group(ref e) => e.id.as_str(), - NodeKind::Path(ref e) => e.id.as_str(), - NodeKind::Image(ref e) => e.id.as_str(), - NodeKind::Text(ref e) => e.id.as_str(), - } - } - - /// Returns note's class names. - #[cfg(feature = "class")] - pub fn class(&self) -> &str { - match self { - NodeKind::Group(ref e) => e.class.as_str(), - NodeKind::Path(ref e) => e.class.as_str(), - NodeKind::Image(ref e) => e.class.as_str(), - NodeKind::Text(ref e) => e.class.as_str(), - } - } - - /// Returns node's transform. - pub fn transform(&self) -> Transform { - match self { - NodeKind::Group(ref e) => e.transform, - NodeKind::Path(ref e) => e.transform, - NodeKind::Image(ref e) => e.transform, - NodeKind::Text(ref e) => e.transform, - } - } -} - -/// A group container. -/// -/// The preprocessor will remove all groups that don't impact rendering. -/// Those that left is just an indicator that a new canvas should be created. -/// -/// `g` element in SVG. -#[derive(Clone, Debug)] -pub struct Group { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Isn't automatically generated. - /// Can be empty. - pub id: String, - - /// Element's class names. - #[cfg(feature = "class")] - pub class: String, - - /// Element transform. - pub transform: Transform, - - /// Group opacity. - /// - /// After the group is rendered we should combine - /// it with a parent group using the specified opacity. - pub opacity: Opacity, - - /// Group blend mode. - /// - /// `mix-blend-mode` in SVG. - pub blend_mode: BlendMode, - - /// Group isolation. - /// - /// `isolation` in SVG. - pub isolate: bool, - - /// Element's clip path. - pub clip_path: Option>, - - /// Element's mask. - pub mask: Option>, - - /// Element's filters. - pub filters: Vec>, -} - -impl Default for Group { - fn default() -> Self { - Group { - id: String::new(), - #[cfg(feature = "class")] - class: String::new(), - transform: Transform::default(), - opacity: Opacity::ONE, - blend_mode: BlendMode::Normal, - isolate: false, - clip_path: None, - mask: None, - filters: Vec::new(), - } - } -} - -impl Group { - /// Checks if this group should be isolated during rendering. - pub fn should_isolate(&self) -> bool { - self.isolate - || self.opacity != Opacity::ONE - || self.clip_path.is_some() - || self.mask.is_some() - || !self.filters.is_empty() - || self.blend_mode != BlendMode::Normal // TODO: probably not needed? - } -} - -/// Representation of the [`paint-order`] property. -/// -/// `usvg` will handle `markers` automatically, -/// therefore we provide only `fill` and `stroke` variants. -/// -/// [`paint-order`]: https://www.w3.org/TR/SVG2/painting.html#PaintOrder -#[derive(Clone, Copy, PartialEq, Debug)] -#[allow(missing_docs)] -pub enum PaintOrder { - FillAndStroke, - StrokeAndFill, -} - -impl Default for PaintOrder { - fn default() -> Self { - Self::FillAndStroke - } -} - -/// A path element. -#[derive(Clone, Debug)] -pub struct Path { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Isn't automatically generated. - /// Can be empty. - pub id: String, - - /// Element's class names. - #[cfg(feature = "class")] - pub class: String, - - /// Element transform. - pub transform: Transform, - - /// Element visibility. - pub visibility: Visibility, - - /// Fill style. - pub fill: Option, - - /// Stroke style. - pub stroke: Option, - - /// Fill and stroke paint order. - /// - /// Since markers will be replaced with regular nodes automatically, - /// `usvg` doesn't provide the `markers` order type. It's was already done. - /// - /// `paint-order` in SVG. - pub paint_order: PaintOrder, - - /// Rendering mode. - /// - /// `shape-rendering` in SVG. - pub rendering_mode: ShapeRendering, - - /// Contains a text bbox. - /// - /// Text bbox is different from path bbox. The later one contains a tight path bbox, - /// while the text bbox is based on the actual font metrics and usually larger than tight bbox. - /// - /// Also, path bbox doesn't include leading and trailing whitespaces, - /// because there is nothing to include. But text bbox does. - /// - /// As the name suggests, this property will be set only for paths - /// that were converted from text. - pub text_bbox: Option, - - /// Segments list. - /// - /// All segments are in absolute coordinates. - pub data: Rc, -} - -impl Path { - /// Creates a new `Path` with default values. - pub fn new(data: Rc) -> Self { - Path { - id: String::new(), - #[cfg(feature = "class")] - class: String::new(), - transform: Transform::default(), - visibility: Visibility::Visible, - fill: None, - stroke: None, - paint_order: PaintOrder::default(), - rendering_mode: ShapeRendering::default(), - text_bbox: None, - data, - } - } -} - -/// An embedded image kind. -#[derive(Clone)] -pub enum ImageKind { - /// A reference to raw JPEG data. Should be decoded by the caller. - JPEG(Arc>), - /// A reference to raw PNG data. Should be decoded by the caller. - PNG(Arc>), - /// A reference to raw GIF data. Should be decoded by the caller. - GIF(Arc>), - /// A preprocessed SVG tree. Can be rendered as is. - SVG(Tree), -} - -impl std::fmt::Debug for ImageKind { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - ImageKind::JPEG(_) => f.write_str("ImageKind::JPEG(..)"), - ImageKind::PNG(_) => f.write_str("ImageKind::PNG(..)"), - ImageKind::GIF(_) => f.write_str("ImageKind::GIF(..)"), - ImageKind::SVG(_) => f.write_str("ImageKind::SVG(..)"), - } - } -} - -/// A raster image element. -/// -/// `image` element in SVG. -#[derive(Clone, Debug)] -pub struct Image { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Isn't automatically generated. - /// Can be empty. - pub id: String, - - /// Element's class names. - #[cfg(feature = "class")] - pub class: String, - - /// Element transform. - pub transform: Transform, - - /// Element visibility. - pub visibility: Visibility, - - /// An image rectangle in which it should be fit. - /// - /// Combination of the `x`, `y`, `width`, `height` and `preserveAspectRatio` - /// attributes. - pub view_box: ViewBox, - - /// Rendering mode. - /// - /// `image-rendering` in SVG. - pub rendering_mode: ImageRendering, - - /// Image data. - pub kind: ImageKind, -} - -/// Alias for `rctree::Node`. -pub type Node = rctree::Node; - -// TODO: impl a Debug -/// A nodes tree container. -#[allow(missing_debug_implementations)] -#[derive(Clone)] -pub struct Tree { - /// Image size. - /// - /// Size of an image that should be created to fit the SVG. - /// - /// `width` and `height` in SVG. - pub size: Size, - - /// SVG viewbox. - /// - /// Specifies which part of the SVG image should be rendered. - /// - /// `viewBox` and `preserveAspectRatio` in SVG. - pub view_box: ViewBox, - - /// The root element of the SVG tree. - /// - /// The root node is always `Group`. - pub root: Node, -} - -impl Tree { - // TODO: remove - /// Returns renderable node by ID. - /// - /// If an empty ID is provided, than this method will always return `None`. - /// Even if tree has nodes with empty ID. - pub fn node_by_id(&self, id: &str) -> Option { - if id.is_empty() { - return None; - } - - self.root.descendants().find(|node| &*node.id() == id) - } - - /// Checks if the current tree has any text nodes. - pub fn has_text_nodes(&self) -> bool { - has_text_nodes(&self.root) - } - - /// Calls a closure for each [`Paint`] in the tree. - /// - /// Doesn't guarantee to have unique paint servers. A caller must deduplicate them manually. - pub fn paint_servers(&self, mut f: F) { - loop_over_paint_servers(&self.root, &mut f) - } - - /// Calls a closure for each [`ClipPath`] in the tree. - /// - /// Doesn't guarantee to have unique clip paths. A caller must deduplicate them manually. - pub fn clip_paths)>(&self, mut f: F) { - loop_over_clip_paths(&self.root, &mut f) - } - - /// Calls a closure for each [`Mask`] in the tree. - /// - /// Doesn't guarantee to have unique masks. A caller must deduplicate them manually. - pub fn masks)>(&self, mut f: F) { - loop_over_masks(&self.root, &mut f) - } - - /// Calls a closure for each [`Filter`](filter::Filter) in the tree. - /// - /// Doesn't guarantee to have unique filters. A caller must deduplicate them manually. - pub fn filters)>(&self, mut f: F) { - loop_over_filters(&self.root, &mut f) - } -} - -fn has_text_nodes(root: &Node) -> bool { - for node in root.descendants() { - if let NodeKind::Text(_) = *node.borrow() { - return true; - } - - let mut has_text = false; - node.subroots(|subroot| { - if has_text_nodes(&subroot) { - has_text = true; - } - }); - - if has_text { - return true; - } - } - - false -} - -fn loop_over_paint_servers(root: &Node, f: &mut dyn FnMut(&Paint)) { - fn push(paint: Option<&Paint>, f: &mut dyn FnMut(&Paint)) { - if let Some(paint) = paint { - f(paint); - } - } - - for node in root.descendants() { - if let NodeKind::Path(ref path) = *node.borrow() { - push(path.fill.as_ref().map(|f| &f.paint), f); - push(path.stroke.as_ref().map(|f| &f.paint), f); - } else if let NodeKind::Text(ref text) = *node.borrow() { - for chunk in &text.chunks { - for span in &chunk.spans { - push(span.fill.as_ref().map(|f| &f.paint), f); - push(span.stroke.as_ref().map(|f| &f.paint), f); - - if let Some(ref underline) = span.decoration.underline { - push(underline.fill.as_ref().map(|f| &f.paint), f); - push(underline.stroke.as_ref().map(|f| &f.paint), f); - } - - if let Some(ref overline) = span.decoration.overline { - push(overline.fill.as_ref().map(|f| &f.paint), f); - push(overline.stroke.as_ref().map(|f| &f.paint), f); - } - - if let Some(ref line_through) = span.decoration.line_through { - push(line_through.fill.as_ref().map(|f| &f.paint), f); - push(line_through.stroke.as_ref().map(|f| &f.paint), f); - } - } - } - } - - node.subroots(|subroot| loop_over_paint_servers(&subroot, f)); - } -} - -fn loop_over_clip_paths(root: &Node, f: &mut dyn FnMut(Rc)) { - for node in root.descendants() { - if let NodeKind::Group(ref g) = *node.borrow() { - if let Some(ref clip) = g.clip_path { - f(clip.clone()); - - if let Some(ref sub_clip) = clip.clip_path { - f(sub_clip.clone()); - } - } - } - - node.subroots(|subroot| loop_over_clip_paths(&subroot, f)); - } -} - -fn loop_over_masks(root: &Node, f: &mut dyn FnMut(Rc)) { - for node in root.descendants() { - if let NodeKind::Group(ref g) = *node.borrow() { - if let Some(ref mask) = g.mask { - f(mask.clone()); - - if let Some(ref sub_mask) = mask.mask { - f(sub_mask.clone()); - } - } - } - - node.subroots(|subroot| loop_over_masks(&subroot, f)); - } -} - -fn loop_over_filters(root: &Node, f: &mut dyn FnMut(Rc)) { - for node in root.descendants() { - if let NodeKind::Group(ref g) = *node.borrow() { - for filter in &g.filters { - f(filter.clone()); - } - } - - node.subroots(|subroot| loop_over_filters(&subroot, f)); - } -} - -fn node_subroots(node: &Node, f: &mut dyn FnMut(Node)) { - let mut push_patt = |paint: Option<&Paint>| { - if let Some(Paint::Pattern(ref patt)) = paint { - f(patt.root.clone()); - } - }; - - match *node.borrow() { - NodeKind::Group(ref g) => { - if let Some(ref clip) = g.clip_path { - f(clip.root.clone()); - - if let Some(ref sub_clip) = clip.clip_path { - f(sub_clip.root.clone()); - } - } - - if let Some(ref mask) = g.mask { - f(mask.root.clone()); - - if let Some(ref sub_mask) = mask.mask { - f(sub_mask.root.clone()); - } - } - - for filter in &g.filters { - for primitive in &filter.primitives { - if let filter::Kind::Image(ref image) = primitive.kind { - if let filter::ImageKind::Use(ref use_node) = image.data { - f(use_node.clone()); - } - } - } - } - } - NodeKind::Path(ref path) => { - push_patt(path.fill.as_ref().map(|f| &f.paint)); - push_patt(path.stroke.as_ref().map(|f| &f.paint)); - } - NodeKind::Image(_) => {} - NodeKind::Text(ref text) => { - for chunk in &text.chunks { - for span in &chunk.spans { - push_patt(span.fill.as_ref().map(|f| &f.paint)); - push_patt(span.stroke.as_ref().map(|f| &f.paint)); - - // Each text decoration can have paint. - if let Some(ref underline) = span.decoration.underline { - push_patt(underline.fill.as_ref().map(|f| &f.paint)); - push_patt(underline.stroke.as_ref().map(|f| &f.paint)); - } - - if let Some(ref overline) = span.decoration.overline { - push_patt(overline.fill.as_ref().map(|f| &f.paint)); - push_patt(overline.stroke.as_ref().map(|f| &f.paint)); - } - - if let Some(ref line_through) = span.decoration.line_through { - push_patt(line_through.fill.as_ref().map(|f| &f.paint)); - push_patt(line_through.stroke.as_ref().map(|f| &f.paint)); - } - } - } - } - } -} - -/// Additional `Node` methods. -pub trait NodeExt { - /// Returns node's ID. - /// - /// If a current node doesn't support ID - an empty string - /// will be returned. - fn id(&self) -> std::cell::Ref; - - /// Returns node's transform. - /// - /// If a current node doesn't support transformation - a default - /// transform will be returned. - fn transform(&self) -> Transform; - - /// Returns node's absolute transform. - /// - /// If a current node doesn't support transformation - a default - /// transform will be returned. - fn abs_transform(&self) -> Transform; - - /// Appends `kind` as a node child. - /// - /// Shorthand for `Node::append(Node::new(Box::new(kind)))`. - fn append_kind(&self, kind: NodeKind) -> Node; - - /// Calculates node's absolute bounding box. - /// - /// Always returns `None` for `NodeKind::Text` since we cannot calculate its bbox - /// without converting it into paths first. - fn calculate_bbox(&self) -> Option; - - /// Calls a closure for each subroot this `Node` has. - /// - /// The [`Tree::root`](Tree::root) field contain only render-able SVG elements. - /// But some elements, specifically clip paths, masks, patterns and feImage - /// can store their own SVG subtrees. - /// And while one can access them manually, it's pretty verbose. - /// This methods allows looping over _all_ SVG elements present in the `Tree`. - /// - /// # Example - /// - /// ```no_run - /// use usvg_tree::NodeExt; - /// - /// fn all_nodes(root: &usvg_tree::Node) { - /// for node in root.descendants() { - /// // do stuff... - /// - /// // hand subroots as well - /// node.subroots(|subroot| all_nodes(&subroot)); - /// } - /// } - /// ``` - fn subroots(&self, f: F); -} - -impl NodeExt for Node { - #[inline] - fn id(&self) -> std::cell::Ref { - std::cell::Ref::map(self.borrow(), |v| v.id()) - } - - #[inline] - fn transform(&self) -> Transform { - self.borrow().transform() - } - - fn abs_transform(&self) -> Transform { - let mut ts_list = Vec::new(); - for p in self.ancestors() { - ts_list.push(p.transform()); - } - - let mut abs_ts = Transform::default(); - for ts in ts_list.iter().rev() { - abs_ts = abs_ts.pre_concat(*ts); - } - - abs_ts - } - - #[inline] - fn append_kind(&self, kind: NodeKind) -> Node { - let new_node = Node::new(kind); - self.append(new_node.clone()); - new_node - } - - #[inline] - fn calculate_bbox(&self) -> Option { - calc_node_bbox(self, self.abs_transform()).and_then(|r| r.to_rect()) - } - - fn subroots(&self, mut f: F) { - node_subroots(self, &mut f) - } -} - -fn calc_node_bbox(node: &Node, ts: Transform) -> Option { - match *node.borrow() { - NodeKind::Path(ref path) => path.data.bounds().transform(ts).map(BBox::from), - NodeKind::Image(ref img) => img.view_box.rect.transform(ts).map(BBox::from), - NodeKind::Group(_) => { - let mut bbox = BBox::default(); - - for child in node.children() { - let child_transform = ts.pre_concat(child.transform()); - if let Some(c_bbox) = calc_node_bbox(&child, child_transform) { - bbox = bbox.expand(c_bbox); - } - } - - // Make sure bbox was changed. - if bbox.is_default() { - return None; - } - - Some(bbox) - } - NodeKind::Text(_) => None, - } -} diff --git a/crates/usvg-tree/src/text.rs b/crates/usvg-tree/src/text.rs deleted file mode 100644 index f265e24fb..000000000 --- a/crates/usvg-tree/src/text.rs +++ /dev/null @@ -1,342 +0,0 @@ -// 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/. - -use std::rc::Rc; - -use strict_num::NonZeroPositiveF32; - -use crate::{Fill, PaintOrder, Stroke, TextRendering, Transform, Visibility}; - -/// A font stretch property. -#[allow(missing_docs)] -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] -pub enum FontStretch { - UltraCondensed, - ExtraCondensed, - Condensed, - SemiCondensed, - Normal, - SemiExpanded, - Expanded, - ExtraExpanded, - UltraExpanded, -} - -impl Default for FontStretch { - #[inline] - fn default() -> Self { - Self::Normal - } -} - -/// A font style property. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub enum FontStyle { - /// A face that is neither italic not obliqued. - Normal, - /// A form that is generally cursive in nature. - Italic, - /// A typically-sloped version of the regular face. - Oblique, -} - -impl Default for FontStyle { - #[inline] - fn default() -> FontStyle { - Self::Normal - } -} - -/// Text font properties. -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct Font { - /// A list of family names. - /// - /// Never empty. Uses `usvg_parser::Options::font_family` as fallback. - pub families: Vec, - /// A font style. - pub style: FontStyle, - /// A font stretch. - pub stretch: FontStretch, - /// A font width. - pub weight: u16, -} - -/// A dominant baseline property. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum DominantBaseline { - Auto, - UseScript, - NoChange, - ResetSize, - Ideographic, - Alphabetic, - Hanging, - Mathematical, - Central, - Middle, - TextAfterEdge, - TextBeforeEdge, -} - -impl Default for DominantBaseline { - fn default() -> Self { - Self::Auto - } -} - -/// An alignment baseline property. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum AlignmentBaseline { - Auto, - Baseline, - BeforeEdge, - TextBeforeEdge, - Middle, - Central, - AfterEdge, - TextAfterEdge, - Ideographic, - Alphabetic, - Hanging, - Mathematical, -} - -impl Default for AlignmentBaseline { - fn default() -> Self { - Self::Auto - } -} - -/// A baseline shift property. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum BaselineShift { - Baseline, - Subscript, - Superscript, - Number(f32), -} - -impl Default for BaselineShift { - #[inline] - fn default() -> BaselineShift { - BaselineShift::Baseline - } -} - -/// A length adjust property. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum LengthAdjust { - Spacing, - SpacingAndGlyphs, -} - -impl Default for LengthAdjust { - fn default() -> Self { - Self::Spacing - } -} - -/// A text span decoration style. -/// -/// In SVG, text decoration and text it's applied to can have different styles. -/// So you can have black text and green underline. -/// -/// Also, in SVG you can specify text decoration stroking. -#[derive(Clone, Debug)] -pub struct TextDecorationStyle { - /// A fill style. - pub fill: Option, - /// A stroke style. - pub stroke: Option, -} - -/// A text span decoration. -#[derive(Clone, Debug)] -pub struct TextDecoration { - /// An optional underline and its style. - pub underline: Option, - /// An optional overline and its style. - pub overline: Option, - /// An optional line-through and its style. - pub line_through: Option, -} - -/// A text style span. -/// -/// Spans do not overlap inside a text chunk. -#[derive(Clone, Debug)] -pub struct TextSpan { - /// A span start in bytes. - /// - /// Offset is relative to the parent text chunk and not the parent text element. - pub start: usize, - /// A span end in bytes. - /// - /// Offset is relative to the parent text chunk and not the parent text element. - pub end: usize, - /// A fill style. - pub fill: Option, - /// A stroke style. - pub stroke: Option, - /// A paint order style. - pub paint_order: PaintOrder, - /// A font. - pub font: Font, - /// A font size. - pub font_size: NonZeroPositiveF32, - /// Indicates that small caps should be used. - /// - /// Set by `font-variant="small-caps"` - pub small_caps: bool, - /// Indicates that a kerning should be applied. - /// - /// Supports both `kerning` and `font-kerning` properties. - pub apply_kerning: bool, - /// A span decorations. - pub decoration: TextDecoration, - /// A span dominant baseline. - pub dominant_baseline: DominantBaseline, - /// A span alignment baseline. - pub alignment_baseline: AlignmentBaseline, - /// A list of all baseline shift that should be applied to this span. - /// - /// Ordered from `text` element down to the actual `span` element. - pub baseline_shift: Vec, - /// A visibility property. - pub visibility: Visibility, - /// A letter spacing property. - pub letter_spacing: f32, - /// A word spacing property. - pub word_spacing: f32, - /// A text length property. - pub text_length: Option, - /// A length adjust property. - pub length_adjust: LengthAdjust, -} - -/// A text chunk anchor property. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum TextAnchor { - Start, - Middle, - End, -} - -impl Default for TextAnchor { - fn default() -> Self { - Self::Start - } -} - -/// A path used by text-on-path. -#[derive(Clone, Debug)] -pub struct TextPath { - /// A text offset in SVG coordinates. - /// - /// Percentage values already resolved. - pub start_offset: f32, - - /// A path. - pub path: Rc, -} - -/// A text chunk flow property. -#[derive(Clone, Debug)] -pub enum TextFlow { - /// A linear layout. - /// - /// Includes left-to-right, right-to-left and top-to-bottom. - Linear, - /// A text-on-path layout. - Path(Rc), -} - -/// A text chunk. -/// -/// Text alignment and BIDI reordering can only be done inside a text chunk. -#[derive(Clone, Debug)] -pub struct TextChunk { - /// An absolute X axis offset. - pub x: Option, - /// An absolute Y axis offset. - pub y: Option, - /// A text anchor. - pub anchor: TextAnchor, - /// A list of text chunk style spans. - pub spans: Vec, - /// A text chunk flow. - pub text_flow: TextFlow, - /// A text chunk actual text. - pub text: String, -} - -/// A text character position. -/// -/// _Character_ is a Unicode codepoint. -#[derive(Clone, Copy, Debug)] -pub struct CharacterPosition { - /// An absolute X axis position. - pub x: Option, - /// An absolute Y axis position. - pub y: Option, - /// A relative X axis offset. - pub dx: Option, - /// A relative Y axis offset. - pub dy: Option, -} - -/// A writing mode. -#[allow(missing_docs)] -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum WritingMode { - LeftToRight, - TopToBottom, -} - -/// A text element. -/// -/// `text` element in SVG. -#[derive(Clone, Debug)] -pub struct Text { - /// Element's ID. - /// - /// Taken from the SVG itself. - /// Isn't automatically generated. - /// Can be empty. - pub id: String, - - /// Element's class names. - #[cfg(feature = "class")] - pub class: String, - - /// Element transform. - pub transform: Transform, - - /// Rendering mode. - /// - /// `text-rendering` in SVG. - pub rendering_mode: TextRendering, - - /// A list of character positions. - /// - /// One position for each Unicode codepoint. Aka `char` in Rust. - pub positions: Vec, - - /// A list of rotation angles. - /// - /// One angle for each Unicode codepoint. Aka `char` in Rust. - pub rotate: Vec, - - /// A writing mode. - pub writing_mode: WritingMode, - - /// A list of text chunks. - pub chunks: Vec, -} diff --git a/crates/usvg/Cargo.toml b/crates/usvg/Cargo.toml index 61c972acb..8df8fbd43 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "usvg" -version = "0.36.0+class" +version = "0.41.0+class" authors = ["Yevhenii Reizner "] keywords = ["svg"] license = "MPL-2.0" -edition = "2018" +edition = "2021" description = "An SVG simplification library." categories = ["multimedia::images"] repository = "https://github.com/RazrFalcon/resvg" @@ -18,26 +18,40 @@ name = "usvg" required-features = ["text", "system-fonts", "memmap-fonts"] [dependencies] -base64 = "0.21" # for embedded images +base64 = "0.22" # for embedded images log = "0.4" pico-args = { version = "0.5", features = ["eq-separator"] } -usvg-parser = { path = "../usvg-parser", version = "0.36.0+class" } -usvg-tree = { path = "../usvg-tree", version = "0.36.0+class" } +strict-num = "0.1.1" +svgtypes = "0.15.0" +tiny-skia-path = "0.11.4" xmlwriter = "0.1" -[dependencies.usvg-text-layout] -path = "../usvg-text-layout" -version = "0.36.0+class" -default-features = false -optional = true +# parser +data-url = "0.3" # for href parsing +flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } # SVGZ decoding +imagesize = "0.12" # raster images size detection +kurbo = "0.11" # Bezier curves utils +roxmltree = "0.19" +simplecss = "0.2" +siphasher = "1.0" # perfect hash implementation + +# text +fontdb = { version = "0.16.1", default-features = false, optional = true } +rustybuzz = { version = "0.13", optional = true } +unicode-bidi = { version = "0.3", optional = true } +unicode-script = { version = "0.5", optional = true } +unicode-vo = { version = "0.1", optional = true } + +[dev-dependencies] +once_cell = "1.5" [features] default = ["text", "system-fonts", "memmap-fonts"] # Enables text-to-path conversion support. # Adds around 400KiB to your binary. -text = ["usvg-text-layout"] +text = ["fontdb", "rustybuzz", "unicode-bidi", "unicode-script", "unicode-vo"] # Enables system fonts loading. -system-fonts = ["usvg-text-layout/system-fonts"] +system-fonts = ["fontdb/fs", "fontdb/fontconfig"] # Enables font files memmaping for faster loading. -memmap-fonts = ["usvg-text-layout/memmap-fonts"] -class = ["usvg-parser/class", "usvg-tree/class"] \ No newline at end of file +memmap-fonts = ["fontdb/memmap"] +class = [] diff --git a/crates/usvg/README.md b/crates/usvg/README.md index 7d6dd2b37..a1907ae88 100644 --- a/crates/usvg/README.md +++ b/crates/usvg/README.md @@ -8,7 +8,7 @@ SVG is notoriously hard to parse. `usvg` presents a layer between an XML library and a potential SVG rendering library. It will parse an input SVG into a strongly-typed tree structure were all the elements, attributes, references and other SVG features are already resolved -and presented in a simplest way possible. +and presented in the simplest way possible. So a caller doesn't have to worry about most of the issues related to SVG parsing and can focus just on the rendering part. @@ -34,7 +34,8 @@ and can focus just on the rendering part. text chunks and spans resolving - Markers will be converted into regular elements. No need to place them manually - All filters are supported. Including filter functions, like `filter="contrast(50%)"` -- Recursive elements will be detected an removed +- Recursive elements will be detected and removed +- `objectBoundingBox` will be replaced with `userSpaceOnUse` ## Limitations @@ -42,7 +43,6 @@ and can focus just on the rendering part. - CSS support is minimal - Only [static](http://www.w3.org/TR/SVG11/feature#SVG-static) SVG features, e.g. no `a`, `view`, `cursor`, `script`, no events and no animations -- Text elements must be converted into paths before writing to SVG ## License diff --git a/crates/usvg-parser/codegen/Cargo.toml b/crates/usvg/codegen/Cargo.toml similarity index 93% rename from crates/usvg-parser/codegen/Cargo.toml rename to crates/usvg/codegen/Cargo.toml index 8aea91925..56fbb701a 100644 --- a/crates/usvg-parser/codegen/Cargo.toml +++ b/crates/usvg/codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "codegen" version = "0.1.0" authors = ["Yevhenii Reizner "] license = "MIT" -edition = "2018" +edition = "2021" [workspace] diff --git a/crates/usvg-parser/codegen/README.md b/crates/usvg/codegen/README.md similarity index 55% rename from crates/usvg-parser/codegen/README.md rename to crates/usvg/codegen/README.md index 48719080d..4a71b27fb 100644 --- a/crates/usvg-parser/codegen/README.md +++ b/crates/usvg/codegen/README.md @@ -1,4 +1,4 @@ -We don't use cargo build script, since this data will be changed rarely and +We don't use cargo build script, since this data will rarely be changed and there is no point in regenerating it each time. To regenerate files run: diff --git a/crates/usvg-parser/codegen/attributes.txt b/crates/usvg/codegen/attributes.txt similarity index 100% rename from crates/usvg-parser/codegen/attributes.txt rename to crates/usvg/codegen/attributes.txt diff --git a/crates/usvg-parser/codegen/elements.txt b/crates/usvg/codegen/elements.txt similarity index 100% rename from crates/usvg-parser/codegen/elements.txt rename to crates/usvg/codegen/elements.txt diff --git a/crates/usvg-parser/codegen/main.rs b/crates/usvg/codegen/main.rs similarity index 88% rename from crates/usvg-parser/codegen/main.rs rename to crates/usvg/codegen/main.rs index c62bcb49b..fb77adebd 100644 --- a/crates/usvg-parser/codegen/main.rs +++ b/crates/usvg/codegen/main.rs @@ -74,26 +74,14 @@ fn main() { } fn gen() -> Result<(), Box> { - let f = &mut fs::File::create("../src/svgtree/names.rs")?; + let f = &mut fs::File::create("../src/parser/svgtree/names.rs")?; writeln!(f, "// This file is autogenerated. Do not edit it!")?; writeln!(f, "// See ./codegen for details.\n")?; - gen_map( - "elements.txt", - "An element ID.", - "EId", - "ELEMENTS", - f, - )?; + gen_map("elements.txt", "An element ID.", "EId", "ELEMENTS", f)?; - gen_map( - "attributes.txt", - "An attribute ID.", - "AId", - "ATTRIBUTES", - f, - )?; + gen_map("attributes.txt", "An attribute ID.", "AId", "ATTRIBUTES", f)?; writeln!(f, "{}", PHF_SRC)?; @@ -114,7 +102,6 @@ fn gen_map( let joined_names = names.iter().map(|n| to_enum_name(n)).join(",\n "); - let mut map = phf_codegen::Map::new(); for name in &names { map.entry(*name, &format!("{}::{}", enum_name, to_enum_name(name))); @@ -134,10 +121,18 @@ fn gen_map( writeln!(f, " {}", joined_names)?; writeln!(f, "}}\n")?; - writeln!(f, "static {}: Map<{}> = {};\n", map_name, enum_name, map_data)?; + writeln!( + f, + "static {}: Map<{}> = {};\n", + map_name, enum_name, map_data + )?; writeln!(f, "impl {} {{", enum_name)?; - writeln!(f, " pub(crate) fn from_str(text: &str) -> Option<{}> {{", enum_name)?; + writeln!( + f, + " pub(crate) fn from_str(text: &str) -> Option<{}> {{", + enum_name + )?; writeln!(f, " {}.get(text).cloned()", map_name)?; writeln!(f, " }}")?; writeln!(f, "")?; @@ -149,13 +144,19 @@ fn gen_map( writeln!(f, "}}\n")?; writeln!(f, "impl std::fmt::Debug for {} {{", enum_name)?; - writeln!(f, " fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {{")?; + writeln!( + f, + " fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {{" + )?; writeln!(f, " write!(f, \"{{}}\", self.to_str())")?; writeln!(f, " }}")?; writeln!(f, "}}\n")?; writeln!(f, "impl std::fmt::Display for {} {{", enum_name)?; - writeln!(f, " fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {{")?; + writeln!( + f, + " fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {{" + )?; writeln!(f, " write!(f, \"{{:?}}\", self)")?; writeln!(f, " }}")?; writeln!(f, "}}")?; diff --git a/crates/usvg-parser/docs/post-processing.md b/crates/usvg/docs/post-processing.md similarity index 95% rename from crates/usvg-parser/docs/post-processing.md rename to crates/usvg/docs/post-processing.md index 26723ced1..33c184d93 100644 --- a/crates/usvg-parser/docs/post-processing.md +++ b/crates/usvg/docs/post-processing.md @@ -2,7 +2,7 @@ ## No namespaces -In an SVG tree all elements and attributes belong to to the SVG namespace. +In an SVG tree all elements and attributes belong to the SVG namespace. ## No non-SVG elements and attributes @@ -129,11 +129,11 @@ and more complex one like As mentioned above, SVG supports references. And it can reference any element in the document.
Instead of checking each element in the tree each time, which would be pretty slow, -we have a ID<->Node HashMap to quickly retrieve a requested element. +we have an ID<->Node HashMap to quickly retrieve a requested element. ## Links are groups -The `
` element in SVG is just a `` with an URL.
+The `
` element in SVG is just a `` with a URL.
Since we really support only the static SVG subset, we can replace `
` with ``. ## `tref` resolving diff --git a/crates/usvg/docs/spec.adoc b/crates/usvg/docs/spec.adoc index f57b7ed68..bb7e8b9be 100644 --- a/crates/usvg/docs/spec.adoc +++ b/crates/usvg/docs/spec.adoc @@ -19,6 +19,7 @@ Here is the main differences between SVG Full and SVG Micro. - No unused elements. - No redundant groups. - No units. +- No `objectBoundingBox` units. - No `style` attribute, except for `mix-blend-mode` and `isolation` - Default attributes are implicit. @@ -118,7 +119,7 @@ children will be resolved. * `fy` = < >> + Guarantee to be inside the circle defined by `cx`, `cy` and `r`. * `r` = < >> -* `gradientUnits` = `userSpaceOnUse`? +* `gradientUnits` = `userSpaceOnUse` * `spreadMethod` = `reflect | repeat`? * `gradientTransform` = < >>? @@ -162,10 +163,7 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b * `height` = < >> * `viewBox` = < >>? * `preserveAspectRatio` = < >>? -* `patternUnits` = `userSpaceOnUse`? + - Default: objectBoundingBox -* `patternContentUnits` = `objectBoundingBox`? + - Default: userSpaceOnUse +* `patternUnits` = `userSpaceOnUse` * `patternTransform` = < >>? [[clipPath-element]] @@ -183,8 +181,6 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b * `clip-path` = < >>? + An optional reference to a supplemental `clipPath`. + Default: none -* `clipPathUnits` = `objectBoundingBox`? + - Default: userSpaceOnUse * `transform` = < >>? [[mask-element]] @@ -210,10 +206,7 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b * `height` = < >> * `mask-type` = `alpha`? + Default: luminance -* `maskUnits` = `userSpaceOnUse`? + - Default: objectBoundingBox -* `maskContentUnits` = `objectBoundingBox`? + - Default: userSpaceOnUse +* `maskUnits` = `userSpaceOnUse` [[filter-element]] @@ -233,10 +226,7 @@ Doesn't have a `xlink:href` attribute because all attributes and children will b * `y` = < >> * `width` = < >> * `height` = < >> -* `filterUnits` = `userSpaceOnUse`? + - Default: objectBoundingBox -* `primitiveUnits` = `objectBoundingBox`? + - Default: userSpaceOnUse +* `filterUnits` = `userSpaceOnUse` [[g-element]] @@ -329,7 +319,8 @@ A group will have at least one of the attributes present. Default: geometricPrecision * `visibility` = `hidden | collapse`? + Default: visible -* `transform` = < >>? +* `transform` = < >>? + + Can only be set on paths inside of `clipPath`. [[image-element]] @@ -354,7 +345,6 @@ A group will have at least one of the attributes present. Default: optimizeQuality * `visibility` = `hidden | collapse`? + Default: visible -* `transform` = < >>? == Filter primitives diff --git a/crates/usvg/src/lib.rs b/crates/usvg/src/lib.rs index 79bfb5299..553c7320a 100644 --- a/crates/usvg/src/lib.rs +++ b/crates/usvg/src/lib.rs @@ -8,7 +8,7 @@ SVG is notoriously hard to parse. `usvg` presents a layer between an XML library and a potential SVG rendering library. It will parse an input SVG into a strongly-typed tree structure were all the elements, attributes, references and other SVG features are already resolved -and presented in a simplest way possible. +and presented in the simplest way possible. So a caller doesn't have to worry about most of the issues related to SVG parsing and can focus just on the rendering part. @@ -19,7 +19,7 @@ and can focus just on the rendering part. - CSS will be applied - Only simple paths - Basic shapes (like `rect` and `circle`) will be converted into paths - - Paths contain only absolute *MoveTo*, *LineTo*, *CurveTo* and *ClosePath* segments. + - Paths contain only absolute *MoveTo*, *LineTo*, *QuadTo*, *CurveTo* and *ClosePath* segments. ArcTo, implicit and relative segments will be converted - `use` will be resolved and replaced with the reference content - Nested `svg` will be resolved @@ -34,7 +34,8 @@ and can focus just on the rendering part. text chunks and spans resolving - Markers will be converted into regular elements. No need to place them manually - All filters are supported. Including filter functions, like `filter="contrast(50%)"` -- Recursive elements will be detected an removed +- Recursive elements will be detected and removed +- `objectBoundingBox` will be replaced with `userSpaceOnUse` ## Limitations @@ -42,7 +43,6 @@ and can focus just on the rendering part. - CSS support is minimal - Only [static](http://www.w3.org/TR/SVG11/feature#SVG-static) SVG features, e.g. no `a`, `view`, `cursor`, `script`, no events and no animations -- Text elements must be converted into paths before writing to SVG. [SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics */ @@ -52,23 +52,21 @@ and can focus just on the rendering part. #![warn(missing_debug_implementations)] #![warn(missing_copy_implementations)] +mod parser; +#[cfg(feature = "text")] +mod text; +mod tree; mod writer; -pub use usvg_parser::*; +pub use parser::*; #[cfg(feature = "text")] -pub use usvg_text_layout::*; -pub use usvg_tree::*; +pub use text::*; +pub use tree::*; -pub use writer::XmlOptions; +pub use roxmltree; -/// A trait to write `usvg::Tree` back to SVG. -pub trait TreeWriting { - /// Writes `usvg::Tree` back to SVG. - fn to_string(&self, opt: &XmlOptions) -> String; -} +#[cfg(feature = "text")] +pub use fontdb; -impl TreeWriting for usvg_tree::Tree { - fn to_string(&self, opt: &XmlOptions) -> String { - writer::convert(self, opt) - } -} +pub use writer::WriteOptions; +pub use xmlwriter::Indent; diff --git a/crates/usvg/src/main.rs b/crates/usvg/src/main.rs index 6abea8aec..29c20d709 100644 --- a/crates/usvg/src/main.rs +++ b/crates/usvg/src/main.rs @@ -8,9 +8,6 @@ use std::path::PathBuf; use std::process; use pico_args::Arguments; -use usvg::TreeWriting; -use usvg_parser::TreeParsing; -use usvg_text_layout::TreeTextToPath; const HELP: &str = "\ usvg (micro SVG) is an SVG simplification tool. @@ -91,6 +88,7 @@ OPTIONS: Refer to the explanation of the '--default-width' option. [values: 1..4294967295 (inclusive)] [default: 100] + --preserve-text Do not convert text into paths. --id-prefix Adds a prefix to each ID attribute --indent INDENT Sets the XML nodes indent [values: none, 0, 1, 2, 3, 4, tabs] [default: 4] @@ -113,9 +111,9 @@ ARGS: struct Args { dpi: u32, languages: Vec, - shape_rendering: usvg_tree::ShapeRendering, - text_rendering: usvg_tree::TextRendering, - image_rendering: usvg_tree::ImageRendering, + shape_rendering: usvg::ShapeRendering, + text_rendering: usvg::TextRendering, + image_rendering: usvg::ImageRendering, resources_dir: Option, font_family: Option, @@ -128,6 +126,7 @@ struct Args { font_files: Vec, font_dirs: Vec, skip_system_fonts: bool, + preserve_text: bool, list_fonts: bool, default_width: u32, default_height: u32, @@ -149,12 +148,12 @@ fn collect_args() -> Result { if input.contains(["-h", "--help"]) { print!("{}", HELP); - std::process::exit(0); + process::exit(0); } if input.contains(["-V", "--version"]) { println!("{}", env!("CARGO_PKG_VERSION")); - std::process::exit(0); + process::exit(0); } Ok(Args { @@ -187,6 +186,7 @@ fn collect_args() -> Result { font_files: input.values_from_str("--use-font-file")?, font_dirs: input.values_from_str("--use-fonts-dir")?, skip_system_fonts: input.contains("--skip-system-fonts"), + preserve_text: input.contains("--preserve-text"), list_fonts: input.contains("--list-fonts"), default_width: input .opt_value_from_fn("--default-width", parse_length)? @@ -310,7 +310,7 @@ fn main() { if let Err(e) = process(args) { eprintln!("Error: {}.", e.to_string()); - std::process::exit(1); + process::exit(1); } } @@ -336,7 +336,7 @@ fn process(args: Args) -> Result<(), String> { (svg_from, svg_to) }; - let mut fontdb = usvg_text_layout::fontdb::Database::new(); + let mut fontdb = usvg::fontdb::Database::new(); if !args.skip_system_fonts { // TODO: only when needed fontdb.load_system_fonts(); @@ -364,7 +364,7 @@ fn process(args: Args) -> Result<(), String> { if args.list_fonts { for face in fontdb.faces() { - if let usvg_text_layout::fontdb::Source::File(ref path) = &face.source { + if let usvg::fontdb::Source::File(ref path) = &face.source { let families: Vec<_> = face .families .iter() @@ -399,7 +399,7 @@ fn process(args: Args) -> Result<(), String> { } }; - let re_opt = usvg_parser::Options { + let re_opt = usvg::Options { resources_dir, dpi: args.dpi as f32, font_family: args @@ -412,12 +412,9 @@ fn process(args: Args) -> Result<(), String> { shape_rendering: args.shape_rendering, text_rendering: args.text_rendering, image_rendering: args.image_rendering, - default_size: usvg_tree::Size::from_wh( - args.default_width as f32, - args.default_height as f32, - ) - .unwrap(), - image_href_resolver: usvg_parser::ImageHrefResolver::default(), + default_size: usvg::Size::from_wh(args.default_width as f32, args.default_height as f32) + .unwrap(), + image_href_resolver: usvg::ImageHrefResolver::default(), }; let input_svg = match in_svg { @@ -425,18 +422,16 @@ fn process(args: Args) -> Result<(), String> { InputFrom::File(ref path) => std::fs::read(path).map_err(|e| e.to_string()), }?; - let mut tree = usvg_tree::Tree::from_data(&input_svg, &re_opt).map_err(|e| format!("{}", e))?; - tree.convert_text(&fontdb); + let tree = usvg::Tree::from_data(&input_svg, &re_opt, &fontdb).map_err(|e| format!("{}", e))?; - let xml_opt = usvg::XmlOptions { + let xml_opt = usvg::WriteOptions { id_prefix: args.id_prefix, + preserve_text: args.preserve_text, coordinates_precision: args.coordinates_precision.unwrap_or(8), transforms_precision: args.transforms_precision.unwrap_or(8), - writer_opts: xmlwriter::Options { - use_single_quote: false, - indent: args.indent, - attributes_indent: args.attrs_indent, - }, + use_single_quote: false, + indent: args.indent, + attributes_indent: args.attrs_indent, }; let s = tree.to_string(&xml_opt); @@ -486,15 +481,14 @@ impl log::Log for SimpleLogger { }; let line = record.line().unwrap_or(0); + let args = record.args(); match record.level() { - log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, record.args()), - log::Level::Warn => { - eprintln!("Warning (in {}:{}): {}", target, line, record.args()) - } - log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, record.args()), - log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, record.args()), - log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, record.args()), + log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args), + log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args), + log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args), + log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args), + log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args), } } } diff --git a/crates/usvg-parser/src/clippath.rs b/crates/usvg/src/parser/clippath.rs similarity index 54% rename from crates/usvg-parser/src/clippath.rs rename to crates/usvg/src/parser/clippath.rs index 45cd1ae66..b95caafa5 100644 --- a/crates/usvg-parser/src/clippath.rs +++ b/crates/usvg/src/parser/clippath.rs @@ -2,36 +2,60 @@ // 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/. -use std::rc::Rc; use std::str::FromStr; +use std::sync::Arc; -use usvg_tree::{ClipPath, Group, Node, NodeKind, Transform, Units}; - -use crate::converter; -use crate::svgtree::{AId, EId, SvgNode}; +use super::converter; +use super::svgtree::{AId, EId, SvgNode}; +use crate::{ClipPath, Group, NonEmptyString, NonZeroRect, Transform, Units}; pub(crate) fn convert( node: SvgNode, state: &converter::State, + object_bbox: Option, cache: &mut converter::Cache, -) -> Option> { +) -> Option> { // A `clip-path` attribute must reference a `clipPath` element. if node.tag_name() != Some(EId::ClipPath) { return None; } // The whole clip path should be ignored when a transform is invalid. - let transform = resolve_transform(node)?; + let mut transform = resolve_clip_path_transform(node, state)?; + + let units = node + .attribute(AId::ClipPathUnits) + .unwrap_or(Units::UserSpaceOnUse); // Check if this element was already converted. - if let Some(clip) = cache.clip_paths.get(node.element_id()) { - return Some(clip.clone()); + // + // Only `userSpaceOnUse` clipPaths can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse; + if cacheable { + if let Some(clip) = cache.clip_paths.get(node.element_id()) { + return Some(clip.clone()); + } + } + + if units == Units::ObjectBoundingBox { + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!("Clipping of zero-sized shapes is not allowed."); + return None; + } + }; + + let ts = Transform::from_bbox(object_bbox); + transform = transform.pre_concat(ts); } // Resolve linked clip path. let mut clip_path = None; if let Some(link) = node.attribute::(AId::ClipPath) { - clip_path = convert(link, state, cache); + clip_path = convert(link, state, object_bbox, cache); // Linked `clipPath` must be valid. if clip_path.is_none() { @@ -39,16 +63,18 @@ pub(crate) fn convert( } } - let units = node - .attribute(AId::ClipPathUnits) - .unwrap_or(Units::UserSpaceOnUse); + let mut id = NonEmptyString::new(node.element_id().to_string())?; + // Generate ID only when we're parsing `objectBoundingBox` clip for the second time. + if !cacheable && cache.clip_paths.contains_key(id.get()) { + id = cache.gen_clip_path_id(); + } + let id_copy = id.get().to_string(); let mut clip = ClipPath { - id: node.element_id().to_string(), - units, + id, transform, clip_path, - root: Node::new(NodeKind::Group(Group::default())), + root: Group::empty(), }; let mut clip_state = state.clone(); @@ -56,10 +82,9 @@ pub(crate) fn convert( converter::convert_clip_path_elements(node, &clip_state, cache, &mut clip.root); if clip.root.has_children() { - let clip = Rc::new(clip); - cache - .clip_paths - .insert(node.element_id().to_string(), clip.clone()); + clip.root.calculate_bounding_boxes(); + let clip = Arc::new(clip); + cache.clip_paths.insert(id_copy, clip.clone()); Some(clip) } else { // A clip path without children is invalid. @@ -67,7 +92,7 @@ pub(crate) fn convert( } } -fn resolve_transform(node: SvgNode) -> Option { +fn resolve_clip_path_transform(node: SvgNode, state: &converter::State) -> Option { // Do not use Node::attribute::, because it will always // return a valid transform. @@ -94,7 +119,7 @@ fn resolve_transform(node: SvgNode) -> Option { ); if ts.is_valid() { - Some(ts) + Some(node.resolve_transform(AId::Transform, state)) } else { None } diff --git a/crates/usvg/src/parser/converter.rs b/crates/usvg/src/parser/converter.rs new file mode 100644 index 000000000..aa70f682d --- /dev/null +++ b/crates/usvg/src/parser/converter.rs @@ -0,0 +1,953 @@ +// 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/. + +use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::str::FromStr; +use std::sync::Arc; + +use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; + +use super::svgtree::{self, AId, EId, FromValue, SvgNode}; +use super::units::{self, convert_length}; +use super::{marker, Error, Options}; +use crate::parser::paint_server::process_paint; +use crate::*; + +#[derive(Clone)] +pub struct State<'a> { + pub(crate) parent_clip_path: Option>, + pub(crate) parent_markers: Vec>, + /// Stores the resolved fill and stroke of a use node + /// or a path element (for markers) + pub(crate) context_element: Option<(Option, Option)>, + pub(crate) fe_image_link: bool, + /// A viewBox of the parent SVG element. + pub(crate) view_box: NonZeroRect, + /// A size of the parent `use` element. + /// Used only during nested `svg` size resolving. + /// Width and height can be set independently. + pub(crate) use_size: (Option, Option), + pub(crate) opt: &'a Options, + #[cfg(feature = "text")] + pub(crate) fontdb: &'a fontdb::Database, +} + +#[derive(Clone, Default)] +pub struct Cache { + pub clip_paths: HashMap>, + pub masks: HashMap>, + pub filters: HashMap>, + pub paint: HashMap, + + // used for ID generation + all_ids: HashSet, + linear_gradient_index: usize, + radial_gradient_index: usize, + pattern_index: usize, + clip_path_index: usize, + mask_index: usize, + filter_index: usize, +} + +impl Cache { + // TODO: macros? + pub(crate) fn gen_linear_gradient_id(&mut self) -> NonEmptyString { + loop { + self.linear_gradient_index += 1; + let new_id = format!("linearGradient{}", self.linear_gradient_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_radial_gradient_id(&mut self) -> NonEmptyString { + loop { + self.radial_gradient_index += 1; + let new_id = format!("radialGradient{}", self.radial_gradient_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_pattern_id(&mut self) -> NonEmptyString { + loop { + self.pattern_index += 1; + let new_id = format!("pattern{}", self.pattern_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_clip_path_id(&mut self) -> NonEmptyString { + loop { + self.clip_path_index += 1; + let new_id = format!("clipPath{}", self.clip_path_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_mask_id(&mut self) -> NonEmptyString { + loop { + self.mask_index += 1; + let new_id = format!("mask{}", self.mask_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } + + pub(crate) fn gen_filter_id(&mut self) -> NonEmptyString { + loop { + self.filter_index += 1; + let new_id = format!("filter{}", self.filter_index); + let new_hash = string_hash(&new_id); + if !self.all_ids.contains(&new_hash) { + return NonEmptyString::new(new_id).unwrap(); + } + } + } +} + +// TODO: is there a simpler way? +fn string_hash(s: &str) -> u64 { + let mut h = std::collections::hash_map::DefaultHasher::new(); + s.hash(&mut h); + h.finish() +} + +impl<'a, 'input: 'a> SvgNode<'a, 'input> { + pub(crate) fn convert_length( + &self, + aid: AId, + object_units: Units, + state: &State, + def: Length, + ) -> f32 { + units::convert_length( + self.attribute(aid).unwrap_or(def), + *self, + aid, + object_units, + state, + ) + } + + pub fn convert_user_length(&self, aid: AId, state: &State, def: Length) -> f32 { + self.convert_length(aid, Units::UserSpaceOnUse, state, def) + } + + pub fn parse_viewbox(&self) -> Option { + let vb: svgtypes::ViewBox = self.attribute(AId::ViewBox)?; + NonZeroRect::from_xywh(vb.x as f32, vb.y as f32, vb.w as f32, vb.h as f32) + } + + pub fn resolve_length(&self, aid: AId, state: &State, def: f32) -> f32 { + debug_assert!( + !matches!(aid, AId::BaselineShift | AId::FontSize), + "{} cannot be resolved via this function", + aid + ); + + if let Some(n) = self.ancestors().find(|n| n.has_attribute(aid)) { + if let Some(length) = n.attribute(aid) { + return units::convert_user_length(length, n, aid, state); + } + } + + def + } + + pub fn resolve_valid_length( + &self, + aid: AId, + state: &State, + def: f32, + ) -> Option { + let n = self.resolve_length(aid, state, def); + NonZeroPositiveF32::new(n) + } + + pub(crate) fn try_convert_length( + &self, + aid: AId, + object_units: Units, + state: &State, + ) -> Option { + Some(units::convert_length( + self.attribute(aid)?, + *self, + aid, + object_units, + state, + )) + } + + pub fn has_valid_transform(&self, aid: AId) -> bool { + // Do not use Node::attribute::, because it will always + // return a valid transform. + + let attr = match self.attribute(aid) { + Some(attr) => attr, + None => return true, + }; + + let ts = match svgtypes::Transform::from_str(attr) { + Ok(v) => v, + Err(_) => return true, + }; + + let ts = Transform::from_row( + ts.a as f32, + ts.b as f32, + ts.c as f32, + ts.d as f32, + ts.e as f32, + ts.f as f32, + ); + ts.is_valid() + } + + pub fn is_visible_element(&self, opt: &crate::Options) -> bool { + self.attribute(AId::Display) != Some("none") + && self.has_valid_transform(AId::Transform) + && super::switch::is_condition_passed(*self, opt) + } +} + +pub trait SvgColorExt { + fn split_alpha(self) -> (Color, Opacity); +} + +impl SvgColorExt for svgtypes::Color { + fn split_alpha(self) -> (Color, Opacity) { + ( + Color::new_rgb(self.red, self.green, self.blue), + Opacity::new_u8(self.alpha), + ) + } +} + +/// Converts an input `Document` into a `Tree`. +/// +/// # Errors +/// +/// - If `Document` doesn't have an SVG node - returns an empty tree. +/// - If `Document` doesn't have a valid size - returns `Error::InvalidSize`. +pub(crate) fn convert_doc( + svg_doc: &svgtree::Document, + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, +) -> Result { + let svg = svg_doc.root_element(); + let (size, restore_viewbox) = resolve_svg_size( + &svg, + opt, + #[cfg(feature = "text")] + fontdb, + ); + let size = size?; + let view_box = ViewBox { + rect: svg + .parse_viewbox() + .unwrap_or_else(|| size.to_non_zero_rect(0.0, 0.0)), + aspect: svg.attribute(AId::PreserveAspectRatio).unwrap_or_default(), + }; + + let mut tree = Tree { + size, + view_box, + root: Group::empty(), + linear_gradients: Vec::new(), + radial_gradients: Vec::new(), + patterns: Vec::new(), + clip_paths: Vec::new(), + masks: Vec::new(), + filters: Vec::new(), + }; + + if !svg.is_visible_element(opt) { + return Ok(tree); + } + + let state = State { + parent_clip_path: None, + context_element: None, + parent_markers: Vec::new(), + fe_image_link: false, + view_box: view_box.rect, + use_size: (None, None), + opt, + #[cfg(feature = "text")] + fontdb, + }; + + let mut cache = Cache::default(); + + for node in svg_doc.descendants() { + if let Some(tag) = node.tag_name() { + if matches!( + tag, + EId::ClipPath + | EId::Filter + | EId::LinearGradient + | EId::Mask + | EId::Pattern + | EId::RadialGradient + ) { + if !node.element_id().is_empty() { + cache.all_ids.insert(string_hash(node.element_id())); + } + } + } + } + + convert_children(svg_doc.root(), &state, &mut cache, &mut tree.root); + + // Clear cache to make sure that all `Arc` objects have a single strong reference. + cache.clip_paths.clear(); + cache.masks.clear(); + cache.filters.clear(); + cache.paint.clear(); + + super::paint_server::update_paint_servers( + &mut tree.root, + Transform::default(), + None, + None, + &mut cache, + ); + tree.collect_paint_servers(); + tree.root.collect_clip_paths(&mut tree.clip_paths); + tree.root.collect_masks(&mut tree.masks); + tree.root.collect_filters(&mut tree.filters); + tree.root.calculate_bounding_boxes(); + + if restore_viewbox { + calculate_svg_bbox(&mut tree); + } + + Ok(tree) +} + +fn resolve_svg_size( + svg: &SvgNode, + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, +) -> (Result, bool) { + let mut state = State { + parent_clip_path: None, + context_element: None, + parent_markers: Vec::new(), + fe_image_link: false, + view_box: NonZeroRect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), + use_size: (None, None), + opt, + #[cfg(feature = "text")] + fontdb, + }; + + let def = Length::new(100.0, Unit::Percent); + let mut width: Length = svg.attribute(AId::Width).unwrap_or(def); + let mut height: Length = svg.attribute(AId::Height).unwrap_or(def); + + let view_box = svg.parse_viewbox(); + + let restore_viewbox = + if (width.unit == Unit::Percent || height.unit == Unit::Percent) && view_box.is_none() { + // Apply the percentages to the fallback size. + if width.unit == Unit::Percent { + width = Length::new( + (width.number / 100.0) * state.opt.default_size.width() as f64, + Unit::None, + ); + } + + if height.unit == Unit::Percent { + height = Length::new( + (height.number / 100.0) * state.opt.default_size.height() as f64, + Unit::None, + ); + } + + true + } else { + false + }; + + let size = if let Some(vbox) = view_box { + state.view_box = vbox; + + let w = if width.unit == Unit::Percent { + vbox.width() * (width.number as f32 / 100.0) + } else { + svg.convert_user_length(AId::Width, &state, def) + }; + + let h = if height.unit == Unit::Percent { + vbox.height() * (height.number as f32 / 100.0) + } else { + svg.convert_user_length(AId::Height, &state, def) + }; + + Size::from_wh(w, h) + } else { + Size::from_wh( + svg.convert_user_length(AId::Width, &state, def), + svg.convert_user_length(AId::Height, &state, def), + ) + }; + + (size.ok_or(Error::InvalidSize), restore_viewbox) +} + +/// Calculates SVG's size and viewBox in case there were not set. +/// +/// Simply iterates over all nodes and calculates a bounding box. +fn calculate_svg_bbox(tree: &mut Tree) { + let bbox = tree.root.abs_bounding_box(); + + if let Some(rect) = NonZeroRect::from_xywh(0.0, 0.0, bbox.right(), bbox.bottom()) { + tree.view_box.rect = rect; + } + + if let Some(size) = Size::from_wh(bbox.right(), bbox.bottom()) { + tree.size = size; + } +} + +#[inline(never)] +pub(crate) fn convert_children( + parent_node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + for node in parent_node.children() { + convert_element(node, state, cache, parent); + } +} + +#[inline(never)] +pub(crate) fn convert_element(node: SvgNode, state: &State, cache: &mut Cache, parent: &mut Group) { + let tag_name = match node.tag_name() { + Some(v) => v, + None => return, + }; + + if !tag_name.is_graphic() && !matches!(tag_name, EId::G | EId::Switch | EId::Svg) { + return; + } + + if !node.is_visible_element(state.opt) { + return; + } + + if tag_name == EId::Use { + super::use_node::convert(node, state, cache, parent); + return; + } + + if tag_name == EId::Switch { + super::switch::convert(node, state, cache, parent); + return; + } + + if let Some(g) = convert_group(node, state, false, cache, parent, &|cache, g| { + convert_element_impl(tag_name, node, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); + } +} + +#[inline(never)] +fn convert_element_impl( + tag_name: EId, + node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + match tag_name { + EId::Rect + | EId::Circle + | EId::Ellipse + | EId::Line + | EId::Polyline + | EId::Polygon + | EId::Path => { + if let Some(path) = super::shapes::convert(node, state) { + convert_path(node, path, state, cache, parent); + } + } + EId::Image => { + super::image::convert(node, state, parent); + } + EId::Text => { + #[cfg(feature = "text")] + { + super::text::convert(node, state, cache, parent); + } + } + EId::Svg => { + if node.parent_element().is_some() { + super::use_node::convert_svg(node, state, cache, parent); + } else { + // Skip root `svg`. + convert_children(node, state, cache, parent); + } + } + EId::G => { + convert_children(node, state, cache, parent); + } + _ => {} + } +} + +// `clipPath` can have only shape and `text` children. +// +// `line` doesn't impact rendering because stroke is always disabled +// for `clipPath` children. +#[inline(never)] +pub(crate) fn convert_clip_path_elements( + clip_node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + for node in clip_node.children() { + let tag_name = match node.tag_name() { + Some(v) => v, + None => continue, + }; + + if !tag_name.is_graphic() { + continue; + } + + if !node.is_visible_element(state.opt) { + continue; + } + + if tag_name == EId::Use { + super::use_node::convert(node, state, cache, parent); + continue; + } + + if let Some(g) = convert_group(node, state, false, cache, parent, &|cache, g| { + convert_clip_path_elements_impl(tag_name, node, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); + } + } +} + +#[inline(never)] +fn convert_clip_path_elements_impl( + tag_name: EId, + node: SvgNode, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + match tag_name { + EId::Rect | EId::Circle | EId::Ellipse | EId::Polyline | EId::Polygon | EId::Path => { + if let Some(path) = super::shapes::convert(node, state) { + convert_path(node, path, state, cache, parent); + } + } + EId::Text => { + #[cfg(feature = "text")] + { + super::text::convert(node, state, cache, parent); + } + } + _ => { + log::warn!("'{}' is no a valid 'clip-path' child.", tag_name); + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +enum Isolation { + Auto, + Isolate, +} + +impl Default for Isolation { + fn default() -> Self { + Self::Auto + } +} + +impl<'a, 'input: 'a> FromValue<'a, 'input> for Isolation { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + match value { + "auto" => Some(Isolation::Auto), + "isolate" => Some(Isolation::Isolate), + _ => None, + } + } +} + +// TODO: explain +pub(crate) fn convert_group( + node: SvgNode, + state: &State, + force: bool, + cache: &mut Cache, + parent: &mut Group, + collect_children: &dyn Fn(&mut Cache, &mut Group), +) -> Option { + // A `clipPath` child cannot have an opacity. + let opacity = if state.parent_clip_path.is_none() { + node.attribute::(AId::Opacity) + .unwrap_or(Opacity::ONE) + } else { + Opacity::ONE + }; + + let transform = node.resolve_transform(AId::Transform, state); + let blend_mode: BlendMode = node.attribute(AId::MixBlendMode).unwrap_or_default(); + let isolation: Isolation = node.attribute(AId::Isolation).unwrap_or_default(); + let isolate = isolation == Isolation::Isolate; + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let is_g_or_use = matches!(node.tag_name(), Some(EId::G) | Some(EId::Use)); + let id = if is_g_or_use && state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + #[cfg(feature = "class")] + let class = node.class().to_string(); + + let abs_transform = parent.abs_transform.pre_concat(transform); + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + let mut g = Group { + id, + #[cfg(feature = "class")] + class, + transform, + abs_transform, + opacity, + blend_mode, + isolate, + clip_path: None, + mask: None, + filters: Vec::new(), + is_context_element: false, + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + abs_layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + children: Vec::new(), + }; + collect_children(cache, &mut g); + + // We need to know group's bounding box before converting + // clipPaths, masks and filters. + let object_bbox = g.calculate_object_bbox(); + + // `mask` and `filter` cannot be set on `clipPath` children. + // But `clip-path` can. + + let mut clip_path = None; + if let Some(link) = node.attribute::(AId::ClipPath) { + clip_path = super::clippath::convert(link, state, object_bbox, cache); + if clip_path.is_none() { + return None; + } + } + + let mut mask = None; + if state.parent_clip_path.is_none() { + if let Some(link) = node.attribute::(AId::Mask) { + mask = super::mask::convert(link, state, object_bbox, cache); + if mask.is_none() { + return None; + } + } + } + + let filters = { + let mut filters = Vec::new(); + if state.parent_clip_path.is_none() { + if node.attribute(AId::Filter) == Some("none") { + // Do nothing. + } else if node.has_attribute(AId::Filter) { + if let Ok(f) = super::filter::convert(node, state, object_bbox, cache) { + filters = f; + } else { + // A filter that not a link or a filter with a link to a non existing element. + // + // Unlike `clip-path` and `mask`, when a `filter` link is invalid + // then the whole element should be ignored. + // + // This is kinda an undefined behaviour. + // In most cases, Chrome, Firefox and rsvg will ignore such elements, + // but in some cases Chrome allows it. Not sure why. + // Inkscape (0.92) simply ignores such attributes, rendering element as is. + // Batik (1.12) crashes. + // + // Test file: e-filter-051.svg + return None; + } + } + } + + filters + }; + + let required = opacity.get().approx_ne_ulps(&1.0, 4) + || clip_path.is_some() + || mask.is_some() + || !filters.is_empty() + || !transform.is_identity() + || blend_mode != BlendMode::Normal + || isolate + || is_g_or_use + || force; + + if !required { + parent.children.append(&mut g.children); + return None; + } + + g.clip_path = clip_path; + g.mask = mask; + g.filters = filters; + + // Must be called after we set Group::filters + g.calculate_bounding_boxes(); + + Some(g) +} + +fn convert_path( + node: SvgNode, + tiny_skia_path: Arc, + state: &State, + cache: &mut Cache, + parent: &mut Group, +) { + debug_assert!(tiny_skia_path.len() >= 2); + if tiny_skia_path.len() < 2 { + return; + } + + let has_bbox = tiny_skia_path.bounds().width() > 0.0 && tiny_skia_path.bounds().height() > 0.0; + let mut fill = super::style::resolve_fill(node, has_bbox, state, cache); + let mut stroke = super::style::resolve_stroke(node, has_bbox, state, cache); + let mut visibility: Visibility = node.find_attribute(AId::Visibility).unwrap_or_default(); + let rendering_mode: ShapeRendering = node + .find_attribute(AId::ShapeRendering) + .unwrap_or(state.opt.shape_rendering); + + // TODO: handle `markers` before `stroke` + let raw_paint_order: svgtypes::PaintOrder = + node.find_attribute(AId::PaintOrder).unwrap_or_default(); + let paint_order = svg_paint_order_to_usvg(raw_paint_order); + let path_transform = parent.abs_transform; + + // If a path doesn't have a fill or a stroke then it's invisible. + // By setting `visibility` to `hidden` we are disabling rendering of this path. + if fill.is_none() && stroke.is_none() { + visibility = Visibility::Hidden; + } + + if let Some(fill) = fill.as_mut() { + if let Some(ContextElement::PathNode(context_transform, context_bbox)) = + fill.context_element + { + process_paint( + &mut fill.paint, + true, + context_transform, + context_bbox.map(|r| r.to_rect()), + path_transform, + tiny_skia_path.bounds(), + cache, + ); + fill.context_element = None; + } + } + + if let Some(stroke) = stroke.as_mut() { + if let Some(ContextElement::PathNode(context_transform, context_bbox)) = + stroke.context_element + { + process_paint( + &mut stroke.paint, + true, + context_transform, + context_bbox.map(|r| r.to_rect()), + path_transform, + tiny_skia_path.bounds(), + cache, + ); + stroke.context_element = None; + } + } + + let mut marker = None; + if marker::is_valid(node) && visibility == Visibility::Visible { + let mut marker_group = Group::empty(); + let mut marker_state = state.clone(); + + let bbox = tiny_skia_path + .compute_tight_bounds() + .and_then(|r| r.to_non_zero_rect()); + + let fill = fill.clone().map(|mut f| { + f.context_element = Some(ContextElement::PathNode(path_transform, bbox)); + f + }); + + let stroke = stroke.clone().map(|mut s| { + s.context_element = Some(ContextElement::PathNode(path_transform, bbox)); + s + }); + + marker_state.context_element = Some((fill, stroke)); + + marker::convert( + node, + &tiny_skia_path, + &marker_state, + cache, + &mut marker_group, + ); + marker_group.calculate_bounding_boxes(); + marker = Some(marker_group); + } + + // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. + let id = if state.parent_markers.is_empty() { + node.element_id().to_string() + } else { + String::new() + }; + #[cfg(feature = "class")] + let class = node.class().to_string(); + + let path = Path::new( + id, + #[cfg(feature = "class")] + class, + visibility, + fill, + stroke, + paint_order, + rendering_mode, + tiny_skia_path, + path_transform, + ); + + let path = match path { + Some(v) => v, + None => return, + }; + + match raw_paint_order.order { + [PaintOrderKind::Markers, _, _] => { + if let Some(markers_node) = marker { + parent.children.push(Node::Group(Box::new(markers_node))); + } + + parent.children.push(Node::Path(Box::new(path.clone()))); + } + [first, PaintOrderKind::Markers, last] => { + append_single_paint_path(first, &path, parent); + + if let Some(markers_node) = marker { + parent.children.push(Node::Group(Box::new(markers_node))); + } + + append_single_paint_path(last, &path, parent); + } + [_, _, PaintOrderKind::Markers] => { + parent.children.push(Node::Path(Box::new(path.clone()))); + + if let Some(markers_node) = marker { + parent.children.push(Node::Group(Box::new(markers_node))); + } + } + _ => parent.children.push(Node::Path(Box::new(path.clone()))), + } +} + +fn append_single_paint_path(paint_order_kind: PaintOrderKind, path: &Path, parent: &mut Group) { + match paint_order_kind { + PaintOrderKind::Fill => { + if path.fill.is_some() { + let mut fill_path = path.clone(); + fill_path.stroke = None; + fill_path.id = String::new(); + parent.children.push(Node::Path(Box::new(fill_path))); + } + } + PaintOrderKind::Stroke => { + if path.stroke.is_some() { + let mut stroke_path = path.clone(); + stroke_path.fill = None; + stroke_path.id = String::new(); + parent.children.push(Node::Path(Box::new(stroke_path))); + } + } + _ => {} + } +} + +pub fn svg_paint_order_to_usvg(order: svgtypes::PaintOrder) -> PaintOrder { + match (order.order[0], order.order[1]) { + (svgtypes::PaintOrderKind::Stroke, _) => PaintOrder::StrokeAndFill, + (svgtypes::PaintOrderKind::Markers, svgtypes::PaintOrderKind::Stroke) => { + PaintOrder::StrokeAndFill + } + _ => PaintOrder::FillAndStroke, + } +} + +impl SvgNode<'_, '_> { + pub(crate) fn resolve_transform(&self, transform_aid: AId, state: &State) -> Transform { + let mut transform: Transform = self.attribute(transform_aid).unwrap_or_default(); + let transform_origin: Option = self.attribute(AId::TransformOrigin); + + if let Some(transform_origin) = transform_origin { + let dx = convert_length( + transform_origin.x_offset, + *self, + AId::Width, + Units::UserSpaceOnUse, + state, + ); + let dy = convert_length( + transform_origin.y_offset, + *self, + AId::Height, + Units::UserSpaceOnUse, + state, + ); + transform = Transform::default() + .pre_translate(dx, dy) + .pre_concat(transform) + .pre_translate(-dx, -dy); + } + + transform + } +} diff --git a/crates/usvg-parser/src/filter.rs b/crates/usvg/src/parser/filter.rs similarity index 79% rename from crates/usvg-parser/src/filter.rs rename to crates/usvg/src/parser/filter.rs index e73637ead..9bba4adc4 100644 --- a/crates/usvg-parser/src/filter.rs +++ b/crates/usvg/src/parser/filter.rs @@ -5,27 +5,28 @@ //! A collection of SVG filters. use std::collections::HashSet; -use std::rc::Rc; use std::str::FromStr; +use std::sync::Arc; use strict_num::PositiveF32; use svgtypes::{Length, LengthUnit as Unit}; -use usvg_tree::filter::*; -use usvg_tree::{ - strict_num, ApproxZeroUlps, Color, Group, Node, NodeKind, NonZeroF32, NonZeroRect, Opacity, + +use crate::{ + filter::{self, *}, + ApproxZeroUlps, Color, Group, Node, NonEmptyString, NonZeroF32, NonZeroRect, Opacity, Size, Units, }; -use crate::converter::SvgColorExt; -use crate::paint_server::{convert_units, resolve_number}; -use crate::svgtree::{AId, EId, FromValue, SvgNode}; -use crate::{converter, OptionLog}; +use super::converter::{self, SvgColorExt}; +use super::paint_server::{convert_units, resolve_number}; +use super::svgtree::{AId, EId, FromValue, SvgNode}; +use super::OptionLog; -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::filter::ColorInterpolation { +impl<'a, 'input: 'a> FromValue<'a, 'input> for filter::ColorInterpolation { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "sRGB" => Some(usvg_tree::filter::ColorInterpolation::SRGB), - "linearRGB" => Some(usvg_tree::filter::ColorInterpolation::LinearRGB), + "sRGB" => Some(filter::ColorInterpolation::SRGB), + "linearRGB" => Some(filter::ColorInterpolation::LinearRGB), _ => None, } } @@ -34,8 +35,9 @@ impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::filter::ColorInterpola pub(crate) fn convert( node: SvgNode, state: &converter::State, + object_bbox: Option, cache: &mut converter::Cache, -) -> Result>, ()> { +) -> Result>, ()> { let value = match node.attribute::<&str>(AId::Filter) { Some(v) => v, None => return Ok(Vec::new()), @@ -45,28 +47,36 @@ pub(crate) fn convert( let mut filters = Vec::new(); let create_base_filter_func = - |kind, filters: &mut Vec>, cache: &mut converter::Cache| { + |kind, filters: &mut Vec>, cache: &mut converter::Cache| { // Filter functions, unlike `filter` elements, do not have a filter region. // We're currently do not support an unlimited region, so we simply use a fairly large one. // This if far from ideal, but good for now. // TODO: Should be fixed eventually. - let rect = match kind { + let mut rect = match kind { Kind::DropShadow(_) | Kind::GaussianBlur(_) => { NonZeroRect::from_xywh(-0.5, -0.5, 2.0, 2.0).unwrap() } _ => NonZeroRect::from_xywh(-0.1, -0.1, 1.2, 1.2).unwrap(), }; - filters.push(Rc::new(Filter { + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!( + "Filter '{}' has an invalid region. Skipped.", + node.element_id() + ); + return; + } + }; + + rect = rect.bbox_transform(object_bbox); + + filters.push(Arc::new(Filter { id: cache.gen_filter_id(), - units: Units::ObjectBoundingBox, - primitive_units: Units::UserSpaceOnUse, rect, primitives: vec![Primitive { - x: None, - y: None, - width: None, - height: None, + rect: rect, // Unlike `filter` elements, filter functions use sRGB colors by default. color_interpolation: ColorInterpolation::SRGB, result: "result".to_string(), @@ -127,7 +137,7 @@ pub(crate) fn convert( } svgtypes::FilterValue::Url(url) => { if let Some(link) = node.document().element_by_id(url) { - if let Ok(res) = convert_url(link, state, cache) { + if let Ok(res) = convert_url(link, state, object_bbox, cache) { if let Some(f) = res { filters.push(f); } @@ -155,15 +165,24 @@ pub(crate) fn convert( fn convert_url( node: SvgNode, state: &converter::State, + object_bbox: Option, cache: &mut converter::Cache, -) -> Result>, ()> { - if let Some(filter) = cache.filters.get(node.element_id()) { - return Ok(Some(filter.clone())); - } - +) -> Result>, ()> { let units = convert_units(node, AId::FilterUnits, Units::ObjectBoundingBox); let primitive_units = convert_units(node, AId::PrimitiveUnits, Units::UserSpaceOnUse); + // Check if this element was already converted. + // + // Only `userSpaceOnUse` clipPaths can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse && primitive_units == Units::UserSpaceOnUse; + if cacheable { + if let Some(filter) = cache.filters.get(node.element_id()) { + return Ok(Some(filter.clone())); + } + } + let rect = NonZeroRect::from_xywh( resolve_number( node, @@ -194,7 +213,8 @@ fn convert_url( Length::new(120.0, Unit::Percent), ), ); - let rect = rect + + let mut rect = rect .log_none(|| { log::warn!( "Filter '{}' has an invalid region. Skipped.", @@ -203,26 +223,45 @@ fn convert_url( }) .ok_or(())?; + if units == Units::ObjectBoundingBox { + if let Some(object_bbox) = object_bbox { + rect = rect.bbox_transform(object_bbox); + } else { + log::warn!("Filters on zero-sized shapes are not allowed."); + return Err(()); + } + } + let node_with_primitives = match find_filter_with_primitives(node) { Some(v) => v, None => return Err(()), }; - let primitives = collect_children(&node_with_primitives, primitive_units, state, cache); + let primitives = collect_children( + &node_with_primitives, + primitive_units, + state, + object_bbox, + rect, + cache, + ); if primitives.is_empty() { return Err(()); } - let filter = Rc::new(Filter { - id: node.element_id().to_string(), - units, - primitive_units, + let mut id = NonEmptyString::new(node.element_id().to_string()).ok_or(())?; + // Generate ID only when we're parsing `objectBoundingBox` filter for the second time. + if !cacheable && cache.filters.contains_key(id.get()) { + id = cache.gen_filter_id(); + } + let id_copy = id.get().to_string(); + + let filter = Arc::new(Filter { + id, rect, primitives, }); - cache - .filters - .insert(node.element_id().to_string(), filter.clone()); + cache.filters.insert(id_copy, filter.clone()); Ok(Some(filter)) } @@ -255,6 +294,8 @@ fn collect_children( filter: &SvgNode, units: Units, state: &converter::State, + object_bbox: Option, + filter_region: NonZeroRect, cache: &mut converter::Cache, ) -> Vec { let mut primitives = Vec::new(); @@ -264,6 +305,17 @@ fn collect_children( idx: 1, }; + let scale = if units == Units::ObjectBoundingBox { + if let Some(object_bbox) = object_bbox { + object_bbox.size() + } else { + // No need to warn. Already checked. + return Vec::new(); + } + } else { + Size::from_wh(1.0, 1.0).unwrap() + }; + for child in filter.children() { let tag_name = match child.tag_name() { Some(v) => v, @@ -272,9 +324,9 @@ fn collect_children( let kind = match tag_name { - EId::FeDropShadow => convert_drop_shadow(child, &primitives), - EId::FeGaussianBlur => convert_gaussian_blur(child, &primitives), - EId::FeOffset => convert_offset(child, &primitives), + EId::FeDropShadow => convert_drop_shadow(child, scale, &primitives), + EId::FeGaussianBlur => convert_gaussian_blur(child, scale, &primitives), + EId::FeOffset => convert_offset(child, scale, &primitives), EId::FeBlend => convert_blend(child, &primitives), EId::FeFlood => convert_flood(child), EId::FeComposite => convert_composite(child, &primitives), @@ -285,8 +337,8 @@ fn collect_children( EId::FeColorMatrix => convert_color_matrix(child, &primitives), EId::FeConvolveMatrix => convert_convolve_matrix(child, &primitives) .unwrap_or_else(create_dummy_primitive), - EId::FeMorphology => convert_morphology(child, &primitives), - EId::FeDisplacementMap => convert_displacement_map(child, &primitives), + EId::FeMorphology => convert_morphology(child, scale, &primitives), + EId::FeDisplacementMap => convert_displacement_map(child, scale, &primitives), EId::FeTurbulence => convert_turbulence(child), EId::FeDiffuseLighting => convert_diffuse_lighting(child, &primitives) .unwrap_or_else(create_dummy_primitive), @@ -298,8 +350,17 @@ fn collect_children( } }; - let fe = convert_primitive(child, kind, units, state, &mut results); - primitives.push(fe); + if let Some(fe) = convert_primitive( + child, + kind, + units, + state, + object_bbox, + filter_region, + &mut results, + ) { + primitives.push(fe); + } } // TODO: remove primitives which results are not used @@ -312,19 +373,79 @@ fn convert_primitive( kind: Kind, units: Units, state: &converter::State, + bbox: Option, + filter_region: NonZeroRect, results: &mut FilterResults, -) -> Primitive { - Primitive { - x: fe.try_convert_length(AId::X, units, state), - y: fe.try_convert_length(AId::Y, units, state), - // TODO: validate and test - width: fe.try_convert_length(AId::Width, units, state), - height: fe.try_convert_length(AId::Height, units, state), - color_interpolation: fe - .find_attribute(AId::ColorInterpolationFilters) - .unwrap_or_default(), +) -> Option { + let rect = resolve_primitive_region(fe, &kind, units, state, bbox, filter_region)?; + + let color_interpolation = fe + .find_attribute(AId::ColorInterpolationFilters) + .unwrap_or_default(); + + Some(Primitive { + rect, + color_interpolation, result: gen_result(fe, results), kind, + }) +} + +// TODO: rewrite/simplify/explain/whatever +fn resolve_primitive_region( + fe: SvgNode, + kind: &Kind, + units: Units, + state: &converter::State, + bbox: Option, + filter_region: NonZeroRect, +) -> Option { + let x = fe.try_convert_length(AId::X, units, state); + let y = fe.try_convert_length(AId::Y, units, state); + let width = fe.try_convert_length(AId::Width, units, state); + let height = fe.try_convert_length(AId::Height, units, state); + + let region = match kind { + Kind::Flood(..) | Kind::Image(..) => { + // `feImage` uses the object bbox. + if units == Units::ObjectBoundingBox { + let bbox = bbox?; + + // TODO: wrong + // let ts_bbox = tiny_skia::Rect::new(ts.e, ts.f, ts.a, ts.d).unwrap(); + + let r = NonZeroRect::from_xywh( + x.unwrap_or(0.0), + y.unwrap_or(0.0), + width.unwrap_or(1.0), + height.unwrap_or(1.0), + )?; + + return Some(r.bbox_transform(bbox)); + } else { + filter_region + } + } + _ => filter_region, + }; + + // TODO: Wrong! Does not account rotate and skew. + if units == Units::ObjectBoundingBox { + let subregion_bbox = NonZeroRect::from_xywh( + x.unwrap_or(0.0), + y.unwrap_or(0.0), + width.unwrap_or(1.0), + height.unwrap_or(1.0), + )?; + + Some(region.bbox_transform(subregion_bbox)) + } else { + NonZeroRect::from_xywh( + x.unwrap_or(region.x()), + y.unwrap_or(region.y()), + width.unwrap_or(region.width()), + height.unwrap_or(region.height()), + ) } } @@ -604,7 +725,7 @@ fn convert_convolve_matrix(fe: SvgNode, primitives: &[Primitive]) -> Option Kind { +fn convert_displacement_map(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { let parse_channel = |aid| match fe.attribute(aid).unwrap_or("A") { "R" => ColorChannel::R, "G" => ColorChannel::G, @@ -612,17 +733,21 @@ fn convert_displacement_map(fe: SvgNode, primitives: &[Primitive]) -> Kind { _ => ColorChannel::A, }; + // TODO: should probably split scale to scale_x and scale_y, + // but resvg doesn't support displacement map anyway... + let scale = (scale.width() + scale.height()) / 2.0; + Kind::DisplacementMap(DisplacementMap { input1: resolve_input(fe, AId::In, primitives), input2: resolve_input(fe, AId::In2, primitives), - scale: fe.attribute(AId::Scale).unwrap_or(0.0), + scale: fe.attribute(AId::Scale).unwrap_or(0.0) * scale, x_channel_selector: parse_channel(AId::XChannelSelector), y_channel_selector: parse_channel(AId::YChannelSelector), }) } -fn convert_drop_shadow(fe: SvgNode, primitives: &[Primitive]) -> Kind { - let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, "2 2"); +fn convert_drop_shadow(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, scale, "2 2"); let (color, opacity) = fe .attribute(AId::FloodColor) @@ -635,8 +760,8 @@ fn convert_drop_shadow(fe: SvgNode, primitives: &[Primitive]) -> Kind { Kind::DropShadow(DropShadow { input: resolve_input(fe, AId::In, primitives), - dx: fe.attribute(AId::Dx).unwrap_or(2.0), - dy: fe.attribute(AId::Dy).unwrap_or(2.0), + dx: fe.attribute(AId::Dx).unwrap_or(2.0) * scale.width(), + dy: fe.attribute(AId::Dy).unwrap_or(2.0) * scale.height(), std_dev_x, std_dev_y, color, @@ -660,8 +785,8 @@ fn convert_flood(fe: SvgNode) -> Kind { }) } -fn convert_gaussian_blur(fe: SvgNode, primitives: &[Primitive]) -> Kind { - let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, "0 0"); +fn convert_gaussian_blur(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { + let (std_dev_x, std_dev_y) = convert_std_dev_attr(fe, scale, "0 0"); Kind::GaussianBlur(GaussianBlur { input: resolve_input(fe, AId::In, primitives), std_dev_x, @@ -669,7 +794,7 @@ fn convert_gaussian_blur(fe: SvgNode, primitives: &[Primitive]) -> Kind { }) } -fn convert_std_dev_attr(fe: SvgNode, default: &str) -> (PositiveF32, PositiveF32) { +fn convert_std_dev_attr(fe: SvgNode, scale: Size, default: &str) -> (PositiveF32, PositiveF32) { let text = fe.attribute(AId::StdDeviation).unwrap_or(default); let mut parser = svgtypes::NumberListParser::from(text); @@ -685,6 +810,9 @@ fn convert_std_dev_attr(fe: SvgNode, default: &str) -> (PositiveF32, PositiveF32 _ => (0.0, 0.0), }; + let std_dev_x = (std_dev_x as f32) * scale.width(); + let std_dev_y = (std_dev_y as f32) * scale.height(); + let std_dev_x = PositiveF32::new(std_dev_x as f32).unwrap_or(PositiveF32::ZERO); let std_dev_y = PositiveF32::new(std_dev_y as f32).unwrap_or(PositiveF32::ZERO); @@ -697,24 +825,37 @@ fn convert_image(fe: SvgNode, state: &converter::State, cache: &mut converter::C .find_attribute(AId::ImageRendering) .unwrap_or(state.opt.image_rendering); - if let Some(node) = fe.attribute::(AId::Href) { + if let Some(node) = fe.try_attribute::(AId::Href) { let mut state = state.clone(); state.fe_image_link = true; - let mut root = Node::new(NodeKind::Group(Group::default())); - crate::converter::convert_element(node, &state, cache, &mut root); - return if let Some(node) = root.first_child() { - node.detach(); // drops `root` node + let mut root = Group::empty(); + super::converter::convert_element(node, &state, cache, &mut root); + return if root.has_children() { + root.calculate_bounding_boxes(); + // Transfer node id from group's child to the group itself if needed. + if let Some(Node::Group(ref mut g)) = root.children.first_mut() { + if let Some(child2) = g.children.first_mut() { + g.id = child2.id().to_string(); + match child2 { + Node::Group(ref mut g2) => g2.id.clear(), + Node::Path(ref mut path) => path.id.clear(), + Node::Image(ref mut image) => image.id.clear(), + Node::Text(ref mut text) => text.id.clear(), + } + } + } + Kind::Image(Image { aspect, rendering_mode, - data: ImageKind::Use(node), + data: ImageKind::Use(Box::new(root)), }) } else { create_dummy_primitive() }; } - let href = match fe.attribute(AId::Href) { + let href = match fe.try_attribute(AId::Href) { Some(s) => s, _ => { log::warn!("The 'feImage' element lacks the 'xlink:href' attribute. Skipped."); @@ -722,7 +863,7 @@ fn convert_image(fe: SvgNode, state: &converter::State, cache: &mut converter::C } }; - let href = crate::image::get_href_data(href, state.opt); + let href = super::image::get_href_data(href, state); let img_data = match href { Some(data) => data, None => return create_dummy_primitive(), @@ -838,14 +979,14 @@ fn convert_merge(fe: SvgNode, primitives: &[Primitive]) -> Kind { Kind::Merge(Merge { inputs }) } -fn convert_morphology(fe: SvgNode, primitives: &[Primitive]) -> Kind { +fn convert_morphology(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { let operator = match fe.attribute(AId::Operator).unwrap_or("erode") { "dilate" => MorphologyOperator::Dilate, _ => MorphologyOperator::Erode, }; - let mut radius_x = PositiveF32::new(1.0).unwrap(); - let mut radius_y = PositiveF32::new(1.0).unwrap(); + let mut radius_x = PositiveF32::new(scale.width()).unwrap(); + let mut radius_y = PositiveF32::new(scale.height()).unwrap(); if let Some(list) = fe.attribute::>(AId::Radius) { let mut rx = 0.0; let mut ry = 0.0; @@ -873,8 +1014,8 @@ fn convert_morphology(fe: SvgNode, primitives: &[Primitive]) -> Kind { // Both values must be positive. if rx.is_sign_positive() && ry.is_sign_positive() { - radius_x = PositiveF32::new(rx).unwrap(); - radius_y = PositiveF32::new(ry).unwrap(); + radius_x = PositiveF32::new(rx * scale.width()).unwrap(); + radius_y = PositiveF32::new(ry * scale.height()).unwrap(); } } @@ -886,11 +1027,11 @@ fn convert_morphology(fe: SvgNode, primitives: &[Primitive]) -> Kind { }) } -fn convert_offset(fe: SvgNode, primitives: &[Primitive]) -> Kind { +fn convert_offset(fe: SvgNode, scale: Size, primitives: &[Primitive]) -> Kind { Kind::Offset(Offset { input: resolve_input(fe, AId::In, primitives), - dx: fe.attribute(AId::Dx).unwrap_or(0.0), - dy: fe.attribute(AId::Dy).unwrap_or(0.0), + dx: fe.attribute(AId::Dx).unwrap_or(0.0) * scale.width(), + dy: fe.attribute(AId::Dy).unwrap_or(0.0) * scale.height(), }) } @@ -1085,7 +1226,7 @@ fn convert_contrast_function(amount: f64) -> Kind { #[inline(never)] fn convert_blur_function(node: SvgNode, std_dev: Length, state: &converter::State) -> Kind { - let std_dev = PositiveF32::new(crate::units::convert_user_length( + let std_dev = PositiveF32::new(super::units::convert_user_length( std_dev, node, AId::Dx, @@ -1108,7 +1249,7 @@ fn convert_drop_shadow_function( std_dev: Length, state: &converter::State, ) -> Kind { - let std_dev = PositiveF32::new(crate::units::convert_user_length( + let std_dev = PositiveF32::new(super::units::convert_user_length( std_dev, node, AId::Dx, @@ -1125,8 +1266,8 @@ fn convert_drop_shadow_function( Kind::DropShadow(DropShadow { input: Input::SourceGraphic, - dx: crate::units::convert_user_length(dx, node, AId::Dx, state), - dy: crate::units::convert_user_length(dy, node, AId::Dy, state), + dx: super::units::convert_user_length(dx, node, AId::Dx, state), + dy: super::units::convert_user_length(dy, node, AId::Dy, state), std_dev_x: std_dev, std_dev_y: std_dev, color, diff --git a/crates/usvg-parser/src/image.rs b/crates/usvg/src/parser/image.rs similarity index 58% rename from crates/usvg-parser/src/image.rs rename to crates/usvg/src/parser/image.rs index c731617bd..bb8e266aa 100644 --- a/crates/usvg-parser/src/image.rs +++ b/crates/usvg/src/parser/image.rs @@ -5,15 +5,28 @@ use std::sync::Arc; use svgtypes::Length; -use usvg_tree::{Image, ImageKind, Node, NodeExt, NodeKind, NonZeroRect, Size, Tree, ViewBox}; -use crate::svgtree::{AId, SvgNode}; -use crate::{converter, OptionLog, Options, TreeParsing}; +use super::svgtree::{AId, SvgNode}; +use super::{converter, OptionLog, Options}; +use crate::{Group, Image, ImageKind, Node, NonZeroRect, Size, Tree, ViewBox}; /// A shorthand for [ImageHrefResolver]'s data function. +#[cfg(feature = "text")] +pub type ImageHrefDataResolverFn = + Box>, &Options, &fontdb::Database) -> Option + Send + Sync>; + +/// A shorthand for [ImageHrefResolver]'s data function. +#[cfg(not(feature = "text"))] pub type ImageHrefDataResolverFn = Box>, &Options) -> Option + Send + Sync>; + /// A shorthand for [ImageHrefResolver]'s string function. +#[cfg(feature = "text")] +pub type ImageHrefStringResolverFn = + Box Option + Send + Sync>; + +/// A shorthand for [ImageHrefResolver]'s string function. +#[cfg(not(feature = "text"))] pub type ImageHrefStringResolverFn = Box Option + Send + Sync>; /// An `xlink:href` resolver for `` elements. @@ -53,16 +66,29 @@ impl ImageHrefResolver { /// The actual images would not be decoded. It's up to the renderer. pub fn default_data_resolver() -> ImageHrefDataResolverFn { Box::new( - move |mime: &str, data: Arc>, opts: &Options| match mime { + move |mime: &str, + data: Arc>, + opts: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database| match mime { "image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)), "image/png" => Some(ImageKind::PNG(data)), "image/gif" => Some(ImageKind::GIF(data)), - "image/svg+xml" => load_sub_svg(&data, opts), + "image/svg+xml" => load_sub_svg( + &data, + opts, + #[cfg(feature = "text")] + fontdb, + ), "text/plain" => match get_image_data_format(&data) { Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)), Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)), Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)), - _ => load_sub_svg(&data, opts), + _ => load_sub_svg( + &data, + opts, + #[cfg(feature = "text")] + fontdb, + ), }, _ => None, }, @@ -77,33 +103,42 @@ impl ImageHrefResolver { /// Paths have to be absolute or relative to the input SVG file or relative to /// [Options::resources_dir](crate::Options::resources_dir). pub fn default_string_resolver() -> ImageHrefStringResolverFn { - Box::new(move |href: &str, opts: &Options| { - let path = opts.get_abs_path(std::path::Path::new(href)); - - if path.exists() { - let data = match std::fs::read(&path) { - Ok(data) => data, - Err(_) => { - log::warn!("Failed to load '{}'. Skipped.", href); - return None; - } - }; - - match get_image_file_format(&path, &data) { - Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), - Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), - Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), - Some(ImageFormat::SVG) => load_sub_svg(&data, opts), - _ => { - log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href); - None + Box::new( + move |href: &str, + opts: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database| { + let path = opts.get_abs_path(std::path::Path::new(href)); + + if path.exists() { + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(_) => { + log::warn!("Failed to load '{}'. Skipped.", href); + return None; + } + }; + + match get_image_file_format(&path, &data) { + Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), + Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), + Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), + Some(ImageFormat::SVG) => load_sub_svg( + &data, + opts, + #[cfg(feature = "text")] + fontdb, + ), + _ => { + log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href); + None + } } + } else { + log::warn!("'{}' is not a path to an image.", href); + None } - } else { - log::warn!("'{}' is not a path to an image.", href); - None - } - }) + }, + ) } } @@ -121,12 +156,12 @@ enum ImageFormat { SVG, } -pub(crate) fn convert(node: SvgNode, state: &converter::State, parent: &mut Node) -> Option<()> { +pub(crate) fn convert(node: SvgNode, state: &converter::State, parent: &mut Group) -> Option<()> { let href = node - .attribute(AId::Href) + .try_attribute(AId::Href) .log_none(|| log::warn!("Image lacks the 'xlink:href' attribute. Skipped."))?; - let kind = get_href_data(href, state.opt)?; + let kind = get_href_data(href, state)?; let visibility = node.find_attribute(AId::Visibility).unwrap_or_default(); let rendering_mode = node @@ -143,20 +178,35 @@ pub(crate) fn convert(node: SvgNode, state: &converter::State, parent: &mut Node ImageKind::SVG(ref svg) => svg.size, }; - let rect = NonZeroRect::from_xywh( - node.convert_user_length(AId::X, state, Length::zero()), - node.convert_user_length(AId::Y, state, Length::zero()), - node.convert_user_length( - AId::Width, - state, - Length::new_number(actual_size.width() as f64), - ), - node.convert_user_length( - AId::Height, - state, - Length::new_number(actual_size.height() as f64), - ), + let x = node.convert_user_length(AId::X, state, Length::zero()); + let y = node.convert_user_length(AId::Y, state, Length::zero()); + let mut width = node.convert_user_length( + AId::Width, + state, + Length::new_number(actual_size.width() as f64), ); + let mut height = node.convert_user_length( + AId::Height, + state, + Length::new_number(actual_size.height() as f64), + ); + + match ( + node.attribute::(AId::Width), + node.attribute::(AId::Height), + ) { + (Some(_), None) => { + // Only width was defined, so we need to scale height accordingly. + height = actual_size.height() * (width / actual_size.width()); + } + (None, Some(_)) => { + // Only height was defined, so we need to scale width accordingly. + width = actual_size.width() * (height / actual_size.height()); + } + _ => {} + }; + + let rect = NonZeroRect::from_xywh(x, y, width, height); let rect = rect.log_none(|| log::warn!("Image has an invalid size. Skipped."))?; let view_box = ViewBox { @@ -173,21 +223,24 @@ pub(crate) fn convert(node: SvgNode, state: &converter::State, parent: &mut Node #[cfg(feature = "class")] let class = node.class().to_string(); - parent.append_kind(NodeKind::Image(Image { + let abs_bounding_box = view_box.rect.transform(parent.abs_transform)?; + + parent.children.push(Node::Image(Box::new(Image { id, #[cfg(feature = "class")] class, - transform: Default::default(), visibility, view_box, rendering_mode, kind, - })); + abs_transform: parent.abs_transform, + abs_bounding_box, + }))); Some(()) } -pub(crate) fn get_href_data(href: &str, opt: &Options) -> Option { +pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option { if let Ok(url) = data_url::DataUrl::process(href) { let (data, _) = url.decode_to_vec().ok()?; @@ -197,9 +250,20 @@ pub(crate) fn get_href_data(href: &str, opt: &Options) -> Option { url.mime_type().subtype.as_str() ); - (opt.image_href_resolver.resolve_data)(&mime, Arc::new(data), opt) + (state.opt.image_href_resolver.resolve_data)( + &mime, + Arc::new(data), + state.opt, + #[cfg(feature = "text")] + state.fontdb, + ) } else { - (opt.image_href_resolver.resolve_string)(href, opt) + (state.opt.image_href_resolver.resolve_string)( + href, + state.opt, + #[cfg(feature = "text")] + state.fontdb, + ) } } @@ -228,7 +292,11 @@ fn get_image_data_format(data: &[u8]) -> Option { /// /// Unlike `Tree::from_*` methods, this one will also remove all `image` elements /// from the loaded SVG, as required by the spec. -pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option { +pub(crate) fn load_sub_svg( + data: &[u8], + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, +) -> Option { let mut sub_opt = Options::default(); sub_opt.resources_dir = None; sub_opt.dpi = opt.dpi; @@ -239,7 +307,20 @@ pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option { sub_opt.image_rendering = opt.image_rendering; sub_opt.default_size = opt.default_size; - let tree = match Tree::from_data(data, &sub_opt) { + // The referenced SVG image cannot have any 'image' elements by itself. + // Not only recursive. Any. Don't know why. + sub_opt.image_href_resolver = ImageHrefResolver { + resolve_data: Box::new(|_, _, _, #[cfg(feature = "text")] _| None), + resolve_string: Box::new(|_, _, #[cfg(feature = "text")] _| None), + }; + + let tree = Tree::from_data( + data, + &sub_opt, + #[cfg(feature = "text")] + fontdb, + ); + let tree = match tree { Ok(tree) => tree, Err(_) => { log::warn!("Failed to load subsvg image."); @@ -247,34 +328,5 @@ pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option { } }; - sanitize_sub_svg(&tree); Some(ImageKind::SVG(tree)) } - -// TODO: technically can simply override Options::image_href_resolver? -fn sanitize_sub_svg(tree: &Tree) { - // Remove all Image nodes. - // - // The referenced SVG image cannot have any 'image' elements by itself. - // Not only recursive. Any. Don't know why. - - // TODO: implement drain or something to the rctree. - let mut changed = true; - while changed { - changed = false; - - for node in tree.root.descendants() { - let mut rm = false; - // TODO: feImage? - if let NodeKind::Image(_) = *node.borrow() { - rm = true; - }; - - if rm { - node.detach(); - changed = true; - break; - } - } - } -} diff --git a/crates/usvg-parser/src/marker.rs b/crates/usvg/src/parser/marker.rs similarity index 94% rename from crates/usvg-parser/src/marker.rs rename to crates/usvg/src/parser/marker.rs index 5de79a0b4..07259a421 100644 --- a/crates/usvg-parser/src/marker.rs +++ b/crates/usvg/src/parser/marker.rs @@ -2,18 +2,18 @@ // 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/. -use std::rc::Rc; +use std::sync::Arc; use strict_num::NonZeroPositiveF32; use svgtypes::Length; use tiny_skia_path::Point; -use usvg_tree::{ - strict_num, tiny_skia_path, ApproxEqUlps, ApproxZeroUlps, ClipPath, Group, Node, NodeExt, - NodeKind, NonZeroRect, Path, Size, Transform, ViewBox, -}; -use crate::converter; -use crate::svgtree::{AId, EId, SvgNode}; +use super::converter; +use super::svgtree::{AId, EId, SvgNode}; +use crate::{ + ApproxEqUlps, ApproxZeroUlps, ClipPath, Fill, Group, Node, NonZeroRect, Path, Size, Transform, + ViewBox, +}; // Similar to `tiny_skia_path::PathSegment`, but without the `QuadTo`. #[derive(Copy, Clone, Debug)] @@ -44,7 +44,7 @@ pub(crate) fn convert( path: &tiny_skia_path::Path, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + parent: &mut Group, ) { let list = [ (AId::MarkerStart, MarkerKind::Start), @@ -93,7 +93,7 @@ fn resolve( marker_kind: MarkerKind, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + parent: &mut Group, ) -> Option<()> { let stroke_scale = stroke_scale(shape_node, marker_node, state)?.get(); @@ -119,17 +119,16 @@ fn resolve( r.size().to_non_zero_rect(0.0, 0.0) }; - let mut clip_path = ClipPath::default(); - clip_path.id = cache.gen_clip_path_id(); + let mut clip_path = ClipPath::empty(cache.gen_clip_path_id()); - let mut path = Path::new(Rc::new(tiny_skia_path::PathBuilder::from_rect( + let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( clip_rect.to_rect(), - ))); - path.fill = Some(usvg_tree::Fill::default()); + )))?; + path.fill = Some(Fill::default()); - clip_path.root.append_kind(NodeKind::Path(path)); + clip_path.root.children.push(Node::Path(Box::new(path))); - Some(Rc::new(clip_path)) + Some(Arc::new(clip_path)) } else { None }; @@ -184,7 +183,7 @@ fn resolve( if let Some(vbox) = view_box { let size = Size::from_wh(r.width() * stroke_scale, r.height() * stroke_scale).unwrap(); - let vbox_ts = usvg_tree::utils::view_box_to_transform(vbox.rect, vbox.aspect, size); + let vbox_ts = vbox.to_transform(size); let (sx, sy) = vbox_ts.get_scale(); ts = ts.pre_scale(sx, sy); } else { @@ -194,18 +193,20 @@ fn resolve( ts = ts.pre_translate(-r.x(), -r.y()); // TODO: do not create a group when no clipPath - let mut g_node = parent.append_kind(NodeKind::Group(Group { + let mut g = Group { transform: ts, + abs_transform: parent.abs_transform.pre_concat(ts), clip_path: clip_path.clone(), - ..Group::default() - })); + ..Group::empty() + }; let mut marker_state = state.clone(); marker_state.parent_markers.push(marker_node); - converter::convert_children(marker_node, &marker_state, cache, &mut g_node); + converter::convert_children(marker_node, &marker_state, cache, &mut g); + g.calculate_bounding_boxes(); - if !g_node.has_children() { - g_node.detach(); + if g.has_children() { + parent.children.push(Node::Group(Box::new(g))); } }; @@ -479,7 +480,7 @@ fn convert_orientation(node: SvgNode) -> MarkerOrientation { _ => match node.attribute::(AId::Orient) { Some(angle) => MarkerOrientation::Angle(angle.to_degrees() as f32), None => MarkerOrientation::Angle(0.0), - } + }, } } diff --git a/crates/usvg/src/parser/mask.rs b/crates/usvg/src/parser/mask.rs new file mode 100644 index 000000000..cdc88e904 --- /dev/null +++ b/crates/usvg/src/parser/mask.rs @@ -0,0 +1,151 @@ +// 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/. + +use std::sync::Arc; + +use svgtypes::{Length, LengthUnit as Unit}; + +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, OptionLog}; +use crate::{Group, Mask, MaskType, Node, NonEmptyString, NonZeroRect, Transform, Units}; + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + object_bbox: Option, + cache: &mut converter::Cache, +) -> Option> { + // A `mask` attribute must reference a `mask` element. + if node.tag_name() != Some(EId::Mask) { + return None; + } + + let units = node + .attribute(AId::MaskUnits) + .unwrap_or(Units::ObjectBoundingBox); + + let content_units = node + .attribute(AId::MaskContentUnits) + .unwrap_or(Units::UserSpaceOnUse); + + // Check if this element was already converted. + // + // Only `userSpaceOnUse` masks can be shared, + // because `objectBoundingBox` one will be converted into user one + // and will become node-specific. + let cacheable = units == Units::UserSpaceOnUse && content_units == Units::UserSpaceOnUse; + if cacheable { + if let Some(mask) = cache.masks.get(node.element_id()) { + return Some(mask.clone()); + } + } + + let rect = NonZeroRect::from_xywh( + node.convert_length(AId::X, units, state, Length::new(-10.0, Unit::Percent)), + node.convert_length(AId::Y, units, state, Length::new(-10.0, Unit::Percent)), + node.convert_length(AId::Width, units, state, Length::new(120.0, Unit::Percent)), + node.convert_length(AId::Height, units, state, Length::new(120.0, Unit::Percent)), + ); + let mut rect = + rect.log_none(|| log::warn!("Mask '{}' has an invalid size. Skipped.", node.element_id()))?; + + let mut mask_all = false; + if units == Units::ObjectBoundingBox { + if let Some(bbox) = object_bbox { + rect = rect.bbox_transform(bbox) + } else { + // When mask units are `objectBoundingBox` and bbox is zero-sized - the whole + // element should be masked. + // Technically an UB, but this is what Chrome and Firefox do. + mask_all = true; + } + } + + let mut id = NonEmptyString::new(node.element_id().to_string())?; + // Generate ID only when we're parsing `objectBoundingBox` mask for the second time. + if !cacheable && cache.masks.contains_key(id.get()) { + id = cache.gen_mask_id(); + } + let id_copy = id.get().to_string(); + + if mask_all { + let mask = Arc::new(Mask { + id, + rect, + kind: MaskType::Luminance, + mask: None, + root: Group::empty(), + }); + cache.masks.insert(id_copy, mask.clone()); + return Some(mask); + } + + // Resolve linked mask. + let mut mask = None; + if let Some(link) = node.attribute::(AId::Mask) { + mask = convert(link, state, object_bbox, cache); + + // Linked `mask` must be valid. + if mask.is_none() { + return None; + } + } + + let kind = if node.attribute(AId::MaskType) == Some("alpha") { + MaskType::Alpha + } else { + MaskType::Luminance + }; + + let mut mask = Mask { + id, + rect, + kind, + mask, + root: Group::empty(), + }; + + // To emulate content `objectBoundingBox` units we have to put + // mask children into a group with a transform. + let mut subroot = None; + if content_units == Units::ObjectBoundingBox { + let object_bbox = match object_bbox { + Some(v) => v, + None => { + log::warn!("Masking of zero-sized shapes is not allowed."); + return None; + } + }; + + let mut g = Group::empty(); + g.transform = Transform::from_bbox(object_bbox); + // Make sure to set `abs_transform`, because it must propagate to all children. + g.abs_transform = g.transform; + + subroot = Some(g); + } + + { + // Prefer `subroot` to `mask.root`. + let real_root = subroot.as_mut().unwrap_or(&mut mask.root); + converter::convert_children(node, state, cache, real_root); + + // A mask without children at this point is invalid. + // Only masks with zero bbox and `objectBoundingBox` can be empty. + if !real_root.has_children() { + return None; + } + } + + if let Some(mut subroot) = subroot { + subroot.calculate_bounding_boxes(); + mask.root.children.push(Node::Group(Box::new(subroot))); + } + + mask.root.calculate_bounding_boxes(); + + let mask = Arc::new(mask); + cache.masks.insert(id_copy, mask.clone()); + Some(mask) +} diff --git a/crates/usvg-parser/src/lib.rs b/crates/usvg/src/parser/mod.rs similarity index 68% rename from crates/usvg-parser/src/lib.rs rename to crates/usvg/src/parser/mod.rs index 64a789617..962683301 100644 --- a/crates/usvg-parser/src/lib.rs +++ b/crates/usvg/src/parser/mod.rs @@ -2,25 +2,6 @@ // 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/. -/*! -`usvg-parser` is an [SVG] parser used by [usvg]. - -[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -[usvg]: https://github.com/RazrFalcon/resvg/tree/master/crates/usvg -*/ - -#![forbid(unsafe_code)] -#![warn(missing_docs)] -#![warn(missing_debug_implementations)] -#![warn(missing_copy_implementations)] -#![allow(clippy::collapsible_else_if)] -#![allow(clippy::collapsible_if)] -#![allow(clippy::field_reassign_with_default)] -#![allow(clippy::identity_op)] -#![allow(clippy::question_mark)] -#![allow(clippy::too_many_arguments)] -#![allow(clippy::upper_case_acronyms)] - mod clippath; mod converter; mod filter; @@ -33,14 +14,15 @@ mod shapes; mod style; mod svgtree; mod switch; -mod text; mod units; mod use_node; -pub use crate::options::*; -pub use image::ImageHrefResolver; -pub use roxmltree; -pub use svgtree::{AId, EId}; +#[cfg(feature = "text")] +mod text; + +pub use image::{ImageHrefDataResolverFn, ImageHrefResolver, ImageHrefStringResolverFn}; +pub use options::Options; +pub(crate) use svgtree::{AId, EId}; /// List of all errors. #[derive(Debug)] @@ -109,37 +91,41 @@ impl OptionLog for Option { } } -/// A trait to parse `usvg_tree::Tree` from various sources. -pub trait TreeParsing: Sized { - /// Parses `Tree` from an SVG data. - /// - /// Can contain an SVG string or a gzip compressed data. - fn from_data(data: &[u8], opt: &Options) -> Result; - - /// Parses `Tree` from an SVG string. - fn from_str(text: &str, opt: &Options) -> Result; - - /// Parses `Tree` from `roxmltree::Document`. - fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result; -} - -impl TreeParsing for usvg_tree::Tree { +impl crate::Tree { /// Parses `Tree` from an SVG data. /// /// Can contain an SVG string or a gzip compressed data. - fn from_data(data: &[u8], opt: &Options) -> Result { + pub fn from_data( + data: &[u8], + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, + ) -> Result { if data.starts_with(&[0x1f, 0x8b]) { let data = decompress_svgz(data)?; let text = std::str::from_utf8(&data).map_err(|_| Error::NotAnUtf8Str)?; - Self::from_str(text, opt) + Self::from_str( + text, + opt, + #[cfg(feature = "text")] + fontdb, + ) } else { let text = std::str::from_utf8(data).map_err(|_| Error::NotAnUtf8Str)?; - Self::from_str(text, opt) + Self::from_str( + text, + opt, + #[cfg(feature = "text")] + fontdb, + ) } } /// Parses `Tree` from an SVG string. - fn from_str(text: &str, opt: &Options) -> Result { + pub fn from_str( + text: &str, + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, + ) -> Result { let xml_opt = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() @@ -148,13 +134,27 @@ impl TreeParsing for usvg_tree::Tree { let doc = roxmltree::Document::parse_with_options(text, xml_opt).map_err(Error::ParsingFailed)?; - Self::from_xmltree(&doc, opt) + Self::from_xmltree( + &doc, + opt, + #[cfg(feature = "text")] + fontdb, + ) } /// Parses `Tree` from `roxmltree::Document`. - fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result { + pub fn from_xmltree( + doc: &roxmltree::Document, + opt: &Options, + #[cfg(feature = "text")] fontdb: &fontdb::Database, + ) -> Result { let doc = svgtree::Document::parse_tree(doc)?; - crate::converter::convert_doc(&doc, opt) + self::converter::convert_doc( + &doc, + opt, + #[cfg(feature = "text")] + fontdb, + ) } } diff --git a/crates/usvg-parser/src/options.rs b/crates/usvg/src/parser/options.rs similarity index 97% rename from crates/usvg-parser/src/options.rs rename to crates/usvg/src/parser/options.rs index 5cfe1ec4b..77973e321 100644 --- a/crates/usvg-parser/src/options.rs +++ b/crates/usvg/src/parser/options.rs @@ -2,9 +2,7 @@ // 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/. -use usvg_tree::{ImageRendering, ShapeRendering, Size, TextRendering}; - -use crate::ImageHrefResolver; +use crate::{ImageHrefResolver, ImageRendering, ShapeRendering, Size, TextRendering}; /// Processing options. #[derive(Debug)] diff --git a/crates/usvg/src/parser/paint_server.rs b/crates/usvg/src/parser/paint_server.rs new file mode 100644 index 000000000..b473b0bb1 --- /dev/null +++ b/crates/usvg/src/parser/paint_server.rs @@ -0,0 +1,1065 @@ +// 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/. + +use std::str::FromStr; +use std::sync::Arc; + +use strict_num::PositiveF32; +use svgtypes::{Length, LengthUnit as Unit}; + +use super::converter::{self, Cache, SvgColorExt}; +use super::svgtree::{AId, EId, SvgNode}; +use super::OptionLog; +use crate::*; + +pub(crate) enum ServerOrColor { + Server(Paint), + Color { color: Color, opacity: Opacity }, +} + +pub(crate) fn convert( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + // Check for existing. + if let Some(paint) = cache.paint.get(node.element_id()) { + return Some(ServerOrColor::Server(paint.clone())); + } + + // Unwrap is safe, because we already checked for is_paint_server(). + let paint = match node.tag_name().unwrap() { + EId::LinearGradient => convert_linear(node, state), + EId::RadialGradient => convert_radial(node, state), + EId::Pattern => convert_pattern(node, state, cache), + _ => unreachable!(), + }; + + if let Some(ServerOrColor::Server(ref paint)) = paint { + cache + .paint + .insert(node.element_id().to_string(), paint.clone()); + } + + paint +} + +#[inline(never)] +fn convert_linear(node: SvgNode, state: &converter::State) -> Option { + let id = NonEmptyString::new(node.element_id().to_string())?; + + let stops = convert_stops(find_gradient_with_stops(node)?); + if stops.len() < 2 { + return stops_to_color(&stops); + } + + let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); + let transform = node.resolve_transform(AId::GradientTransform, state); + + let gradient = LinearGradient { + x1: resolve_number(node, AId::X1, units, state, Length::zero()), + y1: resolve_number(node, AId::Y1, units, state, Length::zero()), + x2: resolve_number( + node, + AId::X2, + units, + state, + Length::new(100.0, Unit::Percent), + ), + y2: resolve_number(node, AId::Y2, units, state, Length::zero()), + base: BaseGradient { + id, + units, + transform, + spread_method: convert_spread_method(node), + stops, + }, + }; + + Some(ServerOrColor::Server(Paint::LinearGradient(Arc::new( + gradient, + )))) +} + +#[inline(never)] +fn convert_radial(node: SvgNode, state: &converter::State) -> Option { + let id = NonEmptyString::new(node.element_id().to_string())?; + + let stops = convert_stops(find_gradient_with_stops(node)?); + if stops.len() < 2 { + return stops_to_color(&stops); + } + + let units = convert_units(node, AId::GradientUnits, Units::ObjectBoundingBox); + let r = resolve_number(node, AId::R, units, state, Length::new(50.0, Unit::Percent)); + + // 'A value of zero will cause the area to be painted as a single color + // using the color and opacity of the last gradient stop.' + // + // https://www.w3.org/TR/SVG11/pservers.html#RadialGradientElementRAttribute + if !r.is_valid_length() { + let stop = stops.last().unwrap(); + return Some(ServerOrColor::Color { + color: stop.color, + opacity: stop.opacity, + }); + } + + let spread_method = convert_spread_method(node); + let cx = resolve_number( + node, + AId::Cx, + units, + state, + Length::new(50.0, Unit::Percent), + ); + let cy = resolve_number( + node, + AId::Cy, + units, + state, + Length::new(50.0, Unit::Percent), + ); + let fx = resolve_number(node, AId::Fx, units, state, Length::new_number(cx as f64)); + let fy = resolve_number(node, AId::Fy, units, state, Length::new_number(cy as f64)); + let transform = node.resolve_transform(AId::GradientTransform, state); + + let gradient = RadialGradient { + cx, + cy, + r: PositiveF32::new(r).unwrap(), + fx, + fy, + base: BaseGradient { + id, + units, + transform, + spread_method, + stops, + }, + }; + + Some(ServerOrColor::Server(Paint::RadialGradient(Arc::new( + gradient, + )))) +} + +#[inline(never)] +fn convert_pattern( + node: SvgNode, + state: &converter::State, + cache: &mut converter::Cache, +) -> Option { + let node_with_children = find_pattern_with_children(node)?; + + let id = NonEmptyString::new(node.element_id().to_string())?; + + let view_box = { + let n1 = resolve_attr(node, AId::ViewBox); + let n2 = resolve_attr(node, AId::PreserveAspectRatio); + n1.parse_viewbox().map(|vb| ViewBox { + rect: vb, + aspect: n2.attribute(AId::PreserveAspectRatio).unwrap_or_default(), + }) + }; + + let units = convert_units(node, AId::PatternUnits, Units::ObjectBoundingBox); + let content_units = convert_units(node, AId::PatternContentUnits, Units::UserSpaceOnUse); + + let transform = node.resolve_transform(AId::PatternTransform, state); + + let rect = NonZeroRect::from_xywh( + resolve_number(node, AId::X, units, state, Length::zero()), + resolve_number(node, AId::Y, units, state, Length::zero()), + resolve_number(node, AId::Width, units, state, Length::zero()), + resolve_number(node, AId::Height, units, state, Length::zero()), + ); + let rect = rect.log_none(|| { + log::warn!( + "Pattern '{}' has an invalid size. Skipped.", + node.element_id() + ) + })?; + + let mut patt = Pattern { + id, + units, + content_units, + transform, + rect, + view_box, + root: Group::empty(), + }; + + converter::convert_children(node_with_children, state, cache, &mut patt.root); + + if !patt.root.has_children() { + return None; + } + + patt.root.calculate_bounding_boxes(); + + Some(ServerOrColor::Server(Paint::Pattern(Arc::new(patt)))) +} + +fn convert_spread_method(node: SvgNode) -> SpreadMethod { + let node = resolve_attr(node, AId::SpreadMethod); + node.attribute(AId::SpreadMethod).unwrap_or_default() +} + +pub(crate) fn convert_units(node: SvgNode, name: AId, def: Units) -> Units { + let node = resolve_attr(node, name); + node.attribute(name).unwrap_or(def) +} + +fn find_gradient_with_stops<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, +) -> Option> { + for link in node.href_iter() { + if !link.tag_name().unwrap().is_gradient() { + log::warn!( + "Gradient '{}' cannot reference '{}' via 'xlink:href'.", + node.element_id(), + link.tag_name().unwrap() + ); + return None; + } + + if link.children().any(|n| n.tag_name() == Some(EId::Stop)) { + return Some(link); + } + } + + None +} + +fn find_pattern_with_children<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, +) -> Option> { + for link in node.href_iter() { + if link.tag_name() != Some(EId::Pattern) { + log::warn!( + "Pattern '{}' cannot reference '{}' via 'xlink:href'.", + node.element_id(), + link.tag_name().unwrap() + ); + return None; + } + + if link.has_children() { + return Some(link); + } + } + + None +} + +fn convert_stops(grad: SvgNode) -> Vec { + let mut stops = Vec::new(); + + { + let mut prev_offset = Length::zero(); + for stop in grad.children() { + if stop.tag_name() != Some(EId::Stop) { + log::warn!("Invalid gradient child: '{:?}'.", stop.tag_name().unwrap()); + continue; + } + + // `number` can be either a number or a percentage. + let offset = stop.attribute(AId::Offset).unwrap_or(prev_offset); + let offset = match offset.unit { + Unit::None => offset.number, + Unit::Percent => offset.number / 100.0, + _ => prev_offset.number, + }; + prev_offset = Length::new_number(offset); + let offset = crate::f32_bound(0.0, offset as f32, 1.0); + + let (color, opacity) = match stop.attribute(AId::StopColor) { + Some("currentColor") => stop + .find_attribute(AId::Color) + .unwrap_or_else(svgtypes::Color::black), + Some(value) => { + if let Ok(c) = svgtypes::Color::from_str(value) { + c + } else { + log::warn!("Failed to parse stop-color value: '{}'.", value); + svgtypes::Color::black() + } + } + _ => svgtypes::Color::black(), + } + .split_alpha(); + + let stop_opacity = stop + .attribute::(AId::StopOpacity) + .unwrap_or(Opacity::ONE); + stops.push(Stop { + offset: StopOffset::new_clamped(offset), + color, + opacity: opacity * stop_opacity, + }); + } + } + + // Remove stops with equal offset. + // + // Example: + // offset="0.5" + // offset="0.7" + // offset="0.7" <-- this one should be removed + // offset="0.7" + // offset="0.9" + if stops.len() >= 3 { + let mut i = 0; + while i < stops.len() - 2 { + let offset1 = stops[i + 0].offset.get(); + let offset2 = stops[i + 1].offset.get(); + let offset3 = stops[i + 2].offset.get(); + + if offset1.approx_eq_ulps(&offset2, 4) && offset2.approx_eq_ulps(&offset3, 4) { + // Remove offset in the middle. + stops.remove(i + 1); + } else { + i += 1; + } + } + } + + // Remove zeros. + // + // From: + // offset="0.0" + // offset="0.0" + // offset="0.7" + // + // To: + // offset="0.0" + // offset="0.00000001" + // offset="0.7" + if stops.len() >= 2 { + let mut i = 0; + while i < stops.len() - 1 { + let offset1 = stops[i + 0].offset.get(); + let offset2 = stops[i + 1].offset.get(); + + if offset1.approx_eq_ulps(&0.0, 4) && offset2.approx_eq_ulps(&0.0, 4) { + stops[i + 1].offset = StopOffset::new_clamped(offset1 + f32::EPSILON); + } + + i += 1; + } + } + + // Shift equal offsets. + // + // From: + // offset="0.5" + // offset="0.7" + // offset="0.7" + // + // To: + // offset="0.5" + // offset="0.699999999" + // offset="0.7" + { + let mut i = 1; + while i < stops.len() { + let offset1 = stops[i - 1].offset.get(); + let offset2 = stops[i - 0].offset.get(); + + // Next offset must be smaller then previous. + if offset1 > offset2 || offset1.approx_eq_ulps(&offset2, 4) { + // Make previous offset a bit smaller. + let new_offset = offset1 - f32::EPSILON; + stops[i - 1].offset = StopOffset::new_clamped(new_offset); + stops[i - 0].offset = StopOffset::new_clamped(offset1); + } + + i += 1; + } + } + + stops +} + +#[inline(never)] +pub(crate) fn resolve_number( + node: SvgNode, + name: AId, + units: Units, + state: &converter::State, + def: Length, +) -> f32 { + resolve_attr(node, name).convert_length(name, units, state, def) +} + +fn resolve_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + if node.has_attribute(name) { + return node; + } + + match node.tag_name().unwrap() { + EId::LinearGradient => resolve_lg_attr(node, name), + EId::RadialGradient => resolve_rg_attr(node, name), + EId::Pattern => resolve_pattern_attr(node, name), + EId::Filter => resolve_filter_attr(node, name), + _ => node, + } +} + +fn resolve_lg_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + match (name, tag_name) { + // Coordinates can be resolved only from + // ref element with the same type. + (AId::X1, EId::LinearGradient) + | (AId::Y1, EId::LinearGradient) + | (AId::X2, EId::LinearGradient) + | (AId::Y2, EId::LinearGradient) + // Other attributes can be resolved + // from any kind of gradient. + | (AId::GradientUnits, EId::LinearGradient) + | (AId::GradientUnits, EId::RadialGradient) + | (AId::SpreadMethod, EId::LinearGradient) + | (AId::SpreadMethod, EId::RadialGradient) + | (AId::GradientTransform, EId::LinearGradient) + | (AId::GradientTransform, EId::RadialGradient) => { + if link.has_attribute(name) { + return link; + } + } + _ => break, + } + } + + node +} + +fn resolve_rg_attr<'a, 'input>(node: SvgNode<'a, 'input>, name: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + match (name, tag_name) { + // Coordinates can be resolved only from + // ref element with the same type. + (AId::Cx, EId::RadialGradient) + | (AId::Cy, EId::RadialGradient) + | (AId::R, EId::RadialGradient) + | (AId::Fx, EId::RadialGradient) + | (AId::Fy, EId::RadialGradient) + // Other attributes can be resolved + // from any kind of gradient. + | (AId::GradientUnits, EId::LinearGradient) + | (AId::GradientUnits, EId::RadialGradient) + | (AId::SpreadMethod, EId::LinearGradient) + | (AId::SpreadMethod, EId::RadialGradient) + | (AId::GradientTransform, EId::LinearGradient) + | (AId::GradientTransform, EId::RadialGradient) => { + if link.has_attribute(name) { + return link; + } + } + _ => break, + } + } + + node +} + +fn resolve_pattern_attr<'a, 'input: 'a>( + node: SvgNode<'a, 'input>, + name: AId, +) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + if tag_name != EId::Pattern { + break; + } + + if link.has_attribute(name) { + return link; + } + } + + node +} + +fn resolve_filter_attr<'a, 'input: 'a>(node: SvgNode<'a, 'input>, aid: AId) -> SvgNode<'a, 'input> { + for link in node.href_iter() { + let tag_name = match link.tag_name() { + Some(v) => v, + None => return node, + }; + + if tag_name != EId::Filter { + break; + } + + if link.has_attribute(aid) { + return link; + } + } + + node +} + +fn stops_to_color(stops: &[Stop]) -> Option { + if stops.is_empty() { + None + } else { + Some(ServerOrColor::Color { + color: stops[0].color, + opacity: stops[0].opacity, + }) + } +} + +// Update paints servers by doing the following: +// 1. Replace context fills/strokes that are linked to +// a use node with their actual values. +// 2. Convert all object units to UserSpaceOnUse +pub fn update_paint_servers( + group: &mut Group, + context_transform: Transform, + context_bbox: Option, + text_bbox: Option, + cache: &mut Cache, +) { + for child in &mut group.children { + // Set context transform and bbox if applicable if the + // current group is a use node. + let (context_transform, context_bbox) = if group.is_context_element { + (group.abs_transform, Some(group.bounding_box)) + } else { + (context_transform, context_bbox) + }; + + node_to_user_coordinates(child, context_transform, context_bbox, text_bbox, cache); + } +} + +// When parsing clipPaths, masks and filters we already know group's bounding box. +// But with gradients and patterns we don't, because we have to know text bounding box +// before we even parsed it. Which is impossible. +// Therefore our only choice is to parse gradients and patterns preserving their units +// and then replace them with `userSpaceOnUse` after the whole tree parsing is finished. +// So while gradients and patterns do still store their units, +// they are not exposed in the public API and for the caller they are always `userSpaceOnUse`. +fn node_to_user_coordinates( + node: &mut Node, + context_transform: Transform, + context_bbox: Option, + text_bbox: Option, + cache: &mut Cache, +) { + match node { + Node::Group(ref mut g) => { + // No need to check clip paths, because they cannot have paint servers. + if let Some(ref mut mask) = g.mask { + if let Some(ref mut mask) = Arc::get_mut(mask) { + update_paint_servers( + &mut mask.root, + context_transform, + context_bbox, + None, + cache, + ); + + if let Some(ref mut sub_mask) = mask.mask { + if let Some(ref mut sub_mask) = Arc::get_mut(sub_mask) { + update_paint_servers( + &mut sub_mask.root, + context_transform, + context_bbox, + None, + cache, + ); + } + } + } + } + + for filter in &mut g.filters { + if let Some(ref mut filter) = Arc::get_mut(filter) { + for primitive in &mut filter.primitives { + if let filter::Kind::Image(ref mut image) = primitive.kind { + if let filter::ImageKind::Use(ref mut use_node) = image.data { + update_paint_servers( + use_node, + context_transform, + context_bbox, + None, + cache, + ); + } + } + } + } + } + + update_paint_servers(g, context_transform, context_bbox, text_bbox, cache); + } + Node::Path(ref mut path) => { + // Paths inside `Text::flattened` are special and must use text's bounding box + // instead of their own. + let bbox = text_bbox.unwrap_or(path.bounding_box); + + process_fill( + &mut path.fill, + path.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut path.stroke, + path.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + } + Node::Image(ref mut image) => { + if let ImageKind::SVG(ref mut tree) = image.kind { + update_paint_servers(&mut tree.root, context_transform, context_bbox, None, cache); + } + } + Node::Text(ref mut text) => { + // By the SVG spec, `tspan` doesn't have a bbox and uses the parent `text` bbox. + // Therefore we have to use text's bbox when converting tspan and flatted text + // paint servers. + let bbox = text.bounding_box; + + // We need to update three things: + // 1. The fills/strokes of the original elements in the usvg tree. + // 2. The fills/strokes of the layouted elements of the text. + // 3. The fills/strokes of the outlined text. + + // 1. + for chunk in &mut text.chunks { + for span in &mut chunk.spans { + process_fill( + &mut span.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut span.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_text_decoration(&mut span.decoration.underline, bbox, cache); + process_text_decoration(&mut span.decoration.overline, bbox, cache); + process_text_decoration(&mut span.decoration.line_through, bbox, cache); + } + } + + // 2. + #[cfg(feature = "text")] + for span in &mut text.layouted { + process_fill( + &mut span.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut span.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + + let mut process_decoration = |path: &mut Path| { + process_fill( + &mut path.fill, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + process_stroke( + &mut path.stroke, + text.abs_transform, + context_transform, + context_bbox, + bbox, + cache, + ); + }; + + if let Some(ref mut path) = span.overline { + process_decoration(path); + } + + if let Some(ref mut path) = span.underline { + process_decoration(path); + } + + if let Some(ref mut path) = span.line_through { + process_decoration(path); + } + } + + // 3. + update_paint_servers( + &mut text.flattened, + context_transform, + context_bbox, + Some(bbox), + cache, + ); + } + } +} + +fn process_fill( + fill: &mut Option, + path_transform: Transform, + context_transform: Transform, + context_bbox: Option, + bbox: Rect, + cache: &mut Cache, +) { + let mut ok = false; + if let Some(ref mut fill) = fill { + // Path context elements (i.e. for markers) have already been resolved, + // so we only care about use nodes. + ok = process_paint( + &mut fill.paint, + matches!(fill.context_element, Some(ContextElement::UseNode)), + context_transform, + context_bbox, + path_transform, + bbox, + cache, + ); + } + if !ok { + *fill = None; + } +} + +fn process_stroke( + stroke: &mut Option, + path_transform: Transform, + context_transform: Transform, + context_bbox: Option, + bbox: Rect, + cache: &mut Cache, +) { + let mut ok = false; + if let Some(ref mut stroke) = stroke { + // Path context elements (i.e. for markers) have already been resolved, + // so we only care about use nodes. + ok = process_paint( + &mut stroke.paint, + matches!(stroke.context_element, Some(ContextElement::UseNode)), + context_transform, + context_bbox, + path_transform, + bbox, + cache, + ); + } + if !ok { + *stroke = None; + } +} + +fn process_context_paint( + paint: &mut Paint, + context_transform: Transform, + path_transform: Transform, + cache: &mut Cache, +) -> Option<()> { + // The idea is the following: We have a certain context element that has + // a transform A, and further below in the tree we have for example a path + // whose paint has a transform C. In order to get from A to C, there is some + // transformation matrix B such that A x B = C. We now need to figure out + // a way to get from C back to A, so that the transformation of the paint + // matches the one from the context element, even if B was applied. How + // do we do that? We calculate CxB^(-1), which will overall then have + // the same effect as A. How do we calculate B^(-1)? + // --> (A^(-1)xC)^(-1) + let rev_transform = context_transform + .invert()? + .pre_concat(path_transform) + .invert()?; + + match paint { + Paint::Color(_) => {} + Paint::LinearGradient(ref lg) => { + let transform = lg.transform.post_concat(rev_transform); + *paint = Paint::LinearGradient(Arc::new(LinearGradient { + x1: lg.x1, + y1: lg.y1, + x2: lg.x2, + y2: lg.y2, + base: BaseGradient { + id: cache.gen_linear_gradient_id(), + units: lg.units, + transform, + spread_method: lg.spread_method, + stops: lg.stops.clone(), + }, + })); + } + Paint::RadialGradient(ref rg) => { + let transform = rg.transform.post_concat(rev_transform); + *paint = Paint::RadialGradient(Arc::new(RadialGradient { + cx: rg.cx, + cy: rg.cy, + r: rg.r, + fx: rg.fx, + fy: rg.fy, + base: BaseGradient { + id: cache.gen_radial_gradient_id(), + units: rg.units, + transform, + spread_method: rg.spread_method, + stops: rg.stops.clone(), + }, + })) + } + Paint::Pattern(ref pat) => { + let transform = pat.transform.post_concat(rev_transform); + *paint = Paint::Pattern(Arc::new(Pattern { + id: cache.gen_pattern_id(), + units: pat.units, + content_units: pat.content_units, + transform, + rect: pat.rect, + view_box: pat.view_box, + root: pat.root.clone(), + })) + } + } + + Some(()) +} + +pub(crate) fn process_paint( + paint: &mut Paint, + has_context: bool, + context_transform: Transform, + context_bbox: Option, + path_transform: Transform, + bbox: Rect, + cache: &mut Cache, +) -> bool { + if paint.units() == Units::ObjectBoundingBox + || paint.content_units() == Units::ObjectBoundingBox + { + let bbox = if has_context { + let Some(bbox) = context_bbox else { + return false; + }; + bbox + } else { + bbox + }; + + if paint.to_user_coordinates(bbox, cache).is_none() { + return false; + } + } + + if let Paint::Pattern(ref mut patt) = paint { + if let Some(ref mut patt) = Arc::get_mut(patt) { + update_paint_servers(&mut patt.root, Transform::default(), None, None, cache); + } + } + + if has_context { + process_context_paint(paint, context_transform, path_transform, cache); + } + + true +} + +fn process_text_decoration(style: &mut Option, bbox: Rect, cache: &mut Cache) { + if let Some(ref mut style) = style { + process_fill( + &mut style.fill, + Transform::default(), + Transform::default(), + None, + bbox, + cache, + ); + process_stroke( + &mut style.stroke, + Transform::default(), + Transform::default(), + None, + bbox, + cache, + ); + } +} + +impl Paint { + fn to_user_coordinates(&mut self, bbox: Rect, cache: &mut Cache) -> Option<()> { + let name = if matches!(self, Paint::Pattern(_)) { + "Pattern" + } else { + "Gradient" + }; + let bbox = bbox + .to_non_zero_rect() + .log_none(|| log::warn!("{} on zero-sized shapes is not allowed.", name))?; + + // `Arc::get_mut()` allow us to modify some paint servers in-place. + // This reduces the amount of cloning and preserves the original ID as well. + match self { + Paint::Color(_) => {} // unreachable + Paint::LinearGradient(ref mut lg) => { + let transform = lg.transform.post_concat(Transform::from_bbox(bbox)); + if let Some(ref mut lg) = Arc::get_mut(lg) { + lg.base.transform = transform; + lg.base.units = Units::UserSpaceOnUse; + } else { + *lg = Arc::new(LinearGradient { + x1: lg.x1, + y1: lg.y1, + x2: lg.x2, + y2: lg.y2, + base: BaseGradient { + id: cache.gen_linear_gradient_id(), + units: Units::UserSpaceOnUse, + transform, + spread_method: lg.spread_method, + stops: lg.stops.clone(), + }, + }); + } + } + Paint::RadialGradient(ref mut rg) => { + let transform = rg.transform.post_concat(Transform::from_bbox(bbox)); + if let Some(ref mut rg) = Arc::get_mut(rg) { + rg.base.transform = transform; + rg.base.units = Units::UserSpaceOnUse; + } else { + *rg = Arc::new(RadialGradient { + cx: rg.cx, + cy: rg.cy, + r: rg.r, + fx: rg.fx, + fy: rg.fy, + base: BaseGradient { + id: cache.gen_radial_gradient_id(), + units: Units::UserSpaceOnUse, + transform, + spread_method: rg.spread_method, + stops: rg.stops.clone(), + }, + }); + } + } + Paint::Pattern(ref mut patt) => { + let rect = if patt.units == Units::ObjectBoundingBox { + patt.rect.bbox_transform(bbox) + } else { + patt.rect + }; + + if let Some(ref mut patt) = Arc::get_mut(patt) { + patt.rect = rect; + patt.units = Units::UserSpaceOnUse; + + if patt.content_units == Units::ObjectBoundingBox && patt.view_box().is_none() { + // No need to shift patterns. + let transform = Transform::from_scale(bbox.width(), bbox.height()); + + let mut g = std::mem::replace(&mut patt.root, Group::empty()); + g.transform = transform; + g.abs_transform = transform; + + patt.root.children.push(Node::Group(Box::new(g))); + patt.root.calculate_bounding_boxes(); + } + + patt.content_units = Units::UserSpaceOnUse; + } else { + let root = if patt.content_units == Units::ObjectBoundingBox + && patt.view_box().is_none() + { + // No need to shift patterns. + let transform = Transform::from_scale(bbox.width(), bbox.height()); + + let mut g = patt.root.clone(); + g.transform = transform; + g.abs_transform = transform; + + let mut root = Group::empty(); + root.children.push(Node::Group(Box::new(g))); + root.calculate_bounding_boxes(); + root + } else { + patt.root.clone() + }; + + *patt = Arc::new(Pattern { + id: cache.gen_pattern_id(), + units: Units::UserSpaceOnUse, + content_units: Units::UserSpaceOnUse, + transform: patt.transform, + rect, + view_box: patt.view_box, + root, + }) + } + } + } + + Some(()) + } +} + +impl Paint { + #[inline] + pub(crate) fn units(&self) -> Units { + match self { + Self::Color(_) => Units::UserSpaceOnUse, + Self::LinearGradient(ref lg) => lg.units, + Self::RadialGradient(ref rg) => rg.units, + Self::Pattern(ref patt) => patt.units, + } + } + + #[inline] + pub(crate) fn content_units(&self) -> Units { + match self { + Self::Pattern(ref patt) => patt.content_units, + _ => Units::UserSpaceOnUse, + } + } +} diff --git a/crates/usvg-parser/src/shapes.rs b/crates/usvg/src/parser/shapes.rs similarity index 92% rename from crates/usvg-parser/src/shapes.rs rename to crates/usvg/src/parser/shapes.rs index 42314e07a..57e574bd0 100644 --- a/crates/usvg-parser/src/shapes.rs +++ b/crates/usvg/src/parser/shapes.rs @@ -2,16 +2,16 @@ // 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/. -use std::rc::Rc; +use std::sync::Arc; use svgtypes::Length; use tiny_skia_path::Path; -use usvg_tree::{tiny_skia_path, ApproxEqUlps, IsValidLength}; -use crate::svgtree::{AId, EId, SvgNode}; -use crate::{converter, units}; +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, units}; +use crate::{ApproxEqUlps, IsValidLength, Rect}; -pub(crate) fn convert(node: SvgNode, state: &converter::State) -> Option> { +pub(crate) fn convert(node: SvgNode, state: &converter::State) -> Option> { match node.tag_name()? { EId::Rect => convert_rect(node, state), EId::Circle => convert_circle(node, state), @@ -24,7 +24,7 @@ pub(crate) fn convert(node: SvgNode, state: &converter::State) -> Option Option> { +pub(crate) fn convert_path(node: SvgNode) -> Option> { let value: &str = node.attribute(AId::D)?; let mut builder = tiny_skia_path::PathBuilder::new(); for segment in svgtypes::SimplifyingPathParser::from(value) { @@ -61,10 +61,10 @@ pub(crate) fn convert_path(node: SvgNode) -> Option> { } } - builder.finish().map(Rc::new) + builder.finish().map(Arc::new) } -fn convert_rect(node: SvgNode, state: &converter::State) -> Option> { +fn convert_rect(node: SvgNode, state: &converter::State) -> Option> { // 'width' and 'height' attributes must be positive and non-zero. let width = node.convert_user_length(AId::Width, state, Length::zero()); let height = node.convert_user_length(AId::Height, state, Length::zero()); @@ -100,7 +100,7 @@ fn convert_rect(node: SvgNode, state: &converter::State) -> Option> { // Conversion according to https://www.w3.org/TR/SVG11/shapes.html#RectElement let path = if rx.approx_eq_ulps(&0.0, 4) { - tiny_skia_path::PathBuilder::from_rect(usvg_tree::Rect::from_xywh(x, y, width, height)?) + tiny_skia_path::PathBuilder::from_rect(Rect::from_xywh(x, y, width, height)?) } else { let mut builder = tiny_skia_path::PathBuilder::new(); builder.move_to(x + rx, y); @@ -122,7 +122,7 @@ fn convert_rect(node: SvgNode, state: &converter::State) -> Option> { builder.finish()? }; - Some(Rc::new(path)) + Some(Arc::new(path)) } fn resolve_rx_ry(node: SvgNode, state: &converter::State) -> (f32, f32) { @@ -160,7 +160,7 @@ fn resolve_rx_ry(node: SvgNode, state: &converter::State) -> (f32, f32) { } } -fn convert_line(node: SvgNode, state: &converter::State) -> Option> { +fn convert_line(node: SvgNode, state: &converter::State) -> Option> { let x1 = node.convert_user_length(AId::X1, state, Length::zero()); let y1 = node.convert_user_length(AId::Y1, state, Length::zero()); let x2 = node.convert_user_length(AId::X2, state, Length::zero()); @@ -169,18 +169,18 @@ fn convert_line(node: SvgNode, state: &converter::State) -> Option> { let mut builder = tiny_skia_path::PathBuilder::new(); builder.move_to(x1, y1); builder.line_to(x2, y2); - builder.finish().map(Rc::new) + builder.finish().map(Arc::new) } -fn convert_polyline(node: SvgNode) -> Option> { +fn convert_polyline(node: SvgNode) -> Option> { let builder = points_to_path(node, "Polyline")?; - builder.finish().map(Rc::new) + builder.finish().map(Arc::new) } -fn convert_polygon(node: SvgNode) -> Option> { +fn convert_polygon(node: SvgNode) -> Option> { let mut builder = points_to_path(node, "Polygon")?; builder.close(); - builder.finish().map(Rc::new) + builder.finish().map(Arc::new) } fn points_to_path(node: SvgNode, eid: &str) -> Option { @@ -220,7 +220,7 @@ fn points_to_path(node: SvgNode, eid: &str) -> Option Option> { +fn convert_circle(node: SvgNode, state: &converter::State) -> Option> { let cx = node.convert_user_length(AId::Cx, state, Length::zero()); let cy = node.convert_user_length(AId::Cy, state, Length::zero()); let r = node.convert_user_length(AId::R, state, Length::zero()); @@ -236,7 +236,7 @@ fn convert_circle(node: SvgNode, state: &converter::State) -> Option> { ellipse_to_path(cx, cy, r, r) } -fn convert_ellipse(node: SvgNode, state: &converter::State) -> Option> { +fn convert_ellipse(node: SvgNode, state: &converter::State) -> Option> { let cx = node.convert_user_length(AId::Cx, state, Length::zero()); let cy = node.convert_user_length(AId::Cy, state, Length::zero()); let (rx, ry) = resolve_rx_ry(node, state); @@ -260,7 +260,7 @@ fn convert_ellipse(node: SvgNode, state: &converter::State) -> Option> ellipse_to_path(cx, cy, rx, ry) } -fn ellipse_to_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Option> { +fn ellipse_to_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Option> { let mut builder = tiny_skia_path::PathBuilder::new(); builder.move_to(cx + rx, cy); builder.arc_to(rx, ry, 0.0, false, true, cx, cy + ry); @@ -268,7 +268,7 @@ fn ellipse_to_path(cx: f32, cy: f32, rx: f32, ry: f32) -> Option> { builder.arc_to(rx, ry, 0.0, false, true, cx, cy - ry); builder.arc_to(rx, ry, 0.0, false, true, cx + rx, cy); builder.close(); - builder.finish().map(Rc::new) + builder.finish().map(Arc::new) } trait PathBuilderExt { diff --git a/crates/usvg-parser/src/style.rs b/crates/usvg/src/parser/style.rs similarity index 72% rename from crates/usvg-parser/src/style.rs rename to crates/usvg/src/parser/style.rs index ccea18f0c..47af77ed3 100644 --- a/crates/usvg-parser/src/style.rs +++ b/crates/usvg/src/parser/style.rs @@ -2,40 +2,43 @@ // 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/. -use usvg_tree::{ApproxEqUlps, Color, Fill, Opacity, Paint, Stroke, StrokeMiterlimit, Units}; +use super::converter::{self, SvgColorExt}; +use super::paint_server; +use super::svgtree::{AId, FromValue, SvgNode}; +use crate::tree::ContextElement; +use crate::{ + ApproxEqUlps, Color, Fill, FillRule, LineCap, LineJoin, Opacity, Paint, Stroke, + StrokeMiterlimit, Units, +}; -use crate::converter::SvgColorExt; -use crate::svgtree::{AId, FromValue, SvgNode}; -use crate::{converter, paint_server}; - -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::LineCap { +impl<'a, 'input: 'a> FromValue<'a, 'input> for LineCap { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "butt" => Some(usvg_tree::LineCap::Butt), - "round" => Some(usvg_tree::LineCap::Round), - "square" => Some(usvg_tree::LineCap::Square), + "butt" => Some(LineCap::Butt), + "round" => Some(LineCap::Round), + "square" => Some(LineCap::Square), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::LineJoin { +impl<'a, 'input: 'a> FromValue<'a, 'input> for LineJoin { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "miter" => Some(usvg_tree::LineJoin::Miter), - "miter-clip" => Some(usvg_tree::LineJoin::MiterClip), - "round" => Some(usvg_tree::LineJoin::Round), - "bevel" => Some(usvg_tree::LineJoin::Bevel), + "miter" => Some(LineJoin::Miter), + "miter-clip" => Some(LineJoin::MiterClip), + "round" => Some(LineJoin::Round), + "bevel" => Some(LineJoin::Bevel), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::FillRule { +impl<'a, 'input: 'a> FromValue<'a, 'input> for FillRule { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "nonzero" => Some(usvg_tree::FillRule::NonZero), - "evenodd" => Some(usvg_tree::FillRule::EvenOdd), + "nonzero" => Some(FillRule::NonZero), + "evenodd" => Some(FillRule::EvenOdd), _ => None, } } @@ -53,15 +56,17 @@ pub(crate) fn resolve_fill( paint: Paint::Color(Color::black()), opacity: Opacity::ONE, rule: node.find_attribute(AId::ClipRule).unwrap_or_default(), + context_element: None, }); } let mut sub_opacity = Opacity::ONE; - let paint = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Fill)) { - convert_paint(n, AId::Fill, has_bbox, state, &mut sub_opacity, cache)? - } else { - Paint::Color(Color::black()) - }; + let (paint, context_element) = + if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Fill)) { + convert_paint(n, AId::Fill, has_bbox, state, &mut sub_opacity, cache)? + } else { + (Paint::Color(Color::black()), None) + }; let fill_opacity = node .find_attribute::(AId::FillOpacity) @@ -71,6 +76,7 @@ pub(crate) fn resolve_fill( paint, opacity: sub_opacity * fill_opacity, rule: node.find_attribute(AId::FillRule).unwrap_or_default(), + context_element, }) } @@ -86,11 +92,12 @@ pub(crate) fn resolve_stroke( } let mut sub_opacity = Opacity::ONE; - let paint = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Stroke)) { - convert_paint(n, AId::Stroke, has_bbox, state, &mut sub_opacity, cache)? - } else { - return None; - }; + let (paint, context_element) = + if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::Stroke)) { + convert_paint(n, AId::Stroke, has_bbox, state, &mut sub_opacity, cache)? + } else { + return None; + }; let width = node.resolve_valid_length(AId::StrokeWidth, state, 1.0)?; @@ -112,6 +119,7 @@ pub(crate) fn resolve_stroke( width, linecap: node.find_attribute(AId::StrokeLinecap).unwrap_or_default(), linejoin: node.find_attribute(AId::StrokeLinejoin).unwrap_or_default(), + context_element, }; Some(stroke) @@ -124,7 +132,7 @@ fn convert_paint( state: &converter::State, opacity: &mut Opacity, cache: &mut converter::Cache, -) -> Option { +) -> Option<(Paint, Option)> { let value: &str = node.attribute(aid)?; let paint = match svgtypes::Paint::from_str(value) { Ok(v) => v, @@ -135,6 +143,12 @@ fn convert_paint( value ); svgtypes::Paint::Color(svgtypes::Color::black()) + } else if aid == AId::Stroke { + log::warn!( + "Failed to parse stroke value: '{}'. Fallback to no stroke.", + value + ); + return None; } else { return None; } @@ -144,18 +158,30 @@ fn convert_paint( match paint { svgtypes::Paint::None => None, svgtypes::Paint::Inherit => None, // already resolved by svgtree + svgtypes::Paint::ContextFill => state + .context_element + .clone() + .map(|(f, _)| f) + .flatten() + .map(|f| (f.paint, f.context_element)), + svgtypes::Paint::ContextStroke => state + .context_element + .clone() + .map(|(_, s)| s) + .flatten() + .map(|s| (s.paint, s.context_element)), svgtypes::Paint::CurrentColor => { let svg_color: svgtypes::Color = node .find_attribute(AId::Color) .unwrap_or_else(svgtypes::Color::black); let (color, alpha) = svg_color.split_alpha(); *opacity = alpha; - Some(Paint::Color(color)) + Some((Paint::Color(color), None)) } svgtypes::Paint::Color(svg_color) => { let (color, alpha) = svg_color.split_alpha(); *opacity = alpha; - Some(Paint::Color(color)) + Some((Paint::Color(color), None)) } svgtypes::Paint::FuncIRI(func_iri, fallback) => { if let Some(link) = node.document().element_by_id(func_iri) { @@ -167,24 +193,25 @@ fn convert_paint( // for painting only when the shape itself has a bbox. // // See SVG spec 7.11 for details. - if !has_bbox && paint.units() == Some(Units::ObjectBoundingBox) { - from_fallback(node, fallback, opacity) + + if !has_bbox && paint.units() == Units::ObjectBoundingBox { + from_fallback(node, fallback, opacity).map(|p| (p, None)) } else { - Some(paint) + Some((paint, None)) } } Some(paint_server::ServerOrColor::Color { color, opacity: so }) => { *opacity = so; - Some(Paint::Color(color)) + Some((Paint::Color(color), None)) } - None => from_fallback(node, fallback, opacity), + None => from_fallback(node, fallback, opacity).map(|p| (p, None)), } } else { log::warn!("'{}' cannot be used to {} a shape.", tag_name, aid); None } } else { - from_fallback(node, fallback, opacity) + from_fallback(node, fallback, opacity).map(|p| (p, None)) } } } diff --git a/crates/usvg-parser/src/svgtree/mod.rs b/crates/usvg/src/parser/svgtree/mod.rs similarity index 89% rename from crates/usvg-parser/src/svgtree/mod.rs rename to crates/usvg/src/parser/svgtree/mod.rs index ff048ad3f..29c70aa73 100644 --- a/crates/usvg-parser/src/svgtree/mod.rs +++ b/crates/usvg/src/parser/svgtree/mod.rs @@ -10,6 +10,12 @@ use std::str::FromStr; mod parse; mod text; +use tiny_skia_path::Transform; + +use crate::{ + BlendMode, ImageRendering, Opacity, ShapeRendering, SpreadMethod, TextRendering, Units, + Visibility, +}; pub use names::{AId, EId}; /// An SVG tree container. @@ -294,6 +300,18 @@ impl<'a, 'input: 'a> SvgNode<'a, 'input> { } } + /// Returns an attribute value. + /// + /// Same as `SvgNode::attribute`, but doesn't show a warning. + pub fn try_attribute>(&self, aid: AId) -> Option { + let value = self + .attributes() + .iter() + .find(|a| a.name == aid) + .map(|a| a.value.as_str())?; + T::parse(*self, aid, value) + } + #[inline] fn node_attribute(&self, aid: AId) -> Option> { let value = self.attribute(aid)?; @@ -698,6 +716,7 @@ impl AId { | AId::MarkerMid | AId::MarkerStart | AId::Mask + | AId::MaskType | AId::MixBlendMode // technically not presentation | AId::Opacity | AId::Overflow @@ -718,6 +737,7 @@ impl AId { | AId::TextOverflow | AId::TextRendering | AId::Transform + | AId::TransformOrigin | AId::UnicodeBidi | AId::VectorEffect | AId::Visibility @@ -810,6 +830,7 @@ fn is_non_inheritable(id: AId) -> bool { | AId::StopOpacity | AId::TextDecoration | AId::Transform + | AId::TransformOrigin ) } @@ -841,29 +862,27 @@ impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::Length { } // TODO: to svgtypes? -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::Opacity { +impl<'a, 'input: 'a> FromValue<'a, 'input> for Opacity { fn parse(_: SvgNode, _: AId, value: &str) -> Option { let length = svgtypes::Length::from_str(value).ok()?; if length.unit == svgtypes::LengthUnit::Percent { - Some(usvg_tree::Opacity::new_clamped( - length.number as f32 / 100.0, - )) + Some(Opacity::new_clamped(length.number as f32 / 100.0)) } else if length.unit == svgtypes::LengthUnit::None { - Some(usvg_tree::Opacity::new_clamped(length.number as f32)) + Some(Opacity::new_clamped(length.number as f32)) } else { None } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::Transform { +impl<'a, 'input: 'a> FromValue<'a, 'input> for Transform { fn parse(_: SvgNode, _: AId, value: &str) -> Option { let ts = match svgtypes::Transform::from_str(value) { Ok(v) => v, Err(_) => return None, }; - let ts = usvg_tree::Transform::from_row( + let ts = Transform::from_row( ts.a as f32, ts.b as f32, ts.c as f32, @@ -875,22 +894,28 @@ impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::Transform { if ts.is_valid() { Some(ts) } else { - Some(usvg_tree::Transform::default()) + Some(Transform::default()) } } } +impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::TransformOrigin { + fn parse(_: SvgNode, _: AId, value: &str) -> Option { + Self::from_str(value).ok() + } +} + impl<'a, 'input: 'a> FromValue<'a, 'input> for svgtypes::ViewBox { fn parse(_: SvgNode, _: AId, value: &str) -> Option { Self::from_str(value).ok() } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::Units { +impl<'a, 'input: 'a> FromValue<'a, 'input> for Units { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "userSpaceOnUse" => Some(usvg_tree::Units::UserSpaceOnUse), - "objectBoundingBox" => Some(usvg_tree::Units::ObjectBoundingBox), + "userSpaceOnUse" => Some(Units::UserSpaceOnUse), + "objectBoundingBox" => Some(Units::ObjectBoundingBox), _ => None, } } @@ -954,79 +979,79 @@ impl<'a, 'input: 'a> FromValue<'a, 'input> for Vec { } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::Visibility { +impl<'a, 'input: 'a> FromValue<'a, 'input> for Visibility { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "visible" => Some(usvg_tree::Visibility::Visible), - "hidden" => Some(usvg_tree::Visibility::Hidden), - "collapse" => Some(usvg_tree::Visibility::Collapse), + "visible" => Some(Visibility::Visible), + "hidden" => Some(Visibility::Hidden), + "collapse" => Some(Visibility::Collapse), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::SpreadMethod { +impl<'a, 'input: 'a> FromValue<'a, 'input> for SpreadMethod { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "pad" => Some(usvg_tree::SpreadMethod::Pad), - "reflect" => Some(usvg_tree::SpreadMethod::Reflect), - "repeat" => Some(usvg_tree::SpreadMethod::Repeat), + "pad" => Some(SpreadMethod::Pad), + "reflect" => Some(SpreadMethod::Reflect), + "repeat" => Some(SpreadMethod::Repeat), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::ShapeRendering { +impl<'a, 'input: 'a> FromValue<'a, 'input> for ShapeRendering { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "optimizeSpeed" => Some(usvg_tree::ShapeRendering::OptimizeSpeed), - "crispEdges" => Some(usvg_tree::ShapeRendering::CrispEdges), - "auto" | "geometricPrecision" => Some(usvg_tree::ShapeRendering::GeometricPrecision), + "optimizeSpeed" => Some(ShapeRendering::OptimizeSpeed), + "crispEdges" => Some(ShapeRendering::CrispEdges), + "auto" | "geometricPrecision" => Some(ShapeRendering::GeometricPrecision), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::TextRendering { +impl<'a, 'input: 'a> FromValue<'a, 'input> for TextRendering { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "optimizeSpeed" => Some(usvg_tree::TextRendering::OptimizeSpeed), - "auto" | "optimizeLegibility" => Some(usvg_tree::TextRendering::OptimizeLegibility), - "geometricPrecision" => Some(usvg_tree::TextRendering::GeometricPrecision), + "optimizeSpeed" => Some(TextRendering::OptimizeSpeed), + "auto" | "optimizeLegibility" => Some(TextRendering::OptimizeLegibility), + "geometricPrecision" => Some(TextRendering::GeometricPrecision), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::ImageRendering { +impl<'a, 'input: 'a> FromValue<'a, 'input> for ImageRendering { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "auto" | "optimizeQuality" => Some(usvg_tree::ImageRendering::OptimizeQuality), - "optimizeSpeed" => Some(usvg_tree::ImageRendering::OptimizeSpeed), + "auto" | "optimizeQuality" => Some(ImageRendering::OptimizeQuality), + "optimizeSpeed" => Some(ImageRendering::OptimizeSpeed), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::BlendMode { +impl<'a, 'input: 'a> FromValue<'a, 'input> for BlendMode { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "normal" => Some(usvg_tree::BlendMode::Normal), - "multiply" => Some(usvg_tree::BlendMode::Multiply), - "screen" => Some(usvg_tree::BlendMode::Screen), - "overlay" => Some(usvg_tree::BlendMode::Overlay), - "darken" => Some(usvg_tree::BlendMode::Darken), - "lighten" => Some(usvg_tree::BlendMode::Lighten), - "color-dodge" => Some(usvg_tree::BlendMode::ColorDodge), - "color-burn" => Some(usvg_tree::BlendMode::ColorBurn), - "hard-light" => Some(usvg_tree::BlendMode::HardLight), - "soft-light" => Some(usvg_tree::BlendMode::SoftLight), - "difference" => Some(usvg_tree::BlendMode::Difference), - "exclusion" => Some(usvg_tree::BlendMode::Exclusion), - "hue" => Some(usvg_tree::BlendMode::Hue), - "saturation" => Some(usvg_tree::BlendMode::Saturation), - "color" => Some(usvg_tree::BlendMode::Color), - "luminosity" => Some(usvg_tree::BlendMode::Luminosity), + "normal" => Some(BlendMode::Normal), + "multiply" => Some(BlendMode::Multiply), + "screen" => Some(BlendMode::Screen), + "overlay" => Some(BlendMode::Overlay), + "darken" => Some(BlendMode::Darken), + "lighten" => Some(BlendMode::Lighten), + "color-dodge" => Some(BlendMode::ColorDodge), + "color-burn" => Some(BlendMode::ColorBurn), + "hard-light" => Some(BlendMode::HardLight), + "soft-light" => Some(BlendMode::SoftLight), + "difference" => Some(BlendMode::Difference), + "exclusion" => Some(BlendMode::Exclusion), + "hue" => Some(BlendMode::Hue), + "saturation" => Some(BlendMode::Saturation), + "color" => Some(BlendMode::Color), + "luminosity" => Some(BlendMode::Luminosity), _ => None, } } diff --git a/crates/usvg-parser/src/svgtree/names.rs b/crates/usvg/src/parser/svgtree/names.rs similarity index 97% rename from crates/usvg-parser/src/svgtree/names.rs rename to crates/usvg/src/parser/svgtree/names.rs index 93334237b..bc8e8f0b9 100644 --- a/crates/usvg-parser/src/svgtree/names.rs +++ b/crates/usvg/src/parser/svgtree/names.rs @@ -57,7 +57,7 @@ pub enum EId { TextPath, Tref, Tspan, - Use + Use, } static ELEMENTS: Map = Map { @@ -367,7 +367,7 @@ pub enum AId { Y1, Y2, YChannelSelector, - Z + Z, } static ATTRIBUTES: Map = Map { @@ -454,7 +454,10 @@ static ATTRIBUTES: Map = Map { ("viewBox", AId::ViewBox), ("visibility", AId::Visibility), ("ry", AId::Ry), - ("glyph-orientation-horizontal", AId::GlyphOrientationHorizontal), + ( + "glyph-orientation-horizontal", + AId::GlyphOrientationHorizontal, + ), ("gradientTransform", AId::GradientTransform), ("markerUnits", AId::MarkerUnits), ("shape-inside", AId::ShapeInside), @@ -499,7 +502,10 @@ static ATTRIBUTES: Map = Map { ("text-rendering", AId::TextRendering), ("mask-border", AId::MaskBorder), ("exponent", AId::Exponent), - ("color-interpolation-filters", AId::ColorInterpolationFilters), + ( + "color-interpolation-filters", + AId::ColorInterpolationFilters, + ), ("diffuseConstant", AId::DiffuseConstant), ("space", AId::Space), ("font-synthesis", AId::FontSynthesis), @@ -659,18 +665,15 @@ impl std::fmt::Display for AId { struct Map { pub key: u64, pub disps: &'static [(u32, u32)], - pub entries: &'static[(&'static str, V)], + pub entries: &'static [(&'static str, V)], } impl Map { fn get(&self, key: &str) -> Option<&V> { - use std::borrow::Borrow; - let hash = hash(key, self.key); let index = get_index(hash, self.disps, self.entries.len()); let entry = &self.entries[index as usize]; - let b = entry.0.borrow(); - if b == key { + if entry.0 == key { Some(&entry.1) } else { None @@ -703,9 +706,11 @@ fn split(hash: u64) -> (u32, u32, u32) { const BITS: u32 = 21; const MASK: u64 = (1 << BITS) - 1; - ((hash & MASK) as u32, - ((hash >> BITS) & MASK) as u32, - ((hash >> (2 * BITS)) & MASK) as u32) + ( + (hash & MASK) as u32, + ((hash >> BITS) & MASK) as u32, + ((hash >> (2 * BITS)) & MASK) as u32, + ) } #[inline] diff --git a/crates/usvg-parser/src/svgtree/parse.rs b/crates/usvg/src/parser/svgtree/parse.rs similarity index 88% rename from crates/usvg-parser/src/svgtree/parse.rs rename to crates/usvg/src/parser/svgtree/parse.rs index 33966d9e2..af413d91a 100644 --- a/crates/usvg-parser/src/svgtree/parse.rs +++ b/crates/usvg/src/parser/svgtree/parse.rs @@ -5,6 +5,8 @@ use std::collections::HashMap; use roxmltree::Error; +use simplecss::Declaration; +use svgtypes::FontShorthand; use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange}; @@ -266,21 +268,63 @@ pub(crate) fn parse_svg_element<'input>( } }; + let mut write_declaration = |declaration: &Declaration| { + // TODO: perform XML attribute normalization + if declaration.name == "marker" { + insert_attribute(AId::MarkerStart, declaration.value); + insert_attribute(AId::MarkerMid, declaration.value); + insert_attribute(AId::MarkerEnd, declaration.value); + } else if declaration.name == "font" { + if let Ok(shorthand) = FontShorthand::from_str(declaration.value) { + // First we need to reset all values to their default. + insert_attribute(AId::FontStyle, "normal"); + insert_attribute(AId::FontVariant, "normal"); + insert_attribute(AId::FontWeight, "normal"); + insert_attribute(AId::FontStretch, "normal"); + insert_attribute(AId::LineHeight, "normal"); + insert_attribute(AId::FontSizeAdjust, "none"); + insert_attribute(AId::FontKerning, "auto"); + insert_attribute(AId::FontVariantCaps, "normal"); + insert_attribute(AId::FontVariantLigatures, "normal"); + insert_attribute(AId::FontVariantNumeric, "normal"); + insert_attribute(AId::FontVariantEastAsian, "normal"); + insert_attribute(AId::FontVariantPosition, "normal"); + + // Then, we set the properties that have been declared. + shorthand + .font_stretch + .map(|s| insert_attribute(AId::FontStretch, s)); + shorthand + .font_weight + .map(|s| insert_attribute(AId::FontWeight, s)); + shorthand + .font_variant + .map(|s| insert_attribute(AId::FontVariant, s)); + shorthand + .font_style + .map(|s| insert_attribute(AId::FontStyle, s)); + insert_attribute(AId::FontSize, shorthand.font_size); + insert_attribute(AId::FontFamily, shorthand.font_family); + } else { + log::warn!( + "Failed to parse {} value: '{}'", + AId::Font, + declaration.value + ); + } + } else if let Some(aid) = AId::from_str(declaration.name) { + // Parse only the presentation attributes. + if aid.is_presentation() { + insert_attribute(aid, declaration.value); + } + } + }; + // Apply CSS. for rule in &style_sheet.rules { if rule.selector.matches(&XmlNode(xml_node)) { for declaration in &rule.declarations { - // TODO: perform XML attribute normalization - if let Some(aid) = AId::from_str(declaration.name) { - // Parse only the presentation attributes. - if aid.is_presentation() { - insert_attribute(aid, declaration.value); - } - } else if declaration.name == "marker" { - insert_attribute(AId::MarkerStart, declaration.value); - insert_attribute(AId::MarkerMid, declaration.value); - insert_attribute(AId::MarkerEnd, declaration.value); - } + write_declaration(declaration); } } } @@ -288,13 +332,7 @@ pub(crate) fn parse_svg_element<'input>( // Split a `style` attribute. if let Some(value) = xml_node.attribute("style") { for declaration in simplecss::DeclarationTokenizer::from(value) { - // TODO: preform XML attribute normalization - if let Some(aid) = AId::from_str(declaration.name) { - // Parse only the presentation attributes. - if aid.is_presentation() { - insert_attribute(aid, declaration.value); - } - } + write_declaration(&declaration); } } diff --git a/crates/usvg-parser/src/svgtree/text.rs b/crates/usvg/src/parser/svgtree/text.rs similarity index 99% rename from crates/usvg-parser/src/svgtree/text.rs rename to crates/usvg/src/parser/svgtree/text.rs index e7e6b1b64..fab2a9b90 100644 --- a/crates/usvg-parser/src/svgtree/text.rs +++ b/crates/usvg/src/parser/svgtree/text.rs @@ -155,7 +155,7 @@ impl StrTrim for String { } fn remove_last_space(&mut self) { - debug_assert_eq!(self.chars().rev().next().unwrap(), ' '); + debug_assert_eq!(self.chars().next_back().unwrap(), ' '); self.pop(); } } diff --git a/crates/usvg-parser/src/switch.rs b/crates/usvg/src/parser/switch.rs similarity index 90% rename from crates/usvg-parser/src/switch.rs rename to crates/usvg/src/parser/switch.rs index 9e2c25bb7..794bf6d1a 100644 --- a/crates/usvg-parser/src/switch.rs +++ b/crates/usvg/src/parser/switch.rs @@ -2,10 +2,9 @@ // 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/. -use usvg_tree::Node; - -use crate::svgtree::{AId, SvgNode}; -use crate::{converter, Options}; +use super::svgtree::{AId, SvgNode}; +use super::{converter, Options}; +use crate::{Group, Node}; // Full list can be found here: https://www.w3.org/TR/SVG11/feature.html static FEATURES: &[&str] = &[ @@ -46,19 +45,15 @@ pub(crate) fn convert( node: SvgNode, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + parent: &mut Group, ) -> Option<()> { let child = node .children() .find(|n| is_condition_passed(*n, state.opt))?; - match converter::convert_group(node, state, false, cache, parent) { - converter::GroupKind::Create(ref mut g) => { - converter::convert_element(child, state, cache, g); - } - converter::GroupKind::Skip => { - converter::convert_element(child, state, cache, parent); - } - converter::GroupKind::Ignore => {} + if let Some(g) = converter::convert_group(node, state, false, cache, parent, &|cache, g| { + converter::convert_element(child, state, cache, g); + }) { + parent.children.push(Node::Group(Box::new(g))); } Some(()) diff --git a/crates/usvg-parser/src/text.rs b/crates/usvg/src/parser/text.rs similarity index 76% rename from crates/usvg-parser/src/text.rs rename to crates/usvg/src/parser/text.rs index ed2914b31..075c3d476 100644 --- a/crates/usvg-parser/src/text.rs +++ b/crates/usvg/src/parser/text.rs @@ -2,92 +2,107 @@ // 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/. -use std::rc::Rc; +use std::sync::Arc; use kurbo::{ParamCurve, ParamCurveArclen}; -use svgtypes::{Length, LengthUnit}; -use usvg_tree::*; +use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit}; -use crate::svgtree::{AId, EId, FromValue, SvgNode}; -use crate::{converter, style}; +use super::svgtree::{AId, EId, FromValue, SvgNode}; +use super::{converter, style, OptionLog}; +use crate::*; -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::TextAnchor { +impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "start" => Some(usvg_tree::TextAnchor::Start), - "middle" => Some(usvg_tree::TextAnchor::Middle), - "end" => Some(usvg_tree::TextAnchor::End), + "start" => Some(TextAnchor::Start), + "middle" => Some(TextAnchor::Middle), + "end" => Some(TextAnchor::End), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::AlignmentBaseline { +impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "auto" => Some(usvg_tree::AlignmentBaseline::Auto), - "baseline" => Some(usvg_tree::AlignmentBaseline::Baseline), - "before-edge" => Some(usvg_tree::AlignmentBaseline::BeforeEdge), - "text-before-edge" => Some(usvg_tree::AlignmentBaseline::TextBeforeEdge), - "middle" => Some(usvg_tree::AlignmentBaseline::Middle), - "central" => Some(usvg_tree::AlignmentBaseline::Central), - "after-edge" => Some(usvg_tree::AlignmentBaseline::AfterEdge), - "text-after-edge" => Some(usvg_tree::AlignmentBaseline::TextAfterEdge), - "ideographic" => Some(usvg_tree::AlignmentBaseline::Ideographic), - "alphabetic" => Some(usvg_tree::AlignmentBaseline::Alphabetic), - "hanging" => Some(usvg_tree::AlignmentBaseline::Hanging), - "mathematical" => Some(usvg_tree::AlignmentBaseline::Mathematical), + "auto" => Some(AlignmentBaseline::Auto), + "baseline" => Some(AlignmentBaseline::Baseline), + "before-edge" => Some(AlignmentBaseline::BeforeEdge), + "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge), + "middle" => Some(AlignmentBaseline::Middle), + "central" => Some(AlignmentBaseline::Central), + "after-edge" => Some(AlignmentBaseline::AfterEdge), + "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge), + "ideographic" => Some(AlignmentBaseline::Ideographic), + "alphabetic" => Some(AlignmentBaseline::Alphabetic), + "hanging" => Some(AlignmentBaseline::Hanging), + "mathematical" => Some(AlignmentBaseline::Mathematical), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::DominantBaseline { +impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "auto" => Some(usvg_tree::DominantBaseline::Auto), - "use-script" => Some(usvg_tree::DominantBaseline::UseScript), - "no-change" => Some(usvg_tree::DominantBaseline::NoChange), - "reset-size" => Some(usvg_tree::DominantBaseline::ResetSize), - "ideographic" => Some(usvg_tree::DominantBaseline::Ideographic), - "alphabetic" => Some(usvg_tree::DominantBaseline::Alphabetic), - "hanging" => Some(usvg_tree::DominantBaseline::Hanging), - "mathematical" => Some(usvg_tree::DominantBaseline::Mathematical), - "central" => Some(usvg_tree::DominantBaseline::Central), - "middle" => Some(usvg_tree::DominantBaseline::Middle), - "text-after-edge" => Some(usvg_tree::DominantBaseline::TextAfterEdge), - "text-before-edge" => Some(usvg_tree::DominantBaseline::TextBeforeEdge), + "auto" => Some(DominantBaseline::Auto), + "use-script" => Some(DominantBaseline::UseScript), + "no-change" => Some(DominantBaseline::NoChange), + "reset-size" => Some(DominantBaseline::ResetSize), + "ideographic" => Some(DominantBaseline::Ideographic), + "alphabetic" => Some(DominantBaseline::Alphabetic), + "hanging" => Some(DominantBaseline::Hanging), + "mathematical" => Some(DominantBaseline::Mathematical), + "central" => Some(DominantBaseline::Central), + "middle" => Some(DominantBaseline::Middle), + "text-after-edge" => Some(DominantBaseline::TextAfterEdge), + "text-before-edge" => Some(DominantBaseline::TextBeforeEdge), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::LengthAdjust { +impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "spacing" => Some(usvg_tree::LengthAdjust::Spacing), - "spacingAndGlyphs" => Some(usvg_tree::LengthAdjust::SpacingAndGlyphs), + "spacing" => Some(LengthAdjust::Spacing), + "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs), _ => None, } } } -impl<'a, 'input: 'a> FromValue<'a, 'input> for usvg_tree::FontStyle { +impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle { fn parse(_: SvgNode, _: AId, value: &str) -> Option { match value { - "normal" => Some(usvg_tree::FontStyle::Normal), - "italic" => Some(usvg_tree::FontStyle::Italic), - "oblique" => Some(usvg_tree::FontStyle::Oblique), + "normal" => Some(FontStyle::Normal), + "italic" => Some(FontStyle::Italic), + "oblique" => Some(FontStyle::Oblique), _ => None, } } } +/// A text character position. +/// +/// _Character_ is a Unicode codepoint. +#[derive(Clone, Copy, Debug)] +struct CharacterPosition { + /// An absolute X axis position. + x: Option, + /// An absolute Y axis position. + y: Option, + /// A relative X axis offset. + dx: Option, + /// A relative Y axis offset. + dy: Option, +} + pub(crate) fn convert( text_node: SvgNode, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + parent: &mut Group, ) { let pos_list = resolve_positions_list(text_node, state); let rotate_list = resolve_rotate_list(text_node); @@ -108,18 +123,33 @@ pub(crate) fn convert( #[cfg(feature = "class")] let class = text_node.class().to_string(); - let text = Text { + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + + let mut text = Text { id, #[cfg(feature = "class")] class, - transform: Transform::default(), rendering_mode, - positions: pos_list, + dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(), + dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(), rotate: rotate_list, writing_mode, chunks, + abs_transform: parent.abs_transform, + // All fields below will be reset by `text_to_paths`. + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + flattened: Box::new(Group::empty()), + layouted: vec![], }; - parent.append_kind(NodeKind::Text(text)); + + if text::convert(&mut text, state.fontdb).is_none() { + return; + } + + parent.children.push(Node::Text(Box::new(text))); } struct IterState { @@ -144,20 +174,12 @@ fn collect_text_chunks( chunks: Vec::new(), }; - collect_text_chunks_impl( - text_node, - text_node, - pos_list, - state, - cache, - &mut iter_state, - ); + collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state); iter_state.chunks } fn collect_text_chunks_impl( - text_node: SvgNode, parent: SvgNode, pos_list: &[CharacterPosition], state: &converter::State, @@ -189,7 +211,7 @@ fn collect_text_chunks_impl( iter_state.split_chunk = true; } - collect_text_chunks_impl(text_node, child, pos_list, state, cache, iter_state); + collect_text_chunks_impl(child, pos_list, state, cache, iter_state); iter_state.text_flow = TextFlow::Linear; @@ -209,7 +231,7 @@ fn collect_text_chunks_impl( let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default(); // TODO: what to do when <= 0? UB? - let font_size = crate::units::resolve_font_size(parent, state); + let font_size = super::units::resolve_font_size(parent, state); let font_size = match NonZeroPositiveF32::new(font_size) { Some(n) => n, None => { @@ -223,7 +245,7 @@ fn collect_text_chunks_impl( let raw_paint_order: svgtypes::PaintOrder = parent.find_attribute(AId::PaintOrder).unwrap_or_default(); - let paint_order = crate::converter::svg_paint_order_to_usvg(raw_paint_order); + let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order); let mut dominant_baseline = parent .find_attribute(AId::DominantBaseline) @@ -265,7 +287,7 @@ fn collect_text_chunks_impl( font_size, small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"), apply_kerning, - decoration: resolve_decoration(text_node, parent, state, cache), + decoration: resolve_decoration(parent, state, cache), visibility: parent.find_attribute(AId::Visibility).unwrap_or_default(), dominant_baseline, alignment_baseline: parent @@ -339,13 +361,14 @@ fn collect_text_chunks_impl( fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option { let linked_node = node.attribute::(AId::Href)?; - let path = crate::shapes::convert(linked_node, state)?; + let path = super::shapes::convert(linked_node, state)?; // The reference path's transform needs to be applied - let path = if let Some(node_transform) = linked_node.attribute::(AId::Transform) { + let transform = linked_node.resolve_transform(AId::Transform, state); + let path = if !transform.is_identity() { let mut path_copy = path.as_ref().clone(); - path_copy = path_copy.transform(node_transform)?; - Rc::new(path_copy) + path_copy = path_copy.transform(transform)?; + Arc::new(path_copy) } else { path }; @@ -360,7 +383,16 @@ fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option Font { @@ -368,33 +400,27 @@ fn convert_font(node: SvgNode, state: &converter::State) -> Font { let stretch = conv_font_stretch(node); let weight = resolve_font_weight(node); - let font_family = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) { + let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) + { n.attribute(AId::FontFamily).unwrap_or("") } else { "" }; - let mut families = Vec::new(); - for mut family in font_family.split(',') { - // TODO: to a proper parser - - if family.starts_with(['\'', '"']) { - family = &family[1..]; - } - - if family.ends_with(['\'', '"']) { - family = &family[..family.len() - 1]; - } - - family = family.trim(); - - if !family.is_empty() { - families.push(family.to_string()); - } - } + let mut families = parse_font_families(font_families) + .ok() + .log_none(|| { + log::warn!( + "Failed to parse {} value: '{}'. Falling back to {}.", + AId::FontFamily, + font_families, + state.opt.font_family + ) + }) + .unwrap_or_default(); if families.is_empty() { - families.push(state.opt.font_family.clone()) + families.push(FontFamily::Named(state.opt.font_family.clone())) } Font { @@ -555,7 +581,7 @@ fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec { - if let Some(num_list) = crate::units::convert_list(child, $aid, state) { + if let Some(num_list) = super::units::convert_list(child, $aid, state) { // Note that we are using not the total count, // but the amount of characters in the current `tspan` (with children). let len = std::cmp::min(num_list.len(), child_chars); @@ -616,73 +642,55 @@ fn resolve_rotate_list(text_node: SvgNode) -> Vec { } /// Resolves node's `text-decoration` property. -/// -/// `text` and `tspan` can point to the same node. fn resolve_decoration( - text_node: SvgNode, tspan: SvgNode, state: &converter::State, cache: &mut converter::Cache, ) -> TextDecoration { - // TODO: explain the algorithm - - let text_dec = conv_text_decoration(text_node); - let tspan_dec = conv_text_decoration2(tspan); - - let mut gen_style = |in_tspan: bool, in_text: bool| { - let n = if in_tspan { - tspan - } else if in_text { - text_node + // Checks if a decoration is present in a single node. + fn find_decoration(node: SvgNode, value: &str) -> bool { + if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) { + str_value.split(' ').any(|v| v == value) } else { + false + } + } + + // The algorithm is as follows: First, we check whether the given text decoration appears in ANY + // ancestor, i.e. it can also appear in ancestors outside of the element. If the text + // decoration is declared somewhere, it means that this tspan will have it. However, we still + // need to find the corresponding fill/stroke for it. To do this, we iterate through all + // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will + // stop at latest at the text node, and use its fill/stroke. + let mut gen_style = |text_decoration: &str| { + if !tspan + .ancestors() + .any(|n| find_decoration(n, text_decoration)) + { return None; - }; + } + + let mut fill_node = None; + let mut stroke_node = None; + + for node in tspan.ancestors() { + if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) { + fill_node = fill_node.map_or(Some(node), Some); + stroke_node = stroke_node.map_or(Some(node), Some); + break; + } + } Some(TextDecorationStyle { - fill: style::resolve_fill(n, true, state, cache), - stroke: style::resolve_stroke(n, true, state, cache), + fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)), + stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)), }) }; TextDecoration { - underline: gen_style(tspan_dec.has_underline, text_dec.has_underline), - overline: gen_style(tspan_dec.has_overline, text_dec.has_overline), - line_through: gen_style(tspan_dec.has_line_through, text_dec.has_line_through), - } -} - -struct TextDecorationTypes { - has_underline: bool, - has_overline: bool, - has_line_through: bool, -} - -/// Resolves the `text` node's `text-decoration` property. -fn conv_text_decoration(text_node: SvgNode) -> TextDecorationTypes { - fn find_decoration(node: SvgNode, value: &str) -> bool { - node.ancestors().any(|n| { - if let Some(str_value) = n.attribute::<&str>(AId::TextDecoration) { - str_value.split(' ').any(|v| v == value) - } else { - false - } - }) - } - - TextDecorationTypes { - has_underline: find_decoration(text_node, "underline"), - has_overline: find_decoration(text_node, "overline"), - has_line_through: find_decoration(text_node, "line-through"), - } -} - -/// Resolves the default `text-decoration` property. -fn conv_text_decoration2(tspan: SvgNode) -> TextDecorationTypes { - let s = tspan.attribute(AId::TextDecoration); - TextDecorationTypes { - has_underline: s == Some("underline"), - has_overline: s == Some("overline"), - has_line_through: s == Some("line-through"), + underline: gen_style("underline"), + overline: gen_style("overline"), + line_through: gen_style("line-through"), } } @@ -693,12 +701,12 @@ fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec(AId::BaselineShift) { + if let Some(len) = n.try_attribute::(AId::BaselineShift) { if len.unit == LengthUnit::Percent { - let n = crate::units::resolve_font_size(n, state) * (len.number as f32 / 100.0); + let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0); shift.push(BaselineShift::Number(n)); } else { - let n = crate::units::convert_length( + let n = super::units::convert_length( len, n, AId::BaselineShift, diff --git a/crates/usvg-parser/src/units.rs b/crates/usvg/src/parser/units.rs similarity index 96% rename from crates/usvg-parser/src/units.rs rename to crates/usvg/src/parser/units.rs index 951a7cbfe..f15abb241 100644 --- a/crates/usvg-parser/src/units.rs +++ b/crates/usvg/src/parser/units.rs @@ -3,10 +3,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use svgtypes::{Length, LengthUnit as Unit}; -use usvg_tree::Units; -use crate::converter; -use crate::svgtree::{AId, SvgNode}; +use super::converter; +use super::svgtree::{AId, SvgNode}; +use crate::Units; #[inline(never)] pub(crate) fn convert_length( @@ -98,7 +98,7 @@ pub(crate) fn resolve_font_size(node: SvgNode, state: &converter::State) -> f32 let mut font_size = state.opt.font_size; for n in nodes.iter().rev().skip(1) { // skip Root - if let Some(length) = n.attribute::(AId::FontSize) { + if let Some(length) = n.try_attribute::(AId::FontSize) { let dpi = state.opt.dpi; let n = length.number as f32; font_size = match length.unit { diff --git a/crates/usvg-parser/src/use_node.rs b/crates/usvg/src/parser/use_node.rs similarity index 63% rename from crates/usvg-parser/src/use_node.rs rename to crates/usvg/src/parser/use_node.rs index b3bfab2ec..a15c08871 100644 --- a/crates/usvg-parser/src/use_node.rs +++ b/crates/usvg/src/parser/use_node.rs @@ -2,70 +2,87 @@ // 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/. -use std::rc::Rc; +use std::sync::Arc; use svgtypes::{Length, LengthUnit}; -use usvg_tree::{ - tiny_skia_path, Group, IsValidLength, Node, NodeExt, NodeKind, NonZeroRect, Path, Size, - Transform, -}; -use crate::converter; -use crate::svgtree::{AId, EId, SvgNode}; +use super::svgtree::{AId, EId, SvgNode}; +use super::{converter, style}; +use crate::tree::ContextElement; +use crate::{Group, IsValidLength, Node, NonZeroRect, Path, Size, Transform, ViewBox}; pub(crate) fn convert( node: SvgNode, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, -) -> Option<()> { - let child = node.first_child()?; + parent: &mut Group, +) { + let child = match node.first_child() { + Some(v) => v, + None => return, + }; if state.parent_clip_path.is_some() && child.tag_name() == Some(EId::Symbol) { // Ignore `symbol` referenced by `use` inside a `clipPath`. // It will be ignored later anyway, but this will prevent // a redundant `clipPath` creation (which is required for `symbol`). - return None; + return; } + let mut use_state = state.clone(); + use_state.context_element = Some(( + style::resolve_fill(node, true, state, cache).map(|mut f| { + f.context_element = Some(ContextElement::UseNode); + f + }), + style::resolve_stroke(node, true, state, cache).map(|mut s| { + s.context_element = Some(ContextElement::UseNode); + s + }), + )); + // We require an original transformation to setup 'clipPath'. - let mut orig_ts: Transform = node.attribute(AId::Transform).unwrap_or_default(); + let mut orig_ts = node.resolve_transform(AId::Transform, state); let mut new_ts = Transform::default(); { - let x = node.convert_user_length(AId::X, state, Length::zero()); - let y = node.convert_user_length(AId::Y, state, Length::zero()); + let x = node.convert_user_length(AId::X, &use_state, Length::zero()); + let y = node.convert_user_length(AId::Y, &use_state, Length::zero()); new_ts = new_ts.pre_translate(x, y); } let linked_to_symbol = child.tag_name() == Some(EId::Symbol); if linked_to_symbol { - if let Some(ts) = viewbox_transform(node, child, state) { + if let Some(ts) = viewbox_transform(node, child, &use_state) { new_ts = new_ts.pre_concat(ts); } - if let Some(clip_rect) = get_clip_rect(node, child, state) { - let mut g = clip_element(node, clip_rect, orig_ts, state, cache, parent); + if let Some(clip_rect) = get_clip_rect(node, child, &use_state) { + let mut g = clip_element(node, clip_rect, orig_ts, &use_state, cache); + g.abs_transform = parent.abs_transform; // Make group for `use`. - let mut parent = match converter::convert_group(node, state, true, cache, &mut g) { - converter::GroupKind::Create(g) => { - // We must reset transform, because it was already set - // to the group with clip-path. - if let NodeKind::Group(ref mut g) = *g.borrow_mut() { - g.id = String::new(); // Prevent ID duplication. - g.transform = Transform::default(); - } - - g - } - converter::GroupKind::Skip => g.clone(), - converter::GroupKind::Ignore => return None, - }; - - convert_children(child, new_ts, state, cache, &mut parent); - return None; + if let Some(mut g2) = + converter::convert_group(node, &use_state, true, cache, &mut g, &|cache, g2| { + convert_children(child, new_ts, &use_state, cache, false, g2); + }) + { + // We must reset transform, because it was already set + // to the group with clip-path. + g.is_context_element = true; + g2.id = String::new(); // Prevent ID duplication. + g2.transform = Transform::default(); + g.children.push(Node::Group(Box::new(g2))); + } + + if g.children.is_empty() { + return; + } + + g.calculate_bounding_boxes(); + parent.children.push(Node::Group(Box::new(g))); + return; } } @@ -73,19 +90,15 @@ pub(crate) fn convert( if linked_to_symbol { // Make group for `use`. - let mut parent = match converter::convert_group(node, state, false, cache, parent) { - converter::GroupKind::Create(g) => { - if let NodeKind::Group(ref mut g) = *g.borrow_mut() { - g.transform = Transform::default(); - } - - g - } - converter::GroupKind::Skip => parent.clone(), - converter::GroupKind::Ignore => return None, - }; - - convert_children(child, orig_ts, state, cache, &mut parent); + if let Some(mut g) = + converter::convert_group(node, &use_state, false, cache, parent, &|cache, g| { + convert_children(child, orig_ts, &use_state, cache, false, g); + }) + { + g.is_context_element = true; + g.transform = Transform::default(); + parent.children.push(Node::Group(Box::new(g))); + } } else { let linked_to_svg = child.tag_name() == Some(EId::Svg); if linked_to_svg { @@ -94,8 +107,6 @@ pub(crate) fn convert( // instead of `svg` element size. let def = Length::new(100.0, LengthUnit::Percent); - - let mut state = state.clone(); // As per usual, the SVG spec doesn't clarify this edge case, // but it seems like `use` size has to be reset by each `use`. // Meaning if we have two nested `use` elements, where one had set `width` and @@ -107,33 +118,31 @@ pub(crate) fn convert( // // // In this case `svg2` size is 80x100 and not 100x100. - state.use_size = (None, None); + use_state.use_size = (None, None); // Width and height can be set independently. if node.has_attribute(AId::Width) { - state.use_size.0 = Some(node.convert_user_length(AId::Width, &state, def)); + use_state.use_size.0 = Some(node.convert_user_length(AId::Width, &use_state, def)); } if node.has_attribute(AId::Height) { - state.use_size.1 = Some(node.convert_user_length(AId::Height, &state, def)); + use_state.use_size.1 = Some(node.convert_user_length(AId::Height, &use_state, def)); } - convert_children(node, orig_ts, &state, cache, parent); + convert_children(node, orig_ts, &use_state, cache, true, parent); } else { - convert_children(node, orig_ts, state, cache, parent); + convert_children(node, orig_ts, &use_state, cache, true, parent); } } - - Some(()) } pub(crate) fn convert_svg( node: SvgNode, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + parent: &mut Group, ) { // We require original transformation to setup 'clipPath'. - let mut orig_ts: Transform = node.attribute(AId::Transform).unwrap_or_default(); + let mut orig_ts = node.resolve_transform(AId::Transform, state); let mut new_ts = Transform::default(); { @@ -169,11 +178,14 @@ pub(crate) fn convert_svg( }; if let Some(clip_rect) = get_clip_rect(node, node, state) { - let mut g = clip_element(node, clip_rect, orig_ts, state, cache, parent); - convert_children(node, new_ts, &new_state, cache, &mut g); + let mut g = clip_element(node, clip_rect, orig_ts, state, cache); + g.abs_transform = parent.abs_transform; + convert_children(node, new_ts, &new_state, cache, false, &mut g); + g.calculate_bounding_boxes(); + parent.children.push(Node::Group(Box::new(g))); } else { orig_ts = orig_ts.pre_concat(new_ts); - convert_children(node, orig_ts, &new_state, cache, parent); + convert_children(node, orig_ts, &new_state, cache, false, parent); } } @@ -183,8 +195,7 @@ fn clip_element( transform: Transform, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, -) -> Node { +) -> Group { // We can't set `clip-path` on the element itself, // because it will be affected by a possible transform. // So we have to create an additional group. @@ -205,14 +216,14 @@ fn clip_element( // // - let mut clip_path = usvg_tree::ClipPath::default(); - clip_path.id = cache.gen_clip_path_id(); + let mut clip_path = crate::ClipPath::empty(cache.gen_clip_path_id()); - let mut path = Path::new(Rc::new(tiny_skia_path::PathBuilder::from_rect( + let mut path = Path::new_simple(Arc::new(tiny_skia_path::PathBuilder::from_rect( clip_rect.to_rect(), - ))); - path.fill = Some(usvg_tree::Fill::default()); - clip_path.root.append_kind(NodeKind::Path(path)); + ))) + .unwrap(); + path.fill = Some(crate::Fill::default()); + clip_path.root.children.push(Node::Path(Box::new(path))); // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. let id = if state.parent_markers.is_empty() { @@ -221,12 +232,12 @@ fn clip_element( String::new() }; - parent.append_kind(NodeKind::Group(Group { + Group { id, transform, - clip_path: Some(Rc::new(clip_path)), - ..Group::default() - })) + clip_path: Some(Arc::new(clip_path)), + ..Group::empty() + } } fn convert_children( @@ -234,26 +245,29 @@ fn convert_children( transform: Transform, state: &converter::State, cache: &mut converter::Cache, - parent: &mut Node, + is_context_element: bool, + parent: &mut Group, ) { + // Temporarily adjust absolute transform so `convert_group` would account for `transform`. + let old_abs_transform = parent.abs_transform; + parent.abs_transform = parent.abs_transform.pre_concat(transform); + let required = !transform.is_identity(); - let mut parent = match converter::convert_group(node, state, required, cache, parent) { - converter::GroupKind::Create(g) => { - if let NodeKind::Group(ref mut g) = *g.borrow_mut() { - g.transform = transform; + if let Some(mut g) = + converter::convert_group(node, state, required, cache, parent, &|cache, g| { + if state.parent_clip_path.is_some() { + converter::convert_clip_path_elements(node, state, cache, g); + } else { + converter::convert_children(node, state, cache, g); } - - g - } - converter::GroupKind::Skip => parent.clone(), - converter::GroupKind::Ignore => return, - }; - - if state.parent_clip_path.is_some() { - converter::convert_clip_path_elements(node, state, cache, &mut parent); - } else { - converter::convert_children(node, state, cache, &mut parent); + }) + { + g.is_context_element = is_context_element; + g.transform = transform; + parent.children.push(Node::Group(Box::new(g))); } + + parent.abs_transform = old_abs_transform; } fn get_clip_rect( @@ -325,10 +339,11 @@ fn viewbox_transform( } let size = Size::from_wh(w, h)?; - let vb = linked.parse_viewbox()?; + let rect = linked.parse_viewbox()?; let aspect = linked .attribute(AId::PreserveAspectRatio) .unwrap_or_default(); + let view_box = ViewBox { rect, aspect }; - Some(usvg_tree::utils::view_box_to_transform(vb, aspect, size)) + Some(view_box.to_transform(size)) } diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs new file mode 100644 index 000000000..78bed3757 --- /dev/null +++ b/crates/usvg/src/text/flatten.rs @@ -0,0 +1,135 @@ +// 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/. + +use fontdb::{Database, ID}; +use rustybuzz::ttf_parser; +use rustybuzz::ttf_parser::GlyphId; +use std::sync::Arc; +use tiny_skia_path::{NonZeroRect, Transform}; + +use crate::tree::BBox; +use crate::{Group, Node, Path, ShapeRendering, Text, TextRendering}; + +fn resolve_rendering_mode(text: &Text) -> ShapeRendering { + match text.rendering_mode { + TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges, + TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision, + TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision, + } +} + +pub(crate) fn flatten(text: &mut Text, fontdb: &fontdb::Database) -> Option<(Group, NonZeroRect)> { + let mut new_paths = vec![]; + + let mut stroke_bbox = BBox::default(); + let rendering_mode = resolve_rendering_mode(text); + + for span in &text.layouted { + if let Some(path) = span.overline.as_ref() { + stroke_bbox = stroke_bbox.expand(path.data.bounds()); + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_paths.push(path); + } + + if let Some(path) = span.underline.as_ref() { + stroke_bbox = stroke_bbox.expand(path.data.bounds()); + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_paths.push(path); + } + + let mut span_builder = tiny_skia_path::PathBuilder::new(); + + for glyph in &span.positioned_glyphs { + if let Some(outline) = fontdb.outline(glyph.font, glyph.glyph_id) { + if let Some(outline) = outline.transform(glyph.transform) { + span_builder.push_path(&outline); + } + } + } + + if let Some(path) = span_builder.finish().and_then(|p| { + Path::new( + String::new(), + #[cfg(feature = "class")] + String::new(), + span.visibility, + span.fill.clone(), + span.stroke.clone(), + span.paint_order, + rendering_mode, + Arc::new(p), + Transform::default(), + ) + }) { + stroke_bbox = stroke_bbox.expand(path.stroke_bounding_box()); + new_paths.push(path); + } + + if let Some(path) = span.line_through.as_ref() { + stroke_bbox = stroke_bbox.expand(path.data.bounds()); + let mut path = path.clone(); + path.rendering_mode = rendering_mode; + new_paths.push(path); + } + } + + let mut group = Group { + id: text.id.clone(), + ..Group::empty() + }; + + for path in new_paths { + group.children.push(Node::Path(Box::new(path))); + } + + group.calculate_bounding_boxes(); + Some((group, stroke_bbox.to_non_zero_rect()?)) +} + +struct PathBuilder { + builder: tiny_skia_path::PathBuilder, +} + +impl ttf_parser::OutlineBuilder for PathBuilder { + fn move_to(&mut self, x: f32, y: f32) { + self.builder.move_to(x, y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.builder.line_to(x, y); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + self.builder.quad_to(x1, y1, x, y); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + self.builder.cubic_to(x1, y1, x2, y2, x, y); + } + + fn close(&mut self) { + self.builder.close(); + } +} + +pub(crate) trait DatabaseExt { + fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; +} + +impl DatabaseExt for Database { + #[inline(never)] + fn outline(&self, id: ID, glyph_id: GlyphId) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + + let mut builder = PathBuilder { + builder: tiny_skia_path::PathBuilder::new(), + }; + font.outline_glyph(glyph_id, &mut builder)?; + builder.builder.finish() + })? + } +} diff --git a/crates/usvg-text-layout/src/lib.rs b/crates/usvg/src/text/layout.rs similarity index 71% rename from crates/usvg-text-layout/src/lib.rs rename to crates/usvg/src/text/layout.rs index 2bd49f453..3a6838bdf 100644 --- a/crates/usvg-text-layout/src/lib.rs +++ b/crates/usvg/src/text/layout.rs @@ -2,430 +2,475 @@ // 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/. -/*! -An [SVG] text layout implementation on top of [usvg] crate. - -[usvg]: https://github.com/RazrFalcon/resvg/crates/usvg -[SVG]: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -*/ - -#![forbid(unsafe_code)] -#![warn(missing_docs)] -#![warn(missing_debug_implementations)] -#![warn(missing_copy_implementations)] -#![allow(clippy::many_single_char_names)] -#![allow(clippy::collapsible_else_if)] -#![allow(clippy::too_many_arguments)] -#![allow(clippy::neg_cmp_op_on_partial_ord)] -#![allow(clippy::identity_op)] -#![allow(clippy::question_mark)] -#![allow(clippy::upper_case_acronyms)] - -pub use fontdb; - use std::collections::HashMap; -use std::convert::TryFrom; use std::num::NonZeroU16; -use std::rc::Rc; +use std::sync::Arc; use fontdb::{Database, ID}; use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; use rustybuzz::ttf_parser; -use ttf_parser::GlyphId; +use rustybuzz::ttf_parser::GlyphId; +use strict_num::NonZeroPositiveF32; +use svgtypes::FontFamily; +use tiny_skia_path::{NonZeroRect, Transform}; use unicode_script::UnicodeScript; -use usvg_tree::*; -/// A `usvg::Tree` extension trait. -pub trait TreeTextToPath { - /// Converts text nodes into paths. - fn convert_text(&mut self, fontdb: &fontdb::Database); +use crate::tree::{BBox, IsValidLength}; +use crate::{ + AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font, + FontStretch, FontStyle, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, + TextAnchor, TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, Visibility, + WritingMode, +}; + +/// A glyph that has already been positioned correctly. +/// +/// Note that the transform already takes the font size into consideration, so applying the +/// transform to the outline of the glyphs is all that is necessary to display it correctly. +#[derive(Clone, Debug)] +pub struct PositionedGlyph { + /// The transform of the glyph. This transform should be applied to the _glyph outlines_, meaning + /// that paint servers referenced by the glyph's span should not be affected by it. + pub transform: Transform, + /// The ID of the glyph. + pub glyph_id: GlyphId, + /// The text from the original string that corresponds to that glyph. + pub text: String, + /// The ID of the font the glyph should be taken from. + pub font: ID, } -impl TreeTextToPath for usvg_tree::Tree { - fn convert_text(&mut self, fontdb: &fontdb::Database) { - convert_text(self.root.clone(), fontdb); - } +/// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and +/// visibility. +#[derive(Clone, Debug)] +pub struct Span { + /// The fill of the span. + pub fill: Option, + /// The stroke of the span. + pub stroke: Option, + /// The paint order of the span. + pub paint_order: PaintOrder, + /// The font size of the span. + pub font_size: NonZeroPositiveF32, + /// The visibility of the span. + pub visibility: Visibility, + /// The glyphs that make up the span. + pub positioned_glyphs: Vec, + /// An underline text decoration of the span. + /// Needs to be rendered before all glyphs. + pub underline: Option, + /// An overline text decoration of the span. + /// Needs to be rendered before all glyphs. + pub overline: Option, + /// A line-through text decoration of the span. + /// Needs to be rendered after all glyphs. + pub line_through: Option, } -/// A `usvg::Text` extension trait. -pub trait TextToPath { - /// Converts the text node into path(s). - /// - /// `absolute_ts` is node's absolute transform. Used primarily during text-on-path resolving. - fn convert(&self, fontdb: &fontdb::Database, absolute_ts: Transform) -> Option; +#[derive(Clone, Debug)] +struct GlyphCluster { + byte_idx: ByteIndex, + codepoint: char, + width: f32, + advance: f32, + ascent: f32, + descent: f32, + x_height: f32, + has_relative_shift: bool, + glyphs: Vec, + transform: Transform, + path_transform: Transform, + visible: bool, } -impl TextToPath for Text { - fn convert(&self, fontdb: &fontdb::Database, absolute_ts: Transform) -> Option { - let (new_paths, bbox) = text_to_paths(self, fontdb, absolute_ts)?; - - // Create a group will all paths that was created during text-to-path conversion. - let group = Node::new(NodeKind::Group(Group { - id: self.id.clone(), - transform: self.transform, - ..Group::default() - })); - - let rendering_mode = resolve_rendering_mode(self); - for mut path in new_paths { - fix_obj_bounding_box(&mut path, bbox); - path.rendering_mode = rendering_mode; - group.append_kind(NodeKind::Path(path)); - } +impl GlyphCluster { + pub(crate) fn height(&self) -> f32 { + self.ascent - self.descent + } - Some(group) + pub(crate) fn transform(&self) -> Transform { + self.path_transform.post_concat(self.transform) } } -fn convert_text(root: Node, fontdb: &fontdb::Database) { - let mut text_nodes = Vec::new(); - // We have to update text nodes in clipPaths, masks and patterns as well. - for node in root.descendants() { - if let NodeKind::Text(_) = *node.borrow() { - text_nodes.push(node.clone()); - } +pub(crate) fn layout_text( + text_node: &Text, + fontdb: &fontdb::Database, +) -> Option<(Vec, NonZeroRect)> { + let mut fonts_cache: FontsCache = HashMap::new(); - node.subroots(|subroot| convert_text(subroot, fontdb)) + for chunk in &text_node.chunks { + for span in &chunk.spans { + if !fonts_cache.contains_key(&span.font) { + if let Some(font) = resolve_font(&span.font, fontdb) { + fonts_cache.insert(span.font.clone(), Arc::new(font)); + } + } + } } - if text_nodes.is_empty() { - return; - } + let mut spans = vec![]; + let mut char_offset = 0; + let mut last_x = 0.0; + let mut last_y = 0.0; + let mut bbox = BBox::default(); + for chunk in &text_node.chunks { + let (x, y) = match chunk.text_flow { + TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)), + TextFlow::Path(_) => (0.0, 0.0), + }; - for node in &text_nodes { - let mut new_node = None; - if let NodeKind::Text(ref text) = *node.borrow() { - let mut absolute_ts = node.parent().unwrap().abs_transform(); - absolute_ts = absolute_ts.pre_concat(text.transform); - new_node = text.convert(fontdb, absolute_ts); + let mut clusters = process_chunk(chunk, &fonts_cache, fontdb); + if clusters.is_empty() { + char_offset += chunk.text.chars().count(); + continue; } - if let Some(new_node) = new_node { - node.insert_after(new_node); + apply_writing_mode(text_node.writing_mode, &mut clusters); + apply_letter_spacing(chunk, &mut clusters); + apply_word_spacing(chunk, &mut clusters); + + apply_length_adjust(chunk, &mut clusters); + let mut curr_pos = resolve_clusters_positions( + text_node, + chunk, + char_offset, + text_node.writing_mode, + &fonts_cache, + &mut clusters, + ); + + let mut text_ts = Transform::default(); + if text_node.writing_mode == WritingMode::TopToBottom { + if let TextFlow::Linear = chunk.text_flow { + text_ts = text_ts.pre_rotate_at(90.0, x, y); + } } - } - text_nodes.iter().for_each(|n| n.detach()); -} + for span in &chunk.spans { + let font = match fonts_cache.get(&span.font) { + Some(v) => v, + None => continue, + }; -trait DatabaseExt { - fn load_font(&self, id: ID) -> Option; - fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; - fn has_char(&self, id: ID, c: char) -> bool; -} + let decoration_spans = collect_decoration_spans(span, &clusters); -impl DatabaseExt for Database { - #[inline(never)] - fn load_font(&self, id: ID) -> Option { - self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; + let mut span_ts = text_ts; + span_ts = span_ts.pre_translate(x, y); + if let TextFlow::Linear = chunk.text_flow { + let shift = resolve_baseline(span, font, text_node.writing_mode); - let units_per_em = NonZeroU16::new(font.units_per_em())?; + // In case of a horizontal flow, shift transform and not clusters, + // because clusters can be rotated and an additional shift will lead + // to invalid results. + span_ts = span_ts.pre_translate(0.0, shift); + } - let ascent = font.ascender(); - let descent = font.descender(); + let mut underline = None; + let mut overline = None; + let mut line_through = None; - let x_height = font - .x_height() - .and_then(|x| u16::try_from(x).ok()) - .and_then(NonZeroU16::new); - let x_height = match x_height { - Some(height) => height, - None => { - // If not set - fallback to height * 45%. - // 45% is what Firefox uses. - u16::try_from((f32::from(ascent - descent) * 0.45) as i32) - .ok() - .and_then(NonZeroU16::new)? + if let Some(decoration) = span.decoration.underline.clone() { + // TODO: No idea what offset should be used for top-to-bottom layout. + // There is + // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property + // but it doesn't go into details. + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.underline_position(span.font_size.get()), + WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0, + }; + + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + underline = Some(path); } - }; + } - let line_through = font.strikeout_metrics(); - let line_through_position = match line_through { - Some(metrics) => metrics.position, - None => x_height.get() as i16 / 2, - }; + if let Some(decoration) = span.decoration.overline.clone() { + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.ascent(span.font_size.get()), + WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0, + }; - let (underline_position, underline_thickness) = match font.underline_metrics() { - Some(metrics) => { - let thickness = u16::try_from(metrics.thickness) - .ok() - .and_then(NonZeroU16::new) - // `ttf_parser` guarantees that units_per_em is >= 16 - .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + overline = Some(path); + } + } - (metrics.position, thickness) + if let Some(decoration) = span.decoration.line_through.clone() { + let offset = match text_node.writing_mode { + WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()), + WritingMode::TopToBottom => 0.0, + }; + + if let Some(path) = + convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) + { + bbox = bbox.expand(path.data.bounds()); + line_through = Some(path); } - None => ( - -(units_per_em.get() as i16) / 9, - NonZeroU16::new(units_per_em.get() / 12).unwrap(), - ), - }; + } - // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). - let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; - let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; - if let Some(metrics) = font.subscript_metrics() { - subscript_offset = metrics.y_offset; + let mut fill = span.fill.clone(); + if let Some(ref mut fill) = fill { + // The `fill-rule` should be ignored. + // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder + // + // 'Since the fill-rule property does not apply to SVG text elements, + // the specific order of the subpaths within the equivalent path does not matter.' + fill.rule = FillRule::NonZero; } - if let Some(metrics) = font.superscript_metrics() { - superscript_offset = metrics.y_offset; + if let Some((span_fragments, span_bbox)) = convert_span(span, &clusters, span_ts) { + bbox = bbox.expand(span_bbox); + + let positioned_glyphs = span_fragments + .into_iter() + .flat_map(|mut gc| { + let cluster_ts = gc.transform(); + gc.glyphs.iter_mut().for_each(|pg| { + pg.transform = pg.transform.post_concat(cluster_ts).post_concat(span_ts) + }); + gc.glyphs + }) + .collect(); + + spans.push(Span { + fill, + stroke: span.stroke.clone(), + paint_order: span.paint_order, + font_size: span.font_size, + visibility: span.visibility, + positioned_glyphs, + underline, + overline, + line_through, + }); } + } - Some(ResolvedFont { - id, - units_per_em, - ascent, - descent, - x_height, - underline_position, - underline_thickness, - line_through_position, - subscript_offset, - superscript_offset, - }) - })? - } + char_offset += chunk.text.chars().count(); - #[inline(never)] - fn outline(&self, id: ID, glyph_id: GlyphId) -> Option { - self.with_face_data(id, |data, face_index| -> Option { - let font = ttf_parser::Face::parse(data, face_index).ok()?; + if text_node.writing_mode == WritingMode::TopToBottom { + if let TextFlow::Linear = chunk.text_flow { + std::mem::swap(&mut curr_pos.0, &mut curr_pos.1); + } + } - let mut builder = PathBuilder { - builder: tiny_skia_path::PathBuilder::new(), - }; - font.outline_glyph(glyph_id, &mut builder)?; - builder.builder.finish() - })? + last_x = x + curr_pos.0; + last_y = y + curr_pos.1; } - #[inline(never)] - fn has_char(&self, id: ID, c: char) -> bool { - let res = self.with_face_data(id, |font_data, face_index| -> Option { - let font = ttf_parser::Face::parse(font_data, face_index).ok()?; - font.glyph_index(c)?; - Some(true) - }); + let bbox = bbox.to_non_zero_rect()?; - res == Some(Some(true)) - } + Some((spans, bbox)) } -#[derive(Clone, Copy, Debug)] -struct ResolvedFont { - id: ID, - - units_per_em: NonZeroU16, - - // All values below are in font units. - ascent: i16, - descent: i16, - x_height: NonZeroU16, +fn convert_span( + span: &TextSpan, + clusters: &[GlyphCluster], + text_ts: Transform, +) -> Option<(Vec, NonZeroRect)> { + let mut span_clusters = vec![]; + let mut bboxes_builder = tiny_skia_path::PathBuilder::new(); - underline_position: i16, - underline_thickness: NonZeroU16, + for cluster in clusters { + if !cluster.visible { + continue; + } - // line-through thickness should be the the same as underline thickness - // according to the TrueType spec: - // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize - line_through_position: i16, + if span_contains(span, cluster.byte_idx) { + span_clusters.push(cluster.clone()); + } - subscript_offset: i16, - superscript_offset: i16, -} + let mut advance = cluster.advance; + if advance <= 0.0 { + advance = 1.0; + } -impl ResolvedFont { - #[inline] - fn scale(&self, font_size: f32) -> f32 { - font_size / self.units_per_em.get() as f32 + // We have to calculate text bbox using font metrics and not glyph shape. + if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) { + if let Some(r) = r.transform(cluster.transform()) { + bboxes_builder.push_rect(r.to_rect()); + } + } } - #[inline] - fn ascent(&self, font_size: f32) -> f32 { - self.ascent as f32 * self.scale(font_size) - } + let mut bboxes = bboxes_builder.finish()?; + bboxes = bboxes.transform(text_ts)?; + let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?; - #[inline] - fn descent(&self, font_size: f32) -> f32 { - self.descent as f32 * self.scale(font_size) - } + Some((span_clusters, bbox)) +} - #[inline] - fn height(&self, font_size: f32) -> f32 { - self.ascent(font_size) - self.descent(font_size) - } +fn collect_decoration_spans(span: &TextSpan, clusters: &[GlyphCluster]) -> Vec { + let mut spans = Vec::new(); - #[inline] - fn x_height(&self, font_size: f32) -> f32 { - self.x_height.get() as f32 * self.scale(font_size) - } + let mut started = false; + let mut width = 0.0; + let mut transform = Transform::default(); - #[inline] - fn underline_position(&self, font_size: f32) -> f32 { - self.underline_position as f32 * self.scale(font_size) - } - - #[inline] - fn underline_thickness(&self, font_size: f32) -> f32 { - self.underline_thickness.get() as f32 * self.scale(font_size) - } + for cluster in clusters { + if span_contains(span, cluster.byte_idx) { + if started && cluster.has_relative_shift { + started = false; + spans.push(DecorationSpan { width, transform }); + } - #[inline] - fn line_through_position(&self, font_size: f32) -> f32 { - self.line_through_position as f32 * self.scale(font_size) + if !started { + width = cluster.advance; + started = true; + transform = cluster.transform; + } else { + width += cluster.advance; + } + } else if started { + spans.push(DecorationSpan { width, transform }); + started = false; + } } - #[inline] - fn subscript_offset(&self, font_size: f32) -> f32 { - self.subscript_offset as f32 * self.scale(font_size) + if started { + spans.push(DecorationSpan { width, transform }); } - #[inline] - fn superscript_offset(&self, font_size: f32) -> f32 { - self.superscript_offset as f32 * self.scale(font_size) - } + spans +} - fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 { - let alignment = match baseline { - DominantBaseline::Auto => AlignmentBaseline::Auto, - DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported - DominantBaseline::NoChange => AlignmentBaseline::Auto, // already resolved - DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported - DominantBaseline::Ideographic => AlignmentBaseline::Ideographic, - DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic, - DominantBaseline::Hanging => AlignmentBaseline::Hanging, - DominantBaseline::Mathematical => AlignmentBaseline::Mathematical, - DominantBaseline::Central => AlignmentBaseline::Central, - DominantBaseline::Middle => AlignmentBaseline::Middle, - DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge, - DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge, - }; +pub(crate) fn convert_decoration( + dy: f32, + span: &TextSpan, + font: &ResolvedFont, + mut decoration: TextDecorationStyle, + decoration_spans: &[DecorationSpan], + transform: Transform, +) -> Option { + debug_assert!(!decoration_spans.is_empty()); - self.alignment_baseline_shift(alignment, font_size) - } + let thickness = font.underline_thickness(span.font_size.get()); - // The `alignment-baseline` property is a mess. - // - // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties) - // goes on and on about what this property suppose to do, but doesn't actually explain - // how it should be implemented. It's just a very verbose overview. - // - // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't. - // Same goes for basically every SVG library in existence. - // Meaning we have no idea how exactly it should be implemented. - // - // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`, - // `text-after-edge` and `ideographic` variants. Producing vastly different output. - // - // As per spec, a proper implementation should get baseline values from the font itself, - // using `BASE` and `bsln` TrueType tables. If those tables are not present, - // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts). - // And in the worst case scenario simply fallback to hardcoded values. - // - // Also, most fonts do not provide `BASE` and `bsln` tables to begin with. - // - // Again, as of Nov 2022, Chrome does only the latter: - // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153 - // - // Since baseline TrueType tables parsing and baseline synthesis are pretty hard, - // we do what Chrome does - use hardcoded values. And it seems like Safari does the same. - // - // - // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul, - // and it's far more complex now. Not sure if anyone actually supports it. - fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 { - match alignment { - AlignmentBaseline::Auto => 0.0, - AlignmentBaseline::Baseline => 0.0, - AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => { - self.ascent(font_size) - } - AlignmentBaseline::Middle => self.x_height(font_size) * 0.5, - AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5, - AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => { - self.descent(font_size) + let mut builder = tiny_skia_path::PathBuilder::new(); + for dec_span in decoration_spans { + let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) { + Some(v) => v, + None => { + log::warn!("a decoration span has a malformed bbox"); + continue; } - AlignmentBaseline::Ideographic => self.descent(font_size), - AlignmentBaseline::Alphabetic => 0.0, - AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8, - AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5, - } - } -} - -struct PathBuilder { - builder: tiny_skia_path::PathBuilder, -} + }; -impl ttf_parser::OutlineBuilder for PathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.builder.move_to(x, y); - } + let ts = dec_span.transform.pre_translate(0.0, dy); - fn line_to(&mut self, x: f32, y: f32) { - self.builder.line_to(x, y); - } + let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect()); + path = match path.transform(ts) { + Some(v) => v, + None => continue, + }; - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.builder.quad_to(x1, y1, x, y); + builder.push_path(&path); } - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.builder.cubic_to(x1, y1, x2, y2, x, y); - } + let mut path_data = builder.finish()?; + path_data = path_data.transform(transform)?; - fn close(&mut self) { - self.builder.close(); - } + Path::new( + String::new(), + #[cfg(feature = "class")] + String::new(), + span.visibility, + decoration.fill.take(), + decoration.stroke.take(), + PaintOrder::default(), + ShapeRendering::default(), + Arc::new(path_data), + Transform::default(), + ) } -/// A read-only text index in bytes. +/// A text decoration span. /// -/// Guarantee to be on a char boundary and in text bounds. -#[derive(Clone, Copy, PartialEq)] -struct ByteIndex(usize); +/// Basically a horizontal line, that will be used for underline, overline and line-through. +/// It doesn't have a height, since it depends on the Font metrics. +#[derive(Clone, Copy)] +pub(crate) struct DecorationSpan { + pub(crate) width: f32, + pub(crate) transform: Transform, +} -impl ByteIndex { - fn new(i: usize) -> Self { - ByteIndex(i) +/// Resolves clusters positions. +/// +/// Mainly sets the `transform` property. +/// +/// Returns the last text position. The next text chunk should start from that position. +fn resolve_clusters_positions( + text: &Text, + chunk: &TextChunk, + char_offset: usize, + writing_mode: WritingMode, + fonts_cache: &FontsCache, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + match chunk.text_flow { + TextFlow::Linear => { + resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters) + } + TextFlow::Path(ref path) => resolve_clusters_positions_path( + text, + chunk, + char_offset, + path, + writing_mode, + fonts_cache, + clusters, + ), } +} - fn value(&self) -> usize { - self.0 - } +fn clusters_length(clusters: &[GlyphCluster]) -> f32 { + clusters.iter().fold(0.0, |w, cluster| w + cluster.advance) +} - /// Converts byte position into a code point position. - fn code_point_at(&self, text: &str) -> usize { - text.char_indices() - .take_while(|(i, _)| *i != self.0) - .count() - } +fn resolve_clusters_positions_horizontal( + text: &Text, + chunk: &TextChunk, + offset: usize, + writing_mode: WritingMode, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { + let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); + let mut y = 0.0; - /// Converts byte position into a character. - fn char_from(&self, text: &str) -> char { - text[self.0..].chars().next().unwrap() - } -} + for cluster in clusters { + let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); + if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) { + if writing_mode == WritingMode::LeftToRight { + x += dx; + y += dy; + } else { + y -= dx; + x += dy; + } + cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4); + } -fn resolve_rendering_mode(text: &Text) -> ShapeRendering { - match text.rendering_mode { - TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges, - TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision, - TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision, - } -} + cluster.transform = cluster.transform.pre_translate(x, y); -fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> { - chunk - .spans - .iter() - .find(|&span| span_contains(span, byte_offset)) -} + if let Some(angle) = text.rotate.get(cp).cloned() { + if !angle.approx_zero_ulps(4) { + cluster.transform = cluster.transform.pre_rotate(angle); + cluster.has_relative_shift = true; + } + } -fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool { - byte_offset.value() >= span.start && byte_offset.value() < span.end + x += cluster.advance; + } + + (x, y) } // Baseline resolving in SVG is a mess. @@ -436,7 +481,11 @@ fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool { // For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output. // // See `alignment_baseline_shift` method comment for more details. -fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingMode) -> f32 { +pub(crate) fn resolve_baseline( + span: &TextSpan, + font: &ResolvedFont, + writing_mode: WritingMode, +) -> f32 { let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get()); // TODO: support vertical layout as well @@ -453,646 +502,726 @@ fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingM shift } -type FontsCache = HashMap>; - -fn text_to_paths( - text_node: &Text, - fontdb: &fontdb::Database, - abs_ts: Transform, -) -> Option<(Vec, Rect)> { - let mut fonts_cache: FontsCache = HashMap::new(); - for chunk in &text_node.chunks { - for span in &chunk.spans { - if !fonts_cache.contains_key(&span.font) { - if let Some(font) = resolve_font(&span.font, fontdb) { - fonts_cache.insert(span.font.clone(), Rc::new(font)); - } - } +fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 { + let mut shift = 0.0; + for baseline in baselines.iter().rev() { + match baseline { + BaselineShift::Baseline => {} + BaselineShift::Subscript => shift -= font.subscript_offset(font_size), + BaselineShift::Superscript => shift += font.superscript_offset(font_size), + BaselineShift::Number(n) => shift += n, } } - let mut bbox = BBox::default(); - let mut char_offset = 0; + shift +} + +fn resolve_clusters_positions_path( + text: &Text, + chunk: &TextChunk, + char_offset: usize, + path: &TextPath, + writing_mode: WritingMode, + fonts_cache: &FontsCache, + clusters: &mut [GlyphCluster], +) -> (f32, f32) { let mut last_x = 0.0; let mut last_y = 0.0; - let mut new_paths = Vec::new(); - for chunk in &text_node.chunks { - let (x, y) = match chunk.text_flow { - TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)), - TextFlow::Path(_) => (0.0, 0.0), - }; - let mut clusters = outline_chunk(chunk, &fonts_cache, fontdb); - if clusters.is_empty() { - char_offset += chunk.text.chars().count(); - continue; - } + let mut dy = 0.0; - apply_writing_mode(text_node.writing_mode, &mut clusters); - apply_letter_spacing(chunk, &mut clusters); - apply_word_spacing(chunk, &mut clusters); - apply_length_adjust(chunk, &mut clusters); - let mut curr_pos = resolve_clusters_positions( - chunk, - char_offset, - &text_node.positions, - &text_node.rotate, - text_node.writing_mode, - abs_ts, - &fonts_cache, - &mut clusters, - ); + // In the text path mode, chunk's x/y coordinates provide an additional offset along the path. + // The X coordinate is used in a horizontal mode, and Y in vertical. + let chunk_offset = match writing_mode { + WritingMode::LeftToRight => chunk.x.unwrap_or(0.0), + WritingMode::TopToBottom => chunk.y.unwrap_or(0.0), + }; - let mut text_ts = Transform::default(); - if text_node.writing_mode == WritingMode::TopToBottom { - if let TextFlow::Linear = chunk.text_flow { - text_ts = text_ts.pre_rotate_at(90.0, x, y); + let start_offset = + chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters)); + + let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset); + for (cluster, normal) in clusters.iter_mut().zip(normals) { + let (x, y, angle) = match normal { + Some(normal) => (normal.x, normal.y, normal.angle), + None => { + // Hide clusters that are outside the text path. + cluster.visible = false; + continue; } - } + }; - for span in &chunk.spans { - let font = match fonts_cache.get(&span.font) { - Some(v) => v, - None => continue, - }; + // We have to break a decoration line for each cluster during text-on-path. + cluster.has_relative_shift = true; - let decoration_spans = collect_decoration_spans(span, &clusters); + let orig_ts = cluster.transform; - let mut span_ts = text_ts; - span_ts = span_ts.pre_translate(x, y); - if let TextFlow::Linear = chunk.text_flow { - let shift = resolve_baseline(span, font, text_node.writing_mode); + // Clusters should be rotated by the x-midpoint x baseline position. + let half_width = cluster.width / 2.0; + cluster.transform = Transform::default(); + cluster.transform = cluster.transform.pre_translate(x - half_width, y); + cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0); - // In case of a horizontal flow, shift transform and not clusters, - // because clusters can be rotated and an additional shift will lead - // to invalid results. - span_ts = span_ts.pre_translate(0.0, shift); - } + let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); + dy += text.dy.get(cp).cloned().unwrap_or(0.0); - if let Some(decoration) = span.decoration.underline.clone() { - // TODO: No idea what offset should be used for top-to-bottom layout. - // There is - // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property - // but it doesn't go into details. - let offset = match text_node.writing_mode { - WritingMode::LeftToRight => -font.underline_position(span.font_size.get()), - WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0, + let baseline_shift = chunk_span_at(chunk, cluster.byte_idx) + .map(|span| { + let font = match fonts_cache.get(&span.font) { + Some(v) => v, + None => return 0.0, }; + -resolve_baseline(span, font, writing_mode) + }) + .unwrap_or(0.0); - if let Some(path) = - convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) - { - bbox = bbox.expand(path.data.bounds()); - new_paths.push(path); - } + // Shift only by `dy` since we already applied `dx` + // during offset along the path calculation. + if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) { + let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64); + cluster.transform = cluster + .transform + .pre_translate(shift.x as f32, shift.y as f32); + } + + if let Some(angle) = text.rotate.get(cp).cloned() { + if !angle.approx_zero_ulps(4) { + cluster.transform = cluster.transform.pre_rotate(angle); } + } - if let Some(decoration) = span.decoration.overline.clone() { - let offset = match text_node.writing_mode { - WritingMode::LeftToRight => -font.ascent(span.font_size.get()), - WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0, - }; + // The possible `lengthAdjust` transform should be applied after text-on-path positioning. + cluster.transform = cluster.transform.pre_concat(orig_ts); - if let Some(path) = - convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) - { - bbox = bbox.expand(path.data.bounds()); - new_paths.push(path); - } - } + last_x = x + cluster.advance; + last_y = y; + } - if let Some(path) = convert_span(span, &mut clusters, span_ts) { - // Use `text_bbox` here and not `path.data.bbox()`. - if let Some(r) = path.text_bbox { - bbox = bbox.expand(r); - } + (last_x, last_y) +} - new_paths.push(path); - } +pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 { + match a { + TextAnchor::Start => 0.0, // Nothing. + TextAnchor::Middle => -text_width / 2.0, + TextAnchor::End => -text_width, + } +} - if let Some(decoration) = span.decoration.line_through.clone() { - let offset = match text_node.writing_mode { - WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()), - WritingMode::TopToBottom => 0.0, - }; +pub(crate) struct PathNormal { + pub(crate) x: f32, + pub(crate) y: f32, + pub(crate) angle: f32, +} - if let Some(path) = - convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts) - { - bbox = bbox.expand(path.data.bounds()); - new_paths.push(path); - } - } - } +fn collect_normals( + text: &Text, + chunk: &TextChunk, + clusters: &[GlyphCluster], + path: &tiny_skia_path::Path, + char_offset: usize, + offset: f32, +) -> Vec> { + let mut offsets = Vec::with_capacity(clusters.len()); + let mut normals = Vec::with_capacity(clusters.len()); + { + let mut advance = offset; + for cluster in clusters { + // Clusters should be rotated by the x-midpoint x baseline position. + let half_width = cluster.width / 2.0; - char_offset += chunk.text.chars().count(); + // Include relative position. + let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); + advance += text.dx.get(cp).cloned().unwrap_or(0.0); - if text_node.writing_mode == WritingMode::TopToBottom { - if let TextFlow::Linear = chunk.text_flow { - std::mem::swap(&mut curr_pos.0, &mut curr_pos.1); + let offset = advance + half_width; + + // Clusters outside the path have no normals. + if offset < 0.0 { + normals.push(None); } - } - last_x = x + curr_pos.0; - last_y = y + curr_pos.1; + offsets.push(offset as f64); + advance += cluster.advance; + } } - let bbox = bbox.to_rect()?; - Some((new_paths, bbox)) -} + let mut prev_mx = path.points()[0].x; + let mut prev_my = path.points()[0].y; + let mut prev_x = prev_mx; + let mut prev_y = prev_my; -fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option { - let mut name_list = Vec::new(); - for family in &font.families { - name_list.push(match family.as_str() { - "serif" => fontdb::Family::Serif, - "sans-serif" => fontdb::Family::SansSerif, - "cursive" => fontdb::Family::Cursive, - "fantasy" => fontdb::Family::Fantasy, - "monospace" => fontdb::Family::Monospace, - _ => fontdb::Family::Name(family), - }); + fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { + let line = kurbo::Line::new( + kurbo::Point::new(px as f64, py as f64), + kurbo::Point::new(x as f64, y as f64), + ); + let p1 = line.eval(0.33); + let p2 = line.eval(0.66); + kurbo::CubicBez { + p0: line.p0, + p1, + p2, + p3: line.p1, + } } - // Use the default font as fallback. - name_list.push(fontdb::Family::Serif); + let mut length: f64 = 0.0; + for seg in path.segments() { + let curve = match seg { + tiny_skia_path::PathSegment::MoveTo(p) => { + prev_mx = p.x; + prev_my = p.y; + prev_x = p.x; + prev_y = p.y; + continue; + } + tiny_skia_path::PathSegment::LineTo(p) => { + create_curve_from_line(prev_x, prev_y, p.x, p.y) + } + tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez { + p0: kurbo::Point::new(prev_x as f64, prev_y as f64), + p1: kurbo::Point::new(p1.x as f64, p1.y as f64), + p2: kurbo::Point::new(p.x as f64, p.y as f64), + } + .raise(), + tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez { + p0: kurbo::Point::new(prev_x as f64, prev_y as f64), + p1: kurbo::Point::new(p1.x as f64, p1.y as f64), + p2: kurbo::Point::new(p2.x as f64, p2.y as f64), + p3: kurbo::Point::new(p.x as f64, p.y as f64), + }, + tiny_skia_path::PathSegment::Close => { + create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) + } + }; - let stretch = match font.stretch { - FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, - FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, - FontStretch::Condensed => fontdb::Stretch::Condensed, - FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, - FontStretch::Normal => fontdb::Stretch::Normal, - FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, - FontStretch::Expanded => fontdb::Stretch::Expanded, - FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, - FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, - }; + let arclen_accuracy = { + let base_arclen_accuracy = 0.5; + // Accuracy depends on a current scale. + // When we have a tiny path scaled by a large value, + // we have to increase out accuracy accordingly. + let (sx, sy) = text.abs_transform.get_scale(); + // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy. + base_arclen_accuracy / (sx * sy).sqrt().max(1.0) + }; - let style = match font.style { - FontStyle::Normal => fontdb::Style::Normal, - FontStyle::Italic => fontdb::Style::Italic, - FontStyle::Oblique => fontdb::Style::Oblique, - }; + let curve_len = curve.arclen(arclen_accuracy as f64); - let query = fontdb::Query { - families: &name_list, - weight: fontdb::Weight(font.weight), - stretch, - style, - }; + for offset in &offsets[normals.len()..] { + if *offset >= length && *offset <= length + curve_len { + let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64); + // some rounding error may occur, so we give offset a little tolerance + debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset)); + offset = offset.min(1.0).max(0.0); - let id = fontdb.query(&query); - if id.is_none() { - log::warn!("No match for '{}' font-family.", font.families.join(", ")); + let pos = curve.eval(offset); + let d = curve.deriv().eval(offset); + let d = kurbo::Vec2::new(-d.y, d.x); // tangent + let angle = d.atan2().to_degrees() - 90.0; + + normals.push(Some(PathNormal { + x: pos.x as f32, + y: pos.y as f32, + angle: angle as f32, + })); + + if normals.len() == offsets.len() { + break; + } + } + } + + length += curve_len; + prev_x = curve.p3.x as f32; + prev_y = curve.p3.y as f32; } - fontdb.load_font(id?) + // If path ended and we still have unresolved normals - set them to `None`. + for _ in 0..(offsets.len() - normals.len()) { + normals.push(None); + } + + normals } -fn convert_span( - span: &TextSpan, - clusters: &mut [OutlinedCluster], - text_ts: Transform, -) -> Option { - let mut path_builder = tiny_skia_path::PathBuilder::new(); - let mut bboxes_builder = tiny_skia_path::PathBuilder::new(); +/// Converts a text chunk into a list of outlined clusters. +/// +/// This function will do the BIDI reordering, text shaping and glyphs outlining, +/// but not the text layouting. So all clusters are in the 0x0 position. +fn process_chunk( + chunk: &TextChunk, + fonts_cache: &FontsCache, + fontdb: &fontdb::Database, +) -> Vec { + // The way this function works is a bit tricky. + // + // The first problem is BIDI reordering. + // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous. + // + // For example: + // Hello שלום. + // + // Would be shaped as: + // H e l l o ש ל ו ם . (characters) + // 0 1 2 3 4 5 12 10 8 6 14 (cluster indices in UTF-8) + // --- --- (green span) + // + // As you can see, our continuous `lo של` span was split into two separated one. + // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black. + // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output. + // To properly handle this we simply shape the whole chunk. + // + // But this introduces another issue - what to do when we have multiple fonts? + // The easy solution would be to simply shape text with each font, + // where the first font output is used as a base one and all others overwrite it. + // This way in case of: + // Hello world + // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple. + // + // Well, it would work most of the time, but not always. + // This is because different fonts can produce different amount of glyphs for the same text. + // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`, + // but some can use `fi` (U+FB01) instead. + // Meaning that during merging we have to overwrite not individual glyphs, but clusters. - for cluster in clusters { - if !cluster.visible { + let mut glyphs = Vec::new(); + for span in &chunk.spans { + let font = match fonts_cache.get(&span.font) { + Some(v) => v.clone(), + None => continue, + }; + + let tmp_glyphs = shape_text( + &chunk.text, + font, + span.small_caps, + span.apply_kerning, + fontdb, + ); + + // Do nothing with the first run. + if glyphs.is_empty() { + glyphs = tmp_glyphs; continue; } - if span_contains(span, cluster.byte_idx) { - let path = cluster - .path - .take() - .and_then(|p| p.transform(cluster.transform)); - - if let Some(path) = path { - path_builder.push_path(&path); + // Overwrite span's glyphs. + let mut iter = tmp_glyphs.into_iter(); + while let Some(new_glyph) = iter.next() { + if !span_contains(span, new_glyph.byte_idx) { + continue; } - // TODO: make sure `advance` is never negative beforehand. - let mut advance = cluster.advance; - if advance <= 0.0 { - advance = 1.0; - } + let Some(idx) = glyphs.iter().position(|g| g.byte_idx == new_glyph.byte_idx) else { + continue; + }; - // We have to calculate text bbox using font metrics and not glyph shape. - if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) - { - if let Some(r) = r.transform(cluster.transform) { - bboxes_builder.push_rect(r.to_rect()); + let prev_cluster_len = glyphs[idx].cluster_len; + if prev_cluster_len < new_glyph.cluster_len { + // If the new font represents the same cluster with fewer glyphs + // then remove remaining glyphs. + for _ in 1..new_glyph.cluster_len { + glyphs.remove(idx + 1); + } + } else if prev_cluster_len > new_glyph.cluster_len { + // If the new font represents the same cluster with more glyphs + // then insert them after the current one. + for j in 1..prev_cluster_len { + if let Some(g) = iter.next() { + glyphs.insert(idx + j, g); + } } } + + glyphs[idx] = new_glyph; } } - let mut path = path_builder.finish()?; - path = path.transform(text_ts)?; - - let mut bboxes = bboxes_builder.finish()?; - bboxes = bboxes.transform(text_ts)?; - - let mut fill = span.fill.clone(); - if let Some(ref mut fill) = fill { - // The `fill-rule` should be ignored. - // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder - // - // 'Since the fill-rule property does not apply to SVG text elements, - // the specific order of the subpaths within the equivalent path does not matter.' - fill.rule = FillRule::NonZero; + // Convert glyphs to clusters. + let mut clusters = Vec::new(); + for (range, byte_idx) in GlyphClusters::new(&glyphs) { + if let Some(span) = chunk_span_at(chunk, byte_idx) { + clusters.push(form_glyph_clusters( + &glyphs[range], + &chunk.text, + span.font_size.get(), + )); + } } - let path = Path { - id: String::new(), - #[cfg(feature = "class")] - class: String::new(), - transform: Transform::default(), - visibility: span.visibility, - fill, - stroke: span.stroke.clone(), - paint_order: span.paint_order, - rendering_mode: ShapeRendering::default(), - text_bbox: Some(bboxes.bounds().to_non_zero_rect()?), - data: Rc::new(path), - }; - - Some(path) + clusters } -fn collect_decoration_spans(span: &TextSpan, clusters: &[OutlinedCluster]) -> Vec { - let mut spans = Vec::new(); +fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear); - let mut started = false; - let mut width = 0.0; - let mut transform = Transform::default(); - for cluster in clusters { - if span_contains(span, cluster.byte_idx) { - if started && cluster.has_relative_shift { - started = false; - spans.push(DecorationSpan { width, transform }); - } + for span in &chunk.spans { + let target_width = match span.text_length { + Some(v) => v, + None => continue, + }; - if !started { - width = cluster.advance; - started = true; - transform = cluster.transform; - } else { - width += cluster.advance; + let mut width = 0.0; + let mut cluster_indexes = Vec::new(); + for i in span.start..span.end { + if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) { + cluster_indexes.push(index); } - } else if started { - spans.push(DecorationSpan { width, transform }); - started = false; } - } - - if started { - spans.push(DecorationSpan { width, transform }); - } + // Complex scripts can have mutli-codepoint clusters therefore we have to remove duplicates. + cluster_indexes.sort(); + cluster_indexes.dedup(); - spans -} + for i in &cluster_indexes { + // Use the original cluster `width` and not `advance`. + // This method essentially discards any `word-spacing` and `letter-spacing`. + width += clusters[*i].width; + } -fn convert_decoration( - dy: f32, - span: &TextSpan, - font: &ResolvedFont, - mut decoration: TextDecorationStyle, - decoration_spans: &[DecorationSpan], - transform: Transform, -) -> Option { - debug_assert!(!decoration_spans.is_empty()); + if cluster_indexes.is_empty() { + continue; + } - let thickness = font.underline_thickness(span.font_size.get()); + if span.length_adjust == LengthAdjust::Spacing { + let factor = if cluster_indexes.len() > 1 { + (target_width - width) / (cluster_indexes.len() - 1) as f32 + } else { + 0.0 + }; - let mut builder = tiny_skia_path::PathBuilder::new(); - for dec_span in decoration_spans { - let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) { - Some(v) => v, - None => { - log::warn!("a decoration span has a malformed bbox"); + for i in cluster_indexes { + clusters[i].advance = clusters[i].width + factor; + } + } else { + let factor = target_width / width; + // Prevent multiplying by zero. + if factor < 0.001 { continue; } - }; - let ts = dec_span.transform.pre_translate(0.0, dy); + for i in cluster_indexes { + clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0); - let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect()); - path = match path.transform(ts) { - Some(v) => v, - None => continue, - }; + // Technically just a hack to support the current text-on-path algorithm. + if !is_horizontal { + clusters[i].advance *= factor; + clusters[i].width *= factor; + } + } + } + } +} - builder.push_path(&path); +/// Rotates clusters according to +/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html). +fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) { + if writing_mode != WritingMode::TopToBottom { + return; } - let mut path_data = builder.finish()?; - path_data = path_data.transform(transform)?; + for cluster in clusters { + let orientation = unicode_vo::char_orientation(cluster.codepoint); + if orientation == unicode_vo::Orientation::Upright { + // Additional offset. Not sure why. + let dy = cluster.width - cluster.height(); - let mut path = Path::new(Rc::new(path_data)); - path.visibility = span.visibility; - path.fill = decoration.fill.take(); - path.stroke = decoration.stroke.take(); - Some(path) -} + // Rotate a cluster 90deg counter clockwise by the center. + let mut ts = Transform::default(); + ts = ts.pre_translate(cluster.width / 2.0, 0.0); + ts = ts.pre_rotate(-90.0); + ts = ts.pre_translate(-cluster.width / 2.0, -dy); -/// By the SVG spec, `tspan` doesn't have a bbox and uses the parent `text` bbox. -/// Since we converted `text` and `tspan` to `path`, we have to update -/// all linked paint servers (gradients and patterns) too. -fn fix_obj_bounding_box(path: &mut Path, bbox: Rect) { - if let Some(ref mut fill) = path.fill { - if let Some(new_paint) = paint_server_to_user_space_on_use(fill.paint.clone(), bbox) { - fill.paint = new_paint; + cluster.path_transform = ts; + + // Move "baseline" to the middle and make height equal to width. + cluster.ascent = cluster.width / 2.0; + cluster.descent = -cluster.width / 2.0; + } else { + // Could not find a spec that explains this, + // but this is how other applications are shifting the "rotated" characters + // in the top-to-bottom mode. + cluster.transform = cluster.transform.pre_translate(0.0, cluster.x_height / 2.0); } } +} + +/// Applies the `letter-spacing` property to a text chunk clusters. +/// +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property). +fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + // At least one span should have a non-zero spacing. + if !chunk + .spans + .iter() + .any(|span| !span.letter_spacing.approx_zero_ulps(4)) + { + return; + } + + let num_clusters = clusters.len(); + for (i, cluster) in clusters.iter_mut().enumerate() { + // Spacing must be applied only to characters that belongs to the script + // that supports spacing. + // We are checking only the first code point, since it should be enough. + // https://www.w3.org/TR/css-text-3/#cursive-tracking + let script = cluster.codepoint.script(); + if script_supports_letter_spacing(script) { + if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { + // A space after the last cluster should be ignored, + // since it affects the bbox and text alignment. + if i != num_clusters - 1 { + cluster.advance += span.letter_spacing; + } - if let Some(ref mut stroke) = path.stroke { - if let Some(new_paint) = paint_server_to_user_space_on_use(stroke.paint.clone(), bbox) { - stroke.paint = new_paint; + // If the cluster advance became negative - clear it. + // This is an UB so we can do whatever we want, and we mimic Chrome's behavior. + if !cluster.advance.is_valid_length() { + cluster.width = 0.0; + cluster.advance = 0.0; + cluster.glyphs = vec![]; + } + } } } } -/// Converts a selected paint server's units to `UserSpaceOnUse`. -/// -/// Creates a deep copy of a selected paint server and returns its ID. +/// Applies the `word-spacing` property to a text chunk clusters. /// -/// Returns `None` if a paint server already uses `UserSpaceOnUse`. -fn paint_server_to_user_space_on_use(paint: Paint, bbox: Rect) -> Option { - if paint.units() != Some(Units::ObjectBoundingBox) { - return None; +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing). +fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { + // At least one span should have a non-zero spacing. + if !chunk + .spans + .iter() + .any(|span| !span.word_spacing.approx_zero_ulps(4)) + { + return; } - // TODO: is `pattern` copying safe? Maybe we should reset id's on all `pattern` children. - // We have to clone a paint server, in case some other element is already using it. - // If not, the `convert` module will remove unused defs anyway. - - // Update id, transform and units. - let ts = Transform::from_bbox(bbox.to_non_zero_rect()?); - let paint = match paint { - Paint::Color(_) => paint, - Paint::LinearGradient(ref lg) => { - let transform = lg.transform.post_concat(ts); - Paint::LinearGradient(Rc::new(LinearGradient { - id: String::new(), - x1: lg.x1, - y1: lg.y1, - x2: lg.x2, - y2: lg.y2, - base: BaseGradient { - units: Units::UserSpaceOnUse, - transform, - spread_method: lg.spread_method, - stops: lg.stops.clone(), - }, - })) - } - Paint::RadialGradient(ref rg) => { - let transform = rg.transform.post_concat(ts); - Paint::RadialGradient(Rc::new(RadialGradient { - id: String::new(), - cx: rg.cx, - cy: rg.cy, - r: rg.r, - fx: rg.fx, - fy: rg.fy, - base: BaseGradient { - units: Units::UserSpaceOnUse, - transform, - spread_method: rg.spread_method, - stops: rg.stops.clone(), - }, - })) - } - Paint::Pattern(ref patt) => { - let transform = patt.transform.post_concat(ts); - Paint::Pattern(Rc::new(Pattern { - id: String::new(), - units: Units::UserSpaceOnUse, - content_units: patt.content_units, - transform, - rect: patt.rect, - view_box: patt.view_box, - root: patt.root.clone().make_deep_copy(), - })) - } - }; - - Some(paint) -} + for cluster in clusters { + if is_word_separator_characters(cluster.codepoint) { + if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { + // Technically, word spacing 'should be applied half on each + // side of the character', but it doesn't affect us in any way, + // so we are ignoring this. + cluster.advance += span.word_spacing; -/// A text decoration span. -/// -/// Basically a horizontal line, that will be used for underline, overline and line-through. -/// It doesn't have a height, since it depends on the Font metrics. -#[derive(Clone, Copy)] -struct DecorationSpan { - width: f32, - transform: Transform, + // After word spacing, `advance` can be negative. + } + } + } } -/// A glyph. -/// -/// Basically, a glyph ID and it's metrics. -#[derive(Clone)] -struct Glyph { - /// The glyph ID in the font. - id: GlyphId, - - /// Position in bytes in the original string. - /// - /// We use it to match a glyph with a character in the text chunk and therefore with the style. - byte_idx: ByteIndex, - - /// The glyph offset in font units. - dx: i32, - - /// The glyph offset in font units. - dy: i32, +fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster { + debug_assert!(!glyphs.is_empty()); - /// The glyph width / X-advance in font units. - width: i32, + let mut width = 0.0; + let mut x: f32 = 0.0; - /// Reference to the source font. - /// - /// Each glyph can have it's own source font. - font: Rc, -} + let mut positioned_glyphs = vec![]; -impl Glyph { - fn is_missing(&self) -> bool { - self.id.0 == 0 - } -} + for glyph in glyphs { + let sx = glyph.font.scale(font_size); -/// An outlined cluster. -/// -/// Cluster/grapheme is a single, unbroken, renderable character. -/// It can be positioned, rotated, spaced, etc. -/// -/// Let's say we have `й` which is *CYRILLIC SMALL LETTER I* and *COMBINING BREVE*. -/// It consists of two code points, will be shaped (via harfbuzz) as two glyphs into one cluster, -/// and then will be combined into the one `OutlinedCluster`. -#[derive(Clone)] -struct OutlinedCluster { - /// Position in bytes in the original string. - /// - /// We use it to match a cluster with a character in the text chunk and therefore with the style. - byte_idx: ByteIndex, + // By default, glyphs are upside-down, so we have to mirror them. + let mut ts = Transform::from_scale(1.0, -1.0); - /// Cluster's original codepoint. - /// - /// Technically, a cluster can contain multiple codepoints, - /// but we are storing only the first one. - codepoint: char, + // Scale to font-size. + ts = ts.pre_scale(sx, sx); - /// Cluster's width. - /// - /// It's different from advance in that it's not affected by letter spacing and word spacing. - width: f32, + // Apply offset. + // + // The first glyph in the cluster will have an offset from 0x0, + // but the later one will have an offset from the "current position". + // So we have to keep an advance. + // TODO: should be done only inside a single text span + ts = ts.pre_translate(x + glyph.dx as f32, glyph.dy as f32); + + positioned_glyphs.push(PositionedGlyph { + transform: ts, + font: glyph.font.id, + text: glyph.text.clone(), + glyph_id: glyph.id, + }); - /// An advance along the X axis. - /// - /// Can be negative. - advance: f32, + x += glyph.width as f32; - /// An ascent in SVG coordinates. - ascent: f32, + let glyph_width = glyph.width as f32 * sx; + if glyph_width > width { + width = glyph_width; + } + } - /// A descent in SVG coordinates. - descent: f32, + let byte_idx = glyphs[0].byte_idx; + let font = glyphs[0].font.clone(); + GlyphCluster { + byte_idx, + codepoint: byte_idx.char_from(text), + width, + advance: width, + ascent: font.ascent(font_size), + descent: font.descent(font_size), + x_height: font.x_height(font_size), + has_relative_shift: false, + transform: Transform::default(), + path_transform: Transform::default(), + glyphs: positioned_glyphs, + visible: true, + } +} - /// A x-height in SVG coordinates. - x_height: f32, +fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option { + let mut name_list = Vec::new(); + for family in &font.families { + name_list.push(match family { + FontFamily::Serif => fontdb::Family::Serif, + FontFamily::SansSerif => fontdb::Family::SansSerif, + FontFamily::Cursive => fontdb::Family::Cursive, + FontFamily::Fantasy => fontdb::Family::Fantasy, + FontFamily::Monospace => fontdb::Family::Monospace, + FontFamily::Named(s) => fontdb::Family::Name(s), + }); + } - /// Indicates that this cluster was affected by the relative shift (via dx/dy attributes) - /// during the text layouting. Which breaks the `text-decoration` line. - /// - /// Used during the `text-decoration` processing. - has_relative_shift: bool, + // Use the default font as fallback. + name_list.push(fontdb::Family::Serif); - /// An actual outline. - path: Option, + let stretch = match font.stretch { + FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, + FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, + FontStretch::Condensed => fontdb::Stretch::Condensed, + FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, + FontStretch::Normal => fontdb::Stretch::Normal, + FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, + FontStretch::Expanded => fontdb::Stretch::Expanded, + FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, + FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, + }; - /// A cluster's transform that contains it's position, rotation, etc. - transform: Transform, + let style = match font.style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + }; - /// Not all clusters should be rendered. - /// - /// For example, if a cluster is outside the text path than it should not be rendered. - visible: bool, -} + let query = fontdb::Query { + families: &name_list, + weight: fontdb::Weight(font.weight), + stretch, + style, + }; -impl OutlinedCluster { - fn height(&self) -> f32 { - self.ascent - self.descent + let id = fontdb.query(&query); + if id.is_none() { + log::warn!( + "No match for '{}' font-family.", + font.families + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", ") + ); } -} -/// An iterator over glyph clusters. -/// -/// Input: 0 2 2 2 3 4 4 5 5 -/// Result: 0 1 4 5 7 -struct GlyphClusters<'a> { - data: &'a [Glyph], - idx: usize, + fontdb.load_font(id?) } -impl<'a> GlyphClusters<'a> { - fn new(data: &'a [Glyph]) -> Self { - GlyphClusters { data, idx: 0 } - } +pub(crate) trait DatabaseExt { + fn load_font(&self, id: ID) -> Option; + fn has_char(&self, id: ID, c: char) -> bool; } -impl<'a> Iterator for GlyphClusters<'a> { - type Item = (std::ops::Range, ByteIndex); - - fn next(&mut self) -> Option { - if self.idx == self.data.len() { - return None; - } - - let start = self.idx; - let cluster = self.data[self.idx].byte_idx; - for g in &self.data[self.idx..] { - if g.byte_idx != cluster { - break; - } +impl DatabaseExt for Database { + #[inline(never)] + fn load_font(&self, id: ID) -> Option { + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; - self.idx += 1; - } + let units_per_em = NonZeroU16::new(font.units_per_em())?; - Some((start..self.idx, cluster)) - } -} + let ascent = font.ascender(); + let descent = font.descender(); -/// Converts a text chunk into a list of outlined clusters. -/// -/// This function will do the BIDI reordering, text shaping and glyphs outlining, -/// but not the text layouting. So all clusters are in the 0x0 position. -fn outline_chunk( - chunk: &TextChunk, - fonts_cache: &FontsCache, - fontdb: &fontdb::Database, -) -> Vec { - let mut glyphs = Vec::new(); - for span in &chunk.spans { - let font = match fonts_cache.get(&span.font) { - Some(v) => v.clone(), - None => continue, - }; + let x_height = font + .x_height() + .and_then(|x| u16::try_from(x).ok()) + .and_then(NonZeroU16::new); + let x_height = match x_height { + Some(height) => height, + None => { + // If not set - fallback to height * 45%. + // 45% is what Firefox uses. + u16::try_from((f32::from(ascent - descent) * 0.45) as i32) + .ok() + .and_then(NonZeroU16::new)? + } + }; - let tmp_glyphs = shape_text( - &chunk.text, - font, - span.small_caps, - span.apply_kerning, - fontdb, - ); + let line_through = font.strikeout_metrics(); + let line_through_position = match line_through { + Some(metrics) => metrics.position, + None => x_height.get() as i16 / 2, + }; - // Do nothing with the first run. - if glyphs.is_empty() { - glyphs = tmp_glyphs; - continue; - } + let (underline_position, underline_thickness) = match font.underline_metrics() { + Some(metrics) => { + let thickness = u16::try_from(metrics.thickness) + .ok() + .and_then(NonZeroU16::new) + // `ttf_parser` guarantees that units_per_em is >= 16 + .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap()); - // We assume, that shaping with an any font will produce the same amount of glyphs. - // Otherwise an error. - if glyphs.len() != tmp_glyphs.len() { - log::warn!("Text layouting failed."); - return Vec::new(); - } + (metrics.position, thickness) + } + None => ( + -(units_per_em.get() as i16) / 9, + NonZeroU16::new(units_per_em.get() / 12).unwrap(), + ), + }; - // Copy span's glyphs. - for (i, glyph) in tmp_glyphs.iter().enumerate() { - if span_contains(span, glyph.byte_idx) { - glyphs[i] = glyph.clone(); + // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg). + let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16; + let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16; + if let Some(metrics) = font.subscript_metrics() { + subscript_offset = metrics.y_offset; } - } - } - // Convert glyphs to clusters. - let mut clusters = Vec::new(); - for (range, byte_idx) in GlyphClusters::new(&glyphs) { - if let Some(span) = chunk_span_at(chunk, byte_idx) { - clusters.push(outline_cluster( - &glyphs[range], - &chunk.text, - span.font_size.get(), - fontdb, - )); - } + if let Some(metrics) = font.superscript_metrics() { + superscript_offset = metrics.y_offset; + } + + Some(ResolvedFont { + id, + units_per_em, + ascent, + descent, + x_height, + underline_position, + underline_thickness, + line_through_position, + subscript_offset, + superscript_offset, + }) + })? } - clusters + #[inline(never)] + fn has_char(&self, id: ID, c: char) -> bool { + let res = self.with_face_data(id, |font_data, face_index| -> Option { + let font = ttf_parser::Face::parse(font_data, face_index).ok()?; + font.glyph_index(c)?; + Some(true) + }); + + res == Some(Some(true)) + } } /// Text shaping with font fallback. -fn shape_text( +pub(crate) fn shape_text( text: &str, - font: Rc, + font: Arc, small_caps: bool, apply_kerning: bool, fontdb: &fontdb::Database, @@ -1115,7 +1244,7 @@ fn shape_text( if let Some(c) = missing { let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) { - Some(v) => Rc::new(v), + Some(v) => Arc::new(v), None => break 'outer, }; @@ -1179,7 +1308,7 @@ fn shape_text( /// This function will do the BIDI reordering and text shaping. fn shape_text_with_font( text: &str, - font: Rc, + font: Arc, small_caps: bool, apply_kerning: bool, fontdb: &fontdb::Database, @@ -1200,10 +1329,11 @@ fn shape_text_with_font( continue; } - let hb_direction = if levels[run.start].is_rtl() { - rustybuzz::Direction::RightToLeft - } else { + let ltr = levels[run.start].is_ltr(); + let hb_direction = if ltr { rustybuzz::Direction::LeftToRight + } else { + rustybuzz::Direction::RightToLeft }; let mut buffer = rustybuzz::UnicodeBuffer::new(); @@ -1232,12 +1362,21 @@ fn shape_text_with_font( let positions = output.glyph_positions(); let infos = output.glyph_infos(); - for (pos, info) in positions.iter().zip(infos) { + for i in 0..output.len() { + let pos = positions[i]; + let info = infos[i]; let idx = run.start + info.cluster as usize; - debug_assert!(text.get(idx..).is_some()); + + let start = info.cluster as usize; + + let end = if ltr { i.checked_add(1) } else { i.checked_sub(1) } + .and_then(|last| infos.get(last)) + .map_or(sub_text.len(), |info| info.cluster as usize); glyphs.push(Glyph { byte_idx: ByteIndex::new(idx), + cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? + text: sub_text[start..end].to_string(), id: GlyphId(info.glyph_id as u16), dx: pos.x_offset, dy: pos.y_offset, @@ -1251,69 +1390,6 @@ fn shape_text_with_font( })? } -/// Outlines a glyph cluster. -/// -/// Uses one or more `Glyph`s to construct an `OutlinedCluster`. -fn outline_cluster( - glyphs: &[Glyph], - text: &str, - font_size: f32, - db: &fontdb::Database, -) -> OutlinedCluster { - debug_assert!(!glyphs.is_empty()); - - let mut builder = tiny_skia_path::PathBuilder::new(); - let mut width = 0.0; - let mut x: f32 = 0.0; - - for glyph in glyphs { - let sx = glyph.font.scale(font_size); - - if let Some(outline) = db.outline(glyph.font.id, glyph.id) { - // By default, glyphs are upside-down, so we have to mirror them. - let mut ts = Transform::from_scale(1.0, -1.0); - - // Scale to font-size. - ts = ts.pre_scale(sx, sx); - - // Apply offset. - // - // The first glyph in the cluster will have an offset from 0x0, - // but the later one will have an offset from the "current position". - // So we have to keep an advance. - // TODO: should be done only inside a single text span - ts = ts.pre_translate(x + glyph.dx as f32, glyph.dy as f32); - - if let Some(outline) = outline.transform(ts) { - builder.push_path(&outline); - } - } - - x += glyph.width as f32; - - let glyph_width = glyph.width as f32 * sx; - if glyph_width > width { - width = glyph_width; - } - } - - let byte_idx = glyphs[0].byte_idx; - let font = glyphs[0].font.clone(); - OutlinedCluster { - byte_idx, - codepoint: byte_idx.char_from(text), - width, - advance: width, - ascent: font.ascent(font_size), - descent: font.descent(font_size), - x_height: font.x_height(font_size), - has_relative_shift: false, - path: builder.finish(), - transform: Transform::default(), - visible: true, - } -} - /// Finds a font with a specified char. /// /// This is a rudimentary font fallback algorithm. @@ -1327,578 +1403,337 @@ fn find_font_for_char( // Iterate over fonts and check if any of them support the specified char. for face in fontdb.faces() { // Ignore fonts, that were used for shaping already. - if exclude_fonts.contains(&face.id) { - continue; - } - - // Check that the new face has the same style. - let base_face = fontdb.face(base_font_id)?; - if base_face.style != face.style - && base_face.weight != face.weight - && base_face.stretch != face.stretch - { - continue; - } - - if !fontdb.has_char(face.id, c) { - continue; - } - - let base_family = base_face - .families - .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); - - let new_family = face - .families - .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); - - log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); - return fontdb.load_font(face.id); - } - - None -} - -/// Resolves clusters positions. -/// -/// Mainly sets the `transform` property. -/// -/// Returns the last text position. The next text chunk should start from that position. -fn resolve_clusters_positions( - chunk: &TextChunk, - char_offset: usize, - pos_list: &[CharacterPosition], - rotate_list: &[f32], - writing_mode: WritingMode, - ts: Transform, - fonts_cache: &FontsCache, - clusters: &mut [OutlinedCluster], -) -> (f32, f32) { - match chunk.text_flow { - TextFlow::Linear => resolve_clusters_positions_horizontal( - chunk, - char_offset, - pos_list, - rotate_list, - writing_mode, - clusters, - ), - TextFlow::Path(ref path) => resolve_clusters_positions_path( - chunk, - char_offset, - path, - pos_list, - rotate_list, - writing_mode, - ts, - fonts_cache, - clusters, - ), - } -} - -fn resolve_clusters_positions_horizontal( - chunk: &TextChunk, - offset: usize, - pos_list: &[CharacterPosition], - rotate_list: &[f32], - writing_mode: WritingMode, - clusters: &mut [OutlinedCluster], -) -> (f32, f32) { - let mut x = process_anchor(chunk.anchor, clusters_length(clusters)); - let mut y = 0.0; - - for cluster in clusters { - let cp = offset + cluster.byte_idx.code_point_at(&chunk.text); - if let Some(pos) = pos_list.get(cp) { - if writing_mode == WritingMode::LeftToRight { - x += pos.dx.unwrap_or(0.0); - y += pos.dy.unwrap_or(0.0); - } else { - y -= pos.dx.unwrap_or(0.0); - x += pos.dy.unwrap_or(0.0); - } - cluster.has_relative_shift = pos.dx.is_some() || pos.dy.is_some(); - } - - cluster.transform = cluster.transform.pre_translate(x, y); - - if let Some(angle) = rotate_list.get(cp).cloned() { - if !angle.approx_zero_ulps(4) { - cluster.transform = cluster.transform.pre_rotate(angle); - cluster.has_relative_shift = true; - } - } - - x += cluster.advance; - } - - (x, y) -} - -fn resolve_clusters_positions_path( - chunk: &TextChunk, - char_offset: usize, - path: &TextPath, - pos_list: &[CharacterPosition], - rotate_list: &[f32], - writing_mode: WritingMode, - ts: Transform, - fonts_cache: &FontsCache, - clusters: &mut [OutlinedCluster], -) -> (f32, f32) { - let mut last_x = 0.0; - let mut last_y = 0.0; - - let mut dy = 0.0; - - // In the text path mode, chunk's x/y coordinates provide an additional offset along the path. - // The X coordinate is used in a horizontal mode, and Y in vertical. - let chunk_offset = match writing_mode { - WritingMode::LeftToRight => chunk.x.unwrap_or(0.0), - WritingMode::TopToBottom => chunk.y.unwrap_or(0.0), - }; - - let start_offset = - chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters)); - - let normals = collect_normals( - chunk, - clusters, - &path.path, - pos_list, - char_offset, - start_offset, - ts, - ); - for (cluster, normal) in clusters.iter_mut().zip(normals) { - let (x, y, angle) = match normal { - Some(normal) => (normal.x, normal.y, normal.angle), - None => { - // Hide clusters that are outside the text path. - cluster.visible = false; - continue; - } - }; - - // We have to break a decoration line for each cluster during text-on-path. - cluster.has_relative_shift = true; - - let orig_ts = cluster.transform; - - // Clusters should be rotated by the x-midpoint x baseline position. - let half_width = cluster.width / 2.0; - cluster.transform = Transform::default(); - cluster.transform = cluster.transform.pre_translate(x - half_width, y); - cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0); - - let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); - if let Some(pos) = pos_list.get(cp) { - dy += pos.dy.unwrap_or(0.0); - } - - let baseline_shift = chunk_span_at(chunk, cluster.byte_idx) - .map(|span| { - let font = match fonts_cache.get(&span.font) { - Some(v) => v, - None => return 0.0, - }; - -resolve_baseline(span, font, writing_mode) - }) - .unwrap_or(0.0); - - // Shift only by `dy` since we already applied `dx` - // during offset along the path calculation. - if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) { - let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64); - cluster.transform = cluster - .transform - .pre_translate(shift.x as f32, shift.y as f32); + if exclude_fonts.contains(&face.id) { + continue; } - if let Some(angle) = rotate_list.get(cp).cloned() { - if !angle.approx_zero_ulps(4) { - cluster.transform = cluster.transform.pre_rotate(angle); - } + // Check that the new face has the same style. + let base_face = fontdb.face(base_font_id)?; + if base_face.style != face.style + && base_face.weight != face.weight + && base_face.stretch != face.stretch + { + continue; } - // The possible `lengthAdjust` transform should be applied after text-on-path positioning. - cluster.transform = cluster.transform.pre_concat(orig_ts); + if !fontdb.has_char(face.id, c) { + continue; + } - last_x = x + cluster.advance; - last_y = y; + let base_family = base_face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + let new_family = face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); + return fontdb.load_font(face.id); } - (last_x, last_y) + None } -fn clusters_length(clusters: &[OutlinedCluster]) -> f32 { - clusters.iter().fold(0.0, |w, cluster| w + cluster.advance) +/// An iterator over glyph clusters. +/// +/// Input: 0 2 2 2 3 4 4 5 5 +/// Result: 0 1 4 5 7 +pub(crate) struct GlyphClusters<'a> { + data: &'a [Glyph], + idx: usize, } -fn process_anchor(a: TextAnchor, text_width: f32) -> f32 { - match a { - TextAnchor::Start => 0.0, // Nothing. - TextAnchor::Middle => -text_width / 2.0, - TextAnchor::End => -text_width, +impl<'a> GlyphClusters<'a> { + pub(crate) fn new(data: &'a [Glyph]) -> Self { + GlyphClusters { data, idx: 0 } } } -struct PathNormal { - x: f32, - y: f32, - angle: f32, -} - -fn collect_normals( - chunk: &TextChunk, - clusters: &[OutlinedCluster], - path: &tiny_skia_path::Path, - pos_list: &[CharacterPosition], - char_offset: usize, - offset: f32, - ts: Transform, -) -> Vec> { - let mut offsets = Vec::with_capacity(clusters.len()); - let mut normals = Vec::with_capacity(clusters.len()); - { - let mut advance = offset; - for cluster in clusters { - // Clusters should be rotated by the x-midpoint x baseline position. - let half_width = cluster.width / 2.0; - - // Include relative position. - let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text); - if let Some(pos) = pos_list.get(cp) { - advance += pos.dx.unwrap_or(0.0); - } +impl<'a> Iterator for GlyphClusters<'a> { + type Item = (std::ops::Range, ByteIndex); - let offset = advance + half_width; + fn next(&mut self) -> Option { + if self.idx == self.data.len() { + return None; + } - // Clusters outside the path have no normals. - if offset < 0.0 { - normals.push(None); + let start = self.idx; + let cluster = self.data[self.idx].byte_idx; + for g in &self.data[self.idx..] { + if g.byte_idx != cluster { + break; } - offsets.push(offset as f64); - advance += cluster.advance; + self.idx += 1; } - } - - let mut prev_mx = path.points()[0].x; - let mut prev_my = path.points()[0].y; - let mut prev_x = prev_mx; - let mut prev_y = prev_my; - fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { - let line = kurbo::Line::new( - kurbo::Point::new(px as f64, py as f64), - kurbo::Point::new(x as f64, y as f64), - ); - let p1 = line.eval(0.33); - let p2 = line.eval(0.66); - kurbo::CubicBez { - p0: line.p0, - p1, - p2, - p3: line.p1, - } + Some((start..self.idx, cluster)) } +} - let mut length: f64 = 0.0; - for seg in path.segments() { - let curve = match seg { - tiny_skia_path::PathSegment::MoveTo(p) => { - prev_mx = p.x; - prev_my = p.y; - prev_x = p.x; - prev_y = p.y; - continue; - } - tiny_skia_path::PathSegment::LineTo(p) => { - create_curve_from_line(prev_x, prev_y, p.x, p.y) - } - tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez { - p0: kurbo::Point::new(prev_x as f64, prev_y as f64), - p1: kurbo::Point::new(p1.x as f64, p1.y as f64), - p2: kurbo::Point::new(p.x as f64, p.y as f64), - } - .raise(), - tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez { - p0: kurbo::Point::new(prev_x as f64, prev_y as f64), - p1: kurbo::Point::new(p1.x as f64, p1.y as f64), - p2: kurbo::Point::new(p2.x as f64, p2.y as f64), - p3: kurbo::Point::new(p.x as f64, p.y as f64), - }, - tiny_skia_path::PathSegment::Close => { - create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) - } - }; +/// Checks that selected script supports letter spacing. +/// +/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking). +/// +/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64 +pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool { + use unicode_script::Script; - let arclen_accuracy = { - let base_arclen_accuracy = 0.5; - // Accuracy depends on a current scale. - // When we have a tiny path scaled by a large value, - // we have to increase out accuracy accordingly. - let (sx, sy) = ts.get_scale(); - // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy. - base_arclen_accuracy / (sx * sy).sqrt().max(1.0) - }; + !matches!( + script, + Script::Arabic + | Script::Syriac + | Script::Nko + | Script::Manichaean + | Script::Psalter_Pahlavi + | Script::Mandaic + | Script::Mongolian + | Script::Phags_Pa + | Script::Devanagari + | Script::Bengali + | Script::Gurmukhi + | Script::Modi + | Script::Sharada + | Script::Syloti_Nagri + | Script::Tirhuta + | Script::Ogham + ) +} - let curve_len = curve.arclen(arclen_accuracy as f64); +/// A glyph. +/// +/// Basically, a glyph ID and it's metrics. +#[derive(Clone)] +pub(crate) struct Glyph { + /// The glyph ID in the font. + pub(crate) id: GlyphId, - for offset in &offsets[normals.len()..] { - if *offset >= length && *offset <= length + curve_len { - let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64); - // some rounding error may occur, so we give offset a little tolerance - debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset)); - offset = offset.min(1.0).max(0.0); + /// Position in bytes in the original string. + /// + /// We use it to match a glyph with a character in the text chunk and therefore with the style. + pub(crate) byte_idx: ByteIndex, - let pos = curve.eval(offset); - let d = curve.deriv().eval(offset); - let d = kurbo::Vec2::new(-d.y, d.x); // tangent - let angle = d.atan2().to_degrees() - 90.0; + // The length of the cluster in bytes. + pub(crate) cluster_len: usize, - normals.push(Some(PathNormal { - x: pos.x as f32, - y: pos.y as f32, - angle: angle as f32, - })); + /// The text from the original string that corresponds to that glyph. + pub(crate) text: String, - if normals.len() == offsets.len() { - break; - } - } - } + /// The glyph offset in font units. + pub(crate) dx: i32, - length += curve_len; - prev_x = curve.p3.x as f32; - prev_y = curve.p3.y as f32; - } + /// The glyph offset in font units. + pub(crate) dy: i32, - // If path ended and we still have unresolved normals - set them to `None`. - for _ in 0..(offsets.len() - normals.len()) { - normals.push(None); - } + /// The glyph width / X-advance in font units. + pub(crate) width: i32, - normals + /// Reference to the source font. + /// + /// Each glyph can have it's own source font. + pub(crate) font: Arc, } -/// Applies the `letter-spacing` property to a text chunk clusters. -/// -/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property). -fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) { - // At least one span should have a non-zero spacing. - if !chunk - .spans - .iter() - .any(|span| !span.letter_spacing.approx_zero_ulps(4)) - { - return; +impl Glyph { + fn is_missing(&self) -> bool { + self.id.0 == 0 } +} - let num_clusters = clusters.len(); - for (i, cluster) in clusters.iter_mut().enumerate() { - // Spacing must be applied only to characters that belongs to the script - // that supports spacing. - // We are checking only the first code point, since it should be enough. - // https://www.w3.org/TR/css-text-3/#cursive-tracking - let script = cluster.codepoint.script(); - if script_supports_letter_spacing(script) { - if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { - // A space after the last cluster should be ignored, - // since it affects the bbox and text alignment. - if i != num_clusters - 1 { - cluster.advance += span.letter_spacing; - } +#[derive(Clone, Copy, Debug)] +pub(crate) struct ResolvedFont { + pub(crate) id: ID, - // If the cluster advance became negative - clear it. - // This is an UB so we can do whatever we want, and we mimic Chrome's behavior. - if !cluster.advance.is_valid_length() { - cluster.width = 0.0; - cluster.advance = 0.0; - cluster.path = None; - } - } - } - } -} + units_per_em: NonZeroU16, + + // All values below are in font units. + ascent: i16, + descent: i16, + x_height: NonZeroU16, -/// Checks that selected script supports letter spacing. -/// -/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking). -/// -/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64 -fn script_supports_letter_spacing(script: unicode_script::Script) -> bool { - use unicode_script::Script; + underline_position: i16, + underline_thickness: NonZeroU16, - !matches!( - script, - Script::Arabic - | Script::Syriac - | Script::Nko - | Script::Manichaean - | Script::Psalter_Pahlavi - | Script::Mandaic - | Script::Mongolian - | Script::Phags_Pa - | Script::Devanagari - | Script::Bengali - | Script::Gurmukhi - | Script::Modi - | Script::Sharada - | Script::Syloti_Nagri - | Script::Tirhuta - | Script::Ogham - ) + // line-through thickness should be the the same as underline thickness + // according to the TrueType spec: + // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize + line_through_position: i16, + + subscript_offset: i16, + superscript_offset: i16, } -/// Applies the `word-spacing` property to a text chunk clusters. -/// -/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing). -fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) { - // At least one span should have a non-zero spacing. - if !chunk +pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> { + chunk .spans .iter() - .any(|span| !span.word_spacing.approx_zero_ulps(4)) - { - return; - } - - for cluster in clusters { - if is_word_separator_characters(cluster.codepoint) { - if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) { - // Technically, word spacing 'should be applied half on each - // side of the character', but it doesn't affect us in any way, - // so we are ignoring this. - cluster.advance += span.word_spacing; + .find(|&span| span_contains(span, byte_offset)) +} - // After word spacing, `advance` can be negative. - } - } - } +pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool { + byte_offset.value() >= span.start && byte_offset.value() < span.end } /// Checks that the selected character is a word separator. /// /// According to: https://www.w3.org/TR/css-text-3/#word-separator -fn is_word_separator_characters(c: char) -> bool { +pub(crate) fn is_word_separator_characters(c: char) -> bool { matches!( c as u32, 0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F ) } -fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) { - let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear); +impl ResolvedFont { + #[inline] + pub(crate) fn scale(&self, font_size: f32) -> f32 { + font_size / self.units_per_em.get() as f32 + } - for span in &chunk.spans { - let target_width = match span.text_length { - Some(v) => v, - None => continue, - }; + #[inline] + pub(crate) fn ascent(&self, font_size: f32) -> f32 { + self.ascent as f32 * self.scale(font_size) + } - let mut width = 0.0; - let mut cluster_indexes = Vec::new(); - for i in span.start..span.end { - if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) { - cluster_indexes.push(index); - } - } - // Complex scripts can have mutli-codepoint clusters therefore we have to remove duplicates. - cluster_indexes.sort(); - cluster_indexes.dedup(); + #[inline] + pub(crate) fn descent(&self, font_size: f32) -> f32 { + self.descent as f32 * self.scale(font_size) + } - for i in &cluster_indexes { - // Use the original cluster `width` and not `advance`. - // This method essentially discards any `word-spacing` and `letter-spacing`. - width += clusters[*i].width; - } + #[inline] + pub(crate) fn height(&self, font_size: f32) -> f32 { + self.ascent(font_size) - self.descent(font_size) + } - if cluster_indexes.is_empty() { - continue; - } + #[inline] + pub(crate) fn x_height(&self, font_size: f32) -> f32 { + self.x_height.get() as f32 * self.scale(font_size) + } - if span.length_adjust == LengthAdjust::Spacing { - let factor = if cluster_indexes.len() > 1 { - (target_width - width) / (cluster_indexes.len() - 1) as f32 - } else { - 0.0 - }; + #[inline] + pub(crate) fn underline_position(&self, font_size: f32) -> f32 { + self.underline_position as f32 * self.scale(font_size) + } - for i in cluster_indexes { - clusters[i].advance = clusters[i].width + factor; - } - } else { - let factor = target_width / width; - // Prevent multiplying by zero. - if factor < 0.001 { - continue; - } + #[inline] + fn underline_thickness(&self, font_size: f32) -> f32 { + self.underline_thickness.get() as f32 * self.scale(font_size) + } - for i in cluster_indexes { - clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0); + #[inline] + pub(crate) fn line_through_position(&self, font_size: f32) -> f32 { + self.line_through_position as f32 * self.scale(font_size) + } - // Technically just a hack to support the current text-on-path algorithm. - if !is_horizontal { - clusters[i].advance *= factor; - clusters[i].width *= factor; - } - } - } + #[inline] + fn subscript_offset(&self, font_size: f32) -> f32 { + self.subscript_offset as f32 * self.scale(font_size) } -} -/// Rotates clusters according to -/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html). -fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [OutlinedCluster]) { - if writing_mode != WritingMode::TopToBottom { - return; + #[inline] + fn superscript_offset(&self, font_size: f32) -> f32 { + self.superscript_offset as f32 * self.scale(font_size) } - for cluster in clusters { - let orientation = unicode_vo::char_orientation(cluster.codepoint); - if orientation == unicode_vo::Orientation::Upright { - // Additional offset. Not sure why. - let dy = cluster.width - cluster.height(); + fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 { + let alignment = match baseline { + DominantBaseline::Auto => AlignmentBaseline::Auto, + DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported + DominantBaseline::NoChange => AlignmentBaseline::Auto, // already resolved + DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported + DominantBaseline::Ideographic => AlignmentBaseline::Ideographic, + DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic, + DominantBaseline::Hanging => AlignmentBaseline::Hanging, + DominantBaseline::Mathematical => AlignmentBaseline::Mathematical, + DominantBaseline::Central => AlignmentBaseline::Central, + DominantBaseline::Middle => AlignmentBaseline::Middle, + DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge, + DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge, + }; - // Rotate a cluster 90deg counter clockwise by the center. - let mut ts = Transform::default(); - ts = ts.pre_translate(cluster.width / 2.0, 0.0); - ts = ts.pre_rotate(-90.0); - ts = ts.pre_translate(-cluster.width / 2.0, -dy); + self.alignment_baseline_shift(alignment, font_size) + } - if let Some(path) = cluster.path.take() { - cluster.path = path.transform(ts); + // The `alignment-baseline` property is a mess. + // + // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties) + // goes on and on about what this property suppose to do, but doesn't actually explain + // how it should be implemented. It's just a very verbose overview. + // + // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't. + // Same goes for basically every SVG library in existence. + // Meaning we have no idea how exactly it should be implemented. + // + // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`, + // `text-after-edge` and `ideographic` variants. Producing vastly different output. + // + // As per spec, a proper implementation should get baseline values from the font itself, + // using `BASE` and `bsln` TrueType tables. If those tables are not present, + // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts). + // And in the worst case scenario simply fallback to hardcoded values. + // + // Also, most fonts do not provide `BASE` and `bsln` tables to begin with. + // + // Again, as of Nov 2022, Chrome does only the latter: + // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153 + // + // Since baseline TrueType tables parsing and baseline synthesis are pretty hard, + // we do what Chrome does - use hardcoded values. And it seems like Safari does the same. + // + // + // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul, + // and it's far more complex now. Not sure if anyone actually supports it. + fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 { + match alignment { + AlignmentBaseline::Auto => 0.0, + AlignmentBaseline::Baseline => 0.0, + AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => { + self.ascent(font_size) } - - // Move "baseline" to the middle and make height equal to width. - cluster.ascent = cluster.width / 2.0; - cluster.descent = -cluster.width / 2.0; - } else { - // Could not find a spec that explains this, - // but this is how other applications are shifting the "rotated" characters - // in the top-to-bottom mode. - cluster.transform = cluster.transform.pre_translate(0.0, cluster.x_height / 2.0); + AlignmentBaseline::Middle => self.x_height(font_size) * 0.5, + AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5, + AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => { + self.descent(font_size) + } + AlignmentBaseline::Ideographic => self.descent(font_size), + AlignmentBaseline::Alphabetic => 0.0, + AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8, + AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5, } } } -fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 { - let mut shift = 0.0; - for baseline in baselines.iter().rev() { - match baseline { - BaselineShift::Baseline => {} - BaselineShift::Subscript => shift -= font.subscript_offset(font_size), - BaselineShift::Superscript => shift += font.superscript_offset(font_size), - BaselineShift::Number(n) => shift += n, - } +pub(crate) type FontsCache = HashMap>; + +/// A read-only text index in bytes. +/// +/// Guarantee to be on a char boundary and in text bounds. +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) struct ByteIndex(usize); + +impl ByteIndex { + fn new(i: usize) -> Self { + ByteIndex(i) } - shift + pub(crate) fn value(&self) -> usize { + self.0 + } + + /// Converts byte position into a code point position. + pub(crate) fn code_point_at(&self, text: &str) -> usize { + text.char_indices() + .take_while(|(i, _)| *i != self.0) + .count() + } + + /// Converts byte position into a character. + pub(crate) fn char_from(&self, text: &str) -> char { + text[self.0..].chars().next().unwrap() + } } diff --git a/crates/usvg/src/text/mod.rs b/crates/usvg/src/text/mod.rs new file mode 100644 index 000000000..e1679c330 --- /dev/null +++ b/crates/usvg/src/text/mod.rs @@ -0,0 +1,29 @@ +// 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/. + +use crate::Text; + +mod flatten; + +/// Provides access to the layout of a text node. +pub mod layout; + +/// Convert a text into its paths. This is done in two steps: +/// 1. We convert the text into glyphs and position them according to the rules specified in the +/// SVG specifiation. While doing so, we also calculate the text bbox (which is not based on the +/// outlines of a glyph, but instead the glyph metrics as well as decoration spans). +/// 2. We convert all of the positioned glyphs into outlines. +pub(crate) fn convert(text: &mut Text, fontdb: &fontdb::Database) -> Option<()> { + let (text_fragments, bbox) = layout::layout_text(text, fontdb)?; + text.layouted = text_fragments; + text.bounding_box = bbox.to_rect(); + text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect(); + + let (group, stroke_bbox) = flatten::flatten(text, fontdb)?; + text.flattened = Box::new(group); + text.stroke_bounding_box = stroke_bbox.to_rect(); + text.abs_stroke_bounding_box = stroke_bbox.transform(text.abs_transform)?.to_rect(); + + Some(()) +} diff --git a/crates/usvg-tree/src/filter.rs b/crates/usvg/src/tree/filter.rs similarity index 65% rename from crates/usvg-tree/src/filter.rs rename to crates/usvg/src/tree/filter.rs index 40aa139e9..cd78a208a 100644 --- a/crates/usvg-tree/src/filter.rs +++ b/crates/usvg/src/tree/filter.rs @@ -7,65 +7,77 @@ use strict_num::PositiveF32; use svgtypes::AspectRatio; -use crate::{BlendMode, Color, ImageRendering, Node, NonZeroF32, NonZeroRect, Opacity, Units}; +use crate::{ + BlendMode, Color, Group, ImageRendering, NonEmptyString, NonZeroF32, NonZeroRect, Opacity, +}; /// A filter element. /// /// `filter` element in the SVG. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Filter { + pub(crate) id: NonEmptyString, + pub(crate) rect: NonZeroRect, + pub(crate) primitives: Vec, +} + +impl Filter { /// Element's ID. /// - /// Taken from the SVG itself or generated by the parser. + /// Taken from the SVG itself. /// Used only during SVG writing. `resvg` doesn't rely on this property. - pub id: String, - - /// Region coordinate system units. - /// - /// `filterUnits` in the SVG. - pub units: Units, - - /// Content coordinate system units. - /// - /// `primitiveUnits` in the SVG. - pub primitive_units: Units, + pub fn id(&self) -> &str { + self.id.get() + } /// Filter region. /// /// `x`, `y`, `width` and `height` in the SVG. - pub rect: NonZeroRect, + pub fn rect(&self) -> NonZeroRect { + self.rect + } /// A list of filter primitives. - pub primitives: Vec, + pub fn primitives(&self) -> &[Primitive] { + &self.primitives + } } /// A filter primitive element. #[derive(Clone, Debug)] pub struct Primitive { - /// `x` coordinate of the filter subregion. - pub x: Option, - - /// `y` coordinate of the filter subregion. - pub y: Option, - - /// The filter subregion width. - pub width: Option, + pub(crate) rect: NonZeroRect, + pub(crate) color_interpolation: ColorInterpolation, + pub(crate) result: String, + pub(crate) kind: Kind, +} - /// The filter subregion height. - pub height: Option, +impl Primitive { + /// Filter subregion. + /// + /// `x`, `y`, `width` and `height` in the SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } /// Color interpolation mode. /// /// `color-interpolation-filters` in the SVG. - pub color_interpolation: ColorInterpolation, + pub fn color_interpolation(&self) -> ColorInterpolation { + self.color_interpolation + } /// Assigned name for this filter primitive. /// /// `result` in the SVG. - pub result: String, + pub fn result(&self) -> &str { + &self.result + } /// Filter primitive kind. - pub kind: Kind, + pub fn kind(&self) -> &Kind { + &self.kind + } } /// A filter kind. @@ -144,20 +156,32 @@ impl Default for ColorInterpolation { /// `feBlend` element in the SVG. #[derive(Clone, Debug)] pub struct Blend { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) mode: BlendMode, +} + +impl Blend { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input1: Input, + pub fn input1(&self) -> &Input { + &self.input1 + } /// Identifies input for the given filter primitive. /// /// `in2` in the SVG. - pub input2: Input, + pub fn input2(&self) -> &Input { + &self.input2 + } /// A blending mode. /// /// `mode` in the SVG. - pub mode: BlendMode, + pub fn mode(&self) -> BlendMode { + self.mode + } } /// A color matrix filter primitive. @@ -165,15 +189,24 @@ pub struct Blend { /// `feColorMatrix` element in the SVG. #[derive(Clone, Debug)] pub struct ColorMatrix { + pub(crate) input: Input, + pub(crate) kind: ColorMatrixKind, +} + +impl ColorMatrix { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A matrix kind. /// /// `type` in the SVG. - pub kind: ColorMatrixKind, + pub fn kind(&self) -> &ColorMatrixKind { + &self.kind + } } /// A color matrix filter primitive kind. @@ -200,22 +233,40 @@ impl Default for ColorMatrixKind { /// `feComponentTransfer` element in the SVG. #[derive(Clone, Debug)] pub struct ComponentTransfer { + pub(crate) input: Input, + pub(crate) func_r: TransferFunction, + pub(crate) func_g: TransferFunction, + pub(crate) func_b: TransferFunction, + pub(crate) func_a: TransferFunction, +} + +impl ComponentTransfer { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// `feFuncR` in the SVG. - pub func_r: TransferFunction, + pub fn func_r(&self) -> &TransferFunction { + &self.func_r + } /// `feFuncG` in the SVG. - pub func_g: TransferFunction, + pub fn func_g(&self) -> &TransferFunction { + &self.func_g + } /// `feFuncB` in the SVG. - pub func_b: TransferFunction, + pub fn func_b(&self) -> &TransferFunction { + &self.func_b + } /// `feFuncA` in the SVG. - pub func_a: TransferFunction, + pub fn func_a(&self) -> &TransferFunction { + &self.func_a + } } /// A transfer function used by `FeComponentTransfer`. @@ -254,20 +305,32 @@ pub enum TransferFunction { /// `feComposite` element in the SVG. #[derive(Clone, Debug)] pub struct Composite { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) operator: CompositeOperator, +} + +impl Composite { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input1: Input, + pub fn input1(&self) -> &Input { + &self.input1 + } /// Identifies input for the given filter primitive. /// /// `in2` in the SVG. - pub input2: Input, + pub fn input2(&self) -> &Input { + &self.input2 + } /// A compositing operation. /// /// `operator` in the SVG. - pub operator: CompositeOperator, + pub fn operator(&self) -> CompositeOperator { + self.operator + } } /// An images compositing operation. @@ -287,33 +350,54 @@ pub enum CompositeOperator { /// `feConvolveMatrix` element in the SVG. #[derive(Clone, Debug)] pub struct ConvolveMatrix { + pub(crate) input: Input, + pub(crate) matrix: ConvolveMatrixData, + pub(crate) divisor: NonZeroF32, + pub(crate) bias: f32, + pub(crate) edge_mode: EdgeMode, + pub(crate) preserve_alpha: bool, +} + +impl ConvolveMatrix { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A convolve matrix. - pub matrix: ConvolveMatrixData, + pub fn matrix(&self) -> &ConvolveMatrixData { + &self.matrix + } /// A matrix divisor. /// /// `divisor` in the SVG. - pub divisor: NonZeroF32, + pub fn divisor(&self) -> NonZeroF32 { + self.divisor + } /// A kernel matrix bias. /// /// `bias` in the SVG. - pub bias: f32, + pub fn bias(&self) -> f32 { + self.bias + } /// An edges processing mode. /// /// `edgeMode` in the SVG. - pub edge_mode: EdgeMode, + pub fn edge_mode(&self) -> EdgeMode { + self.edge_mode + } /// An alpha preserving flag. /// /// `preserveAlpha` in the SVG. - pub preserve_alpha: bool, + pub fn preserve_alpha(&self) -> bool { + self.preserve_alpha + } } /// A convolve matrix representation. @@ -321,28 +405,46 @@ pub struct ConvolveMatrix { /// Used primarily by [`ConvolveMatrix`]. #[derive(Clone, Debug)] pub struct ConvolveMatrixData { + pub(crate) target_x: u32, + pub(crate) target_y: u32, + pub(crate) columns: u32, + pub(crate) rows: u32, + pub(crate) data: Vec, +} + +impl ConvolveMatrixData { /// Returns a matrix's X target. /// /// `targetX` in the SVG. - pub target_x: u32, + pub fn target_x(&self) -> u32 { + self.target_x + } /// Returns a matrix's Y target. /// /// `targetY` in the SVG. - pub target_y: u32, + pub fn target_y(&self) -> u32 { + self.target_y + } /// Returns a number of columns in the matrix. /// /// Part of the `order` attribute in the SVG. - pub columns: u32, + pub fn columns(&self) -> u32 { + self.columns + } /// Returns a number of rows in the matrix. /// /// Part of the `order` attribute in the SVG. - pub rows: u32, + pub fn rows(&self) -> u32 { + self.rows + } /// The actual matrix. - pub data: Vec, + pub fn data(&self) -> &[f32] { + &self.data + } } impl ConvolveMatrixData { @@ -353,7 +455,7 @@ impl ConvolveMatrixData { /// - `columns` * `rows` != `data.len()` /// - `target_x` >= `columns` /// - `target_y` >= `rows` - pub fn new( + pub(crate) fn new( target_x: u32, target_y: u32, columns: u32, @@ -397,30 +499,48 @@ pub enum EdgeMode { /// `feDisplacementMap` element in the SVG. #[derive(Clone, Debug)] pub struct DisplacementMap { + pub(crate) input1: Input, + pub(crate) input2: Input, + pub(crate) scale: f32, + pub(crate) x_channel_selector: ColorChannel, + pub(crate) y_channel_selector: ColorChannel, +} + +impl DisplacementMap { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input1: Input, + pub fn input1(&self) -> &Input { + &self.input1 + } /// Identifies input for the given filter primitive. /// /// `in2` in the SVG. - pub input2: Input, + pub fn input2(&self) -> &Input { + &self.input2 + } /// Scale factor. /// /// `scale` in the SVG. - pub scale: f32, + pub fn scale(&self) -> f32 { + self.scale + } /// Indicates a source color channel along the X-axis. /// /// `xChannelSelector` in the SVG. - pub x_channel_selector: ColorChannel, + pub fn x_channel_selector(&self) -> ColorChannel { + self.x_channel_selector + } /// Indicates a source color channel along the Y-axis. /// /// `yChannelSelector` in the SVG. - pub y_channel_selector: ColorChannel, + pub fn y_channel_selector(&self) -> ColorChannel { + self.y_channel_selector + } } /// A color channel. @@ -440,36 +560,60 @@ pub enum ColorChannel { /// `feDropShadow` element in the SVG. #[derive(Clone, Debug)] pub struct DropShadow { + pub(crate) input: Input, + pub(crate) dx: f32, + pub(crate) dy: f32, + pub(crate) std_dev_x: PositiveF32, + pub(crate) std_dev_y: PositiveF32, + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl DropShadow { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// The amount to offset the input graphic along the X-axis. - pub dx: f32, + pub fn dx(&self) -> f32 { + self.dx + } /// The amount to offset the input graphic along the Y-axis. - pub dy: f32, + pub fn dy(&self) -> f32 { + self.dy + } /// A standard deviation along the X-axis. /// /// `stdDeviation` in the SVG. - pub std_dev_x: PositiveF32, + pub fn std_dev_x(&self) -> PositiveF32 { + self.std_dev_x + } /// A standard deviation along the Y-axis. /// /// `stdDeviation` in the SVG. - pub std_dev_y: PositiveF32, + pub fn std_dev_y(&self) -> PositiveF32 { + self.std_dev_y + } /// A flood color. /// /// `flood-color` in the SVG. - pub color: Color, + pub fn color(&self) -> Color { + self.color + } /// A flood opacity. /// /// `flood-opacity` in the SVG. - pub opacity: Opacity, + pub fn opacity(&self) -> Opacity { + self.opacity + } } /// A flood filter primitive. @@ -477,15 +621,24 @@ pub struct DropShadow { /// `feFlood` element in the SVG. #[derive(Clone, Copy, Debug)] pub struct Flood { + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl Flood { /// A flood color. /// /// `flood-color` in the SVG. - pub color: Color, + pub fn color(&self) -> Color { + self.color + } /// A flood opacity. /// /// `flood-opacity` in the SVG. - pub opacity: Opacity, + pub fn opacity(&self) -> Opacity { + self.opacity + } } /// A Gaussian blur filter primitive. @@ -493,20 +646,32 @@ pub struct Flood { /// `feGaussianBlur` element in the SVG. #[derive(Clone, Debug)] pub struct GaussianBlur { + pub(crate) input: Input, + pub(crate) std_dev_x: PositiveF32, + pub(crate) std_dev_y: PositiveF32, +} + +impl GaussianBlur { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A standard deviation along the X-axis. /// /// `stdDeviation` in the SVG. - pub std_dev_x: PositiveF32, + pub fn std_dev_x(&self) -> PositiveF32 { + self.std_dev_x + } /// A standard deviation along the Y-axis. /// /// `stdDeviation` in the SVG. - pub std_dev_y: PositiveF32, + pub fn std_dev_y(&self) -> PositiveF32 { + self.std_dev_y + } } /// An image filter primitive. @@ -514,16 +679,28 @@ pub struct GaussianBlur { /// `feImage` element in the SVG. #[derive(Clone, Debug)] pub struct Image { + pub(crate) aspect: AspectRatio, + pub(crate) rendering_mode: ImageRendering, + pub(crate) data: ImageKind, +} + +impl Image { /// Value of the `preserveAspectRatio` attribute. - pub aspect: AspectRatio, + pub fn aspect(&self) -> AspectRatio { + self.aspect + } /// Rendering method. /// /// `image-rendering` in SVG. - pub rendering_mode: ImageRendering, + pub fn rendering_mode(&self) -> ImageRendering { + self.rendering_mode + } /// Image data. - pub data: ImageKind, + pub fn data(&self) -> &ImageKind { + &self.data + } } /// Kind of the `feImage` data. @@ -533,10 +710,7 @@ pub enum ImageKind { Image(crate::ImageKind), /// An SVG node. - /// - /// Isn't inside a dummy group like clip, mask and pattern because - /// `feImage` can reference only a single element. - Use(Node), + Use(Box), } /// A diffuse lighting filter primitive. @@ -544,28 +718,46 @@ pub enum ImageKind { /// `feDiffuseLighting` element in the SVG. #[derive(Clone, Debug)] pub struct DiffuseLighting { + pub(crate) input: Input, + pub(crate) surface_scale: f32, + pub(crate) diffuse_constant: f32, + pub(crate) lighting_color: Color, + pub(crate) light_source: LightSource, +} + +impl DiffuseLighting { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A surface scale. /// /// `surfaceScale` in the SVG. - pub surface_scale: f32, + pub fn surface_scale(&self) -> f32 { + self.surface_scale + } /// A diffuse constant. /// /// `diffuseConstant` in the SVG. - pub diffuse_constant: f32, + pub fn diffuse_constant(&self) -> f32 { + self.diffuse_constant + } /// A lighting color. /// /// `lighting-color` in the SVG. - pub lighting_color: Color, + pub fn lighting_color(&self) -> Color { + self.lighting_color + } /// A light source. - pub light_source: LightSource, + pub fn light_source(&self) -> LightSource { + self.light_source + } } /// A specular lighting filter primitive. @@ -573,35 +765,56 @@ pub struct DiffuseLighting { /// `feSpecularLighting` element in the SVG. #[derive(Clone, Debug)] pub struct SpecularLighting { + pub(crate) input: Input, + pub(crate) surface_scale: f32, + pub(crate) specular_constant: f32, + pub(crate) specular_exponent: f32, + pub(crate) lighting_color: Color, + pub(crate) light_source: LightSource, +} + +impl SpecularLighting { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A surface scale. /// /// `surfaceScale` in the SVG. - pub surface_scale: f32, + pub fn surface_scale(&self) -> f32 { + self.surface_scale + } /// A specular constant. /// /// `specularConstant` in the SVG. - pub specular_constant: f32, + pub fn specular_constant(&self) -> f32 { + self.specular_constant + } /// A specular exponent. /// /// Should be in 1..128 range. /// /// `specularExponent` in the SVG. - pub specular_exponent: f32, + pub fn specular_exponent(&self) -> f32 { + self.specular_exponent + } /// A lighting color. /// /// `lighting-color` in the SVG. - pub lighting_color: Color, + pub fn lighting_color(&self) -> Color { + self.lighting_color + } /// A light source. - pub light_source: LightSource, + pub fn light_source(&self) -> LightSource { + self.light_source + } } /// A light source kind. @@ -702,10 +915,16 @@ pub struct SpotLight { /// `feMerge` element in the SVG. #[derive(Clone, Debug)] pub struct Merge { + pub(crate) inputs: Vec, +} + +impl Merge { /// List of input layers that should be merged. /// /// List of `feMergeNode`'s in the SVG. - pub inputs: Vec, + pub fn inputs(&self) -> &[Input] { + &self.inputs + } } /// A morphology filter primitive. @@ -713,29 +932,44 @@ pub struct Merge { /// `feMorphology` element in the SVG. #[derive(Clone, Debug)] pub struct Morphology { + pub(crate) input: Input, + pub(crate) operator: MorphologyOperator, + pub(crate) radius_x: PositiveF32, + pub(crate) radius_y: PositiveF32, +} + +impl Morphology { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// A filter operator. /// /// `operator` in the SVG. - pub operator: MorphologyOperator, + pub fn operator(&self) -> MorphologyOperator { + self.operator + } /// A filter radius along the X-axis. /// /// A value of zero disables the effect of the given filter primitive. /// /// `radius` in the SVG. - pub radius_x: PositiveF32, + pub fn radius_x(&self) -> PositiveF32 { + self.radius_x + } /// A filter radius along the Y-axis. /// /// A value of zero disables the effect of the given filter primitive. /// /// `radius` in the SVG. - pub radius_y: PositiveF32, + pub fn radius_y(&self) -> PositiveF32 { + self.radius_y + } } /// A morphology operation. @@ -751,16 +985,28 @@ pub enum MorphologyOperator { /// `feOffset` element in the SVG. #[derive(Clone, Debug)] pub struct Offset { + pub(crate) input: Input, + pub(crate) dx: f32, + pub(crate) dy: f32, +} + +impl Offset { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } /// The amount to offset the input graphic along the X-axis. - pub dx: f32, + pub fn dx(&self) -> f32 { + self.dx + } /// The amount to offset the input graphic along the Y-axis. - pub dy: f32, + pub fn dy(&self) -> f32 { + self.dy + } } /// A tile filter primitive. @@ -768,10 +1014,16 @@ pub struct Offset { /// `feTile` element in the SVG. #[derive(Clone, Debug)] pub struct Tile { + pub(crate) input: Input, +} + +impl Tile { /// Identifies input for the given filter primitive. /// /// `in` in the SVG. - pub input: Input, + pub fn input(&self) -> &Input { + &self.input + } } /// A turbulence generation filter primitive. @@ -779,35 +1031,56 @@ pub struct Tile { /// `feTurbulence` element in the SVG. #[derive(Clone, Copy, Debug)] pub struct Turbulence { + pub(crate) base_frequency_x: PositiveF32, + pub(crate) base_frequency_y: PositiveF32, + pub(crate) num_octaves: u32, + pub(crate) seed: i32, + pub(crate) stitch_tiles: bool, + pub(crate) kind: TurbulenceKind, +} + +impl Turbulence { /// Identifies the base frequency for the noise function. /// /// `baseFrequency` in the SVG. - pub base_frequency_x: PositiveF32, + pub fn base_frequency_x(&self) -> PositiveF32 { + self.base_frequency_x + } /// Identifies the base frequency for the noise function. /// /// `baseFrequency` in the SVG. - pub base_frequency_y: PositiveF32, + pub fn base_frequency_y(&self) -> PositiveF32 { + self.base_frequency_y + } /// Identifies the number of octaves for the noise function. /// /// `numOctaves` in the SVG. - pub num_octaves: u32, + pub fn num_octaves(&self) -> u32 { + self.num_octaves + } /// The starting number for the pseudo random number generator. /// /// `seed` in the SVG. - pub seed: i32, + pub fn seed(&self) -> i32 { + self.seed + } /// Smooth transitions at the border of tiles. /// /// `stitchTiles` in the SVG. - pub stitch_tiles: bool, + pub fn stitch_tiles(&self) -> bool { + self.stitch_tiles + } /// Indicates whether the filter primitive should perform a noise or turbulence function. /// /// `type` in the SVG. - pub kind: TurbulenceKind, + pub fn kind(&self) -> TurbulenceKind { + self.kind + } } /// A turbulence kind for the `feTurbulence` filter. diff --git a/crates/usvg-tree/src/geom.rs b/crates/usvg/src/tree/geom.rs similarity index 88% rename from crates/usvg-tree/src/geom.rs rename to crates/usvg/src/tree/geom.rs index 20a84ec69..bd3abd9c3 100644 --- a/crates/usvg-tree/src/geom.rs +++ b/crates/usvg/src/tree/geom.rs @@ -5,7 +5,7 @@ use strict_num::ApproxEqUlps; pub use tiny_skia_path::{NonZeroRect, Rect, Size, Transform}; -use crate::AspectRatio; +use crate::{Align, AspectRatio}; /// Approximate zero equality comparisons. pub trait ApproxZeroUlps: ApproxEqUlps { @@ -26,7 +26,7 @@ impl ApproxZeroUlps for f64 { } /// Checks that the current number is > 0. -pub trait IsValidLength { +pub(crate) trait IsValidLength { /// Checks that the current number is > 0. fn is_valid_length(&self) -> bool; } @@ -55,9 +55,47 @@ pub struct ViewBox { pub aspect: AspectRatio, } +impl ViewBox { + /// Converts `viewBox` into `Transform`. + pub fn to_transform(&self, img_size: Size) -> Transform { + let vr = self.rect; + + let sx = img_size.width() / vr.width(); + let sy = img_size.height() / vr.height(); + + let (sx, sy) = if self.aspect.align == Align::None { + (sx, sy) + } else { + let s = if self.aspect.slice { + if sx < sy { + sy + } else { + sx + } + } else { + if sx > sy { + sy + } else { + sx + } + }; + + (s, s) + }; + + let x = -vr.x() * sx; + let y = -vr.y() * sy; + let w = img_size.width() - vr.width() * sx; + let h = img_size.height() - vr.height() * sy; + + let (tx, ty) = utils::aligned_pos(self.aspect.align, x, y, w, h); + Transform::from_row(sx, 0.0, 0.0, sy, tx, ty) + } +} + /// A bounding box calculator. #[derive(Clone, Copy, Debug)] -pub struct BBox { +pub(crate) struct BBox { left: f32, top: f32, right: f32, @@ -121,11 +159,6 @@ impl BBox { } } - /// Transforms the bounding box. - pub fn transform(&self, ts: tiny_skia_path::Transform) -> Option { - self.to_rect()?.transform(ts).map(Self::from) - } - /// Converts a bounding box into [`Rect`]. pub fn to_rect(&self) -> Option { if !self.is_default() { @@ -147,49 +180,8 @@ impl BBox { /// Some useful utilities. pub mod utils { - use super::*; use crate::Align; - /// Converts `viewBox` to `Transform`. - pub fn view_box_to_transform( - view_box: NonZeroRect, - aspect: AspectRatio, - img_size: Size, - ) -> Transform { - let vr = view_box; - - let sx = img_size.width() / vr.width(); - let sy = img_size.height() / vr.height(); - - let (sx, sy) = if aspect.align == Align::None { - (sx, sy) - } else { - let s = if aspect.slice { - if sx < sy { - sy - } else { - sx - } - } else { - if sx > sy { - sy - } else { - sx - } - }; - - (s, s) - }; - - let x = -vr.x() * sx; - let y = -vr.y() * sy; - let w = img_size.width() - vr.width() * sx; - let h = img_size.height() - vr.height() * sy; - - let (tx, ty) = aligned_pos(aspect.align, x, y, w, h); - Transform::from_row(sx, 0.0, 0.0, sy, tx, ty) - } - /// Returns object aligned position. pub fn aligned_pos(align: Align, x: f32, y: f32, w: f32, h: f32) -> (f32, f32) { match align { diff --git a/crates/usvg/src/tree/mod.rs b/crates/usvg/src/tree/mod.rs new file mode 100644 index 000000000..b70a07551 --- /dev/null +++ b/crates/usvg/src/tree/mod.rs @@ -0,0 +1,1893 @@ +// 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/. + +pub mod filter; +mod geom; +mod text; + +use std::sync::Arc; + +pub use strict_num::{self, ApproxEqUlps, NonZeroPositiveF32, NormalizedF32, PositiveF32}; +pub use svgtypes::{Align, AspectRatio}; + +pub use tiny_skia_path; + +pub use self::geom::*; +pub use self::text::*; + +/// An alias to `NormalizedF32`. +pub type Opacity = NormalizedF32; + +// Must not be clone-able to preserve ID uniqueness. +#[derive(Debug)] +pub(crate) struct NonEmptyString(String); + +impl NonEmptyString { + pub(crate) fn new(string: String) -> Option { + if string.trim().is_empty() { + return None; + } + + Some(NonEmptyString(string)) + } + + pub(crate) fn get(&self) -> &str { + &self.0 + } +} + +/// A non-zero `f32`. +/// +/// Just like `f32` but immutable and guarantee to never be zero. +#[derive(Clone, Copy, Debug)] +pub struct NonZeroF32(f32); + +impl NonZeroF32 { + /// Creates a new `NonZeroF32` value. + #[inline] + pub fn new(n: f32) -> Option { + if n.approx_eq_ulps(&0.0, 4) { + None + } else { + Some(NonZeroF32(n)) + } + } + + /// Returns an underlying value. + #[inline] + pub fn get(&self) -> f32 { + self.0 + } +} + +#[derive(Clone, Copy, PartialEq, Debug)] +pub(crate) enum Units { + UserSpaceOnUse, + ObjectBoundingBox, +} + +// `Units` cannot have a default value, because it changes depending on an element. + +/// A visibility property. +/// +/// `visibility` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Visibility { + Visible, + Hidden, + Collapse, +} + +impl Default for Visibility { + fn default() -> Self { + Self::Visible + } +} + +/// A shape rendering method. +/// +/// `shape-rendering` attribute in the SVG. +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub enum ShapeRendering { + OptimizeSpeed, + CrispEdges, + GeometricPrecision, +} + +impl ShapeRendering { + /// Checks if anti-aliasing should be enabled. + pub fn use_shape_antialiasing(self) -> bool { + match self { + ShapeRendering::OptimizeSpeed => false, + ShapeRendering::CrispEdges => false, + ShapeRendering::GeometricPrecision => true, + } + } +} + +impl Default for ShapeRendering { + fn default() -> Self { + Self::GeometricPrecision + } +} + +impl std::str::FromStr for ShapeRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeSpeed" => Ok(ShapeRendering::OptimizeSpeed), + "crispEdges" => Ok(ShapeRendering::CrispEdges), + "geometricPrecision" => Ok(ShapeRendering::GeometricPrecision), + _ => Err("invalid"), + } + } +} + +/// A text rendering method. +/// +/// `text-rendering` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum TextRendering { + OptimizeSpeed, + OptimizeLegibility, + GeometricPrecision, +} + +impl Default for TextRendering { + fn default() -> Self { + Self::OptimizeLegibility + } +} + +impl std::str::FromStr for TextRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeSpeed" => Ok(TextRendering::OptimizeSpeed), + "optimizeLegibility" => Ok(TextRendering::OptimizeLegibility), + "geometricPrecision" => Ok(TextRendering::GeometricPrecision), + _ => Err("invalid"), + } + } +} + +/// An image rendering method. +/// +/// `image-rendering` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum ImageRendering { + OptimizeQuality, + OptimizeSpeed, +} + +impl Default for ImageRendering { + fn default() -> Self { + Self::OptimizeQuality + } +} + +impl std::str::FromStr for ImageRendering { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "optimizeQuality" => Ok(ImageRendering::OptimizeQuality), + "optimizeSpeed" => Ok(ImageRendering::OptimizeSpeed), + _ => Err("invalid"), + } + } +} + +/// A blending mode property. +/// +/// `mix-blend-mode` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +} + +impl Default for BlendMode { + fn default() -> Self { + Self::Normal + } +} + +/// A spread method. +/// +/// `spreadMethod` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum SpreadMethod { + Pad, + Reflect, + Repeat, +} + +impl Default for SpreadMethod { + fn default() -> Self { + Self::Pad + } +} + +/// A generic gradient. +#[derive(Debug)] +pub struct BaseGradient { + pub(crate) id: NonEmptyString, + pub(crate) units: Units, // used only during parsing + pub(crate) transform: Transform, + pub(crate) spread_method: SpreadMethod, + pub(crate) stops: Vec, +} + +impl BaseGradient { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Gradient transform. + /// + /// `gradientTransform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Gradient spreading method. + /// + /// `spreadMethod` in SVG. + pub fn spread_method(&self) -> SpreadMethod { + self.spread_method + } + + /// A list of `stop` elements. + pub fn stops(&self) -> &[Stop] { + &self.stops + } +} + +/// A linear gradient. +/// +/// `linearGradient` element in SVG. +#[derive(Debug)] +pub struct LinearGradient { + pub(crate) base: BaseGradient, + pub(crate) x1: f32, + pub(crate) y1: f32, + pub(crate) x2: f32, + pub(crate) y2: f32, +} + +impl LinearGradient { + /// `x1` coordinate. + pub fn x1(&self) -> f32 { + self.x1 + } + + /// `y1` coordinate. + pub fn y1(&self) -> f32 { + self.y1 + } + + /// `x2` coordinate. + pub fn x2(&self) -> f32 { + self.x2 + } + + /// `y2` coordinate. + pub fn y2(&self) -> f32 { + self.y2 + } +} + +impl std::ops::Deref for LinearGradient { + type Target = BaseGradient; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +/// A radial gradient. +/// +/// `radialGradient` element in SVG. +#[derive(Debug)] +pub struct RadialGradient { + pub(crate) base: BaseGradient, + pub(crate) cx: f32, + pub(crate) cy: f32, + pub(crate) r: PositiveF32, + pub(crate) fx: f32, + pub(crate) fy: f32, +} + +impl RadialGradient { + /// `cx` coordinate. + pub fn cx(&self) -> f32 { + self.cx + } + + /// `cy` coordinate. + pub fn cy(&self) -> f32 { + self.cy + } + + /// Gradient radius. + pub fn r(&self) -> PositiveF32 { + self.r + } + + /// `fx` coordinate. + pub fn fx(&self) -> f32 { + self.fx + } + + /// `fy` coordinate. + pub fn fy(&self) -> f32 { + self.fy + } +} + +impl std::ops::Deref for RadialGradient { + type Target = BaseGradient; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +/// An alias to `NormalizedF32`. +pub type StopOffset = NormalizedF32; + +/// Gradient's stop element. +/// +/// `stop` element in SVG. +#[derive(Clone, Copy, Debug)] +pub struct Stop { + pub(crate) offset: StopOffset, + pub(crate) color: Color, + pub(crate) opacity: Opacity, +} + +impl Stop { + /// Gradient stop offset. + /// + /// `offset` in SVG. + pub fn offset(&self) -> StopOffset { + self.offset + } + + /// Gradient stop color. + /// + /// `stop-color` in SVG. + pub fn color(&self) -> Color { + self.color + } + + /// Gradient stop opacity. + /// + /// `stop-opacity` in SVG. + pub fn opacity(&self) -> Opacity { + self.opacity + } +} + +/// A pattern element. +/// +/// `pattern` element in SVG. +#[derive(Debug)] +pub struct Pattern { + pub(crate) id: NonEmptyString, + pub(crate) units: Units, // used only during parsing + pub(crate) content_units: Units, // used only during parsing + pub(crate) transform: Transform, + pub(crate) rect: NonZeroRect, + pub(crate) view_box: Option, + pub(crate) root: Group, +} + +impl Pattern { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Pattern transform. + /// + /// `patternTransform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Pattern rectangle. + /// + /// `x`, `y`, `width` and `height` in SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// Pattern viewbox. + pub fn view_box(&self) -> Option { + self.view_box + } + + /// Pattern children. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// An alias to `NonZeroPositiveF32`. +pub type StrokeWidth = NonZeroPositiveF32; + +/// A `stroke-miterlimit` value. +/// +/// Just like `f32` but immutable and guarantee to be >=1.0. +#[derive(Clone, Copy, Debug)] +pub struct StrokeMiterlimit(f32); + +impl StrokeMiterlimit { + /// Creates a new `StrokeMiterlimit` value. + #[inline] + pub fn new(n: f32) -> Self { + debug_assert!(n.is_finite()); + debug_assert!(n >= 1.0); + + let n = if !(n >= 1.0) { 1.0 } else { n }; + + StrokeMiterlimit(n) + } + + /// Returns an underlying value. + #[inline] + pub fn get(&self) -> f32 { + self.0 + } +} + +impl Default for StrokeMiterlimit { + #[inline] + fn default() -> Self { + StrokeMiterlimit::new(4.0) + } +} + +impl From for StrokeMiterlimit { + #[inline] + fn from(n: f32) -> Self { + Self::new(n) + } +} + +impl PartialEq for StrokeMiterlimit { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.approx_eq_ulps(&other.0, 4) + } +} + +/// A line cap. +/// +/// `stroke-linecap` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Default for LineCap { + fn default() -> Self { + Self::Butt + } +} + +/// A line join. +/// +/// `stroke-linejoin` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LineJoin { + Miter, + MiterClip, + Round, + Bevel, +} + +impl Default for LineJoin { + fn default() -> Self { + Self::Miter + } +} + +/// A stroke style. +#[derive(Clone, Debug)] +pub struct Stroke { + pub(crate) paint: Paint, + pub(crate) dasharray: Option>, + pub(crate) dashoffset: f32, + pub(crate) miterlimit: StrokeMiterlimit, + pub(crate) opacity: Opacity, + pub(crate) width: StrokeWidth, + pub(crate) linecap: LineCap, + pub(crate) linejoin: LineJoin, + // Whether the current stroke needs to be resolved relative + // to a context element. + pub(crate) context_element: Option, +} + +impl Stroke { + /// Stroke paint. + pub fn paint(&self) -> &Paint { + &self.paint + } + + /// Stroke dash array. + pub fn dasharray(&self) -> Option<&[f32]> { + self.dasharray.as_deref() + } + + /// Stroke dash offset. + pub fn dashoffset(&self) -> f32 { + self.dashoffset + } + + /// Stroke miter limit. + pub fn miterlimit(&self) -> StrokeMiterlimit { + self.miterlimit + } + + /// Stroke opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Stroke width. + pub fn width(&self) -> StrokeWidth { + self.width + } + + /// Stroke linecap. + pub fn linecap(&self) -> LineCap { + self.linecap + } + + /// Stroke linejoin. + pub fn linejoin(&self) -> LineJoin { + self.linejoin + } + + /// Converts into a `tiny_skia_path::Stroke` type. + pub fn to_tiny_skia(&self) -> tiny_skia_path::Stroke { + let mut stroke = tiny_skia_path::Stroke { + width: self.width.get(), + miter_limit: self.miterlimit.get(), + line_cap: match self.linecap { + LineCap::Butt => tiny_skia_path::LineCap::Butt, + LineCap::Round => tiny_skia_path::LineCap::Round, + LineCap::Square => tiny_skia_path::LineCap::Square, + }, + line_join: match self.linejoin { + LineJoin::Miter => tiny_skia_path::LineJoin::Miter, + LineJoin::MiterClip => tiny_skia_path::LineJoin::MiterClip, + LineJoin::Round => tiny_skia_path::LineJoin::Round, + LineJoin::Bevel => tiny_skia_path::LineJoin::Bevel, + }, + // According to the spec, dash should not be accounted during + // bbox calculation. + dash: None, + }; + + if let Some(ref list) = self.dasharray { + stroke.dash = tiny_skia_path::StrokeDash::new(list.clone(), self.dashoffset); + } + + stroke + } +} + +/// A fill rule. +/// +/// `fill-rule` attribute in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum FillRule { + NonZero, + EvenOdd, +} + +impl Default for FillRule { + fn default() -> Self { + Self::NonZero + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum ContextElement { + /// The current context element is a use node. Since we can get + /// the bounding box of a use node only once we have converted + /// all elements, we need to fix the transform and units of + /// the stroke/fill after converting the whole tree. + UseNode, + /// The current context element is a path node (i.e. only applicable + /// if we draw the marker of a path). Since we already know the bounding + /// box of the path when rendering the markers, we can convert them directly, + /// so we do it while parsing. + PathNode(Transform, Option), +} + +/// A fill style. +#[derive(Clone, Debug)] +pub struct Fill { + pub(crate) paint: Paint, + pub(crate) opacity: Opacity, + pub(crate) rule: FillRule, + // Whether the current fill needs to be resolved relative + // to a context element. + pub(crate) context_element: Option, +} + +impl Fill { + /// Fill paint. + pub fn paint(&self) -> &Paint { + &self.paint + } + + /// Fill opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Fill rule. + pub fn rule(&self) -> FillRule { + self.rule + } +} + +impl Default for Fill { + fn default() -> Self { + Fill { + paint: Paint::Color(Color::black()), + opacity: Opacity::ONE, + rule: FillRule::default(), + context_element: None, + } + } +} + +/// A 8-bit RGB color. +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub struct Color { + pub red: u8, + pub green: u8, + pub blue: u8, +} + +impl Color { + /// Constructs a new `Color` from RGB values. + #[inline] + pub fn new_rgb(red: u8, green: u8, blue: u8) -> Color { + Color { red, green, blue } + } + + /// Constructs a new `Color` set to black. + #[inline] + pub fn black() -> Color { + Color::new_rgb(0, 0, 0) + } + + /// Constructs a new `Color` set to white. + #[inline] + pub fn white() -> Color { + Color::new_rgb(255, 255, 255) + } +} + +/// A paint style. +/// +/// `paint` value type in the SVG. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum Paint { + Color(Color), + LinearGradient(Arc), + RadialGradient(Arc), + Pattern(Arc), +} + +impl PartialEq for Paint { + #[inline] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Color(lc), Self::Color(rc)) => lc == rc, + (Self::LinearGradient(ref lg1), Self::LinearGradient(ref lg2)) => Arc::ptr_eq(lg1, lg2), + (Self::RadialGradient(ref rg1), Self::RadialGradient(ref rg2)) => Arc::ptr_eq(rg1, rg2), + (Self::Pattern(ref p1), Self::Pattern(ref p2)) => Arc::ptr_eq(p1, p2), + _ => false, + } + } +} + +/// A clip-path element. +/// +/// `clipPath` element in SVG. +#[derive(Debug)] +pub struct ClipPath { + pub(crate) id: NonEmptyString, + pub(crate) transform: Transform, + pub(crate) clip_path: Option>, + pub(crate) root: Group, +} + +impl ClipPath { + pub(crate) fn empty(id: NonEmptyString) -> Self { + ClipPath { + id, + transform: Transform::default(), + clip_path: None, + root: Group::empty(), + } + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Clip path transform. + /// + /// `transform` in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Additional clip path. + /// + /// `clip-path` in SVG. + pub fn clip_path(&self) -> Option<&ClipPath> { + self.clip_path.as_deref() + } + + /// Clip path children. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// A mask type. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum MaskType { + /// Indicates that the luminance values of the mask should be used. + Luminance, + /// Indicates that the alpha values of the mask should be used. + Alpha, +} + +impl Default for MaskType { + fn default() -> Self { + Self::Luminance + } +} + +/// A mask element. +/// +/// `mask` element in SVG. +#[derive(Debug)] +pub struct Mask { + pub(crate) id: NonEmptyString, + pub(crate) rect: NonZeroRect, + pub(crate) kind: MaskType, + pub(crate) mask: Option>, + pub(crate) root: Group, +} + +impl Mask { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Used only during SVG writing. `resvg` doesn't rely on this property. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Mask rectangle. + /// + /// `x`, `y`, `width` and `height` in SVG. + pub fn rect(&self) -> NonZeroRect { + self.rect + } + + /// Mask type. + /// + /// `mask-type` in SVG. + pub fn kind(&self) -> MaskType { + self.kind + } + + /// Additional mask. + /// + /// `mask` in SVG. + pub fn mask(&self) -> Option<&Mask> { + self.mask.as_deref() + } + + /// Mask children. + /// + /// A mask can have no children, in which case the whole element should be masked out. + pub fn root(&self) -> &Group { + &self.root + } +} + +/// Node's kind. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum Node { + Group(Box), + Path(Box), + Image(Box), + Text(Box), +} + +impl Node { + /// Returns node's ID. + pub fn id(&self) -> &str { + match self { + Node::Group(ref e) => e.id.as_str(), + Node::Path(ref e) => e.id.as_str(), + Node::Image(ref e) => e.id.as_str(), + Node::Text(ref e) => e.id.as_str(), + } + } + + /// Returns note's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + match self { + Node::Group(ref e) => e.class.as_str(), + Node::Path(ref e) => e.class.as_str(), + Node::Image(ref e) => e.class.as_str(), + Node::Text(ref e) => e.class.as_str(), + } + } + + /// Returns node's absolute transform. + /// + /// This method is cheap since absolute transforms are already resolved. + pub fn abs_transform(&self) -> Transform { + match self { + Node::Group(ref group) => group.abs_transform(), + Node::Path(ref path) => path.abs_transform(), + Node::Image(ref image) => image.abs_transform(), + Node::Text(ref text) => text.abs_transform(), + } + } + + /// Returns node's bounding box in object coordinates, if any. + pub fn bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.bounding_box(), + Node::Path(ref path) => path.bounding_box(), + Node::Image(ref image) => image.bounding_box(), + Node::Text(ref text) => text.bounding_box(), + } + } + + /// Returns node's bounding box in canvas coordinates, if any. + pub fn abs_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.abs_bounding_box(), + Node::Path(ref path) => path.abs_bounding_box(), + Node::Image(ref image) => image.abs_bounding_box(), + Node::Text(ref text) => text.abs_bounding_box(), + } + } + + /// Returns node's bounding box, including stroke, in object coordinates, if any. + pub fn stroke_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.stroke_bounding_box(), + Node::Path(ref path) => path.stroke_bounding_box(), + // Image cannot be stroked. + Node::Image(ref image) => image.bounding_box(), + Node::Text(ref text) => text.stroke_bounding_box(), + } + } + + /// Returns node's bounding box, including stroke, in canvas coordinates, if any. + pub fn abs_stroke_bounding_box(&self) -> Rect { + match self { + Node::Group(ref group) => group.abs_stroke_bounding_box(), + Node::Path(ref path) => path.abs_stroke_bounding_box(), + // Image cannot be stroked. + Node::Image(ref image) => image.abs_bounding_box(), + Node::Text(ref text) => text.abs_stroke_bounding_box(), + } + } + + /// Element's "layer" bounding box in canvas units, if any. + /// + /// For most nodes this is just `abs_bounding_box`, + /// but for groups this is `abs_layer_bounding_box`. + /// + /// See [`Group::layer_bounding_box`] for details. + pub fn abs_layer_bounding_box(&self) -> Option { + match self { + Node::Group(ref group) => Some(group.abs_layer_bounding_box()), + // Hor/ver path without stroke can return None. This is expected. + Node::Path(ref path) => path.abs_bounding_box().to_non_zero_rect(), + Node::Image(ref image) => image.abs_bounding_box().to_non_zero_rect(), + Node::Text(ref text) => text.abs_bounding_box().to_non_zero_rect(), + } + } + + /// Calls a closure for each subroot this `Node` has. + /// + /// The [`Tree::root`](Tree::root) field contain only render-able SVG elements. + /// But some elements, specifically clip paths, masks, patterns and feImage + /// can store their own SVG subtrees. + /// And while one can access them manually, it's pretty verbose. + /// This methods allows looping over _all_ SVG elements present in the `Tree`. + /// + /// # Example + /// + /// ```no_run + /// fn all_nodes(parent: &usvg::Group) { + /// for node in parent.children() { + /// // do stuff... + /// + /// if let usvg::Node::Group(ref g) = node { + /// all_nodes(g); + /// } + /// + /// // handle subroots as well + /// node.subroots(|subroot| all_nodes(subroot)); + /// } + /// } + /// ``` + pub fn subroots(&self, mut f: F) { + match self { + Node::Group(ref group) => group.subroots(&mut f), + Node::Path(ref path) => path.subroots(&mut f), + Node::Image(ref image) => image.subroots(&mut f), + Node::Text(ref text) => text.subroots(&mut f), + } + } +} + +/// A group container. +/// +/// The preprocessor will remove all groups that don't impact rendering. +/// Those that left is just an indicator that a new canvas should be created. +/// +/// `g` element in SVG. +#[derive(Clone, Debug)] +pub struct Group { + pub(crate) id: String, + #[cfg(feature = "class")] + pub(crate) class: String, + pub(crate) transform: Transform, + pub(crate) abs_transform: Transform, + pub(crate) opacity: Opacity, + pub(crate) blend_mode: BlendMode, + pub(crate) isolate: bool, + pub(crate) clip_path: Option>, + /// Whether the group is a context element (i.e. a use node) + pub(crate) is_context_element: bool, + pub(crate) mask: Option>, + pub(crate) filters: Vec>, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, + pub(crate) layer_bounding_box: NonZeroRect, + pub(crate) abs_layer_bounding_box: NonZeroRect, + pub(crate) children: Vec, +} + +impl Group { + pub(crate) fn empty() -> Self { + let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); + Group { + id: String::new(), + #[cfg(feature = "class")] + class: String::new(), + transform: Transform::default(), + abs_transform: Transform::default(), + opacity: Opacity::ONE, + blend_mode: BlendMode::Normal, + isolate: false, + clip_path: None, + mask: None, + filters: Vec::new(), + is_context_element: false, + bounding_box: dummy, + abs_bounding_box: dummy, + stroke_bounding_box: dummy, + abs_stroke_bounding_box: dummy, + layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + abs_layer_bounding_box: NonZeroRect::from_xywh(0.0, 0.0, 1.0, 1.0).unwrap(), + children: Vec::new(), + } + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + &self.class + } + + /// Element's transform. + /// + /// This is a relative transform. The one that is set via the `transform` attribute in SVG. + pub fn transform(&self) -> Transform { + self.transform + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including group's transform. + /// + /// Note that subroots, like clipPaths, masks and patterns, have their own root transform, + /// which isn't affected by the node that references this subroot. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Group opacity. + /// + /// After the group is rendered we should combine + /// it with a parent group using the specified opacity. + pub fn opacity(&self) -> Opacity { + self.opacity + } + + /// Group blend mode. + /// + /// `mix-blend-mode` in SVG. + pub fn blend_mode(&self) -> BlendMode { + self.blend_mode + } + + /// Group isolation. + /// + /// `isolation` in SVG. + pub fn isolate(&self) -> bool { + self.isolate + } + + /// Element's clip path. + pub fn clip_path(&self) -> Option<&ClipPath> { + self.clip_path.as_deref() + } + + /// Element's mask. + pub fn mask(&self) -> Option<&Mask> { + self.mask.as_deref() + } + + /// Element's filters. + pub fn filters(&self) -> &[Arc] { + &self.filters + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + /// + /// Can be set to `None` in case of an empty group. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Similar to `bounding_box`, but includes stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in user coordinates. + /// + /// Similar to `abs_bounding_box`, but includes stroke. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + /// Element's "layer" bounding box in object units. + /// + /// Conceptually, this is `stroke_bounding_box` expanded and/or clipped + /// by `filters_bounding_box`, but also including all the children. + /// This is the bounding box `resvg` will later use to allocate layers/pixmaps + /// during isolated groups rendering. + /// + /// Only groups have it, because only groups can have filters. + /// For other nodes layer bounding box is the same as stroke bounding box. + /// + /// Unlike other bounding boxes, cannot have zero size. + pub fn layer_bounding_box(&self) -> NonZeroRect { + self.layer_bounding_box + } + + /// Element's "layer" bounding box in canvas units. + pub fn abs_layer_bounding_box(&self) -> NonZeroRect { + self.abs_layer_bounding_box + } + + /// Group's children. + pub fn children(&self) -> &[Node] { + &self.children + } + + /// Checks if this group should be isolated during rendering. + pub fn should_isolate(&self) -> bool { + self.isolate + || self.opacity != Opacity::ONE + || self.clip_path.is_some() + || self.mask.is_some() + || !self.filters.is_empty() + || self.blend_mode != BlendMode::Normal // TODO: probably not needed? + } + + /// Returns `true` if the group has any children. + pub fn has_children(&self) -> bool { + !self.children.is_empty() + } + + /// Calculates a node's filter bounding box. + /// + /// Filters with `objectBoundingBox` and missing or zero `bounding_box` would be ignored. + /// + /// Note that a filter region can act like a clipping rectangle, + /// therefore this function can produce a bounding box smaller than `bounding_box`. + /// + /// Returns `None` when then group has no filters. + /// + /// This function is very fast, that's why we do not store this bbox as a `Group` field. + pub fn filters_bounding_box(&self) -> Option { + let mut full_region = BBox::default(); + for filter in &self.filters { + full_region = full_region.expand(filter.rect); + } + + full_region.to_non_zero_rect() + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let Some(ref clip) = self.clip_path { + f(&clip.root); + + if let Some(ref sub_clip) = clip.clip_path { + f(&sub_clip.root); + } + } + + if let Some(ref mask) = self.mask { + f(&mask.root); + + if let Some(ref sub_mask) = mask.mask { + f(&sub_mask.root); + } + } + + for filter in &self.filters { + for primitive in &filter.primitives { + if let filter::Kind::Image(ref image) = primitive.kind { + if let filter::ImageKind::Use(ref use_node) = image.data { + f(use_node); + } + } + } + } + } +} + +/// Representation of the [`paint-order`] property. +/// +/// `usvg` will handle `markers` automatically, +/// therefore we provide only `fill` and `stroke` variants. +/// +/// [`paint-order`]: https://www.w3.org/TR/SVG2/painting.html#PaintOrder +#[derive(Clone, Copy, PartialEq, Debug)] +#[allow(missing_docs)] +pub enum PaintOrder { + FillAndStroke, + StrokeAndFill, +} + +impl Default for PaintOrder { + fn default() -> Self { + Self::FillAndStroke + } +} + +/// A path element. +#[derive(Clone, Debug)] +pub struct Path { + pub(crate) id: String, + #[cfg(feature = "class")] + pub(crate) class: String, + pub(crate) visibility: Visibility, + pub(crate) fill: Option, + pub(crate) stroke: Option, + pub(crate) paint_order: PaintOrder, + pub(crate) rendering_mode: ShapeRendering, + pub(crate) data: Arc, + pub(crate) abs_transform: Transform, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, +} + +impl Path { + pub(crate) fn new_simple(data: Arc) -> Option { + Self::new( + String::new(), + #[cfg(feature = "class")] + String::new(), + Visibility::default(), + None, + None, + PaintOrder::default(), + ShapeRendering::default(), + data, + Transform::default(), + ) + } + + pub(crate) fn new( + id: String, + #[cfg(feature = "class")] class: String, + visibility: Visibility, + fill: Option, + stroke: Option, + paint_order: PaintOrder, + rendering_mode: ShapeRendering, + data: Arc, + abs_transform: Transform, + ) -> Option { + let bounding_box = data.compute_tight_bounds()?; + let stroke_bounding_box = + Path::calculate_stroke_bbox(stroke.as_ref(), &data).unwrap_or(bounding_box); + + let abs_bounding_box: Rect; + let abs_stroke_bounding_box: Rect; + if abs_transform.has_skew() { + // TODO: avoid re-alloc + let path2 = data.as_ref().clone(); + let path2 = path2.transform(abs_transform)?; + abs_bounding_box = path2.compute_tight_bounds()?; + abs_stroke_bounding_box = + Path::calculate_stroke_bbox(stroke.as_ref(), &path2).unwrap_or(abs_bounding_box); + } else { + // A transform without a skew can be performed just on a bbox. + abs_bounding_box = bounding_box.transform(abs_transform)?; + abs_stroke_bounding_box = stroke_bounding_box.transform(abs_transform)?; + } + + Some(Path { + id, + #[cfg(feature = "class")] + class, + visibility, + fill, + stroke, + paint_order, + rendering_mode, + data, + abs_transform, + bounding_box, + abs_bounding_box, + stroke_bounding_box, + abs_stroke_bounding_box, + }) + } + + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + &self.class + } + + /// Element visibility. + pub fn visibility(&self) -> Visibility { + self.visibility + } + + /// Fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// Stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } + + /// Fill and stroke paint order. + /// + /// Since markers will be replaced with regular nodes automatically, + /// `usvg` doesn't provide the `markers` order type. It's was already done. + /// + /// `paint-order` in SVG. + pub fn paint_order(&self) -> PaintOrder { + self.paint_order + } + + /// Rendering mode. + /// + /// `shape-rendering` in SVG. + pub fn rendering_mode(&self) -> ShapeRendering { + self.rendering_mode + } + + // TODO: find a better name + /// Segments list. + /// + /// All segments are in absolute coordinates. + pub fn data(&self) -> &tiny_skia_path::Path { + self.data.as_ref() + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Will have the same value as `bounding_box` when path has no stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in canvas coordinates. + /// + /// Will have the same value as `abs_bounding_box` when path has no stroke. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + fn calculate_stroke_bbox(stroke: Option<&Stroke>, path: &tiny_skia_path::Path) -> Option { + let mut stroke = stroke?.to_tiny_skia(); + // According to the spec, dash should not be accounted during bbox calculation. + stroke.dash = None; + + // TODO: avoid for round and bevel caps + + // Expensive, but there is not much we can do about it. + if let Some(stroked_path) = path.stroke(&stroke, 1.0) { + return stroked_path.compute_tight_bounds(); + } + + None + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let Some(Paint::Pattern(ref patt)) = self.fill.as_ref().map(|f| &f.paint) { + f(patt.root()) + } + if let Some(Paint::Pattern(ref patt)) = self.stroke.as_ref().map(|f| &f.paint) { + f(patt.root()) + } + } +} + +/// An embedded image kind. +#[derive(Clone)] +pub enum ImageKind { + /// A reference to raw JPEG data. Should be decoded by the caller. + JPEG(Arc>), + /// A reference to raw PNG data. Should be decoded by the caller. + PNG(Arc>), + /// A reference to raw GIF data. Should be decoded by the caller. + GIF(Arc>), + /// A preprocessed SVG tree. Can be rendered as is. + SVG(Tree), +} + +impl std::fmt::Debug for ImageKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ImageKind::JPEG(_) => f.write_str("ImageKind::JPEG(..)"), + ImageKind::PNG(_) => f.write_str("ImageKind::PNG(..)"), + ImageKind::GIF(_) => f.write_str("ImageKind::GIF(..)"), + ImageKind::SVG(_) => f.write_str("ImageKind::SVG(..)"), + } + } +} + +/// A raster image element. +/// +/// `image` element in SVG. +#[derive(Clone, Debug)] +pub struct Image { + pub(crate) id: String, + #[cfg(feature = "class")] + pub(crate) class: String, + pub(crate) visibility: Visibility, + pub(crate) view_box: ViewBox, + pub(crate) rendering_mode: ImageRendering, + pub(crate) kind: ImageKind, + pub(crate) abs_transform: Transform, + pub(crate) abs_bounding_box: NonZeroRect, +} + +impl Image { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + &self.class + } + + /// Element visibility. + pub fn visibility(&self) -> Visibility { + self.visibility + } + + /// An image rectangle in which it should be fit. + /// + /// Combination of the `x`, `y`, `width`, `height` and `preserveAspectRatio` + /// attributes. + pub fn view_box(&self) -> ViewBox { + self.view_box + } + + /// Rendering mode. + /// + /// `image-rendering` in SVG. + pub fn rendering_mode(&self) -> ImageRendering { + self.rendering_mode + } + + /// Image data. + pub fn kind(&self) -> &ImageKind { + &self.kind + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's object bounding box. + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + pub fn bounding_box(&self) -> Rect { + self.view_box.rect.to_rect() + } + + /// Element's bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box.to_rect() + } + + fn subroots(&self, f: &mut dyn FnMut(&Group)) { + if let ImageKind::SVG(ref tree) = self.kind { + f(&tree.root) + } + } +} + +/// A nodes tree container. +#[allow(missing_debug_implementations)] +#[derive(Clone, Debug)] +pub struct Tree { + pub(crate) size: Size, + pub(crate) view_box: ViewBox, + pub(crate) root: Group, + pub(crate) linear_gradients: Vec>, + pub(crate) radial_gradients: Vec>, + pub(crate) patterns: Vec>, + pub(crate) clip_paths: Vec>, + pub(crate) masks: Vec>, + pub(crate) filters: Vec>, +} + +impl Tree { + /// Image size. + /// + /// Size of an image that should be created to fit the SVG. + /// + /// `width` and `height` in SVG. + pub fn size(&self) -> Size { + self.size + } + + /// SVG viewbox. + /// + /// Specifies which part of the SVG image should be rendered. + /// + /// `viewBox` and `preserveAspectRatio` in SVG. + pub fn view_box(&self) -> ViewBox { + self.view_box + } + + /// The root element of the SVG tree. + pub fn root(&self) -> &Group { + &self.root + } + + /// Returns a renderable node by ID. + /// + /// If an empty ID is provided, than this method will always return `None`. + pub fn node_by_id(&self, id: &str) -> Option<&Node> { + if id.is_empty() { + return None; + } + + node_by_id(&self.root, id) + } + + /// Checks if the current tree has any text nodes. + pub fn has_text_nodes(&self) -> bool { + has_text_nodes(&self.root) + } + + /// Returns a list of all unique [`LinearGradient`]s in the tree. + pub fn linear_gradients(&self) -> &[Arc] { + &self.linear_gradients + } + + /// Returns a list of all unique [`RadialGradient`]s in the tree. + pub fn radial_gradients(&self) -> &[Arc] { + &self.radial_gradients + } + + /// Returns a list of all unique [`Pattern`]s in the tree. + pub fn patterns(&self) -> &[Arc] { + &self.patterns + } + + /// Returns a list of all unique [`ClipPath`]s in the tree. + pub fn clip_paths(&self) -> &[Arc] { + &self.clip_paths + } + + /// Returns a list of all unique [`Mask`]s in the tree. + pub fn masks(&self) -> &[Arc] { + &self.masks + } + + /// Returns a list of all unique [`Filter`](filter::Filter)s in the tree. + pub fn filters(&self) -> &[Arc] { + &self.filters + } + + pub(crate) fn collect_paint_servers(&mut self) { + loop_over_paint_servers(&self.root, &mut |paint| match paint { + Paint::Color(_) => {} + Paint::LinearGradient(lg) => { + if !self + .linear_gradients + .iter() + .any(|other| Arc::ptr_eq(&lg, other)) + { + self.linear_gradients.push(lg.clone()); + } + } + Paint::RadialGradient(rg) => { + if !self + .radial_gradients + .iter() + .any(|other| Arc::ptr_eq(&rg, other)) + { + self.radial_gradients.push(rg.clone()); + } + } + Paint::Pattern(patt) => { + if !self.patterns.iter().any(|other| Arc::ptr_eq(&patt, other)) { + self.patterns.push(patt.clone()); + } + } + }); + } +} + +fn node_by_id<'a>(parent: &'a Group, id: &str) -> Option<&'a Node> { + for child in &parent.children { + if child.id() == id { + return Some(child); + } + + if let Node::Group(ref g) = child { + if let Some(n) = node_by_id(g, id) { + return Some(n); + } + } + } + + None +} + +fn has_text_nodes(root: &Group) -> bool { + for node in &root.children { + if let Node::Text(_) = node { + return true; + } + + let mut has_text = false; + + if let Node::Image(ref image) = node { + if let ImageKind::SVG(ref tree) = image.kind { + if has_text_nodes(&tree.root) { + has_text = true; + } + } + } + + node.subroots(|subroot| has_text |= has_text_nodes(subroot)); + + if has_text { + return true; + } + } + + true +} + +fn loop_over_paint_servers(parent: &Group, f: &mut dyn FnMut(&Paint)) { + fn push(paint: Option<&Paint>, f: &mut dyn FnMut(&Paint)) { + if let Some(paint) = paint { + f(paint); + } + } + + for node in &parent.children { + match node { + Node::Group(ref group) => loop_over_paint_servers(group, f), + Node::Path(ref path) => { + push(path.fill.as_ref().map(|f| &f.paint), f); + push(path.stroke.as_ref().map(|f| &f.paint), f); + } + Node::Image(_) => {} + // Flattened text would be used instead. + Node::Text(_) => {} + } + + node.subroots(|subroot| loop_over_paint_servers(subroot, f)); + } +} + +impl Group { + pub(crate) fn collect_clip_paths(&self, clip_paths: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + if let Some(ref clip) = g.clip_path { + if !clip_paths.iter().any(|other| Arc::ptr_eq(&clip, other)) { + clip_paths.push(clip.clone()); + } + + if let Some(ref sub_clip) = clip.clip_path { + if !clip_paths.iter().any(|other| Arc::ptr_eq(&sub_clip, other)) { + clip_paths.push(sub_clip.clone()); + } + } + } + } + + node.subroots(|subroot| subroot.collect_clip_paths(clip_paths)); + + if let Node::Group(ref g) = node { + g.collect_clip_paths(clip_paths); + } + } + } + + pub(crate) fn collect_masks(&self, masks: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + if let Some(ref mask) = g.mask { + if !masks.iter().any(|other| Arc::ptr_eq(&mask, other)) { + masks.push(mask.clone()); + } + + if let Some(ref sub_mask) = mask.mask { + if !masks.iter().any(|other| Arc::ptr_eq(&sub_mask, other)) { + masks.push(sub_mask.clone()); + } + } + } + } + + node.subroots(|subroot| subroot.collect_masks(masks)); + + if let Node::Group(ref g) = node { + g.collect_masks(masks); + } + } + } + + pub(crate) fn collect_filters(&self, filters: &mut Vec>) { + for node in self.children() { + if let Node::Group(ref g) = node { + for filter in g.filters() { + if !filters.iter().any(|other| Arc::ptr_eq(&filter, other)) { + filters.push(filter.clone()); + } + } + } + + node.subroots(|subroot| subroot.collect_filters(filters)); + + if let Node::Group(ref g) = node { + g.collect_filters(filters); + } + } + } + + pub(crate) fn calculate_object_bbox(&mut self) -> Option { + let mut bbox = BBox::default(); + for child in &self.children { + let mut c_bbox = child.bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + bbox = bbox.expand(c_bbox); + } + + bbox.to_non_zero_rect() + } + + pub(crate) fn calculate_bounding_boxes(&mut self) -> Option<()> { + let mut bbox = BBox::default(); + let mut abs_bbox = BBox::default(); + let mut stroke_bbox = BBox::default(); + let mut abs_stroke_bbox = BBox::default(); + let mut layer_bbox = BBox::default(); + for child in &self.children { + { + let mut c_bbox = child.bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + bbox = bbox.expand(c_bbox); + } + + abs_bbox = abs_bbox.expand(child.abs_bounding_box()); + + { + let mut c_bbox = child.stroke_bounding_box(); + if let Node::Group(ref group) = child { + if let Some(r) = c_bbox.transform(group.transform) { + c_bbox = r; + } + } + + stroke_bbox = stroke_bbox.expand(c_bbox); + } + + abs_stroke_bbox = abs_stroke_bbox.expand(child.abs_stroke_bounding_box()); + + if let Node::Group(ref group) = child { + let r = group.layer_bounding_box; + if let Some(r) = r.transform(group.transform) { + layer_bbox = layer_bbox.expand(r); + } + } else { + // Not a group - no need to transform. + layer_bbox = layer_bbox.expand(child.stroke_bounding_box()); + } + } + + // `bbox` can be None for empty groups, but we still have to + // calculate `layer_bounding_box after` it. + if let Some(bbox) = bbox.to_rect() { + self.bounding_box = bbox; + self.abs_bounding_box = abs_bbox.to_rect()?; + self.stroke_bounding_box = stroke_bbox.to_rect()?; + self.abs_stroke_bounding_box = abs_stroke_bbox.to_rect()?; + } + + // Filter bbox has a higher priority than layers bbox. + if let Some(filter_bbox) = self.filters_bounding_box() { + self.layer_bounding_box = filter_bbox; + } else { + self.layer_bounding_box = layer_bbox.to_non_zero_rect()?; + } + + self.abs_layer_bounding_box = self.layer_bounding_box.transform(self.abs_transform)?; + + Some(()) + } +} diff --git a/crates/usvg/src/tree/text.rs b/crates/usvg/src/tree/text.rs new file mode 100644 index 000000000..bec3852c5 --- /dev/null +++ b/crates/usvg/src/tree/text.rs @@ -0,0 +1,602 @@ +// 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/. + +use std::sync::Arc; + +use strict_num::NonZeroPositiveF32; +pub use svgtypes::FontFamily; + +#[cfg(feature = "text")] +use crate::layout::Span; +use crate::{ + Fill, Group, NonEmptyString, PaintOrder, Rect, Stroke, TextRendering, Transform, Visibility, +}; + +/// A font stretch property. +#[allow(missing_docs)] +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] +pub enum FontStretch { + UltraCondensed, + ExtraCondensed, + Condensed, + SemiCondensed, + Normal, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl Default for FontStretch { + #[inline] + fn default() -> Self { + Self::Normal + } +} + +/// A font style property. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub enum FontStyle { + /// A face that is neither italic not obliqued. + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +impl Default for FontStyle { + #[inline] + fn default() -> FontStyle { + Self::Normal + } +} + +/// Text font properties. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct Font { + pub(crate) families: Vec, + pub(crate) style: FontStyle, + pub(crate) stretch: FontStretch, + pub(crate) weight: u16, +} + +impl Font { + /// A list of family names. + /// + /// Never empty. Uses `usvg::Options::font_family` as fallback. + pub fn families(&self) -> &[FontFamily] { + &self.families + } + + /// A font style. + pub fn style(&self) -> FontStyle { + self.style + } + + /// A font stretch. + pub fn stretch(&self) -> FontStretch { + self.stretch + } + + /// A font width. + pub fn weight(&self) -> u16 { + self.weight + } +} + +/// A dominant baseline property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum DominantBaseline { + Auto, + UseScript, + NoChange, + ResetSize, + Ideographic, + Alphabetic, + Hanging, + Mathematical, + Central, + Middle, + TextAfterEdge, + TextBeforeEdge, +} + +impl Default for DominantBaseline { + fn default() -> Self { + Self::Auto + } +} + +/// An alignment baseline property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum AlignmentBaseline { + Auto, + Baseline, + BeforeEdge, + TextBeforeEdge, + Middle, + Central, + AfterEdge, + TextAfterEdge, + Ideographic, + Alphabetic, + Hanging, + Mathematical, +} + +impl Default for AlignmentBaseline { + fn default() -> Self { + Self::Auto + } +} + +/// A baseline shift property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BaselineShift { + Baseline, + Subscript, + Superscript, + Number(f32), +} + +impl Default for BaselineShift { + #[inline] + fn default() -> BaselineShift { + BaselineShift::Baseline + } +} + +/// A length adjust property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum LengthAdjust { + Spacing, + SpacingAndGlyphs, +} + +impl Default for LengthAdjust { + fn default() -> Self { + Self::Spacing + } +} + +/// A text span decoration style. +/// +/// In SVG, text decoration and text it's applied to can have different styles. +/// So you can have black text and green underline. +/// +/// Also, in SVG you can specify text decoration stroking. +#[derive(Clone, Debug)] +pub struct TextDecorationStyle { + pub(crate) fill: Option, + pub(crate) stroke: Option, +} + +impl TextDecorationStyle { + /// A fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// A stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } +} + +/// A text span decoration. +#[derive(Clone, Debug)] +pub struct TextDecoration { + pub(crate) underline: Option, + pub(crate) overline: Option, + pub(crate) line_through: Option, +} + +impl TextDecoration { + /// An optional underline and its style. + pub fn underline(&self) -> Option<&TextDecorationStyle> { + self.underline.as_ref() + } + + /// An optional overline and its style. + pub fn overline(&self) -> Option<&TextDecorationStyle> { + self.overline.as_ref() + } + + /// An optional line-through and its style. + pub fn line_through(&self) -> Option<&TextDecorationStyle> { + self.line_through.as_ref() + } +} + +/// A text style span. +/// +/// Spans do not overlap inside a text chunk. +#[derive(Clone, Debug)] +pub struct TextSpan { + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) fill: Option, + pub(crate) stroke: Option, + pub(crate) paint_order: PaintOrder, + pub(crate) font: Font, + pub(crate) font_size: NonZeroPositiveF32, + pub(crate) small_caps: bool, + pub(crate) apply_kerning: bool, + pub(crate) decoration: TextDecoration, + pub(crate) dominant_baseline: DominantBaseline, + pub(crate) alignment_baseline: AlignmentBaseline, + pub(crate) baseline_shift: Vec, + pub(crate) visibility: Visibility, + pub(crate) letter_spacing: f32, + pub(crate) word_spacing: f32, + pub(crate) text_length: Option, + pub(crate) length_adjust: LengthAdjust, +} + +impl TextSpan { + /// A span start in bytes. + /// + /// Offset is relative to the parent text chunk and not the parent text element. + pub fn start(&self) -> usize { + self.start + } + + /// A span end in bytes. + /// + /// Offset is relative to the parent text chunk and not the parent text element. + pub fn end(&self) -> usize { + self.end + } + + /// A fill style. + pub fn fill(&self) -> Option<&Fill> { + self.fill.as_ref() + } + + /// A stroke style. + pub fn stroke(&self) -> Option<&Stroke> { + self.stroke.as_ref() + } + + /// A paint order style. + pub fn paint_order(&self) -> PaintOrder { + self.paint_order + } + + /// A font. + pub fn font(&self) -> &Font { + &self.font + } + + /// A font size. + pub fn font_size(&self) -> NonZeroPositiveF32 { + self.font_size + } + + /// Indicates that small caps should be used. + /// + /// Set by `font-variant="small-caps"` + pub fn small_caps(&self) -> bool { + self.small_caps + } + + /// Indicates that a kerning should be applied. + /// + /// Supports both `kerning` and `font-kerning` properties. + pub fn apply_kerning(&self) -> bool { + self.apply_kerning + } + + /// A span decorations. + pub fn decoration(&self) -> &TextDecoration { + &self.decoration + } + + /// A span dominant baseline. + pub fn dominant_baseline(&self) -> DominantBaseline { + self.dominant_baseline + } + + /// A span alignment baseline. + pub fn alignment_baseline(&self) -> AlignmentBaseline { + self.alignment_baseline + } + + /// A list of all baseline shift that should be applied to this span. + /// + /// Ordered from `text` element down to the actual `span` element. + pub fn baseline_shift(&self) -> &[BaselineShift] { + &self.baseline_shift + } + + /// A visibility property. + pub fn visibility(&self) -> Visibility { + self.visibility + } + + /// A letter spacing property. + pub fn letter_spacing(&self) -> f32 { + self.letter_spacing + } + + /// A word spacing property. + pub fn word_spacing(&self) -> f32 { + self.word_spacing + } + + /// A text length property. + pub fn text_length(&self) -> Option { + self.text_length + } + + /// A length adjust property. + pub fn length_adjust(&self) -> LengthAdjust { + self.length_adjust + } +} + +/// A text chunk anchor property. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum TextAnchor { + Start, + Middle, + End, +} + +impl Default for TextAnchor { + fn default() -> Self { + Self::Start + } +} + +/// A path used by text-on-path. +#[derive(Debug)] +pub struct TextPath { + pub(crate) id: NonEmptyString, + #[cfg(feature = "class")] + pub(crate) class: String, + pub(crate) start_offset: f32, + pub(crate) path: Arc, +} + +impl TextPath { + /// Element's ID. + /// + /// Taken from the SVG itself. + pub fn id(&self) -> &str { + self.id.get() + } + + /// Element's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + &self.class + } + + /// A text offset in SVG coordinates. + /// + /// Percentage values already resolved. + pub fn start_offset(&self) -> f32 { + self.start_offset + } + + /// A path. + pub fn path(&self) -> &tiny_skia_path::Path { + &self.path + } +} + +/// A text chunk flow property. +#[derive(Clone, Debug)] +pub enum TextFlow { + /// A linear layout. + /// + /// Includes left-to-right, right-to-left and top-to-bottom. + Linear, + /// A text-on-path layout. + Path(Arc), +} + +/// A text chunk. +/// +/// Text alignment and BIDI reordering can only be done inside a text chunk. +#[derive(Clone, Debug)] +pub struct TextChunk { + pub(crate) x: Option, + pub(crate) y: Option, + pub(crate) anchor: TextAnchor, + pub(crate) spans: Vec, + pub(crate) text_flow: TextFlow, + pub(crate) text: String, +} + +impl TextChunk { + /// An absolute X axis offset. + pub fn x(&self) -> Option { + self.x + } + + /// An absolute Y axis offset. + pub fn y(&self) -> Option { + self.y + } + + /// A text anchor. + pub fn anchor(&self) -> TextAnchor { + self.anchor + } + + /// A list of text chunk style spans. + pub fn spans(&self) -> &[TextSpan] { + &self.spans + } + + /// A text chunk flow. + pub fn text_flow(&self) -> TextFlow { + self.text_flow.clone() + } + + /// A text chunk actual text. + pub fn text(&self) -> &str { + &self.text + } +} + +/// A writing mode. +#[allow(missing_docs)] +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum WritingMode { + LeftToRight, + TopToBottom, +} + +/// A text element. +/// +/// `text` element in SVG. +#[derive(Clone, Debug)] +pub struct Text { + pub(crate) id: String, + #[cfg(feature = "class")] + pub(crate) class: String, + pub(crate) rendering_mode: TextRendering, + pub(crate) dx: Vec, + pub(crate) dy: Vec, + pub(crate) rotate: Vec, + pub(crate) writing_mode: WritingMode, + pub(crate) chunks: Vec, + pub(crate) abs_transform: Transform, + pub(crate) bounding_box: Rect, + pub(crate) abs_bounding_box: Rect, + pub(crate) stroke_bounding_box: Rect, + pub(crate) abs_stroke_bounding_box: Rect, + pub(crate) flattened: Box, + #[cfg(feature = "text")] + pub(crate) layouted: Vec, +} + +impl Text { + /// Element's ID. + /// + /// Taken from the SVG itself. + /// Isn't automatically generated. + /// Can be empty. + pub fn id(&self) -> &str { + &self.id + } + + /// Element's class names. + #[cfg(feature = "class")] + pub fn class(&self) -> &str { + &self.class + } + + /// Rendering mode. + /// + /// `text-rendering` in SVG. + pub fn rendering_mode(&self) -> TextRendering { + self.rendering_mode + } + + /// A relative X axis offsets. + /// + /// One offset for each Unicode codepoint. Aka `char` in Rust. + pub fn dx(&self) -> &[f32] { + &self.dx + } + + /// A relative Y axis offsets. + /// + /// One offset for each Unicode codepoint. Aka `char` in Rust. + pub fn dy(&self) -> &[f32] { + &self.dy + } + + /// A list of rotation angles. + /// + /// One angle for each Unicode codepoint. Aka `char` in Rust. + pub fn rotate(&self) -> &[f32] { + &self.rotate + } + + /// A writing mode. + pub fn writing_mode(&self) -> WritingMode { + self.writing_mode + } + + /// A list of text chunks. + pub fn chunks(&self) -> &[TextChunk] { + &self.chunks + } + + /// Element's absolute transform. + /// + /// Contains all ancestors transforms including elements's transform. + /// + /// Note that this is not the relative transform present in SVG. + /// The SVG one would be set only on groups. + pub fn abs_transform(&self) -> Transform { + self.abs_transform + } + + /// Element's text bounding box. + /// + /// Text bounding box is special in SVG and doesn't represent + /// tight bounds of the element's content. + /// You can find more about it + /// [here](https://razrfalcon.github.io/notes-on-svg-parsing/text/bbox.html). + /// + /// `objectBoundingBox` in SVG terms. Meaning it doesn't affected by parent transforms. + /// + /// Returns `None` when the `text` build feature was disabled. + /// This is because we have to perform a text layout before calculating a bounding box. + pub fn bounding_box(&self) -> Rect { + self.bounding_box + } + + /// Element's text bounding box in canvas coordinates. + /// + /// `userSpaceOnUse` in SVG terms. + pub fn abs_bounding_box(&self) -> Rect { + self.abs_bounding_box + } + + /// Element's object bounding box including stroke. + /// + /// Similar to `bounding_box`, but includes stroke. + /// + /// Will have the same value as `bounding_box` when path has no stroke. + pub fn stroke_bounding_box(&self) -> Rect { + self.stroke_bounding_box + } + + /// Element's bounding box including stroke in canvas coordinates. + pub fn abs_stroke_bounding_box(&self) -> Rect { + self.abs_stroke_bounding_box + } + + /// Text converted into paths, ready to render. + pub fn flattened(&self) -> &Group { + &self.flattened + } + + /// The positioned glyphs and decoration spans of the text. + /// + /// This should only be used if you need more low-level access + /// to the glyphs that make up the text. If you just need the + /// outlines of the text, you should use `flattened` instead. + #[cfg(feature = "text")] + pub fn layouted(&self) -> &[Span] { + &self.layouted + } + + pub(crate) fn subroots(&self, f: &mut dyn FnMut(&Group)) { + f(&self.flattened); + } +} diff --git a/crates/usvg/src/writer.rs b/crates/usvg/src/writer.rs index 3d0e21062..15434f8bc 100644 --- a/crates/usvg/src/writer.rs +++ b/crates/usvg/src/writer.rs @@ -4,13 +4,20 @@ use std::fmt::Display; use std::io::Write; -use std::rc::Rc; -use crate::TreeWriting; -use usvg_parser::{AId, EId}; -use usvg_tree::*; +use svgtypes::{parse_font_families, FontFamily}; use xmlwriter::XmlWriter; +use crate::parser::{AId, EId}; +use crate::*; + +impl Tree { + /// Writes `usvg::Tree` back to SVG. + pub fn to_string(&self, opt: &WriteOptions) -> String { + convert(self, opt) + } +} + /// Checks that type has a default value. trait IsDefault: Default { /// Checks that type has a default value. @@ -26,10 +33,15 @@ impl IsDefault for T { /// XML writing options. #[derive(Clone, Debug)] -pub struct XmlOptions { +pub struct WriteOptions { /// Used to add a custom prefix to each element ID during writing. pub id_prefix: Option, + /// Do not convert text into paths. + /// + /// Default: false + pub preserve_text: bool, + /// Set the coordinates numeric precision. /// /// Smaller precision can lead to a malformed output in some cases. @@ -44,78 +56,144 @@ pub struct XmlOptions { /// Default: 8 pub transforms_precision: u8, - /// `xmlwriter` options. - pub writer_opts: xmlwriter::Options, + /// Use single quote marks instead of double quote. + /// + /// # Examples + /// + /// Before: + /// + /// ```text + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// ``` + /// + /// Default: disabled + pub use_single_quote: bool, + + /// Set XML nodes indention. + /// + /// # Examples + /// + /// `Indent::None` + /// Before: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// ``` + /// + /// Default: 4 spaces + pub indent: Indent, + + /// Set XML attributes indention. + /// + /// # Examples + /// + /// `Indent::Spaces(2)` + /// + /// Before: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// After: + /// + /// ```text + /// + /// + /// + /// ``` + /// + /// Default: `None` + pub attributes_indent: Indent, } -impl Default for XmlOptions { +impl Default for WriteOptions { fn default() -> Self { Self { id_prefix: Default::default(), + preserve_text: false, coordinates_precision: 8, transforms_precision: 8, - writer_opts: Default::default(), + use_single_quote: false, + indent: Indent::Spaces(4), + attributes_indent: Indent::None, } } } -pub(crate) fn convert(tree: &Tree, opt: &XmlOptions) -> String { - let mut xml = XmlWriter::new(opt.writer_opts); +pub(crate) fn convert(tree: &Tree, opt: &WriteOptions) -> String { + let mut xml = XmlWriter::new(xmlwriter::Options { + use_single_quote: opt.use_single_quote, + indent: opt.indent, + attributes_indent: opt.attributes_indent, + }); xml.start_svg_element(EId::Svg); xml.write_svg_attribute(AId::Width, &tree.size.width()); xml.write_svg_attribute(AId::Height, &tree.size.height()); xml.write_viewbox(&tree.view_box); xml.write_attribute("xmlns", "http://www.w3.org/2000/svg"); - if has_xlink(tree) { + if has_xlink(&tree.root) { xml.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); } xml.start_svg_element(EId::Defs); - conv_defs(tree, opt, &mut xml); + write_defs(tree, opt, &mut xml); xml.end_element(); - conv_elements(&tree.root, false, opt, &mut xml); + write_elements(&tree.root, false, opt, &mut xml); xml.end_document() } -fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { - let mut filters = Vec::new(); - tree.filters(|filter| { - if !filters.iter().any(|other| Rc::ptr_eq(&filter, other)) { - filters.push(filter); - } - }); - +fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { let mut written_fe_image_nodes: Vec = Vec::new(); - for filter in filters { + for filter in tree.filters() { for fe in &filter.primitives { if let filter::Kind::Image(ref img) = fe.kind { if let filter::ImageKind::Use(ref node) = img.data { - if !written_fe_image_nodes.iter().any(|id| id == &*node.id()) { - conv_element(node, false, opt, xml); - written_fe_image_nodes.push(node.id().to_string()); + if let Some(child) = node.children.first() { + if !written_fe_image_nodes.iter().any(|id| id == child.id()) { + write_element(child, false, opt, xml); + written_fe_image_nodes.push(child.id().to_string()); + } } } } } xml.start_svg_element(EId::Filter); - xml.write_id_attribute(&filter.id, opt); + xml.write_id_attribute(filter.id(), opt); xml.write_rect_attrs(filter.rect); - xml.write_units(AId::FilterUnits, filter.units, Units::ObjectBoundingBox); xml.write_units( - AId::PrimitiveUnits, - filter.primitive_units, + AId::FilterUnits, Units::UserSpaceOnUse, + Units::ObjectBoundingBox, ); for fe in &filter.primitives { match fe.kind { filter::Kind::DropShadow(ref shadow) => { xml.start_svg_element(EId::FeDropShadow); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &shadow.input); xml.write_attribute_fmt( AId::StdDeviation.to_str(), @@ -130,7 +208,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::GaussianBlur(ref blur) => { xml.start_svg_element(EId::FeGaussianBlur); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &blur.input); xml.write_attribute_fmt( AId::StdDeviation.to_str(), @@ -141,7 +219,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Offset(ref offset) => { xml.start_svg_element(EId::FeOffset); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &offset.input); xml.write_svg_attribute(AId::Dx, &offset.dx); xml.write_svg_attribute(AId::Dy, &offset.dy); @@ -150,7 +228,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Blend(ref blend) => { xml.start_svg_element(EId::FeBlend); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &blend.input1); xml.write_filter_input(AId::In2, &blend.input2); xml.write_svg_attribute( @@ -179,7 +257,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Flood(ref flood) => { xml.start_svg_element(EId::FeFlood); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_color(AId::FloodColor, flood.color); xml.write_svg_attribute(AId::FloodOpacity, &flood.opacity.get()); xml.write_svg_attribute(AId::Result, &fe.result); @@ -187,7 +265,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Composite(ref composite) => { xml.start_svg_element(EId::FeComposite); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &composite.input1); xml.write_filter_input(AId::In2, &composite.input2); xml.write_svg_attribute( @@ -216,7 +294,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Merge(ref merge) => { xml.start_svg_element(EId::FeMerge); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_svg_attribute(AId::Result, &fe.result); for input in &merge.inputs { xml.start_svg_element(EId::FeMergeNode); @@ -228,14 +306,14 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Tile(ref tile) => { xml.start_svg_element(EId::FeTile); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &tile.input); xml.write_svg_attribute(AId::Result, &fe.result); xml.end_element(); } filter::Kind::Image(ref img) => { xml.start_svg_element(EId::FeImage); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_aspect(img.aspect); xml.write_svg_attribute( AId::ImageRendering, @@ -249,11 +327,13 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { xml.write_image_data(kind); } filter::ImageKind::Use(ref node) => { - let prefix = opt.id_prefix.as_deref().unwrap_or_default(); - xml.write_attribute_fmt( - "xlink:href", - format_args!("#{}{}", prefix, node.id()), - ); + if let Some(child) = node.children.first() { + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + xml.write_attribute_fmt( + "xlink:href", + format_args!("#{}{}", prefix, child.id()), + ); + } } } @@ -262,7 +342,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::ComponentTransfer(ref transfer) => { xml.start_svg_element(EId::FeComponentTransfer); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &transfer.input); xml.write_svg_attribute(AId::Result, &fe.result); @@ -275,7 +355,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::ColorMatrix(ref matrix) => { xml.start_svg_element(EId::FeColorMatrix); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &matrix.input); xml.write_svg_attribute(AId::Result, &fe.result); @@ -301,7 +381,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::ConvolveMatrix(ref matrix) => { xml.start_svg_element(EId::FeConvolveMatrix); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &matrix.input); xml.write_svg_attribute(AId::Result, &fe.result); @@ -335,7 +415,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Morphology(ref morphology) => { xml.start_svg_element(EId::FeMorphology); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &morphology.input); xml.write_svg_attribute(AId::Result, &fe.result); @@ -359,7 +439,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::DisplacementMap(ref map) => { xml.start_svg_element(EId::FeDisplacementMap); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &map.input1); xml.write_filter_input(AId::In2, &map.input2); xml.write_svg_attribute(AId::Result, &fe.result); @@ -384,7 +464,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::Turbulence(ref turbulence) => { xml.start_svg_element(EId::FeTurbulence); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_svg_attribute(AId::Result, &fe.result); xml.write_attribute_fmt( @@ -416,7 +496,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::DiffuseLighting(ref light) => { xml.start_svg_element(EId::FeDiffuseLighting); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_svg_attribute(AId::Result, &fe.result); xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); @@ -428,7 +508,7 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } filter::Kind::SpecularLighting(ref light) => { xml.start_svg_element(EId::FeSpecularLighting); - xml.write_filter_primitive_attrs(fe); + xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_svg_attribute(AId::Result, &fe.result); xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); @@ -446,126 +526,136 @@ fn conv_filters(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { } } -fn conv_defs(tree: &Tree, opt: &XmlOptions, xml: &mut XmlWriter) { - let mut paint_servers: Vec = Vec::new(); - tree.paint_servers(|paint| { - if !paint_servers.contains(paint) { - paint_servers.push(paint.clone()); - } - }); - - for paint in paint_servers { - match paint { - Paint::Color(_) => {} - Paint::LinearGradient(lg) => { - xml.start_svg_element(EId::LinearGradient); - xml.write_id_attribute(&lg.id, opt); - xml.write_svg_attribute(AId::X1, &lg.x1); - xml.write_svg_attribute(AId::Y1, &lg.y1); - xml.write_svg_attribute(AId::X2, &lg.x2); - xml.write_svg_attribute(AId::Y2, &lg.y2); - write_base_grad(&lg.base, xml, opt); - xml.end_element(); - } - Paint::RadialGradient(rg) => { - xml.start_svg_element(EId::RadialGradient); - xml.write_id_attribute(&rg.id, opt); - xml.write_svg_attribute(AId::Cx, &rg.cx); - xml.write_svg_attribute(AId::Cy, &rg.cy); - xml.write_svg_attribute(AId::R, &rg.r.get()); - xml.write_svg_attribute(AId::Fx, &rg.fx); - xml.write_svg_attribute(AId::Fy, &rg.fy); - write_base_grad(&rg.base, xml, opt); - xml.end_element(); - } - Paint::Pattern(pattern) => { - xml.start_svg_element(EId::Pattern); - xml.write_id_attribute(&pattern.id, opt); - xml.write_rect_attrs(pattern.rect); - xml.write_units(AId::PatternUnits, pattern.units, Units::ObjectBoundingBox); - xml.write_units( - AId::PatternContentUnits, - pattern.content_units, - Units::UserSpaceOnUse, - ); - xml.write_transform(AId::PatternTransform, pattern.transform, opt); +fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { + for lg in tree.linear_gradients() { + xml.start_svg_element(EId::LinearGradient); + xml.write_id_attribute(lg.id(), opt); + xml.write_svg_attribute(AId::X1, &lg.x1); + xml.write_svg_attribute(AId::Y1, &lg.y1); + xml.write_svg_attribute(AId::X2, &lg.x2); + xml.write_svg_attribute(AId::Y2, &lg.y2); + write_base_grad(&lg.base, opt, xml); + xml.end_element(); + } - if let Some(ref vbox) = pattern.view_box { - xml.write_viewbox(vbox); - } + for rg in tree.radial_gradients() { + xml.start_svg_element(EId::RadialGradient); + xml.write_id_attribute(rg.id(), opt); + xml.write_svg_attribute(AId::Cx, &rg.cx); + xml.write_svg_attribute(AId::Cy, &rg.cy); + xml.write_svg_attribute(AId::R, &rg.r.get()); + xml.write_svg_attribute(AId::Fx, &rg.fx); + xml.write_svg_attribute(AId::Fy, &rg.fy); + write_base_grad(&rg.base, opt, xml); + xml.end_element(); + } - conv_elements(&pattern.root, false, opt, xml); + for pattern in tree.patterns() { + xml.start_svg_element(EId::Pattern); + xml.write_id_attribute(pattern.id(), opt); + xml.write_rect_attrs(pattern.rect); + xml.write_units(AId::PatternUnits, pattern.units, Units::ObjectBoundingBox); + xml.write_units( + AId::PatternContentUnits, + pattern.content_units, + Units::UserSpaceOnUse, + ); + xml.write_transform(AId::PatternTransform, pattern.transform, opt); - xml.end_element(); - } + if let Some(ref vbox) = pattern.view_box { + xml.write_viewbox(vbox); } + + write_elements(&pattern.root, false, opt, xml); + + xml.end_element(); } - conv_filters(tree, opt, xml); + if tree.has_text_nodes() { + write_text_path_paths(&tree.root, opt, xml); + } - let mut clip_paths = Vec::new(); - tree.clip_paths(|clip| { - if !clip_paths.iter().any(|other| Rc::ptr_eq(&clip, other)) { - clip_paths.push(clip); - } - }); - for clip in clip_paths { + write_filters(tree, opt, xml); + + for clip in tree.clip_paths() { xml.start_svg_element(EId::ClipPath); - xml.write_id_attribute(&clip.id, opt); - xml.write_units(AId::ClipPathUnits, clip.units, Units::UserSpaceOnUse); + xml.write_id_attribute(clip.id(), opt); xml.write_transform(AId::Transform, clip.transform, opt); if let Some(ref clip) = clip.clip_path { - xml.write_func_iri(AId::ClipPath, &clip.id, opt); + xml.write_func_iri(AId::ClipPath, clip.id(), opt); } - conv_elements(&clip.root, true, opt, xml); + write_elements(&clip.root, true, opt, xml); xml.end_element(); } - let mut masks = Vec::new(); - tree.masks(|mask| { - if !masks.iter().any(|other| Rc::ptr_eq(&mask, other)) { - masks.push(mask); - } - }); - for mask in masks { + for mask in tree.masks() { xml.start_svg_element(EId::Mask); - xml.write_id_attribute(&mask.id, opt); + xml.write_id_attribute(mask.id(), opt); if mask.kind == MaskType::Alpha { xml.write_svg_attribute(AId::MaskType, "alpha"); } - xml.write_units(AId::MaskUnits, mask.units, Units::ObjectBoundingBox); xml.write_units( - AId::MaskContentUnits, - mask.content_units, + AId::MaskUnits, Units::UserSpaceOnUse, + Units::ObjectBoundingBox, ); xml.write_rect_attrs(mask.rect); if let Some(ref mask) = mask.mask { - xml.write_func_iri(AId::Mask, &mask.id, opt); + xml.write_func_iri(AId::Mask, mask.id(), opt); } - conv_elements(&mask.root, false, opt, xml); + write_elements(&mask.root, false, opt, xml); xml.end_element(); } } -fn conv_elements(parent: &Node, is_clip_path: bool, opt: &XmlOptions, xml: &mut XmlWriter) { - for n in parent.children() { - conv_element(&n, is_clip_path, opt, xml); +fn write_text_path_paths(parent: &Group, opt: &WriteOptions, xml: &mut XmlWriter) { + for node in &parent.children { + if let Node::Group(ref group) = node { + write_text_path_paths(group, opt, xml); + } else if let Node::Text(ref text) = node { + for chunk in &text.chunks { + if let TextFlow::Path(ref text_path) = chunk.text_flow { + let path = Path::new( + text_path.id().to_string(), + #[cfg(feature = "class")] + text_path.class().to_string(), + Visibility::default(), + None, + None, + PaintOrder::default(), + ShapeRendering::default(), + text_path.path.clone(), + Transform::default(), + ); + if let Some(ref path) = path { + write_path(path, false, Transform::default(), None, opt, xml); + } + } + } + } + + node.subroots(|subroot| write_text_path_paths(subroot, opt, xml)); + } +} + +fn write_elements(parent: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + for n in &parent.children { + write_element(n, is_clip_path, opt, xml); } } -fn conv_element(node: &Node, is_clip_path: bool, opt: &XmlOptions, xml: &mut XmlWriter) { - match *node.borrow() { - NodeKind::Path(ref p) => { - write_path(p, is_clip_path, None, opt, xml); +fn write_element(node: &Node, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + match node { + Node::Path(ref p) => { + write_path(p, is_clip_path, Transform::default(), None, opt, xml); } - NodeKind::Image(ref img) => { + Node::Image(ref img) => { xml.start_svg_element(EId::Image); if !img.id.is_empty() { xml.write_id_attribute(&img.id, opt); @@ -585,112 +675,249 @@ fn conv_element(node: &Node, is_clip_path: bool, opt: &XmlOptions, xml: &mut Xml } } - xml.write_transform(AId::Transform, img.transform, opt); xml.write_image_data(&img.kind); xml.end_element(); } - NodeKind::Group(ref g) => { - if is_clip_path { - // ClipPath with a Group element is an `usvg` special case. - // Group will contain a single Path element and we should set - // `clip-path` on it. - - if let NodeKind::Path(ref path) = *node.first_child().unwrap().borrow() { - let mut path = path.clone(); - path.transform = g.transform.pre_concat(path.transform); - - let clip_id = g.clip_path.as_ref().map(|cp| cp.id.as_str()); - write_path(&path, is_clip_path, clip_id, opt, xml); + Node::Group(ref g) => { + write_group_element(g, is_clip_path, opt, xml); + } + Node::Text(ref text) => { + if opt.preserve_text { + xml.start_svg_element(EId::Text); + + if !text.id.is_empty() { + xml.write_id_attribute(&text.id, opt); } - return; - } + xml.write_attribute("xml:space", "preserve"); - xml.start_svg_element(EId::G); - if !g.id.is_empty() { - xml.write_id_attribute(&g.id, opt); - }; + match text.writing_mode { + WritingMode::LeftToRight => {} + WritingMode::TopToBottom => xml.write_svg_attribute(AId::WritingMode, "tb"), + } - if let Some(ref clip) = g.clip_path { - xml.write_func_iri(AId::ClipPath, &clip.id, opt); - } + match text.rendering_mode { + TextRendering::OptimizeSpeed => { + xml.write_svg_attribute(AId::TextRendering, "optimizeSpeed") + } + TextRendering::GeometricPrecision => { + xml.write_svg_attribute(AId::TextRendering, "geometricPrecision") + } + TextRendering::OptimizeLegibility => {} + } - if let Some(ref mask) = g.mask { - xml.write_func_iri(AId::Mask, &mask.id, opt); - } + if text.rotate.iter().any(|r| *r != 0.0) { + xml.write_numbers(AId::Rotate, &text.rotate); + } - if !g.filters.is_empty() { - let prefix = opt.id_prefix.as_deref().unwrap_or_default(); - let ids: Vec<_> = g - .filters - .iter() - .map(|filter| format!("url(#{}{})", prefix, filter.id)) - .collect(); - xml.write_svg_attribute(AId::Filter, &ids.join(" ")); - } + if text.dx.iter().any(|dx| *dx != 0.0) { + xml.write_numbers(AId::Dx, &text.dx); + } + + if text.dy.iter().any(|dy| *dy != 0.0) { + xml.write_numbers(AId::Dy, &text.dy); + } + + xml.set_preserve_whitespaces(true); + + for chunk in &text.chunks { + if let TextFlow::Path(text_path) = &chunk.text_flow { + xml.start_svg_element(EId::TextPath); + + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + xml.write_attribute_fmt( + "xlink:href", + format_args!("#{}{}", prefix, text_path.id()), + ); + + if text_path.start_offset != 0.0 { + xml.write_svg_attribute(AId::StartOffset, &text_path.start_offset); + } + } - if g.opacity != Opacity::ONE { - xml.write_svg_attribute(AId::Opacity, &g.opacity.get()); + xml.start_svg_element(EId::Tspan); + + if let Some(x) = chunk.x { + xml.write_svg_attribute(AId::X, &x); + } + + if let Some(y) = chunk.y { + xml.write_svg_attribute(AId::Y, &y); + } + + match chunk.anchor { + TextAnchor::Start => {} + TextAnchor::Middle => xml.write_svg_attribute(AId::TextAnchor, "middle"), + TextAnchor::End => xml.write_svg_attribute(AId::TextAnchor, "end"), + } + + for span in &chunk.spans { + let decorations: Vec<_> = [ + ("underline", &span.decoration.underline), + ("line-through", &span.decoration.line_through), + ("overline", &span.decoration.overline), + ] + .iter() + .filter_map(|&(key, option_value)| { + option_value.as_ref().map(|value| (key, value)) + }) + .collect(); + + // Decorations need to be dumped BEFORE we write the actual span data + // (so that for example stroke color of span doesn't affect the text + // itself while baseline shifts need to be written after (since they are + // affected by the font size) + for (deco_name, deco) in &decorations { + xml.start_svg_element(EId::Tspan); + xml.write_svg_attribute(AId::TextDecoration, deco_name); + write_fill(&deco.fill, false, opt, xml); + write_stroke(&deco.stroke, opt, xml); + } + + write_span(is_clip_path, opt, xml, chunk, span); + + // End for each tspan we needed to create for decorations + for _ in &decorations { + xml.end_element(); + } + } + xml.end_element(); + + // End textPath element + if matches!(&chunk.text_flow, TextFlow::Path(_)) { + xml.end_element(); + } + } + + xml.end_element(); + xml.set_preserve_whitespaces(false); + } else { + write_group_element(text.flattened(), is_clip_path, opt, xml); } + } + } +} - xml.write_transform(AId::Transform, g.transform, opt); - - if g.blend_mode != BlendMode::Normal || g.isolate { - let blend_mode = match g.blend_mode { - BlendMode::Normal => "normal", - BlendMode::Multiply => "multiply", - BlendMode::Screen => "screen", - BlendMode::Overlay => "overlay", - BlendMode::Darken => "darken", - BlendMode::Lighten => "lighten", - BlendMode::ColorDodge => "color-dodge", - BlendMode::ColorBurn => "color-burn", - BlendMode::HardLight => "hard-light", - BlendMode::SoftLight => "soft-light", - BlendMode::Difference => "difference", - BlendMode::Exclusion => "exclusion", - BlendMode::Hue => "hue", - BlendMode::Saturation => "saturation", - BlendMode::Color => "color", - BlendMode::Luminosity => "luminosity", - }; - - // For reasons unknown, `mix-blend-mode` and `isolation` must be written - // as `style` attribute. - let isolation = if g.isolate { "isolate" } else { "auto" }; - xml.write_attribute_fmt( - AId::Style.to_str(), - format_args!("mix-blend-mode:{};isolation:{}", blend_mode, isolation), +fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { + if is_clip_path { + // The `clipPath` element in SVG doesn't allow groups, only shapes and text. + // The problem is that in `usvg` we can set a `clip-path` only on groups. + // So in cases when a `clipPath` child has a `clip-path` as well, + // it would be inside a group. And we have to skip this group during writing. + // + // Basically, the following SVG: + // + // + // + // + // + // will be represented in usvg as: + // + // + // + // + // + // + // + // + // Same with text. Text elements will be converted into groups, + // but only the group's children should be written. + for child in &g.children { + if let Node::Path(ref path) = child { + let clip_id = g.clip_path.as_ref().map(|cp| cp.id().to_string()); + write_path( + path, + is_clip_path, + g.transform, + clip_id.as_deref(), + opt, + xml, ); } + } + return; + } - conv_elements(node, false, opt, xml); + xml.start_svg_element(EId::G); + if !g.id.is_empty() { + xml.write_id_attribute(&g.id, opt); + }; - xml.end_element(); - } - NodeKind::Text(_) => { - log::warn!("Text must be converted into paths."); - } + if let Some(ref clip) = g.clip_path { + xml.write_func_iri(AId::ClipPath, clip.id(), opt); + } + + if let Some(ref mask) = g.mask { + xml.write_func_iri(AId::Mask, mask.id(), opt); } + + if !g.filters.is_empty() { + let prefix = opt.id_prefix.as_deref().unwrap_or_default(); + let ids: Vec<_> = g + .filters + .iter() + .map(|filter| format!("url(#{}{})", prefix, filter.id())) + .collect(); + xml.write_svg_attribute(AId::Filter, &ids.join(" ")); + } + + if g.opacity != Opacity::ONE { + xml.write_svg_attribute(AId::Opacity, &g.opacity.get()); + } + + xml.write_transform(AId::Transform, g.transform, opt); + + if g.blend_mode != BlendMode::Normal || g.isolate { + let blend_mode = match g.blend_mode { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + }; + + // For reasons unknown, `mix-blend-mode` and `isolation` must be written + // as `style` attribute. + let isolation = if g.isolate { "isolate" } else { "auto" }; + xml.write_attribute_fmt( + AId::Style.to_str(), + format_args!("mix-blend-mode:{};isolation:{}", blend_mode, isolation), + ); + } + + write_elements(g, false, opt, xml); + + xml.end_element(); } trait XmlWriterExt { fn start_svg_element(&mut self, id: EId); fn write_svg_attribute(&mut self, id: AId, value: &V); - fn write_id_attribute(&mut self, value: &str, opt: &XmlOptions); + fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions); fn write_color(&mut self, id: AId, color: Color); fn write_viewbox(&mut self, view_box: &ViewBox); fn write_aspect(&mut self, aspect: AspectRatio); fn write_units(&mut self, id: AId, units: Units, def: Units); - fn write_transform(&mut self, id: AId, units: Transform, opt: &XmlOptions); + fn write_transform(&mut self, id: AId, units: Transform, opt: &WriteOptions); fn write_visibility(&mut self, value: Visibility); - fn write_func_iri(&mut self, aid: AId, id: &str, opt: &XmlOptions); + fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions); fn write_rect_attrs(&mut self, r: NonZeroRect); fn write_numbers(&mut self, aid: AId, list: &[f32]); fn write_image_data(&mut self, kind: &ImageKind); fn write_filter_input(&mut self, id: AId, input: &filter::Input); - fn write_filter_primitive_attrs(&mut self, fe: &filter::Primitive); + fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive); fn write_filter_transfer_function(&mut self, eid: EId, fe: &filter::TransferFunction); } @@ -706,12 +933,14 @@ impl XmlWriterExt for XmlWriter { } #[inline(never)] - fn write_id_attribute(&mut self, value: &str, opt: &XmlOptions) { - debug_assert!(!value.is_empty()); + fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions) { + debug_assert!(!id.is_empty()); + if let Some(ref prefix) = opt.id_prefix { - self.write_attribute_fmt("id", format_args!("{}{}", prefix, value)); + let full_id = format!("{}{}", prefix, id); + self.write_attribute("id", &full_id); } else { - self.write_attribute("id", value); + self.write_attribute("id", id); } } @@ -776,6 +1005,7 @@ impl XmlWriterExt for XmlWriter { }); } + // TODO: simplify fn write_units(&mut self, id: AId, units: Units, def: Units) { if units != def { self.write_attribute( @@ -788,7 +1018,7 @@ impl XmlWriterExt for XmlWriter { } } - fn write_transform(&mut self, id: AId, ts: Transform, opt: &XmlOptions) { + fn write_transform(&mut self, id: AId, ts: Transform, opt: &WriteOptions) { if !ts.is_default() { self.write_attribute_raw(id.to_str(), |buf| { buf.extend_from_slice(b"matrix("); @@ -816,7 +1046,8 @@ impl XmlWriterExt for XmlWriter { } } - fn write_func_iri(&mut self, aid: AId, id: &str, opt: &XmlOptions) { + fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions) { + debug_assert!(!id.is_empty()); let prefix = opt.id_prefix.as_deref().unwrap_or_default(); self.write_attribute_fmt(aid.to_str(), format_args!("url(#{}{})", prefix, id)); } @@ -851,18 +1082,18 @@ impl XmlWriterExt for XmlWriter { ); } - fn write_filter_primitive_attrs(&mut self, fe: &filter::Primitive) { - if let Some(n) = fe.x { - self.write_svg_attribute(AId::X, &n); + fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive) { + if parent_rect.x() != fe.rect().x() { + self.write_svg_attribute(AId::X, &fe.rect().x()); } - if let Some(n) = fe.y { - self.write_svg_attribute(AId::Y, &n); + if parent_rect.y() != fe.rect().y() { + self.write_svg_attribute(AId::Y, &fe.rect().y()); } - if let Some(n) = fe.width { - self.write_svg_attribute(AId::Width, &n); + if parent_rect.width() != fe.rect().width() { + self.write_svg_attribute(AId::Width, &fe.rect().width()); } - if let Some(n) = fe.height { - self.write_svg_attribute(AId::Height, &n); + if parent_rect.height() != fe.rect().height() { + self.write_svg_attribute(AId::Height, &fe.rect().height()); } self.write_attribute( @@ -909,14 +1140,14 @@ impl XmlWriterExt for XmlWriter { self.end_element(); } - fn write_image_data(&mut self, kind: &usvg_tree::ImageKind) { + fn write_image_data(&mut self, kind: &ImageKind) { let svg_string; let (mime, data) = match kind { - usvg_tree::ImageKind::JPEG(ref data) => ("jpeg", data.as_slice()), - usvg_tree::ImageKind::PNG(ref data) => ("png", data.as_slice()), - usvg_tree::ImageKind::GIF(ref data) => ("gif", data.as_slice()), - usvg_tree::ImageKind::SVG(ref tree) => { - svg_string = tree.to_string(&XmlOptions::default()); + ImageKind::JPEG(ref data) => ("jpeg", data.as_slice()), + ImageKind::PNG(ref data) => ("png", data.as_slice()), + ImageKind::GIF(ref data) => ("gif", data.as_slice()), + ImageKind::SVG(ref tree) => { + svg_string = tree.to_string(&WriteOptions::default()); ("svg+xml", svg_string.as_bytes()) } }; @@ -934,10 +1165,10 @@ impl XmlWriterExt for XmlWriter { } } -fn has_xlink(tree: &Tree) -> bool { - for n in tree.root.descendants() { - match *n.borrow() { - NodeKind::Group(ref g) => { +fn has_xlink(parent: &Group) -> bool { + for node in &parent.children { + match node { + Node::Group(ref g) => { for filter in &g.filters { if filter .primitives @@ -947,18 +1178,49 @@ fn has_xlink(tree: &Tree) -> bool { return true; } } + + if let Some(ref mask) = g.mask { + if has_xlink(mask.root()) { + return true; + } + + if let Some(ref sub_mask) = mask.mask { + if has_xlink(&sub_mask.root) { + return true; + } + } + } + + if has_xlink(g) { + return true; + } } - NodeKind::Image(_) => { + Node::Image(_) => { return true; } + Node::Text(ref text) => { + if text + .chunks + .iter() + .any(|t| matches!(t.text_flow, TextFlow::Path(_))) + { + return true; + } + } _ => {} } + + let mut present = false; + node.subroots(|root| present |= has_xlink(root)); + if present { + return true; + } } false } -fn write_base_grad(g: &BaseGradient, xml: &mut XmlWriter, opt: &XmlOptions) { +fn write_base_grad(g: &BaseGradient, opt: &WriteOptions, xml: &mut XmlWriter) { xml.write_units(AId::GradientUnits, g.units, Units::ObjectBoundingBox); xml.write_transform(AId::GradientTransform, g.transform, opt); @@ -983,8 +1245,9 @@ fn write_base_grad(g: &BaseGradient, xml: &mut XmlWriter, opt: &XmlOptions) { fn write_path( path: &Path, is_clip_path: bool, + path_transform: Transform, clip_path: Option<&str>, - opt: &XmlOptions, + opt: &WriteOptions, xml: &mut XmlWriter, ) { xml.start_svg_element(EId::Path); @@ -1013,7 +1276,7 @@ fn write_path( xml.write_func_iri(AId::ClipPath, id, opt); } - xml.write_transform(AId::Transform, path.transform, opt); + xml.write_transform(AId::Transform, path_transform, opt); xml.write_attribute_raw("d", |buf| { use tiny_skia_path::PathSegment; @@ -1072,7 +1335,7 @@ fn write_path( xml.end_element(); } -fn write_fill(fill: &Option, is_clip_path: bool, opt: &XmlOptions, xml: &mut XmlWriter) { +fn write_fill(fill: &Option, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { if let Some(ref fill) = fill { write_paint(AId::Fill, &fill.paint, opt, xml); @@ -1094,7 +1357,7 @@ fn write_fill(fill: &Option, is_clip_path: bool, opt: &XmlOptions, xml: &m } } -fn write_stroke(stroke: &Option, opt: &XmlOptions, xml: &mut XmlWriter) { +fn write_stroke(stroke: &Option, opt: &WriteOptions, xml: &mut XmlWriter) { if let Some(ref stroke) = stroke { write_paint(AId::Stroke, &stroke.paint, opt, xml); @@ -1138,12 +1401,18 @@ fn write_stroke(stroke: &Option, opt: &XmlOptions, xml: &mut XmlWriter) } } -fn write_paint(aid: AId, paint: &Paint, opt: &XmlOptions, xml: &mut XmlWriter) { +fn write_paint(aid: AId, paint: &Paint, opt: &WriteOptions, xml: &mut XmlWriter) { match paint { Paint::Color(c) => xml.write_color(aid, *c), - Paint::LinearGradient(ref lg) => xml.write_func_iri(aid, &lg.id, opt), - Paint::RadialGradient(ref rg) => xml.write_func_iri(aid, &rg.id, opt), - Paint::Pattern(ref patt) => xml.write_func_iri(aid, &patt.id, opt), + Paint::LinearGradient(ref lg) => { + xml.write_func_iri(aid, lg.id(), opt); + } + Paint::RadialGradient(ref rg) => { + xml.write_func_iri(aid, rg.id(), opt); + } + Paint::Pattern(ref patt) => { + xml.write_func_iri(aid, patt.id(), opt); + } } } @@ -1212,3 +1481,167 @@ fn write_num(num: f32, buf: &mut Vec, precision: u8) { write!(buf, "{}", v).unwrap(); } + +/// Write all of the tspan attributes except for decorations. +fn write_span( + is_clip_path: bool, + opt: &WriteOptions, + xml: &mut XmlWriter, + chunk: &TextChunk, + span: &TextSpan, +) { + xml.start_svg_element(EId::Tspan); + + let font_family_to_str = |font_family: &FontFamily| match font_family { + FontFamily::Monospace => "monospace".to_string(), + FontFamily::Serif => "serif".to_string(), + FontFamily::SansSerif => "sans-serif".to_string(), + FontFamily::Cursive => "cursive".to_string(), + FontFamily::Fantasy => "fantasy".to_string(), + FontFamily::Named(s) => { + // Only quote if absolutely necessary + match parse_font_families(s) { + Ok(_) => s.clone(), + Err(_) => { + if opt.use_single_quote { + format!("\"{}\"", s) + } else { + format!("'{}'", s) + } + } + } + } + }; + + if !span.font.families.is_empty() { + let families = span + .font + .families + .iter() + .map(font_family_to_str) + .collect::>() + .join(", "); + xml.write_svg_attribute(AId::FontFamily, &families); + } + + match span.font.style { + FontStyle::Normal => {} + FontStyle::Italic => xml.write_svg_attribute(AId::FontStyle, "italic"), + FontStyle::Oblique => xml.write_svg_attribute(AId::FontStyle, "oblique"), + } + + if span.font.weight != 400 { + xml.write_svg_attribute(AId::FontWeight, &span.font.weight); + } + + if span.font.stretch != FontStretch::Normal { + let name = match span.font.stretch { + FontStretch::Condensed => "condensed", + FontStretch::ExtraCondensed => "extra-condensed", + FontStretch::UltraCondensed => "ultra-condensed", + FontStretch::SemiCondensed => "semi-condensed", + FontStretch::Expanded => "expanded", + FontStretch::SemiExpanded => "semi-expanded", + FontStretch::ExtraExpanded => "extra-expanded", + FontStretch::UltraExpanded => "ultra-expanded", + FontStretch::Normal => unreachable!(), + }; + xml.write_svg_attribute(AId::FontStretch, name); + } + + xml.write_svg_attribute(AId::FontSize, &span.font_size); + + match span.visibility { + Visibility::Visible => {} + Visibility::Hidden => xml.write_svg_attribute(AId::Visibility, "hidden"), + Visibility::Collapse => xml.write_svg_attribute(AId::Visibility, "collapse"), + } + + if span.letter_spacing != 0.0 { + xml.write_svg_attribute(AId::LetterSpacing, &span.letter_spacing); + } + + if span.word_spacing != 0.0 { + xml.write_svg_attribute(AId::WordSpacing, &span.word_spacing); + } + + if let Some(text_length) = span.text_length { + xml.write_svg_attribute(AId::TextLength, &text_length); + } + + if span.length_adjust == LengthAdjust::SpacingAndGlyphs { + xml.write_svg_attribute(AId::LengthAdjust, "spacingAndGlyphs"); + } + + if span.small_caps { + xml.write_svg_attribute(AId::FontVariant, "small-caps"); + } + + if span.paint_order == PaintOrder::StrokeAndFill { + xml.write_svg_attribute(AId::PaintOrder, "stroke fill"); + } + + if !span.apply_kerning { + xml.write_attribute("style", "font-kerning:none") + } + + if span.dominant_baseline != DominantBaseline::Auto { + let name = match span.dominant_baseline { + DominantBaseline::UseScript => "use-script", + DominantBaseline::NoChange => "no-change", + DominantBaseline::ResetSize => "reset-size", + DominantBaseline::TextBeforeEdge => "text-before-edge", + DominantBaseline::Middle => "middle", + DominantBaseline::Central => "central", + DominantBaseline::TextAfterEdge => "text-after-edge", + DominantBaseline::Ideographic => "ideographic", + DominantBaseline::Alphabetic => "alphabetic", + DominantBaseline::Hanging => "hanging", + DominantBaseline::Mathematical => "mathematical", + DominantBaseline::Auto => unreachable!(), + }; + xml.write_svg_attribute(AId::DominantBaseline, name); + } + + if span.alignment_baseline != AlignmentBaseline::Auto { + let name = match span.alignment_baseline { + AlignmentBaseline::Baseline => "baseline", + AlignmentBaseline::BeforeEdge => "before-edge", + AlignmentBaseline::TextBeforeEdge => "text-before-edge", + AlignmentBaseline::Middle => "middle", + AlignmentBaseline::Central => "central", + AlignmentBaseline::AfterEdge => "after-edge", + AlignmentBaseline::TextAfterEdge => "text-after-edge", + AlignmentBaseline::Ideographic => "ideographic", + AlignmentBaseline::Alphabetic => "alphabetic", + AlignmentBaseline::Hanging => "hanging", + AlignmentBaseline::Mathematical => "mathematical", + AlignmentBaseline::Auto => unreachable!(), + }; + xml.write_svg_attribute(AId::AlignmentBaseline, name); + } + + write_fill(&span.fill, is_clip_path, opt, xml); + write_stroke(&span.stroke, opt, xml); + + for baseline_shift in &span.baseline_shift { + xml.start_svg_element(EId::Tspan); + match baseline_shift { + BaselineShift::Baseline => {} + BaselineShift::Number(num) => xml.write_svg_attribute(AId::BaselineShift, num), + BaselineShift::Subscript => xml.write_svg_attribute(AId::BaselineShift, "sub"), + BaselineShift::Superscript => xml.write_svg_attribute(AId::BaselineShift, "super"), + } + } + + let cur_text = &chunk.text[span.start..span.end]; + + xml.write_text(&cur_text.replace('&', "&")); + + // End for each tspan we needed to create for baseline_shift + for _ in &span.baseline_shift { + xml.end_element(); + } + + xml.end_element(); +} diff --git a/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg b/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg new file mode 100644 index 000000000..0fcdfac74 --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-complex-text-expected.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/crates/usvg/tests/files/clip-path-with-complex-text.svg b/crates/usvg/tests/files/clip-path-with-complex-text.svg new file mode 100644 index 000000000..4fd28a1c1 --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-complex-text.svg @@ -0,0 +1,13 @@ + + + + + + + + + Text + + + diff --git a/crates/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg b/crates/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg new file mode 100644 index 000000000..f8956955d --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-object-units-multi-use-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/clip-path-with-object-units-multi-use.svg b/crates/usvg/tests/files/clip-path-with-object-units-multi-use.svg new file mode 100644 index 000000000..ddc3c339f --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/crates/usvg/tests/files/clip-path-with-text-expected.svg b/crates/usvg/tests/files/clip-path-with-text-expected.svg new file mode 100644 index 000000000..f419567ac --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-text-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/crates/usvg/tests/files/clip-path-with-text.svg b/crates/usvg/tests/files/clip-path-with-text.svg new file mode 100644 index 000000000..71f2348a2 --- /dev/null +++ b/crates/usvg/tests/files/clip-path-with-text.svg @@ -0,0 +1,9 @@ + + + + + + Text + + + diff --git a/crates/usvg/tests/files/ellipse-simple-case-expected.svg b/crates/usvg/tests/files/ellipse-simple-case-expected.svg new file mode 100644 index 000000000..32c701cba --- /dev/null +++ b/crates/usvg/tests/files/ellipse-simple-case-expected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/usvg/tests/files/ellipse-simple-case.svg b/crates/usvg/tests/files/ellipse-simple-case.svg new file mode 100644 index 000000000..81c3492c9 --- /dev/null +++ b/crates/usvg/tests/files/ellipse-simple-case.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/usvg/tests/files/filter-id-with-prefix-expected.svg b/crates/usvg/tests/files/filter-id-with-prefix-expected.svg new file mode 100644 index 000000000..abda5908c --- /dev/null +++ b/crates/usvg/tests/files/filter-id-with-prefix-expected.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/filter-id-with-prefix.svg b/crates/usvg/tests/files/filter-id-with-prefix.svg new file mode 100644 index 000000000..252a3a878 --- /dev/null +++ b/crates/usvg/tests/files/filter-id-with-prefix.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/crates/usvg/tests/files/filter-with-object-units-multi-use-expected.svg b/crates/usvg/tests/files/filter-with-object-units-multi-use-expected.svg new file mode 100644 index 000000000..4861eb5bd --- /dev/null +++ b/crates/usvg/tests/files/filter-with-object-units-multi-use-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/filter-with-object-units-multi-use.svg b/crates/usvg/tests/files/filter-with-object-units-multi-use.svg new file mode 100644 index 000000000..194787947 --- /dev/null +++ b/crates/usvg/tests/files/filter-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg b/crates/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg new file mode 100644 index 000000000..8d0b7bb9c --- /dev/null +++ b/crates/usvg/tests/files/generate-id-clip-path-for-symbol-expected.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-clip-path-for-symbol.svg b/crates/usvg/tests/files/generate-id-clip-path-for-symbol.svg new file mode 100644 index 000000000..4386d6234 --- /dev/null +++ b/crates/usvg/tests/files/generate-id-clip-path-for-symbol.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-filter-function-v1-expected.svg b/crates/usvg/tests/files/generate-id-filter-function-v1-expected.svg new file mode 100644 index 000000000..190416f9e --- /dev/null +++ b/crates/usvg/tests/files/generate-id-filter-function-v1-expected.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-filter-function-v1.svg b/crates/usvg/tests/files/generate-id-filter-function-v1.svg new file mode 100644 index 000000000..74d93d636 --- /dev/null +++ b/crates/usvg/tests/files/generate-id-filter-function-v1.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-filter-function-v2-expected.svg b/crates/usvg/tests/files/generate-id-filter-function-v2-expected.svg new file mode 100644 index 000000000..74e44db27 --- /dev/null +++ b/crates/usvg/tests/files/generate-id-filter-function-v2-expected.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/generate-id-filter-function-v2.svg b/crates/usvg/tests/files/generate-id-filter-function-v2.svg new file mode 100644 index 000000000..92cc0e104 --- /dev/null +++ b/crates/usvg/tests/files/generate-id-filter-function-v2.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/crates/usvg/tests/files/mask-with-object-units-multi-use-expected.svg b/crates/usvg/tests/files/mask-with-object-units-multi-use-expected.svg new file mode 100644 index 000000000..ca4ca3510 --- /dev/null +++ b/crates/usvg/tests/files/mask-with-object-units-multi-use-expected.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/mask-with-object-units-multi-use.svg b/crates/usvg/tests/files/mask-with-object-units-multi-use.svg new file mode 100644 index 000000000..4d5478ea2 --- /dev/null +++ b/crates/usvg/tests/files/mask-with-object-units-multi-use.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/crates/usvg/tests/files/path-simple-case-expected.svg b/crates/usvg/tests/files/path-simple-case-expected.svg new file mode 100644 index 000000000..76256d313 --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case-expected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/crates/usvg/tests/files/path-simple-case.svg b/crates/usvg/tests/files/path-simple-case.svg new file mode 100644 index 000000000..bbac55060 --- /dev/null +++ b/crates/usvg/tests/files/path-simple-case.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/usvg/tests/files/preserve-id-clip-path-v1-expected.svg b/crates/usvg/tests/files/preserve-id-clip-path-v1-expected.svg new file mode 100644 index 000000000..961a1cdbf --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-clip-path-v1-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-clip-path-v1.svg b/crates/usvg/tests/files/preserve-id-clip-path-v1.svg new file mode 100644 index 000000000..3cba0ca35 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-clip-path-v1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-clip-path-v2-expected.svg b/crates/usvg/tests/files/preserve-id-clip-path-v2-expected.svg new file mode 100644 index 000000000..d7bf2e65f --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-clip-path-v2-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-clip-path-v2.svg b/crates/usvg/tests/files/preserve-id-clip-path-v2.svg new file mode 100644 index 000000000..d629ba90d --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-clip-path-v2.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-fe-image-expected.svg b/crates/usvg/tests/files/preserve-id-fe-image-expected.svg new file mode 100644 index 000000000..4ea2ba6b1 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-fe-image-expected.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg b/crates/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg new file mode 100644 index 000000000..7283109ca --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-fe-image-with-opacity-expected.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-fe-image-with-opacity.svg b/crates/usvg/tests/files/preserve-id-fe-image-with-opacity.svg new file mode 100644 index 000000000..24be35e08 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-fe-image-with-opacity.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-fe-image.svg b/crates/usvg/tests/files/preserve-id-fe-image.svg new file mode 100644 index 000000000..c1d51b558 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-fe-image.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-filter-expected.svg b/crates/usvg/tests/files/preserve-id-filter-expected.svg new file mode 100644 index 000000000..732775030 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-filter-expected.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-filter.svg b/crates/usvg/tests/files/preserve-id-filter.svg new file mode 100644 index 000000000..a4b2830ef --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-filter.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg b/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg new file mode 100644 index 000000000..9ecdd0365 --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg b/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg new file mode 100644 index 000000000..2f4a7362a --- /dev/null +++ b/crates/usvg/tests/files/preserve-id-for-clip-path-in-pattern.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-clip-path-expected.svg b/crates/usvg/tests/files/preserve-text-in-clip-path-expected.svg new file mode 100644 index 000000000..5b0335b8e --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-clip-path-expected.svg @@ -0,0 +1,11 @@ + + + + + abcdefghijklmnopqrstuvwxyz + + + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-clip-path.svg b/crates/usvg/tests/files/preserve-text-in-clip-path.svg new file mode 100644 index 000000000..ef7ee7a33 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-clip-path.svg @@ -0,0 +1,14 @@ + + + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-mask-expected.svg b/crates/usvg/tests/files/preserve-text-in-mask-expected.svg new file mode 100644 index 000000000..817bda887 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-mask-expected.svg @@ -0,0 +1,12 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-mask.svg b/crates/usvg/tests/files/preserve-text-in-mask.svg new file mode 100644 index 000000000..33f6f31ab --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-mask.svg @@ -0,0 +1,12 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-pattern-expected.svg b/crates/usvg/tests/files/preserve-text-in-pattern-expected.svg new file mode 100644 index 000000000..93478a3d7 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-pattern-expected.svg @@ -0,0 +1,8 @@ + + + + H + + + + diff --git a/crates/usvg/tests/files/preserve-text-in-pattern.svg b/crates/usvg/tests/files/preserve-text-in-pattern.svg new file mode 100644 index 000000000..54c0b2fa0 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-in-pattern.svg @@ -0,0 +1,7 @@ + + + H + + + diff --git a/crates/usvg/tests/files/preserve-text-multiple-font-families-expected.svg b/crates/usvg/tests/files/preserve-text-multiple-font-families-expected.svg new file mode 100644 index 000000000..c1e0c6c87 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-multiple-font-families-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/preserve-text-multiple-font-families.svg b/crates/usvg/tests/files/preserve-text-multiple-font-families.svg new file mode 100644 index 000000000..71fe961df --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-multiple-font-families.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/preserve-text-on-path-expected.svg b/crates/usvg/tests/files/preserve-text-on-path-expected.svg new file mode 100644 index 000000000..8baa03109 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-on-path-expected.svg @@ -0,0 +1,7 @@ + + + + + + abcdefghijklmnopqrstuvwxyz + diff --git a/crates/usvg/tests/files/preserve-text-on-path.svg b/crates/usvg/tests/files/preserve-text-on-path.svg new file mode 100644 index 000000000..5d4392ee5 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-on-path.svg @@ -0,0 +1,15 @@ + + + + + + + + + + abcdefghijklmnopqrstuvwxyz + + + diff --git a/crates/usvg/tests/files/preserve-text-simple-case-expected.svg b/crates/usvg/tests/files/preserve-text-simple-case-expected.svg new file mode 100644 index 000000000..7a7db01ae --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-simple-case-expected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/usvg/tests/files/preserve-text-simple-case.svg b/crates/usvg/tests/files/preserve-text-simple-case.svg new file mode 100644 index 000000000..573bf4224 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-simple-case.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg b/crates/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg new file mode 100644 index 000000000..cf072a844 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-complex-text-decoration-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/preserve-text-with-complex-text-decoration.svg b/crates/usvg/tests/files/preserve-text-with-complex-text-decoration.svg new file mode 100644 index 000000000..d1c8111fd --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-complex-text-decoration.svg @@ -0,0 +1,12 @@ + + + + + + + Text + + + + diff --git a/crates/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg b/crates/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg new file mode 100644 index 000000000..e67e8ae42 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-dx-and-dy-expected.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/preserve-text-with-dx-and-dy.svg b/crates/usvg/tests/files/preserve-text-with-dx-and-dy.svg new file mode 100644 index 000000000..4ce13d3e9 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-dx-and-dy.svg @@ -0,0 +1,6 @@ + + + Text + + diff --git a/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg b/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg new file mode 100644 index 000000000..9b0c8fe2c --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift-expected.svg @@ -0,0 +1,4 @@ + + + A B C D E F + diff --git a/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg b/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg new file mode 100644 index 000000000..a8b5aefc8 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-nested-baseline-shift.svg @@ -0,0 +1,25 @@ + + + + + + + A + + B + + C + + D + + E + + F + + + + + + + diff --git a/crates/usvg/tests/files/preserve-text-with-rotate-expected.svg b/crates/usvg/tests/files/preserve-text-with-rotate-expected.svg new file mode 100644 index 000000000..1239df9e1 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-rotate-expected.svg @@ -0,0 +1,4 @@ + + + Some long Text + diff --git a/crates/usvg/tests/files/preserve-text-with-rotate.svg b/crates/usvg/tests/files/preserve-text-with-rotate.svg new file mode 100644 index 000000000..11eaa4940 --- /dev/null +++ b/crates/usvg/tests/files/preserve-text-with-rotate.svg @@ -0,0 +1,7 @@ + + + Some long + Text + + diff --git a/crates/usvg/tests/files/text-simple-case-expected.svg b/crates/usvg/tests/files/text-simple-case-expected.svg new file mode 100644 index 000000000..7a7db01ae --- /dev/null +++ b/crates/usvg/tests/files/text-simple-case-expected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/usvg/tests/files/text-simple-case.svg b/crates/usvg/tests/files/text-simple-case.svg new file mode 100644 index 000000000..84dd24969 --- /dev/null +++ b/crates/usvg/tests/files/text-simple-case.svg @@ -0,0 +1,4 @@ + + + Text + diff --git a/crates/usvg/tests/files/text-with-generated-gradients-expected.svg b/crates/usvg/tests/files/text-with-generated-gradients-expected.svg new file mode 100644 index 000000000..0a50b9e0a --- /dev/null +++ b/crates/usvg/tests/files/text-with-generated-gradients-expected.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/crates/usvg/tests/files/text-with-generated-gradients.svg b/crates/usvg/tests/files/text-with-generated-gradients.svg new file mode 100644 index 000000000..9fc59a83e --- /dev/null +++ b/crates/usvg/tests/files/text-with-generated-gradients.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + Hello مرحبا. + + diff --git a/crates/usvg/tests/parser.rs b/crates/usvg/tests/parser.rs new file mode 100644 index 000000000..761abe237 --- /dev/null +++ b/crates/usvg/tests/parser.rs @@ -0,0 +1,331 @@ +#[test] +fn clippath_with_invalid_child() { + let svg = " + + + + + + + "; + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + // clipPath is invalid and should be removed together with rect. + assert_eq!(tree.root().has_children(), false); +} + +#[test] +fn simplify_paths() { + let svg = " + + + + "; + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let path = &tree.root().children()[0]; + match path { + usvg::Node::Path(ref path) => { + // Make sure we have MLZ and not MLZZZ + assert_eq!(path.data().verbs().len(), 3); + } + _ => unreachable!(), + }; +} + +#[test] +fn size_detection_1() { + let svg = ""; + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(10.0, 20.0).unwrap()); +} + +#[test] +fn size_detection_2() { + let svg = + ""; + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(30.0, 40.0).unwrap()); +} + +#[test] +fn size_detection_3() { + let svg = + ""; + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(5.0, 20.0).unwrap()); +} + +#[test] +fn size_detection_4() { + let svg = " + + + + "; + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap()); + assert_eq!( + tree.view_box().rect, + usvg::NonZeroRect::from_xywh(0.0, 0.0, 36.0, 36.0).unwrap() + ); +} + +#[test] +fn size_detection_5() { + let svg = ""; + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.size(), usvg::Size::from_wh(100.0, 100.0).unwrap()); +} + +#[test] +fn invalid_size_1() { + let svg = ""; + let fontdb = usvg::fontdb::Database::new(); + let result = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb); + assert!(result.is_err()); +} + +#[test] +fn tree_is_send_and_sync() { + fn ensure_send_and_sync() {} + ensure_send_and_sync::(); +} + +#[test] +fn path_transform() { + let svg = " + + + + "; + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.root().children().len(), 1); + + let group_node = &tree.root().children()[0]; + assert!(matches!(group_node, usvg::Node::Group(_))); + assert_eq!(group_node.abs_transform(), usvg::Transform::from_translate(10.0, 0.0)); + + let group = match group_node { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!(path.abs_transform(), usvg::Transform::from_translate(10.0, 0.0)); +} + +#[test] +fn path_transform_nested() { + let svg = " + + + + + + "; + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + assert_eq!(tree.root().children().len(), 1); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.abs_transform(), usvg::Transform::from_translate(20.0, 0.0)); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.abs_transform(), usvg::Transform::from_translate(30.0, 0.0)); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!(path.abs_transform(), usvg::Transform::from_translate(30.0, 0.0)); +} + +#[test] +fn path_transform_in_symbol_no_clip() { + let svg = " + + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "use1"); + assert_eq!(group_node1.abs_transform(), usvg::Transform::default()); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.abs_transform(), usvg::Transform::from_translate(20.0, 0.0)); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!(path.abs_transform(), usvg::Transform::from_translate(20.0, 0.0)); +} + +#[test] +fn path_transform_in_symbol_with_clip() { + let svg = " + + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + // + // + // + // + // + // + // + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "use1"); + assert_eq!(group_node1.abs_transform(), usvg::Transform::default()); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.abs_transform(), usvg::Transform::default()); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node3 = &group2.children()[0]; + assert!(matches!(group_node3, usvg::Node::Group(_))); + assert_eq!(group_node3.abs_transform(), usvg::Transform::from_translate(20.0, 0.0)); + + let group3 = match group_node3 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group3.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!(path.abs_transform(), usvg::Transform::from_translate(20.0, 0.0)); +} + +#[test] +fn path_transform_in_svg() { + let svg = " + + + + + + + + "; + + // Will be parsed as: + // + // + // + // + // + // + // + // + // + // + // + // + + let fontdb = usvg::fontdb::Database::new(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + + let group_node1 = &tree.root().children()[0]; + assert!(matches!(group_node1, usvg::Node::Group(_))); + assert_eq!(group_node1.id(), "g1"); + assert_eq!(group_node1.abs_transform(), usvg::Transform::from_translate(100.0, 150.0)); + + let group1 = match group_node1 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let group_node2 = &group1.children()[0]; + assert!(matches!(group_node2, usvg::Node::Group(_))); + assert_eq!(group_node2.id(), "svg1"); + assert_eq!(group_node2.abs_transform(), usvg::Transform::from_translate(100.0, 150.0)); + + let group2 = match group_node2 { + usvg::Node::Group(ref g) => g, + _ => unreachable!(), + }; + + let path = &group2.children()[0]; + assert!(matches!(path, usvg::Node::Path(_))); + assert_eq!(path.abs_transform(), usvg::Transform::from_translate(100.0, 150.0)); +} diff --git a/crates/usvg/tests/write.rs b/crates/usvg/tests/write.rs new file mode 100644 index 000000000..45b2d3b89 --- /dev/null +++ b/crates/usvg/tests/write.rs @@ -0,0 +1,196 @@ +use once_cell::sync::Lazy; + +static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_fonts_dir("../resvg/tests/fonts"); + fontdb.set_serif_family("Noto Serif"); + fontdb.set_sans_serif_family("Noto Sans"); + fontdb.set_cursive_family("Yellowtail"); + fontdb.set_fantasy_family("Sedgwick Ave Display"); + fontdb.set_monospace_family("Noto Mono"); + std::sync::Mutex::new(fontdb) +}); + +fn resave(name: &str) { + resave_impl(name, None, false); +} + +fn resave_with_text(name: &str) { + resave_impl(name, None, true); +} + +fn resave_with_prefix(name: &str, id_prefix: &str) { + resave_impl(name, Some(id_prefix.to_string()), false); +} + +fn resave_impl(name: &str, id_prefix: Option, preserve_text: bool) { + let input_svg = std::fs::read_to_string(format!("tests/files/{}.svg", name)).unwrap(); + + let tree = { + let fontdb = GLOBAL_FONTDB.lock().unwrap(); + let opt = usvg::Options::default(); + usvg::Tree::from_str(&input_svg, &opt, &fontdb).unwrap() + }; + let mut xml_opt = usvg::WriteOptions::default(); + xml_opt.id_prefix = id_prefix; + xml_opt.preserve_text = preserve_text; + xml_opt.coordinates_precision = 4; // Reduce noise and file size. + xml_opt.transforms_precision = 4; + let output_svg = tree.to_string(&xml_opt); + + // std::fs::write( + // format!("tests/files/{}-expected.svg", name), + // output_svg.clone(), + // ) + // .unwrap(); + + let expected_svg = + std::fs::read_to_string(format!("tests/files/{}-expected.svg", name)).unwrap(); + // Do not use `assert_eq` because it produces an unreadable output. + assert!(output_svg == expected_svg); +} + +#[test] +fn path_simple_case() { + resave("path-simple-case"); +} + +#[test] +fn ellipse_simple_case() { + resave("ellipse-simple-case"); +} + +#[test] +fn text_simple_case() { + resave("text-simple-case"); +} + +#[test] +fn preserve_id_filter() { + resave("preserve-id-filter"); +} + +#[test] +fn preserve_id_fe_image() { + resave("preserve-id-fe-image"); +} + +#[test] +fn preserve_id_fe_image_with_opacity() { + resave("preserve-id-fe-image-with-opacity"); +} + +#[test] +fn generate_filter_id_function_v1() { + resave("generate-id-filter-function-v1"); +} + +#[test] +fn generate_filter_id_function_v2() { + resave("generate-id-filter-function-v2"); +} + +#[test] +fn filter_id_with_prefix() { + resave_with_prefix("filter-id-with-prefix", "prefix-"); +} + +#[test] +fn filter_with_object_units_multi_use() { + resave("filter-with-object-units-multi-use"); +} + +#[test] +fn preserve_id_clip_path_v1() { + resave("preserve-id-clip-path-v1"); +} + +#[test] +fn preserve_id_clip_path_v2() { + resave("preserve-id-clip-path-v2"); +} + +#[test] +fn preserve_id_for_clip_path_in_pattern() { + resave("preserve-id-for-clip-path-in-pattern"); +} + +#[test] +fn generate_id_clip_path_for_symbol() { + resave("generate-id-clip-path-for-symbol"); +} + +#[test] +fn clip_path_with_text() { + resave("clip-path-with-text"); +} + +#[test] +fn clip_path_with_complex_text() { + resave("clip-path-with-complex-text"); +} + +#[test] +fn clip_path_with_object_units_multi_use() { + resave("clip-path-with-object-units-multi-use"); +} + +#[test] +fn mask_with_object_units_multi_use() { + resave("mask-with-object-units-multi-use"); +} + +#[test] +fn text_with_generated_gradients() { + resave("text-with-generated-gradients"); +} + +#[test] +fn preserve_text_multiple_font_families() { + resave_with_text("preserve-text-multiple-font-families"); +} + +#[test] +fn preserve_text_on_path() { + resave_with_text("preserve-text-on-path"); +} + +#[test] +fn preserve_text_in_clip_path() { + resave_with_text("preserve-text-in-clip-path"); +} + +#[test] +fn preserve_text_in_mask() { + resave_with_text("preserve-text-in-mask"); +} + +#[test] +fn preserve_text_in_pattern() { + resave_with_text("preserve-text-in-pattern"); +} + +#[test] +fn preserve_text_simple_case() { + resave("preserve-text-simple-case"); +} + +#[test] +fn preserve_text_with_dx_and_dy() { + resave_with_text("preserve-text-with-dx-and-dy"); +} + +#[test] +fn preserve_text_with_rotate() { + resave_with_text("preserve-text-with-rotate"); +} + +#[test] +fn preserve_text_with_complex_text_decoration() { + resave_with_text("preserve-text-with-complex-text-decoration"); +} + +#[test] +fn preserve_text_with_nested_baseline_shift() { + resave_with_text("preserve-text-with-nested-baseline-shift"); +} diff --git a/docs/svg2-changelog.md b/docs/svg2-changelog.md index 374bbcdd9..c6c2638c1 100644 --- a/docs/svg2-changelog.md +++ b/docs/svg2-changelog.md @@ -73,7 +73,7 @@ NOTE: This list is not final. This just things I was able to find so far. Patche ### Added - [ ] A [`transform-box`](https://www.w3.org/TR/css-transforms-1/#transform-box) property. -- [ ] A [`transform-origin`](https://www.w3.org/TR/css-transforms-1/#transform-origin-property) property. +- [x] A [`transform-origin`](https://www.w3.org/TR/css-transforms-1/#transform-origin-property) property. - [ ] A [`vector-effect`](https://www.w3.org/TR/SVG2/coords.html#VectorEffects) property. ### Changed @@ -141,7 +141,7 @@ Basically everything from [CSS Text Module Level 3](https://www.w3.org/TR/css-te - [ ] A [`shape-margin`](https://www.w3.org/TR/SVG2/text.html#TextShapeMargin) property. - [ ] A [`shape-padding`](https://www.w3.org/TR/SVG2/text.html#TextShapePadding) property. - [ ] New variants to [`font-variant`](https://drafts.csswg.org/css-fonts-3/#font-variant-prop) property. Previously it allowed only `small-caps`. -- [ ] A `font-variant-css21` value to [`font`](https://www.w3.org/TR/css-fonts-3/#propdef-font) property. +- [x] A `font-variant-css21` value to [`font`](https://www.w3.org/TR/css-fonts-3/#propdef-font) property. @@ -191,11 +191,11 @@ Basically everything from [CSS Text Module Level 3](https://www.w3.org/TR/css-te - [ ] An `arcs` variant to the [`stroke-linejoin`](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property. - [x] A `miter-clip` variant to the [`stroke-linejoin`](https://www.w3.org/TR/SVG2/painting.html#LineJoin) property. - [x] (partial support) A [`paint-order`](https://www.w3.org/TR/SVG2/painting.html#PaintOrder) property. -- [ ] `context-fill` and `context-stroke` variants to the [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) type. +- [x] `context-fill` and `context-stroke` variants to the [``](https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint) type. - [x] A [`mix-blend-mode`](https://www.w3.org/TR/compositing-1/#mix-blend-mode) property. - [x] An [`isolation`](https://www.w3.org/TR/compositing-1/#isolation) property. -- [ ] `left`, `center` and `right` variants to `refX` and `refY` properties of the the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element. -- [x] An `auto-start-reverse` variant to [`orient`](https://www.w3.org/TR/SVG2/painting.html#OrientAttribute) property of the the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element +- [ ] `left`, `center` and `right` variants to `refX` and `refY` properties of the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element. +- [x] An `auto-start-reverse` variant to [`orient`](https://www.w3.org/TR/SVG2/painting.html#OrientAttribute) property of the [`marker`](https://www.w3.org/TR/SVG2/painting.html#MarkerElement) element ### Changed diff --git a/docs/unsupported.md b/docs/unsupported.md index b343dadd1..96bede864 100644 --- a/docs/unsupported.md +++ b/docs/unsupported.md @@ -29,7 +29,6 @@ For the list of unsupported SVG 2 features see: [svg2-changelog.md](./svg2-chang - `color-profile` - `color-rendering` - `direction` -- `font` (do not confuse with `font-family`) - `font-size-adjust` - `font-stretch` - `glyph-orientation-horizontal` (removed in the SVG 2) diff --git a/tools/explorer-thumbnailer/Cargo.toml b/tools/explorer-thumbnailer/Cargo.toml index 4b749bbff..f99a723c9 100644 --- a/tools/explorer-thumbnailer/Cargo.toml +++ b/tools/explorer-thumbnailer/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "explorer-thumbnailer" -version = "0.36.0+class" +version = "0.41.0+class" authors = ["gentoo90 "] license = "MPL-2.0" -edition = "2018" +edition = "2021" publish = false [workspace] diff --git a/tools/explorer-thumbnailer/install/installer.iss b/tools/explorer-thumbnailer/install/installer.iss index e6d4dd731..84aa8045a 100644 --- a/tools/explorer-thumbnailer/install/installer.iss +++ b/tools/explorer-thumbnailer/install/installer.iss @@ -1,8 +1,8 @@ [Setup] AppName="resvg Explorer Extension" -AppVersion="0.36.0" -VersionInfoVersion="0.0.36.0" -AppVerName="resvg Explorer Extension 0.36.0" +AppVersion="0.41.0" +VersionInfoVersion="0.0.41.0" +AppVerName="resvg Explorer Extension 0.41.0" AppPublisher="Yevhenii Reizner" AppPublisherURL=https://github.com/RazrFalcon/resvg DefaultDirName="{pf}\resvg Explorer Extension" diff --git a/tools/explorer-thumbnailer/src/error.rs b/tools/explorer-thumbnailer/src/error.rs index 529af210d..a229f7400 100644 --- a/tools/explorer-thumbnailer/src/error.rs +++ b/tools/explorer-thumbnailer/src/error.rs @@ -28,11 +28,9 @@ impl std::fmt::Display for Error { impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &*self { - |IStreamStat(_) - |IStreamRead(_) - |TreeEmpty - |CreateDIBSectionError - |RenderError => None, + IStreamStat(_) | IStreamRead(_) | TreeEmpty | CreateDIBSectionError | RenderError => { + None + } TreeError(source) => Some(source), } } @@ -43,7 +41,7 @@ impl From for HRESULT { match err { IStreamStat(code) | IStreamRead(code) => code, TreeError(_) | TreeEmpty | RenderError => S_FALSE, - CreateDIBSectionError => E_POINTER + CreateDIBSectionError => E_POINTER, } } } diff --git a/tools/explorer-thumbnailer/src/thumbnail_provider.rs b/tools/explorer-thumbnailer/src/thumbnail_provider.rs index 94ba970e4..3c0bc18b9 100644 --- a/tools/explorer-thumbnailer/src/thumbnail_provider.rs +++ b/tools/explorer-thumbnailer/src/thumbnail_provider.rs @@ -1,14 +1,14 @@ +use crate::interfaces::{IInitializeWithStream, IThumbnailProvider}; +use crate::utils::{img_to_hbitmap, render_thumbnail, tree_from_istream}; +use crate::WINLOG_SOURCE; +use com::co_class; +use com::sys::{HRESULT, IID, S_OK}; +use log::error; +use resvg::usvg; use std::cell::RefCell; use winapi::shared::minwindef::{DWORD, UINT}; use winapi::shared::windef::HBITMAP; use winapi::um::objidlbase::LPSTREAM; -use com::sys::{HRESULT, IID, S_OK}; -use com::co_class; -use log::error; -use crate::WINLOG_SOURCE; -use crate::interfaces::{IThumbnailProvider, IInitializeWithStream}; -use crate::utils::{img_to_hbitmap, render_thumbnail, tree_from_istream}; -use resvg::usvg; // {4432C229-DFD0-4B18-8C4D-F58932AF6105} pub const CLSID_THUMBNAIL_PROVIDER_CLASS: IID = IID { @@ -20,7 +20,7 @@ pub const CLSID_THUMBNAIL_PROVIDER_CLASS: IID = IID { #[co_class(implements(IThumbnailProvider, IInitializeWithStream))] pub struct ThumbnailProvider { - tree: RefCell> + tree: RefCell>, } impl IInitializeWithStream for ThumbnailProvider { @@ -59,7 +59,8 @@ impl IThumbnailProvider for ThumbnailProvider { impl ThumbnailProvider { pub(crate) fn new() -> Box { // winlog::init fails sometimes but logging still works - #[allow(unused_must_use)] { + #[allow(unused_must_use)] + { winlog::init(WINLOG_SOURCE); } ThumbnailProvider::allocate(RefCell::new(None)) diff --git a/tools/explorer-thumbnailer/src/utils.rs b/tools/explorer-thumbnailer/src/utils.rs index a89d90614..b11c8ef30 100644 --- a/tools/explorer-thumbnailer/src/utils.rs +++ b/tools/explorer-thumbnailer/src/utils.rs @@ -7,7 +7,7 @@ use winapi::um::objidlbase::{LPSTREAM, STATSTG}; use winapi::um::wingdi::{BI_RGB, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, CreateDIBSection}; use com::sys::S_OK; use resvg::{usvg, tiny_skia}; -use usvg::{fontdb, TreeTextToPath, TreeParsing}; +use usvg::fontdb; use crate::error::Error; pub unsafe fn tree_from_istream(pstream: LPSTREAM) -> Result { @@ -31,8 +31,7 @@ pub unsafe fn tree_from_istream(pstream: LPSTREAM) -> Result let mut fontdb = fontdb::Database::new(); fontdb.load_system_fonts(); - let mut tree = usvg::Tree::from_data(&svg_data, &opt).map_err(|e| Error::TreeError(e))?; - tree.convert_text(&fontdb); + let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).map_err(|e| Error::TreeError(e))?; Ok(tree) } @@ -42,21 +41,20 @@ pub fn render_thumbnail(tree: &Option, cx: u32) -> Result rtree.size.height() { - rtree.size.to_int_size().scale_to_width(cx) + let size = if tree.size().width() > tree.size().height() { + tree.size().to_int_size().scale_to_width(cx) } else { - rtree.size.to_int_size().scale_to_height(cx) + tree.size().to_int_size().scale_to_height(cx) }.ok_or(Error::RenderError)?; let transform = tiny_skia::Transform::from_scale( - size.width() as f32 / rtree.size.width() as f32, - size.height() as f32 / rtree.size.height() as f32, + size.width() as f32 / tree.size().width() as f32, + size.height() as f32 / tree.size().height() as f32, ); let mut pixmap = tiny_skia::Pixmap::new(size.width(), size.height()).unwrap(); - rtree.render(transform, &mut pixmap.as_mut()); + resvg::render(&tree, transform, &mut pixmap.as_mut()); Ok(pixmap) } diff --git a/tools/viewsvg/README.md b/tools/viewsvg/README.md index 0aaba5615..d03a30fee 100644 --- a/tools/viewsvg/README.md +++ b/tools/viewsvg/README.md @@ -12,7 +12,7 @@ Note: make sure you have read the parent readme. ```sh # build C-API first -cargo build --release --manifest-path ../../c-api/Cargo.toml +cargo build --release --manifest-path ../../crates/c-api/Cargo.toml # build viewsvg qmake make diff --git a/version-bump.md b/version-bump.md index b18d6025d..6ce9f8204 100644 --- a/version-bump.md +++ b/version-bump.md @@ -1,13 +1,10 @@ - .github/chart.svg - .github/chart-svg2.svg - CHANGELOG.md +- crates/usvg/Cargo.toml - crates/resvg/Cargo.toml - crates/c-api/Cargo.toml - crates/c-api/resvg.h - crates/c-api/ResvgQt.h -- crates/usvg/Cargo.toml -- crates/usvg-parser/Cargo.toml -- crates/usvg-text-layout/Cargo.toml -- crates/usvg-tree/Cargo.toml - tools/explorer-thumbnailer/install/installer.iss - tools/explorer-thumbnailer/Cargo.toml