mirror of
https://github.com/penpot/penpot.git
synced 2026-08-30 08:39:19 +00:00
🐛 Preload fill images for bitmap exports
This commit is contained in:
parent
53a4d2a18a
commit
d97b95f291
@ -60,6 +60,35 @@
|
|||||||
:cause (ex-message cause))
|
:cause (ex-message cause))
|
||||||
(p/resolved nil))))))
|
(p/resolved nil))))))
|
||||||
|
|
||||||
|
(defn wait-for-images
|
||||||
|
"Wait until HTML and SVG images currently present in the page are loadable."
|
||||||
|
([page] (wait-for-images page nil))
|
||||||
|
([page {:keys [timeout] :or {timeout 15000}}]
|
||||||
|
(-> (.evaluate ^js page
|
||||||
|
(js* "() => {
|
||||||
|
const hrefOf = (node) => {
|
||||||
|
if (node.currentSrc) return node.currentSrc;
|
||||||
|
if (node.src) return node.src;
|
||||||
|
const href = node.getAttribute && (node.getAttribute('href') || node.getAttribute('xlink:href'));
|
||||||
|
return href || '';
|
||||||
|
};
|
||||||
|
const urls = Array.from(document.querySelectorAll('img, image'))
|
||||||
|
.map(hrefOf)
|
||||||
|
.filter((href) => href && !href.startsWith('data:'));
|
||||||
|
return Promise.all(urls.map((url) => new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = () => resolve(true);
|
||||||
|
img.onerror = () => resolve(false);
|
||||||
|
img.src = url;
|
||||||
|
}))).then(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
||||||
|
}"))
|
||||||
|
(p/timeout timeout)
|
||||||
|
(p/catch (fn [cause]
|
||||||
|
(l/warn :hint "wait-for-images timed out; continuing anyway"
|
||||||
|
:cause (ex-message cause))
|
||||||
|
(p/resolved nil))))))
|
||||||
|
|
||||||
(defn wait-for
|
(defn wait-for
|
||||||
([locator] (wait-for locator nil))
|
([locator] (wait-for locator nil))
|
||||||
([locator {:keys [state timeout] :or {state "visible" timeout 10000}}]
|
([locator {:keys [state timeout] :or {state "visible" timeout 10000}}]
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
(bw/nav! page (str uri))
|
(bw/nav! page (str uri))
|
||||||
(bw/sleep page 1000) ; the good old fix with sleep
|
(bw/sleep page 1000) ; the good old fix with sleep
|
||||||
(bw/wait-for-fonts page)
|
(bw/wait-for-fonts page)
|
||||||
|
(bw/wait-for-images page)
|
||||||
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
(bw/eval! page (js* "() => document.body.style.background = 'transparent'"))
|
||||||
|
|
||||||
;; take the screnshot of requested objects, one by one
|
;; take the screnshot of requested objects, one by one
|
||||||
@ -59,6 +60,7 @@
|
|||||||
:share-id share-id
|
:share-id share-id
|
||||||
:object-id (mapv :id objects)
|
:object-id (mapv :id objects)
|
||||||
:route "objects"
|
:route "objects"
|
||||||
|
:render-embed true
|
||||||
:skip-children skip-children
|
:skip-children skip-children
|
||||||
:wasm (when is-wasm "true")
|
:wasm (when is-wasm "true")
|
||||||
:scale scale}
|
:scale scale}
|
||||||
|
|||||||
@ -661,17 +661,21 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- get-image-data [shape]
|
(defn- get-image-data [shape]
|
||||||
(cond
|
(let [fill-images (->> (:fills shape)
|
||||||
(= :image (:type shape))
|
(keep :fill-image))]
|
||||||
[(:metadata shape)]
|
(cond
|
||||||
|
(= :image (:type shape))
|
||||||
|
(cond-> [(:metadata shape)]
|
||||||
|
(seq fill-images) (into fill-images))
|
||||||
|
|
||||||
(some? (:fill-image shape))
|
(some? (:fill-image shape))
|
||||||
[(:fill-image shape)]
|
(cond-> [(:fill-image shape)]
|
||||||
|
(seq fill-images) (into fill-images))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
[]))
|
(vec fill-images))))
|
||||||
|
|
||||||
(defn- populate-images-cache
|
(defn populate-images-cache
|
||||||
[objects]
|
[objects]
|
||||||
(let [images (->> objects
|
(let [images (->> objects
|
||||||
(vals)
|
(vals)
|
||||||
|
|||||||
@ -44,7 +44,8 @@
|
|||||||
height (dm/get-prop selrect :height)
|
height (dm/get-prop selrect :height)
|
||||||
|
|
||||||
has-image? (or (some? metadata)
|
has-image? (or (some? metadata)
|
||||||
(some? image))
|
(some? image)
|
||||||
|
(some :fill-image fills))
|
||||||
|
|
||||||
uri (cond
|
uri (cond
|
||||||
(some? metadata)
|
(some? metadata)
|
||||||
@ -53,7 +54,7 @@
|
|||||||
(some? image)
|
(some? image)
|
||||||
(cf/resolve-file-media image))
|
(cf/resolve-file-media image))
|
||||||
|
|
||||||
uris (into [uri]
|
uris (into (cond-> [] (some? uri) (conj uri))
|
||||||
(comp
|
(comp
|
||||||
(keep :fill-image)
|
(keep :fill-image)
|
||||||
(map cf/resolve-file-media))
|
(map cf/resolve-file-media))
|
||||||
@ -129,19 +130,22 @@
|
|||||||
[:> :image image-props])
|
[:> :image image-props])
|
||||||
[:> :rect props])))
|
[:> :rect props])))
|
||||||
|
|
||||||
(when ^boolean has-image?
|
(when (some? uri)
|
||||||
[:g
|
[:g
|
||||||
;; We add this shape to add a padding so the patter won't repeat
|
;; We add this shape to add a padding so the pattern won't repeat
|
||||||
;; Issue: https://tree.taiga.io/project/penpot/issue/5583
|
;; Issue: https://tree.taiga.io/project/penpot/issue/5583
|
||||||
|
;;
|
||||||
|
;; This padding image only applies to legacy/top-level shape images.
|
||||||
|
;; Nested fill images already render inside the fill loop above; emitting
|
||||||
|
;; an extra <image> without a href can break SVG pattern rasterization
|
||||||
|
;; during bitmap export.
|
||||||
[:rect {:x 0
|
[:rect {:x 0
|
||||||
:y 0
|
:y 0
|
||||||
:width (* width no-repeat-padding)
|
:width (* width no-repeat-padding)
|
||||||
:height (* height no-repeat-padding)
|
:height (* height no-repeat-padding)
|
||||||
:fill "none"}]
|
:fill "none"}]
|
||||||
[:image {:href uri
|
[:image {:href (get embed uri uri)
|
||||||
:preserveAspectRatio "none"
|
:preserveAspectRatio "none"
|
||||||
:x 0
|
|
||||||
:y 0
|
|
||||||
:width width
|
:width width
|
||||||
:height height}]])]])])))
|
:height height}]])]])])))
|
||||||
|
|
||||||
|
|||||||
@ -144,8 +144,12 @@
|
|||||||
(rx/observe-on :async)
|
(rx/observe-on :async)
|
||||||
(rx/map (comp :objects second))
|
(rx/map (comp :objects second))
|
||||||
(rx/map (fn [objects]
|
(rx/map (fn [objects]
|
||||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
(render/adapt-objects-for-shape objects object-id)))
|
||||||
#(assoc % :objects objects)))))))))
|
(rx/merge-map (fn [objects]
|
||||||
|
(rx/concat
|
||||||
|
(->> (render/populate-images-cache objects)
|
||||||
|
(rx/ignore))
|
||||||
|
(rx/of #(assoc % :objects objects))))))))))
|
||||||
|
|
||||||
(def ^:private schema:render-objects
|
(def ^:private schema:render-objects
|
||||||
[:map {:title "render-objets"}
|
[:map {:title "render-objets"}
|
||||||
@ -153,6 +157,7 @@
|
|||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:share-id {:optional true} ::sm/uuid]
|
[:share-id {:optional true} ::sm/uuid]
|
||||||
[:embed {:optional true} :boolean]
|
[:embed {:optional true} :boolean]
|
||||||
|
[:render-embed {:optional true} :boolean]
|
||||||
[:skip-children {:optional true} :boolean]
|
[:skip-children {:optional true} :boolean]
|
||||||
[:object-id
|
[:object-id
|
||||||
[:or [::sm/set ::sm/uuid] ::sm/uuid]]])
|
[:or [::sm/set ::sm/uuid] ::sm/uuid]]])
|
||||||
@ -168,8 +173,9 @@
|
|||||||
(defn- render-objects
|
(defn- render-objects
|
||||||
[params]
|
[params]
|
||||||
(try
|
(try
|
||||||
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm scale] :as params}
|
(let [{:keys [file-id page-id embed render-embed share-id object-id skip-children wasm scale] :as params}
|
||||||
(coerce-render-objects-params params)]
|
(coerce-render-objects-params params)
|
||||||
|
embed (if (some? render-embed) render-embed embed)]
|
||||||
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
||||||
(if (uuid? object-id)
|
(if (uuid? object-id)
|
||||||
(mf/html
|
(mf/html
|
||||||
|
|||||||
30
frontend/test/frontend_tests/render_export_images_test.cljs
Normal file
30
frontend/test/frontend_tests/render_export_images_test.cljs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
;; 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 frontend-tests.render-export-images-test
|
||||||
|
(:require
|
||||||
|
[app.main.render :as render]
|
||||||
|
[cljs.test :refer [deftest is testing]]))
|
||||||
|
|
||||||
|
(def image-a
|
||||||
|
{:id #uuid "00000000-0000-0000-0000-000000000001"
|
||||||
|
:name "image-a"})
|
||||||
|
|
||||||
|
(def image-b
|
||||||
|
{:id #uuid "00000000-0000-0000-0000-000000000002"
|
||||||
|
:name "image-b"})
|
||||||
|
|
||||||
|
(deftest export-image-cache-includes-images-from-fills-vector
|
||||||
|
(let [shape {:type :rect
|
||||||
|
:fills [{:fill-color "#ffffff"
|
||||||
|
:fill-opacity 1}
|
||||||
|
{:fill-opacity 1
|
||||||
|
:fill-image image-a}
|
||||||
|
{:fill-opacity 0.5
|
||||||
|
:fill-image image-b}]}
|
||||||
|
images (#'render/get-image-data shape)]
|
||||||
|
(testing "all image fills are preloaded before static export markup is rendered"
|
||||||
|
(is (= [image-a image-b] images)))))
|
||||||
@ -28,6 +28,7 @@
|
|||||||
[frontend-tests.plugins.parser-test]
|
[frontend-tests.plugins.parser-test]
|
||||||
[frontend-tests.plugins.tokens-test]
|
[frontend-tests.plugins.tokens-test]
|
||||||
[frontend-tests.plugins.utils-test]
|
[frontend-tests.plugins.utils-test]
|
||||||
|
[frontend-tests.render-export-images-test]
|
||||||
[frontend-tests.svg-fills-test]
|
[frontend-tests.svg-fills-test]
|
||||||
[frontend-tests.tokens.import-export-test]
|
[frontend-tests.tokens.import-export-test]
|
||||||
[frontend-tests.tokens.logic.token-actions-test]
|
[frontend-tests.tokens.logic.token-actions-test]
|
||||||
@ -76,6 +77,7 @@
|
|||||||
frontend-tests.plugins.parser-test
|
frontend-tests.plugins.parser-test
|
||||||
frontend-tests.plugins.tokens-test
|
frontend-tests.plugins.tokens-test
|
||||||
frontend-tests.plugins.utils-test
|
frontend-tests.plugins.utils-test
|
||||||
|
frontend-tests.render-export-images-test
|
||||||
frontend-tests.svg-fills-test
|
frontend-tests.svg-fills-test
|
||||||
frontend-tests.tokens.import-export-test
|
frontend-tests.tokens.import-export-test
|
||||||
frontend-tests.tokens.logic.token-actions-test
|
frontend-tests.tokens.logic.token-actions-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user