use std::{borrow::Cow, convert::Into};
use unicode_linebreak::linebreaks;
use image::{GenericImageView, Pixel, Rgba, RgbaImage};
use parley::GlyphRun;
use swash::{ColorPalette, scale::outline::Outline};
use taffy::{Layout, Point, Size};
use zeno::{Command, PathData, Stroke};
use crate::{
Result,
layout::{
inline::{InlineBrush, InlineLayout, break_lines},
style::{
Affine, BlendMode, Color, ImageScalingAlgorithm, SizedFontStyle, TextTransform,
WhiteSpaceCollapse,
},
},
rendering::{
BorderProperties, BufferPool, Canvas, CanvasConstrain, ColorTile, MaskMemory,
apply_mask_alpha_to_pixel, blend_pixel, draw_mask, mask_index_from_coord, overlay_area,
sample_transformed_pixel,
},
resources::font::ResolvedGlyph,
};
struct SwashImageView<'a>(&'a swash::scale::image::Image);
impl<'a> GenericImageView for SwashImageView<'a> {
type Pixel = Rgba<u8>;
fn dimensions(&self) -> (u32, u32) {
(self.0.placement.width, self.0.placement.height)
}
fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel {
let index = ((y * self.0.placement.width + x) * 4) as usize;
*Rgba::from_slice(&self.0.data[index..index + 4])
}
}
fn invert_y_coordinate(command: Command) -> Command {
match command {
Command::MoveTo(point) => Command::MoveTo((point.x, -point.y).into()),
Command::LineTo(point) => Command::LineTo((point.x, -point.y).into()),
Command::CurveTo(point1, point2, point3) => Command::CurveTo(
(point1.x, -point1.y).into(),
(point2.x, -point2.y).into(),
(point3.x, -point3.y).into(),
),
Command::QuadTo(point1, point2) => {
Command::QuadTo((point1.x, -point1.y).into(), (point2.x, -point2.y).into())
}
Command::Close => Command::Close,
}
}
pub(crate) fn draw_decoration(
canvas: &mut Canvas,
glyph_run: &GlyphRun<'_, InlineBrush>,
color: Color,
offset: f32,
size: f32,
layout: Layout,
transform: Affine,
) {
let start_x = layout.border.left + layout.padding.left + glyph_run.offset();
let end_x = start_x + glyph_run.advance();
if end_x <= start_x {
return;
}
let snapped_start_x = start_x.floor();
let width = (end_x.ceil() - snapped_start_x) as u32;
let tile = ColorTile {
color: color.into(),
width,
height: size as u32,
};
canvas.overlay_image(
&tile,
BorderProperties::default(),
transform
* Affine::translation(
snapped_start_x,
layout.border.top + layout.padding.top + offset,
),
ImageScalingAlgorithm::Auto,
BlendMode::Normal,
);
}
pub(crate) fn draw_glyph_clip_image<I: GenericImageView<Pixel = Rgba<u8>>>(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
clip_image: &I,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
match glyph {
ResolvedGlyph::Image(bitmap) => {
transform *= Affine::translation(bitmap.placement.left as f32, -bitmap.placement.top as f32);
let mask_capacity = (bitmap.placement.width * bitmap.placement.height) as usize;
let mut mask = canvas.buffer_pool.acquire(mask_capacity);
for (i, alpha) in bitmap.data.iter().skip(3).step_by(4).copied().enumerate() {
if i < mask.len() {
mask[i] = alpha;
}
}
let mut bottom = canvas
.buffer_pool
.acquire_image(bitmap.placement.width, bitmap.placement.height)?;
let fill_dimensions = clip_image.dimensions();
overlay_area(
&mut bottom,
Point::ZERO,
Size {
width: bitmap.placement.width,
height: bitmap.placement.height,
},
BlendMode::Normal,
&[],
|x, y| {
let alpha = mask[mask_index_from_coord(x, y, bitmap.placement.width)];
let source_x = (x as i32 + inline_offset.x as i32 + bitmap.placement.left) as u32;
let source_y = (y as i32 + inline_offset.y as i32 - bitmap.placement.top) as u32;
if source_x >= fill_dimensions.0 || source_y >= fill_dimensions.1 {
return Color::transparent().into();
}
let mut pixel = clip_image.get_pixel(source_x, source_y);
apply_mask_alpha_to_pixel(&mut pixel, alpha);
pixel
},
);
canvas.overlay_image(
&bottom,
BorderProperties::default(),
transform,
ImageScalingAlgorithm::Auto,
BlendMode::Normal,
);
canvas.buffer_pool.release_image(bottom);
canvas.buffer_pool.release(mask);
}
ResolvedGlyph::Outline(outline) => {
let Some(inverse) = transform.invert() else {
return Ok(());
};
let paths = collect_outline_paths(outline);
let (mask, placement) =
canvas
.mask_memory
.render(&paths, Some(transform), None, &mut canvas.buffer_pool);
overlay_area(
&mut canvas.image,
Point {
x: placement.left as f32,
y: placement.top as f32,
},
Size {
width: placement.width,
height: placement.height,
},
BlendMode::Normal,
&canvas.constrains,
|x, y| {
let alpha = mask[mask_index_from_coord(x, y, placement.width)];
if alpha == 0 {
return Color::transparent().into();
}
let sampled_pixel = sample_transformed_pixel(
clip_image,
inverse,
style.parent.image_rendering,
(x as i32 + placement.left) as f32,
(y as i32 + placement.top) as f32,
inline_offset,
);
let Some(mut pixel) = sampled_pixel else {
return Color::transparent().into();
};
apply_mask_alpha_to_pixel(&mut pixel, alpha);
pixel
},
);
canvas.buffer_pool.release(mask);
draw_text_stroke_clip_image(canvas, style, transform, &paths, clip_image, inline_offset);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_glyph(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
color: Color,
palette: Option<ColorPalette>,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
match glyph {
ResolvedGlyph::Image(bitmap) => {
transform *= Affine::translation(bitmap.placement.left as f32, -bitmap.placement.top as f32);
let image = SwashImageView(bitmap);
canvas.overlay_image(
&image,
Default::default(),
transform,
Default::default(),
BlendMode::Normal,
);
}
ResolvedGlyph::Outline(outline) => {
let paths = collect_outline_paths(outline);
if outline.is_color()
&& let Some(palette) = palette
{
draw_color_outline_image(
&mut canvas.image,
&mut canvas.mask_memory,
&mut canvas.buffer_pool,
outline,
palette,
transform,
&canvas.constrains,
color.0[3],
);
} else {
let (mask, placement) =
canvas
.mask_memory
.render(&paths, Some(transform), None, &mut canvas.buffer_pool);
draw_mask(
&mut canvas.image,
&mask,
placement,
color,
BlendMode::Normal,
&canvas.constrains,
);
canvas.buffer_pool.release(mask);
}
draw_text_stroke(canvas, style, transform, &paths);
}
}
Ok(())
}
fn draw_text_stroke_clip_image<I: GenericImageView<Pixel = Rgba<u8>>>(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
clip_image: &I,
inline_offset: Point<f32>,
) {
if style.stroke_width <= 0.0 {
return;
}
let Some(inverse) = transform.invert() else {
return;
};
let mut stroke = Stroke::new(style.stroke_width);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = canvas.mask_memory.render(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
overlay_area(
&mut canvas.image,
Point {
x: stroke_placement.left as f32,
y: stroke_placement.top as f32,
},
Size {
width: stroke_placement.width,
height: stroke_placement.height,
},
BlendMode::Normal,
&canvas.constrains,
|x, y| {
let alpha = stroke_mask[mask_index_from_coord(x, y, stroke_placement.width)];
if alpha == 0 {
return Color::transparent().into();
}
let inline_x = (x as i32 + stroke_placement.left) as f32;
let inline_y = (y as i32 + stroke_placement.top) as f32;
let sampled_pixel = sample_transformed_pixel(
clip_image,
inverse,
style.parent.image_rendering,
inline_x,
inline_y,
inline_offset,
);
let Some(mut pixel) = sampled_pixel else {
return Color::transparent().into();
};
blend_pixel(
&mut pixel,
style.text_stroke_color.into(),
BlendMode::Normal,
);
apply_mask_alpha_to_pixel(&mut pixel, alpha);
pixel
},
);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_stroke(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
) {
if style.stroke_width <= 0.0 {
return;
}
let mut stroke = Stroke::new(style.stroke_width);
stroke.join = style.parent.stroke_linejoin.into();
let (stroke_mask, stroke_placement) = canvas.mask_memory.render(
paths,
Some(transform),
Some(stroke.into()),
&mut canvas.buffer_pool,
);
draw_mask(
&mut canvas.image,
&stroke_mask,
stroke_placement,
style.text_stroke_color,
BlendMode::Normal,
&canvas.constrains,
);
canvas.buffer_pool.release(stroke_mask);
}
fn draw_text_shadow(
canvas: &mut Canvas,
style: &SizedFontStyle,
transform: Affine,
paths: &[Command],
) -> Result<()> {
let Some(ref shadows) = style.text_shadow else {
return Ok(());
};
for shadow in shadows.iter() {
shadow.draw_outset(canvas, paths, transform, Default::default(), None)?;
}
Ok(())
}
pub(crate) fn draw_glyph_text_shadow(
glyph: &ResolvedGlyph,
canvas: &mut Canvas,
style: &SizedFontStyle,
mut transform: Affine,
inline_offset: Point<f32>,
) -> Result<()> {
transform *= Affine::translation(inline_offset.x, inline_offset.y);
if let ResolvedGlyph::Outline(outline) = glyph {
let paths = collect_outline_paths(outline);
draw_text_shadow(canvas, style, transform, &paths)?;
}
Ok(())
}
pub(crate) fn collect_outline_paths(outline: &Outline) -> Vec<Command> {
outline
.path()
.commands()
.map(invert_y_coordinate)
.collect::<Vec<_>>()
}
#[allow(clippy::too_many_arguments)]
fn draw_color_outline_image(
canvas: &mut RgbaImage,
mask_memory: &mut MaskMemory,
buffer_pool: &mut BufferPool,
outline: &Outline,
palette: ColorPalette,
transform: Affine,
constrains: &[CanvasConstrain],
opacity: u8,
) {
if opacity == 0 {
return;
}
for i in 0..outline.len() {
let Some(layer) = outline.get(i) else {
break;
};
let Some(color) = layer.color_index().map(|index| Color(palette.get(index))) else {
continue;
};
let color = color.with_opacity(opacity);
let paths = layer
.path()
.commands()
.map(invert_y_coordinate)
.collect::<Vec<_>>();
let (mask, placement) = mask_memory.render(&paths, Some(transform), None, buffer_pool);
draw_mask(
canvas,
&mask,
placement,
color,
BlendMode::Normal,
constrains,
);
buffer_pool.release(mask);
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum MaxHeight {
Absolute(f32),
Lines(u32),
HeightAndLines(f32, u32),
}
pub(crate) fn apply_text_transform<'a>(input: &'a str, transform: TextTransform) -> Cow<'a, str> {
match transform {
TextTransform::None => Cow::Borrowed(input),
TextTransform::Uppercase => Cow::Owned(input.to_uppercase()),
TextTransform::Lowercase => Cow::Owned(input.to_lowercase()),
TextTransform::Capitalize => {
let mut result = String::with_capacity(input.len());
let mut start_of_word = true;
for ch in input.chars() {
if ch.is_alphabetic() {
if start_of_word {
result.extend(ch.to_uppercase());
start_of_word = false;
} else {
result.extend(ch.to_lowercase());
}
} else {
start_of_word = !ch.is_numeric();
result.push(ch);
}
}
Cow::Owned(result)
}
}
}
pub(crate) fn apply_white_space_collapse<'a>(
input: &'a str,
collapse: WhiteSpaceCollapse,
) -> Cow<'a, str> {
match collapse {
WhiteSpaceCollapse::Preserve => Cow::Borrowed(input),
WhiteSpaceCollapse::Collapse => {
let mut out = String::with_capacity(input.len());
let mut last_was_ws = false;
for ch in input.chars() {
if ch.is_whitespace() {
if !last_was_ws {
out.push(' ');
last_was_ws = true;
}
} else {
out.push(ch);
last_was_ws = false;
}
}
Cow::Owned(out.trim().to_string())
}
WhiteSpaceCollapse::PreserveSpaces => {
let mut out = String::with_capacity(input.len());
let mut last_was_space = false;
for ch in input.chars() {
if matches!(ch, '\n' | '\r' | '\x0B' | '\x0C' | '\u{2028}' | '\u{2029}') {
if !last_was_space {
out.push(' ');
last_was_space = true;
}
} else {
out.push(ch);
last_was_space = ch == ' ' || ch == '\t';
}
}
Cow::Owned(out)
}
WhiteSpaceCollapse::PreserveBreaks => {
let mut out = String::with_capacity(input.len());
let mut last_was_space = false;
let mut last_was_line_break = false;
for ch in input.chars() {
if ch == ' ' || ch == '\t' {
if last_was_line_break {
continue;
}
if !last_was_space {
out.push(' ');
last_was_space = true;
}
} else {
out.push(ch);
last_was_space = false;
last_was_line_break =
matches!(ch, '\n' | '\r' | '\x0B' | '\x0C' | '\u{2028}' | '\u{2029}');
}
}
Cow::Owned(out.trim().to_string())
}
}
}
fn count_word_splits(layout: &InlineLayout, text: &str) -> usize {
let mut splits = 0;
let lines: Vec<_> = layout.lines().collect();
let legal_breaks: Vec<usize> = linebreaks(text).map(|(offset, _)| offset).collect();
for i in 0..lines.len().saturating_sub(1) {
let end = lines[i].text_range().end;
let start = lines[i + 1].text_range().start;
if end == start && end > 0 && end < text.len() && !legal_breaks.contains(&end) {
splits += 1;
}
}
splits
}
pub(crate) fn make_balanced_text(
inline_layout: &mut InlineLayout,
text: &str,
max_width: f32,
max_height: Option<MaxHeight>,
target_lines: usize,
device_pixel_ratio: f32,
) -> bool {
if target_lines <= 1 {
return false;
}
let initial_splits = count_word_splits(inline_layout, text);
let mut left = max_width / 2.0;
let mut right = max_width;
const MAX_ITERATIONS: u32 = 20;
let mut iterations = 0;
while left + device_pixel_ratio < right && iterations < MAX_ITERATIONS {
iterations += 1;
let mid = (left + right) / 2.0;
break_lines(inline_layout, mid, None);
let lines_at_mid = inline_layout.lines().count();
if lines_at_mid > target_lines || count_word_splits(inline_layout, text) > initial_splits {
left = mid;
} else {
right = mid;
}
}
let balanced_width = right.ceil();
if (balanced_width - max_width).abs() < device_pixel_ratio {
break_lines(inline_layout, max_width, max_height);
false
} else {
break_lines(inline_layout, balanced_width, max_height);
true
}
}
pub(crate) fn make_pretty_text(
inline_layout: &mut InlineLayout,
max_width: f32,
max_height: Option<MaxHeight>,
) -> bool {
let Some(last_line_width) = inline_layout
.lines()
.last()
.map(|line| line.runs().map(|run| run.advance()).sum::<f32>())
else {
return false;
};
if last_line_width >= max_width / 3.0 {
return false;
}
let original_lines = inline_layout.lines().count();
if original_lines <= 1 {
return false;
}
let adjusted_width = max_width * 0.9;
break_lines(inline_layout, adjusted_width, None);
let adjusted_lines = inline_layout.lines().count();
let max_acceptable_lines = ((original_lines as f32) * 1.3).ceil() as usize;
if adjusted_lines <= max_acceptable_lines {
true
} else {
break_lines(inline_layout, max_width, max_height);
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_white_space_preserve() {
let input = " a \t b\n";
let out = apply_white_space_collapse(input, WhiteSpaceCollapse::Preserve);
assert_eq!(out, input);
}
#[test]
fn test_white_space_collapse() {
let input = " a \n\t b c\n\n ";
let out = apply_white_space_collapse(input, WhiteSpaceCollapse::Collapse);
assert_eq!(out, "a b c");
}
#[test]
fn test_white_space_preserve_spaces() {
let input = "a \n b";
let out = apply_white_space_collapse(input, WhiteSpaceCollapse::PreserveSpaces);
assert_eq!(out, "a b");
}
#[test]
fn test_white_space_preserve_breaks() {
let input = "a \n b\tc";
let out = apply_white_space_collapse(input, WhiteSpaceCollapse::PreserveBreaks);
assert_eq!(out, "a \nb c");
}
}