0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-12 15:51:37 -05:00
penpot/frontend/src/app/rasterizer.cljs
2023-11-16 23:07:00 +01:00

264 lines
8.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
(ns app.rasterizer
"A main entry point for the rasterizer process that is
executed on a separated iframe."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as log]
[app.config :as cf]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.object :as obj]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cuerdas.core :as str]))
(log/set-level! :trace)
(declare send-success!)
(declare send-failure!)
(defonce parent-origin
(dm/str cf/public-uri))
(defn- get-document-element
[^js svg]
(.-documentElement svg))
(defn- create-image
[uri]
(rx/create
(fn [subs]
(let [image (js/Image.)]
(obj/set! image "onload" #(do
(rx/push! subs image)
(rx/end! subs)))
(obj/set! image "crossOrigin" "anonymous")
(obj/set! image "onerror" #(rx/error! subs %))
(obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal
:code :abort
:hint "operation aborted")))
(obj/set! image "src" uri)
(fn []
(obj/set! image "src" "")
(obj/set! image "onload" nil)
(obj/set! image "onerror" nil)
(obj/set! image "onabort" nil))))))
(defn- svg-get-size
[svg max]
(let [doc (get-document-element svg)
vbox (dom/get-attribute doc "viewBox")]
(when (string? vbox)
(let [[_ _ width height] (str/split vbox #"\s+")
width (d/parse-integer width 0)
height (d/parse-integer height 0)
ratio (/ width height)]
(if (> width height)
[max (* max (/ 1 ratio))]
[(* max ratio) max])))))
(defn- svg-has-intrinsic-size?
"Returns true if the SVG has an intrinsic size."
[svg]
(let [doc (get-document-element svg)
width (dom/get-attribute doc "width")
height (dom/get-attribute doc "height")]
(d/num? width height)))
(defn- svg-set-intrinsic-size!
"Sets the intrinsic size of an SVG to the given max size."
[^js svg max]
(when-not (svg-has-intrinsic-size? svg)
(let [doc (get-document-element svg)
[w h] (svg-get-size svg max)]
(dom/set-attribute! doc "width" (dm/str w))
(dom/set-attribute! doc "height" (dm/str h))))
svg)
(defn- fetch-as-data-uri
"Fetches a URL as a Data URI."
[uri]
(->> (http/send! {:uri uri
:response-type :blob
:method :get
:mode :cors
:omit-default-headers true})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)))
(defn- svg-update-image!
"Updates an image in an SVG to a Data URI."
[image]
(if-let [href (dom/get-attribute image "href")]
(if (str/starts-with? href "data:")
(rx/of image)
(->> (fetch-as-data-uri href)
(rx/map (fn [url]
(dom/set-attribute! image "href" url)
image))))
(rx/empty)))
(defn- svg-resolve-images!
"Resolves all images in an SVG to Data URIs."
[svg]
(->> (rx/from (dom/query-all svg "image"))
(rx/mapcat svg-update-image!)
(rx/ignore)))
(defn- svg-add-style!
"Adds a <style> node to an SVG."
[svg styles]
(let [doc (get-document-element svg)
style (dom/create-element svg "http://www.w3.org/2000/svg" "style")]
(dom/append-child! style (dom/create-text svg styles))
(dom/append-child! doc style)))
(defn- svg-resolve-styles!
"Resolves all fonts in an SVG to Data URIs."
[svg styles]
(->> (rx/from (re-seq #"url\((https?://[^)]+)\)" styles))
(rx/map second)
(rx/mapcat (fn [url]
(->> (fetch-as-data-uri url)
(rx/map (fn [uri] [url uri])))))
(rx/reduce (fn [styles [url uri]]
(str/replace styles url uri))
styles)
(rx/tap (partial svg-add-style! svg))
(rx/ignore)))
(defn- svg-resolve-all!
"Resolves all images and fonts in an SVG to Data URIs."
[svg styles]
(rx/concat
(svg-resolve-images! svg)
(svg-resolve-styles! svg styles)
(rx/of svg)))
(defn- svg-parse
"Parses an SVG string into an SVG DOM."
[data]
(let [parser (js/DOMParser.)]
(.parseFromString ^js parser data "image/svg+xml")))
(defn- svg-stringify
"Converts an SVG to a string."
[svg]
(let [doc (get-document-element svg)
serializer (js/XMLSerializer.)]
(.serializeToString ^js serializer doc)))
(defn- svg-prepare
"Prepares an SVG for rendering (resolves images to Data URIs and adds intrinsic size)."
[data styles width]
(let [svg (svg-parse data)]
(->> (svg-resolve-all! svg styles)
(rx/map #(svg-set-intrinsic-size! % width))
(rx/map svg-stringify))))
(defn- bitmap->blob
"Converts an ImageBitmap to a Blob."
[bitmap]
(rx/create
(fn [subs]
(let [canvas (dom/create-element "canvas")]
(set! (.-width ^js canvas) (.-width ^js bitmap))
(set! (.-height ^js canvas) (.-height ^js bitmap))
(let [context (.getContext ^js canvas "bitmaprenderer")]
(.transferFromImageBitmap ^js context bitmap)
(.toBlob canvas #(do (rx/push! subs %)
(rx/end! subs))))
(constantly nil)))))
(defn- render-image-bitmap
"Renders a thumbnail using it's SVG and returns an ImageBitmap of the image."
[payload]
(let [data (unchecked-get payload "data")
styles (unchecked-get payload "styles")
width (d/nilv (unchecked-get payload "width") 300)
quality (d/nilv (unchecked-get payload "quality") "medium")]
(->> (svg-prepare data styles width)
(rx/map #(wapi/create-blob % "image/svg+xml"))
(rx/map wapi/create-uri)
(rx/mapcat (fn [uri]
(->> (create-image uri)
(rx/mapcat #(wapi/create-image-bitmap % #js {:resizeWidth width
:resizeQuality quality}))
(rx/tap #(wapi/revoke-uri uri))))))))
(defn- render-blob
"Renders a thumbnail using it's SVG and returns a Blob of the image."
[payload]
(->> (render-image-bitmap payload)
(rx/mapcat bitmap->blob)))
(defn- render
"Renders a thumbnail and returns a stream."
[payload]
(let [result (d/nilv (unchecked-get payload "result") "blob")]
(case result
"image-bitmap" (render-image-bitmap payload)
(render-blob payload))))
(defn- on-message
"Handles messages from the main thread."
[event]
(let [evdata (unchecked-get event "data")
evorigin (unchecked-get event "origin")]
(when (str/starts-with? parent-origin evorigin)
(let [id (unchecked-get evdata "id")
payload (unchecked-get evdata "payload")
scope (unchecked-get evdata "scope")]
(when (and (some? payload)
(= scope "penpot/rasterizer"))
(->> (render payload)
(rx/subs (partial send-success! id)
(partial send-failure! id))))))))
(defn- listen
"Initializes the listener for messages from the main thread."
[]
(.addEventListener js/window "message" on-message))
(defn- send-answer!
"Sends an answer message."
[id type payload]
(let [message #js {:id id
:type type
:scope "penpot/rasterizer"
:payload payload}]
(when-not (identical? js/window js/parent)
(.postMessage js/parent message parent-origin))))
(defn- send-success!
"Sends a success message."
[id payload]
(send-answer! id "success" payload))
(defn- send-failure!
"Sends a failure message."
[id cause]
(send-answer! id "failure" (ex-message cause)))
(defn- send-ready!
"Sends a ready message."
[]
(send-answer! nil "ready" nil))
(defn ^:export init
[]
(listen)
(send-ready!)
(log/info :hint "initialized"
:public-uri (dm/str cf/public-uri)
:parent-uri (dm/str parent-origin)))