diff --git a/.gitattributes b/.gitattributes index 24a8e87..74b6958 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.png filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 0428627..876a36f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ Cargo.lock sample/udhr* target +**/.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 6de8ca1..33b1fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ harness = false members = ["examples/*"] [dev-dependencies] +tiny-skia = "0.11" criterion = { version = "0.5.1", default-features = false, features = [ "cargo_bench_support", ] } diff --git a/fonts/FiraMono-Medium.ttf b/fonts/FiraMono-Medium.ttf index 1e95ced..0a2f13f 100644 Binary files a/fonts/FiraMono-Medium.ttf and b/fonts/FiraMono-Medium.ttf differ diff --git a/fonts/NotoSans-LICENSE b/fonts/NotoSans-LICENSE new file mode 100644 index 0000000..c985727 --- /dev/null +++ b/fonts/NotoSans-LICENSE @@ -0,0 +1,93 @@ +Copyright 2012 Google Inc. All Rights Reserved. + +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: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +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/fonts/NotoSans-Regular.ttf b/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..bf87419 --- /dev/null +++ b/fonts/NotoSans-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ec33f84606cbaa0a1a944488e14f97faf2f6a25ecdd8354f5358f06da13c7d9 +size 556216 diff --git a/fonts/NotoSansArabic.ttf b/fonts/NotoSansArabic.ttf new file mode 100644 index 0000000..10a9939 --- /dev/null +++ b/fonts/NotoSansArabic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee489b994b3e62def9874c918145e32b133b625abaf98cec60502bdb40102c56 +size 765740 diff --git a/fonts/NotoSansHebrew.ttf b/fonts/NotoSansHebrew.ttf new file mode 100644 index 0000000..c507baa --- /dev/null +++ b/fonts/NotoSansHebrew.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d4fef85b449ade4d165de982969374fa30b2a5fe7bc679f5a3f5bfc047fb703 +size 183688 diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..9e250f8 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,144 @@ +use std::path::PathBuf; + +use cosmic_text::{ + fontdb::Database, Attrs, AttrsOwned, Buffer, Color, Family, FontSystem, Metrics, Shaping, + SwashCache, +}; +use tiny_skia::{Paint, Pixmap, Rect, Transform}; + +/// The test configuration. +/// The text in the test will be rendered as image using the one of the fonts found under the +/// `fonts` directory in this repository. +/// The image will then be compared to an image with the name `name` under the `tests/images` +/// directory in this repository. +/// If the images do not match the test will fail. +/// NOTE: if an environment variable `GENERATE_IMAGES` is set, the test will create and save +/// the images instead. +#[derive(Debug)] +pub struct DrawTestCfg { + /// The name of the test. + /// Will be used for the image name under the `tests/images` directory in this repository. + name: String, + /// The text to render to image + text: String, + /// The name, details of the font to be used. + /// Expected to be one of the fonts found under the `fonts` directory in this repository. + font: AttrsOwned, + + font_size: f32, + line_height: f32, + canvas_width: u32, + canvas_height: u32, +} + +impl Default for DrawTestCfg { + fn default() -> Self { + let font = Attrs::new().family(Family::Serif); + Self { + name: "default".into(), + font: AttrsOwned::new(font), + text: "".into(), + font_size: 16.0, + line_height: 20.0, + canvas_width: 300, + canvas_height: 300, + } + } +} + +impl DrawTestCfg { + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + ..Default::default() + } + } + + pub fn text(mut self, text: impl Into) -> Self { + self.text = text.into(); + self + } + + pub fn font_attrs(mut self, attrs: Attrs) -> Self { + self.font = AttrsOwned::new(attrs); + self + } + + pub fn font_size(mut self, font_size: f32, line_height: f32) -> Self { + self.font_size = font_size; + self.line_height = line_height; + self + } + + pub fn canvas(mut self, width: u32, height: u32) -> Self { + self.canvas_width = width; + self.canvas_height = height; + self + } + + pub fn validate_text_rendering(self) { + let repo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + // Create a db with just the fonts in our fonts dir to make sure we only test those + let fonts_path = PathBuf::from(&repo_dir).join("fonts"); + let mut font_db = Database::new(); + font_db.load_fonts_dir(fonts_path); + let mut font_system = FontSystem::new_with_locale_and_db("En-US".into(), font_db); + let mut swash_cache = SwashCache::new(); + let metrics = Metrics::new(self.font_size, self.line_height); + let mut buffer = Buffer::new(&mut font_system, metrics); + let mut buffer = buffer.borrow_with(&mut font_system); + let margins = 5; + buffer.set_size( + (self.canvas_width - margins * 2) as f32, + (self.canvas_height - margins * 2) as f32, + ); + buffer.set_text(&self.text, self.font.as_attrs(), Shaping::Advanced); + buffer.shape_until_scroll(); + + // Black + let text_color = Color::rgb(0x00, 0x00, 0x00); + + let mut pixmap = Pixmap::new(self.canvas_width, self.canvas_height).unwrap(); + pixmap.fill(tiny_skia::Color::WHITE); + + buffer.draw(&mut swash_cache, text_color, |x, y, w, h, color| { + let mut paint = Paint { + anti_alias: true, + ..Paint::default() + }; + paint.set_color_rgba8(color.r(), color.g(), color.b(), color.a()); + let rect = Rect::from_xywh( + (x + margins as i32) as f32, + (y + margins as i32) as f32, + w as f32, + h as f32, + ) + .unwrap(); + pixmap.fill_rect(rect, &paint, Transform::identity(), None); + }); + + let image_name = format!("{}.png", self.name); + let reference_image_path = PathBuf::from(&repo_dir) + .join("tests") + .join("images") + .join(image_name); + + let generate_images = std::env::var("GENERATE_IMAGES") + .map(|v| { + let val = v.trim().to_ascii_lowercase(); + ["t", "true", "1"].iter().any(|&v| v == val) + }) + .unwrap_or_default(); + + if generate_images { + pixmap.save_png(reference_image_path).unwrap(); + } else { + let reference_image_data = std::fs::read(reference_image_path).unwrap(); + let image_data = pixmap.encode_png().unwrap(); + assert_eq!( + reference_image_data, image_data, + "rendering failed of {self:?}" + ) + } + } +} diff --git a/tests/images/a_hebrew_paragraph.png b/tests/images/a_hebrew_paragraph.png new file mode 100644 index 0000000..e665371 --- /dev/null +++ b/tests/images/a_hebrew_paragraph.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0350ffaa09ac5b9976b0441cfb0fab0db0b13e3a2d33260592e113c347bedfe +size 23202 diff --git a/tests/images/a_hebrew_word.png b/tests/images/a_hebrew_word.png new file mode 100644 index 0000000..1159d48 --- /dev/null +++ b/tests/images/a_hebrew_word.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95bbe8f4db74914f6a124547f024463a3e434b3c7be3f86c1464af71ed39bba0 +size 3512 diff --git a/tests/images/an_arabic_paragraph.png b/tests/images/an_arabic_paragraph.png new file mode 100644 index 0000000..6fac02d --- /dev/null +++ b/tests/images/an_arabic_paragraph.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb6966eec660c39584f11d353940f6a274eb90125c8b58fabf3c3e5066a5e1e4 +size 24322 diff --git a/tests/images/an_arabic_word.png b/tests/images/an_arabic_word.png new file mode 100644 index 0000000..d40901b --- /dev/null +++ b/tests/images/an_arabic_word.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba0219c8e226f4bd79e0681a3189013ed035459fd3ed90405ec53d595e70ab81 +size 3851 diff --git a/tests/images/some_english_mixed_with_arabic.png b/tests/images/some_english_mixed_with_arabic.png new file mode 100644 index 0000000..59fc677 --- /dev/null +++ b/tests/images/some_english_mixed_with_arabic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbac330d91e73d8e28a2a83010e7f231bff6abfc9d9eda8720cb339c587084e +size 23093 diff --git a/tests/images/some_english_mixed_with_hebrew.png b/tests/images/some_english_mixed_with_hebrew.png new file mode 100644 index 0000000..cef8d58 --- /dev/null +++ b/tests/images/some_english_mixed_with_hebrew.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29fa23360829f2c11b960343b716eda01a069f6256a09b4eeb30d010abf7977f +size 57340 diff --git a/tests/shaping_and_rendering.rs b/tests/shaping_and_rendering.rs new file mode 100644 index 0000000..e70bcbe --- /dev/null +++ b/tests/shaping_and_rendering.rs @@ -0,0 +1,75 @@ +use common::DrawTestCfg; +use cosmic_text::Attrs; +use fontdb::Family; + +mod common; + +#[test] +fn test_hebrew_word_rendering() { + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("a_hebrew_word") + .font_size(36., 40.) + .font_attrs(attrs) + .text("בדיקה") + .canvas(100, 60) + .validate_text_rendering(); +} + +#[test] +fn test_hebrew_paragraph_rendering() { + let paragraph = "השועל החום המהיר קופץ מעל הכלב העצלן"; + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("a_hebrew_paragraph") + .font_size(36., 40.) + .font_attrs(attrs) + .text(paragraph) + .canvas(400, 110) + .validate_text_rendering(); +} + +#[test] +fn test_english_mixed_with_hebrew_paragraph_rendering() { + let paragraph = "Many computer programs fail to display bidirectional text correctly. For example, this page is mostly LTR English script, and here is the RTL Hebrew name Sarah: שרה, spelled sin (ש) on the right, resh (ר) in the middle, and heh (ה) on the left."; + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("some_english_mixed_with_hebrew") + .font_size(16., 20.) + .font_attrs(attrs) + .text(paragraph) + .canvas(400, 120) + .validate_text_rendering(); +} + +#[test] +fn test_arabic_word_rendering() { + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("an_arabic_word") + .font_size(36., 40.) + .font_attrs(attrs) + .text("خالصة") + .canvas(100, 60) + .validate_text_rendering(); +} + +#[test] +fn test_arabic_paragraph_rendering() { + let paragraph = "الثعلب البني السريع يقفز فوق الكلب الكسول"; + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("an_arabic_paragraph") + .font_size(36., 40.) + .font_attrs(attrs) + .text(paragraph) + .canvas(400, 110) + .validate_text_rendering(); +} + +#[test] +fn test_english_mixed_with_arabic_paragraph_rendering() { + let paragraph = "I like to render اللغة العربية in Rust!"; + let attrs = Attrs::new().family(Family::Name("Noto Sans")); + DrawTestCfg::new("some_english_mixed_with_arabic") + .font_size(36., 40.) + .font_attrs(attrs) + .text(paragraph) + .canvas(400, 110) + .validate_text_rendering(); +}