diff --git a/crates/resvg/tests/fonts/CFF-and-SBIX-LICENSE-APACHE.txt b/crates/resvg/tests/fonts/CFF-and-SBIX-LICENSE-APACHE.txt new file mode 100644 index 000000000..bb2ebeee3 --- /dev/null +++ b/crates/resvg/tests/fonts/CFF-and-SBIX-LICENSE-APACHE.txt @@ -0,0 +1 @@ +Copyright 2019 Simon Cozens. All rights reserved. Licensed under the Apache License, Version 2.0 (the โ€œLicenseโ€); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an โ€œAS ISโ€ BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/crates/resvg/tests/fonts/CFF-and-SBIX.otf b/crates/resvg/tests/fonts/CFF-and-SBIX.otf new file mode 100644 index 000000000..3d84af1a9 Binary files /dev/null and b/crates/resvg/tests/fonts/CFF-and-SBIX.otf differ diff --git a/crates/resvg/tests/fonts/NotoColorEmojiCBDT-LICENSE_APACHE.txt b/crates/resvg/tests/fonts/NotoColorEmojiCBDT-LICENSE_APACHE.txt new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/crates/resvg/tests/fonts/NotoColorEmojiCBDT-LICENSE_APACHE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/crates/resvg/tests/fonts/NotoColorEmojiCBDT.subset.ttf b/crates/resvg/tests/fonts/NotoColorEmojiCBDT.subset.ttf new file mode 100644 index 000000000..bdf68edc1 Binary files /dev/null and b/crates/resvg/tests/fonts/NotoColorEmojiCBDT.subset.ttf differ diff --git a/crates/resvg/tests/fonts/NotoEmoji-Regular.ttf b/crates/resvg/tests/fonts/NotoEmoji-Regular.ttf deleted file mode 100644 index 19b7badf4..000000000 Binary files a/crates/resvg/tests/fonts/NotoEmoji-Regular.ttf and /dev/null differ diff --git a/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-OFL.txt b/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-OFL.txt new file mode 100644 index 000000000..fcf543c74 --- /dev/null +++ b/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Noto Project Authors (https://github.com/notofonts/znamenny) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-Regular.ttf b/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-Regular.ttf new file mode 100644 index 000000000..66fa2c777 Binary files /dev/null and b/crates/resvg/tests/fonts/NotoZnamennyMusicalNotation-Regular.ttf differ diff --git a/crates/resvg/tests/fonts/README.md b/crates/resvg/tests/fonts/README.md new file mode 100644 index 000000000..55bb6199f --- /dev/null +++ b/crates/resvg/tests/fonts/README.md @@ -0,0 +1,9 @@ +How fonts were subsetted: + +Twitter Color Emoji +1. Download: https://github.com/13rac1/twemoji-color-font/releases/download/v14.0.2/TwitterColorEmoji-SVGinOT-14.0.2.zip +2. Run `fonttools subset TwitterColorEmoji-SVGinOT.ttf --unicodes="U+1F601,U+1F980,U+1F3F3,U+FE0F,U+200D,U+1F308,U+1F600,U+1F603,U+1F90C,U+1F90F" --output-file=TwitterColorEmoji.subset.ttf` + +Noto Color Emoji (CBDT) +1. Download: https://github.com/googlefonts/noto-emoji/blob/main/fonts/NotoColorEmoji.ttf +2. Run `fonttools subset NotoColorEmoji.ttf --unicodes="U+1F600" --output-file=NotoColorEmojiCBDT.subset.ttf` \ No newline at end of file diff --git a/crates/resvg/tests/fonts/TwitterColorEmoji-LICENSE-MIT.txt b/crates/resvg/tests/fonts/TwitterColorEmoji-LICENSE-MIT.txt new file mode 100644 index 000000000..a2e8b0a96 --- /dev/null +++ b/crates/resvg/tests/fonts/TwitterColorEmoji-LICENSE-MIT.txt @@ -0,0 +1,20 @@ +Applies to "EmojiOne SVGinOT Font" code only +Copyright (c) 2022 Brad Erickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/crates/resvg/tests/fonts/TwitterColorEmoji.subset.ttf b/crates/resvg/tests/fonts/TwitterColorEmoji.subset.ttf new file mode 100644 index 000000000..de1daaf18 Binary files /dev/null and b/crates/resvg/tests/fonts/TwitterColorEmoji.subset.ttf differ diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index c294a30d5..4220d6625 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -1360,6 +1360,15 @@ use crate::render; #[test] fn text_baseline_shift_sub() { assert_eq!(render("tests/text/baseline-shift/sub"), 0); } #[test] fn text_baseline_shift_super() { assert_eq!(render("tests/text/baseline-shift/super"), 0); } #[test] fn text_baseline_shift_with_rotate() { assert_eq!(render("tests/text/baseline-shift/with-rotate"), 0); } +#[test] fn text_color_font_cbdt() { assert_eq!(render("tests/text/color-font/cbdt"), 0); } +#[test] fn text_color_font_colrv0() { assert_eq!(render("tests/text/color-font/colrv0"), 0); } +#[test] fn text_color_font_compound_emojis_and_coordinates_list() { assert_eq!(render("tests/text/color-font/compound-emojis-and-coordinates-list"), 0); } +#[test] fn text_color_font_compound_emojis() { assert_eq!(render("tests/text/color-font/compound-emojis"), 0); } +#[test] fn text_color_font_mixed_text_rtl() { assert_eq!(render("tests/text/color-font/mixed-text-rtl"), 0); } +#[test] fn text_color_font_mixed_text() { assert_eq!(render("tests/text/color-font/mixed-text"), 0); } +#[test] fn text_color_font_sbix() { assert_eq!(render("tests/text/color-font/sbix"), 0); } +#[test] fn text_color_font_svg() { assert_eq!(render("tests/text/color-font/svg"), 0); } +#[test] fn text_color_font_writing_mode_eq_tb() { assert_eq!(render("tests/text/color-font/writing-mode=tb"), 0); } #[test] fn text_direction_rtl_with_vertical_writing_mode() { assert_eq!(render("tests/text/direction/rtl-with-vertical-writing-mode"), 0); } #[test] fn text_direction_rtl() { assert_eq!(render("tests/text/direction/rtl"), 0); } #[test] fn text_dominant_baseline_alignment_baseline_and_baseline_shift_on_tspans() { assert_eq!(render("tests/text/dominant-baseline/alignment-baseline-and-baseline-shift-on-tspans"), 0); } @@ -1465,14 +1474,11 @@ use crate::render; #[test] fn text_text_complex_grapheme_split_by_tspan() { assert_eq!(render("tests/text/text/complex-grapheme-split-by-tspan"), 0); } #[test] fn text_text_complex_graphemes_and_coordinates_list() { assert_eq!(render("tests/text/text/complex-graphemes-and-coordinates-list"), 0); } #[test] fn text_text_complex_graphemes() { assert_eq!(render("tests/text/text/complex-graphemes"), 0); } -#[test] fn text_text_compound_emojis_and_coordinates_list() { assert_eq!(render("tests/text/text/compound-emojis-and-coordinates-list"), 0); } -#[test] fn text_text_compound_emojis() { assert_eq!(render("tests/text/text/compound-emojis"), 0); } #[test] fn text_text_dx_and_dy_instead_of_x_and_y() { assert_eq!(render("tests/text/text/dx-and-dy-instead-of-x-and-y"), 0); } #[test] fn text_text_dx_and_dy_with_less_values_than_characters() { assert_eq!(render("tests/text/text/dx-and-dy-with-less-values-than-characters"), 0); } #[test] fn text_text_dx_and_dy_with_more_values_than_characters() { assert_eq!(render("tests/text/text/dx-and-dy-with-more-values-than-characters"), 0); } #[test] fn text_text_dx_and_dy_with_multiple_values() { assert_eq!(render("tests/text/text/dx-and-dy-with-multiple-values"), 0); } #[test] fn text_text_em_and_ex_coordinates() { assert_eq!(render("tests/text/text/em-and-ex-coordinates"), 0); } -#[test] fn text_text_emojis() { assert_eq!(render("tests/text/text/emojis"), 0); } #[test] fn text_text_escaped_text_1() { assert_eq!(render("tests/text/text/escaped-text-1"), 0); } #[test] fn text_text_escaped_text_2() { assert_eq!(render("tests/text/text/escaped-text-2"), 0); } #[test] fn text_text_escaped_text_3() { assert_eq!(render("tests/text/text/escaped-text-3"), 0); } diff --git a/crates/resvg/tests/tests/text/alignment-baseline/hanging-on-vertical.png b/crates/resvg/tests/tests/text/alignment-baseline/hanging-on-vertical.png index a9c1f0a99..116dda54c 100644 Binary files a/crates/resvg/tests/tests/text/alignment-baseline/hanging-on-vertical.png and b/crates/resvg/tests/tests/text/alignment-baseline/hanging-on-vertical.png differ diff --git a/crates/resvg/tests/tests/text/color-font/cbdt.png b/crates/resvg/tests/tests/text/color-font/cbdt.png new file mode 100644 index 000000000..adcdfa237 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/cbdt.png differ diff --git a/crates/resvg/tests/tests/text/color-font/cbdt.svg b/crates/resvg/tests/tests/text/color-font/cbdt.svg new file mode 100644 index 000000000..f4895cac1 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/cbdt.svg @@ -0,0 +1,12 @@ + + `cbdt` simple case + + + + ๐Ÿ˜€ + + + + diff --git a/crates/resvg/tests/tests/text/color-font/colrv0.png b/crates/resvg/tests/tests/text/color-font/colrv0.png new file mode 100644 index 000000000..e4b090d24 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/colrv0.png differ diff --git a/crates/resvg/tests/tests/text/color-font/colrv0.svg b/crates/resvg/tests/tests/text/color-font/colrv0.svg new file mode 100644 index 000000000..61c9b0384 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/colrv0.svg @@ -0,0 +1,12 @@ + + `COLRv0` + + + + ๐œฝœ๐œผฝ ๐œฝฑ ๐œฝฒ๐œผ† ๐œฝณ ๐œฝฏ๐œผผ๐œผ… ๐œพ’๐œผฐ๐œผป๐œผ…๐œผข๐œผช + + + + diff --git a/crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.png b/crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.png new file mode 100644 index 000000000..b6ecffc1f Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.png differ diff --git a/crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.svg b/crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.svg similarity index 88% rename from crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.svg rename to crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.svg index 7bcd1ecb0..323d30624 100644 --- a/crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.svg +++ b/crates/resvg/tests/tests/text/color-font/compound-emojis-and-coordinates-list.svg @@ -1,5 +1,5 @@ + font-family="Twitter Color Emoji" font-size="32"> Compound emojis and coordinates list - Compound emojis + font-family="Twitter Color Emoji" font-size="32"> + `svg` with compound emojis diff --git a/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.png b/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.png new file mode 100644 index 000000000..4be0084c6 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.png differ diff --git a/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.svg b/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.svg new file mode 100644 index 000000000..803c47551 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/mixed-text-rtl.svg @@ -0,0 +1,12 @@ + + `svg` with rtl + + + + ุงู‚ุฑุฃ๐Ÿ˜€ ุงู„ู…ุฒูŠุฏ ุนู† SVG ๐Ÿ˜ƒุฃูŠุถู‹ุง. + + + + diff --git a/crates/resvg/tests/tests/text/color-font/mixed-text.png b/crates/resvg/tests/tests/text/color-font/mixed-text.png new file mode 100644 index 000000000..6e31b50da Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/mixed-text.png differ diff --git a/crates/resvg/tests/tests/text/color-font/mixed-text.svg b/crates/resvg/tests/tests/text/color-font/mixed-text.svg new file mode 100644 index 000000000..f296a6883 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/mixed-text.svg @@ -0,0 +1,12 @@ + + `svg` with mixed text + + + + Hi๐Ÿ˜€there. + + + + diff --git a/crates/resvg/tests/tests/text/color-font/sbix.png b/crates/resvg/tests/tests/text/color-font/sbix.png new file mode 100644 index 000000000..f7839ac35 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/sbix.png differ diff --git a/crates/resvg/tests/tests/text/text/emojis.svg b/crates/resvg/tests/tests/text/color-font/sbix.svg similarity index 65% rename from crates/resvg/tests/tests/text/text/emojis.svg rename to crates/resvg/tests/tests/text/color-font/sbix.svg index ab2df284a..fe1648d16 100644 --- a/crates/resvg/tests/tests/text/text/emojis.svg +++ b/crates/resvg/tests/tests/text/color-font/sbix.svg @@ -1,11 +1,11 @@ - Emojis + font-family="CFF Outlines and SBIX" font-size="64"> + `sbix` - ๐Ÿ˜€๐Ÿ˜๐Ÿ˜‚๐Ÿคฃ + A diff --git a/crates/resvg/tests/tests/text/color-font/svg.png b/crates/resvg/tests/tests/text/color-font/svg.png new file mode 100644 index 000000000..476deb877 Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/svg.png differ diff --git a/crates/resvg/tests/tests/text/color-font/svg.svg b/crates/resvg/tests/tests/text/color-font/svg.svg new file mode 100644 index 000000000..5f6196880 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/svg.svg @@ -0,0 +1,12 @@ + + `svg` simple case + + + + ๐Ÿ˜€ + + + + diff --git a/crates/resvg/tests/tests/text/color-font/writing-mode=tb.png b/crates/resvg/tests/tests/text/color-font/writing-mode=tb.png new file mode 100644 index 000000000..98fdf53ba Binary files /dev/null and b/crates/resvg/tests/tests/text/color-font/writing-mode=tb.png differ diff --git a/crates/resvg/tests/tests/text/color-font/writing-mode=tb.svg b/crates/resvg/tests/tests/text/color-font/writing-mode=tb.svg new file mode 100644 index 000000000..9fca412f9 --- /dev/null +++ b/crates/resvg/tests/tests/text/color-font/writing-mode=tb.svg @@ -0,0 +1,13 @@ + + writing-mode=tb + + + + ๐Ÿ˜๐Ÿ˜ + ๐Ÿ˜๐Ÿ˜ + + + + diff --git a/crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.png b/crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.png deleted file mode 100644 index 0604a6514..000000000 Binary files a/crates/resvg/tests/tests/text/text/compound-emojis-and-coordinates-list.png and /dev/null differ diff --git a/crates/resvg/tests/tests/text/text/compound-emojis.png b/crates/resvg/tests/tests/text/text/compound-emojis.png deleted file mode 100644 index a8edaf436..000000000 Binary files a/crates/resvg/tests/tests/text/text/compound-emojis.png and /dev/null differ diff --git a/crates/resvg/tests/tests/text/text/emojis.png b/crates/resvg/tests/tests/text/text/emojis.png deleted file mode 100644 index ac8f7f49d..000000000 Binary files a/crates/resvg/tests/tests/text/text/emojis.png and /dev/null differ diff --git a/crates/resvg/tests/tests/text/textPath/complex.png b/crates/resvg/tests/tests/text/textPath/complex.png index a907668e0..3639e971d 100644 Binary files a/crates/resvg/tests/tests/text/textPath/complex.png and b/crates/resvg/tests/tests/text/textPath/complex.png differ diff --git a/crates/resvg/tests/tests/text/textPath/writing-mode=tb.png b/crates/resvg/tests/tests/text/textPath/writing-mode=tb.png index a1ba4f6a0..6f3f78ab2 100644 Binary files a/crates/resvg/tests/tests/text/textPath/writing-mode=tb.png and b/crates/resvg/tests/tests/text/textPath/writing-mode=tb.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.png b/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.png index 3decfdc0b..c7684715a 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.png and b/crates/resvg/tests/tests/text/writing-mode/japanese-with-tb.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png b/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png index 7e6091a59..74a7d219c 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png and b/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb-and-underline.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb.png b/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb.png index 6cc1d5925..2ef403454 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb.png and b/crates/resvg/tests/tests/text/writing-mode/mixed-languages-with-tb.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-and-punctuation.png b/crates/resvg/tests/tests/text/writing-mode/tb-and-punctuation.png index b42bc6535..cc6885a64 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/tb-and-punctuation.png and b/crates/resvg/tests/tests/text/writing-mode/tb-and-punctuation.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate-and-underline.png b/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate-and-underline.png index dc12a9d2e..2e974af07 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate-and-underline.png and b/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate-and-underline.png differ diff --git a/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate.png b/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate.png index ca7f1d7d4..5c609c77a 100644 Binary files a/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate.png and b/crates/resvg/tests/tests/text/writing-mode/tb-with-rotate.png differ diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs index b4d7b6c8c..249bfe945 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -2,14 +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::mem; +use std::sync::Arc; + use fontdb::{Database, ID}; use rustybuzz::ttf_parser; -use rustybuzz::ttf_parser::GlyphId; -use std::sync::Arc; -use tiny_skia_path::{NonZeroRect, Transform}; +use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat}; +use tiny_skia_path::{NonZeroRect, Size, Transform}; -use crate::tree::BBox; -use crate::{Group, Node, Path, ShapeRendering, Text, TextRendering}; +use crate::*; fn resolve_rendering_mode(text: &Text) -> ShapeRendering { match text.rendering_mode { @@ -19,58 +20,134 @@ fn resolve_rendering_mode(text: &Text) -> ShapeRendering { } } +fn push_outline_paths( + span: &layout::Span, + builder: &mut tiny_skia_path::PathBuilder, + new_children: &mut Vec, + rendering_mode: ShapeRendering, +) { + let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new()); + + if let Some(path) = builder.finish().and_then(|p| { + Path::new( + String::new(), + span.visibility, + span.fill.clone(), + span.stroke.clone(), + span.paint_order, + rendering_mode, + Arc::new(p), + Transform::default(), + ) + }) { + new_children.push(Node::Path(Box::new(path))); + } +} + pub(crate) fn flatten(text: &mut Text, fontdb: &fontdb::Database) -> Option<(Group, NonZeroRect)> { - let mut new_paths = vec![]; + let mut new_children = 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); + new_children.push(Node::Path(Box::new(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); + new_children.push(Node::Path(Box::new(path))); } + // Instead of always processing each glyph separately, we always collect + // as many outline glyphs as possible by pushing them into the span_builder + // and only if we encounter a different glyph, or we reach the very end of the + // span to we push the actual outline paths into new_children. This way, we don't need + // to create a new path for every glyph if we have many consecutive glyphs + // with just outlines (which is the most common case). 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); + // A COLRv0 glyph. Will return a vector of paths that make up the glyph description. + // TODO: Don't use black for foreground color? But not sure whether to use fill or stroke + // color. + if let Some(layers) = fontdb.colr(glyph.font, glyph.id) { + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + + let mut group = Group { + transform: glyph.colr_transform(), + ..Group::empty() + }; + + for path in layers { + // TODO: Probably need to update abs_transform of children? + group.children.push(Node::Path(Box::new(path))); } + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); } - } + // An SVG glyph. Will return the usvg tree containing the glyph descriptions. + else if let Some(tree) = fontdb.svg(glyph.font, glyph.id) { + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); - if let Some(path) = span_builder.finish().and_then(|p| { - Path::new( - 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); + let mut group = Group { + transform: glyph.svg_transform(), + ..Group::empty() + }; + // TODO: Probably need to update abs_transform of children? + group.children.push(Node::Group(Box::new(tree.root))); + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); + } + // A bitmap glyph. + else if let Some(img) = fontdb.raster(glyph.font, glyph.id) { + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + + let transform = if img.is_sbix { + glyph.sbix_transform( + img.x as f32, + img.y as f32, + img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32, + img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32, + img.pixels_per_em as f32, + img.image.size.height(), + ) + } else { + glyph.cbdt_transform( + img.x as f32, + img.y as f32, + img.pixels_per_em as f32, + img.image.size.height(), + ) + }; + + let mut group = Group { + transform, + ..Group::empty() + }; + group.children.push(Node::Image(Box::new(img.image))); + group.calculate_bounding_boxes(); + + new_children.push(Node::Group(Box::new(group))); + } else if let Some(outline) = fontdb + .outline(glyph.font, glyph.id) + .and_then(|p| p.transform(glyph.outline_transform())) + { + span_builder.push_path(&outline); + } } + push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + 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); + new_children.push(Node::Path(Box::new(path))); } } @@ -79,12 +156,13 @@ pub(crate) fn flatten(text: &mut Text, fontdb: &fontdb::Database) -> Option<(Gro ..Group::empty() }; - for path in new_paths { - group.children.push(Node::Path(Box::new(path))); + for child in new_children { + group.children.push(child); } group.calculate_bounding_boxes(); - Some((group, stroke_bbox.to_non_zero_rect()?)) + let stroke_bbox = group.stroke_bounding_box().to_non_zero_rect()?; + Some((group, stroke_bbox)) } struct PathBuilder { @@ -115,6 +193,18 @@ impl ttf_parser::OutlineBuilder for PathBuilder { pub(crate) trait DatabaseExt { fn outline(&self, id: ID, glyph_id: GlyphId) -> Option; + fn raster(&self, id: ID, glyph_id: GlyphId) -> Option; + fn svg(&self, id: ID, glyph_id: GlyphId) -> Option; + fn colr(&self, id: ID, glyph_id: GlyphId) -> Option>; +} + +pub(crate) struct BitmapImage { + image: Image, + x: i16, + y: i16, + pixels_per_em: u16, + glyph_bbox: Option, + is_sbix: bool, } impl DatabaseExt for Database { @@ -126,8 +216,126 @@ impl DatabaseExt for Database { let mut builder = PathBuilder { builder: tiny_skia_path::PathBuilder::new(), }; + font.outline_glyph(glyph_id, &mut builder)?; builder.builder.finish() })? } + + fn raster(&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 image = font.glyph_raster_image(glyph_id, u16::MAX)?; + + if image.format == RasterImageFormat::PNG { + let bitmap_image = BitmapImage { + image: Image { + id: String::new(), + visibility: Visibility::Visible, + size: Size::from_wh(image.width as f32, image.height as f32)?, + rendering_mode: ImageRendering::OptimizeQuality, + kind: ImageKind::PNG(Arc::new(image.data.into())), + abs_transform: Transform::default(), + abs_bounding_box: NonZeroRect::from_xywh( + 0.0, + 0.0, + image.width as f32, + image.height as f32, + )?, + }, + x: image.x, + y: image.y, + pixels_per_em: image.pixels_per_em, + glyph_bbox: font.glyph_bounding_box(glyph_id), + // ttf-parser always checks sbix first, so if this table exists, it was used. + is_sbix: font.tables().sbix.is_some(), + }; + + return Some(bitmap_image); + } + + None + })? + } + + fn svg(&self, id: ID, glyph_id: GlyphId) -> Option { + // TODO: Technically not 100% accurate because the SVG format in a OTF font + // is actually a subset/superset of a normal SVG, but it seems to work fine + // for Twitter Color Emoji, so might as well use what we already have. + self.with_face_data(id, |data, face_index| -> Option { + let font = ttf_parser::Face::parse(data, face_index).ok()?; + let image = font.glyph_svg_image(glyph_id)?; + Tree::from_data(image.data, &Options::default(), &fontdb::Database::new()).ok() + })? + } + + fn colr(&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 paths = vec![]; + let mut glyph_painter = GlyphPainter { + face: &font, + paths: &mut paths, + builder: PathBuilder { + builder: tiny_skia_path::PathBuilder::new(), + }, + }; + + font.paint_color_glyph(glyph_id, 0, &mut glyph_painter)?; + + Some(paths) + })? + } +} + +struct GlyphPainter<'a> { + face: &'a ttf_parser::Face<'a>, + paths: &'a mut Vec, + builder: PathBuilder, +} + +impl ttf_parser::colr::Painter for GlyphPainter<'_> { + fn outline(&mut self, glyph_id: ttf_parser::GlyphId) { + let builder = &mut self.builder; + match self.face.outline_glyph(glyph_id, builder) { + Some(v) => v, + None => return, + }; + } + + fn paint_foreground(&mut self) { + self.paint_color(ttf_parser::RgbaColor::new(0, 0, 0, 255)); + } + + fn paint_color(&mut self, color: ttf_parser::RgbaColor) { + let builder = mem::replace( + &mut self.builder, + PathBuilder { + builder: tiny_skia_path::PathBuilder::new(), + }, + ); + + if let Some(path) = builder.builder.finish().and_then(|p| { + let fill = Fill { + paint: Paint::Color(Color::new_rgb(color.red, color.green, color.blue)), + opacity: Opacity::new(f32::from(color.alpha) / 255.0).unwrap(), + rule: FillRule::NonZero, + context_element: None, + }; + + Path::new( + String::new(), + Visibility::Visible, + Some(fill), + None, + PaintOrder::FillAndStroke, + ShapeRendering::GeometricPrecision, + Arc::new(p), + Transform::default(), + ) + }) { + self.paths.push(path) + } + } } diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index 6b098cd9e..67cebcfca 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -29,17 +29,129 @@ use crate::{ /// 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, + /// Returns the transform of the glyph itself within the cluster. For example, + /// for zalgo text, it contains the transform to position the glyphs above/below + /// the main glyph. + glyph_ts: Transform, + /// Returns the transform of the whole cluster that the glyph is part of. + cluster_ts: Transform, + /// Returns the transform of the span that the glyph is a part of. + span_ts: Transform, + /// The units per em of the font the glyph belongs to. + units_per_em: u16, + /// The font size the glyph should be scaled to. + font_size: f32, /// The ID of the glyph. - pub glyph_id: GlyphId, + pub 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 PositionedGlyph { + /// Returns the transform of glyph, assuming that an outline + /// glyph is being used (i.e. from the `glyf` or `CFF/CFF2` table). + pub fn outline_transform(&self) -> Transform { + let mut ts = Transform::identity(); + + // Outlines are mirrored by default. + ts = ts.pre_scale(1.0, -1.0); + + let sx = self.font_size / self.units_per_em as f32; + + ts = ts.pre_scale(sx, sx); + ts = ts + .pre_concat(self.glyph_ts) + .post_concat(self.cluster_ts) + .post_concat(self.span_ts); + + ts + } + + /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph + /// is being used. + pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform { + self.span_ts + .pre_concat(self.cluster_ts) + .pre_concat(self.glyph_ts) + .pre_concat(Transform::from_scale( + self.font_size / pixels_per_em, + self.font_size / pixels_per_em, + )) + // Right now, the top-left corner of the image would be placed in + // on the "text cursor", but we want the bottom-left corner to be there, + // so we need to shift it up and also apply the x/y offset. + .pre_translate(x, -height - y) + } + + /// Returns the transform for the glyph, assuming that a sbix-based raster glyph + /// is being used. + pub fn sbix_transform( + &self, + x: f32, + y: f32, + x_min: f32, + y_min: f32, + pixels_per_em: f32, + height: f32, + ) -> Transform { + // In contrast to CBDT, we also need to look at the outline bbox of the glyph and add a shift if necessary. + let bbox_x_shift = self.font_size * (-x_min / self.units_per_em as f32); + + let bbox_y_shift = if y_min.approx_zero_ulps(4) { + // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift + // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows, + // this shift will not be applied. However, if this shift is not applied the emojis are a bit + // too high up when being together with other text, so we try to imitate this. + // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425 + // So whenever the y-shift is 0, we approximate this vertical shift that seems to be produced by it. + // This value seems to be pretty close to what is happening on MacOS. + // We can still remove this if it turns out to be a problem, but Apple Color Emoji is pretty + // much the only `sbix` font out there and they all seem to have a y-shift of 0, so it + // makes sense to keep it. + 0.128 * self.font_size + } else { + self.font_size * (-y_min / self.units_per_em as f32) + }; + + self.span_ts + .pre_concat(self.cluster_ts) + .pre_concat(self.glyph_ts) + .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift)) + .pre_concat(Transform::from_scale( + self.font_size / pixels_per_em, + self.font_size / pixels_per_em, + )) + // Right now, the top-left corner of the image would be placed in + // on the "text cursor", but we want the bottom-left corner to be there, + // so we need to shift it up and also apply the x/y offset. + .pre_translate(x, -height - y) + } + + /// Returns the transform for the glyph, assuming that an SVG glyph is + /// being used. + pub fn svg_transform(&self) -> Transform { + let mut ts = Transform::identity(); + + let sx = self.font_size / self.units_per_em as f32; + + ts = ts.pre_scale(sx, sx); + ts = ts + .pre_concat(self.glyph_ts) + .post_concat(self.cluster_ts) + .post_concat(self.span_ts); + + ts + } + + /// Returns the transform for the glyph, assuming that a COLR glyph is + /// being used. + pub fn colr_transform(&self) -> Transform { + self.outline_transform() + } +} + /// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and /// visibility. #[derive(Clone, Debug)] @@ -233,7 +345,8 @@ pub(crate) fn layout_text( .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) + pg.cluster_ts = cluster_ts; + pg.span_ts = span_ts; }); gc.glyphs }) @@ -920,14 +1033,15 @@ fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) 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(); - - // 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); + // Position glyph in the center of vertical axis. + ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0); + // Rotate by 90 degrees in the center. + ts = ts.pre_rotate_at( + -90.0, + cluster.width / 2.0, + -(cluster.ascent + cluster.descent) / 2.0, + ); cluster.path_transform = ts; @@ -1023,25 +1137,25 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu for glyph in glyphs { let sx = glyph.font.scale(font_size); - // 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); + let ts = Transform::from_translate(x + glyph.dx as f32, glyph.dy as f32); positioned_glyphs.push(PositionedGlyph { - transform: ts, + glyph_ts: ts, + // Will be set later. + cluster_ts: Transform::default(), + // Will be set later. + span_ts: Transform::default(), + units_per_em: glyph.font.units_per_em.get(), + font_size, font: glyph.font.id, text: glyph.text.clone(), - glyph_id: glyph.id, + id: glyph.id, }); x += glyph.width as f32;