diff --git a/.gitignore b/.gitignore index 01bbf5a..46694a2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ downloads/ utt.lock thumbnails/ _fresh/ +lib/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..55c16bf --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "extensions/utils"] + path = extensions/utils + url = https://github.com/lifegpc/c-utils diff --git a/deno.json b/deno.json index d8d99e1..25b08a9 100644 --- a/deno.json +++ b/deno.json @@ -19,7 +19,8 @@ "config.json", "static/sw.js", "static/sw.meta.json", - "_fresh" + "_fresh", + "extensions/build" ] }, "compilerOptions": { diff --git a/extensions/.gitignore b/extensions/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/extensions/.gitignore @@ -0,0 +1 @@ +build diff --git a/extensions/CMakeLists.txt b/extensions/CMakeLists.txt new file mode 100644 index 0000000..60e5283 --- /dev/null +++ b/extensions/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.18) + +project(extensions) + +set(ENABLE_ICONV OFF CACHE BOOL "Libiconv is not needed.") +add_subdirectory(utils) +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/utils") + +add_subdirectory(thumbnail) diff --git a/extensions/cmake/FindAVCODEC.cmake b/extensions/cmake/FindAVCODEC.cmake new file mode 100644 index 0000000..c55c536 --- /dev/null +++ b/extensions/cmake/FindAVCODEC.cmake @@ -0,0 +1,33 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_AVCODEC QUIET IMPORTED_TARGET GLOBAL libavcodec) +endif() + +if (PC_AVCODEC_FOUND) + set(AVCODEC_FOUND TRUE) + set(AVCODEC_VERSION ${PC_AVCODEC_VERSION}) + set(AVCODEC_VERSION_STRING ${PC_AVCODEC_STRING}) + set(AVCODEC_LIBRARYS ${PC_AVCODEC_LIBRARIES}) + if (USE_STATIC_LIBS) + set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_STATIC_INCLUDE_DIRS}) + else() + set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_INCLUDE_DIRS}) + endif() + if (NOT AVCODEC_INCLUDE_DIRS) + find_path(AVCODEC_INCLUDE_DIRS NAMES libavcodec/avcodec.h) + endif() + if (NOT TARGET AVCODEC::AVCODEC) + add_library(AVCODEC::AVCODEC ALIAS PkgConfig::PC_AVCODEC) + endif() +else() + message(FATAL_ERROR "failed.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AVCODEC + FOUND_VAR AVCODEC_FOUND + REQUIRED_VARS + AVCODEC_LIBRARYS + AVCODEC_INCLUDE_DIRS + VERSION_VAR AVCODEC_VERSION +) diff --git a/extensions/cmake/FindAVFORMAT.cmake b/extensions/cmake/FindAVFORMAT.cmake new file mode 100644 index 0000000..1f18044 --- /dev/null +++ b/extensions/cmake/FindAVFORMAT.cmake @@ -0,0 +1,33 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_AVFORMAT QUIET IMPORTED_TARGET GLOBAL libavformat) +endif() + +if (PC_AVFORMAT_FOUND) + set(AVFORMAT_FOUND TRUE) + set(AVFORMAT_VERSION ${PC_AVFORMAT_VERSION}) + set(AVFORMAT_VERSION_STRING ${PC_AVFORMAT_STRING}) + set(AVFORMAT_LIBRARYS ${PC_AVFORMAT_LIBRARIES}) + if (USE_STATIC_LIBS) + set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_STATIC_INCLUDE_DIRS}) + else() + set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_INCLUDE_DIRS}) + endif() + if (NOT AVFORMAT_INCLUDE_DIRS) + find_path(AVFORMAT_INCLUDE_DIRS NAMES libavformat/avformat.h) + endif() + if (NOT TARGET AVFORMAT::AVFORMAT) + add_library(AVFORMAT::AVFORMAT ALIAS PkgConfig::PC_AVFORMAT) + endif() +else() + message(FATAL_ERROR "failed.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AVFORMAT + FOUND_VAR AVFORMAT_FOUND + REQUIRED_VARS + AVFORMAT_LIBRARYS + AVFORMAT_INCLUDE_DIRS + VERSION_VAR AVFORMAT_VERSION +) diff --git a/extensions/cmake/FindAVUTIL.cmake b/extensions/cmake/FindAVUTIL.cmake new file mode 100644 index 0000000..be90604 --- /dev/null +++ b/extensions/cmake/FindAVUTIL.cmake @@ -0,0 +1,33 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_AVUTIL QUIET IMPORTED_TARGET GLOBAL libavutil) +endif() + +if (PC_AVUTIL_FOUND) + set(AVUTIL_FOUND TRUE) + set(AVUTIL_VERSION ${PC_AVUTIL_VERSION}) + set(AVUTIL_VERSION_STRING ${PC_AVUTIL_STRING}) + set(AVUTIL_LIBRARYS ${PC_AVUTIL_LIBRARIES}) + if (USE_STATIC_LIBS) + set(AVUTIL_INCLUDE_DIRS ${PC_AVUTIL_STATIC_INCLUDE_DIRS}) + else() + set(AVUTIL_INCLUDE_DIRS ${PC_AVUTIL_INCLUDE_DIRS}) + endif() + if (NOT AVUTIL_INCLUDE_DIRS) + find_path(AVUTIL_INCLUDE_DIRS NAMES libavutil/avutil.h) + endif() + if (NOT TARGET AVUTIL::AVUTIL) + add_library(AVUTIL::AVUTIL ALIAS PkgConfig::PC_AVUTIL) + endif() +else() + message(FATAL_ERROR "failed.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AVUTIL + FOUND_VAR AVUTIL_FOUND + REQUIRED_VARS + AVUTIL_LIBRARYS + AVUTIL_INCLUDE_DIRS + VERSION_VAR AVUTIL_VERSION +) diff --git a/extensions/cmake/FindSWSCALE.cmake b/extensions/cmake/FindSWSCALE.cmake new file mode 100644 index 0000000..49c5d98 --- /dev/null +++ b/extensions/cmake/FindSWSCALE.cmake @@ -0,0 +1,32 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_SWSCALE QUIET IMPORTED_TARGET GLOBAL libswscale) +endif() + +if (PC_SWSCALE_FOUND) + set(SWSCALE_FOUND TRUE) + set(SWSCALE_VERSION ${PC_SWSCALE_VERSION}) + set(SWSCALE_VERSION_STRING ${PC_SWSCALE_STRING}) + set(SWSCALE_LIBRARYS ${PC_SWSCALE_LIBRARIES}) + if (USE_STATIC_LIBS) + set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_STATIC_INCLUDE_DIRS}) + else() + set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_INCLUDE_DIRS}) + endif() + if (NOT SWSCALE_INCLUDE_DIRS) + find_path(SWSCALE_INCLUDE_DIRS NAMES libswscale/swscale.h) + endif() + if (NOT TARGET SWSCALE::SWSCALE) + add_library(SWSCALE::SWSCALE ALIAS PkgConfig::PC_SWSCALE) + endif() +else() + message(FATAL_ERROR "failed.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SWSCALE + FOUND_VAR SWSCALE_FOUND + REQUIRED_VARS + SWSCALE_LIBRARYS + VERSION_VAR SWSCALE_VERSION +) diff --git a/extensions/thumbnail/CMakeLists.txt b/extensions/thumbnail/CMakeLists.txt new file mode 100644 index 0000000..4ddce30 --- /dev/null +++ b/extensions/thumbnail/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.18) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") + +project(thumbnail) +if (MSVC) + add_compile_options(/utf-8) +endif() + +if (NOT TARGET utils) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../utils" "${CMAKE_BINARY_DIR}/utils") + include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../utils") +endif() + +find_package(AVFORMAT 58 REQUIRED) +find_package(AVCODEC 58 REQUIRED) +find_package(AVUTIL 56 REQUIRED) +find_package(SWSCALE 5 REQUIRED) + +include(GNUInstallDirs) + +add_library(thumbnail SHARED thumbnail.h thumbnail.c) +target_link_libraries(thumbnail AVFORMAT::AVFORMAT) +target_link_libraries(thumbnail AVCODEC::AVCODEC) +target_link_libraries(thumbnail AVUTIL::AVUTIL) +target_link_libraries(thumbnail SWSCALE::SWSCALE) +target_link_libraries(thumbnail utils) + +install(TARGETS thumbnail) diff --git a/extensions/thumbnail/thumbnail.c b/extensions/thumbnail/thumbnail.c new file mode 100644 index 0000000..eea51ca --- /dev/null +++ b/extensions/thumbnail/thumbnail.c @@ -0,0 +1,329 @@ +#include "thumbnail.h" +#include "cfileop.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libswscale/swscale.h" +#include + +PUBLIC_API void thumbnail_fferror(int e, char* buf, size_t bufsize) { + if (!buf) return; + av_make_error_string(buf, bufsize, e); +} + +int thumbnail_convert_frame(THUMBNAIL_ERROR* err, AVFrame* ifr, AVFrame** ofr, AVCodecContext* occ, THUMBNAIL_METHOD method) { + if (!err || !ifr || !ofr) return 1; + int re = 0; + struct SwsContext* sws = NULL; + AVFrame* fr = NULL, * fr2 = NULL; + if (!(fr = av_frame_alloc())) { + err->e = THUMBNAIL_OOM; + re = 1; + goto end; + } + int theight = ifr->height * occ->width / ifr->width; + char simple_way = 0; + if (occ->height == theight || method == THUMBNAIL_FILL) { + simple_way = 1; + } + if (simple_way) { + fr->width = occ->width; + fr->height = occ->height; + fr->format = occ->pix_fmt; + fr->sample_aspect_ratio = ifr->sample_aspect_ratio; + } + if ((err->fferr = av_frame_get_buffer(fr, 0)) < 0) { + err->e = THUMBNAIL_FFMPEG_ERROR; + re = 1; + av_log(NULL, AV_LOG_ERROR, "Failed to get buffer for output frame: %s\n", av_err2str(err->fferr)); + goto end; + } + if ((err->fferr = av_frame_make_writable(fr)) < 0) { + err->e = THUMBNAIL_FFMPEG_ERROR; + re = 1; + av_log(NULL, AV_LOG_ERROR, "Failed to make writeable for output frame: %s\n", av_err2str(err->fferr)); + goto end; + } + if (simple_way) { + if (!(sws = sws_getContext(ifr->width, ifr->height, (enum AVPixelFormat)ifr->format, fr->width, fr->height, (enum AVPixelFormat)fr->format, SWS_BILINEAR, NULL, NULL, NULL))) { + err->e = THUMBNAIL_UNABLE_SCALE; + re = 1; + goto end; + } + if ((err->fferr = sws_scale(sws, (const uint8_t* const*)ifr->data, ifr->linesize, 0, ifr->height, fr->data, fr->linesize)) < 0) { + err->e = THUMBNAIL_UNABLE_SCALE; + re = 1; + goto end; + } + } +end: + if (re == 1 && fr) av_frame_free(&fr); + else if (re == 0) *ofr = fr; + if (fr2) av_frame_free(&fr2); + if (sws) sws_freeContext(sws); + return re; +} + +int thumbnail_encode_video(THUMBNAIL_ERROR* err, AVFrame* ofr, AVFormatContext* oc, AVCodecContext* occ, char* writed_data) { + if (!err || !oc || !occ || !writed_data) return 1; + int re = 0; + AVPacket* pkt = av_packet_alloc(); + if (!pkt) { + err->e = THUMBNAIL_OOM; + re = 1; + goto end; + } + *writed_data = 0; + if (ofr) { + ofr->pts = 0; + ofr->pkt_dts = 0; + } + if ((err->fferr = avcodec_send_frame(occ, ofr)) < 0) { + if (err->fferr == AVERROR_EOF) { + err->fferr = 0; + } else { + av_log(NULL, AV_LOG_ERROR, "Failed to send frame to encoder: %s\n", av_err2str(err->fferr)); + err->e = THUMBNAIL_FFMPEG_ERROR; + re = 1; + goto end; + } + } + err->fferr = avcodec_receive_packet(occ, pkt); + if (err->fferr >= 0) { + *writed_data = 1; + } else if (err->fferr == AVERROR_EOF || err->fferr == AVERROR(EAGAIN)) { + err->fferr = 0; + goto end; + } else { + av_log(NULL, AV_LOG_ERROR, "Failed to recive data from encoder: %s\n", av_err2str(err->fferr)); + err->e = THUMBNAIL_FFMPEG_ERROR; + re = 1; + goto end; + } + if (*writed_data && pkt) { + pkt->stream_index = 0; + if ((err->fferr = av_write_frame(oc, pkt)) < 0) { + err->e = THUMBNAIL_FFMPEG_ERROR; + av_log(NULL, AV_LOG_ERROR, "Failed to write data to muxer: %s\n", av_err2str(err->fferr)); + re = 1; + goto end; + } + } +end: + if (pkt) av_packet_free(&pkt); + return re; +} + +THUMBNAIL_ERROR gen_thumbnail(const char* src, const char* dest, int width, int height, THUMBNAIL_METHOD method) { + THUMBNAIL_ERROR re = { THUMBNAIL_OK, 0 }; + AVFormatContext* ic = NULL, * oc = NULL; + AVStream* is = NULL, * os = NULL; + const AVCodec* input_codec = NULL, * output_codec = NULL; + AVCodecContext* icc = NULL, * occ = NULL; + AVPacket pkt; + AVFrame* ifr = NULL, * ofr = NULL; + if (method < THUMBNAIL_COVER || method > THUMBNAIL_FILL) { + re.e = THUMBNAIL_UNKNOWN_METHOD; + goto end; + } + if (!src || !dest) { + re.e = THUMBNAIL_NULL_POINTER; + goto end; + } + if (fileop_exists(dest)) { + if (!fileop_remove(dest)) { + re.e = THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED; + goto end; + } + } + if ((re.fferr = avformat_open_input(&ic, src, NULL, NULL)) < 0) { + re.e = THUMBNAIL_FFMPEG_ERROR; + av_log(NULL, AV_LOG_ERROR, "Failed to open file: %s\n", av_err2str(re.fferr)); + goto end; + } + if ((re.fferr = avformat_find_stream_info(ic, NULL)) < 0) { + re.e = THUMBNAIL_FFMPEG_ERROR; + av_log(NULL, AV_LOG_ERROR, "Failed to find stream info in file: %s\n", av_err2str(re.fferr)); + goto end; + } + for (unsigned int i = 0; i < ic->nb_streams; i++) { + AVStream* s = ic->streams[i]; + if (s->codecpar && s->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + is = s; + break; + } + } + if (!is) { + re.e = THUMBNAIL_NO_VIDEO_STREAM; + goto end; + } + if ((re.fferr = avformat_alloc_output_context2(&oc, NULL, "mjpeg", dest)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to allocate output context: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (!(input_codec = avcodec_find_decoder(is->codecpar->codec_id))) { + re.e = THUMBNAIL_NO_DECODER; + goto end; + } + if (!(icc = avcodec_alloc_context3(input_codec))) { + re.e = THUMBNAIL_OOM; + goto end; + } + if ((re.fferr = avcodec_parameters_to_context(icc, is->codecpar)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to copy decode parameters: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if ((re.fferr = avcodec_open2(icc, input_codec, NULL)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to open decoder: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + output_codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG); + if (!output_codec) { + re.e = THUMBNAIL_NO_ENCODER; + goto end; + } + if (!(occ = avcodec_alloc_context3(output_codec))) { + re.e = THUMBNAIL_OOM; + goto end; + } + occ->width = width; + occ->height = height; + occ->pix_fmt = AV_PIX_FMT_YUVJ420P; + occ->sample_aspect_ratio = icc->sample_aspect_ratio; + occ->time_base = AV_TIME_BASE_Q; + occ->color_range = AVCOL_RANGE_JPEG; + if ((re.fferr = avcodec_open2(occ, output_codec, NULL)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to open encoder: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (!(os = avformat_new_stream(oc, NULL))) { + re.e = THUMBNAIL_OOM; + goto end; + } + if ((re.fferr = avcodec_parameters_from_context(os->codecpar, occ)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder's params to muxer: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (!(oc->oformat->flags & AVFMT_NOFILE)) { + if ((re.fferr = avio_open(&oc->pb, dest, AVIO_FLAG_WRITE)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to open output file: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + } + if ((re.fferr = avformat_write_header(oc, NULL)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to write headers to file: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + while (1) { + if ((re.fferr = av_read_frame(ic, &pkt)) < 0) { + av_log(NULL, AV_LOG_ERROR, "Failed to read frames from file: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (pkt.data == NULL) { + av_packet_unref(&pkt); + av_log(NULL, AV_LOG_ERROR, "No data find in frames: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (pkt.stream_index != is->index) { + av_packet_unref(&pkt); + continue; + } + if (!(ifr = av_frame_alloc())) { + av_packet_unref(&pkt); + re.e = THUMBNAIL_OOM; + goto end; + } + if ((re.fferr = avcodec_send_packet(icc, &pkt)) < 0) { + av_packet_unref(&pkt); + av_log(NULL, AV_LOG_ERROR, "Failed to send packet to decoder: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if ((re.fferr = avcodec_receive_frame(icc, ifr)) < 0) { + if (re.fferr == AVERROR(EAGAIN)) { + av_packet_unref(&pkt); + re.fferr = 0; + continue; + } + av_packet_unref(&pkt); + av_log(NULL, AV_LOG_ERROR, "Failed to receive frame from decoder: %s\n", av_err2str(re.fferr)); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + if (thumbnail_convert_frame(&re, ifr, &ofr, occ, method)) { + av_packet_unref(&pkt); + re.e = THUMBNAIL_FFMPEG_ERROR; + goto end; + } + char writed = 0; + if (thumbnail_encode_video(&re, ofr, oc, occ, &writed)) { + av_packet_unref(&pkt); + goto end; + } + while (1) { + if (thumbnail_encode_video(&re, NULL, oc, occ, &writed)) { + av_packet_unref(&pkt); + goto end; + } + if (!writed) break; + } + av_packet_unref(&pkt); + break; + } + av_write_trailer(oc); +end: + if (ifr) av_frame_free(&ifr); + if (ofr) av_frame_free(&ofr); + if (icc) avcodec_free_context(&icc); + if (occ) avcodec_free_context(&occ); + if (oc) { + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + avformat_free_context(oc); + } + if (ic) avformat_close_input(&ic); + return re; +} + +const char* thumbnail_berror(THUMBNAIL_ERROR_E e) { + switch (e) { + case THUMBNAIL_OK: + return "OK"; + case THUMBNAIL_FFMPEG_ERROR: + return "A error occured in ffmpeg code."; + case THUMBNAIL_NULL_POINTER: + return "Arguments have null pointers."; + case THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED: + return "Can not remove output file."; + case THUMBNAIL_NO_VIDEO_STREAM: + return "Can not find video stream in source file."; + case THUMBNAIL_OOM: + return "Out of memory."; + case THUMBNAIL_NO_DECODER: + return "No available decoder."; + case THUMBNAIL_NO_ENCODER: + return "No available encoder."; + case THUMBNAIL_UNABLE_SCALE: + return "Unable to scale image."; + default: + return "Unknown error."; + } +} + +void thumbnail_error(THUMBNAIL_ERROR e, char* buf, size_t bufsize) { + switch (e.e) { + case THUMBNAIL_FFMPEG_ERROR: + thumbnail_fferror(e.fferr, buf, bufsize); + break; + default: + strncpy(buf, thumbnail_berror(e.e), bufsize); + break; + } +} diff --git a/extensions/thumbnail/thumbnail.h b/extensions/thumbnail/thumbnail.h new file mode 100644 index 0000000..ba83365 --- /dev/null +++ b/extensions/thumbnail/thumbnail.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#define PUBLIC_API __declspec(dllexport) + +typedef enum THUMBNAIL_ERROR_E { + THUMBNAIL_OK, + THUMBNAIL_NULL_POINTER, + THUMBNAIL_REMOVE_OUTPUT_FILE_FAILED, + THUMBNAIL_FFMPEG_ERROR, + THUMBNAIL_NO_VIDEO_STREAM, + THUMBNAIL_UNKNOWN_METHOD, + THUMBNAIL_NO_DECODER, + THUMBNAIL_OOM, + THUMBNAIL_NO_ENCODER, + THUMBNAIL_UNABLE_SCALE, +} THUMBNAIL_ERROR_E; + +typedef struct THUMBNAIL_ERROR { + THUMBNAIL_ERROR_E e; + int fferr; +}THUMBNAIL_ERROR; + +typedef enum THUMBNAIL_METHOD { + THUMBNAIL_COVER = 1, + THUMBNAIL_CONTAIN, + THUMBNAIL_FILL, +} THUMBNAIL_METHOD; + +PUBLIC_API THUMBNAIL_ERROR gen_thumbnail(const char* src, const char* dest, int width, int height, THUMBNAIL_METHOD method); +PUBLIC_API void thumbnail_error(THUMBNAIL_ERROR e, char* buf, size_t bufsize); diff --git a/extensions/utils b/extensions/utils new file mode 160000 index 0000000..40e3df5 --- /dev/null +++ b/extensions/utils @@ -0,0 +1 @@ +Subproject commit 40e3df54a92500264f48359811bb5bb641bd01c4 diff --git a/import_map.json b/import_map.json index c31e034..969d33c 100644 --- a/import_map.json +++ b/import_map.json @@ -27,6 +27,7 @@ "@material/web/": "https://unpkg.lifegpc.workers.dev/@material/web@1.0.0-pre.13/", "@lit-labs/react/": "https://esm.sh/@lit-labs/react@1.2.1/", "bootstrap/": "https://esm.sh/bootstrap@5.3.0/", - "filesize": "https://esm.sh/filesize@10.0.7" + "filesize": "https://esm.sh/filesize@10.0.7", + "pwn/": "https://deno.land/x/pwn@1.0.0/" } } diff --git a/routes/api/thumbnail/[id].ts b/routes/api/thumbnail/[id].ts index 5fd4cd9..2195466 100644 --- a/routes/api/thumbnail/[id].ts +++ b/routes/api/thumbnail/[id].ts @@ -2,7 +2,11 @@ import { Handlers } from "$fresh/server.ts"; import { exists } from "std/fs/exists.ts"; import { get_task_manager } from "../../../server.ts"; import { parse_bool, parse_int } from "../../../server/parse_form.ts"; -import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts"; +import { + generate_filename, + ThumbnailConfig, + ThumbnailGenMethod, +} from "../../../thumbnail/base.ts"; import { sure_dir } from "../../../utils.ts"; import { ThumbnailMethod } from "../../../config.ts"; import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts"; @@ -35,7 +39,12 @@ export const handler: Handlers = { const height = await parse_int(u.searchParams.get("height"), null); const quality = await parse_int(u.searchParams.get("quality"), 1); const force = await parse_bool(u.searchParams.get("force"), false); - const cfg: ThumbnailConfig = { width: 0, height: 0, quality }; + const cfg: ThumbnailConfig = { + width: 0, + height: 0, + quality, + method: ThumbnailGenMethod.Unknown, + }; if (width !== null && height !== null) { cfg.width = width; cfg.height = height; diff --git a/thumbnail/base.ts b/thumbnail/base.ts index ac34640..b450330 100644 --- a/thumbnail/base.ts +++ b/thumbnail/base.ts @@ -2,10 +2,18 @@ import { join } from "std/path/mod.ts"; import { filterFilename } from "../utils.ts"; import type { EhFile } from "../db.ts"; +export enum ThumbnailGenMethod { + Unknown, + Cover, + Contain, + Fill, +} + export type ThumbnailConfig = { width: number; height: number; quality: number; + method: ThumbnailGenMethod; }; export function generate_filename( @@ -13,10 +21,22 @@ export function generate_filename( f: EhFile, cfg: ThumbnailConfig, ) { + let method = ""; + switch (cfg.method) { + case ThumbnailGenMethod.Cover: + method = "-cover"; + break; + case ThumbnailGenMethod.Contain: + method = "-contain"; + break; + case ThumbnailGenMethod.Fill: + method = "-fill"; + break; + } return join( base, filterFilename( - `${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}.jpg`, + `${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}${method}.jpg`, ), ); } diff --git a/thumbnail/ffmpeg_api.ts b/thumbnail/ffmpeg_api.ts new file mode 100644 index 0000000..233d178 --- /dev/null +++ b/thumbnail/ffmpeg_api.ts @@ -0,0 +1,68 @@ +/// +import { Struct } from "pwn/mod.ts"; +import { ThumbnailGenMethod } from "./base.ts"; + +let libSuffix = ""; +let libPrefix = "lib"; +switch (Deno.build.os) { + case "windows": + libSuffix = "dll"; + libPrefix = ""; + break; + case "darwin": + libSuffix = "dylib"; + break; + default: + libSuffix = "so"; + break; +} + +let libPath = import.meta.resolve(`../lib/${libPrefix}thumbnail.${libSuffix}`) + .slice(7); +if (Deno.build.os === "windows") { + libPath = libPath.slice(1); +} + +const lib = Deno.dlopen( + libPath, + { + "gen_thumbnail": { + "parameters": ["buffer", "buffer", "i32", "i32", "i32"], + "result": { struct: ["i32", "i32"] }, + "nonblocking": true, + }, + "thumbnail_error": { + "parameters": [{ struct: ["i32", "i32"] }, "buffer", "usize"], + "result": "void", + }, + } as const, +); +const _Result = new Struct({ e: "s32", fferr: "s32" }); + +function get_error(fferr: Uint8Array) { + const u = new Uint8Array(64); + lib.symbols.thumbnail_error(fferr, u, u.length); + let len = u.findIndex((i) => i === 0); + if (len === -1) len = u.length; + return (new TextDecoder()).decode(u.slice(0, len)); +} + +export async function gen_thumbnail( + src: string, + dest: string, + width: number, + height: number, + method: ThumbnailGenMethod, +) { + const t = new TextEncoder(); + const ore = await lib.symbols.gen_thumbnail( + t.encode(`${src}\0`), + t.encode(`${dest}\0`), + width, + height, + method, + ); + const re = _Result.unpack(ore); + if (re.e) return get_error(ore); + return; +}