diff --git a/Cargo.lock b/Cargo.lock index 373beeac..72d89389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1980,6 +1980,7 @@ dependencies = [ "peniko", "skrifa", "swash", + "tiny-skia", ] [[package]] diff --git a/README.md b/README.md index cf5f8e51..4bb69ea8 100644 --- a/README.md +++ b/README.md @@ -107,4 +107,9 @@ Licensed under either of at your option. +Some files used for tests are under different licenses: + +- The font file `Roboto-Regular.ttf` in `/parley/tests/assets/roboto_fonts/` is licensed solely as documented in that folder (and is licensed under the Apache License, Version 2.0). + + [Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct diff --git a/parley/Cargo.toml b/parley/Cargo.toml index 640125e6..a6bc492c 100644 --- a/parley/Cargo.toml +++ b/parley/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true +exclude = ["/tests"] [package.metadata.docs.rs] all-features = true @@ -31,3 +32,6 @@ fontique = { workspace = true } core_maths = { version = "0.1.0", optional = true } accesskit = { workspace = true, optional = true } hashbrown = { workspace = true, optional = true } + +[dev-dependencies] +tiny-skia = "0.11.4" diff --git a/parley/README.md b/parley/README.md index a769e7aa..3d79ec23 100644 --- a/parley/README.md +++ b/parley/README.md @@ -52,4 +52,8 @@ Licensed under either of at your option. +Some files used for tests are under different licenses: + +- The font file `Roboto-Regular.ttf` in `/parley/tests/assets/roboto_fonts/` is licensed solely as documented in that folder (and is licensed under the Apache License, Version 2.0). + [Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct diff --git a/parley/src/lib.rs b/parley/src/lib.rs index ed5468b8..81c654d0 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -118,6 +118,9 @@ mod util; pub mod layout; pub mod style; +#[cfg(test)] +mod tests; + pub use peniko::kurbo::Rect; pub use peniko::Font; diff --git a/parley/src/tests/mod.rs b/parley/src/tests/mod.rs new file mode 100644 index 00000000..ee3a0b32 --- /dev/null +++ b/parley/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod test_basic; +mod utils; diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs new file mode 100644 index 00000000..72b0e63b --- /dev/null +++ b/parley/src/tests/test_basic.rs @@ -0,0 +1,42 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::{testenv, Alignment, InlineBox}; + +#[test] +fn plain_multiline_text() { + let mut env = testenv!(); + + let text = "Hello world!\nLine 2\nLine 4"; + let mut builder = env.builder(text); + let mut layout = builder.build(text); + layout.break_all_lines(None); + layout.align(None, Alignment::Start); + + env.check_snapshot(&layout); +} + +#[test] +fn placing_inboxes() { + let mut env = testenv!(); + + for (position, test_case_name) in [ + (0, "start"), + (3, "in_word"), + (12, "end_nl"), + (13, "start_nl"), + ] { + let text = "Hello world!\nLine 2\nLine 4"; + let mut builder = env.builder(text); + builder.push_inline_box(InlineBox { + id: 0, + index: position, + width: 10.0, + height: 10.0, + }); + let mut layout = builder.build(text); + layout.break_all_lines(None); + layout.align(None, Alignment::Start); + env.check_snapshot_with_name(test_case_name, &layout); + } +} diff --git a/parley/src/tests/utils/env.rs b/parley/src/tests/utils/env.rs new file mode 100644 index 00000000..c2bcbde6 --- /dev/null +++ b/parley/src/tests/utils/env.rs @@ -0,0 +1,236 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::tests::utils::renderer::render_layout; +use crate::{ + FontContext, FontFamily, FontStack, Layout, LayoutContext, RangedBuilder, StyleProperty, +}; +use fontique::{Collection, CollectionOptions}; +use peniko::Color; +use std::path::{Path, PathBuf}; +use tiny_skia::Pixmap; + +// Creates a new instance of TestEnv and put current function name in constructor +#[macro_export] +macro_rules! testenv { + () => {{ + // Get name of the current function + fn f() {} + fn type_name_of(_: T) -> &'static str { + std::any::type_name::() + } + let name = type_name_of(f); + let name = &name[..name.len() - 3]; + let name = &name[name.rfind(':').map(|x| x + 1).unwrap_or(0)..]; + + // Create test env + $crate::tests::utils::TestEnv::new(name) + }}; +} + +fn current_imgs_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("current") +} + +fn snapshot_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("snapshots") +} + +fn font_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("assets") + .join("roboto_fonts") +} + +const DEFAULT_FONT_NAME: &str = "Roboto"; + +pub(crate) struct TestEnv { + test_name: String, + check_counter: u32, + font_cx: FontContext, + layout_cx: LayoutContext, + foreground_color: Color, + background_color: Color, + tolerance: f32, + errors: Vec<(PathBuf, String)>, +} + +fn is_accept_mode() -> bool { + std::env::var("PARLEY_TEST") + .map(|x| x.to_ascii_lowercase() == "accept") + .unwrap_or(false) +} + +pub(crate) fn load_fonts_dir(collection: &mut Collection, path: &Path) -> std::io::Result<()> { + let paths = std::fs::read_dir(path)?; + for entry in paths { + let entry = entry?; + if !entry.metadata()?.is_file() { + continue; + } + let path = entry.path(); + if path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| !["ttf", "otf", "ttc", "otc"].contains(&ext)) + .unwrap_or(true) + { + continue; + } + let font_data = std::fs::read(&path)?; + collection.register_fonts(font_data); + } + Ok(()) +} + +impl TestEnv { + pub(crate) fn new(test_name: &str) -> Self { + let file_prefix = format!("{}-", test_name); + let entries = std::fs::read_dir(current_imgs_dir()).unwrap(); + for entry in entries.flatten() { + let path = entry.path(); + if path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.starts_with(&file_prefix) && name.ends_with(".png")) + .unwrap_or(false) + { + std::fs::remove_file(&path).unwrap(); + } + } + + let mut collection = Collection::new(CollectionOptions { + shared: false, + system_fonts: false, + }); + load_fonts_dir(&mut collection, &font_dir()).unwrap(); + collection + .family_id(DEFAULT_FONT_NAME) + .unwrap_or_else(|| panic!("{} font not found", DEFAULT_FONT_NAME)); + TestEnv { + test_name: test_name.to_string(), + check_counter: 0, + font_cx: FontContext { + collection, + source_cache: Default::default(), + }, + tolerance: 0.0, + layout_cx: LayoutContext::new(), + foreground_color: Color::rgb8(0, 0, 0), + background_color: Color::rgb8(255, 255, 255), + errors: Vec::new(), + } + } + + pub(crate) fn builder<'a>(&'a mut self, text: &'a str) -> RangedBuilder<'a, Color> { + let mut builder = self.layout_cx.ranged_builder(&mut self.font_cx, text, 1.0); + builder.push_default(StyleProperty::Brush(self.foreground_color)); + builder.push_default(StyleProperty::FontStack(FontStack::Single( + FontFamily::Named(DEFAULT_FONT_NAME.into()), + ))); + builder + } + + fn image_name(&mut self, test_case_name: &str) -> String { + if test_case_name.is_empty() { + let name = format!("{}-{}.png", self.test_name, self.check_counter); + self.check_counter += 1; + name + } else { + assert!(test_case_name + .chars() + .all(|c| c == '_' || char::is_alphanumeric(c))); + format!("{}-{}.png", self.test_name, test_case_name) + } + } + + fn check_images(&self, current_img: &Pixmap, snapshot_path: &Path) -> Result<(), String> { + if !snapshot_path.is_file() { + return Err(format!("Cannot find snapshot {}", snapshot_path.display())); + } + let snapshot_img = Pixmap::load_png(snapshot_path) + .map_err(|_| format!("Loading snapshot {} failed", snapshot_path.display()))?; + if snapshot_img.width() != current_img.width() + || snapshot_img.height() != current_img.height() + { + return Err(format!( + "Snapshot has different size: snapshot {}x{}; generated image: {}x{}", + snapshot_img.width(), + snapshot_img.height(), + current_img.width(), + current_img.height() + )); + } + + let mut n_different_pixels = 0; + let mut color_cumulative_difference = 0.0; + for (pixel1, pixel2) in snapshot_img.pixels().iter().zip(current_img.pixels()) { + if pixel1 != pixel2 { + n_different_pixels += 1; + } + let diff_r = (pixel1.red() as f32 - pixel2.red() as f32).abs(); + let diff_g = (pixel1.green() as f32 - pixel2.green() as f32).abs(); + let diff_b = (pixel1.blue() as f32 - pixel2.blue() as f32).abs(); + color_cumulative_difference += diff_r.max(diff_g).max(diff_b); + } + if color_cumulative_difference > self.tolerance { + return Err(format!( + "Testing image differs in {n_different_pixels} pixels (color difference = {color_cumulative_difference})", + )); + } + Ok(()) + } + + pub(crate) fn check_snapshot_with_name( + &mut self, + test_case_name: &str, + layout: &Layout, + ) { + let current_img = render_layout(layout, self.background_color, self.foreground_color); + let image_name = self.image_name(test_case_name); + + let snapshot_path = snapshot_dir().join(&image_name); + let comparison_path = current_imgs_dir().join(&image_name); + + if let Err(e) = self.check_images(¤t_img, &snapshot_path) { + if is_accept_mode() { + current_img.save_png(&snapshot_path).unwrap(); + } else { + current_img.save_png(&comparison_path).unwrap(); + self.errors.push((comparison_path, e)); + } + } + } + + pub(crate) fn check_snapshot(&mut self, layout: &Layout) { + self.check_snapshot_with_name("", layout); + } +} + +impl Drop for TestEnv { + // Dropping of TestEnv cause panic (if there is not already one) + // We do not panic immediately when error is detected because we want to + // generate all images in the test and do visual confirmation of the whole + // set and not stop at the first error. + fn drop(&mut self) { + if !self.errors.is_empty() && !std::thread::panicking() { + use std::fmt::Write; + let mut panic_msg = String::new(); + for (path, msg) in &self.errors { + write!( + &mut panic_msg, + "{}\nImage written into: {}\n", + msg, + path.display() + ) + .unwrap(); + } + panic!("{}", &panic_msg); + } + } +} diff --git a/parley/src/tests/utils/mod.rs b/parley/src/tests/utils/mod.rs new file mode 100644 index 00000000..93396911 --- /dev/null +++ b/parley/src/tests/utils/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod env; +mod renderer; + +pub(crate) use env::TestEnv; diff --git a/parley/src/tests/utils/renderer.rs b/parley/src/tests/utils/renderer.rs new file mode 100644 index 00000000..78d67399 --- /dev/null +++ b/parley/src/tests/utils/renderer.rs @@ -0,0 +1,217 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A simple example that lays out some text using Parley, extracts outlines using Skrifa and +//! then paints those outlines using Tiny-Skia. +//! +//! Note: Emoji rendering is not currently implemented in this example. See the swash example +//! if you need emoji rendering. + +use crate::{GlyphRun, Layout, PositionedLayoutItem}; +use peniko::Color as PenikoColor; +use skrifa::{ + instance::{LocationRef, NormalizedCoord, Size}, + outline::{DrawSettings, OutlinePen}, + raw::FontRef as ReadFontsRef, + GlyphId, MetadataProvider, OutlineGlyph, +}; +use tiny_skia::{ + Color as TinySkiaColor, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Rect, Transform, +}; + +pub(crate) fn render_layout( + layout: &Layout, + background_color: peniko::Color, + inline_box_color: peniko::Color, +) -> Pixmap { + let padding = 20; + let width = layout.width().ceil() as u32; + let height = layout.height().ceil() as u32; + let padded_width = width + padding * 2; + let padded_height = height + padding * 2; + + let mut img = Pixmap::new(padded_width, padded_height).unwrap(); + + img.fill(to_tiny_skia(background_color)); + + let mut pen = TinySkiaPen::new(img.as_mut()); + + // Render each glyph run + for line in layout.lines() { + for item in line.items() { + match item { + PositionedLayoutItem::GlyphRun(glyph_run) => { + render_glyph_run(&glyph_run, &mut pen, padding); + } + PositionedLayoutItem::InlineBox(inline_box) => { + pen.set_origin(inline_box.x + padding as f32, inline_box.y + padding as f32); + pen.set_color(to_tiny_skia(inline_box_color)); + pen.fill_rect(inline_box.width, inline_box.height); + } + }; + } + } + img +} + +fn to_tiny_skia(color: PenikoColor) -> TinySkiaColor { + TinySkiaColor::from_rgba8(color.r, color.g, color.b, color.a) +} + +fn render_glyph_run(glyph_run: &GlyphRun, pen: &mut TinySkiaPen<'_>, padding: u32) { + // Resolve properties of the GlyphRun + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let style = glyph_run.style(); + let color = style.brush; + + // Get the "Run" from the "GlyphRun" + let run = glyph_run.run(); + + // Resolve properties of the Run + let font = run.font(); + let font_size = run.font_size(); + + let normalized_coords = run + .normalized_coords() + .iter() + .map(|coord| NormalizedCoord::from_bits(*coord)) + .collect::>(); + + // Get glyph outlines using Skrifa. This can be cached in production code. + let font_collection_ref = font.data.as_ref(); + let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap(); + let outlines = font_ref.outline_glyphs(); + + // Iterates over the glyphs in the GlyphRun + for glyph in glyph_run.glyphs() { + let glyph_x = run_x + glyph.x + padding as f32; + let glyph_y = run_y - glyph.y + padding as f32; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + if let Some(glyph_outline) = outlines.get(glyph_id) { + pen.set_origin(glyph_x, glyph_y); + pen.set_color(to_tiny_skia(color)); + pen.draw_glyph(&glyph_outline, font_size, &normalized_coords); + } + } + + // Draw decorations: underline & strikethrough + let style = glyph_run.style(); + let run_metrics = run.metrics(); + if let Some(decoration) = &style.underline { + let offset = decoration.offset.unwrap_or(run_metrics.underline_offset); + let size = decoration.size.unwrap_or(run_metrics.underline_size); + render_decoration(pen, glyph_run, decoration.brush, offset, size, padding); + } + if let Some(decoration) = &style.strikethrough { + let offset = decoration + .offset + .unwrap_or(run_metrics.strikethrough_offset); + let size = decoration.size.unwrap_or(run_metrics.strikethrough_size); + render_decoration(pen, glyph_run, decoration.brush, offset, size, padding); + } +} + +fn render_decoration( + pen: &mut TinySkiaPen<'_>, + glyph_run: &GlyphRun, + color: PenikoColor, + offset: f32, + width: f32, + padding: u32, +) { + let y = glyph_run.baseline() - offset + padding as f32; + let x = glyph_run.offset() + padding as f32; + pen.set_color(to_tiny_skia(color)); + pen.set_origin(x, y); + pen.fill_rect(glyph_run.advance(), width); +} + +struct TinySkiaPen<'a> { + pixmap: PixmapMut<'a>, + x: f32, + y: f32, + paint: Paint<'static>, + open_path: PathBuilder, +} + +impl TinySkiaPen<'_> { + fn new(pixmap: PixmapMut) -> TinySkiaPen { + TinySkiaPen { + pixmap, + x: 0.0, + y: 0.0, + paint: Paint::default(), + open_path: PathBuilder::new(), + } + } + + fn set_origin(&mut self, x: f32, y: f32) { + self.x = x; + self.y = y; + } + + fn set_color(&mut self, color: TinySkiaColor) { + self.paint.set_color(color); + } + + fn fill_rect(&mut self, width: f32, height: f32) { + let rect = Rect::from_xywh(self.x, self.y, width, height).unwrap(); + self.pixmap + .fill_rect(rect, &self.paint, Transform::identity(), None); + } + + fn draw_glyph( + &mut self, + glyph: &OutlineGlyph<'_>, + size: f32, + normalized_coords: &[NormalizedCoord], + ) { + let location_ref = LocationRef::new(normalized_coords); + let settings = DrawSettings::unhinted(Size::new(size), location_ref); + glyph.draw(settings, self).unwrap(); + + let builder = core::mem::replace(&mut self.open_path, PathBuilder::new()); + if let Some(path) = builder.finish() { + self.pixmap.fill_path( + &path, + &self.paint, + FillRule::Winding, + Transform::identity(), + None, + ); + } + } +} + +impl OutlinePen for TinySkiaPen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.open_path.move_to(self.x + x, self.y - y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.open_path.line_to(self.x + x, self.y - y); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.open_path + .quad_to(self.x + cx0, self.y - cy0, self.x + x, self.y - y); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.open_path.cubic_to( + self.x + cx0, + self.y - cy0, + self.x + cx1, + self.y - cy1, + self.x + x, + self.y - y, + ); + } + + fn close(&mut self) { + self.open_path.close(); + } +} diff --git a/parley/tests/README.md b/parley/tests/README.md new file mode 100644 index 00000000..8c9bd74f --- /dev/null +++ b/parley/tests/README.md @@ -0,0 +1,16 @@ +# Usage + +```bash +$ cargo test +``` + +If a test fails, you can compare images in /parley/tests/current (images created by the current test) +and /parley/tests/snapshots (the accepted versions). + +If you think that everything is ok, you can start tests as follows: + +```bash +$ PARLEY_TEST="accept" cargo test +``` + +It will update snapshots of the failed tests. \ No newline at end of file diff --git a/parley/tests/assets/roboto_fonts/LICENSE.txt b/parley/tests/assets/roboto_fonts/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/parley/tests/assets/roboto_fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. diff --git a/parley/tests/assets/roboto_fonts/Roboto-Regular.ttf b/parley/tests/assets/roboto_fonts/Roboto-Regular.ttf new file mode 100644 index 00000000..3d6861b4 Binary files /dev/null and b/parley/tests/assets/roboto_fonts/Roboto-Regular.ttf differ diff --git a/parley/tests/current/.gitignore b/parley/tests/current/.gitignore new file mode 100644 index 00000000..e33609d2 --- /dev/null +++ b/parley/tests/current/.gitignore @@ -0,0 +1 @@ +*.png diff --git a/parley/tests/snapshots/placing_inboxes-end_nl.png b/parley/tests/snapshots/placing_inboxes-end_nl.png new file mode 100644 index 00000000..d3634064 Binary files /dev/null and b/parley/tests/snapshots/placing_inboxes-end_nl.png differ diff --git a/parley/tests/snapshots/placing_inboxes-in_word.png b/parley/tests/snapshots/placing_inboxes-in_word.png new file mode 100644 index 00000000..b185b30f Binary files /dev/null and b/parley/tests/snapshots/placing_inboxes-in_word.png differ diff --git a/parley/tests/snapshots/placing_inboxes-start.png b/parley/tests/snapshots/placing_inboxes-start.png new file mode 100644 index 00000000..21d8df21 Binary files /dev/null and b/parley/tests/snapshots/placing_inboxes-start.png differ diff --git a/parley/tests/snapshots/placing_inboxes-start_nl.png b/parley/tests/snapshots/placing_inboxes-start_nl.png new file mode 100644 index 00000000..67475f9c Binary files /dev/null and b/parley/tests/snapshots/placing_inboxes-start_nl.png differ diff --git a/parley/tests/snapshots/plain_multiline_text-0.png b/parley/tests/snapshots/plain_multiline_text-0.png new file mode 100644 index 00000000..7f123a12 Binary files /dev/null and b/parley/tests/snapshots/plain_multiline_text-0.png differ