From 9733c41ae44389b7bc8ba07604501bc53bf00a3c Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 3 Jun 2025 08:21:09 +0200 Subject: [PATCH 1/2] :bug: Fix blend mode on merge fills --- render-wasm/src/shapes/fills.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index d18d9e9df..9b282fd60 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -240,7 +240,7 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint { if let Some(shader) = shader { combined_shader = match combined_shader { Some(existing_shader) => Some(skia::shaders::blend( - skia::Blender::mode(skia::BlendMode::Overlay), + skia::Blender::mode(skia::BlendMode::DstOver), existing_shader, shader, )), From c40de5fb8794a82707a609bade43b8b4835b4230 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 3 Jun 2025 08:21:15 +0200 Subject: [PATCH 2/2] :tada: Implement font fallback to support multiple languages --- frontend/src/app/render_wasm/api.cljs | 13 +-- frontend/src/app/render_wasm/api/fonts.cljs | 90 ++++++++++++++++----- frontend/src/app/render_wasm/api/texts.cljs | 61 +++++++++++++- render-wasm/src/render/fonts.rs | 13 +++ render-wasm/src/shapes/text.rs | 24 ++++-- render-wasm/src/utils.rs | 6 ++ render-wasm/src/wasm/fonts.rs | 10 +-- 7 files changed, 179 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 7e32e6a86..83060faba 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -614,7 +614,8 @@ (let [paragraph-set (first (dm/get-prop content :children)) paragraphs (dm/get-prop paragraph-set :children) fonts (fonts/get-content-fonts content) - emoji? (atom false)] + emoji? (atom false) + languages (atom #{})] (loop [index 0] (when (< index (count paragraphs)) (let [paragraph (nth paragraphs index) @@ -623,12 +624,14 @@ (let [text (apply str (map :text leaves))] (when (and (not @emoji?) (t/contains-emoji? text)) (reset! emoji? true)) + (swap! languages into (t/get-languages text)) (t/write-shape-text leaves paragraph text)) (recur (inc index)))))) - (let [fonts (if @emoji? - (f/add-emoji-font fonts) - fonts)] - (f/store-fonts fonts)))) + (let [updated-fonts + (-> fonts + (cond-> emoji? (f/add-emoji-font)) + (f/add-noto-fonts @languages))] + (f/store-fonts updated-fonts)))) (defn set-shape-text [content] diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index fdfe703ee..bb52dca1c 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -15,7 +15,6 @@ [app.render-wasm.helpers :as h] [app.render-wasm.wasm :as wasm] [app.util.http :as http] - [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] [goog.object :as gobj] @@ -77,7 +76,7 @@ ;; IMPORTANT: It should be noted that only TTF fonts can be stored. (defn- store-font-buffer - [font-data font-array-buffer emoji?] + [font-data font-array-buffer emoji? fallback?] (let [id-buffer (:family-id-buffer font-data) size (.-byteLength font-array-buffer) ptr (h/call wasm/internal-module "_alloc_bytes" size) @@ -91,17 +90,17 @@ (aget id-buffer 3) (:weight font-data) (:style font-data) - emoji?) + emoji? + fallback?) true)) (defn- store-font-url - [font-data font-url emoji?] + [font-data font-url emoji? fallback?] (->> (http/send! {:method :get :uri font-url - :response-type :blob}) - (rx/map :body) - (rx/mapcat wapi/read-file-as-array-buffer) - (rx/map (fn [array-buffer] (store-font-buffer font-data array-buffer emoji?))))) + :response-type :buffer}) + (rx/map (fn [{:keys [body]}] + (store-font-buffer font-data body emoji? fallback?))))) (defn- google-font-ttf-url [font-id font-variant-id] @@ -121,7 +120,7 @@ (dm/str (u/join cf/public-uri "fonts/" asset-id)))) (defn- store-font-id - [font-data asset-id emoji?] + [font-data asset-id emoji? fallback?] (when asset-id (let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data)) id-buffer (uuid/get-u32 (:wasm-id font-data)) @@ -133,7 +132,8 @@ (aget id-buffer 3) (:weight font-data) (:style font-data)))] - (when-not font-stored? (store-font-url font-data uri emoji?))))) + (when-not font-stored? + (store-font-url font-data uri emoji? fallback?))))) (defn serialize-font-style [font-style] @@ -165,13 +165,13 @@ (defn store-font [font] - (let [font-id (dm/get-prop font :font-id) - font-variant-id (dm/get-prop font :font-variant-id) + (let [font-id (get font :font-id) + font-variant-id (get font :font-variant-id) + emoji? (get font :is-emoji false) + fallback? (get font :is-fallback false) wasm-id (font-id->uuid font-id) raw-weight (or (:weight (font-db-data font-id font-variant-id)) 400) - weight (serialize-font-weight raw-weight) - style (serialize-font-style (cond (str/includes? font-variant-id "italic") "italic" :else "normal")) @@ -180,9 +180,8 @@ :font-id font-id :font-variant-id font-variant-id :style style - :weight weight} - emoji? (dm/get-prop font :emoji?)] - (store-font-id font-data asset-id emoji?))) + :weight weight}] + (store-font-id font-data asset-id emoji? fallback?))) (defn store-fonts [fonts] @@ -195,4 +194,59 @@ :font-variant-id "regular" :style 0 :weight 400 - :emoji? true})) \ No newline at end of file + :is-emoji true})) + +(def noto-fonts + {:japanese {:font-id "gfont-noto-sans-jp" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :chinese {:font-id "gfont-noto-sans-sc" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :korean {:font-id "gfont-noto-sans-kr" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :arabic {:font-id "gfont-noto-sans-arabic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :cyrillic {:font-id "gfont-noto-sans-cyrillic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :greek {:font-id "gfont-noto-sans-greek" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :hebrew {:font-id "gfont-noto-sans-hebrew" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :thai {:font-id "gfont-noto-sans-thai" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :devanagari {:font-id "gfont-noto-sans-devanagari" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :tamil {:font-id "gfont-noto-sans-tamil" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :latin-ext {:font-id "gfont-noto-sans" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :vietnamese {:font-id "gfont-noto-sans" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :armenian {:font-id "gfont-noto-sans-armenian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :bengali {:font-id "gfont-noto-sans-bengali" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :cherokee {:font-id "gfont-noto-sans-cherokee" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :ethiopic {:font-id "gfont-noto-sans-ethiopic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :georgian {:font-id "gfont-noto-sans-georgian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :gujarati {:font-id "gfont-noto-sans-gujarati" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :gurmukhi {:font-id "gfont-noto-sans-gurmukhi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :khmer {:font-id "gfont-noto-sans-khmer" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :lao {:font-id "gfont-noto-sans-lao" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :malayalam {:font-id "gfont-noto-sans-malayalam" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :myanmar {:font-id "gfont-noto-sans-myanmar" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :sinhala {:font-id "gfont-noto-sans-sinhala" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :telugu {:font-id "gfont-noto-sans-telugu" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :tibetan {:font-id "gfont-noto-sans-tibetan" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :javanese {:font-id "noto-sans-javanese" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :kannada {:font-id "noto-sans-kannada" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :oriya {:font-id "noto-sans-oriya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :mongolian {:font-id "noto-sans-mongolian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :syriac {:font-id "noto-sans-syriac" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :tifinagh {:font-id "noto-sans-tifinagh" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :coptic {:font-id "noto-sans-coptic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :ol-chiki {:font-id "noto-sans-ol-chiki" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :vai {:font-id "noto-sans-vai" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :shavian {:font-id "noto-sans-shavian" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :osmanya {:font-id "noto-sans-osmanya" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :runic {:font-id "noto-sans-runic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :old-italic {:font-id "noto-sans-old-italic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :brahmi {:font-id "noto-sans-brahmi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :modi {:font-id "noto-sans-modi" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :sora-sompeng {:font-id "noto-sans-sora-sompeng" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :bamum {:font-id "noto-sans-bamum" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true} + :meroitic {:font-id "noto-sans-meroitic" :font-variant-id "regular" :style 0 :weight 400 :is-fallback true}}) + + +(defn add-noto-fonts [fonts languages] + (reduce (fn [acc lang] + (if-let [font (get noto-fonts lang)] + (conj acc font) + acc)) + fonts + languages)) diff --git a/frontend/src/app/render_wasm/api/texts.cljs b/frontend/src/app/render_wasm/api/texts.cljs index d57556c18..8e56eca51 100644 --- a/frontend/src/app/render_wasm/api/texts.cljs +++ b/frontend/src/app/render_wasm/api/texts.cljs @@ -148,7 +148,62 @@ (h/call wasm/internal-module "_set_shape_text_content")) -(def emoji-pattern #"[\uD83C-\uDBFF][\uDC00-\uDFFF]") +(def ^:private emoji-pattern #"[\uD83C-\uDBFF][\uDC00-\uDFFF]") -(defn contains-emoji? [s] - (boolean (re-find emoji-pattern s))) +(def ^:private unicode-ranges + {:japanese #"[\u3040-\u30FF\u31F0-\u31FF\uFF66-\uFF9F]" + :chinese #"[\u4E00-\u9FFF\u3400-\u4DBF]" + :korean #"[\uAC00-\uD7AF]" + :arabic #"[\u0600-\u06FF\u0750-\u077F\u0870-\u089F\u08A0-\u08FF]" + :cyrillic #"[\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F]" + :greek #"[\u0370-\u03FF\u1F00-\u1FFF]" + :hebrew #"[\u0590-\u05FF\uFB1D-\uFB4F]" + :thai #"[\u0E00-\u0E7F]" + :devanagari #"[\u0900-\u097F\uA8E0-\uA8FF]" + :tamil #"[\u0B80-\u0BFF]" + :latin-ext #"[\u0100-\u017F\u0180-\u024F]" + :vietnamese #"[\u1EA0-\u1EF9]" + :armenian #"[\u0530-\u058F\uFB13-\uFB17]" + :bengali #"[\u0980-\u09FF]" + :cherokee #"[\u13A0-\u13FF]" + :ethiopic #"[\u1200-\u137F]" + :georgian #"[\u10A0-\u10FF]" + :gujarati #"[\u0A80-\u0AFF]" + :gurmukhi #"[\u0A00-\u0A7F]" + :khmer #"[\u1780-\u17FF\u19E0-\u19FF]" + :lao #"[\u0E80-\u0EFF]" + :malayalam #"[\u0D00-\u0D7F]" + :myanmar #"[\u1000-\u109F\uAA60-\uAA7F]" + :sinhala #"[\u0D80-\u0DFF]" + :telugu #"[\u0C00-\u0C7F]" + :tibetan #"[\u0F00-\u0FFF]" + :javanese #"[\uA980-\uA9DF]" + :kannada #"[\u0C80-\u0CFF]" + :oriya #"[\u0B00-\u0B7F]" + :mongolian #"[\u1800-\u18AF]" + :syriac #"[\u0700-\u074F]" + :tifinagh #"[\u2D30-\u2D7F]" + :coptic #"[\u2C80-\u2CFF]" + :ol-chiki #"[\u1C50-\u1C7F]" + :vai #"[\uA500-\uA63F]" + :shavian #"[\u10450-\u1047F]" + :osmanya #"[\u10480-\u104AF]" + :runic #"[\u16A0-\u16FF]" + :old-italic #"[\u10300-\u1032F]" + :brahmi #"[\u11000-\u1107F]" + :modi #"[\u11600-\u1165F]" + :sora-sompeng #"[\u110D0-\u110FF]" + :bamum #"[\uA6A0-\uA6FF]" + :meroitic #"[\u10980-\u1099F]"}) + + +(defn contains-emoji? [text] + (boolean (re-find emoji-pattern text))) + +(defn get-languages [text] + (reduce-kv (fn [result lang pattern] + (if (re-find pattern text) + (conj result lang) + result)) + #{} + unicode-ranges)) diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index e21205f23..6b2fd465a 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -1,4 +1,5 @@ use skia_safe::{self as skia, textlayout, Font, FontMgr}; +use std::collections::HashSet; use crate::shapes::{FontFamily, FontStyle}; use crate::uuid::Uuid; @@ -21,6 +22,7 @@ pub struct FontStore { font_provider: textlayout::TypefaceFontProvider, font_collection: textlayout::FontCollection, debug_font: Font, + fallback_fonts: HashSet, } impl FontStore { @@ -41,6 +43,7 @@ impl FontStore { font_provider, font_collection, debug_font, + fallback_fonts: HashSet::new(), } } @@ -61,6 +64,7 @@ impl FontStore { family: FontFamily, font_data: &[u8], is_emoji: bool, + is_fallback: bool, ) -> Result<(), String> { if self.has_family(&family) { return Ok(()); @@ -80,6 +84,11 @@ impl FontStore { self.font_provider.register_typeface(typeface, font_name); self.font_collection.clear_caches(); + + if is_fallback { + self.fallback_fonts.insert(alias); + } + Ok(()) } @@ -87,6 +96,10 @@ impl FontStore { let serialized = format!("{}", family); self.font_provider.family_names().any(|x| x == serialized) } + + pub fn get_fallback(&self) -> &HashSet { + &self.fallback_fonts + } } fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontProvider { diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 7e7e82b23..7fc23c67a 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -7,10 +7,11 @@ use skia_safe::{ paint::Paint, textlayout::{FontCollection, ParagraphBuilder, ParagraphStyle}, }; +use std::collections::HashSet; use super::FontFamily; use crate::shapes::{self, merge_fills, set_paint_fill, Stroke, StrokeKind}; -use crate::utils::uuid_from_u32; +use crate::utils::{get_fallback_fonts, uuid_from_u32}; use crate::wasm::fills::parse_fills_from_bytes; use crate::Uuid; @@ -94,6 +95,7 @@ impl TextContent { } pub fn to_paragraphs(&self, fonts: &FontCollection) -> Vec> { + let fallback_fonts = get_fallback_fonts(); let mut paragraph_group = Vec::new(); let paragraphs = self .paragraphs @@ -102,7 +104,7 @@ impl TextContent { let paragraph_style = p.paragraph_to_style(); let mut builder = ParagraphBuilder::new(¶graph_style, fonts); for leaf in &p.children { - let text_style = leaf.to_style(p, &self.bounds); // FIXME + let text_style = leaf.to_style(p, &self.bounds, fallback_fonts); // FIXME let text = leaf.apply_text_transform(); builder.push_style(&text_style); builder.add_text(&text); @@ -121,6 +123,7 @@ impl TextContent { bounds: &Rect, fonts: &FontCollection, ) -> Vec> { + let fallback_fonts = get_fallback_fonts(); let mut paragraph_group = Vec::new(); let stroke_paints = get_text_stroke_paints(stroke, bounds); @@ -130,7 +133,8 @@ impl TextContent { let paragraph_style = paragraph.paragraph_to_style(); let mut builder = ParagraphBuilder::new(¶graph_style, fonts); for leaf in ¶graph.children { - let stroke_style = leaf.to_stroke_style(paragraph, &stroke_paint); + let stroke_style = + leaf.to_stroke_style(paragraph, &stroke_paint, fallback_fonts); let text: String = leaf.apply_text_transform(); builder.push_style(&stroke_style); builder.add_text(&text); @@ -336,6 +340,7 @@ impl TextLeaf { &self, paragraph: &Paragraph, content_bounds: &Rect, + fallback_fonts: &HashSet, ) -> skia::textlayout::TextStyle { let mut style = skia::textlayout::TextStyle::default(); @@ -359,14 +364,18 @@ impl TextLeaf { 3 => skia::textlayout::TextDecoration::OVERLINE, _ => skia::textlayout::TextDecoration::NO_DECORATION, }); - // FIXME + + // FIXME fix decoration styles style.set_decoration_color(paint.color()); - style.set_font_families(&[ + let mut font_families = vec![ self.serialized_font_family(), default_font(), DEFAULT_EMOJI_FONT.to_string(), - ]); + ]; + + font_families.extend(fallback_fonts.iter().cloned()); + style.set_font_families(&font_families); style } @@ -375,8 +384,9 @@ impl TextLeaf { &self, paragraph: &Paragraph, stroke_paint: &Paint, + fallback_fonts: &HashSet, ) -> skia::textlayout::TextStyle { - let mut style = self.to_style(paragraph, &Rect::default()); + let mut style = self.to_style(paragraph, &Rect::default(), fallback_fonts); style.set_foreground_paint(stroke_paint); style.set_font_size(self.font_size); style.set_letter_spacing(paragraph.letter_spacing); diff --git a/render-wasm/src/utils.rs b/render-wasm/src/utils.rs index 17992f82a..62d3a504f 100644 --- a/render-wasm/src/utils.rs +++ b/render-wasm/src/utils.rs @@ -2,6 +2,7 @@ use crate::skia::Image; use crate::uuid::Uuid; use crate::with_state; use crate::STATE; +use std::collections::HashSet; pub fn uuid_from_u32_quartet(a: u32, b: u32, c: u32, d: u32) -> Uuid { let hi: u64 = ((a as u64) << 32) | b as u64; @@ -25,3 +26,8 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid { pub fn get_image(image_id: &Uuid) -> Option<&Image> { with_state!(state, { state.render_state().images.get(image_id) }) } + +// FIXME: move to a different place ? +pub fn get_fallback_fonts() -> &'static HashSet { + with_state!(state, { state.render_state().fonts().get_fallback() }) +} diff --git a/render-wasm/src/wasm/fonts.rs b/render-wasm/src/wasm/fonts.rs index 17f4b96b1..84ef27703 100644 --- a/render-wasm/src/wasm/fonts.rs +++ b/render-wasm/src/wasm/fonts.rs @@ -14,19 +14,19 @@ pub extern "C" fn store_font( weight: u32, style: u8, is_emoji: bool, + is_fallback: bool, ) { with_state!(state, { let id = uuid_from_u32_quartet(a, b, c, d); let font_bytes = mem::bytes(); + let family = FontFamily::new(id, weight, style.into()); - let res = state + let _ = state .render_state() .fonts_mut() - .add(family, &font_bytes, is_emoji); + .add(family, &font_bytes, is_emoji, is_fallback); - if let Err(msg) = res { - eprintln!("{}", msg); - } + mem::free_bytes(); }); }