;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) UXBOX Labs SL (ns app.media "Media postprocessing." (:require [app.common.data :as d] [app.common.exceptions :as ex] [app.common.media :as cm] [app.common.spec :as us] [app.rlimits :as rlm] [app.rpc.queries.svg :as svg] [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs]) (:import java.io.ByteArrayInputStream org.im4java.core.ConvertCmd org.im4java.core.IMOperation org.im4java.core.Info)) ;; --- Generic specs (s/def :internal.http.upload/filename ::us/string) (s/def :internal.http.upload/size ::us/integer) (s/def :internal.http.upload/content-type cm/valid-media-types) (s/def :internal.http.upload/tempfile any?) (s/def ::upload (s/keys :req-un [:internal.http.upload/filename :internal.http.upload/size :internal.http.upload/tempfile :internal.http.upload/content-type])) ;; --- Thumbnails Generation (s/def ::cmd keyword?) (s/def ::path (s/or :path fs/path? :string string? :file fs/file?)) (s/def ::input (s/keys :req-un [::path] :opt-un [::cm/mtype])) (s/def ::width integer?) (s/def ::height integer?) (s/def ::format #{:jpeg :webp :png}) (s/def ::quality #(< 0 % 101)) (s/def ::thumbnail-params (s/keys :req-un [::cmd ::input ::format ::width ::height])) ;; Related info on how thumbnails generation ;; http://www.imagemagick.org/Usage/thumbnails/ (defn- generic-process [{:keys [input format operation] :as params}] (let [{:keys [path mtype]} input format (or (cm/mtype->format mtype) format) ext (cm/format->extension format) tmp (fs/create-tempfile :suffix ext)] (doto (ConvertCmd.) (.run operation (into-array (map str [path tmp])))) (let [thumbnail-data (fs/slurp-bytes tmp)] (fs/delete tmp) (assoc params :format format :mtype (cm/format->mtype format) :size (alength ^bytes thumbnail-data) :data (ByteArrayInputStream. thumbnail-data))))) (defmulti process :cmd) (defmethod process :generic-thumbnail [{:keys [quality width height] :as params}] (us/assert ::thumbnail-params params) (let [op (doto (IMOperation.) (.addImage) (.autoOrient) (.strip) (.thumbnail ^Integer (int width) ^Integer (int height) ">") (.quality (double quality)) (.addImage))] (generic-process (assoc params :operation op)))) (defmethod process :profile-thumbnail [{:keys [quality width height] :as params}] (us/assert ::thumbnail-params params) (let [op (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 op)))) (defn get-basic-info-from-svg [{:keys [tag attrs] :as data}] (when (not= tag :svg) (ex/raise :type :validation :code :unable-to-parse-svg :hint "uploaded svg has invalid content")) (reduce (fn [default f] (if-let [res (f attrs)] (reduced res) default)) {:width 100 :height 100} [(fn parse-width-and-height [{:keys [width height]}] (when (and (string? width) (string? height)) (let [width (d/parse-double width) height (d/parse-double height)] (when (and width height) {:width (int width) :height (int height)})))) (fn parse-viewbox [{:keys [viewBox]}] (let [[x y width height] (->> (str/split viewBox #"\s+" 4) (map d/parse-double))] (when (and x y width height) {:width (int width) :height (int height)})))])) (defmethod process :info [{:keys [input] :as params}] (us/assert ::input input) (let [{:keys [path mtype]} input] (if (= mtype "image/svg+xml") (let [data (svg/parse (slurp path)) info (get-basic-info-from-svg data)] (when-not info (ex/raise :type :validation :code :unable-to-retrieve-dimensions :hint "uploaded svg does not provides dimensions")) (assoc info :mtype mtype)) (let [instance (Info. (str path)) mtype' (.getProperty instance "Mime type")] (when (and (string? mtype) (not= mtype mtype')) (ex/raise :type :validation :code :media-type-mismatch :hint (str "Seems like you are uploading a file whose content does not match the extension." "Expected: " mtype ". Got: " mtype'))) ;; For an animated GIF, getImageWidth/Height returns the delta size of one frame (if no frame given ;; it returns size of the last one), whereas getPageWidth/Height always return the full size of ;; any frame. {:width (.getPageWidth instance) :height (.getPageHeight instance) :mtype mtype})))) (defmethod process :default [{:keys [cmd] :as params}] (ex/raise :type :internal :code :not-implemented :hint (str "No impl found for process cmd:" cmd))) (defn run [{:keys [rlimits]} params] (us/assert map? rlimits) (let [rlimit (get rlimits :image)] (when-not rlimit (ex/raise :type :internal :code :rlimit-not-configured :hint ":image rlimit not configured")) (try (rlm/execute rlimit (process params)) (catch org.im4java.core.InfoException e (ex/raise :type :validation :code :invalid-image :cause e))))) ;; --- Utility functions (defn validate-media-type ([mtype] (validate-media-type mtype cm/valid-media-types)) ([mtype allowed] (when-not (contains? allowed mtype) (ex/raise :type :validation :code :media-type-not-allowed :hint "Seems like you are uploading an invalid media object"))))