3 Commits

Author SHA1 Message Date
1f242d3568 Merge 9d46f6cf58 into 376f687c38 2025-04-28 19:14:41 +00:00
9d46f6cf58 server-specific changes 2025-04-28 19:14:35 +00:00
0825866dce client+server/docker: add .psd support 2025-02-07 18:28:31 +01:00
13 changed files with 82 additions and 24 deletions

6
.gitignore vendored
View File

@ -13,3 +13,9 @@ server/**/lib/
server/**/bin/ server/**/bin/
server/**/pyvenv.cfg server/**/pyvenv.cfg
__pycache__/ __pycache__/
# Developer Tools
.vscode/
.idea/
*.sublime-project
*.sublime-workspace

View File

@ -7,7 +7,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features ## 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) - Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments - Post comments
- Post notes / annotations, including arbitrary polygons - Post notes / annotations, including arbitrary polygons

View File

@ -1,7 +1,11 @@
<div class='post-content post-type-<%- ctx.post.type %>'> <div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %> <% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/> <% if (ctx.post.mimeType === 'image/vnd.adobe.photoshop') { %>
<img class='resize-listener' alt='' src='<%- ctx.post.thumbnailUrl %>'/>
<% } else { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<% } %>
<% } else if (ctx.post.type === 'flash') { %> <% } else if (ctx.post.type === 'flash') { %>

View File

@ -13,6 +13,7 @@
'image/avif': 'AVIF', 'image/avif': 'AVIF',
'image/heif': 'HEIF', 'image/heif': 'HEIF',
'image/heic': 'HEIC', 'image/heic': 'HEIC',
'image/vnd.adobe.photoshop': 'PSD',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4', 'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV', 'video/quicktime': 'MOV',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -20,6 +20,7 @@ function _mimeTypeToPostType(mimeType) {
"image/avif": "image", "image/avif": "image",
"image/heif": "image", "image/heif": "image",
"image/heic": "image", "image/heic": "image",
"image/vnd.adobe.photoshop": "image",
"video/mp4": "video", "video/mp4": "video",
"video/webm": "video", "video/webm": "video",
"video/quicktime": "video", "video/quicktime": "video",
@ -165,7 +166,7 @@ class PostUploadView extends events.EventTarget {
this._contentInputNode, this._contentInputNode,
{ {
extraText: 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, allowUrls: true,
allowMultiple: true, allowMultiple: true,
lock: false, lock: false,

View File

@ -5,7 +5,7 @@
services: services:
server: server:
image: szurubooru/server:latest build: server
depends_on: depends_on:
- sql - sql
environment: environment:
@ -23,9 +23,10 @@ services:
volumes: volumes:
- "${MOUNT_DATA}:/data" - "${MOUNT_DATA}:/data"
- "./server/config.yaml:/opt/app/config.yaml" - "./server/config.yaml:/opt/app/config.yaml"
restart: unless-stopped
client: client:
image: szurubooru/client:latest build: client
depends_on: depends_on:
- server - server
environment: environment:
@ -35,6 +36,7 @@ services:
- "${MOUNT_DATA}:/data:ro" - "${MOUNT_DATA}:/data:ro"
ports: ports:
- "${PORT}:80" - "${PORT}:80"
restart: unless-stopped
sql: sql:
image: postgres:11-alpine image: postgres:11-alpine

View File

@ -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 WORKDIR /opt/app
# Install system dependencies (Python, development packages, and runtime libraries)
RUN apk --no-cache add \ RUN apk --no-cache add \
python3 \ python3 \
python3-dev \ python3-dev \
@ -13,31 +16,47 @@ RUN apk --no-cache add \
libheif-dev \ libheif-dev \
libavif \ libavif \
libavif-dev \ libavif-dev \
freetype-dev \
ffmpeg \ ffmpeg \
lapack \
lapack-dev \
# from requirements.txt: # from requirements.txt:
py3-yaml \ py3-yaml \
py3-psycopg2 \ py3-psycopg2 \
py3-sqlalchemy \
py3-certifi \ py3-certifi \
py3-numpy \
py3-pillow \
py3-pynacl \ py3-pynacl \
py3-tz \ py3-tz \
py3-pyrfc3339 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 \ 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" \ "alembic>=0.8.5" \
"attrs==25.1.0" \
"coloredlogs==5.0" \ "coloredlogs==5.0" \
"pyheif==0.6.1" \ "pyheif==0.6.1" \
"heif-image-plugin>=0.3.2" \ "heif-image-plugin>=0.3.2" \
yt-dlp \ 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 RUN apk --no-cache del py3-pip
COPY ./ /opt/app/ COPY ./ /opt/app/
RUN rm -rf /opt/app/szurubooru/tests RUN rm -rf /opt/app/szurubooru/tests
#######################################
FROM --platform=$BUILDPLATFORM prereqs as testing # Testing Stage
#######################################
FROM --platform=$BUILDPLATFORM prereqs AS testing
WORKDIR /opt/app WORKDIR /opt/app
RUN apk --no-cache add \ RUN apk --no-cache add \
@ -60,8 +79,10 @@ USER app
ENTRYPOINT ["pytest", "--tb=short"] ENTRYPOINT ["pytest", "--tb=short"]
CMD ["szurubooru/"] CMD ["szurubooru/"]
#######################################
FROM prereqs as release # Release Stage
#######################################
FROM prereqs AS release
WORKDIR /opt/app WORKDIR /opt/app
ARG PUID=1000 ARG PUID=1000

View File

@ -1,15 +1,20 @@
aggdraw==1.3.12
alembic>=0.8.5 alembic>=0.8.5
attrs==25.1.0
certifi>=2017.11.5 certifi>=2017.11.5
coloredlogs==5.0 coloredlogs==5.0
heif-image-plugin==0.3.2 heif-image-plugin>=0.3.2
numpy>=1.8.2 numpy>=1.21.6
pillow-avif-plugin~=1.1.0 pillow-avif-plugin~=1.1.0
pillow>=4.3.0 pillow>=6.1.0
psd-tools==1.10.4
psycopg2-binary>=2.6.1 psycopg2-binary>=2.6.1
pyheif==0.6.1 pyheif==0.6.1
pynacl>=1.2.1 pynacl>=1.2.1
pyRFC3339>=1.0 pyRFC3339>=1.0
pytz>=2018.3 pytz>=2018.3
pyyaml>=3.11 pyyaml>=3.11
SQLAlchemy>=1.0.12, <1.4 scikit-image==0.19.3
scipy==1.8.0
SQLAlchemy==1.3.21
yt-dlp yt-dlp

View File

@ -7,13 +7,14 @@ import subprocess
from io import BytesIO from io import BytesIO
from typing import List from typing import List
import HeifImagePlugin
import pillow_avif
from PIL import Image as PILImage from PIL import Image as PILImage
from psd_tools import PSDImage
from szurubooru import errors from szurubooru import errors
from szurubooru.func import mime, util from szurubooru.func import mime, util
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,6 +25,15 @@ def convert_heif_to_png(content: bytes) -> bytes:
return img_byte_arr.getvalue() 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: class Image:
def __init__(self, content: bytes) -> None: def __init__(self, content: bytes) -> None:
self.content = content self.content = content
@ -265,10 +275,13 @@ class Image:
get_logs: bool = False, get_logs: bool = False,
) -> bytes: ) -> bytes:
mime_type = mime.get_mime_type(self.content) 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): 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) 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) extension = mime.get_extension(mime_type)
assert extension assert extension
with util.create_temp_file(suffix="." + extension) as handle: with util.create_temp_file(suffix="." + extension) as handle:

View File

@ -33,6 +33,9 @@ def get_mime_type(content: bytes) -> str:
if content[4:12] in (b"ftypheic", b"ftypheix"): if content[4:12] in (b"ftypheic", b"ftypheix"):
return "image/heic" return "image/heic"
if content[0:4] == b"8BPS":
return "image/vnd.adobe.photoshop"
if content[0:4] == b"\x1A\x45\xDF\xA3": if content[0:4] == b"\x1A\x45\xDF\xA3":
return "video/webm" return "video/webm"
@ -56,6 +59,7 @@ def get_extension(mime_type: str) -> Optional[str]:
"image/avif": "avif", "image/avif": "avif",
"image/heif": "heif", "image/heif": "heif",
"image/heic": "heic", "image/heic": "heic",
"image/vnd.adobe.photoshop": "psd",
"video/mp4": "mp4", "video/mp4": "mp4",
"video/quicktime": "mov", "video/quicktime": "mov",
"video/webm": "webm", "video/webm": "webm",
@ -87,6 +91,7 @@ def is_image(mime_type: str) -> bool:
"image/avif", "image/avif",
"image/heif", "image/heif",
"image/heic", "image/heic",
"image/vnd.adobe.photoshop",
) )