diff --git a/.gitignore b/.gitignore
index b21e3adf..96ba748c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,9 @@ server/**/lib/
server/**/bin/
server/**/pyvenv.cfg
__pycache__/
+
+# Developer Tools
+.vscode/
+.idea/
+*.sublime-project
+*.sublime-workspace
diff --git a/README.md b/README.md
index 6a38501c..8f879978 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features
-- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
+- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations, project files (PSD)
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments
- Post notes / annotations, including arbitrary polygons
diff --git a/client/html/post_content.tpl b/client/html/post_content.tpl
index fd5b094c..3e75fa62 100644
--- a/client/html/post_content.tpl
+++ b/client/html/post_content.tpl
@@ -1,7 +1,11 @@
<% 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",
)