* 🐛 Fix text export with custom fonts across SVG, PNG and JPG
Text layers using custom or non-standard fonts were rendered incorrectly
on export regardless of the target format. The exporter was not resolving
the font face correctly before rasterization/serialization, causing the
output to fall back to a default glyph set and producing broken or
misaligned text. This fix ensures font data is resolved and embedded
consistently in the export pipeline for all output formats.
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
* 📚 Add entry to CHANGES.md under 2.17.0
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
---------
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
* ✨ Add loader feedback while importing and exporting files
Show a loader icon with a status label ("Importing files…" /
"Exporting files…") in the import and export dialog footers while the
operation is running, so users get clear in-progress feedback and
cannot retrigger the action by mistake.
Closes#9020
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* ✨ Address import/export loader feedback PR review
- Show the loader beside file names in the import dialog while files
are being imported (previously queued entries kept showing the
Penpot logo until each one moved into :import-progress).
- Drop the loader from the "Importing files…" / "Exporting files…"
footer status, leaving just the text styled with the modal title
color, per the design proposal.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* ✨ Match design proposal for import/export progress feedback
- Move the in-progress label from the modal footer into the modal
body, under the file rows, styled italic with the modal title
color.
- Rename the labels to match the design wording: "Uploading file…"
for import and "Downloading file…" for export.
- Restore the disabled "Accept" button in the import footer during
the import-progress phase, mirroring the disabled "Close" button
used by export.
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
* 🐛 Rename deprecated bodySmallTypography mixin to body-small-typography
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
---------
Signed-off-by: moorsecopers99 <patellscott18@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 🎉 Add get-file-stats RPC command
Introduce a new lightweight RPC query that returns aggregate statistics
for a single file: page count, shape counts by type, component/color/
typography counts, and inbound and outbound library reference counts.
Mirrors the existing get-file-summary permission and decoding pattern.
Useful for plugin authors enforcing per-file budgets, the
@penpot/library npm SDK, and future admin dashboards. Purely additive
— no migrations, no UI, no breaking changes.
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
* 🐛 Bind *load-fn* around file data walk in get-file-stats
The binding previously wrapped only — a plain key
lookup that does not realize any pointers — so by the time
walked and accessed on
each page, was unbound and every PointerMap
dereference threw , failing the three new tests.
Move inside the form so the walk runs
with available, matching the existing pattern used in
.
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
---------
Signed-off-by: edwin-rivera-dev <bytelogic772@gmail.com>
Signed-off-by: Edwin Rivera <bytelogic772@gmail.com>
Apply consistent path construction across bitmap, PDF, and SVG
renderers in the exporter. Use path join utilities instead of
hardcoding the render.html path, ensuring the path is properly
appended to the public URI base path.
- bitmap.cljs: Use u/ensure-path-slash and u/join for path
- pdf.cljs: Use u/join and ensure-path-slash on base-uri
- svg.cljs: Use u/ensure-path-slash and u/join for path
- Remove penpotWorkerURI from index.mustache and rasterizer.mustache templates
- Remove worker_main entry from the build manifest
- Construct worker URI in config.cljs by joining public-uri with worker path
- Fix global variable casing for plugins-list-uri and templates-uri
- Fix alignment in worker.cljs let bindings
The inline (fn [] (ts/schedule ...)) passed as :on-blur to text-options
was an exact copy of the on-text-blur callback already defined via
mf/use-fn earlier in the same let block. Pass on-text-blur directly
to avoid allocating a new function object on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
toggle-main-menu and toggle-more-options close over a state atom and
need no external deps. The declared deps (main-menu-open? /
more-options-open?) were unused inside the function bodies, causing
each callback to be reallocated on every toggle — self-reinforcing
churn. Drop the mf/deps calls to make both callbacks stable.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
(contains? cf/flags :token-typography-row) is a pure constant:
cf/flags is immutable after startup. Define it once as a private
namespace-level var token-typography-row? instead of re-evaluating
the check on every render in text-decoration-options* and text-menu*.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the (case type (tr ...) ...) expression in mf/with-memo [type]
so the translation is resolved only when the type prop changes
instead of on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the two radio-button option maps in mf/with-memo [token-applied]
so the vector and its (tr ...) calls are evaluated only when the
token-applied prop changes, not on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the three radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the two radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the four radio-button option maps in mf/with-memo [] so the
vector and its (tr ...) calls are evaluated once per mount instead
of on every render.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Wrap the radio-button options vector in `mf/with-memo []` so the
vector allocation and `(tr ...)` calls happen once per component
mount instead of on every render.
Also document the translation memoization rule in frontend/AGENTS.md:
`(tr ...)` must never be called at namespace level (locale is
runtime-only), and static option lists should always be wrapped in
`mf/with-memo []`.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Skip storage objects touched less than 2 hours ago, matching the pattern
used by upload-session-gc. Update all affected tests to advance the clock
past the threshold using ct/*clock* bindings.
Introduce a purpose-agnostic three-step session-based upload API that
allows uploading large binary blobs (media files and .penpot imports)
without hitting multipart size limits.
Backend:
- Migration 0147: new `upload_session` table (profile_id, total_chunks,
created_at) with indexes on profile_id and created_at.
- Three new RPC commands in media.clj:
* `create-upload-session` – allocates a session row; enforces
`upload-sessions-per-profile` and `upload-chunks-per-session`
quota limits (configurable in config.clj, defaults 5 / 20).
* `upload-chunk` – stores each slice as a storage object;
validates chunk index bounds and profile ownership.
* `assemble-file-media-object` – reassembles chunks via the shared
`assemble-chunks!` helper and creates the final media object.
- `assemble-chunks!` is a public helper in media.clj shared by both
`assemble-file-media-object` and `import-binfile`.
- `import-binfile` (binfile.clj): accepts an optional `upload-id` param;
when provided, materialises the temp file from chunks instead of
expecting an inline multipart body, removing the 200 MiB body limit
on .penpot imports. Schema updated with an `:and` validator requiring
either `:file` or `:upload-id`.
- quotes.clj: new `upload-sessions-per-profile` quota check.
- Background GC task (`tasks/upload_session_gc.clj`): deletes stalled
(never-completed) sessions older than 1 hour; scheduled daily at
midnight via the cron system in main.clj.
- backend/AGENTS.md: document the background-task wiring pattern.
Frontend:
- New `app.main.data.uploads` namespace: generic `upload-blob-chunked`
helper drives steps 1–2 (create session + upload all chunks with a
concurrency cap of 2) and emits `{:session-id uuid}` for callers.
- `config.cljs`: expose `upload-chunk-size` (default 25 MiB, overridable
via `penpotUploadChunkSize` global).
- `workspace/media.cljs`: blobs ≥ chunk-size go through the chunked path
(`upload-blob-chunked` → `assemble-file-media-object`); smaller blobs
use the existing direct `upload-file-media-object` path.
`handle-media-error` simplified; `on-error` callback removed.
- `worker/import.cljs`: new `import-blob-via-upload` helper replaces the
inline multipart approach for both binfile-v1 and binfile-v3 imports.
- `repo.cljs`: `:upload-chunk` derived as a `::multipart-upload`;
`form-data?` removed from `import-binfile` (JSON params only).
Tests:
- Backend (rpc_media_test.clj): happy path, idempotency, permission
isolation, invalid media type, missing chunks, session-not-found,
chunk-index out-of-range, and quota-limit scenarios.
- Frontend (uploads_test.cljs): session creation and chunk-count
correctness for `upload-blob-chunked`.
- Frontend (workspace_media_test.cljs): direct-upload path for small
blobs, chunked path for large blobs, and chunk-count correctness for
`process-blobs`.
- `helpers/http.cljs`: shared fetch-mock helpers (`install-fetch-mock!`,
`make-json-response`, `make-transit-response`, `url->cmd`).
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
- Add session ID tracking to RPC layer (backend and frontend)
- Send session ID header with RPC requests for request correlation
- Rename file-restore to file-restored for consistency
- Extract initialize-file function from initialize-workspace flow
- Improve file restoration initialization with wait-for-persistence
- Extract initialize-version event handler for version restoration
- Fix viewport key generation with file version numbers for proper re-renders
- Update layout item schema and constraints to use internal sizing state
- Add v-sizing state retrieval in layout-size-constraints component
- Refactor file-change notifications stream handling with rx/map
- Fix team-id lookup in restore-version-from-plugins
Improves request traceability across frontend/backend sessions and streamlines
the workspace initialization flow for file restoration scenarios.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>