diff --git a/Dockerfile b/Dockerfile index c81e133..11ba329 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,22 +25,32 @@ RUN apt-get update && apt-get install -y \ && rm -rf /var/lib/apt/lists/* RUN cd ~ && \ - curl -L "https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.1.tar.gz" -o ffmpeg.tar.gz && \ + curl -L "https://github.com/webmproject/libwebp/archive/refs/tags/v1.4.0.tar.gz" -o libwebp.tar.gz && \ + tar -xzvf libwebp.tar.gz && \ + cd libwebp-1.4.0 && \ + cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \ + -DWEBP_LINK_STATIC=OFF -DCMAKE_INSTALL_PREFIX=/clib ../ && \ + make -j$(grep -c ^processor /proc/cpuinfo) && make install && \ + cd ~ && rm -rf libwebp-1.4.0 libwebp.tar.gz + +RUN cd ~ && \ + curl -L "https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.1.tar.gz" -o ffmpeg.tar.gz && \ tar -xzvf ffmpeg.tar.gz && \ - cd FFmpeg-n7.0.1 && \ + cd FFmpeg-n7.1 && \ ./configure --enable-pic --prefix=/clib --enable-shared --disable-static \ --enable-gpl --enable-version3 --disable-doc --disable-ffplay \ --disable-network --disable-autodetect --enable-zlib \ - --disable-encoders --enable-encoder=mjpeg \ + --disable-encoders --enable-encoder=mjpeg,libwebp \ --disable-muxers --enable-muxer=image2,image2pipe \ --disable-decoders --enable-decoder=mjpeg,png,gif \ --disable-demuxers --enable-demuxer=image_jpeg_pipe,image_png_pipe,image_gif_pipe \ --disable-parsers --enable-parser=h264,png,gif \ --disable-bsfs --enable-bsf=dts2pts,null \ --disable-protocols --enable-protocol=async,concat,concatf,data,fd,file,md5,pipe,subfile \ - --disable-devices --disable-filters --enable-filter=crop,pad,scale && \ + --disable-devices --disable-filters --enable-filter=crop,pad,scale \ + --enable-libwebp && \ make -j$(grep -c ^processor /proc/cpuinfo) && make install && \ - cd ~ && rm -rf FFmpeg-n7.0.1 ffmpeg.tar.gz + cd ~ && rm -rf FFmpeg-n7.1 ffmpeg.tar.gz RUN cd ~ && \ curl -L "https://github.com/curl/curl/releases/download/curl-8_8_0/curl-8.8.0.tar.gz" -o curl-8.8.0.tar.gz && \ diff --git a/api.yml b/api.yml index 3b6f210..9a34fb5 100644 --- a/api.yml +++ b/api.yml @@ -188,6 +188,8 @@ components: enable_server_timing: type: boolean description: Whether to enable server time tracking + thumbnail_format: + $ref: "#/components/schemas/ThumbnailFormat" Config: allOf: - $ref: "#/components/schemas/ConfigOptional" @@ -223,6 +225,7 @@ components: - import_method - max_import_img_count - enable_server_timing + - thumbnail_format ConfigUpdated: description: result of updateConfig type: object @@ -1004,6 +1007,16 @@ components: const: 4 - title: UpdateTagTranslation const: 5 + ThumbnailFormat: + description: Thumbnail format + type: integer + oneOf: + - title: JPEG + const: 0 + description: MJPEG + - title: WEBP + const: 1 + description: WEBP ThumbnailMethod: description: Thumbnail method type: integer diff --git a/config.ts b/config.ts index c4d6883..abda194 100644 --- a/config.ts +++ b/config.ts @@ -40,6 +40,7 @@ export type ConfigType = { import_method: ImportMethod; max_import_img_count: number; enable_server_timing: boolean; + thumbnail_format: ThumbnailFormat; }; export enum ThumbnailMethod { @@ -47,6 +48,11 @@ export enum ThumbnailMethod { FFMPEG_API, } +export enum ThumbnailFormat { + JPEG, + WEBP, +} + export enum ImportMethod { Copy, CopyThenDelete, @@ -173,6 +179,11 @@ export class Config { get thumbnail_dir() { return this._return_string("thumbnail_dir") || "./thumbnails"; } + get thumbnail_format() { + const n = this._return_number("thumbnail_format") || 0; + if (n < 0 || n > 1) return ThumbnailFormat.JPEG; + return n as ThumbnailFormat; + } get remove_previous_gallery() { return this._return_bool("remove_previous_gallery") || false; } @@ -282,6 +293,7 @@ export class Config { import_method: this.import_method, max_import_img_count: this.max_import_img_count, enable_server_timing: this.enable_server_timing, + thumbnail_format: this.thumbnail_format, }; } } diff --git a/routes/api/thumbnail/[id].ts b/routes/api/thumbnail/[id].ts index 40021dd..cc1b97c 100644 --- a/routes/api/thumbnail/[id].ts +++ b/routes/api/thumbnail/[id].ts @@ -11,7 +11,7 @@ import { ThumbnailGenMethod, } from "../../../thumbnail/base.ts"; import { compareNum, isNumNaN, parseBigInt, sure_dir } from "../../../utils.ts"; -import { ThumbnailMethod } from "../../../config.ts"; +import { ThumbnailFormat, ThumbnailMethod } from "../../../config.ts"; import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts"; import { get_file_response, @@ -130,7 +130,11 @@ export const handler: Handlers = { ); } } - const output = generate_filename(b, f, cfg); + let fmt = m.cfg.thumbnail_format; + if (method == ThumbnailMethod.FFMPEG_API) { + fmt = ThumbnailFormat.JPEG; + } + const output = generate_filename(b, f, cfg, fmt); if (!(await exists(output))) { if (method === ThumbnailMethod.FFMPEG_BINARY) { cfg.input = { @@ -142,6 +146,7 @@ export const handler: Handlers = { f.path, output, cfg, + fmt, ); if (!re) { return new Response("Failed to generate thumbnail.", { @@ -171,7 +176,7 @@ export const handler: Handlers = { const verify = u.searchParams.get("verify"); if (verify === null) { const bs = new SortableURLSearchParams( - gen_thumbnail_config_params(cfg), + gen_thumbnail_config_params(cfg, fmt), ["verify"], ); const tverify = encode( @@ -188,8 +193,9 @@ export const handler: Handlers = { const b = new URLSearchParams(bs.toString()); b.append("verify", tverify); if (m.cfg.use_path_based_img_url) { + const ext = fmt == ThumbnailFormat.WEBP ? "webp" : "jpg"; return Response.redirect( - `${get_host(req)}/thumbnail/${b}/${f.id}.jpg`, + `${get_host(req)}/thumbnail/${b}/${f.id}.${ext}`, ); } return Response.redirect( diff --git a/routes/thumbnail/[id].ts b/routes/thumbnail/[id].ts index 966f0ef..39c9b54 100644 --- a/routes/thumbnail/[id].ts +++ b/routes/thumbnail/[id].ts @@ -47,6 +47,7 @@ export const handler: Handlers = { const quality = await parse_int(u.searchParams.get("quality"), null); const method = await parse_int(u.searchParams.get("method"), null); const align = await parse_int(u.searchParams.get("align"), null); + const fmt = await parse_int(u.searchParams.get("fmt"), 0); if ( width === null || height === null || quality === null || method === null || align === null @@ -64,7 +65,7 @@ export const handler: Handlers = { method, align, }; - const output = generate_filename(b, f, cfg); + const output = generate_filename(b, f, cfg, fmt); if (!(await exists(output))) { return new Response("file not exists.", { status: 500 }); } diff --git a/routes/thumbnail/[verify]/[id].ts b/routes/thumbnail/[verify]/[id].ts index 26f8b1a..4c8852b 100644 --- a/routes/thumbnail/[verify]/[id].ts +++ b/routes/thumbnail/[verify]/[id].ts @@ -49,6 +49,7 @@ export const handler: Handlers = { const quality = await parse_int(search.get("quality"), null); const method = await parse_int(search.get("method"), null); const align = await parse_int(search.get("align"), null); + const fmt = await parse_int(search.get("fmt"), 0); if ( width === null || height === null || quality === null || method === null || align === null @@ -66,7 +67,7 @@ export const handler: Handlers = { method, align, }; - const output = generate_filename(b, f, cfg); + const output = generate_filename(b, f, cfg, fmt); if (!(await exists(output))) { return new Response("file not exists.", { status: 500 }); } diff --git a/tasks/update_tag_translation.ts b/tasks/update_tag_translation.ts index 4b74be7..b235ea6 100644 --- a/tasks/update_tag_translation.ts +++ b/tasks/update_tag_translation.ts @@ -37,7 +37,10 @@ export async function update_tag_translation( } sendEvent(); const file = isDocker() ? "/tmp/utt.lock" : "./utt.lock"; - await Deno.writeTextFile(file, "", { create: true, signal: manager.aborts }); + await Deno.writeTextFile(file, "", { + create: true, + signal: manager.aborts, + }); for (const d of f.data) { await asyncForEach(Object.getOwnPropertyNames(d.data), async (name) => { const tag = `${d.namespace}:${name}`; diff --git a/thumbnail/base.ts b/thumbnail/base.ts index 42de7ed..b4f671d 100644 --- a/thumbnail/base.ts +++ b/thumbnail/base.ts @@ -1,6 +1,7 @@ import { join } from "@std/path"; import { filterFilename } from "../utils.ts"; import type { EhFile } from "../db.ts"; +import { ThumbnailFormat } from "../config.ts"; export enum ThumbnailGenMethod { Unknown, @@ -26,13 +27,17 @@ export type ThumbnailConfig = { input?: { width: number; height: number }; }; -export function gen_thumbnail_config_params(cfg: ThumbnailConfig) { +export function gen_thumbnail_config_params( + cfg: ThumbnailConfig, + fmt: ThumbnailFormat, +) { return { width: cfg.width.toString(), height: cfg.height.toString(), quality: cfg.quality.toString(), method: cfg.method.toString(), align: cfg.align.toString(), + fmt: fmt.toString(), }; } @@ -69,10 +74,12 @@ export function generate_filename( base: string, f: EhFile, cfg: ThumbnailConfig, + fmt: ThumbnailFormat, ) { let method = ""; let balign = ""; let align = ""; + const ext = fmt == ThumbnailFormat.JPEG ? "jpg" : "webp"; switch (cfg.align) { case ThumbnailAlign.Left: balign = "-left"; @@ -100,7 +107,7 @@ export function generate_filename( return join( base, filterFilename( - `${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}${method}${align}.jpg`, + `${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}${method}${align}.${ext}`, ), ); } diff --git a/thumbnail/ffmpeg_binary.ts b/thumbnail/ffmpeg_binary.ts index af16a67..e72908f 100644 --- a/thumbnail/ffmpeg_binary.ts +++ b/thumbnail/ffmpeg_binary.ts @@ -1,3 +1,4 @@ +import { ThumbnailFormat } from "../config.ts"; import { ThumbnailAlign } from "./base.ts"; import { type ThumbnailConfig, ThumbnailGenMethod } from "./base.ts"; @@ -45,8 +46,10 @@ export async function fb_generate_thumbnail( i: string, o: string, cfg: ThumbnailConfig, + fmt: ThumbnailFormat, ) { let add = ""; + const codec = fmt == ThumbnailFormat.WEBP ? "libwebp" : "mjpeg"; if (cfg.method == ThumbnailGenMethod.Cover) { const size = cfg.input ?? await fb_get_size(i); if (!size) return false; @@ -90,6 +93,8 @@ export async function fb_generate_thumbnail( "-n", "-i", i, + "-c", + codec, "-vf", add, "-qmin",