<% if (['image', 'animation'].includes(ctx.post.type)) { %> - + <% if (ctx.post.mimeType === 'image/vnd.adobe.photoshop') { %> + + <% } else { %> + + <% } %> <% } else if (ctx.post.type === 'flash') { %> diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl index 60efc26a..3fc9176a 100644 --- a/client/html/post_readonly_sidebar.tpl +++ b/client/html/post_readonly_sidebar.tpl @@ -13,6 +13,7 @@ 'image/avif': 'AVIF', 'image/heif': 'HEIF', 'image/heic': 'HEIC', + 'image/vnd.adobe.photoshop': 'PSD', 'video/webm': 'WEBM', 'video/mp4': 'MPEG-4', 'video/quicktime': 'MOV', diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js index 4ef4c1ad..1e00ade7 100644 --- a/client/js/views/post_upload_view.js +++ b/client/js/views/post_upload_view.js @@ -20,6 +20,7 @@ function _mimeTypeToPostType(mimeType) { "image/avif": "image", "image/heif": "image", "image/heic": "image", + "image/vnd.adobe.photoshop": "image", "video/mp4": "video", "video/webm": "video", "video/quicktime": "video", @@ -165,7 +166,7 @@ class PostUploadView extends events.EventTarget { this._contentInputNode, { extraText: - "Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf, .avif, .heif, .heic", + "Allowed extensions: .jpg, .png, .gif, .webm, .mp4, .swf, .avif, .heif, .heic, .psd", allowUrls: true, allowMultiple: true, lock: false, diff --git a/server/Dockerfile b/server/Dockerfile index 3e4dadfb..8fbac5ca 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,9 +1,12 @@ -ARG ALPINE_VERSION=3.13 +ARG ALPINE_VERSION=3.16 - -FROM alpine:$ALPINE_VERSION as prereqs +####################################### +# Base Stage (prereqs) +####################################### +FROM alpine:$ALPINE_VERSION AS prereqs WORKDIR /opt/app +# Install system dependencies (Python, development packages, and runtime libraries) RUN apk --no-cache add \ python3 \ python3-dev \ @@ -13,31 +16,47 @@ RUN apk --no-cache add \ libheif-dev \ libavif \ libavif-dev \ + freetype-dev \ ffmpeg \ + lapack \ + lapack-dev \ # from requirements.txt: py3-yaml \ py3-psycopg2 \ - py3-sqlalchemy \ py3-certifi \ - py3-numpy \ - py3-pillow \ py3-pynacl \ py3-tz \ py3-pyrfc3339 + +# Install pip-only packages, using a wheel to avoid building scikit-image +RUN pip3 install --no-cache-dir wheel + RUN pip3 install --no-cache-dir --disable-pip-version-check \ + --extra-index-url https://alpine-wheels.github.io/index \ + "aggdraw==1.3.12" \ "alembic>=0.8.5" \ + "attrs==25.1.0" \ "coloredlogs==5.0" \ "pyheif==0.6.1" \ "heif-image-plugin>=0.3.2" \ yt-dlp \ - "pillow-avif-plugin~=1.1.0" + "pillow>=6.1.0" \ + "pillow-avif-plugin~=1.1.0" \ + "psd-tools==1.10.4" \ + "numpy==1.21.6" \ + "scikit-image==0.19.3" \ + "scipy==1.8.0" \ + "sqlalchemy==1.3.21" + RUN apk --no-cache del py3-pip COPY ./ /opt/app/ RUN rm -rf /opt/app/szurubooru/tests - -FROM --platform=$BUILDPLATFORM prereqs as testing +####################################### +# Testing Stage +####################################### +FROM --platform=$BUILDPLATFORM prereqs AS testing WORKDIR /opt/app RUN apk --no-cache add \ @@ -60,8 +79,10 @@ USER app ENTRYPOINT ["pytest", "--tb=short"] CMD ["szurubooru/"] - -FROM prereqs as release +####################################### +# Release Stage +####################################### +FROM prereqs AS release WORKDIR /opt/app ARG PUID=1000 diff --git a/server/requirements.txt b/server/requirements.txt index ffe18f0c..1e606127 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,15 +1,20 @@ +aggdraw==1.3.12 alembic>=0.8.5 +attrs==25.1.0 certifi>=2017.11.5 coloredlogs==5.0 -heif-image-plugin==0.3.2 -numpy>=1.8.2 +heif-image-plugin>=0.3.2 +numpy>=1.21.6 pillow-avif-plugin~=1.1.0 -pillow>=4.3.0 +pillow>=6.1.0 +psd-tools==1.10.4 psycopg2-binary>=2.6.1 pyheif==0.6.1 pynacl>=1.2.1 pyRFC3339>=1.0 pytz>=2018.3 pyyaml>=3.11 -SQLAlchemy>=1.0.12, <1.4 +scikit-image==0.19.3 +scipy==1.8.0 +SQLAlchemy==1.3.21 yt-dlp diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index e135d182..a9f495b3 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -7,13 +7,14 @@ import subprocess from io import BytesIO from typing import List -import HeifImagePlugin -import pillow_avif from PIL import Image as PILImage +from psd_tools import PSDImage + from szurubooru import errors from szurubooru.func import mime, util + logger = logging.getLogger(__name__) @@ -24,6 +25,15 @@ def convert_heif_to_png(content: bytes) -> bytes: return img_byte_arr.getvalue() + +def convert_psd_to_png(content: bytes) -> bytes: + psd_object = PSDImage.open(BytesIO(content)) + img = psd_object.composite() + img_byte_arr = BytesIO() + img.save(img_byte_arr, format="PNG") + return img_byte_arr.getvalue() + + class Image: def __init__(self, content: bytes) -> None: self.content = content @@ -265,10 +275,13 @@ class Image: get_logs: bool = False, ) -> bytes: mime_type = mime.get_mime_type(self.content) + # FFmpeg does not support HEIF or PSD. + # https://trac.ffmpeg.org/ticket/6521 + # https://ffmpeg.org/pipermail/ffmpeg-devel/2016-July/196477.html if mime.is_heif(mime_type): - # FFmpeg does not support HEIF. - # https://trac.ffmpeg.org/ticket/6521 self.content = convert_heif_to_png(self.content) + elif mime_type == "image/vnd.adobe.photoshop": + self.content = convert_psd_to_png(self.content) extension = mime.get_extension(mime_type) assert extension with util.create_temp_file(suffix="." + extension) as handle: diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py index 8fae5679..99c1bda9 100644 --- a/server/szurubooru/func/mime.py +++ b/server/szurubooru/func/mime.py @@ -33,6 +33,9 @@ def get_mime_type(content: bytes) -> str: if content[4:12] in (b"ftypheic", b"ftypheix"): return "image/heic" + if content[0:4] == b"8BPS": + return "image/vnd.adobe.photoshop" + if content[0:4] == b"\x1A\x45\xDF\xA3": return "video/webm" @@ -56,6 +59,7 @@ def get_extension(mime_type: str) -> Optional[str]: "image/avif": "avif", "image/heif": "heif", "image/heic": "heic", + "image/vnd.adobe.photoshop": "psd", "video/mp4": "mp4", "video/quicktime": "mov", "video/webm": "webm", @@ -87,6 +91,7 @@ def is_image(mime_type: str) -> bool: "image/avif", "image/heif", "image/heic", + "image/vnd.adobe.photoshop", )