multiple size variants for images

This commit is contained in:
Tienson Qin
2025-12-29 09:15:58 +08:00
parent e77690b45b
commit a3d7178124
2 changed files with 213 additions and 18 deletions

View File

@@ -287,15 +287,58 @@
(str "/asset/" graph-uuid "/" asset-uuid "." asset-type)
:else nil)))
(def ^:private publish-image-variant-sizes
[1024 1600])
(def ^:private publish-image-variant-types
#{"png" "jpg" "jpeg" "webp"})
(def ^:private publish-image-sizes-attr
"(max-width: 640px) 92vw, (max-width: 1024px) 88vw, 920px")
(defn- asset-variant-url
[graph-uuid asset-uuid asset-type variant]
(str "/asset/" graph-uuid "/" asset-uuid "@" variant "." asset-type))
(defn- variant-width
[block size]
(let [asset-width (:logseq.property.asset/width block)
asset-height (:logseq.property.asset/height block)]
(if (and (number? asset-width)
(number? asset-height)
(pos? asset-width)
(pos? asset-height))
(let [max-dim (max asset-width asset-height)
scale (min 1 (/ size max-dim))]
(js/Math.round (* asset-width scale)))
size)))
(defn- asset-node [block ctx]
(let [asset-type (:logseq.property.asset/type block)
asset-url (asset-url block ctx)
external-url (:logseq.property.asset/external-url block)
title (or (:block/title block) (str asset-type))
ext (string/lower-case (or asset-type ""))]
ext (string/lower-case (or asset-type ""))
graph-uuid (:graph-uuid ctx)
asset-uuid (:block/uuid block)
variant? (and (not (string? external-url))
graph-uuid
asset-uuid
(contains? publish-image-variant-types ext))
srcset (when variant?
(->> publish-image-variant-sizes
(map (fn [size]
(let [width (variant-width block size)]
(str (asset-variant-url graph-uuid asset-uuid asset-type size)
" "
width
"w"))))
(string/join ", ")))]
(when asset-url
(cond
(contains? #{"png" "jpg" "jpeg" "gif" "webp" "svg" "bmp" "avif"} ext)
[:img.asset-image {:src asset-url :alt title}]
[:img.asset-image (cond-> {:src asset-url :alt title}
srcset (assoc :srcset srcset :sizes publish-image-sizes-attr))]
(contains? #{"mp4" "webm" "mov"} ext)
[:video.asset-video {:src asset-url :controls true}]

View File

@@ -6,6 +6,7 @@
[frontend.db :as db]
[frontend.fs :as fs]
[frontend.handler.notification :as notification]
[frontend.image :as image]
[frontend.state :as state]
[frontend.util :as util]
[logseq.common.path :as path]
@@ -50,6 +51,141 @@
("pdf") "application/pdf"
"application/octet-stream"))
(def ^:private publish-image-variant-sizes
[1024 1600])
(def ^:private publish-image-quality
0.9)
(def ^:private publish-image-types
#{"png" "jpg" "jpeg" "webp"})
(defn- image-asset?
[asset-type]
(contains? publish-image-types (string/lower-case (or asset-type ""))))
(defn- asset-uuid-with-variant
[asset-uuid variant]
(if variant
(str asset-uuid "@" variant)
asset-uuid))
(defn- <sha256-hex-buffer
[array-buffer]
(p/let [digest (.digest (.-subtle js/crypto) "SHA-256" array-buffer)
bytes (js/Uint8Array. digest)]
(->> bytes
(map (fn [b]
(.padStart (.toString b 16) 2 "0")))
(apply str))))
(defn- <blob-checksum
[blob]
(p/let [buffer (.arrayBuffer blob)]
(<sha256-hex-buffer buffer)))
(defn- <canvas->blob
[canvas content-type quality]
(p/create
(fn [resolve _reject]
(.toBlob canvas
(fn [blob]
(resolve blob))
content-type
quality))))
(defn- <canvas-from-blob
[blob max-dim]
(if (exists? js/createImageBitmap)
(p/let [bitmap (js/createImageBitmap blob #js {:imageOrientation "from-image"})
width (.-width bitmap)
height (.-height bitmap)
scale (min 1 (/ max-dim (max width height)))
target-width (js/Math.round (* width scale))
target-height (js/Math.round (* height scale))
canvas (js/document.createElement "canvas")
ctx ^js (.getContext canvas "2d")]
(set! (.-width canvas) target-width)
(set! (.-height canvas) target-height)
(set! (.-imageSmoothingEnabled ctx) true)
(set! (.-imageSmoothingQuality ctx) "high")
(.drawImage ctx bitmap 0 0 target-width target-height)
(when (.-close bitmap)
(.close bitmap))
canvas)
(p/create
(fn [resolve reject]
(let [img (js/Image.)
url (js/URL.createObjectURL blob)]
(set! (.-onload img)
(fn []
(image/get-orientation img
(fn [canvas]
(js/URL.revokeObjectURL url)
(resolve canvas))
max-dim
max-dim)))
(set! (.-onerror img)
(fn [error]
(js/URL.revokeObjectURL url)
(reject error)))
(set! (.-src img) url))))))
(defn- <build-image-uploads
[asset-uuid asset-type title blob content-type]
(p/let [variant-promises (map (fn [size]
(p/let [canvas (<canvas-from-blob blob size)
blob' (<canvas->blob canvas content-type publish-image-quality)]
(when blob'
{:variant size
:blob blob'})))
publish-image-variant-sizes)
variants (p/then (p/all variant-promises)
(fn [entries]
(->> entries (remove nil?) vec)))]
(when (seq variants)
(let [sorted (sort-by :variant variants)
largest (last sorted)
uploads (vec (concat [(assoc largest :variant nil)] sorted))]
(p/all
(map (fn [{:keys [variant blob]}]
(p/let [checksum (<blob-checksum blob)]
{:asset_uuid (asset-uuid-with-variant asset-uuid variant)
:asset_type asset-type
:content_type content-type
:checksum checksum
:size (.-size blob)
:title title
:blob blob}))
uploads))))))
(defn- <upload-blob-asset!
[graph-uuid asset-token {:keys [asset_uuid asset_type checksum size title content_type blob]}]
(let [meta {:graph graph-uuid
:asset_uuid asset_uuid
:asset_type asset_type
:checksum checksum
:size size
:title title
:content_type content_type}
headers (cond-> {"content-type" content_type
"x-asset-meta" (js/JSON.stringify (clj->js meta))}
asset-token (assoc "authorization" (str "Bearer " asset-token)))]
(js/fetch (asset-upload-endpoint)
(clj->js {:method "POST"
:headers headers
:body blob}))))
(defn- <upload-raw-asset!
[graph-uuid asset-token asset-meta content-type content]
(let [headers (cond-> {"content-type" content-type
"x-asset-meta" (js/JSON.stringify (clj->js asset-meta))}
asset-token (assoc "authorization" (str "Bearer " asset-token)))]
(js/fetch (asset-upload-endpoint)
(clj->js {:method "POST"
:headers headers
:body content}))))
(defn- merge-attr
[entity attr value]
(let [existing (get entity attr ::none)]
@@ -91,22 +227,38 @@
(p/let [repo-dir (config/get-repo-dir repo)
asset-path (path/path-join "assets" (str asset-uuid "." asset-type))
content (fs/read-file-raw repo-dir asset-path {})
meta {:graph graph-uuid
:asset_uuid asset-uuid
:asset_type asset-type
:checksum (:logseq.property.asset/checksum asset)
:size (:logseq.property.asset/size asset)
:title (:block/title asset)}
headers (cond-> {"content-type" (asset-content-type asset-type)
"x-asset-meta" (js/JSON.stringify (clj->js meta))}
token (assoc "authorization" (str "Bearer " token)))
resp (js/fetch (asset-upload-endpoint)
(clj->js {:method "POST"
:headers headers
:body content}))]
(when-not (.-ok resp)
(js/console.warn "Asset publish failed" {:asset asset-uuid :status (.-status resp)}))
resp))))
content-type (asset-content-type asset-type)]
(if (image-asset? asset-type)
(p/let [blob (js/Blob. (array content) (clj->js {:type content-type}))
uploads (<build-image-uploads asset-uuid asset-type (:block/title asset) blob content-type)]
(if (seq uploads)
(p/let [responses (p/all (map (fn [upload]
(<upload-blob-asset! graph-uuid token upload))
uploads))]
(doseq [resp responses]
(when-not (.-ok resp)
(js/console.warn "Asset publish failed" {:asset asset-uuid :status (.-status resp)})))
(last responses))
(p/let [meta {:graph graph-uuid
:asset_uuid asset-uuid
:asset_type asset-type
:checksum (:logseq.property.asset/checksum asset)
:size (:logseq.property.asset/size asset)
:title (:block/title asset)}
resp (<upload-raw-asset! graph-uuid token meta content-type content)]
(when-not (.-ok resp)
(js/console.warn "Asset publish failed" {:asset asset-uuid :status (.-status resp)}))
resp)))
(p/let [meta {:graph graph-uuid
:asset_uuid asset-uuid
:asset_type asset-type
:checksum (:logseq.property.asset/checksum asset)
:size (:logseq.property.asset/size asset)
:title (:block/title asset)}
resp (<upload-raw-asset! graph-uuid token meta content-type content)]
(when-not (.-ok resp)
(js/console.warn "Asset publish failed" {:asset asset-uuid :status (.-status resp)}))
resp))))))
(defn- <upload-assets!
[repo graph-uuid payload]