🎉 Add .penpot (binfile-v3) support for library

This commit is contained in:
Andrey Antukh 2025-05-15 11:39:34 +02:00
parent 1fea1e8f5b
commit 29d23577d2
20 changed files with 926 additions and 751 deletions

View file

@ -8,33 +8,65 @@
**Breaking changes on penpot library:** **Breaking changes on penpot library:**
The library entrypoint API object has been changed. From now you start creating a new
build context, from where you can add multiple files and attach media. This change add the
ability to build more than one file at same time and export them in an unique .penpot
file.
```js
const context = penpot.createBuildContext()
context.addFile({name:"aa"})
context.addPage({name:"aa"})
context.closePage()
context.closeFile()
;; barray is instance of Uint8Array
const barray = penpot.exportAsBytes(context);
```
The previous `file.export()` method has been removed and several alternatives are
added as first level functions on penpot library API entrypoint:
- `exportAsBytes(BuildContext context) -> Promise<Uint8Array>`
- `exportAsBlob(BuildContext context) -> Promise<Blob>`
- `exportStream(BuildContext context, WritableStream stream) -> Promise<Void>`
The stream variant allows writting data as it is generated to the stream, without the need
to store the generated output entirelly in the memory.
There are also relevant semantic changes in how components should be created: this
refactor removes all notions of the old components (v1). Since v2, the shapes that are
part of a component live on a page. So, from now on, to create a component, you should
first create a frame, then add shapes and/or groups to that frame, and then create a
component by declaring that frame as the component root.
A non exhaustive list of changes:
- Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`, - Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`,
`name`, and `background` props (instead of the previous positional arguments) `name`, and `background` props (instead of the previous positional arguments)
- Rename the `file.createRect` method to `file.addRect` - Rename the `createRect` method to `addRect`
- Rename the `file.createCircle` method to `file.addCircle` - Rename the `createCircle` method to `addCircle`
- Rename the `file.createPath` method to `file.addPath` - Rename the `createPath` method to `addPath`
- Rename the `file.createText` method to `file.addText` - Rename the `createText` method to `addText`
- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style) - Rename the `addArtboard` method to `addBoard`
- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style) - Rename `startComponent` to `addComponent` (to preserve the naming style)
- Rename `file.lookupShape` to `file.getShape` - Rename `createComponentInstance` to `addComponentInstance` (to preserve the naming style)
- Rename `file.asMap` to `file.toMap` - Remove `lookupShape`
- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color) - Remove `asMap`
- Remove `file.deleteLibraryColor` (this library is intended to build files) - Remove `updateLibraryColor` (use `addLibraryColor` if you just need to replace a color)
- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography) - Remove `deleteLibraryColor` (this library is intended to build files)
- Remove `file.deleteLibraryTypography` (this library is intended to build files) - Remove `updateLibraryTypography` (use `addLibraryTypography` if you just need to replace a typography)
- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components) - Remove `deleteLibraryTypography` (this library is intended to build files)
- Remove `file.deleteObject` (this library is intended to build files) - Remove `add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components)
- Remove `file.updateObject` (this library is intended to build files) - Remove `deleteObject` (this library is intended to build files)
- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes) - Remove `updateObject` (this library is intended to build files)
- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property - Remove `finishComponent` (it is no longer necessary; see below for more details on component creation changes)
- Add `file.currentFrameId` read-only property
- Add `file.lastId` read-only property
There are also relevant semantic changes in how components should be created: this refactor removes - Change the `getCurrentPageId` function to a read-only `currentPageId` property
all notions of the old components (v1). Since v2, the shapes that are part of a component live on a - Add `currentFileId` read-only property
page. So, from now on, to create a component, you should first create a frame, then add shapes - Add `currentFrameId` read-only property
and/or groups to that frame, and then create a component by declaring that frame as the component - Add `lastId` read-only property
root.
### :heart: Community contributions (Thank you!) ### :heart: Community contributions (Thank you!)

View file

@ -18,6 +18,7 @@
[app.common.files.migrations :as-alias fmg] [app.common.files.migrations :as-alias fmg]
[app.common.json :as json] [app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.media :as cmedia]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.thumbnails :as cth] [app.common.thumbnails :as cth]
[app.common.types.color :as ctcl] [app.common.types.color :as ctcl]
@ -73,7 +74,7 @@
[:size ::sm/int] [:size ::sm/int]
[:content-type :string] [:content-type :string]
[:bucket [::sm/one-of {:format :string} sto/valid-buckets]] [:bucket [::sm/one-of {:format :string} sto/valid-buckets]]
[:hash :string]]) [:hash {:optional true} :string]])
(def ^:private schema:file-thumbnail (def ^:private schema:file-thumbnail
[:map {:title "FileThumbnail"} [:map {:title "FileThumbnail"}
@ -88,13 +89,19 @@
ctf/schema:file ctf/schema:file
[:map [:options {:optional true} ctf/schema:options]]]) [:map [:options {:optional true} ctf/schema:options]]])
;; --- HELPERS
(defn- default-now
[o]
(or o (dt/now)))
;; --- ENCODERS ;; --- ENCODERS
(def encode-file (def encode-file
(sm/encoder schema:file sm/json-transformer)) (sm/encoder schema:file sm/json-transformer))
(def encode-page (def encode-page
(sm/encoder ::ctp/page sm/json-transformer)) (sm/encoder ctp/schema:page sm/json-transformer))
(def encode-shape (def encode-shape
(sm/encoder ::cts/shape sm/json-transformer)) (sm/encoder ::cts/shape sm/json-transformer))
@ -129,7 +136,7 @@
(sm/decoder schema:manifest sm/json-transformer)) (sm/decoder schema:manifest sm/json-transformer))
(def decode-media (def decode-media
(sm/decoder ::ctf/media sm/json-transformer)) (sm/decoder ctf/schema:media sm/json-transformer))
(def decode-component (def decode-component
(sm/decoder ::ctc/component sm/json-transformer)) (sm/decoder ::ctc/component sm/json-transformer))
@ -229,27 +236,13 @@
:always :always
(bfc/clean-file-features)))))) (bfc/clean-file-features))))))
(defn- resolve-extension
[mtype]
(case mtype
"image/png" ".png"
"image/jpeg" ".jpg"
"image/gif" ".gif"
"image/svg+xml" ".svg"
"image/webp" ".webp"
"font/woff" ".woff"
"font/woff2" ".woff2"
"font/ttf" ".ttf"
"font/otf" ".otf"
"application/octet-stream" ".bin"))
(defn- export-storage-objects (defn- export-storage-objects
[{:keys [::output] :as cfg}] [{:keys [::output] :as cfg}]
(let [storage (sto/resolve cfg)] (let [storage (sto/resolve cfg)]
(doseq [id (-> bfc/*state* deref :storage-objects not-empty)] (doseq [id (-> bfc/*state* deref :storage-objects not-empty)]
(let [sobject (sto/get-object storage id) (let [sobject (sto/get-object storage id)
smeta (meta sobject) smeta (meta sobject)
ext (resolve-extension (:content-type smeta)) ext (cmedia/mtype->extension (:content-type smeta))
path (str "objects/" id ".json") path (str "objects/" id ".json")
params (-> (meta sobject) params (-> (meta sobject)
(assoc :id (:id sobject)) (assoc :id (:id sobject))
@ -574,7 +567,14 @@
(let [object (->> (read-entry input entry) (let [object (->> (read-entry input entry)
(decode-media) (decode-media)
(validate-media)) (validate-media))
object (assoc object :file-id file-id)] object (-> object
(assoc :file-id file-id)
(update :created-at default-now)
(update :modified-at default-now)
;; FIXME: this is set default to true for
;; setting a value, this prop is no longer
;; relevant;
(assoc :is-local true))]
(if (= id (:id object)) (if (= id (:id object))
(conj result object) (conj result object)
result))) result)))
@ -800,7 +800,7 @@
:expected-id (str id) :expected-id (str id)
:found-id (str (:id object)))) :found-id (str (:id object))))
(let [ext (resolve-extension (:content-type object)) (let [ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext) path (str "objects/" id ext)
content (->> path content (->> path
(get-zip-entry input) (get-zip-entry input)
@ -814,13 +814,14 @@
:expected-size (:size object) :expected-size (:size object)
:found-size (sto/get-size content))) :found-size (sto/get-size content)))
(when (not= (:hash object) (sto/get-hash content)) (when-let [hash (get object :hash)]
(ex/raise :type :validation (when (not= hash (sto/get-hash content))
:code :inconsistent-penpot-file (ex/raise :type :validation
:hint "found corrupted storage object: hash does not match" :code :inconsistent-penpot-file
:path path :hint "found corrupted storage object: hash does not match"
:expected-hash (:hash object) :path path
:found-hash (sto/get-hash content))) :expected-hash (:hash object)
:found-hash (sto/get-hash content))))
(let [params (-> object (let [params (-> object
(dissoc :id :size) (dissoc :id :size)

View file

@ -1712,6 +1712,7 @@
[{:fill-image [{:fill-image
{:id (:id fmedia) {:id (:id fmedia)
:name "test" :name "test"
:mtype "image/jpeg"
:width 200 :width 200
:height 200}}]] :height 200}}]]

View file

@ -13,17 +13,12 @@
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.files.changes :as ch] [app.common.files.changes :as ch]
[app.common.files.migrations :as fmig] [app.common.files.migrations :as fmig]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.svg :as csvg] [app.common.svg :as csvg]
[app.common.types.color :as types.color] [app.common.types.color :as types.color]
[app.common.types.component :as types.component]
[app.common.types.components-list :as types.components-list]
[app.common.types.container :as types.container]
[app.common.types.file :as types.file] [app.common.types.file :as types.file]
[app.common.types.page :as types.page] [app.common.types.page :as types.page]
[app.common.types.pages-list :as types.pages-list]
[app.common.types.shape :as types.shape] [app.common.types.shape :as types.shape]
[app.common.types.typography :as types.typography] [app.common.types.typography :as types.typography]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -37,41 +32,36 @@
(def ^:private conjv (fnil conj [])) (def ^:private conjv (fnil conj []))
(def ^:private conjs (fnil conj #{})) (def ^:private conjs (fnil conj #{}))
(defn default-uuid (defn- default-uuid
[v] [v]
(or v (uuid/next))) (or v (uuid/next)))
(defn- track-used-name (defn- track-used-name
[file name] [state name]
(let [container-id (::current-page-id file)] (let [container-id (::current-page-id state)]
(update-in file [::unames container-id] conjs name))) (update-in state [::unames container-id] conjs name)))
(defn- commit-change (defn- commit-change
[file change & {:keys [add-container] [state change & {:keys [add-container]}]
:or {add-container false}}] (let [file-id (get state ::current-file-id)]
(assert (uuid? file-id) "no current file id")
(let [change (cond-> change (let [change (cond-> change
add-container add-container
(assoc :page-id (::current-page-id file) (assoc :page-id (::current-page-id state)
:frame-id (::current-frame-id file)))] :frame-id (::current-frame-id state)))]
(-> file (update-in state [::files file-id :data] ch/process-changes [change] false))))
(update ::changes conjv change)
(update :data ch/process-changes [change] false))))
(defn- lookup-objects
[file]
(dm/get-in file [:data :pages-index (::current-page-id file) :objects]))
(defn- commit-shape (defn- commit-shape
[file shape] [state shape]
(let [parent-id (let [parent-id
(-> file ::parent-stack peek) (-> state ::parent-stack peek)
frame-id frame-id
(::current-frame-id file) (get state ::current-frame-id)
page-id page-id
(::current-page-id file) (get state ::current-page-id)
change change
{:type :add-obj {:type :add-obj
@ -82,39 +72,31 @@
:frame-id frame-id :frame-id frame-id
:page-id page-id}] :page-id page-id}]
(-> file (-> state
(commit-change change) (commit-change change)
(track-used-name (:name shape))))) (track-used-name (:name shape)))))
(defn- generate-name
[type data]
(if (= type :svg-raw)
(let [tag (dm/get-in data [:content :tag])]
(str "svg-" (cond (string? tag) tag
(keyword? tag) (d/name tag)
(nil? tag) "node"
:else (str tag))))
(str/capital (d/name type))))
(defn- unique-name (defn- unique-name
[name file] [name state]
(let [container-id (::current-page-id file) (let [container-id (::current-page-id state)
unames (dm/get-in file [:unames container-id])] unames (dm/get-in state [:unames container-id])]
(d/unique-name name (or unames #{})))) (d/unique-name name (or unames #{}))))
(defn- clear-names [file] (defn- clear-names [file]
(dissoc file ::unames)) (dissoc file ::unames))
(defn- assign-name (defn- assign-shape-name
"Given a tag returns its layer name" "Given a tag returns its layer name"
[data file type] [shape state]
(cond-> shape
(cond-> data (nil? (:name shape))
(nil? (:name data)) (assoc :name (let [type (get shape :type)]
(assoc :name (generate-name type data)) (case type
:frame "Board"
(str/capital (d/name type)))))
:always :always
(update :name unique-name file))) (update :name unique-name state)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMAS ;; SCHEMAS
@ -135,16 +117,25 @@
(def decode-library-typography (def decode-library-typography
(sm/decode-fn types.typography/schema:typography sm/json-transformer)) (sm/decode-fn types.typography/schema:typography sm/json-transformer))
(def decode-component (def schema:add-component
(sm/decode-fn types.component/schema:component sm/json-transformer)) [:map
[:component-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:name {:optional true} ::sm/text]
[:path {:optional true} ::sm/text]])
(def ^:private check-add-component
(sm/check-fn schema:add-component))
(def decode-add-component
(sm/decode-fn schema:add-component sm/json-transformer))
(def schema:add-component-instance (def schema:add-component-instance
[:map [:map
[:component-id ::sm/uuid] [:component-id ::sm/uuid]
[:x ::sm/safe-number] [:file-id {:optional true} ::sm/uuid]])
[:y ::sm/safe-number]])
(def check-add-component-instance (def ^:private check-add-component-instance
(sm/check-fn schema:add-component-instance)) (sm/check-fn schema:add-component-instance))
(def decode-add-component-instance (def decode-add-component-instance
@ -158,37 +149,76 @@
(def decode-add-bool (def decode-add-bool
(sm/decode-fn schema:add-bool sm/json-transformer)) (sm/decode-fn schema:add-bool sm/json-transformer))
(def check-add-bool (def ^:private check-add-bool
(sm/check-fn schema:add-bool)) (sm/check-fn schema:add-bool))
(def schema:add-file-media
[:map
[:id {:optional true} ::sm/uuid]
[:name ::sm/text]
[:width ::sm/int]
[:height ::sm/int]])
(def decode-add-file-media
(sm/decode-fn schema:add-file-media sm/json-transformer))
(def check-add-file-media
(sm/check-fn schema:add-file-media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API ;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn lookup-shape [file shape-id] (defn create-state
(-> (lookup-objects file) []
(get shape-id))) {})
(defn get-current-page (defn get-current-page
[file] [state]
(let [page-id (::current-page-id file)] (let [file-id (get state ::current-file-id)
(dm/get-in file [:data :pages-index page-id]))) page-id (get state ::current-page-id)]
(defn create-file (assert (uuid? file-id) "expected current-file-id to be assigned")
[params] (assert (uuid? page-id) "expected current-page-id to be assigned")
(dm/get-in state [::files file-id :data :pages-index page-id])))
(defn get-current-objects
[state]
(-> (get-current-page state)
(get :objects)))
(defn get-shape
[state shape-id]
(-> (get-current-objects state)
(get shape-id)))
(defn add-file
[state params]
(let [params (-> params (let [params (-> params
(assoc :features cfeat/default-features) (assoc :features cfeat/default-features)
(assoc :migrations fmig/available-migrations))] (assoc :migrations fmig/available-migrations))
(types.file/make-file params :create-page false))) file (types.file/make-file params :create-page false)]
(-> state
(update ::files assoc (:id file) file)
(assoc ::current-file-id (:id file)))))
(declare close-page)
(defn close-file
[state]
(let [state (-> state
(close-page)
(dissoc ::current-file-id))]
state))
(defn add-page (defn add-page
[file params] [state params]
(let [page (-> (types.page/make-empty-page params) (let [page (-> (types.page/make-empty-page params)
(types.page/check-page)) (types.page/check-page))
change {:type :add-page change {:type :add-page
:page page}] :page page}]
(-> file (-> state
(commit-change change) (commit-change change)
;; Current page being edited ;; Current page being edited
@ -203,96 +233,96 @@
;; Last object id added ;; Last object id added
(assoc ::last-id nil)))) (assoc ::last-id nil))))
(defn close-page [file] (defn close-page [state]
(-> file (-> state
(dissoc ::current-page-id) (dissoc ::current-page-id)
(dissoc ::parent-stack) (dissoc ::parent-stack)
(dissoc ::last-id) (dissoc ::last-id)
(clear-names))) (clear-names)))
(defn add-artboard (defn add-board
[file data] [state params]
(let [{:keys [id] :as shape} (let [{:keys [id] :as shape}
(-> data (-> params
(update :id default-uuid) (update :id default-uuid)
(assoc :type :frame) (assoc :type :frame)
(assign-name file :frame) (assign-shape-name state)
(types.shape/setup-shape) (types.shape/setup-shape)
(types.shape/check-shape))] (types.shape/check-shape))]
(-> file (-> state
(commit-shape shape) (commit-shape shape)
(update ::parent-stack conjv id) (update ::parent-stack conjv id)
(assoc ::current-frame-id id) (assoc ::current-frame-id id)
(assoc ::last-id id)))) (assoc ::last-id id))))
(defn close-artboard (defn close-board
[file] [state]
(let [parent-id (-> file ::parent-stack peek) (let [parent-id (-> state ::parent-stack peek)
parent (lookup-shape file parent-id)] parent (get-shape state parent-id)]
(-> file (-> state
(assoc ::current-frame-id (or (:frame-id parent) root-id)) (assoc ::current-frame-id (or (:frame-id parent) root-id))
(update ::parent-stack pop)))) (update ::parent-stack pop))))
(defn add-group (defn add-group
[file params] [state params]
(let [{:keys [id] :as shape} (let [{:keys [id] :as shape}
(-> params (-> params
(update :id default-uuid) (update :id default-uuid)
(assoc :type :group) (assoc :type :group)
(assign-name file :group) (assign-shape-name state)
(types.shape/setup-shape) (types.shape/setup-shape)
(types.shape/check-shape))] (types.shape/check-shape))]
(-> file (-> state
(commit-shape shape) (commit-shape shape)
(assoc ::last-id id) (assoc ::last-id id)
(update ::parent-stack conjv id)))) (update ::parent-stack conjv id))))
(defn close-group (defn close-group
[file] [state]
(let [group-id (-> file :parent-stack peek) (let [group-id (-> state :parent-stack peek)
group (lookup-shape file group-id) group (get-shape state group-id)
children (->> (get group :shapes) children (->> (get group :shapes)
(into [] (keep (partial lookup-shape file))) (into [] (keep (partial get-shape state)))
(not-empty))] (not-empty))]
(assert (some? children) "group expect to have at least 1 children") (assert (some? children) "group expect to have at least 1 children")
(let [file (if (:masked-group group) (let [state (if (:masked-group group)
(let [mask (first children) (let [mask (first children)
change {:type :mod-obj change {:type :mod-obj
:id group-id :id group-id
:operations :operations
[{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true}
{:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true}
{:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true} {:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true}
{:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true} {:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true}
{:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}] {:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}]
(commit-change file change :add-container true)) (commit-change state change :add-container true))
(let [group (gsh/update-group-selrect group children) (let [group (gsh/update-group-selrect group children)
change {:type :mod-obj change {:type :mod-obj
:id group-id :id group-id
:operations :operations
[{:type :set :attr :selrect :val (:selrect group) :ignore-touched true} [{:type :set :attr :selrect :val (:selrect group) :ignore-touched true}
{:type :set :attr :points :val (:points group) :ignore-touched true} {:type :set :attr :points :val (:points group) :ignore-touched true}
{:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true} {:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true}
{:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true} {:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true}
{:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true} {:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}] {:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}]
(commit-change file change :add-container true)))] (commit-change state change :add-container true)))]
(update file ::parent-stack pop)))) (update state ::parent-stack pop))))
(defn add-bool (defn add-bool
[file params] [state params]
(let [{:keys [group-id type]} (let [{:keys [group-id type]}
(check-add-bool params) (check-add-bool params)
group group
(lookup-shape file group-id) (get-shape state group-id)
children children
(->> (get group :shapes) (->> (get group :shapes)
@ -300,7 +330,7 @@
(assert (some? children) "expect group to have at least 1 element") (assert (some? children) "expect group to have at least 1 element")
(let [objects (lookup-objects file) (let [objects (get-current-objects state)
bool (-> group bool (-> group
(assoc :type :bool) (assoc :type :bool)
(gsh/update-bool objects)) (gsh/update-bool objects))
@ -317,101 +347,102 @@
{:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true} {:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true}
{:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}] {:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}]
(-> file (-> state
(commit-change change :add-container true) (commit-change change :add-container true)
(assoc ::last-id group-id))))) (assoc ::last-id group-id)))))
(defn add-shape (defn add-shape
[file params] [state params]
(let [obj (-> params (let [obj (-> params
(d/update-when :svg-attrs csvg/attrs->props) (d/update-when :svg-attrs csvg/attrs->props)
(types.shape/setup-shape) (types.shape/setup-shape)
(assign-name file :type))] (assign-shape-name state))]
(-> file (-> state
(commit-shape obj) (commit-shape obj)
(assoc ::last-id (:id obj))))) (assoc ::last-id (:id obj)))))
(defn add-library-color (defn add-library-color
[file color] [state color]
(let [color (-> color (let [color (-> color
(update :opacity d/nilv 1)
(update :id default-uuid) (update :id default-uuid)
(types.color/check-library-color color)) (types.color/check-library-color color))
change {:type :add-color change {:type :add-color
:color color}] :color color}]
(-> file
(-> state
(commit-change change) (commit-change change)
(assoc ::last-id (:id color))))) (assoc ::last-id (:id color)))))
(defn add-library-typography (defn add-library-typography
[file typography] [state typography]
(let [typography (-> typography (let [typography (-> typography
(update :id default-uuid) (update :id default-uuid)
(d/without-nils)) (d/without-nils))
change {:type :add-typography change {:type :add-typography
:id (:id typography) :id (:id typography)
:typography typography}] :typography typography}]
(-> file (-> state
(commit-change change) (commit-change change)
(assoc ::last-id (:id typography))))) (assoc ::last-id (:id typography)))))
(defn add-component (defn add-component
[file params] [state params]
(let [change1 {:type :add-component (let [{:keys [component-id file-id name path]}
:id (or (:id params) (uuid/next)) (check-add-component params)
:name (:name params)
:path (:path params)
:main-instance-id (:main-instance-id params)
:main-instance-page (:main-instance-page params)}
comp-id (get change1 :id) frame-id
(get state ::current-frame-id)
change2 {:type :mod-obj
:id (:main-instance-id params)
:operations
[{:type :set :attr :component-root :val true}
{:type :set :attr :component-id :val comp-id}
{:type :set :attr :component-file :val (:id file)}]}]
(-> file
(commit-change change1)
(commit-change change2)
(assoc ::last-id comp-id)
(assoc ::current-frame-id comp-id))))
(defn add-component-instance
[{:keys [id data] :as file} params]
(let [{:keys [component-id x y]}
(check-add-component-instance params)
component
(types.components-list/get-component data component-id)
page-id page-id
(get file ::current-page-id)] (get state ::current-page-id)
(assert (uuid? page-id) "page-id is expected to be set") component-id
(assert (uuid? component) "component is expected to exist") (or component-id (uuid/next))
;; FIXME: this should be on files and not in pages-list change1
(let [page (types.pages-list/get-page (:data file) page-id) (d/without-nils
pos (gpt/point x y) {:type :add-component
:id component-id
:name (or name "anonmous")
:path path
:main-instance-id frame-id
:main-instance-page page-id})
[shape shapes] change2
(types.container/make-component-instance page component id pos) {:type :mod-obj
:id frame-id
:operations
[{:type :set :attr :component-root :val true}
{:type :set :attr :component-id :val component-id}
{:type :set :attr :component-file :val file-id}]}]
file (-> state
(reduce #(commit-change %1 (commit-change change1)
{:type :add-obj (commit-change change2))))
:id (:id %2)
:page-id (:id page)
:parent-id (:parent-id %2)
:frame-id (:frame-id %2)
:ignore-touched true
:obj %2})
file
shapes)]
(assoc file ::last-id (:id shape))))) (defn add-component-instance
[state params]
(let [{:keys [component-id file-id]}
(check-add-component-instance params)
file-id
(or file-id (get state ::current-file-id))
frame-id
(get state ::current-frame-id)
change
{:type :mod-obj
:id frame-id
:operations
[{:type :set :attr :component-root :val false}
{:type :set :attr :component-id :val component-id}
{:type :set :attr :component-file :val file-id}]}]
(commit-change state change)))
(defn delete-shape (defn delete-shape
[file id] [file id]
@ -423,10 +454,12 @@
:id id})) :id id}))
(defn update-shape (defn update-shape
[file shape-id f] [state shape-id f]
(let [page-id (::current-page-id file) (let [page-id (get state ::current-page-id)
objects (lookup-objects file)
objects (get-current-objects state)
old-shape (get objects shape-id) old-shape (get objects shape-id)
new-shape (f old-shape) new-shape (f old-shape)
attrs (d/concat-set attrs (d/concat-set
(keys old-shape) (keys old-shape)
@ -440,7 +473,7 @@
changes changes
(conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))] (conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))]
(-> file (-> state
(commit-change (commit-change
{:type :mod-obj {:type :mod-obj
:operations (reduce generate-operation [] attrs) :operations (reduce generate-operation [] attrs)
@ -449,12 +482,12 @@
(assoc ::last-id shape-id)))) (assoc ::last-id shape-id))))
(defn add-guide (defn add-guide
[file guide] [state guide]
(let [guide (cond-> guide (let [guide (cond-> guide
(nil? (:id guide)) (nil? (:id guide))
(assoc :id (uuid/next))) (assoc :id (uuid/next)))
page-id (::current-page-id file)] page-id (::current-page-id state)]
(-> file (-> state
(commit-change (commit-change
{:type :set-guide {:type :set-guide
:page-id page-id :page-id page-id
@ -463,24 +496,54 @@
(assoc ::last-id (:id guide))))) (assoc ::last-id (:id guide)))))
(defn delete-guide (defn delete-guide
[file id] [state id]
(let [page-id (::current-page-id state)]
(let [page-id (::current-page-id file)] (commit-change state
(commit-change file
{:type :set-guide {:type :set-guide
:page-id page-id :page-id page-id
:id id :id id
:params nil}))) :params nil})))
(defn update-guide (defn update-guide
[file guide] [state guide]
(let [page-id (::current-page-id file)] (let [page-id (::current-page-id state)]
(commit-change file (commit-change state
{:type :set-guide {:type :set-guide
:page-id page-id :page-id page-id
:id (:id guide) :id (:id guide)
:params guide}))) :params guide})))
(defn strip-image-extension [filename] (defrecord BlobWrapper [mtype size blob])
(let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
(str/replace filename image-extensions-re ""))) (defn add-file-media
[state params blob]
(assert (instance? BlobWrapper blob) "expect blob to be wrapped")
(let [media-id
(uuid/next)
file-id
(get state ::current-file-id)
{:keys [id width height name]}
(-> params
(update :id default-uuid)
(check-add-file-media params))]
(-> state
(update ::blobs assoc media-id blob)
(update ::media assoc media-id
{:id media-id
:bucket "file-media-object"
:content-type (get blob :mtype)
:size (get blob :size)})
(update ::file-media assoc id
{:id id
:name name
:width width
:height height
:file-id file-id
:media-id media-id
:mtype (get blob :mtype)})
(assoc ::last-id id))))

View file

@ -310,12 +310,12 @@
[:add-media [:add-media
[:map {:title "AddMediaChange"} [:map {:title "AddMediaChange"}
[:type [:= :add-media]] [:type [:= :add-media]]
[:object ::ctf/media-object]]] [:object ctf/schema:media]]]
[:mod-media [:mod-media
[:map {:title "ModMediaChange"} [:map {:title "ModMediaChange"}
[:type [:= :mod-media]] [:type [:= :mod-media]]
[:object ::ctf/media-object]]] [:object ctf/schema:media]]]
[:del-media [:del-media
[:map {:title "DelMediaChange"} [:map {:title "DelMediaChange"}

View file

@ -5,8 +5,8 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.common.media (ns app.common.media
"Media assets helpers (images, fonts, etc)"
(:require (:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
;; We have added ".ttf" as string to solve a problem with chrome input selector ;; We have added ".ttf" as string to solve a problem with chrome input selector
@ -48,38 +48,28 @@
(defn mtype->extension [mtype] (defn mtype->extension [mtype]
;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types ;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
(case mtype (case mtype
"image/apng" ".apng" "image/apng" ".apng"
"image/avif" ".avif" "image/avif" ".avif"
"image/gif" ".gif" "image/gif" ".gif"
"image/jpeg" ".jpg" "image/jpeg" ".jpg"
"image/png" ".png" "image/png" ".png"
"image/svg+xml" ".svg" "image/svg+xml" ".svg"
"image/webp" ".webp" "image/webp" ".webp"
"application/zip" ".zip" "application/zip" ".zip"
"application/penpot" ".penpot" "application/penpot" ".penpot"
"application/pdf" ".pdf" "application/pdf" ".pdf"
"text/plain" ".txt" "text/plain" ".txt"
"font/woff" ".woff"
"font/woff2" ".woff2"
"font/ttf" ".ttf"
"font/otf" ".otf"
"application/octet-stream" ".bin"
nil)) nil))
(s/def ::id uuid?) (defn strip-image-extension
(s/def ::name string?) [filename]
(s/def ::width number?) (let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"]
(s/def ::height number?) (str/replace filename image-extensions-re "")))
(s/def ::created-at inst?)
(s/def ::modified-at inst?)
(s/def ::mtype string?)
(s/def ::uri string?)
(s/def ::media-object
(s/keys :req-un [::id
::name
::width
::height
::mtype
::created-at
::modified-at
::uri]))
(defn parse-font-weight (defn parse-font-weight
[variant] [variant]

View file

@ -54,13 +54,13 @@
::oapi/type "integer" ::oapi/type "integer"
::oapi/format "int64"}})) ::oapi/format "int64"}}))
(def schema:image-color (def schema:image
[:map {:title "ImageColor"} [:map {:title "ImageColor"}
[:name {:optional true} :string]
[:width ::sm/int] [:width ::sm/int]
[:height ::sm/int] [:height ::sm/int]
[:mtype {:optional true} [:maybe :string]] [:mtype ::sm/text]
[:id ::sm/uuid] [:id ::sm/uuid]
[:name {:optional true} ::sm/text]
[:keep-aspect-ratio {:optional true} :boolean]]) [:keep-aspect-ratio {:optional true} :boolean]])
(def gradient-types (def gradient-types
@ -93,7 +93,7 @@
[:ref-id {:optional true} ::sm/uuid] [:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]] [:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]] [:image {:optional true} [:maybe schema:image]]
[:plugin-data {:optional true} ::ctpg/plugin-data]]) [:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:color (def schema:color
@ -106,7 +106,7 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} [:maybe ::sm/safe-number]]
[:color {:optional true} [:maybe schema:rgb-color]] [:color {:optional true} [:maybe schema:rgb-color]]
[:gradient {:optional true} [:maybe schema:gradient]] [:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]]] [:image {:optional true} [:maybe schema:image]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
;; Same as color but with :id prop required ;; Same as color but with :id prop required
@ -115,9 +115,10 @@
(sm/required-keys schema:color-attrs [:id]) (sm/required-keys schema:color-attrs [:id])
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
;; FIXME: revisit if we really need this all registers
(sm/register! ::color schema:color) (sm/register! ::color schema:color)
(sm/register! ::gradient schema:gradient) (sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color) (sm/register! ::image-color schema:image)
(sm/register! ::recent-color schema:recent-color) (sm/register! ::recent-color schema:recent-color)
(sm/register! ::color-attrs schema:color-attrs) (sm/register! ::color-attrs schema:color-attrs)

View file

@ -38,18 +38,18 @@
(def schema:media (def schema:media
"A schema that represents the file media object" "A schema that represents the file media object"
[:map {:title "FileMediaObject"} [:map {:title "FileMedia"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:created-at ::sm/inst] [:created-at {:optional true} ::sm/inst]
[:deleted-at {:optional true} ::sm/inst] [:deleted-at {:optional true} ::sm/inst]
[:name :string] [:name :string]
[:width ::sm/safe-int] [:width ::sm/safe-int]
[:height ::sm/safe-int] [:height ::sm/safe-int]
[:mtype :string] [:mtype :string]
[:file-id {:optional true} ::sm/uuid]
[:media-id ::sm/uuid] [:media-id ::sm/uuid]
[:file-id {:optional true} ::sm/uuid]
[:thumbnail-id {:optional true} ::sm/uuid] [:thumbnail-id {:optional true} ::sm/uuid]
[:is-local :boolean]]) [:is-local {:optional true} :boolean]])
(def schema:colors (def schema:colors
[:map-of {:gen/max 5} ::sm/uuid ::ctc/color]) [:map-of {:gen/max 5} ::sm/uuid ::ctc/color])
@ -102,7 +102,6 @@
(sm/register! ::media schema:media) (sm/register! ::media schema:media)
(sm/register! ::colors schema:colors) (sm/register! ::colors schema:colors)
(sm/register! ::typographies schema:typographies) (sm/register! ::typographies schema:typographies)
(sm/register! ::media-object schema:media)
(def check-file (def check-file
(sm/check-fn schema:file :hint "check error on validating file")) (sm/check-fn schema:file :hint "check error on validating file"))
@ -110,7 +109,7 @@
(def check-file-data (def check-file-data
(sm/check-fn schema:data)) (sm/check-fn schema:data))
(def check-media-object (def check-file-media
(sm/check-fn schema:media)) (sm/check-fn schema:media))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -34,41 +34,35 @@
(defn export-files (defn export-files
[files format] [files format]
(dm/assert! (assert (contains? valid-formats format)
"expected valid files param" "expected valid export format")
(check-export-files files))
(dm/assert! (let [files (check-export-files files)]
"expected valid format"
(contains? valid-formats format))
(ptk/reify ::export-files (ptk/reify ::export-files
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [features (get state :features) (let [team-id (get state :current-team-id)
team-id (:current-team-id state) evname (if (= format :legacy-zip)
evname (if (= format :legacy-zip) "export-standard-files"
"export-standard-files" "export-binary-files")]
"export-binary-files")] (rx/merge
(rx/of (ptk/event ::ev/event {::ev/name evname
(rx/merge ::ev/origin "dashboard"
(rx/of (ptk/event ::ev/event {::ev/name evname :format format
::ev/origin "dashboard" :num-files (count files)}))
:format format (->> (rx/from files)
:num-files (count files)})) (rx/mapcat
(->> (rx/from files) (fn [file]
(rx/mapcat (->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
(fn [file] (rx/map #(assoc file :has-libraries %)))))
(->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) (rx/reduce conj [])
(rx/map #(assoc file :has-libraries %))))) (rx/map (fn [files]
(rx/reduce conj []) (modal/show
(rx/map (fn [files] {:type ::export-files
(modal/show :team-id team-id
{:type ::export-files :files files
:features features :format format}))))))))))
:team-id team-id
:files files
:format format})))))))))
;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;
;; Team Request ;; Team Request

View file

@ -254,7 +254,7 @@
(defn add-media (defn add-media
[media] [media]
(let [media (ctf/check-media-object media)] (let [media (ctf/check-file-media media)]
(ptk/reify ::add-media (ptk/reify ::add-media
ev/Event ev/Event
(-data [_] media) (-data [_] media)

View file

@ -10,11 +10,11 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.files.builder :as fb]
[app.common.files.changes-builder :as pcb] [app.common.files.changes-builder :as pcb]
[app.common.files.shapes-builder :as sb] [app.common.files.shapes-builder :as sb]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.media :as media]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@ -137,7 +137,7 @@
(= (.-type blob) "image/svg+xml"))) (= (.-type blob) "image/svg+xml")))
(prepare-blob [blob] (prepare-blob [blob]
(let [name (or name (if (dmm/file? blob) (fb/strip-image-extension (.-name blob)) "blob"))] (let [name (or name (if (dmm/file? blob) (media/strip-image-extension (.-name blob)) "blob"))]
{:file-id file-id {:file-id file-id
:name name :name name
:is-local local? :is-local local?

View file

@ -27,7 +27,8 @@
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Add empty page (only root-frame)" (t/testing "Add empty page (only root-frame)"
(let [page (-> (fb/create-file {:name "Test"}) (let [page (-> (fb/create-state)
(fb/add-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/get-current-page)) (fb/get-current-page))
@ -36,18 +37,19 @@
(t/is (some? data)))) (t/is (some? data))))
(t/testing "Create simple shape on root" (t/testing "Create simple shape on root"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-shape (fb/add-page {:name "Page 1"})
{:type :rect (fb/add-shape
:x 0 {:type :rect
:y 0 :x 0
:width 100 :y 0
:height 100})) :width 100
page (fb/get-current-page file) :height 100}))
page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
result-x (sd/query data (:id page) uuid/zero :x [0 100])] result-x (sd/query data (:id page) uuid/zero :x [0 100])]
@ -66,17 +68,18 @@
(t/is (= (first (nth result-x 2)) 100)))) (t/is (= (first (nth result-x 2)) 100))))
(t/testing "Add page with single empty frame" (t/testing "Add page with single empty frame"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-board
:y 0 {:x 0
:width 100 :y 0
:height 100}) :width 100
(fb/close-artboard)) :height 100})
(fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
;; frame-id (::fb/last-id file) ;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
@ -91,26 +94,27 @@
(t/testing "Add page with some shapes inside frames" (t/testing "Add page with some shapes inside frames"
(with-redefs [uuid/next (get-mocked-uuid)] (with-redefs [uuid/next (get-mocked-uuid)]
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-board
:y 0 {:x 0
:width 100 :y 0
:height 100})) :width 100
:height 100}))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
file (-> file state (-> state
(fb/add-shape (fb/add-shape
{:type :rect {:type :rect
:x 25 :x 25
:y 25 :y 25
:width 50 :width 50
:height 50}) :height 50})
(fb/close-artboard)) (fb/close-board))
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
@ -123,21 +127,21 @@
(t/is (= (count result-frame-x) 5))))) (t/is (= (count result-frame-x) 5)))))
(t/testing "Add a global guide" (t/testing "Add a global guide"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-guide {:position 50 :axis :x}) (fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-guide {:position 50 :axis :x})
(fb/close-artboard)) (fb/add-board {:x 200 :y 200 :width 100 :height 100})
(fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data)
data (-> (sd/make-snap-data) (sd/add-page page))
(sd/add-page page))
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
@ -151,17 +155,18 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Add a frame guide" (t/testing "Add a frame guide"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-page {:name "Page 1"})
(fb/close-artboard)) (fb/add-board {:x 200 :y 200 :width 100 :height 100})
(fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
file (-> file state (-> state
(fb/add-guide {:position 50 :axis :x :frame-id frame-id})) (fb/add-guide {:position 50 :axis :x :frame-id frame-id}))
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
@ -182,26 +187,26 @@
(t/deftest test-update-index (t/deftest test-update-index
(t/testing "Create frame on root and then remove it." (t/testing "Create frame on root and then remove it."
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-board
:y 0 {:x 0
:width 100 :y 0
:height 100}) :width 100
(fb/close-artboard)) :height 100})
(fb/close-board))
shape-id (::fb/last-id file) shape-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
;; frame-id (::fb/last-id file)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (-> file state (-> state
(fb/delete-shape shape-id)) (fb/delete-shape shape-id))
new-page (fb/get-current-page file) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-x (sd/query data (:id page) uuid/zero :x [0 100])
@ -212,25 +217,26 @@
(t/is (= (count result-y) 0)))) (t/is (= (count result-y) 0))))
(t/testing "Create simple shape on root. Then remove it" (t/testing "Create simple shape on root. Then remove it"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-shape (fb/add-page {:name "Page 1"})
{:type :rect (fb/add-shape
:x 0 {:type :rect
:y 0 :x 0
:width 100 :y 0
:height 100})) :width 100
:height 100}))
shape-id (::fb/last-id file) shape-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
;; frame-id (::fb/last-id file) ;; frame-id (::fb/last-id state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (fb/delete-shape file shape-id) state (fb/delete-shape state shape-id)
new-page (fb/get-current-page file) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-x (sd/query data (:id page) uuid/zero :x [0 100]) result-x (sd/query data (:id page) uuid/zero :x [0 100])
@ -241,30 +247,31 @@
(t/is (= (count result-y) 0)))) (t/is (= (count result-y) 0))))
(t/testing "Create shape inside frame, then remove it" (t/testing "Create shape inside frame, then remove it"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-board
:y 0 {:x 0
:width 100 :y 0
:height 100})) :width 100
frame-id (::fb/last-id file) :height 100}))
frame-id (::fb/last-id state)
file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50}) state (fb/add-shape state {:type :rect :x 25 :y 25 :width 50 :height 50})
shape-id (::fb/last-id file) shape-id (::fb/last-id state)
file (fb/close-artboard file) state (fb/close-board state)
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (fb/delete-shape file shape-id) state (fb/delete-shape state shape-id)
new-page (fb/get-current-page file) new-page (fb/get-current-page state)
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100])] result-frame-x (sd/query data (:id page) frame-id :x [0 100])]
(t/is (some? data)) (t/is (some? data))
@ -272,26 +279,28 @@
(t/is (= (count result-frame-x) 3)))) (t/is (= (count result-frame-x) 3))))
(t/testing "Create global guide then remove it" (t/testing "Create global guide then remove it"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-guide {:position 50 :axis :x})) (fb/add-page {:name "Page 1"})
(fb/add-guide {:position 50 :axis :x}))
guide-id (::fb/last-id file) guide-id (::fb/last-id state)
file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100}) state (-> (fb/add-board state {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data)
(sd/add-page page))
new-page (-> (fb/delete-guide file guide-id) new-page (-> (fb/delete-guide state guide-id)
(fb/get-current-page)) (fb/get-current-page))
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y (sd/query data (:id page) uuid/zero :y [0 100])
result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100])
result-frame-y (sd/query data (:id page) frame-id :y [0 100])] result-frame-y (sd/query data (:id page) frame-id :y [0 100])]
@ -305,10 +314,11 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Create frame guide then remove it" (t/testing "Create frame guide then remove it"
(let [file (-> (fb/create-file {:name "Test"}) (let [file (-> (fb/create-state)
(fb/add-file {:name "Test"})
(fb/add-page {:name "Page 1"}) (fb/add-page {:name "Page 1"})
(fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) (fb/add-board {:x 200 :y 200 :width 100 :height 100})
(fb/close-artboard)) (fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id file)
file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id}) file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id})
@ -336,30 +346,31 @@
(t/is (= (count result-frame-y) 0)))) (t/is (= (count result-frame-y) 0))))
(t/testing "Update frame coordinates" (t/testing "Update frame coordinates"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-artboard (fb/add-page {:name "Page 1"})
{:x 0 (fb/add-board
:y 0 {:x 0
:width 100 :y 0
:height 100}) :width 100
(fb/close-artboard)) :height 100})
(fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data)
(sd/add-page page))
file (fb/update-shape file frame-id state (fb/update-shape state frame-id
(fn [shape] (fn [shape]
(-> shape (-> shape
(dissoc :selrect :points) (dissoc :selrect :points)
(assoc :x 200 :y 200) (assoc :x 200 :y 200)
(cts/setup-shape)))) (cts/setup-shape))))
new-page (fb/get-current-page file) new-page (fb/get-current-page state)
data (sd/update-page data page new-page)
data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100])
@ -373,28 +384,29 @@
(t/is (= (count result-frame-x-2) 3)))) (t/is (= (count result-frame-x-2) 3))))
(t/testing "Update shape coordinates" (t/testing "Update shape coordinates"
(let [file (-> (fb/create-file {:name "Test"}) (let [state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-shape (fb/add-page {:name "Page 1"})
{:type :rect (fb/add-shape
:x 0 {:type :rect
:y 0 :x 0
:width 100 :y 0
:height 100})) :width 100
:height 100}))
shape-id (::fb/last-id file) shape-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) data (-> (sd/make-snap-data)
(sd/add-page page)) (sd/add-page page))
file (fb/update-shape file shape-id state (fb/update-shape state shape-id
(fn [shape] (fn [shape]
(-> shape (-> shape
(dissoc :selrect :points) (dissoc :selrect :points)
(assoc :x 200 :y 200) (assoc :x 200 :y 200)
(cts/setup-shape)))) (cts/setup-shape))))
new-page (fb/get-current-page file) new-page (fb/get-current-page state)
;; FIXME: update ;; FIXME: update
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
@ -406,25 +418,26 @@
(t/is (= (count result-zero-x-2) 3)))) (t/is (= (count result-zero-x-2) 3))))
(t/testing "Update global guide" (t/testing "Update global guide"
(let [guide {:position 50 :axis :x} (let [guide {:position 50 :axis :x}
file (-> (fb/create-file {:name "Test"}) state (-> (fb/create-state)
(fb/add-page {:name "Page 1"}) (fb/add-file {:name "Test"})
(fb/add-guide guide)) (fb/add-page {:name "Page 1"})
(fb/add-guide guide))
guide-id (::fb/last-id file) guide-id (::fb/last-id state)
guide (assoc guide :id guide-id) guide (assoc guide :id guide-id)
file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100}) state (-> (fb/add-board state {:x 500 :y 500 :width 100 :height 100})
(fb/close-artboard)) (fb/close-board))
frame-id (::fb/last-id file) frame-id (::fb/last-id state)
page (fb/get-current-page file) page (fb/get-current-page state)
data (-> (sd/make-snap-data) (sd/add-page page)) data (-> (sd/make-snap-data) (sd/add-page page))
new-page (-> (fb/update-guide file (assoc guide :position 150)) new-page (-> (fb/update-guide state (assoc guide :position 150))
(fb/get-current-page)) (fb/get-current-page))
data (sd/update-page data page new-page) data (sd/update-page data page new-page)
result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100])
result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100]) result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100])

View file

@ -33,7 +33,6 @@
"dependencies": { "dependencies": {
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch", "@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"sax": "^1.4.1",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,47 +1,87 @@
import * as penpot from "../target/library/penpot.js"; import * as penpot from "../target/library/penpot.js";
import { writeFile } from 'fs/promises'; import { writeFile, readFile } from 'fs/promises';
import { createWriteStream } from 'fs'; import { createWriteStream } from 'fs';
import { Writable } from "stream"; import { Writable } from "stream";
console.log(penpot); // console.log(penpot);
(async function() { (async function() {
const file = penpot.createFile({name: "Test"}); const context = penpot.createBuildContext();
file.addPage({name: "Foo Page"})
const boardId = file.addArtboard({name: "Foo Board"})
const rectId = file.addRect({name: "Foo Rect", width:100, height: 200})
file.addLibraryColor({color: "#fabada", opacity: 0.5})
// console.log("created board", boardId);
// console.log("created rect", rectId);
// const board = file.getShape(boardId);
// console.log("=========== BOARD =============")
// console.dir(board, {depth: 10});
// const rect = file.getShape(rectId);
// console.log("=========== RECT =============")
// console.dir(rect, {depth: 10});
{ {
let result = await penpot.exportAsBytes(file) context.addFile({name: "Test File 1"});
context.addPage({name: "Foo Page"})
// Add image media
const buffer = await readFile("./playground/sample.jpg");
const blob = new Blob([buffer], { type: 'image/jpeg' });
const mediaId = context.addFileMedia({
name: "avatar.jpg",
width: 512,
height: 512
}, blob);
// Add image color asset
const assetColorId = context.addLibraryColor({
name: "Avatar",
opacity: 1,
image: {
...context.getMediaAsImage(mediaId),
keepAspectRatio: true
}
});
const boardId = context.addBoard({
name: "Foo Board",
x: 0,
y: 0,
width: 500,
height: 300,
})
const fill = {
fillColorRefId: assetColorId,
fillColorRefFile: context.currentFileId,
fillImage: {
...context.getMediaAsImage(mediaId),
keepAspectRatio: true
}
};
context.addRect({
name: "Rect 1",
x: 20,
y: 20,
width:100,
height:200,
fills: [fill]
});
context.closeBoard();
context.closeFile();
}
{
let result = await penpot.exportAsBytes(context)
await writeFile("sample-sync.zip", result); await writeFile("sample-sync.zip", result);
} }
{ // {
// Create a file stream to write the zip to // // Create a file stream to write the zip to
const output = createWriteStream('sample-stream.zip'); // const output = createWriteStream('sample-stream.zip');
// // Wrap Node's stream in a WHATWG WritableStream
// Wrap Node's stream in a WHATWG WritableStream // const writable = Writable.toWeb(output);
const writable = Writable.toWeb(output); // await penpot.exportStream(context, writable);
// }
await penpot.exportStream(file, writable);
}
})().catch((cause) => { })().catch((cause) => {
console.log(cause); console.error(cause);
const innerCause = cause.cause;
if (innerCause) {
console.error("Inner cause:", innerCause);
}
process.exit(-1); process.exit(-1);
}).finally(() => { }).finally(() => {
process.exit(0); process.exit(0);

View file

@ -25,7 +25,7 @@
:modules :modules
{:penpot {:penpot
{:exports {BuilderError lib.builder/BuilderError {:exports {BuilderError lib.builder/BuilderError
createFile lib.builder/create-file createBuildContext lib.builder/create-build-context
exportAsBytes lib.export/export-bytes exportAsBytes lib.export/export-bytes
exportAsBlob lib.export/export-blob exportAsBlob lib.export/export-blob
exportStream lib.export/export-stream exportStream lib.export/export-stream
@ -34,7 +34,7 @@
:js-options :js-options
{:entry-keys ["module" "browser" "main"] {:entry-keys ["module" "browser" "main"]
:export-conditions ["module" "import", "browser" "require" "default"] :export-conditions ["module" "import", "browser" "require" "default"]
:js-provider :import ;; :js-provider :import
;; :external-index "target/library/dependencies.js" ;; :external-index "target/library/dependencies.js"
;; :external-index-format :esm ;; :external-index-format :esm
} }

View file

@ -55,201 +55,235 @@
(defn- decode-params (defn- decode-params
[params] [params]
(if (obj/plain-object? params) (if (obj/plain-object? params)
(json/->js params) (json/->clj params)
params)) params))
(defn- create-file-api (defn- get-current-page-id
[file] [state]
(let [state* (volatile! file) (dm/str (get state ::fb/current-page-id)))
api (obj/reify {:name "File"}
:id
{:get #(dm/str (:id @state*))}
:currentFrameId (defn- get-last-id
{:get #(dm/str (::fb/current-frame-id @state*))} [state]
(dm/str (get state ::fb/last-id)))
:currentPageId (defn- create-builder-api
{:get #(dm/str (::fb/current-page-id @state*))} [state]
(obj/reify {:name "File"}
:currentFileId
{:get #(dm/str (get @state ::fb/current-file-id))}
:lastId :currentFrameId
{:get #(dm/str (::fb/last-id @state*))} {:get #(dm/str (get @state ::fb/current-frame-id))}
:addPage :currentPageId
(fn [params] {:get #(get-current-page-id @state)}
(try
(let [params (-> params
(decode-params)
(fb/decode-page))]
(vswap! state* fb/add-page params)
(dm/str (::fb/current-page-id @state*)))
(catch :default cause
(handle-exception cause))))
:closePage :lastId
(fn [] {:get #(get-last-id @state)}
(vswap! state* fb/close-page))
:addArtboard :addFile
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> params decode-params fb/decode-file)]
(json/->clj) (-> (swap! state fb/add-file params)
(assoc :type :frame) (get ::fb/current-file-id)))
(fb/decode-shape))] (catch :default cause
(vswap! state* fb/add-artboard params) (handle-exception cause))))
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeArtboard :closeFile
(fn [] (fn []
(vswap! state* fb/close-artboard)) (swap! state fb/close-file)
nil)
:addGroup :addPage
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (fb/decode-page))]
(assoc :type :group)
(fb/decode-shape))]
(vswap! state* fb/add-group params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:closeGroup (-> (swap! state fb/add-page params)
(fn [] (get-current-page-id)))
(vswap! state* fb/close-group))
:addBool (catch :default cause
(fn [params] (handle-exception cause))))
(try
(let [params (-> params
(json/->clj)
(fb/decode-add-bool))]
(vswap! state* fb/add-bool params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addRect :closePage
(fn [params] (fn []
(try (swap! state fb/close-page)
(let [params (-> params nil)
(json/->clj)
(assoc :type :rect)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addCircle :addBoard
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (assoc :type :frame)
(assoc :type :circle) (fb/decode-shape))]
(fb/decode-shape))] (-> (swap! state fb/add-board params)
(vswap! state* fb/add-shape params) (get-last-id)))
(dm/str (::fb/last-id @state*))) (catch :default cause
(catch :default cause (handle-exception cause))))
(handle-exception cause))))
:addPath :closeBoard
(fn [params] (fn []
(try (swap! state fb/close-board)
(let [params (-> params nil)
(json/->clj)
(assoc :type :path)
(fb/decode-shape))]
(vswap! state* fb/add-shape params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addText :addGroup
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (assoc :type :group)
(assoc :type :text) (fb/decode-shape))]
(fb/decode-shape))] (-> (swap! state fb/add-group params)
(vswap! state* fb/add-shape params) (get-last-id)))
(dm/str (::fb/last-id @state*))) (catch :default cause
(catch :default cause (handle-exception cause))))
(handle-exception cause))))
:addLibraryColor :closeGroup
(fn [params] (fn []
(try (swap! state fb/close-group)
(let [params (-> params nil)
(json/->clj)
(fb/decode-library-color)
(d/without-nils))]
(vswap! state* fb/add-library-color params)
(dm/str (::fb/last-id @state*)))
(catch :default cause
(handle-exception cause))))
:addLibraryTypography :addBool
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (fb/decode-add-bool))]
(fb/decode-library-typography) (-> (swap! state fb/add-bool params)
(d/without-nils))] (get-last-id)))
(vswap! state* fb/add-library-typography params) (catch :default cause
(dm/str (::fb/last-id @state*))) (handle-exception cause))))
(catch :default cause
(handle-exception cause))))
:addComponent :addRect
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (assoc :type :rect)
(fb/decode-component) (fb/decode-shape))]
(d/without-nils))] (-> (swap! state fb/add-shape params)
(vswap! state* fb/add-component params) (get-last-id)))
(dm/str (::fb/last-id @state*))) (catch :default cause
(catch :default cause (handle-exception cause))))
(handle-exception cause))))
:addComponentInstance :addCircle
(fn [params] (fn [params]
(try (try
(let [params (-> params (let [params (-> (decode-params params)
(json/->clj) (assoc :type :circle)
(fb/decode-add-component-instance) (fb/decode-shape))]
(d/without-nils))] (-> (swap! state fb/add-shape params)
(vswap! state* fb/add-component-instance params) (get-last-id)))
(dm/str (::fb/last-id @state*))) (catch :default cause
(catch :default cause (handle-exception cause))))
(handle-exception cause))))
:getShape :addPath
(fn [shape-id] (fn [params]
(let [shape-id (uuid/parse shape-id)] (try
(some-> (fb/lookup-shape @state* shape-id) (let [params (-> (decode-params params)
(json/->js)))) (assoc :type :path)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:toMap :addText
(fn [] (fn [params]
(-> @state* (try
(d/without-qualified) (let [params (-> (decode-params params)
(json/->js))))] (assoc :type :text)
(fb/decode-shape))]
(-> (swap! state fb/add-shape params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addLibraryColor
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-library-color)
(d/without-nils))]
(-> (swap! state fb/add-library-color params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addLibraryTypography
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-library-typography)
(d/without-nils))]
(-> (swap! state fb/add-library-typography params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addComponent
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-add-component))]
(-> (swap! state fb/add-component params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addComponentInstance
(fn [params]
(try
(let [params (-> (decode-params params)
(fb/decode-add-component-instance))]
(-> (swap! state fb/add-component-instance params)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:addFileMedia
(fn [params blob]
(when-not (instance? js/Blob blob)
(throw (BuilderError. "validation"
"invalid-media"
"only Blob instance are soported")))
(try
(let [blob (fb/map->BlobWrapper
{:size (.-size ^js blob)
:mtype (.-type ^js blob)
:blob blob})
params
(-> (decode-params params)
(fb/decode-add-file-media))]
(-> (swap! state fb/add-file-media params blob)
(get-last-id)))
(catch :default cause
(handle-exception cause))))
:getMediaAsImage
(fn [id]
(let [id (uuid/parse id)]
(when-let [fmedia (get-in @state [::fb/file-media id])]
(let [image {:id (get fmedia :id)
:width (get fmedia :width)
:height (get fmedia :height)
:name (get fmedia :name)
:mtype (get fmedia :mtype)}]
(json/->js (d/without-nils image))))))
:genId
(fn []
(dm/str (uuid/next)))))
(defn create-build-context
"Create an empty builder state context."
[]
(let [state (atom {})
api (create-builder-api state)]
(specify! api (specify! api
cljs.core/IDeref cljs.core/IDeref
(-deref [_] (-deref [_] @state))))
(d/without-qualified @state*)))))
(defn create-file
[params]
(try
(let [params (-> params json/->clj fb/decode-file)
file (fb/create-file params)]
(create-file-api file))
(catch :default cause
(handle-exception cause))))

View file

@ -8,12 +8,10 @@
"A .penpot export implementation" "A .penpot export implementation"
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.builder :as fb] [app.common.files.builder :as fb]
[app.common.json :as json] [app.common.json :as json]
[app.common.media :as media]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.util.object :as obj]
[app.common.types.color :as types.color] [app.common.types.color :as types.color]
[app.common.types.component :as types.component] [app.common.types.component :as types.component]
[app.common.types.file :as types.file] [app.common.types.file :as types.file]
@ -22,8 +20,8 @@
[app.common.types.shape :as types.shape] [app.common.types.shape :as types.shape]
[app.common.types.tokens-lib :as types.tokens-lib] [app.common.types.tokens-lib :as types.tokens-lib]
[app.common.types.typography :as types.typography] [app.common.types.typography :as types.typography]
[cuerdas.core :as str]
[app.util.zip :as zip] [app.util.zip :as zip]
[cuerdas.core :as str]
[promesa.core :as p])) [promesa.core :as p]))
(def ^:private schema:file (def ^:private schema:file
@ -43,9 +41,6 @@
(def ^:private encode-component (def ^:private encode-component
(sm/encoder types.component/schema:component sm/json-transformer)) (sm/encoder types.component/schema:component sm/json-transformer))
;; (def encode-media
;; (sm/encoder ::ctf/media sm/json-transformer))
(def encode-color (def encode-color
(sm/encoder types.color/schema:color sm/json-transformer)) (sm/encoder types.color/schema:color sm/json-transformer))
@ -68,7 +63,6 @@
"file-data-fragment" "file-data-fragment"
"file-change"}) "file-change"})
;; FIXME: move to types
(def ^:private schema:storage-object (def ^:private schema:storage-object
[:map {:title "StorageObject"} [:map {:title "StorageObject"}
[:id ::sm/uuid] [:id ::sm/uuid]
@ -80,12 +74,6 @@
(def encode-storage-object (def encode-storage-object
(sm/encoder schema:storage-object sm/json-transformer)) (sm/encoder schema:storage-object sm/json-transformer))
;; (def encode-file-thumbnail
;; (sm/encoder schema:file-thumbnail sm/json-transformer))
;; FIXME: naming
(def ^:private file-attrs (def ^:private file-attrs
#{:id #{:id
:name :name
@ -96,8 +84,6 @@
(defn- generate-file-export-procs (defn- generate-file-export-procs
[{:keys [id data] :as file}] [{:keys [id data] :as file}]
;; (prn "generate-file-export-procs")
;; (app.common.pprint/pprint file)
(cons (cons
(let [file (cond-> (select-keys file file-attrs) (let [file (cond-> (select-keys file file-attrs)
(:options data) (:options data)
@ -108,11 +94,11 @@
(concat (concat
(let [pages (get data :pages) (let [pages (get data :pages)
pages-index (get data :pages-index)] pages-index (get data :pages-index)]
(->> (d/enumerate pages) (->> (d/enumerate pages)
(mapcat (mapcat
(fn [[index page-id]] (fn [[index page-id]]
(let [path (str "files/" id "/pages/" page-id ".json") (let [page (get pages-index page-id)
page (get pages-index page-id)
objects (:objects page) objects (:objects page)
page (-> page page (-> page
(dissoc :objects) (dissoc :objects)
@ -155,45 +141,74 @@
(when-let [tokens-lib (get data :tokens-lib)] (when-let [tokens-lib (get data :tokens-lib)]
(list [(str "files/" id "/tokens.json") (list [(str "files/" id "/tokens.json")
(delay (-> tokens-lib (delay (-> tokens-lib
(encode-tokens-lib tokens-lib) encode-tokens-lib
json/encode))]))))) json/encode))])))))
(defn generate-manifest-procs (defn- generate-files-export-procs
[file] [state]
(let [mdata {:id (:id file) (->> (vals (get state ::fb/files))
:name (:name file) (mapcat generate-file-export-procs)))
:features (:features file)}
(defn- generate-media-export-procs
[state]
(->> (get state ::fb/file-media)
(mapcat (fn [[file-media-id file-media]]
(let [media-id (get file-media :media-id)
media (get-in state [::fb/media media-id])
blob (get-in state [::fb/blobs media-id])]
(list
[(str "objects/" media-id (media/mtype->extension (:content-type media)))
(delay (get blob :blob))]
[(str "objects/" media-id ".json")
(delay (-> media
;; FIXME: proper encode?
(json/encode)))]
[(str "files/" (:file-id file-media) "/media/" file-media-id ".json")
(delay (-> file-media
(dissoc :file-id)
(json/encode)))]))))))
(defn- generate-manifest-procs
[state]
(let [files (->> (get state ::fb/files)
(mapv (fn [[file-id file]]
{:id file-id
:name (:name file)
:features (:features file)})))
params {:type "penpot/export-files" params {:type "penpot/export-files"
:version 1 :version 1
;; FIXME: set proper placeholder for replacement on build
:generated-by "penpot-lib/develop" :generated-by "penpot-lib/develop"
:files [mdata] :files files
:relations []}] :relations []}]
(list ["manifest.json" (delay (json/encode params))]))
["manifest.json" (delay (json/encode params))])))
(defn- export (defn- export
[file writer] [state writer]
(->> (p/reduce (fn [writer [path proc]] (->> (p/reduce (fn [writer [path data]]
(let [data (deref proc)] (let [data (if (delay? data) (deref data) data)]
(js/console.log "export" path) (js/console.log "export" path)
(->> (zip/add writer path data) (->> (zip/add writer path data)
(p/fmap (constantly writer))))) (p/fmap (constantly writer)))))
writer writer
(concat (cons (generate-manifest-procs @state)
(generate-manifest-procs @file) (concat
(generate-file-export-procs @file))) (generate-files-export-procs @state)
(generate-media-export-procs @state))))
(p/mcat (fn [writer] (p/mcat (fn [writer]
(zip/close writer))))) (zip/close writer)))))
(defn export-bytes (defn export-bytes
[file] [state]
(export file (zip/writer (zip/bytes-writer)))) (export state (zip/writer (zip/bytes-writer))))
(defn export-blob (defn export-blob
[file] [state]
(export file (zip/writer (zip/blob-writer)))) (export state (zip/writer (zip/blob-writer))))
(defn export-stream (defn export-stream
[file stream] [state stream]
(export file (zip/writer stream))) (export state (zip/writer stream)))

View file

@ -59,7 +59,6 @@ __metadata:
concurrently: "npm:^9.1.2" concurrently: "npm:^9.1.2"
luxon: "npm:^3.6.1" luxon: "npm:^3.6.1"
nodemon: "npm:^3.1.9" nodemon: "npm:^3.1.9"
sax: "npm:^1.4.1"
shadow-cljs: "npm:3.0.5" shadow-cljs: "npm:3.0.5"
source-map-support: "npm:^0.5.21" source-map-support: "npm:^0.5.21"
languageName: unknown languageName: unknown
@ -73,11 +72,11 @@ __metadata:
linkType: hard linkType: hard
"@types/node@npm:^22.12.0": "@types/node@npm:^22.12.0":
version: 22.15.17 version: 22.15.18
resolution: "@types/node@npm:22.15.17" resolution: "@types/node@npm:22.15.18"
dependencies: dependencies:
undici-types: "npm:~6.21.0" undici-types: "npm:~6.21.0"
checksum: 10c0/fb92aa10b628683c5b965749f955bc2322485ecb0ea6c2f4cae5f2c7537a16834607e67083a9e9281faaae8d7dee9ada8d6a5c0de9a52c17d82912ef00c0fdd4 checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6
languageName: node languageName: node
linkType: hard linkType: hard
@ -318,14 +317,14 @@ __metadata:
linkType: hard linkType: hard
"debug@npm:4, debug@npm:^4, debug@npm:^4.3.4": "debug@npm:4, debug@npm:^4, debug@npm:^4.3.4":
version: 4.4.0 version: 4.4.1
resolution: "debug@npm:4.4.0" resolution: "debug@npm:4.4.1"
dependencies: dependencies:
ms: "npm:^2.1.3" ms: "npm:^2.1.3"
peerDependenciesMeta: peerDependenciesMeta:
supports-color: supports-color:
optional: true optional: true
checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55
languageName: node languageName: node
linkType: hard linkType: hard
@ -962,13 +961,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sax@npm:^1.4.1":
version: 1.4.1
resolution: "sax@npm:1.4.1"
checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.3": "semver@npm:^7.3.5, semver@npm:^7.5.3":
version: 7.7.2 version: 7.7.2
resolution: "semver@npm:7.7.2" resolution: "semver@npm:7.7.2"

View file

@ -11,13 +11,14 @@
}, },
"type": "module", "type": "module",
"scripts": { "scripts": {
"fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", "fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src",
"fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", "fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src",
"lint:clj:common": "clj-kondo --parallel=true --lint common/src", "lint:clj:common": "clj-kondo --parallel=true --lint common/src",
"lint:clj:frontend": "clj-kondo --parallel=true --lint frontend/src", "lint:clj:frontend": "clj-kondo --parallel=true --lint frontend/src",
"lint:clj:backend": "clj-kondo --parallel=true --lint backend/src", "lint:clj:backend": "clj-kondo --parallel=true --lint backend/src",
"lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src", "lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src",
"lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter" "lint:clj:library": "clj-kondo --parallel=true --lint library/src",
"lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter && yarn run lint:clj:library"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.43.1", "@playwright/test": "^1.43.1",