From 3686d08052d076d826b3653ae8adbc8c493f8260 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 16 Jun 2026 16:57:41 +0000 Subject: [PATCH] :fire: Remove im4java dependency and replace with direct ImageMagick CLI calls - Replace im4java Java library with direct 'magick' CLI calls via shell/exec! - Add PENPOT_IMAGEMAGICK_* config env vars for resource limits (thread, memory, map, area, disk, time, width, height) - Use configurable ImageMagick environment with sensible defaults matching policy.xml - Remove -Dim4java.useV7=true JVM flag from startup scripts - Remove org.im4java/im4java from deps.edn - All ImageMagick commands now use shell/exec! with 60s timeout and resource limits Co-authored-by: mimo-v2.5-pro --- backend/deps.edn | 4 -- backend/scripts/_env | 1 - backend/scripts/run.template.sh | 2 +- backend/src/app/config.clj | 11 +++ backend/src/app/media.clj | 124 ++++++++++++++++++++------------ 5 files changed, 91 insertions(+), 51 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index 286b52dc3f..87aac35b6f 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -52,10 +52,6 @@ com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"} org.jsoup/jsoup {:mvn/version "1.22.2"} - org.im4java/im4java - {:git/tag "1.4.0-penpot-2" - :git/sha "e2b3e16" - :git/url "https://github.com/penpot/im4java"} at.yawk.lz4/lz4-java {:mvn/version "1.11.0"} diff --git a/backend/scripts/_env b/backend/scripts/_env index b68d7d85ec..58ea02ede0 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -77,7 +77,6 @@ export JAVA_OPTS="\ -Djdk.attach.allowAttachSelf \ -Dlog4j2.configurationFile=log4j2-devenv.xml \ -Djdk.tracePinnedThreads=full \ - -Dim4java.useV7=true \ -XX:+UnlockExperimentalVMOptions \ -XX:+UseShenandoahGC \ -XX:+UseCompactObjectHeaders \ diff --git a/backend/scripts/run.template.sh b/backend/scripts/run.template.sh index 6124e4475b..cff4afc870 100644 --- a/backend/scripts/run.template.sh +++ b/backend/scripts/run.template.sh @@ -18,7 +18,7 @@ if [ -f ./environ ]; then source ./environ fi -export JAVA_OPTS="-Dim4java.useV7=true -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED --enable-preview $JVM_OPTS $JAVA_OPTS" +export JAVA_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED --enable-preview $JVM_OPTS $JAVA_OPTS" ENTRYPOINT=${1:-app.main}; diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 7195988761..91c3f57506 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -127,6 +127,17 @@ [:media-max-file-size {:optional true} ::sm/int] [:font-max-file-size {:optional true} ::sm/int] + + ;; ImageMagick resource limits (PENPOT_IMAGEMAGICK_*) + [:imagemagick-thread-limit {:optional true} :string] + [:imagemagick-memory-limit {:optional true} :string] + [:imagemagick-map-limit {:optional true} :string] + [:imagemagick-area-limit {:optional true} :string] + [:imagemagick-disk-limit {:optional true} :string] + [:imagemagick-time-limit {:optional true} :string] + [:imagemagick-width-limit {:optional true} :string] + [:imagemagick-height-limit {:optional true} :string] + [:deletion-delay {:optional true} ::ct/duration] [:file-clean-delay {:optional true} ::ct/duration] [:telemetry-enabled {:optional true} ::sm/boolean] diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index d6f54254a5..8c9b6711df 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -21,6 +21,7 @@ [app.media.sanitize :as sanitize] [app.storage :as-alias sto] [app.storage.tmp :as tmp] + [app.util.shell :as shell] [buddy.core.bytes :as bb] [buddy.core.codecs :as bc] [clojure.java.shell :as sh] @@ -34,9 +35,7 @@ java.io.InputStream javax.xml.parsers.SAXParserFactory javax.xml.XMLConstants - org.apache.commons.io.IOUtils - org.im4java.core.ConvertCmd - org.im4java.core.IMOperation)) + org.apache.commons.io.IOUtils)) (def schema:upload [:map {:title "Upload"} @@ -152,16 +151,70 @@ ;; Related info on how thumbnails generation ;; http://www.imagemagick.org/Usage/thumbnails/ +(def ^:private imagemagick-defaults + "Default environment variables for ImageMagick resource limits. + These are the soft ceiling — policy.xml is the hard ceiling." + {"MAGICK_THREAD_LIMIT" "2" + "MAGICK_MEMORY_LIMIT" "256MiB" + "MAGICK_MAP_LIMIT" "512MiB" + "MAGICK_AREA_LIMIT" "128MP" + "MAGICK_DISK_LIMIT" "1GiB" + "MAGICK_TIME_LIMIT" "30"}) + +(defn- imagemagick-env + "Returns environment variables for ImageMagick commands. + Reads individual PENPOT_IMAGEMAGICK_* config values, falling back to defaults." + [] + (cond-> imagemagick-defaults + (cf/get :imagemagick-thread-limit) + (assoc "MAGICK_THREAD_LIMIT" (cf/get :imagemagick-thread-limit)) + + (cf/get :imagemagick-memory-limit) + (assoc "MAGICK_MEMORY_LIMIT" (cf/get :imagemagick-memory-limit)) + + (cf/get :imagemagick-map-limit) + (assoc "MAGICK_MAP_LIMIT" (cf/get :imagemagick-map-limit)) + + (cf/get :imagemagick-area-limit) + (assoc "MAGICK_AREA_LIMIT" (cf/get :imagemagick-area-limit)) + + (cf/get :imagemagick-disk-limit) + (assoc "MAGICK_DISK_LIMIT" (cf/get :imagemagick-disk-limit)) + + (cf/get :imagemagick-time-limit) + (assoc "MAGICK_TIME_LIMIT" (cf/get :imagemagick-time-limit)) + + (cf/get :imagemagick-width-limit) + (assoc "MAGICK_WIDTH_LIMIT" (cf/get :imagemagick-width-limit)) + + (cf/get :imagemagick-height-limit) + (assoc "MAGICK_HEIGHT_LIMIT" (cf/get :imagemagick-height-limit)))) + +(defn- exec-magick! + "Execute an ImageMagick command with resource limits. + `args` is a vector of string arguments to pass to `magick`." + [system args] + (let [cmd (into ["magick"] args) + result (shell/exec! system + :cmd cmd + :env (imagemagick-env) + :timeout 60)] + (when (not= 0 (:exit result)) + (ex/raise :type :internal + :code :imagemagick-error + :hint (str "ImageMagick command failed: " (:err result)) + :cmd cmd + :exit (:exit result))) + result)) + (defn- generic-process - [{:keys [input format operation] :as params}] + [{:keys [input format convert-args] :as params}] (let [{:keys [path mtype]} input format (or (cm/mtype->format mtype) format) ext (cm/format->extension format) - tmp (tmp/tempfile :prefix "penpot.media." :suffix ext)] - - (doto (ConvertCmd.) - (.run operation (into-array (map str [path tmp])))) - + tmp (tmp/tempfile :prefix "penpot.media." :suffix ext) + args (into [(str path)] (conj (vec convert-args) (str tmp)))] + (exec-magick! nil args) (assoc params :format format :mtype (cm/format->mtype format) @@ -171,36 +224,24 @@ (defmethod process :generic-thumbnail [params] (let [{:keys [quality width height] :as params} - (check-thumbnail-params params) - - operation - (doto (IMOperation.) - (.addImage) - (.autoOrient) - (.strip) - (.thumbnail ^Integer (int width) ^Integer (int height) ">") - (.quality (double quality)) - (.addImage))] - - (generic-process (assoc params :operation operation)))) + (check-thumbnail-params params)] + (generic-process + (assoc params + :convert-args ["-auto-orient" "-strip" + "-thumbnail" (str width "x" height ">") + "-quality" (str quality)])))) (defmethod process :profile-thumbnail [params] (let [{:keys [quality width height] :as params} - (check-thumbnail-params params) - - operation - (doto (IMOperation.) - (.addImage) - (.autoOrient) - (.strip) - (.thumbnail ^Integer (int width) ^Integer (int height) "^") - (.gravity "center") - (.extent (int width) (int height)) - (.quality (double quality)) - (.addImage))] - - (generic-process (assoc params :operation operation)))) + (check-thumbnail-params params)] + (generic-process + (assoc params + :convert-args ["-auto-orient" "-strip" + "-thumbnail" (str width "x" height "^") + "-gravity" "center" + "-extent" (str width "x" height) + "-quality" (str quality)])))) (defn get-basic-info-from-svg [{:keys [tag attrs] :as data}] @@ -233,8 +274,8 @@ (defn- get-dimensions-with-orientation [^String path] ;; Image magick doesn't give info about exif rotation so we use the identify command ;; If we are processing an animated gif we use the first frame with -scene 0 - (let [dim-result (sh/sh "identify" "-format" "%w %h\n" path) - orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)] + (let [dim-result (exec-magick! nil ["identify" "-format" "%w %h\n" path]) + orient-result (exec-magick! nil ["identify" "-format" "%[EXIF:Orientation]\n" path])] (when (= 0 (:exit dim-result)) (let [[w h] (-> (:out dim-result) str/trim @@ -260,7 +301,7 @@ (merge input info {:ts (ct/now) :size (fs/size path)})) (let [path-str (str path) - identify-res (sh/sh "identify" "-format" "image/%[magick]\n" path-str) + identify-res (exec-magick! nil ["identify" "-format" "image/%[magick]\n" path-str]) ;; identify prints one line per frame (animated GIFs, etc.); we take the first one mtype' (if (zero? (:exit identify-res)) (-> identify-res @@ -291,13 +332,6 @@ :size (fs/size path) :ts (ct/now)))))) -(defmethod process-error org.im4java.core.InfoException - [error] - (ex/raise :type :validation - :code :invalid-image - :hint "invalid image" - :cause error)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMAGE HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;