mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 19:59:54 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29345fd381 | ||
|
|
8504bb58bf | ||
|
|
aabf1b0bdb | ||
|
|
54c692d10c | ||
|
|
b59b259a77 | ||
|
|
c5a29e27a6 |
No files matched your search
@@ -15,6 +15,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.migrations :as-alias fmg]
|
||||
[app.common.files.shape-compact :as fsc]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.media :as cmedia]
|
||||
@@ -45,6 +46,7 @@
|
||||
java.io.InputStream
|
||||
java.io.OutputStreamWriter
|
||||
java.lang.AutoCloseable
|
||||
java.nio.charset.StandardCharsets
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipFile
|
||||
java.util.zip.ZipOutputStream))
|
||||
@@ -217,6 +219,16 @@
|
||||
(.flush writer))
|
||||
(.closeEntry output))
|
||||
|
||||
(defn- write-compact-entry!
|
||||
[^ZipOutputStream output ^String path data]
|
||||
(.putNextEntry output (ZipEntry. path))
|
||||
(let [sw (java.io.StringWriter.)]
|
||||
(json/write sw data :indent false :key-fn json/write-camel-key)
|
||||
(.flush sw)
|
||||
(let [^bytes bytes (.getBytes ^String (str sw) StandardCharsets/UTF_8)]
|
||||
(.write output bytes 0 (alength bytes))))
|
||||
(.closeEntry output))
|
||||
|
||||
(defn- get-file
|
||||
[{:keys [::bfc/embed-assets ::bfc/include-libraries] :as cfg} file-id]
|
||||
|
||||
@@ -302,23 +314,29 @@
|
||||
|
||||
(doseq [[index page-id] (d/enumerate pages)]
|
||||
|
||||
(let [path (str "files/" file-id "/pages/" page-id ".json")
|
||||
page (get pages-index page-id)
|
||||
objects (:objects page)
|
||||
page (-> page
|
||||
(dissoc :objects)
|
||||
(assoc :index index))
|
||||
page (encode-page page)]
|
||||
|
||||
(write-entry! output path page)
|
||||
|
||||
(events/tap :progress {:section :page :id page-id :name (:name page) :file-id file-id})
|
||||
|
||||
(doseq [[shape-id shape] objects]
|
||||
(let [path (str "files/" file-id "/pages/" page-id "/" shape-id ".json")
|
||||
shape (assoc shape :page-id page-id)
|
||||
shape (encode-shape shape)]
|
||||
(write-entry! output path shape)))))
|
||||
(let [page (get pages-index page-id)
|
||||
objects (:objects page)]
|
||||
(if (contains? cf/flags :binfile-v3-compact)
|
||||
(let [path (str "files/" file-id "/pages/" page-id ".json")
|
||||
objects (d/update-vals objects
|
||||
(fn [shape]
|
||||
(-> shape fsc/compact-shape fsc/round-values encode-shape)))
|
||||
page (-> page
|
||||
(assoc :objects objects :index index)
|
||||
(dissoc :options))]
|
||||
(events/tap :progress {:section :page :id page-id :name (:name page) :file-id file-id})
|
||||
(write-compact-entry! output path page))
|
||||
(let [path (str "files/" file-id "/pages/" page-id ".json")
|
||||
page (-> page
|
||||
(dissoc :objects)
|
||||
(assoc :index index))
|
||||
page (encode-page page)]
|
||||
(write-entry! output path page)
|
||||
(events/tap :progress {:section :page :id page-id :name (:name page) :file-id file-id})
|
||||
(doseq [[shape-id shape] objects]
|
||||
(let [path (str "files/" file-id "/pages/" page-id "/" shape-id ".json")
|
||||
shape (encode-shape shape)]
|
||||
(write-entry! output path shape)))))))
|
||||
|
||||
(vswap! bfc/*state* bfc/collect-storage-objects media)
|
||||
(vswap! bfc/*state* bfc/collect-storage-objects thumbnails)
|
||||
@@ -372,11 +390,12 @@
|
||||
|
||||
(defn- export-files
|
||||
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
|
||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
rels (if include-libraries
|
||||
(->> (bfc/get-files-rels cfg ids)
|
||||
(mapv (juxt :file-id :library-file-id)))
|
||||
[])]
|
||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
rels (if include-libraries
|
||||
(->> (bfc/get-files-rels cfg ids)
|
||||
(mapv (juxt :file-id :library-file-id)))
|
||||
[])
|
||||
compact? (contains? cf/flags :binfile-v3-compact)]
|
||||
|
||||
(vswap! bfc/*state* assoc :files (d/ordered-map))
|
||||
|
||||
@@ -390,7 +409,7 @@
|
||||
;; Write manifest file
|
||||
(let [files (:files @bfc/*state*)
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
:version (if compact? 2 1)
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:refer "penpot"
|
||||
:files (vec (vals files))
|
||||
@@ -686,7 +705,7 @@
|
||||
(not-empty)))
|
||||
|
||||
(defn- read-file-pages
|
||||
[{:keys [::bfc/input ::entries] :as cfg} file-id]
|
||||
[{:keys [::bfc/input ::entries ::compact?] :as cfg} file-id]
|
||||
(->> (keep (match-page-entry-fn file-id) entries)
|
||||
(keep (fn [{:keys [id entry]}]
|
||||
(let [page (->> (read-entry input entry)
|
||||
@@ -694,8 +713,17 @@
|
||||
page (dissoc page :options)]
|
||||
(events/tap :progress {:section :page :id id :file-id file-id})
|
||||
(when (= id (:id page))
|
||||
(let [objects (read-file-shapes cfg file-id id)]
|
||||
(assoc page :objects objects))))))
|
||||
(if compact?
|
||||
(let [objects (d/update-vals (:objects page)
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(bfl/clean-shape-pre-decode)
|
||||
(decode-shape)
|
||||
(fsc/expand-shape)
|
||||
(bfl/clean-shape-post-decode))))]
|
||||
(assoc page :objects objects))
|
||||
(let [objects (read-file-shapes cfg file-id id)]
|
||||
(assoc page :objects objects)))))))
|
||||
(sort-by :index)
|
||||
(reduce (fn [result {:keys [id] :as page}]
|
||||
(assoc result id (dissoc page :index)))
|
||||
@@ -937,10 +965,12 @@
|
||||
|
||||
(let [manifest (-> (read-manifest input)
|
||||
(validate-manifest))
|
||||
compact? (= 2 (:version manifest))
|
||||
entries (read-zip-entries input)
|
||||
cfg (-> cfg
|
||||
(assoc ::entries entries)
|
||||
(assoc ::manifest manifest)
|
||||
(assoc ::compact? compact?)
|
||||
(assoc ::bfc/timestamp timestamp))]
|
||||
|
||||
(when-not (= "penpot/export-files" (:type manifest))
|
||||
|
||||
@@ -98,14 +98,13 @@
|
||||
(defn get-file-data-for-thumbnail
|
||||
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file} strip-frames-with-thumbnails]
|
||||
(letfn [;; function responsible on finding the frame marked to be
|
||||
;; used as thumbnail; the returned frame always have
|
||||
;; the :page-id set to the page that it belongs.
|
||||
;; used as thumbnail; returns a [frame page-id] pair.
|
||||
(get-thumbnail-frame [{:keys [data]}]
|
||||
(d/seek #(or (:use-for-thumbnail %)
|
||||
(:use-for-thumbnail? %)) ; NOTE: backward comp (remove on v1.21)
|
||||
(d/seek (fn [[frame]] (or (:use-for-thumbnail frame)
|
||||
(:use-for-thumbnail? frame))) ; NOTE: backward comp (remove on v1.21)
|
||||
(for [page (-> data :pages-index vals)
|
||||
frame (-> page :objects ctt/get-frames)]
|
||||
(assoc frame :page-id (:id page)))))
|
||||
[frame (:id page)])))
|
||||
|
||||
;; function responsible to filter objects data structure of
|
||||
;; all unneeded shapes if a concrete frame is provided. If no
|
||||
@@ -152,9 +151,9 @@
|
||||
|
||||
objects)))]
|
||||
|
||||
(let [frame (get-thumbnail-frame file)
|
||||
(let [[frame page-id] (get-thumbnail-frame file)
|
||||
frame-id (:id frame)
|
||||
page-id (or (:page-id frame)
|
||||
page-id (or page-id
|
||||
(-> data :pages first))
|
||||
|
||||
page (dm/get-in data [:pages-index page-id])
|
||||
|
||||
@@ -9,22 +9,29 @@
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.binfile.v3 :as v3]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.json :as json]
|
||||
[app.common.math :as mth]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.thumbnails :as thc]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.storage :as sto]
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.blob :as blob]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io]))
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.util.zip.ZipFile))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
@@ -86,6 +93,51 @@
|
||||
|
||||
(dissoc file :data)))
|
||||
|
||||
(defn- prepare-simple-file-with-ids
|
||||
[profile]
|
||||
(let [page-id-1 (uuid/custom 1 1)
|
||||
page-id-2 (uuid/custom 1 2)
|
||||
shape-id (uuid/custom 2 1)
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-page
|
||||
:name "test 1"
|
||||
:id page-id-1}
|
||||
{:type :add-page
|
||||
:name "test 2"
|
||||
:id page-id-2}])
|
||||
|
||||
(update-file!
|
||||
:file-id (:id file)
|
||||
:profile-id (:id profile)
|
||||
:revn 0
|
||||
:vern 0
|
||||
:changes
|
||||
[{:type :add-obj
|
||||
:page-id page-id-1
|
||||
:id shape-id
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:components-v2 true
|
||||
:obj (cts/setup-shape
|
||||
{:id shape-id
|
||||
:name "image"
|
||||
:frame-id uuid/zero
|
||||
:parent-id uuid/zero
|
||||
:type :rect})}])
|
||||
|
||||
{:file-id (:id file)
|
||||
:page-id-1 page-id-1
|
||||
:page-id-2 page-id-2
|
||||
:shape-id shape-id}))
|
||||
|
||||
(t/deftest export-binfile-v3
|
||||
(let [profile (th/create-profile* 1)
|
||||
file (prepare-simple-file profile)
|
||||
@@ -105,3 +157,139 @@
|
||||
(v3/import-files!))]
|
||||
(t/is (= (count result) 1))
|
||||
(t/is (every? uuid? result)))))
|
||||
|
||||
(t/deftest export-binfile-v3-compact
|
||||
(let [profile (th/create-profile* 1)
|
||||
{:keys [file-id page-id-1 shape-id]} (prepare-simple-file-with-ids profile)
|
||||
file {:id file-id}
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :binfile-v3-compact)]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{file-id})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output)))
|
||||
|
||||
;; Verify manifest version
|
||||
(let [manifest (v3/get-manifest (str output))]
|
||||
(t/is (= 2 (:version manifest))))
|
||||
|
||||
(let [result (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!))]
|
||||
(t/is (= (count result) 1))
|
||||
(t/is (every? uuid? result))
|
||||
|
||||
;; Verify imported data has correct shapes with preserved geometry
|
||||
(let [imported-id (first result)
|
||||
imported-data (-> (bfc/get-file th/*system* imported-id {:realize? true})
|
||||
:data)
|
||||
pages-index (get imported-data :pages-index)
|
||||
page (get pages-index page-id-1)
|
||||
shape (get-in page [:objects shape-id])]
|
||||
(t/is page "page should exist after import")
|
||||
(t/is shape "shape should exist after import")
|
||||
(t/is (= :rect (:type shape)))
|
||||
(t/is (mth/close? (:x shape) 0))
|
||||
(t/is (mth/close? (:y shape) 0))
|
||||
(t/is (mth/close? (:width shape) 0.01))
|
||||
(t/is (mth/close? (:height shape) 0.01))))))
|
||||
|
||||
(t/deftest export-binfile-v3-compact-round-trip
|
||||
(let [profile (th/create-profile* 1)
|
||||
{:keys [file-id page-id-1 shape-id]} (prepare-simple-file-with-ids profile)
|
||||
output1 (tmp/tempfile :suffix ".zip")
|
||||
output2 (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :binfile-v3-compact)]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{file-id})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output1)))
|
||||
|
||||
;; Verify compact manifest
|
||||
(let [manifest (v3/get-manifest (str output1))]
|
||||
(t/is (= 2 (:version manifest))))
|
||||
|
||||
(let [result (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output1)
|
||||
(v3/import-files!))
|
||||
imported-id (first result)]
|
||||
|
||||
;; Re-export as legacy (without compact flag) and verify
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{imported-id})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output2))
|
||||
|
||||
(let [manifest2 (v3/get-manifest (str output2))
|
||||
result2 (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output2)
|
||||
(v3/import-files!))]
|
||||
(t/is (= 1 (:version manifest2)) "re-export without flag should be legacy")
|
||||
(t/is (= (count result2) 1))
|
||||
(t/is (every? uuid? result2))
|
||||
|
||||
;; Verify shapes survive the compact -> import -> legacy re-export round-trip
|
||||
(let [reimported-id (first result2)
|
||||
reimported-data (-> (bfc/get-file th/*system* reimported-id {:realize? true})
|
||||
:data)
|
||||
pages-index (get reimported-data :pages-index)
|
||||
page (get pages-index page-id-1)
|
||||
shape (get-in page [:objects shape-id])]
|
||||
(t/is page "page should exist after round-trip")
|
||||
(t/is shape "shape should exist after round-trip")
|
||||
(t/is (= :rect (:type shape)))
|
||||
(t/is (mth/close? (:x shape) 0))
|
||||
(t/is (mth/close? (:y shape) 0))
|
||||
(t/is (mth/close? (:width shape) 0.01))
|
||||
(t/is (mth/close? (:height shape) 0.01))))))
|
||||
|
||||
(t/deftest export-binfile-v3-compact-page-structure
|
||||
(let [profile (th/create-profile* 1)
|
||||
{:keys [file-id page-id-1 shape-id]} (prepare-simple-file-with-ids profile)
|
||||
output (tmp/tempfile :suffix ".zip")]
|
||||
|
||||
(with-redefs [cf/flags (conj cf/flags :binfile-v3-compact)]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{file-id})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output)))
|
||||
|
||||
(with-open [^ZipFile zip (ZipFile. (fs/file output))]
|
||||
(let [entries (iterator-seq (.entries zip))
|
||||
|
||||
;; Page entry should exist with embedded objects
|
||||
page-path (str "files/" file-id "/pages/" page-id-1 ".json")
|
||||
page-entry (.getEntry zip page-path)]
|
||||
(t/is page-entry "compact page entry should exist")
|
||||
|
||||
;; Per-shape entries should NOT exist in compact format
|
||||
(doseq [^java.util.zip.ZipEntry entry entries]
|
||||
(let [name (.getName entry)]
|
||||
(t/is (not (.startsWith name (str "files/" file-id "/pages/" page-id-1 "/")))
|
||||
(str "should not have per-shape entries: " name))))
|
||||
|
||||
;; Verify page entry contains objects
|
||||
(with-open [reader (io/reader (.getInputStream zip page-entry))]
|
||||
(let [page-data (json/read reader :key-fn json/read-kebab-key)]
|
||||
(t/is (contains? page-data :objects)
|
||||
"compact page should contain embedded objects")
|
||||
(t/is (> (count (:objects page-data)) 0)
|
||||
"compact page objects should not be empty")
|
||||
(t/is (contains? (:objects page-data) shape-id)
|
||||
"compact page should contain the shape"))))))))
|
||||
@@ -0,0 +1,143 @@
|
||||
;; 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.common.files.shape-compact
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.shape :as cts]))
|
||||
|
||||
#?(:clj (set! *warn-on-reflection* true))
|
||||
|
||||
;; --- Identity transform detection (handles both Matrix instances and plain maps)
|
||||
|
||||
(defn- identity-transform?
|
||||
[transform]
|
||||
(if (nil? transform)
|
||||
true
|
||||
(if (gmt/matrix? transform)
|
||||
(gmt/unit? transform)
|
||||
(and (mth/close? (d/nilv (:a transform) 1) 1)
|
||||
(mth/close? (d/nilv (:b transform) 0) 0)
|
||||
(mth/close? (d/nilv (:c transform) 0) 0)
|
||||
(mth/close? (d/nilv (:d transform) 1) 1)
|
||||
(mth/close? (d/nilv (:e transform) 0) 0)
|
||||
(mth/close? (d/nilv (:f transform) 0) 0)))))
|
||||
|
||||
;; --- Default values that can be omitted
|
||||
|
||||
(def ^:private default-attrs
|
||||
{:rotation 0
|
||||
:proportion-lock false
|
||||
:hide-fill-on-export false
|
||||
:hide-in-viewer false
|
||||
:show-content false
|
||||
:blocked false
|
||||
:collapsed false
|
||||
:locked false
|
||||
:hidden false
|
||||
:masked-group false
|
||||
:fixed-scroll false
|
||||
:grow-type :fixed})
|
||||
|
||||
;; --- Empty collections that can be omitted these are optional attrs with
|
||||
;; default behavior when absent
|
||||
|
||||
(def ^:private empty-collections
|
||||
[:fills :strokes :shadow :exports :interactions :grids :shapes])
|
||||
|
||||
;; --- Compaction
|
||||
|
||||
(defn compact-shape
|
||||
"Prune redundant and derivable fields from a shape for compact serialization.
|
||||
Omits:
|
||||
- nil values
|
||||
- identity transform and transform-inverse
|
||||
- selrect and points when derivable from x/y/width/height (non-rotated shapes)
|
||||
- page-id (redundant with the containing page)
|
||||
- default values (rotation 0, proportion-lock false, etc.)
|
||||
- empty collections (fills, strokes, shadow, etc.)
|
||||
The resulting shape can be restored via expand-shape."
|
||||
[shape]
|
||||
;; Convert to plain map first (cr/defrecord does not fully remove
|
||||
;; declared fields on dissoc; it sets them to nil instead).
|
||||
(let [shape (reduce-kv (fn [m k v]
|
||||
(if (nil? v) m (assoc m k v)))
|
||||
{} shape)
|
||||
id-xf? (identity-transform? (:transform shape))
|
||||
type (dm/get-prop shape :type)
|
||||
path? (or (= type :path) (= type :bool))
|
||||
rot (dm/get-prop shape :rotation)
|
||||
safe? (and id-xf? (or (nil? rot) (zero? rot)))]
|
||||
(-> shape
|
||||
(dissoc :page-id)
|
||||
(cond-> id-xf?
|
||||
(dissoc :transform :transform-inverse))
|
||||
(cond-> (and safe? (not path?))
|
||||
(dissoc :selrect :points))
|
||||
(cond-> (and id-xf? path?)
|
||||
(dissoc :points))
|
||||
(as-> s
|
||||
(reduce-kv (fn [s k v]
|
||||
(let [d (get default-attrs k ::nf)]
|
||||
(if (and (not= d ::nf) (= v d))
|
||||
(dissoc s k)
|
||||
s)))
|
||||
s s))
|
||||
(as-> s
|
||||
(reduce (fn [s k]
|
||||
(let [v (get s k)]
|
||||
(if (and (coll? v) (empty? v))
|
||||
(dissoc s k)
|
||||
s)))
|
||||
s empty-collections)))))
|
||||
|
||||
;; --- Expansion
|
||||
|
||||
(defn expand-shape
|
||||
"Restore fields omitted by compact-shape. Restores identity
|
||||
transform/transform-inverse, and recomputes selrect and points using
|
||||
the standard shape setup functions. Returns a Shape record."
|
||||
[shape]
|
||||
(let [type (dm/get-prop shape :type)
|
||||
path? (or (= type :path) (= type :bool))]
|
||||
(-> shape
|
||||
(cond-> (nil? (:transform shape))
|
||||
(assoc :transform (gmt/matrix)))
|
||||
(cond-> (nil? (:transform-inverse shape))
|
||||
(assoc :transform-inverse (gmt/matrix)))
|
||||
(as-> s
|
||||
(if path?
|
||||
(cts/setup-path s)
|
||||
(cts/setup-rect s)))
|
||||
(cts/create-shape))))
|
||||
|
||||
;; --- Float rounding
|
||||
|
||||
(defn round-values
|
||||
"Round all numeric values in data to 4 decimal places.
|
||||
Eliminates float32 artifacts like 0.6000000238418579."
|
||||
[data]
|
||||
(letfn [(round-n [n]
|
||||
(if (and (number? n) (mth/finite? n) (not (integer? n)))
|
||||
(mth/precision n 4)
|
||||
n))
|
||||
(walk [node]
|
||||
(cond
|
||||
(map? node)
|
||||
(reduce-kv (fn [m k v] (assoc m k (walk v))) node node)
|
||||
|
||||
(vector? node)
|
||||
(mapv walk node)
|
||||
|
||||
(number? node)
|
||||
(round-n node)
|
||||
|
||||
:else
|
||||
node))]
|
||||
(walk data)))
|
||||
@@ -170,7 +170,11 @@
|
||||
:mcp
|
||||
:background-blur
|
||||
:available-viewer-wasm
|
||||
:stroke-path})
|
||||
:stroke-path
|
||||
|
||||
;; Compact binfile-v3 export: one JSON per page with embedded
|
||||
;; shapes instead of one JSON per shape.
|
||||
:binfile-v3-compact})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
;; 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 common-tests.files-shape-compact-test
|
||||
(:require
|
||||
[app.common.files.shape-compact :as sc]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.math :as mth]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn- make-rect
|
||||
[props]
|
||||
(cts/setup-shape (merge {:type :rect :x 0 :y 0 :width 10 :height 10} props)))
|
||||
|
||||
(defn- make-path
|
||||
[props]
|
||||
(cts/setup-shape
|
||||
(merge {:type :path
|
||||
:content [{:command :move-to :params {:x 0 :y 0}}
|
||||
{:command :line-to :params {:x 10 :y 10}}]}
|
||||
props)))
|
||||
|
||||
;; --- round-values
|
||||
|
||||
(t/deftest round-values-integers-unchanged
|
||||
(t/is (= 5 (sc/round-values 5)))
|
||||
(t/is (= 0 (sc/round-values 0))))
|
||||
|
||||
(t/deftest round-values-floats-rounded
|
||||
(t/is (= 0.6 (sc/round-values 0.6000000238418579)))
|
||||
(t/is (= 4213.6 (sc/round-values 4213.60009765625)))
|
||||
(t/is (= 0.1235 (sc/round-values 0.12345678))))
|
||||
|
||||
(t/deftest round-values-nested-maps
|
||||
(let [data {:x 100.1234567 :y 200.9876543
|
||||
:meta {:opacity 0.6000000238418579}}]
|
||||
(t/is (= {:x 100.1235 :y 200.9877
|
||||
:meta {:opacity 0.6}}
|
||||
(sc/round-values data)))))
|
||||
|
||||
(t/deftest round-values-vectors
|
||||
(let [data [1.2345678 2.00001 3]]
|
||||
(t/is (= [1.2346 2.0 3] (sc/round-values data)))))
|
||||
|
||||
(t/deftest round-values-non-numeric-unchanged
|
||||
(t/is (= "hello" (sc/round-values "hello")))
|
||||
(t/is (= :foo (sc/round-values :foo)))
|
||||
(t/is (= true (sc/round-values true)))
|
||||
(t/is (nil? (sc/round-values nil)))
|
||||
(let [id (uuid/next)]
|
||||
(t/is (= id (sc/round-values id)))))
|
||||
|
||||
;; --- compact-shape
|
||||
|
||||
(t/deftest compact-shape-removes-nils
|
||||
(let [shape {:id (uuid/next) :type :rect :name "test"
|
||||
:flip-x nil :flip-y nil
|
||||
:fills [] :strokes []}
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (not (contains? c :flip-x)))
|
||||
(t/is (not (contains? c :flip-y)))
|
||||
(t/is (not (contains? c :fills)))
|
||||
(t/is (not (contains? c :strokes)))))
|
||||
|
||||
(t/deftest compact-shape-removes-identity-transform
|
||||
(let [shape (make-rect {:x 100 :y 200 :width 50 :height 30})
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (not (contains? c :transform)))
|
||||
(t/is (not (contains? c :transform-inverse)))))
|
||||
|
||||
(t/deftest compact-shape-keeps-non-identity-transform
|
||||
(let [shape (-> (make-rect {:x 100 :y 200 :width 50 :height 30})
|
||||
(assoc :transform (gmt/matrix 1 0.5 0 1 0 0)))
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (contains? c :transform))
|
||||
(t/is (contains? c :transform-inverse))))
|
||||
|
||||
(t/deftest compact-shape-removes-derivable-geometry-rect
|
||||
(let [shape (make-rect {:x 100 :y 200 :width 50 :height 30})
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (not (contains? c :selrect)))
|
||||
(t/is (not (contains? c :points)))))
|
||||
|
||||
(t/deftest compact-shape-keeps-geometry-with-rotation
|
||||
(let [shape (-> (make-rect {:x 100 :y 200 :width 50 :height 30})
|
||||
(assoc :rotation 45))
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (contains? c :selrect))
|
||||
(t/is (contains? c :points))))
|
||||
|
||||
(t/deftest compact-shape-path-keeps-selrect
|
||||
(let [shape (make-path {})
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (contains? c :selrect))
|
||||
(t/is (not (contains? c :points)))
|
||||
(t/is (contains? c :content))))
|
||||
|
||||
(t/deftest compact-shape-removes-page-id
|
||||
(let [shape (assoc (make-rect {}) :page-id (uuid/next))
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (not (contains? c :page-id)))))
|
||||
|
||||
(t/deftest compact-shape-removes-defaults
|
||||
(let [shape (make-rect {:rotation 0 :proportion-lock false :blocked false})
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (not (contains? c :rotation)))
|
||||
(t/is (not (contains? c :proportion-lock)))
|
||||
(t/is (not (contains? c :blocked)))))
|
||||
|
||||
(t/deftest compact-shape-keeps-non-defaults
|
||||
(let [shape (make-rect {:rotation 30 :blocked true :opacity 0.5})
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (= 30 (:rotation c)))
|
||||
(t/is (true? (:blocked c)))
|
||||
(t/is (= 0.5 (:opacity c)))))
|
||||
|
||||
(t/deftest compact-shape-preserves-component-attrs
|
||||
(let [comp-id (uuid/next)
|
||||
ref-id (uuid/next)
|
||||
shape (-> (make-rect {})
|
||||
(assoc :component-id comp-id
|
||||
:component-file (uuid/next)
|
||||
:shape-ref ref-id
|
||||
:touched #{:geometry-group}))
|
||||
c (sc/compact-shape shape)]
|
||||
(t/is (= comp-id (:component-id c)))
|
||||
(t/is (= ref-id (:shape-ref c)))
|
||||
(t/is (= #{:geometry-group} (:touched c)))))
|
||||
|
||||
;; --- expand-shape
|
||||
|
||||
(t/deftest expand-shape-restores-transform
|
||||
(let [compact {:id (uuid/next) :type :rect :name "test"
|
||||
:x 100 :y 200 :width 50 :height 30
|
||||
:frame-id uuid/zero :parent-id uuid/zero}
|
||||
expanded (sc/expand-shape compact)]
|
||||
(t/is (gmt/matrix? (:transform expanded)))
|
||||
(t/is (gmt/matrix? (:transform-inverse expanded)))
|
||||
(t/is (true? (gmt/unit? (:transform expanded))))))
|
||||
|
||||
(t/deftest expand-shape-restores-selrect-rect
|
||||
(let [compact {:id (uuid/next) :type :rect :name "test"
|
||||
:x 100 :y 200 :width 50 :height 30
|
||||
:frame-id uuid/zero :parent-id uuid/zero}
|
||||
expanded (sc/expand-shape compact)]
|
||||
(t/is (grc/rect? (:selrect expanded)))
|
||||
(t/is (= 100 (:x (:selrect expanded))))
|
||||
(t/is (= 200 (:y (:selrect expanded))))
|
||||
(t/is (= 50 (:width (:selrect expanded))))
|
||||
(t/is (= 30 (:height (:selrect expanded))))))
|
||||
|
||||
(t/deftest expand-shape-restores-points
|
||||
(let [compact {:id (uuid/next) :type :rect :name "test"
|
||||
:x 100 :y 200 :width 50 :height 30
|
||||
:frame-id uuid/zero :parent-id uuid/zero}
|
||||
expanded (sc/expand-shape compact)
|
||||
points (:points expanded)]
|
||||
(t/is (vector? points))
|
||||
(t/is (= 4 (count points)))
|
||||
(t/is (every? gpt/point? points))))
|
||||
|
||||
(t/deftest expand-shape-path
|
||||
(let [compact {:id (uuid/next) :type :path :name "test"
|
||||
:selrect (grc/make-rect 0 0 10 10)
|
||||
:content [{:command :move-to :params {:x 0 :y 0}}
|
||||
{:command :line-to :params {:x 10 :y 10}}]
|
||||
:frame-id uuid/zero :parent-id uuid/zero}
|
||||
expanded (sc/expand-shape compact)]
|
||||
(t/is (grc/rect? (:selrect expanded)))
|
||||
(t/is (vector? (:points expanded)))
|
||||
(t/is (= 4 (count (:points expanded))))))
|
||||
|
||||
;; --- Round-trip
|
||||
|
||||
(t/deftest compact-expand-roundtrip-rect
|
||||
(let [shape (make-rect {:x 123 :y 456 :width 78 :height 90})
|
||||
compact (sc/compact-shape shape)
|
||||
result (sc/expand-shape compact)]
|
||||
(t/is (= (:id shape) (:id result)))
|
||||
(t/is (= (:type shape) (:type result)))
|
||||
(t/is (= (:name shape) (:name result)))
|
||||
(t/is (= (:x shape) (:x result)))
|
||||
(t/is (= (:y shape) (:y result)))
|
||||
(t/is (= (:width shape) (:width result)))
|
||||
(t/is (= (:height shape) (:height result)))
|
||||
(t/is (grc/rect? (:selrect result)))
|
||||
(t/is (gmt/matrix? (:transform result)))
|
||||
(t/is (sm/validate cts/schema:shape result))))
|
||||
|
||||
(t/deftest compact-expand-roundtrip-preserves-ids
|
||||
(let [id (uuid/next)
|
||||
frame-id (uuid/next)
|
||||
parent-id (uuid/next)
|
||||
shape (-> (make-rect {:x 10 :y 20 :width 30 :height 40})
|
||||
(assoc :id id :frame-id frame-id :parent-id parent-id))
|
||||
compact (sc/compact-shape shape)
|
||||
result (sc/expand-shape compact)]
|
||||
(t/is (= id (:id result)))
|
||||
(t/is (= frame-id (:frame-id result)))
|
||||
(t/is (= parent-id (:parent-id result)))))
|
||||
|
||||
;; --- Generative
|
||||
|
||||
(t/deftest compact-expand-schema-valid
|
||||
(smt/check!
|
||||
(smt/for [shape (sg/generator cts/schema:shape)]
|
||||
(let [compact (sc/compact-shape shape)
|
||||
expanded (sc/expand-shape compact)]
|
||||
(sm/validate cts/schema:shape expanded)))
|
||||
{:num 200}))
|
||||
|
||||
(t/deftest compact-expand-preserves-geometry
|
||||
(smt/check!
|
||||
(smt/for [shape (sg/generator cts/schema:shape)]
|
||||
(let [compact (sc/compact-shape shape)
|
||||
expanded (sc/expand-shape compact)]
|
||||
(and (= (:id shape) (:id expanded))
|
||||
(= (:type shape) (:type expanded))
|
||||
(= (:name shape) (:name expanded))
|
||||
(= (:frame-id shape) (:frame-id expanded))
|
||||
(= (:parent-id shape) (:parent-id expanded))
|
||||
(= (:component-id shape) (:component-id expanded))
|
||||
(= (:component-file shape) (:component-file expanded))
|
||||
(= (:shape-ref shape) (:shape-ref expanded))
|
||||
(= (:touched shape) (:touched expanded)))))
|
||||
{:num 200}))
|
||||
|
||||
(t/deftest compact-expand-preserves-content
|
||||
(smt/check!
|
||||
(smt/for [shape (sg/generator cts/schema:shape)]
|
||||
(let [compact (sc/compact-shape shape)
|
||||
expanded (sc/expand-shape compact)]
|
||||
(or (not= :path (:type shape))
|
||||
(and (= (:id shape) (:id expanded))
|
||||
(= (:content shape) (:content expanded))
|
||||
(some? (:selrect expanded))
|
||||
(vector? (:points expanded))))))
|
||||
{:num 50}))
|
||||
+58
-27
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.builder :as fb]
|
||||
[app.common.files.shape-compact :as fsc]
|
||||
[app.common.json :as json]
|
||||
[app.common.media :as media]
|
||||
[app.common.schema :as sm]
|
||||
@@ -94,7 +95,7 @@
|
||||
(-> shape encode-shape json/encode)))
|
||||
|
||||
(defn- generate-file-export-procs
|
||||
[{:keys [id data] :as file}]
|
||||
[version {:keys [id data] :as file}]
|
||||
(cons
|
||||
(let [file (cond-> (select-keys file file-attrs)
|
||||
(:options data)
|
||||
@@ -104,7 +105,8 @@
|
||||
|
||||
(concat
|
||||
(let [pages (get data :pages)
|
||||
pages-index (get data :pages-index)]
|
||||
pages-index (get data :pages-index)
|
||||
compact? (= version 2)]
|
||||
|
||||
(->> (d/enumerate pages)
|
||||
(mapcat
|
||||
@@ -114,14 +116,31 @@
|
||||
page (-> page
|
||||
(dissoc :objects)
|
||||
(assoc :index index))]
|
||||
(cons
|
||||
[(str "files/" id "/pages/" page-id ".json")
|
||||
(delay (-> page encode-page json/encode))]
|
||||
(map (fn [[shape-id shape]]
|
||||
(let [shape (assoc shape :page-id page-id)]
|
||||
(if compact?
|
||||
(let [compacted-objects
|
||||
(reduce-kv
|
||||
(fn [m shape-id shape]
|
||||
(let [shape (-> shape
|
||||
(cond-> (or (= (:type shape) :path)
|
||||
(= (:type shape) :bool))
|
||||
(update :content vec))
|
||||
fsc/compact-shape
|
||||
fsc/round-values
|
||||
encode-shape)]
|
||||
(assoc m shape-id shape)))
|
||||
{}
|
||||
objects)]
|
||||
(list
|
||||
[(str "files/" id "/pages/" page-id ".json")
|
||||
(delay (-> (assoc page :objects compacted-objects)
|
||||
json/encode))]))
|
||||
(cons
|
||||
[(str "files/" id "/pages/" page-id ".json")
|
||||
(delay (-> page encode-page json/encode))]
|
||||
(map (fn [[shape-id shape]]
|
||||
[(str "files/" id "/pages/" page-id "/" shape-id ".json")
|
||||
(delay (encode-shape* shape))]))
|
||||
objects)))))))
|
||||
(delay (encode-shape* shape))])
|
||||
objects))))))))
|
||||
|
||||
(->> (get data :components)
|
||||
(map (fn [[component-id component]]
|
||||
@@ -156,9 +175,9 @@
|
||||
json/encode))])))))
|
||||
|
||||
(defn- generate-files-export-procs
|
||||
[state]
|
||||
[state version]
|
||||
(->> (vals (get state ::fb/files))
|
||||
(mapcat generate-file-export-procs)))
|
||||
(mapcat #(generate-file-export-procs version %))))
|
||||
|
||||
(defn- generate-media-export-procs
|
||||
[state]
|
||||
@@ -182,7 +201,7 @@
|
||||
(json/encode)))]))))))
|
||||
|
||||
(defn- generate-manifest-procs
|
||||
[state]
|
||||
[state version]
|
||||
(let [opts (get state :options)
|
||||
files (->> (get state ::fb/files)
|
||||
(mapv (fn [[file-id file]]
|
||||
@@ -190,7 +209,7 @@
|
||||
:name (:name file)
|
||||
:features (:features file)})))
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
:version version
|
||||
:generated-by "penpot-library/%version%"
|
||||
:referer (get opts :referer)
|
||||
:files files
|
||||
@@ -202,11 +221,11 @@
|
||||
(delay (json/encode params))]))
|
||||
|
||||
(defn- generate-procs
|
||||
[state]
|
||||
[state version]
|
||||
(let [state (deref state)]
|
||||
(cons (generate-manifest-procs state)
|
||||
(cons (generate-manifest-procs state version)
|
||||
(concat
|
||||
(generate-files-export-procs state)
|
||||
(generate-files-export-procs state version)
|
||||
(generate-media-export-procs state)))))
|
||||
|
||||
(def ^:private
|
||||
@@ -219,8 +238,10 @@
|
||||
(constantly nil))
|
||||
|
||||
(defn- export
|
||||
[state writer progress-fn]
|
||||
(let [procs (into [] xf:add-proc-index (generate-procs state))
|
||||
[state writer progress-fn version]
|
||||
(when-not (or (= version 1) (= version 2))
|
||||
(throw (js/Error. (str "export: invalid version " version ", expected 1 or 2"))))
|
||||
(let [procs (into [] xf:add-proc-index (generate-procs state version))
|
||||
total (count procs)]
|
||||
(->> (p/reduce (fn [writer [path data index]]
|
||||
(let [data (if (delay? data) (deref data) data)
|
||||
@@ -238,7 +259,7 @@
|
||||
|
||||
(defn export-bytes
|
||||
([state]
|
||||
(export state (zip/writer (zip/bytes-writer)) noop-fn))
|
||||
(export state (zip/writer (zip/bytes-writer)) noop-fn 1))
|
||||
([state options]
|
||||
(let [options
|
||||
(if (object? options)
|
||||
@@ -246,13 +267,16 @@
|
||||
options)
|
||||
|
||||
progress-fn
|
||||
(get options :on-progress noop-fn)]
|
||||
(get options :on-progress noop-fn)
|
||||
|
||||
(export state (zip/writer (zip/bytes-writer)) progress-fn))))
|
||||
version
|
||||
(get options :version 1)]
|
||||
|
||||
(export state (zip/writer (zip/bytes-writer)) progress-fn version))))
|
||||
|
||||
(defn export-blob
|
||||
([state]
|
||||
(export state (zip/writer (zip/blob-writer)) noop-fn))
|
||||
(export state (zip/writer (zip/blob-writer)) noop-fn 1))
|
||||
([state options]
|
||||
(let [options
|
||||
(if (object? options)
|
||||
@@ -260,13 +284,16 @@
|
||||
options)
|
||||
|
||||
progress-fn
|
||||
(get options :on-progress noop-fn)]
|
||||
(get options :on-progress noop-fn)
|
||||
|
||||
(export state (zip/writer (zip/blob-writer)) progress-fn))))
|
||||
version
|
||||
(get options :version 1)]
|
||||
|
||||
(export state (zip/writer (zip/blob-writer)) progress-fn version))))
|
||||
|
||||
(defn export-stream
|
||||
([state stream]
|
||||
(export state (zip/writer stream) noop-fn))
|
||||
(export state (zip/writer stream) noop-fn 1))
|
||||
([state stream options]
|
||||
(let [options
|
||||
(if (object? options)
|
||||
@@ -274,5 +301,9 @@
|
||||
options)
|
||||
|
||||
progress-fn
|
||||
(get options :on-progress noop-fn)]
|
||||
(export state (zip/writer stream) progress-fn))))
|
||||
(get options :on-progress noop-fn)
|
||||
|
||||
version
|
||||
(get options :version 1)]
|
||||
|
||||
(export state (zip/writer stream) progress-fn version))))
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BlobReader, ZipReader, TextWriter } from "@zip.js/zip.js";
|
||||
|
||||
import * as penpot from "#self";
|
||||
|
||||
@@ -218,3 +219,97 @@ test("create context with tokens lib as obj", () => {
|
||||
assert.ok(file.data);
|
||||
assert.ok(file.data.tokensLib)
|
||||
});
|
||||
|
||||
test("export compact format produces page-level objects", async () => {
|
||||
const context = penpot.createBuildContext();
|
||||
const fileId = context.addFile({name: "test file"});
|
||||
const pageId = context.addPage({name: "test page"});
|
||||
|
||||
context.addRect({name: "rect1", x: 100, y: 200, width: 50, height: 30});
|
||||
|
||||
const blob = await penpot.exportAsBlob(context, {version: 2});
|
||||
|
||||
const zipReader = new ZipReader(new BlobReader(new Blob([blob])));
|
||||
const entries = await zipReader.getEntries();
|
||||
|
||||
const manifestEntry = entries.find(e => e.filename === "manifest.json");
|
||||
assert.ok(manifestEntry, "manifest should exist");
|
||||
|
||||
const manifest = JSON.parse(await manifestEntry.getData(new TextWriter()));
|
||||
assert.equal(manifest.version, 2);
|
||||
|
||||
const pageEntry = entries.find(e =>
|
||||
e.filename === `files/${fileId}/pages/${pageId}.json`);
|
||||
assert.ok(pageEntry, "page entry should exist");
|
||||
|
||||
const pageData = JSON.parse(await pageEntry.getData(new TextWriter()));
|
||||
assert.ok(pageData.objects, "page should contain objects");
|
||||
assert.ok(Object.keys(pageData.objects).length > 0,
|
||||
"objects should not be empty");
|
||||
|
||||
const shapeEntries = entries.filter(e =>
|
||||
e.filename.startsWith(`files/${fileId}/pages/${pageId}/`) &&
|
||||
e.filename !== `files/${fileId}/pages/${pageId}.json`);
|
||||
assert.equal(shapeEntries.length, 0, "should not have per-shape entries");
|
||||
|
||||
await zipReader.close();
|
||||
});
|
||||
|
||||
test("export legacy format produces per-shape entries", async () => {
|
||||
const context = penpot.createBuildContext();
|
||||
const fileId = context.addFile({name: "test file"});
|
||||
const pageId = context.addPage({name: "test page"});
|
||||
|
||||
context.addRect({name: "rect1", x: 100, y: 200, width: 50, height: 30});
|
||||
|
||||
const blob = await penpot.exportAsBlob(context, {version: 1});
|
||||
|
||||
const zipReader = new ZipReader(new BlobReader(new Blob([blob])));
|
||||
const entries = await zipReader.getEntries();
|
||||
|
||||
const manifestEntry = entries.find(e => e.filename === "manifest.json");
|
||||
assert.ok(manifestEntry, "manifest should exist");
|
||||
|
||||
const manifest = JSON.parse(await manifestEntry.getData(new TextWriter()));
|
||||
assert.equal(manifest.version, 1);
|
||||
|
||||
const pageEntry = entries.find(e =>
|
||||
e.filename === `files/${fileId}/pages/${pageId}.json`);
|
||||
assert.ok(pageEntry, "page entry should exist");
|
||||
|
||||
const pageData = JSON.parse(await pageEntry.getData(new TextWriter()));
|
||||
assert.ok(!pageData.objects, "legacy page should not contain objects");
|
||||
|
||||
const shapeEntries = entries.filter(e =>
|
||||
e.filename.startsWith(`files/${fileId}/pages/${pageId}/`) &&
|
||||
e.filename !== `files/${fileId}/pages/${pageId}.json`);
|
||||
assert.ok(shapeEntries.length > 0, "should have per-shape entries");
|
||||
|
||||
await zipReader.close();
|
||||
});
|
||||
|
||||
test("export default format produces per-shape entries", async () => {
|
||||
const context = penpot.createBuildContext();
|
||||
const fileId = context.addFile({name: "test file"});
|
||||
const pageId = context.addPage({name: "test page"});
|
||||
|
||||
context.addRect({name: "rect1", x: 100, y: 200, width: 50, height: 30});
|
||||
|
||||
const blob = await penpot.exportAsBlob(context);
|
||||
|
||||
const zipReader = new ZipReader(new BlobReader(new Blob([blob])));
|
||||
const entries = await zipReader.getEntries();
|
||||
|
||||
const manifest = JSON.parse(
|
||||
await entries.find(e => e.filename === "manifest.json")
|
||||
.getData(new TextWriter()));
|
||||
assert.equal(manifest.version, 1);
|
||||
|
||||
const shapeEntries = entries.filter(e =>
|
||||
e.filename.startsWith(`files/${fileId}/pages/${pageId}/`) &&
|
||||
e.filename !== `files/${fileId}/pages/${pageId}.json`);
|
||||
assert.ok(shapeEntries.length > 0,
|
||||
"default (legacy) should have per-shape entries");
|
||||
|
||||
await zipReader.close();
|
||||
});
|
||||
Reference in new issue
Block a user