mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 09:16:15 +00:00
fix pdf export
This commit is contained in:
parent
6a85254d43
commit
c512ec189a
@ -192,7 +192,7 @@ fn ruby_draws(layout: &VerticalLayout, ruby: &RubyCell, origin: (f32, f32)) -> V
|
|||||||
.zip(&ruby.glyph_tops)
|
.zip(&ruby.glyph_tops)
|
||||||
.filter_map(|(ruby_glyph, top)| {
|
.filter_map(|(ruby_glyph, top)| {
|
||||||
let run = &layout.ruby_runs[ruby_glyph.run];
|
let run = &layout.ruby_runs[ruby_glyph.run];
|
||||||
let blob = single_glyph_blob(&run.font, *run.glyphs.get(ruby_glyph.glyph)?)?;
|
let blob = run.glyph_blob(ruby_glyph.glyph)?;
|
||||||
let (_, metrics) = run.font.metrics();
|
let (_, metrics) = run.font.metrics();
|
||||||
let advance = run
|
let advance = run
|
||||||
.advances
|
.advances
|
||||||
@ -761,4 +761,40 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,8 @@ pub struct ShapedRun {
|
|||||||
pub clusters: Vec<u32>,
|
pub clusters: Vec<u32>,
|
||||||
pub advance: f32,
|
pub advance: f32,
|
||||||
pub utf8_range: Range<usize>,
|
pub utf8_range: Range<usize>,
|
||||||
|
/// Source text of `utf8_range`, attached to the run's text blobs.
|
||||||
|
pub text: String,
|
||||||
/// Local-y offset that centres the run's ink after 90° rotation.
|
/// Local-y offset that centres the run's ink after 90° rotation.
|
||||||
pub rotated_baseline_shift: f32,
|
pub rotated_baseline_shift: f32,
|
||||||
}
|
}
|
||||||
@ -87,22 +89,77 @@ impl ShapedRun {
|
|||||||
glyph_run_ink_bounds(&self.font, &self.glyphs, &self.positions)
|
glyph_run_ink_bounds(&self.font, &self.glyphs, &self.positions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Byte range in `text` of the clusters covering `glyph..glyph + count`.
|
||||||
|
fn text_range(&self, glyph: usize, count: usize) -> Range<usize> {
|
||||||
|
let base = self.utf8_range.start as u32;
|
||||||
|
let clusters = &self.clusters[glyph..glyph + count];
|
||||||
|
let first = clusters.iter().copied().min().unwrap_or(base);
|
||||||
|
let last = clusters.iter().copied().max().unwrap_or(base);
|
||||||
|
let end = self
|
||||||
|
.clusters
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|cluster| *cluster > last)
|
||||||
|
.min()
|
||||||
|
.map_or(self.text.len(), |cluster| {
|
||||||
|
cluster.saturating_sub(base) as usize
|
||||||
|
});
|
||||||
|
(first.saturating_sub(base) as usize).min(end)..end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text blob of `glyph..glyph + count` at `positions`, carrying the
|
||||||
|
/// clusters' source text so PDF output maps vertical alternates back to
|
||||||
|
/// the original characters.
|
||||||
|
fn text_blob(&self, glyph: usize, count: usize, positions: &[SkPoint]) -> Option<TextBlob> {
|
||||||
|
let range = self.text_range(glyph, count);
|
||||||
|
let Some(text) = self
|
||||||
|
.text
|
||||||
|
.as_bytes()
|
||||||
|
.get(range.clone())
|
||||||
|
.filter(|t| !t.is_empty())
|
||||||
|
else {
|
||||||
|
let mut builder = TextBlobBuilder::new();
|
||||||
|
let (glyphs, points) = builder.alloc_run_pos(&self.font, count, None);
|
||||||
|
glyphs.copy_from_slice(&self.glyphs[glyph..glyph + count]);
|
||||||
|
points.copy_from_slice(positions);
|
||||||
|
return builder.make();
|
||||||
|
};
|
||||||
|
let text_start = self.utf8_range.start as u32 + range.start as u32;
|
||||||
|
let mut builder = TextBlobBuilder::new();
|
||||||
|
let (glyphs, points, utf8_text, clusters) =
|
||||||
|
builder.alloc_run_text_pos(&self.font, count, text.len(), None);
|
||||||
|
glyphs.copy_from_slice(&self.glyphs[glyph..glyph + count]);
|
||||||
|
points.copy_from_slice(positions);
|
||||||
|
utf8_text.copy_from_slice(text);
|
||||||
|
for (cluster, source) in clusters
|
||||||
|
.iter_mut()
|
||||||
|
.zip(&self.clusters[glyph..glyph + count])
|
||||||
|
{
|
||||||
|
*cluster = source.saturating_sub(text_start);
|
||||||
|
}
|
||||||
|
builder.make()
|
||||||
|
}
|
||||||
|
|
||||||
/// Text blob of one cluster, positioned from its first glyph's pen x.
|
/// Text blob of one cluster, positioned from its first glyph's pen x.
|
||||||
pub(super) fn cluster_blob(&self, glyph: usize, count: usize) -> Option<TextBlob> {
|
pub(super) fn cluster_blob(&self, glyph: usize, count: usize) -> Option<TextBlob> {
|
||||||
let mut builder = TextBlobBuilder::new();
|
self.text_blob(glyph, count, &self.cluster_positions(glyph, count))
|
||||||
let (glyphs, points) = builder.alloc_run_pos(&self.font, count, None);
|
|
||||||
glyphs.copy_from_slice(&self.glyphs[glyph..glyph + count]);
|
|
||||||
points.copy_from_slice(&self.cluster_positions(glyph, count));
|
|
||||||
builder.make()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text blob of the whole run at its shaped positions.
|
/// Text blob of the whole run at its shaped positions.
|
||||||
pub(super) fn blob(&self) -> Option<TextBlob> {
|
pub(super) fn blob(&self) -> Option<TextBlob> {
|
||||||
let mut builder = TextBlobBuilder::new();
|
self.text_blob(0, self.glyphs.len(), &self.positions)
|
||||||
let (glyphs, points) = builder.alloc_run_pos(&self.font, self.glyphs.len(), None);
|
}
|
||||||
glyphs.copy_from_slice(&self.glyphs);
|
|
||||||
points.copy_from_slice(&self.positions);
|
/// Text blob of one glyph at its pen origin. The first glyph of a
|
||||||
builder.make()
|
/// cluster carries the cluster's source text; the rest are glyph-only.
|
||||||
|
pub(super) fn glyph_blob(&self, glyph: usize) -> Option<TextBlob> {
|
||||||
|
self.glyphs.get(glyph)?;
|
||||||
|
let starts_cluster = glyph == 0 || self.clusters.get(glyph) != self.clusters.get(glyph - 1);
|
||||||
|
if starts_cluster {
|
||||||
|
self.text_blob(glyph, 1, &[SkPoint::default()])
|
||||||
|
} else {
|
||||||
|
single_glyph_blob(&self.font, self.glyphs[glyph])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local-y shift that centres a cluster's ink after a 90° rotation.
|
/// Local-y shift that centres a cluster's ink after a 90° rotation.
|
||||||
@ -217,6 +274,7 @@ impl RunHandler for RunCollector {
|
|||||||
clusters: self.scratch_clusters.clone(),
|
clusters: self.scratch_clusters.clone(),
|
||||||
advance: info.advance.x,
|
advance: info.advance.x,
|
||||||
utf8_range: info.utf8_range.clone(),
|
utf8_range: info.utf8_range.clone(),
|
||||||
|
text: String::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +337,12 @@ pub(super) fn shape_segment(
|
|||||||
f32::MAX,
|
f32::MAX,
|
||||||
&mut collector,
|
&mut collector,
|
||||||
);
|
);
|
||||||
|
for run in &mut collector.runs {
|
||||||
|
run.text = text
|
||||||
|
.get(run.utf8_range.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
collector.runs
|
collector.runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user