diff --git a/exporter/src/app/browser.cljs b/exporter/src/app/browser.cljs index 5a76b109b6..a9b3f0ed01 100644 --- a/exporter/src/app/browser.cljs +++ b/exporter/src/app/browser.cljs @@ -60,6 +60,35 @@ :cause (ex-message cause)) (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 ([locator] (wait-for locator nil)) ([locator {:keys [state timeout] :or {state "visible" timeout 10000}}] diff --git a/exporter/src/app/renderer/bitmap.cljs b/exporter/src/app/renderer/bitmap.cljs index 3579a4839e..f4101b815f 100644 --- a/exporter/src/app/renderer/bitmap.cljs +++ b/exporter/src/app/renderer/bitmap.cljs @@ -48,6 +48,7 @@ (bw/nav! page (str uri)) (bw/sleep page 1000) ; the good old fix with sleep (bw/wait-for-fonts page) + (bw/wait-for-images page) (bw/eval! page (js* "() => document.body.style.background = 'transparent'")) ;; take the screnshot of requested objects, one by one @@ -59,6 +60,7 @@ :share-id share-id :object-id (mapv :id objects) :route "objects" + :render-embed true :skip-children skip-children :wasm (when is-wasm "true") :scale scale} diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 7c6ee76e1e..95eba93c4a 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -661,17 +661,21 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- get-image-data [shape] - (cond - (= :image (:type shape)) - [(:metadata shape)] + (let [fill-images (->> (:fills shape) + (keep :fill-image))] + (cond + (= :image (:type shape)) + (cond-> [(:metadata shape)] + (seq fill-images) (into fill-images)) - (some? (:fill-image shape)) - [(:fill-image shape)] + (some? (: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] (let [images (->> objects (vals) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index c1be67a728..c62bcf1905 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -44,7 +44,8 @@ height (dm/get-prop selrect :height) has-image? (or (some? metadata) - (some? image)) + (some? image) + (some :fill-image fills)) uri (cond (some? metadata) @@ -53,7 +54,7 @@ (some? image) (cf/resolve-file-media image)) - uris (into [uri] + uris (into (cond-> [] (some? uri) (conj uri)) (comp (keep :fill-image) (map cf/resolve-file-media)) @@ -129,19 +130,22 @@ [:> :image image-props]) [:> :rect props]))) - (when ^boolean has-image? + (when (some? uri) [: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 + ;; + ;; This padding image only applies to legacy/top-level shape images. + ;; Nested fill images already render inside the fill loop above; emitting + ;; an extra without a href can break SVG pattern rasterization + ;; during bitmap export. [:rect {:x 0 :y 0 :width (* width no-repeat-padding) :height (* height no-repeat-padding) :fill "none"}] - [:image {:href uri + [:image {:href (get embed uri uri) :preserveAspectRatio "none" - :x 0 - :y 0 :width width :height height}]])]])]))) diff --git a/frontend/src/app/render.cljs b/frontend/src/app/render.cljs index 99dfed871a..e201479019 100644 --- a/frontend/src/app/render.cljs +++ b/frontend/src/app/render.cljs @@ -144,8 +144,12 @@ (rx/observe-on :async) (rx/map (comp :objects second)) (rx/map (fn [objects] - (let [objects (render/adapt-objects-for-shape objects object-id)] - #(assoc % :objects objects))))))))) + (render/adapt-objects-for-shape objects object-id))) + (rx/merge-map (fn [objects] + (rx/concat + (->> (render/populate-images-cache objects) + (rx/ignore)) + (rx/of #(assoc % :objects objects)))))))))) (def ^:private schema:render-objects [:map {:title "render-objets"} @@ -153,6 +157,7 @@ [:file-id ::sm/uuid] [:share-id {:optional true} ::sm/uuid] [:embed {:optional true} :boolean] + [:render-embed {:optional true} :boolean] [:skip-children {:optional true} :boolean] [:object-id [:or [::sm/set ::sm/uuid] ::sm/uuid]]]) @@ -168,8 +173,9 @@ (defn- render-objects [params] (try - (let [{:keys [file-id page-id embed share-id object-id skip-children wasm scale] :as params} - (coerce-render-objects-params 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) + 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)) (if (uuid? object-id) (mf/html diff --git a/frontend/test/frontend_tests/render_export_images_test.cljs b/frontend/test/frontend_tests/render_export_images_test.cljs new file mode 100644 index 0000000000..505753c14d --- /dev/null +++ b/frontend/test/frontend_tests/render_export_images_test.cljs @@ -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))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index d7048409a8..342c5d80e4 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -28,6 +28,7 @@ [frontend-tests.plugins.parser-test] [frontend-tests.plugins.tokens-test] [frontend-tests.plugins.utils-test] + [frontend-tests.render-export-images-test] [frontend-tests.svg-fills-test] [frontend-tests.tokens.import-export-test] [frontend-tests.tokens.logic.token-actions-test] @@ -76,6 +77,7 @@ frontend-tests.plugins.parser-test frontend-tests.plugins.tokens-test frontend-tests.plugins.utils-test + frontend-tests.render-export-images-test frontend-tests.svg-fills-test frontend-tests.tokens.import-export-test frontend-tests.tokens.logic.token-actions-test