diff --git a/CHANGELOG.md b/CHANGELOG.md index e759488b1..7177fc799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This changelog also contains important changes in dependencies. ## [Unreleased] ### Fixed +- Missing text when a `text` element uses multiple fonts and one of them produces ligatures. - Absolute transform propagation during `use` resolving. - Absolute transform propagation during nested `svg` resolving. - `Node::abs_transform` documentation. The current element's transform _is_ included. diff --git a/crates/resvg/tests/integration/render.rs b/crates/resvg/tests/integration/render.rs index 8ff4e4234..c294a30d5 100644 --- a/crates/resvg/tests/integration/render.rs +++ b/crates/resvg/tests/integration/render.rs @@ -650,6 +650,7 @@ use crate::render; #[test] fn painting_context_in_nested_use_and_marker() { assert_eq!(render("tests/painting/context/in-nested-use-and-marker"), 0); } #[test] fn painting_context_in_nested_use() { assert_eq!(render("tests/painting/context/in-nested-use"), 0); } #[test] fn painting_context_in_use() { assert_eq!(render("tests/painting/context/in-use"), 0); } +#[test] fn painting_context_on_shape_with_zero_size_bbox() { assert_eq!(render("tests/painting/context/on-shape-with-zero-size-bbox"), 0); } #[test] fn painting_context_with_gradient_and_gradient_transform() { assert_eq!(render("tests/painting/context/with-gradient-and-gradient-transform"), 0); } #[test] fn painting_context_with_gradient_in_use() { assert_eq!(render("tests/painting/context/with-gradient-in-use"), 0); } #[test] fn painting_context_with_gradient_on_marker() { assert_eq!(render("tests/painting/context/with-gradient-on-marker"), 0); } @@ -1478,6 +1479,8 @@ use crate::render; #[test] fn text_text_escaped_text_4() { assert_eq!(render("tests/text/text/escaped-text-4"), 0); } #[test] fn text_text_fill_rule_eq_evenodd() { assert_eq!(render("tests/text/text/fill-rule=evenodd"), 0); } #[test] fn text_text_filter_bbox() { assert_eq!(render("tests/text/text/filter-bbox"), 0); } +#[test] fn text_text_ligatures_handling_in_mixed_fonts_1() { assert_eq!(render("tests/text/text/ligatures-handling-in-mixed-fonts-1"), 0); } +#[test] fn text_text_ligatures_handling_in_mixed_fonts_2() { assert_eq!(render("tests/text/text/ligatures-handling-in-mixed-fonts-2"), 0); } #[test] fn text_text_mm_coordinates() { assert_eq!(render("tests/text/text/mm-coordinates"), 0); } #[test] fn text_text_nested() { assert_eq!(render("tests/text/text/nested"), 0); } #[test] fn text_text_no_coordinates() { assert_eq!(render("tests/text/text/no-coordinates"), 0); } diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png new file mode 100644 index 000000000..a5bd97ab9 Binary files /dev/null and b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.png differ diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.svg b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.svg new file mode 100644 index 000000000..b0bcf3a87 --- /dev/null +++ b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-1.svg @@ -0,0 +1,19 @@ + + Ligatures handling in mixed fonts (1) + + This is a very resvg-specific test to make sure we're corrently + handling ligatures in text with multiple fonts + + + + + + + + final final + + + + + diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png new file mode 100644 index 000000000..43a48444a Binary files /dev/null and b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.png differ diff --git a/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg new file mode 100644 index 000000000..1921c8495 --- /dev/null +++ b/crates/resvg/tests/tests/text/text/ligatures-handling-in-mixed-fonts-2.svg @@ -0,0 +1,19 @@ + + Ligatures handling in mixed fonts (2) + + This is a very resvg-specific test to make sure we're corrently + handling ligatures in text with multiple fonts + + + + + + + + final final + + + + + diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index d02449f97..64be0aade 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -752,8 +752,38 @@ fn process_chunk( fonts_cache: &FontsCache, fontdb: &fontdb::Database, ) -> Vec { - let mut glyphs = Vec::new(); + // The way this function works is a bit tricky. + // + // The first problem is BIDI reordering. + // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous. + // + // For example: + // Hello שלום. + // + // Would be shaped as: + // H e l l o ש ל ו ם . (characters) + // 0 1 2 3 4 5 12 10 8 6 14 (cluster indices in UTF-8) + // --- --- (green span) + // + // As you can see, our continuous `lo של` span was split into two separated one. + // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black. + // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output. + // To properly handle this we simply shape the whole chunk. + // + // But this introduces another issue - what to do when we have multiple fonts? + // The easy solution would be to simply shape text with each font, + // where the first font output is used as a base one and all others overwrite it. + // This way in case of: + // Hello world + // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple. + // + // Well, it would work most of the time, but not always. + // This is because different fonts can produce different amount of glyphs for the same text. + // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`, + // but some can use `fi` (U+FB01) instead. + // Meaning that during merging we have to overwrite not individual glyphs, but clusters. + let mut glyphs = Vec::new(); for span in &chunk.spans { let font = match fonts_cache.get(&span.font) { Some(v) => v.clone(), @@ -774,18 +804,35 @@ fn process_chunk( continue; } - // We assume, that shaping with an any font will produce the same amount of glyphs. - // Otherwise an error. - if glyphs.len() != tmp_glyphs.len() { - log::warn!("Text layouting failed."); - return Vec::new(); - } + // Overwrite span's glyphs. + let mut iter = tmp_glyphs.into_iter(); + while let Some(new_glyph) = iter.next() { + if !span_contains(span, new_glyph.byte_idx) { + continue; + } + + let Some(idx) = glyphs.iter().position(|g| g.byte_idx == new_glyph.byte_idx) else { + continue; + }; - // Copy span's glyphs. - for (i, glyph) in tmp_glyphs.iter().enumerate() { - if span_contains(span, glyph.byte_idx) { - glyphs[i] = glyph.clone(); + let prev_cluster_len = glyphs[idx].cluster_len; + if prev_cluster_len < new_glyph.cluster_len { + // If the new font represents the same cluster with fewer glyphs + // then remove remaining glyphs. + for _ in 1..new_glyph.cluster_len { + glyphs.remove(idx + 1); + } + } else if prev_cluster_len > new_glyph.cluster_len { + // If the new font represents the same cluster with more glyphs + // then insert them after the current one. + for j in 1..prev_cluster_len { + if let Some(g) = iter.next() { + glyphs.insert(idx + j, g); + } + } } + + glyphs[idx] = new_glyph; } } @@ -1326,6 +1373,7 @@ fn shape_text_with_font( glyphs.push(Glyph { byte_idx: ByteIndex::new(idx), + cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail? text: sub_text[start..end].to_string(), id: GlyphId(info.glyph_id as u16), dx: pos.x_offset, @@ -1468,6 +1516,9 @@ pub(crate) struct Glyph { /// We use it to match a glyph with a character in the text chunk and therefore with the style. pub(crate) byte_idx: ByteIndex, + // The length of the cluster in bytes. + pub(crate) cluster_len: usize, + /// The text from the original string that corresponds to that glyph. pub(crate) text: String,