penpot/frontend/src/app/render_wasm/text_content.cljs

199 lines
9.2 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.render-wasm.text-content
"Single source of truth for writing a text shape's content into the WASM design
state. The binary layout ([num-spans][paragraph attrs][span attrs][text]) is
identical for the workspace and the headless exporter — only *font resolution*
differs (the workspace uses the loaded fonts DB; the exporter uses its gfonts
catalog + custom variants). So the byte-writing lives here and font resolution
is injected via the `opts` map passed to `write-shape-text!`.
Fully portable (no store/DOM/React), so it runs under Node too."
(:require
[app.common.data :as d]
[app.common.types.fills.impl :as types.fills.impl]
[app.common.uuid :as uuid]
[app.render-wasm.helpers :as h]
[app.render-wasm.mem :as mem]
[app.render-wasm.serializers :as sr]
[app.render-wasm.wasm :as wasm]
[cuerdas.core :as str]))
(def ^:const PARAGRAPH-ATTR-U8-SIZE 12)
(def ^:const SPAN-ATTR-U8-SIZE 64)
(def ^:const MAX-TEXT-FILLS types.fills.impl/MAX-FILLS)
(def ^:private default-font-size 14)
(def ^:private default-line-height 1.2)
(def ^:private default-letter-spacing 0.0)
;; --- pure attribute serializers -------------------------------------------
(defn serialize-font-size
[font-size]
(cond
(number? font-size) font-size
(string? font-size) (or (d/parse-double font-size) default-font-size)
:else default-font-size))
(defn serialize-font-weight
[font-weight]
(if (number? font-weight)
font-weight
(let [font-weight-str (str font-weight)]
(cond
(re-matches #"\d+" font-weight-str) (js/Number font-weight-str)
(str/includes? font-weight-str "bold") 700
(str/includes? font-weight-str "black") 900
(str/includes? font-weight-str "extrabold") 800
(str/includes? font-weight-str "extralight") 200
(str/includes? font-weight-str "light") 300
(str/includes? font-weight-str "medium") 500
(str/includes? font-weight-str "semibold") 600
(str/includes? font-weight-str "thin") 100
:else 400))))
(defn serialize-line-height
([line-height] (serialize-line-height line-height default-line-height))
([line-height default-value]
(cond
(number? line-height) line-height
(string? line-height) (or (d/parse-double line-height) default-value)
:else default-value)))
(defn serialize-letter-spacing
[letter-spacing]
(cond
(number? letter-spacing) letter-spacing
(string? letter-spacing) (or (d/parse-double letter-spacing) default-letter-spacing)
:else default-letter-spacing))
;; --- binary writers --------------------------------------------------------
(defn- encode-text
"Into an UTF8 buffer. Returns an ArrayBuffer instance."
[text]
(let [encoder (js/TextEncoder.)]
(.encode encoder text)))
(defn- write-span-fills
[offset dview fills]
(let [new-ofset (reduce (fn [offset fill]
(let [opacity (get fill :fill-opacity 1.0)
color (get fill :fill-color)
gradient (get fill :fill-color-gradient)
image (get fill :fill-image)]
(cond
(some? color)
(types.fills.impl/write-solid-fill offset dview opacity color)
(some? gradient)
(types.fills.impl/write-gradient-fill offset dview opacity gradient)
(some? image)
(types.fills.impl/write-image-fill offset dview opacity image))))
offset
fills)
padding-fills (max 0 (- MAX-TEXT-FILLS (count fills)))]
(+ new-ofset (* padding-fills types.fills.impl/FILL-U8-SIZE))))
(defn- write-paragraph
[offset dview paragraph]
(let [text-align (sr/translate-text-align (get paragraph :text-align))
text-direction (sr/translate-text-direction (get paragraph :text-direction))
text-decoration (sr/translate-text-decoration (get paragraph :text-decoration))
text-transform (sr/translate-text-transform (get paragraph :text-transform))
line-height (serialize-line-height (get paragraph :line-height))
letter-spacing (serialize-letter-spacing (get paragraph :letter-spacing))]
(-> offset
(mem/write-u8 dview text-align)
(mem/write-u8 dview text-direction)
(mem/write-u8 dview text-decoration)
(mem/write-u8 dview text-transform)
(mem/write-f32 dview line-height)
(mem/write-f32 dview letter-spacing)
(mem/assert-written offset PARAGRAPH-ATTR-U8-SIZE))))
(defn- write-spans
[offset dview spans paragraph normalize-font-id]
(let [paragraph-font-size (get paragraph :font-size)
paragraph-font-weight (-> paragraph :font-weight serialize-font-weight)
paragraph-line-height (serialize-line-height (get paragraph :line-height))]
(reduce
(fn [offset span]
(let [font-style (sr/translate-font-style (get span :font-style "normal"))
font-size (serialize-font-size (get span :font-size paragraph-font-size))
line-height (serialize-line-height (get span :line-height) paragraph-line-height)
letter-spacing (serialize-letter-spacing (get span :letter-spacing))
font-weight (serialize-font-weight (get span :font-weight paragraph-font-weight))
font-id (normalize-font-id (get span :font-id "sourcesanspro"))
font-family (hash (get span :font-family "sourcesanspro"))
text-buffer (encode-text (get span :text ""))
text-length (mem/size text-buffer)
fills (take MAX-TEXT-FILLS (get span :fills []))
font-variant-id (get span :font-variant-id)
font-variant-id (if (uuid? font-variant-id) font-variant-id uuid/zero)
text-decoration (or (sr/translate-text-decoration (:text-decoration span))
(sr/translate-text-decoration (:text-decoration paragraph))
(sr/translate-text-decoration "none"))
text-transform (or (sr/translate-text-transform (:text-transform span))
(sr/translate-text-transform (:text-transform paragraph))
(sr/translate-text-transform "none"))
text-direction (or (sr/translate-text-direction (:text-direction span))
(sr/translate-text-direction (:text-direction paragraph))
(sr/translate-text-direction "ltr"))]
(-> offset
(mem/write-u8 dview font-style)
(mem/write-u8 dview text-decoration)
(mem/write-u8 dview text-transform)
(mem/write-u8 dview text-direction)
(mem/write-f32 dview font-size)
(mem/write-f32 dview line-height)
(mem/write-f32 dview letter-spacing)
(mem/write-u32 dview font-weight)
(mem/write-uuid dview font-id)
(mem/write-i32 dview font-family)
(mem/write-uuid dview (d/nilv font-variant-id uuid/zero))
(mem/write-i32 dview text-length)
(mem/write-i32 dview (count fills))
(mem/assert-written offset SPAN-ATTR-U8-SIZE)
(write-span-fills dview fills))))
offset
spans)))
(defn write-shape-text!
"Writes one paragraph's spans + text into WASM and appends it to the current
shape via `_set_shape_text_content`.
`opts` injects host-specific font resolution:
- `:normalize-font-id` (string font-id -> wasm uuid) — required in practice,
- `:normalize-paragraph`/`:normalize-span` — font-variant normalization from a
fonts DB (workspace); default to identity (the exporter resolves variants
differently / not at all)."
[spans paragraph text {:keys [normalize-font-id normalize-paragraph normalize-span]
:or {normalize-font-id identity
normalize-paragraph identity
normalize-span (fn [span _paragraph] span)}}]
(let [paragraph (normalize-paragraph paragraph)
spans (map #(normalize-span % paragraph) spans)
num-spans (count spans)
fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS)
metadata-size (+ PARAGRAPH-ATTR-U8-SIZE
(* num-spans (+ SPAN-ATTR-U8-SIZE fills-size)))
text-buffer (encode-text text)
text-size (mem/size text-buffer)
total-size (+ 4 metadata-size text-size)
heapu8 (mem/get-heap-u8)
dview (mem/get-data-view)
offset (mem/alloc total-size)]
(-> offset
(mem/write-u32 dview num-spans)
(write-paragraph dview paragraph)
(write-spans dview spans paragraph normalize-font-id)
(mem/write-buffer heapu8 text-buffer))
(h/call wasm/internal-module "_set_shape_text_content")))