2026-10-02 16:38:30 +02:00

801 lines
28 KiB
Rust

use skia_safe::{
self as skia, canvas::SaveLayerRec, textlayout::Paragraph as SkiaParagraph,
textlayout::TextDecoration, BlendMode, Canvas, ImageFilter, Matrix, Paint, Point as SkPoint,
TextBlob,
};
use crate::math::Rect;
use crate::shapes::{Stroke, StrokeKind, TextContent, VerticalAlign};
use super::annotations::{emphasis_mark_center, ruby_strip_x, EmphasisMark, RubyCell};
use super::font_tables::upright_baseline;
use super::layout::{column_base_center, layout_for_box, CellKind, VerticalCell, VerticalLayout};
use super::shaping::{single_glyph_blob, ShapedRun};
/// One text blob drawn at `offset` in the local space that `transform` maps
/// to the document (identity when `None`). Canvas painting and outline export
/// share these draws, so they match for every cell kind.
struct GlyphDraw {
blob: TextBlob,
offset: SkPoint,
transform: Option<Matrix>,
}
impl GlyphDraw {
fn at(blob: TextBlob, offset: impl Into<SkPoint>) -> Self {
Self {
blob,
offset: offset.into(),
transform: None,
}
}
fn transformed(blob: TextBlob, offset: impl Into<SkPoint>, transform: Matrix) -> Self {
Self {
blob,
offset: offset.into(),
transform: Some(transform),
}
}
fn draw(&self, canvas: &Canvas, paint: &Paint) {
match &self.transform {
None => {
canvas.draw_text_blob(&self.blob, self.offset, paint);
}
Some(transform) => {
canvas.save();
canvas.concat(transform);
canvas.draw_text_blob(&self.blob, self.offset, paint);
canvas.restore();
}
}
}
fn into_path(self) -> skia::Path {
let path = text_blob_path(self.blob, self.offset);
match self.transform {
None => path,
Some(transform) => path.make_transform(&transform),
}
}
}
/// Glyphs on their side: +x of the blob runs down the column from `origin`.
fn rotated_at(origin: (f32, f32)) -> Matrix {
let mut matrix = Matrix::translate(origin);
matrix.pre_rotate(90.0, None);
matrix
}
/// Glyph draws of one cell, for a layout whose content origin is `origin`.
fn cell_draws(layout: &VerticalLayout, cell: &VerticalCell, origin: (f32, f32)) -> Vec<GlyphDraw> {
let column = &layout.columns[cell.column];
let x_center = origin.0 + column_base_center(column);
let y_top = origin.1 + cell.top;
match cell.kind {
CellKind::Upright { run, glyph, count } => {
let run = &layout.runs[run];
let baseline =
y_top + cell.glyph_flow_shift + upright_baseline(&run.font, cell.font_size);
run.cluster_blob(glyph, count)
.map(|blob| GlyphDraw::at(blob, (x_center - cell.h_advance / 2.0, baseline)))
.into_iter()
.collect()
}
CellKind::SyntheticRotated { run, glyph, count } => {
let run = &layout.runs[run];
run.cluster_blob(glyph, count)
.map(|blob| {
GlyphDraw::transformed(
blob,
(0.0, run.cluster_rotated_baseline_shift(glyph, count)),
rotated_at((x_center, y_top + cell.glyph_flow_shift)),
)
})
.into_iter()
.collect()
}
CellKind::Rotated { run } => {
let run = &layout.runs[run];
// After rotation +x runs down the column and +y runs across it;
// the shift centres the run's actual ink band on the column axis.
run.blob()
.map(|blob| {
GlyphDraw::transformed(
blob,
(0.0, run.rotated_baseline_shift),
rotated_at((x_center, y_top)),
)
})
.into_iter()
.collect()
}
CellKind::TateChuYoko {
run_start,
run_count,
scale,
} => {
let composite = &layout.runs[run_start..run_start + run_count];
let combined_advance: f32 = composite.iter().map(|run| run.advance).sum();
// Vertical centring uses the tallest run's ascent/descent.
let (ascent, descent) = composite.iter().fold((0.0f32, 0.0f32), |(a, d), run| {
let (_, metrics) = run.font.metrics();
(a.min(metrics.ascent), d.max(metrics.descent))
});
let x0 = x_center - (combined_advance * scale) / 2.0;
let baseline = y_top + cell.font_size / 2.0 - ((ascent + descent) * scale) / 2.0;
let mut transform = Matrix::translate((x0, baseline));
transform.pre_scale((scale, scale), None);
let mut cursor = 0.0f32;
let mut draws = Vec::with_capacity(composite.len());
for run in composite {
if let Some(blob) = run.blob() {
draws.push(GlyphDraw::transformed(blob, (cursor, 0.0), transform));
}
cursor += run.advance;
}
draws
}
CellKind::Warichu {
run_start,
run_count,
first_count,
..
} => {
let (first, second) =
layout.runs[run_start..run_start + run_count].split_at(first_count);
let quarter = cell.font_size / 4.0;
// vertical-rl: the first sub-line reads first, on the right half.
let mut draws = warichu_line_draws(first, x_center + quarter, y_top);
draws.extend(warichu_line_draws(second, x_center - quarter, y_top));
draws
}
}
}
/// One warichu sub-line: half-size upright glyphs stacked down the
/// sub-column centred on `x_center`, one cluster per cell.
fn warichu_line_draws(runs: &[ShapedRun], x_center: f32, y_top: f32) -> Vec<GlyphDraw> {
let mut draws = Vec::new();
let mut cursor = y_top;
for run in runs {
let font_size = run.font.size();
let baseline_offset = upright_baseline(&run.font, font_size);
for (glyph, count) in run.cluster_spans() {
let advance = match run.cluster_advance(glyph, count) {
advance if advance > 0.0 => advance,
_ => font_size,
};
if let Some(blob) = run.cluster_blob(glyph, count) {
draws.push(GlyphDraw::at(
blob,
(x_center - advance / 2.0, cursor + baseline_offset),
));
}
cursor += advance;
}
}
draws
}
/// One column's slice of a ruby annotation, stacked upright in the column's
/// ruby gutter. Flow positions come from the whole base span; each glyph is
/// centred across the gutter by its own fallback-font run.
fn ruby_draws(layout: &VerticalLayout, ruby: &RubyCell, origin: (f32, f32)) -> Vec<GlyphDraw> {
let column = &layout.columns[ruby.column];
let gutter_center = origin.0
+ ruby_strip_x(column, ruby.font_size, ruby.base_font_size, ruby.side)
+ ruby.font_size / 2.0;
ruby.glyphs
.iter()
.zip(&ruby.glyph_tops)
.filter_map(|(ruby_glyph, top)| {
let run = &layout.ruby_runs[ruby_glyph.run];
let blob = run.glyph_blob(ruby_glyph.glyph)?;
let (_, metrics) = run.font.metrics();
let advance = run
.advances
.get(ruby_glyph.glyph)
.copied()
.unwrap_or(ruby.font_size);
Some(GlyphDraw::at(
blob,
(
gutter_center - advance / 2.0,
origin.1 + top - metrics.ascent,
),
))
})
.collect()
}
/// One emphasis mark (圏点) centred on its base cell's flow extent, in the
/// column's right-side gutter.
fn emphasis_draw(
layout: &VerticalLayout,
mark: &EmphasisMark,
origin: (f32, f32),
) -> Option<GlyphDraw> {
let run = &layout.emphasis_runs[mark.run];
let blob = single_glyph_blob(&run.font, *run.glyphs.first()?)?;
let (center_x, center_y) = emphasis_mark_center(layout, mark);
let gutter_center = origin.0 + center_x;
let (_, metrics) = run.font.metrics();
let baseline = origin.1 + center_y - (metrics.ascent + metrics.descent) / 2.0;
let advance = run.advances.first().copied().unwrap_or(0.0);
Some(GlyphDraw::at(
blob,
(gutter_center - advance / 2.0, baseline),
))
}
/// Underline and line-through bars of a cell.
fn decoration_rects(
layout: &VerticalLayout,
cell: &VerticalCell,
origin: (f32, f32),
) -> Vec<skia::Rect> {
let Some(decoration) = cell.decoration else {
return Vec::new();
};
let mut rects = Vec::new();
if decoration.contains(TextDecoration::UNDERLINE) {
rects.push(decoration_bar(layout, cell, origin.0, origin.1, false));
}
if decoration.contains(TextDecoration::LINE_THROUGH) {
rects.push(decoration_bar(layout, cell, origin.0, origin.1, true));
}
rects
}
/// Fill pass: each cell drawn with its own fill paint and its decorations,
/// then ruby and emphasis marks with their base cell's paint.
pub fn paint_layout(
canvas: &Canvas,
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
) {
let origin = layout.origin(bounds, vertical_align);
for cell in &layout.cells {
let paint = &layout.paints[cell.paint];
for draw in cell_draws(layout, cell, origin) {
draw.draw(canvas, paint);
}
let mut decoration_paint = paint.clone();
decoration_paint.set_style(skia::PaintStyle::Fill);
decoration_paint.set_anti_alias(true);
for rect in decoration_rects(layout, cell, origin) {
canvas.draw_rect(rect, &decoration_paint);
}
}
for ruby in &layout.ruby_cells {
for draw in ruby_draws(layout, ruby, origin) {
draw.draw(canvas, &layout.paints[ruby.paint]);
}
}
for mark in &layout.emphasis_marks {
if let Some(draw) = emphasis_draw(layout, mark, origin) {
draw.draw(canvas, &layout.paints[layout.cells[mark.cell].paint]);
}
}
}
/// Paint every cell's glyphs with a single overriding paint (stroke /
/// shadow silhouette passes).
fn paint_glyphs(
canvas: &Canvas,
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
paint: &Paint,
) {
let origin = layout.origin(bounds, vertical_align);
for cell in &layout.cells {
for draw in cell_draws(layout, cell, origin) {
draw.draw(canvas, paint);
}
}
}
/// Paint the text content vertically inside its bounds. Returns false
/// when the content is not vertical, so the caller falls back to the
/// horizontal path.
pub fn paint_text_vertical(
canvas: &Canvas,
text_content: &TextContent,
vertical_align: VerticalAlign,
) -> bool {
if !text_content.is_vertical() {
return false;
}
let bounds = text_content.bounds();
let layout = layout_for_box(text_content, bounds.height());
paint_layout(canvas, &layout, &bounds, vertical_align);
true
}
/// Glyph-outline paths of the layout, in the same order and with the same
/// paints as the canvas fill pass.
fn paths_from_layout(
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
antialias: bool,
) -> Vec<(skia::Path, Paint)> {
let mut paths = Vec::new();
let origin = layout.origin(bounds, vertical_align);
for cell in &layout.cells {
let paint = &layout.paints[cell.paint];
for draw in cell_draws(layout, cell, origin) {
push_text_path(&mut paths, draw.into_path(), paint, antialias);
}
for rect in decoration_rects(layout, cell, origin) {
push_text_path(&mut paths, skia::Path::rect(rect, None), paint, antialias);
}
}
for ruby in &layout.ruby_cells {
for draw in ruby_draws(layout, ruby, origin) {
push_text_path(
&mut paths,
draw.into_path(),
&layout.paints[ruby.paint],
antialias,
);
}
}
for mark in &layout.emphasis_marks {
if let Some(draw) = emphasis_draw(layout, mark, origin) {
push_text_path(
&mut paths,
draw.into_path(),
&layout.paints[layout.cells[mark.cell].paint],
antialias,
);
}
}
paths
}
/// Glyph-outline paths of the vertical layout, matching the canvas pass.
pub fn vertical_text_paths(
text_content: &TextContent,
vertical_align: VerticalAlign,
antialias: bool,
) -> Vec<(skia::Path, Paint)> {
if !text_content.is_vertical() {
return Vec::new();
}
let bounds = text_content.bounds();
let layout = layout_for_box(text_content, bounds.height());
paths_from_layout(&layout, &bounds, vertical_align, antialias)
}
/// Developer overlay: a jlreq-style character-frame grid. Outlines each
/// column band and, per cell, the advance box, the virtual body / em square
/// centred on the column axis, and the glyph-ink band, so aki and
/// letter-spacing are visible. Only the on-screen fills pass calls it, behind
/// the `TEXT_GRID_VISIBLE` render flag; it never reaches exported output.
pub fn paint_grid(canvas: &Canvas, layout: &VerticalLayout, bounds: &Rect, align: VerticalAlign) {
let (origin_x, origin_y) = layout.origin(bounds, align);
let mut column_paint = Paint::default();
column_paint.set_anti_alias(true);
column_paint.set_style(skia::PaintStyle::Stroke);
column_paint.set_stroke_width(1.0);
column_paint.set_color(skia::Color::from_argb(0x55, 0x88, 0x88, 0x88));
let mut advance_paint = Paint::default();
advance_paint.set_anti_alias(true);
advance_paint.set_style(skia::PaintStyle::Stroke);
advance_paint.set_stroke_width(1.0);
advance_paint.set_color(skia::Color::from_argb(0xAA, 0x2F, 0x80, 0xED));
let mut em_paint = Paint::default();
em_paint.set_anti_alias(true);
em_paint.set_style(skia::PaintStyle::Stroke);
em_paint.set_stroke_width(1.0);
em_paint.set_color(skia::Color::from_argb(0x99, 0xEB, 0x57, 0x57));
let mut ink_paint = Paint::default();
ink_paint.set_anti_alias(true);
ink_paint.set_style(skia::PaintStyle::Stroke);
ink_paint.set_stroke_width(1.0);
ink_paint.set_color(skia::Color::from_argb(0x77, 0x27, 0xAE, 0x60));
// Column bands over the full used height.
for column in &layout.columns {
let x = origin_x + column.x;
canvas.draw_rect(
Rect::from_xywh(x, origin_y, column.width, layout.height),
&column_paint,
);
}
for cell in &layout.cells {
let column = &layout.columns[cell.column];
let x_left = origin_x + column.x;
let y_top = origin_y + cell.top;
let x_center = origin_x + column_base_center(column);
// Advance box: the real layout cell along the column axis.
canvas.draw_rect(
Rect::from_xywh(x_left, y_top, column.base_width, cell.extent),
&advance_paint,
);
// Virtual body / em square, centred on the column axis and on the
// cell's advance so aki and letter-spacing are visible.
let em = cell.font_size.max(1.0);
canvas.draw_rect(
Rect::from_xywh(
x_center - em / 2.0,
y_top + (cell.extent - em) / 2.0,
em,
em,
),
&em_paint,
);
// Glyph-ink band along the flow axis.
if cell.ink_bottom > cell.ink_top {
canvas.draw_line(
(x_left, origin_y + cell.top + cell.ink_top),
(x_left + column.width, origin_y + cell.top + cell.ink_top),
&ink_paint,
);
canvas.draw_line(
(x_left, origin_y + cell.top + cell.ink_bottom),
(x_left + column.width, origin_y + cell.top + cell.ink_bottom),
&ink_paint,
);
}
}
}
/// Paint the vertical glyph shadows. The shadow paint carries the
/// blur/offset image filter, so glyphs draw through it with no offscreen
/// layer (a `save_layer` with the filter can exceed GPU limits on tall
/// columns).
pub fn paint_drop_shadow(
canvas: &Canvas,
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
shadow_paint: &Paint,
) {
let mut paint = shadow_paint.clone();
paint.set_color(skia::Color::BLACK);
paint.set_anti_alias(true);
paint_glyphs(canvas, layout, bounds, vertical_align, &paint);
}
/// Paint a stroke on the vertical glyphs. Center strokes draw directly;
/// inner/outer strokes mask with `SrcIn` / `SrcOut` against the glyph
/// silhouette, as the horizontal path does.
pub fn paint_stroke(
canvas: &Canvas,
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
stroke: &Stroke,
selrect: &Rect,
blur: Option<&ImageFilter>,
) {
let (stroke_paints, layer_opacity) =
crate::render::text::get_text_stroke_paints(stroke, selrect, false);
if let Some(blur_filter) = blur {
let mut blur_paint = Paint::default();
blur_paint.set_image_filter(blur_filter.clone());
canvas.save_layer(&SaveLayerRec::default().paint(&blur_paint));
}
if let Some(opacity) = layer_opacity {
let mut opacity_paint = Paint::default();
opacity_paint.set_alpha_f(opacity);
canvas.save_layer(&SaveLayerRec::default().paint(&opacity_paint));
}
for stroke_paint in &stroke_paints {
match stroke.kind {
StrokeKind::Center => {
paint_glyphs(canvas, layout, bounds, vertical_align, stroke_paint)
}
StrokeKind::Inner => paint_masked_stroke(
canvas,
layout,
bounds,
vertical_align,
stroke_paint,
BlendMode::SrcIn,
),
StrokeKind::Outer => paint_masked_stroke(
canvas,
layout,
bounds,
vertical_align,
stroke_paint,
BlendMode::SrcOut,
),
}
}
if layer_opacity.is_some() {
canvas.restore();
}
if blur.is_some() {
canvas.restore();
}
}
fn paint_masked_stroke(
canvas: &Canvas,
layout: &VerticalLayout,
bounds: &Rect,
vertical_align: VerticalAlign,
stroke_paint: &Paint,
blend: BlendMode,
) {
let mut mask = Paint::default();
mask.set_color(skia::Color::BLACK);
mask.set_anti_alias(true);
canvas.save_layer(&SaveLayerRec::default());
paint_glyphs(canvas, layout, bounds, vertical_align, &mask);
let mut blend_paint = Paint::default();
blend_paint.set_blend_mode(blend);
canvas.save_layer(&SaveLayerRec::default().paint(&blend_paint));
paint_glyphs(canvas, layout, bounds, vertical_align, stroke_paint);
canvas.restore();
canvas.restore();
}
fn text_blob_path(mut blob: TextBlob, offset: impl Into<SkPoint>) -> skia::Path {
// get_path is relative to the blob's ink bounds; add them back to match.
let bounds = *blob.bounds();
let offset = offset.into();
SkiaParagraph::get_path(&mut blob).with_offset((offset.x + bounds.left, offset.y + bounds.top))
}
fn push_text_path(
paths: &mut Vec<(skia::Path, Paint)>,
path: skia::Path,
paint: &Paint,
antialias: bool,
) {
if path.is_empty() {
return;
}
let mut paint = paint.clone();
paint.set_anti_alias(antialias);
paths.push((path, paint));
}
/// Decoration bar of a cell in absolute coordinates. Underline runs along
/// the *left* side of the column (the under side in `vertical-rl`);
/// line-through runs down the centre. Both span the cell's extent, so
/// adjacent decorated cells form one bar.
fn decoration_bar(
layout: &VerticalLayout,
cell: &VerticalCell,
origin_x: f32,
origin_y: f32,
line_through: bool,
) -> skia::Rect {
let column = &layout.columns[cell.column];
let x_center = origin_x + column_base_center(column);
let y_top = origin_y + cell.top;
let thickness = (cell.font_size * 0.06).max(1.0);
let bar_x = if line_through {
x_center
} else {
x_center - cell.font_size / 2.0 - cell.font_size / 9.0
};
skia::Rect::from_ltrb(
bar_x - thickness / 2.0,
y_top,
bar_x + thickness / 2.0,
y_top + cell.extent,
)
}
#[cfg(test)]
mod tests {
use super::super::test_support::*;
use super::*;
use crate::shapes::{StrokeStyle, TextEmphasis, TextOrientation, TextSpan};
#[test]
fn shape_to_path_places_vertical_glyphs_down_the_column() {
let content = make_content(&["あく"], 1000.0);
let layout = layout_with(&provider(VMTX_TEST_FONT), &content);
let paths = paths_from_layout(&layout, &content.bounds(), VerticalAlign::Top, true);
assert_eq!(paths.len(), 2, "one outline path per upright glyph");
let first = paths[0].0.bounds();
let second = paths[1].0.bounds();
assert!(
second.top > first.top,
"the second glyph outline follows the first down the vertical flow axis"
);
let horizontal_shift = (second.center_x() - first.center_x()).abs();
let vertical_shift = second.center_y() - first.center_y();
assert!(
vertical_shift > horizontal_shift,
"vertical flow dominates the glyphs' optical side-bearing difference"
);
}
#[test]
fn shape_to_path_preserves_the_text_blob_draw_origin() {
let content = make_content(&["あ"], 1000.0);
let layout = layout_with(&provider(VMTX_TEST_FONT), &content);
let CellKind::Upright { run, glyph, count } = layout.cells[0].kind else {
panic!("expected an upright cell");
};
let blob = layout.runs[run]
.cluster_blob(glyph, count)
.expect("a glyph text blob");
let blob_bounds = *blob.bounds();
let draw_origin = SkPoint::new(120.0, 340.0);
let mut normalized_blob = blob.clone();
let normalized_bounds = *SkiaParagraph::get_path(&mut normalized_blob).bounds();
let path_bounds = *text_blob_path(blob, draw_origin).bounds();
assert!(
(path_bounds.left - (draw_origin.x + blob_bounds.left + normalized_bounds.left)).abs()
< 0.01
);
assert!(
(path_bounds.top - (draw_origin.y + blob_bounds.top + normalized_bounds.top)).abs()
< 0.01
);
assert!((path_bounds.width() - normalized_bounds.width()).abs() < 0.01);
assert!((path_bounds.height() - normalized_bounds.height()).abs() < 0.01);
}
#[test]
fn decoration_bar_underline_left_of_center_line_through_centered() {
let content = decorated_content("あい", TextDecoration::UNDERLINE);
let layout = layout_content(&content, 1000.0);
let (ox, oy) = layout.origin(&content.bounds(), VerticalAlign::Top);
let cell = &layout.cells[0];
let column = &layout.columns[cell.column];
let x_center = ox + column.x + column.width / 2.0;
let underline = decoration_bar(&layout, cell, ox, oy, false);
let strike = decoration_bar(&layout, cell, ox, oy, true);
assert!(underline.center_x() < x_center);
assert!((strike.center_x() - x_center).abs() < 0.01);
assert!((underline.height() - cell.extent).abs() < 0.01);
assert!(underline.width() >= 1.0 - 0.01);
}
#[test]
fn outline_export_includes_every_annotation_glyph() {
let content = spans_content(
vec![TextSpan {
ruby: "aあ".to_string(),
text_emphasis: TextEmphasis::FilledDot,
text_orientation: TextOrientation::Upright,
..make_span("AB")
}],
400.0,
);
let provider = provider_with_fallback(TEST_FONT, VMTX_TEST_FONT);
let layout = layout_with_fallback(&provider, &content, 400.0, &["fallback".to_string()]);
let ruby_glyphs: usize = layout.ruby_cells.iter().map(|ruby| ruby.glyphs.len()).sum();
assert_eq!(ruby_glyphs, 2, "both fallback runs carry a ruby glyph");
let paths = paths_from_layout(&layout, &content.bounds(), VerticalAlign::Top, true);
assert_eq!(
paths.len(),
layout.cells.len() + ruby_glyphs + layout.emphasis_marks.len(),
"one outline per base cell, ruby glyph and emphasis mark"
);
let mut surface = skia::surfaces::raster_n32_premul((256, 256)).unwrap();
paint_layout(
surface.canvas(),
&layout,
&content.bounds(),
VerticalAlign::Top,
);
}
#[test]
fn paint_passes_do_not_panic() {
let content = decorated_content("あいAB。", TextDecoration::LINE_THROUGH);
let layout = layout_content(&content, 1000.0);
let bounds = content.bounds();
let selrect = content.bounds();
let mut surface = skia::surfaces::raster_n32_premul((256, 256)).unwrap();
let canvas = surface.canvas();
// Callers apply shape transforms to the canvas; run every pass under
// a non-trivial transform with upright and rotated cells.
canvas.translate((12.0, 8.0));
canvas.rotate(7.0, Some((64.0, 64.0).into()));
paint_layout(canvas, &layout, &bounds, VerticalAlign::Top);
paint_drop_shadow(
canvas,
&layout,
&bounds,
VerticalAlign::Top,
&Paint::default(),
);
// A real drop-shadow image filter, as `drop_shadow_paints` builds.
let mut shadow_paint = Paint::default();
shadow_paint.set_image_filter(skia::image_filters::drop_shadow(
(12.0, 12.0),
(6.0, 6.0),
skia::Color::from_argb(230, 0, 0, 255),
None,
None,
None,
));
paint_drop_shadow(canvas, &layout, &bounds, VerticalAlign::Top, &shadow_paint);
for kind in [
Stroke::new_center_stroke(3.0, StrokeStyle::Solid, None, None, None, None),
Stroke::new_inner_stroke(3.0, StrokeStyle::Solid, None, None, None, None),
Stroke::new_outer_stroke(3.0, StrokeStyle::Solid, None, None, None, None),
] {
paint_stroke(
canvas,
&layout,
&bounds,
VerticalAlign::Top,
&kind,
&selrect,
None,
);
}
}
/// Uncompressed PDF of the layout's fill pass, as Latin-1 text.
fn layout_pdf(layout: &VerticalLayout, bounds: &Rect) -> String {
let mut bytes: Vec<u8> = Vec::new();
let metadata = skia::pdf::Metadata {
compression_level: skia::pdf::CompressionLevel::None,
..Default::default()
};
{
let document = skia::pdf::new_document(&mut bytes, Some(&metadata));
let mut page = document.begin_page((400.0, 400.0), None);
paint_layout(page.canvas(), layout, bounds, VerticalAlign::Top);
page.end_page().close();
}
bytes.iter().map(|byte| *byte as char).collect()
}
#[test]
fn pdf_text_of_vertical_forms_is_the_source_text() {
let content = make_content(&["「あ」"], 400.0);
let layout = layout_with(&provider(VPAL_TEST_FONT), &content);
let pdf = layout_pdf(&layout, &content.bounds()).to_uppercase();
let to_unicode = pdf
.split_once("BEGINBFCHAR")
.and_then(|(_, rest)| rest.split_once("ENDBFCHAR"))
.map(|(entries, _)| entries)
.expect("a ToUnicode CMap");
// The vertical alternates have no cmap entry of their own; the PDF
// text maps them back to the source characters.
for source in ["<300C>", "<3042>", "<300D>"] {
assert!(to_unicode.contains(source), "ToUnicode maps {source}");
}
assert!(!to_unicode.contains("<0000>"), "no glyph maps to U+0000");
}
}