media server 2/3 - vips
This commit is contained in:
parent
148b0f6e57
commit
d7b70fab8d
4
packages/media/cpp/.gitignore
vendored
4
packages/media/cpp/.gitignore
vendored
@ -4,3 +4,7 @@ dist/
|
||||
CMakeUserPresets.json
|
||||
.vs/
|
||||
*.user
|
||||
|
||||
# Windows libvips SDK (see scripts/fetch-vips-windows.ps1)
|
||||
third_party/vips-dev-*/
|
||||
third_party/vips-dev-x64-all*.zip
|
||||
|
||||
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
project(media-image-service
|
||||
VERSION 0.1.0
|
||||
DESCRIPTION "Polymech image resize service (CLI, REST, IPC)"
|
||||
DESCRIPTION "Polymech image resize service (CLI, REST, IPC) — libvips"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
@ -16,6 +16,16 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
# Windows: official libvips dev tree (see scripts/fetch-vips-windows.ps1) — vips-dev-* under third_party/
|
||||
if(WIN32)
|
||||
file(GLOB _MEDIA_VIPS_SDK "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vips-dev-*")
|
||||
if(_MEDIA_VIPS_SDK)
|
||||
list(APPEND CMAKE_PREFIX_PATH ${_MEDIA_VIPS_SDK})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
|
||||
@ -48,13 +58,6 @@ FetchContent_Declare(
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
FetchContent_Declare(
|
||||
stb
|
||||
GIT_REPOSITORY https://github.com/nothings/stb.git
|
||||
GIT_TAG master
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(cli11 asio nlohmann_json cpp_httplib)
|
||||
|
||||
# laserpants/dotenv-cpp — load .env (same pattern as packages/kbot/cpp).
|
||||
@ -72,13 +75,7 @@ add_library(laserpants_dotenv INTERFACE)
|
||||
target_include_directories(laserpants_dotenv INTERFACE ${laserpants_dotenv_SOURCE_DIR}/include)
|
||||
add_library(laserpants::dotenv ALIAS laserpants_dotenv)
|
||||
|
||||
FetchContent_GetProperties(stb)
|
||||
if(NOT stb_POPULATED)
|
||||
FetchContent_Populate(stb)
|
||||
endif()
|
||||
|
||||
add_library(stb_headers INTERFACE)
|
||||
target_include_directories(stb_headers INTERFACE ${stb_SOURCE_DIR})
|
||||
find_package(Vips REQUIRED)
|
||||
|
||||
add_executable(media-img
|
||||
src/main.cpp
|
||||
@ -110,9 +107,23 @@ target_link_libraries(media-img PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
httplib::httplib
|
||||
laserpants::dotenv
|
||||
stb_headers
|
||||
Vips::vips
|
||||
)
|
||||
|
||||
# GObject (g_object_ref / g_object_unref) — not re-exported through libvips import lib on MSVC.
|
||||
if(WIN32)
|
||||
file(GLOB _MEDIA_VIPS_SDK_LIST "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vips-dev-*")
|
||||
if(_MEDIA_VIPS_SDK_LIST)
|
||||
list(GET _MEDIA_VIPS_SDK_LIST 0 _MEDIA_VIPS_SDK)
|
||||
if(EXISTS "${_MEDIA_VIPS_SDK}/lib/libgobject-2.0.lib")
|
||||
target_link_libraries(media-img PRIVATE
|
||||
"${_MEDIA_VIPS_SDK}/lib/libgobject-2.0.lib"
|
||||
"${_MEDIA_VIPS_SDK}/lib/libglib-2.0.lib"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(media-img PRIVATE pthread)
|
||||
endif()
|
||||
@ -120,3 +131,21 @@ endif()
|
||||
target_compile_definitions(media-img PRIVATE
|
||||
"MEDIA_IMG_VERSION=\"${PROJECT_VERSION}\""
|
||||
)
|
||||
|
||||
# Runtime: libvips and deps are DLLs next to media-img.exe (Windows dev bundle bin/).
|
||||
if(WIN32)
|
||||
file(GLOB _MEDIA_VIPS_SDK_LIST "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vips-dev-*")
|
||||
if(_MEDIA_VIPS_SDK_LIST)
|
||||
list(GET _MEDIA_VIPS_SDK_LIST 0 _MEDIA_VIPS_SDK)
|
||||
if(EXISTS "${_MEDIA_VIPS_SDK}/bin")
|
||||
add_custom_command(
|
||||
TARGET media-img
|
||||
POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory
|
||||
"${_MEDIA_VIPS_SDK}/bin"
|
||||
"${CMAKE_SOURCE_DIR}/dist"
|
||||
COMMENT "Copy libvips DLLs to dist/"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@ -1,120 +1,97 @@
|
||||
# media-img (C++)
|
||||
|
||||
CMake-based **`media-img`** binary: **CLI resize**, **HTTP REST** (`serve`), and **line-delimited JSON IPC** (`ipc`) over **TCP** (all platforms) or **Unix domain sockets** (non-Windows). Loads optional **`.env`** from the working directory via [laserpants/dotenv-cpp](https://github.com/laserpants/dotenv-cpp) (same idea as kbot).
|
||||
CMake-based **`media-img`** binary: **CLI**, **HTTP REST** (`serve`), and **line-delimited JSON IPC** (`ipc`). Image processing uses **libvips** — the same engine as **[Sharp](https://sharp.pixelplumbing.com/)** (Node.js), exposed with a similar option model. Optional **`.env`** is loaded from the working directory ([laserpants/dotenv-cpp](https://github.com/laserpants/dotenv-cpp)).
|
||||
|
||||
## Image stack (not libvips)
|
||||
|
||||
Processing uses **stb** ([nothings/stb](https://github.com/nothings/stb)): `stb_image`, **stb_image_resize2** (`stbir_*`), and `stb_image_write`. There is **no** [libvips](https://www.libvips.org/) / glib dependency in this package.
|
||||
|
||||
## Concurrency and defaults
|
||||
|
||||
| Area | Behavior |
|
||||
|------|----------|
|
||||
| **HTTP `serve`** | Uses **cpp-httplib**’s default **thread pool**: `CPPHTTPLIB_THREAD_POOL_COUNT` is **`max(8, hardware_concurrency() - 1)`** when hardware concurrency is known (see `httplib.h` in the fetched dependency). There is **no** extra app-level job queue or `MEDIA_*` concurrency cap yet—each accepted request runs resize on the pool. |
|
||||
| **IPC `ipc`** | **One JSON line per accepted connection**; the handler replies with one line and returns (sequential per socket). For parallel work, open **multiple connections** or run multiple processes. |
|
||||
| **CLI `resize`** | Single invocation, single file pair. |
|
||||
|
||||
**CLI defaults** (see `src/main.cpp`):
|
||||
|
||||
- `serve`: `--host 127.0.0.1`, `--port 8080`
|
||||
- `ipc`: `--host 127.0.0.1`, `--port 9333`, or `--unix <path>` (Unix only)
|
||||
|
||||
**REST**
|
||||
|
||||
- `GET /health` → `{"ok":true,"service":"media-img"}`
|
||||
- `POST /v1/resize` — JSON body: `input`, `output` (paths), optional `max_width`, `max_height`, `format` (`png` / `jpg` / `jpeg`)
|
||||
|
||||
**IPC** — one line per request, newline-terminated JSON:
|
||||
|
||||
- Success: `{"ok":true}`
|
||||
- Failure: `{"ok":false,"error":"..."}`
|
||||
- **API reference (underlying library):** [libvips `VipsImage`](https://www.libvips.org/API/current/class.Image.html)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Notes |
|
||||
|-------------|--------|
|
||||
| CMake | ≥ 3.20 |
|
||||
| C++ compiler | C++17 (MSVC, GCC, Clang) |
|
||||
| Git | For `FetchContent` dependencies |
|
||||
| Node.js | Optional; for integration tests (`npm run test:media`) — **Node 18+** recommended (`fetch`, `AbortSignal.timeout`) |
|
||||
| C++ compiler | C++17 |
|
||||
| **libvips** | **Required** — pkg-config (Unix) or `third_party/vips-dev-*` / `VIPS_ROOT` (Windows) |
|
||||
| Git | For `FetchContent` (CLI11, Asio, httplib, json, dotenv) |
|
||||
| Node.js | Optional — `npm run test:media` (Node 18+) |
|
||||
|
||||
## Quick start (build)
|
||||
### Installing libvips
|
||||
|
||||
From `packages/media/cpp`:
|
||||
**Debian / Ubuntu**
|
||||
|
||||
```bash
|
||||
npm install # optional if you only use cmake
|
||||
npm run build:release
|
||||
sudo apt install libvips-dev pkg-config
|
||||
```
|
||||
|
||||
Artifacts: **`dist/media-img`** (or **`dist/media-img.exe`** on Windows).
|
||||
**macOS (Homebrew)**
|
||||
|
||||
```bash
|
||||
brew install vips pkg-config
|
||||
```
|
||||
|
||||
**Windows (official dev bundle — recommended)**
|
||||
|
||||
```powershell
|
||||
npm run setup:vips
|
||||
```
|
||||
|
||||
Downloads [build-win64-mxe `vips-dev-x64-all`](https://github.com/libvips/build-win64-mxe/releases) into `third_party/vips-dev-*`. CMake adds that path automatically; DLLs are copied to `dist/` on link.
|
||||
|
||||
Pin the version with **`MEDIA_VIPS_VERSION`** (default `8.18.2`) if needed.
|
||||
|
||||
Alternatively set **`VIPS_ROOT`** or **`CMAKE_PREFIX_PATH`** to a tree with `include/vips/vips.h` and `lib/libvips.lib`.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd packages/media/cpp
|
||||
cmake --preset release
|
||||
cmake --build --preset release
|
||||
```
|
||||
|
||||
### Presets
|
||||
Binary: **`dist/media-img`** (`.exe` on Windows).
|
||||
|
||||
| Preset | Role |
|
||||
|--------|------|
|
||||
| `dev` | Debug build |
|
||||
| `release` | Release build |
|
||||
## Sharp-like options (`resize` / JSON)
|
||||
|
||||
| Sharp concept | `media-img` / JSON field | Notes |
|
||||
|---------------|---------------------------|--------|
|
||||
| `resize.fit` | `fit` | `inside`, `cover`, `contain`, `fill`, `outside` |
|
||||
| `resize.position` | `position` | `centre`, `attention`, `entropy`, … → libvips *interesting* |
|
||||
| `resize.kernel` | `kernel` | `nearest`, `cubic`, `mitchell`, `lanczos2`, `lanczos3` (default) |
|
||||
| `jpeg|webp|… quality` | `quality` | 1–100 |
|
||||
| `png compression` | `png_compression` | 0–9 |
|
||||
| `withoutEnlargement` | `without_enlargement` | Default **true**; CLI `--allow-enlargement` flips |
|
||||
| EXIF orientation | `autorotate` | Default **true**; CLI `--no-autorotate` |
|
||||
| Strip metadata | `strip_metadata` | Default **true**; CLI `--no-strip` |
|
||||
| `rotate` | `rotate` | 0, 90, 180, 270 (after autorotate) |
|
||||
| `flip` / `flop` | `flip` / `flop` | |
|
||||
| Letterbox | `background` | `#rrggbb` for `contain` |
|
||||
|
||||
**REST** `POST /v1/resize` accepts the same keys as columns in the table (plus `input`, `output` paths).
|
||||
|
||||
**IPC** sends one JSON object per line with the same keys.
|
||||
|
||||
## Concurrency
|
||||
|
||||
- **HTTP `serve`**: cpp-httplib default thread pool (`CPPHTTPLIB_THREAD_POOL_COUNT` — see upstream `httplib.h`).
|
||||
- **libvips**: processing is thread-safe per image; configure process-wide concurrency with `VIPS_CONCURRENCY` (or `vips_concurrency_set` in code later if needed).
|
||||
|
||||
## CLI overview
|
||||
|
||||
```bash
|
||||
media-img --help
|
||||
media-img -v,--version
|
||||
media-img resize --help
|
||||
media-img serve --help
|
||||
media-img ipc --help
|
||||
```
|
||||
|
||||
| Subcommand | Description |
|
||||
|------------|-------------|
|
||||
| `resize <input> <output>` | Resize file on disk (`--max-width`, `--max-height`, `--format`) |
|
||||
| `serve` | HTTP server (`--host`, `-p/--port`) |
|
||||
| `ipc` | JSON-line IPC: TCP (`--host`, `--port`) or **`--unix`** (Unix only) |
|
||||
| `kbot ...` | Forwards extra arguments to **`KBOT_EXE`** (optional AI / kbot workflows; build kbot separately) |
|
||||
`kbot` subcommand forwards to **`KBOT_EXE`** (optional).
|
||||
|
||||
Example:
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
set KBOT_EXE=C:\path\to\kbot.exe
|
||||
media-img kbot ai --prompt "hello"
|
||||
```
|
||||
|
||||
## Integration tests (REST + IPC)
|
||||
|
||||
Tests mirror the style of **kbot** orchestrators (e.g. spawn binary, wait for listen, talk over TCP): see **`orchestrator/test-media.mjs`**. They cover:
|
||||
|
||||
- **REST**: `GET /health`, `POST /v1/resize` (PNG + JPEG output), error path for missing input
|
||||
- **IPC TCP**: line JSON request/response, error path
|
||||
- **IPC Unix**: same protocol on a **Unix socket** (skipped on Windows — use TCP there)
|
||||
|
||||
```bash
|
||||
npm run build:release
|
||||
npm run generate:assets # if tests/assets PNGs are missing
|
||||
npm run test:media
|
||||
```
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `npm run test:media` | REST + IPC (TCP + Unix where supported) |
|
||||
| `npm run test:media:rest` | `--rest-only` |
|
||||
| `npm run test:media:ipc` | `--ipc-only` |
|
||||
|
||||
Env: **`MEDIA_IMG_TEST_UNIX`** — Unix socket path for the UDS test (default `/tmp/media-img-test.sock`).
|
||||
|
||||
## Test fixtures
|
||||
|
||||
Under **`tests/assets/`**:
|
||||
|
||||
- **`build-fixtures.mjs`** — Generates RGB PNGs (no extra npm deps). Run: `node tests/assets/build-fixtures.mjs` (also exposed as **`npm run generate:assets`**).
|
||||
- Additional subfolders (`in/`, `out/`, `*_webp/`, watermark samples, etc.) are available for broader manual or future tests.
|
||||
|
||||
## Related docs
|
||||
|
||||
- **`ROLLOUT.md`** — Phased rollout notes.
|
||||
- **`polymech.md`** — Broader Polymech context (if present).
|
||||
Requires a built `dist/media-img` **linked against libvips** and fixture PNGs (`npm run generate:assets` if missing).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
60
packages/media/cpp/cmake/FindVips.cmake
Normal file
60
packages/media/cpp/cmake/FindVips.cmake
Normal file
@ -0,0 +1,60 @@
|
||||
#[=======================================================================[
|
||||
FindVips — libvips (Sharp uses the same library).
|
||||
|
||||
Linux / macOS: `pkg-config vips` after installing:
|
||||
Debian/Ubuntu: sudo apt install libvips-dev
|
||||
macOS: brew install vips
|
||||
|
||||
Windows: vcpkg install vips:x64-windows, then use the vcpkg toolchain file or set
|
||||
CMAKE_PREFIX_PATH to the installed prefix; alternatively set VIPS_ROOT.
|
||||
#]=======================================================================]
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
if(DEFINED ENV{VIPS_ROOT} AND NOT VIPS_ROOT)
|
||||
set(VIPS_ROOT "$ENV{VIPS_ROOT}")
|
||||
endif()
|
||||
if(VIPS_ROOT)
|
||||
list(APPEND CMAKE_PREFIX_PATH "${VIPS_ROOT}")
|
||||
endif()
|
||||
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(VIPS IMPORTED_TARGET vips)
|
||||
endif()
|
||||
|
||||
if(TARGET PkgConfig::VIPS)
|
||||
if(NOT TARGET Vips::vips)
|
||||
add_library(Vips::vips ALIAS PkgConfig::VIPS)
|
||||
endif()
|
||||
set(Vips_FOUND TRUE)
|
||||
else()
|
||||
find_path(
|
||||
Vips_INCLUDE_DIR
|
||||
NAMES vips/vips.h
|
||||
PATHS "${CMAKE_PREFIX_PATH}"
|
||||
PATH_SUFFIXES include
|
||||
)
|
||||
find_library(
|
||||
Vips_LIBRARY
|
||||
NAMES vips libvips vips-8.0
|
||||
PATHS "${CMAKE_PREFIX_PATH}"
|
||||
PATH_SUFFIXES lib lib64
|
||||
)
|
||||
find_package_handle_standard_args(Vips DEFAULT_MSG Vips_LIBRARY Vips_INCLUDE_DIR)
|
||||
if(Vips_FOUND)
|
||||
add_library(Vips::vips UNKNOWN IMPORTED)
|
||||
get_filename_component(Vips_LIBRARY_DIR "${Vips_LIBRARY}" DIRECTORY)
|
||||
# vips.h includes glib.h — Windows SDK bundles glib headers next to vips.
|
||||
set(_VIPS_INCLUDES
|
||||
"${Vips_INCLUDE_DIR}"
|
||||
"${Vips_INCLUDE_DIR}/glib-2.0"
|
||||
"${Vips_LIBRARY_DIR}/glib-2.0/include")
|
||||
set_target_properties(
|
||||
Vips::vips
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION "${Vips_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${_VIPS_INCLUDES}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
@ -14,6 +14,7 @@
|
||||
"clean": "cmake -E rm -rf build dist",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"generate:assets": "node tests/assets/build-fixtures.mjs",
|
||||
"setup:vips": "powershell -ExecutionPolicy Bypass -File scripts/fetch-vips-windows.ps1",
|
||||
"test:media": "node orchestrator/test-media.mjs",
|
||||
"test:media:rest": "node orchestrator/test-media.mjs --rest-only",
|
||||
"test:media:ipc": "node orchestrator/test-media.mjs --ipc-only",
|
||||
|
||||
17
packages/media/cpp/scripts/fetch-vips-windows.ps1
Normal file
17
packages/media/cpp/scripts/fetch-vips-windows.ps1
Normal file
@ -0,0 +1,17 @@
|
||||
# Download official libvips MSVC dev bundle (x64, "all" loaders) into third_party/.
|
||||
# https://github.com/libvips/build-win64-mxe/releases
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$third = Join-Path $root 'third_party'
|
||||
New-Item -ItemType Directory -Force -Path $third | Out-Null
|
||||
|
||||
$version = if ($env:MEDIA_VIPS_VERSION) { $env:MEDIA_VIPS_VERSION } else { '8.18.2' }
|
||||
$name = "vips-dev-x64-all-$version.zip"
|
||||
$url = "https://github.com/libvips/build-win64-mxe/releases/download/v$version/$name"
|
||||
$zip = Join-Path $third $name
|
||||
|
||||
Write-Host "Downloading $url ..."
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
Write-Host "Extracting to $third ..."
|
||||
Expand-Archive -Path $zip -DestinationPath $third -Force
|
||||
Write-Host "Done. CMake will pick up third_party/vips-dev-* automatically."
|
||||
@ -1,95 +1,404 @@
|
||||
#include "resize.hpp"
|
||||
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||
|
||||
#include "stb_image.h"
|
||||
#include "stb_image_resize2.h"
|
||||
#include "stb_image_write.h"
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace media {
|
||||
|
||||
static void compute_target_size(int iw, int ih, const ResizeOptions& opt, int* ow, int* oh) {
|
||||
*ow = iw;
|
||||
*oh = ih;
|
||||
if (opt.max_width <= 0 && opt.max_height <= 0)
|
||||
return;
|
||||
double scale = 1.0;
|
||||
if (opt.max_width > 0 && iw > opt.max_width)
|
||||
scale = std::min(scale, static_cast<double>(opt.max_width) / static_cast<double>(iw));
|
||||
if (opt.max_height > 0 && ih > opt.max_height)
|
||||
scale = std::min(scale, static_cast<double>(opt.max_height) / static_cast<double>(ih));
|
||||
if (scale >= 1.0)
|
||||
return;
|
||||
*ow = std::max(1, static_cast<int>(iw * scale));
|
||||
*oh = std::max(1, static_cast<int>(ih * scale));
|
||||
namespace {
|
||||
|
||||
std::once_flag g_vips_init;
|
||||
|
||||
void ensure_vips() {
|
||||
std::call_once(g_vips_init, []() {
|
||||
if (vips_init("media-img"))
|
||||
std::abort();
|
||||
});
|
||||
}
|
||||
|
||||
bool resize_file(const std::string& input_path, const std::string& output_path,
|
||||
const ResizeOptions& opt, std::string& err_out) {
|
||||
int iw = 0, ih = 0, channels = 0;
|
||||
unsigned char* data =
|
||||
stbi_load(input_path.c_str(), &iw, &ih, &channels, 0);
|
||||
if (!data) {
|
||||
err_out = std::string("stbi_load failed: ") + stbi_failure_reason();
|
||||
return false;
|
||||
}
|
||||
std::string vips_err() {
|
||||
const char *buf = vips_error_buffer();
|
||||
std::string s = buf ? buf : "vips error";
|
||||
vips_error_clear();
|
||||
return s;
|
||||
}
|
||||
|
||||
int ow = iw, oh = ih;
|
||||
compute_target_size(iw, ih, opt, &ow, &oh);
|
||||
|
||||
const int out_channels = channels;
|
||||
std::vector<unsigned char> out(static_cast<size_t>(ow) * static_cast<size_t>(oh) * static_cast<size_t>(out_channels));
|
||||
|
||||
stbir_pixel_layout layout = STBIR_RGBA;
|
||||
if (channels == 1)
|
||||
layout = STBIR_1CHANNEL;
|
||||
else if (channels == 2)
|
||||
layout = STBIR_2CHANNEL;
|
||||
else if (channels == 3)
|
||||
layout = STBIR_RGB;
|
||||
else if (channels == 4)
|
||||
layout = STBIR_RGBA;
|
||||
|
||||
if (!stbir_resize_uint8_srgb(data, iw, ih, 0, out.data(), ow, oh, 0, layout)) {
|
||||
stbi_image_free(data);
|
||||
err_out = "stbir_resize_uint8_srgb failed";
|
||||
return false;
|
||||
}
|
||||
stbi_image_free(data);
|
||||
|
||||
std::string fmt = opt.format;
|
||||
if (fmt.empty()) {
|
||||
auto dot = output_path.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
fmt = output_path.substr(dot + 1);
|
||||
}
|
||||
for (auto& c : fmt) {
|
||||
void to_lower(std::string &s) {
|
||||
for (char &c : s) {
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
c = static_cast<char>(c - 'A' + 'a');
|
||||
}
|
||||
}
|
||||
|
||||
int ok = 0;
|
||||
if (fmt == "jpg" || fmt == "jpeg") {
|
||||
ok = stbi_write_jpg(output_path.c_str(), ow, oh, out_channels, out.data(), 90);
|
||||
} else {
|
||||
if (fmt != "png" && !fmt.empty()) {
|
||||
err_out = "warning: unsupported format '" + fmt + "', writing PNG";
|
||||
VipsKernel parse_kernel(const std::string &k) {
|
||||
std::string x = k;
|
||||
to_lower(x);
|
||||
if (x == "nearest")
|
||||
return VIPS_KERNEL_NEAREST;
|
||||
if (x == "cubic")
|
||||
return VIPS_KERNEL_CUBIC;
|
||||
if (x == "mitchell")
|
||||
return VIPS_KERNEL_MITCHELL;
|
||||
if (x == "lanczos2")
|
||||
return VIPS_KERNEL_LANCZOS2;
|
||||
return VIPS_KERNEL_LANCZOS3;
|
||||
}
|
||||
|
||||
VipsInteresting parse_interesting(const std::string &p) {
|
||||
std::string x = p;
|
||||
to_lower(x);
|
||||
if (x == "attention" || x == "entropy")
|
||||
return VIPS_INTERESTING_ATTENTION;
|
||||
if (x == "low" || x == "left" || x == "top")
|
||||
return VIPS_INTERESTING_LOW;
|
||||
if (x == "high" || x == "right" || x == "bottom")
|
||||
return VIPS_INTERESTING_HIGH;
|
||||
return VIPS_INTERESTING_CENTRE;
|
||||
}
|
||||
|
||||
bool parse_rgb_background(const std::string &bg, double out[3]) {
|
||||
if (bg.empty() || bg[0] != '#' || bg.size() < 4)
|
||||
return false;
|
||||
try {
|
||||
if (bg.size() == 7) {
|
||||
out[0] = static_cast<double>(std::stoi(bg.substr(1, 2), nullptr, 16));
|
||||
out[1] = static_cast<double>(std::stoi(bg.substr(3, 2), nullptr, 16));
|
||||
out[2] = static_cast<double>(std::stoi(bg.substr(5, 2), nullptr, 16));
|
||||
return true;
|
||||
}
|
||||
ok = stbi_write_png(output_path.c_str(), ow, oh, out_channels, out.data(), ow * out_channels);
|
||||
if (bg.size() == 4) {
|
||||
out[0] = static_cast<double>(std::stoi(bg.substr(1, 1), nullptr, 16) * 17);
|
||||
out[1] = static_cast<double>(std::stoi(bg.substr(2, 1), nullptr, 16) * 17);
|
||||
out[2] = static_cast<double>(std::stoi(bg.substr(3, 1), nullptr, 16) * 17);
|
||||
return true;
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string output_format(const std::string &path, const ResizeOptions &opt) {
|
||||
std::string fmt = opt.format;
|
||||
if (fmt.empty()) {
|
||||
const auto dot = path.rfind('.');
|
||||
if (dot != std::string::npos)
|
||||
fmt = path.substr(dot + 1);
|
||||
}
|
||||
to_lower(fmt);
|
||||
return fmt;
|
||||
}
|
||||
|
||||
bool save_image(VipsImage *in, const std::string &path, const ResizeOptions &opt, std::string &err_out) {
|
||||
const std::string fmt = output_format(path, opt);
|
||||
const gboolean strip = opt.strip_metadata ? TRUE : FALSE;
|
||||
|
||||
if (fmt == "jpg" || fmt == "jpeg") {
|
||||
if (vips_jpegsave(in, path.c_str(), "Q", opt.quality, "optimize_coding", TRUE, "strip", strip, NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
}
|
||||
if (fmt == "png") {
|
||||
if (vips_pngsave(in, path.c_str(), "compression", opt.png_compression, NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
}
|
||||
if (fmt == "webp") {
|
||||
if (vips_webpsave(in, path.c_str(), "Q", opt.quality, "strip", strip, NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
}
|
||||
if (fmt == "tif" || fmt == "tiff") {
|
||||
if (vips_tiffsave(in, path.c_str(), NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
}
|
||||
if (fmt == "avif" || fmt == "heic" || fmt == "heif") {
|
||||
if (vips_image_write_to_file(in, path.c_str(), "Q", opt.quality, NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
err_out = "stbi_write failed";
|
||||
if (vips_image_write_to_file(in, path.c_str(), NULL))
|
||||
goto fail;
|
||||
return true;
|
||||
fail:
|
||||
err_out = vips_err();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool apply_user_rotate(VipsImage *in, VipsImage **out, int deg, std::string &err_out) {
|
||||
deg = ((deg % 360) + 360) % 360;
|
||||
if (deg == 0) {
|
||||
*out = in;
|
||||
g_object_ref(in);
|
||||
return true;
|
||||
}
|
||||
VipsAngle a = VIPS_ANGLE_D0;
|
||||
if (deg == 90)
|
||||
a = VIPS_ANGLE_D90;
|
||||
else if (deg == 180)
|
||||
a = VIPS_ANGLE_D180;
|
||||
else if (deg == 270)
|
||||
a = VIPS_ANGLE_D270;
|
||||
else {
|
||||
err_out = "rotate must be 0, 90, 180, or 270";
|
||||
return false;
|
||||
}
|
||||
if (vips_rot(in, out, a, NULL)) {
|
||||
err_out = vips_err();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void apply_resize_options_from_json(const nlohmann::json &j, ResizeOptions &opt) {
|
||||
auto num = [&](const char *key, int &dest) {
|
||||
if (!j.contains(key) || j[key].is_null())
|
||||
return;
|
||||
if (j[key].is_number_integer())
|
||||
dest = j[key].get<int>();
|
||||
};
|
||||
auto num_or_bool = [&](const char *key, bool &dest) {
|
||||
if (!j.contains(key) || j[key].is_null())
|
||||
return;
|
||||
if (j[key].is_boolean())
|
||||
dest = j[key].get<bool>();
|
||||
else if (j[key].is_number_integer())
|
||||
dest = j[key].get<int>() != 0;
|
||||
};
|
||||
auto str = [&](const char *key, std::string &dest) {
|
||||
if (!j.contains(key) || !j[key].is_string())
|
||||
return;
|
||||
dest = j[key].get<std::string>();
|
||||
};
|
||||
|
||||
num("max_width", opt.max_width);
|
||||
num("max_height", opt.max_height);
|
||||
str("format", opt.format);
|
||||
str("fit", opt.fit);
|
||||
str("position", opt.position);
|
||||
str("kernel", opt.kernel);
|
||||
num("quality", opt.quality);
|
||||
num("png_compression", opt.png_compression);
|
||||
num_or_bool("without_enlargement", opt.without_enlargement);
|
||||
num_or_bool("autorotate", opt.autorotate);
|
||||
num_or_bool("strip_metadata", opt.strip_metadata);
|
||||
num("rotate", opt.rotate);
|
||||
num_or_bool("flip", opt.flip);
|
||||
num_or_bool("flop", opt.flop);
|
||||
str("background", opt.background);
|
||||
}
|
||||
|
||||
bool resize_file(const std::string &input_path, const std::string &output_path, const ResizeOptions &opt_in,
|
||||
std::string &err_out) {
|
||||
ensure_vips();
|
||||
ResizeOptions opt = opt_in;
|
||||
to_lower(opt.fit);
|
||||
to_lower(opt.kernel);
|
||||
to_lower(opt.position);
|
||||
|
||||
VipsImage *base = vips_image_new_from_file(input_path.c_str(), "access", VIPS_ACCESS_SEQUENTIAL, NULL);
|
||||
if (!base) {
|
||||
err_out = vips_err();
|
||||
return false;
|
||||
}
|
||||
|
||||
VipsImage *cur = base;
|
||||
VipsImage *next = nullptr;
|
||||
|
||||
if (opt.autorotate) {
|
||||
if (vips_autorot(cur, &next, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
}
|
||||
|
||||
if (opt.rotate != 0) {
|
||||
if (!apply_user_rotate(cur, &next, opt.rotate, err_out)) {
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
}
|
||||
|
||||
if (opt.flip) {
|
||||
if (vips_flip(cur, &next, VIPS_DIRECTION_VERTICAL, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
}
|
||||
if (opt.flop) {
|
||||
if (vips_flip(cur, &next, VIPS_DIRECTION_HORIZONTAL, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
}
|
||||
|
||||
const int iw = vips_image_get_width(cur);
|
||||
const int ih = vips_image_get_height(cur);
|
||||
const int W = opt.max_width;
|
||||
const int H = opt.max_height;
|
||||
const VipsKernel vk = parse_kernel(opt.kernel);
|
||||
const VipsInteresting vi = parse_interesting(opt.position);
|
||||
|
||||
const bool want_resize = (W > 0 || H > 0) || opt.fit == "fill";
|
||||
|
||||
if (!want_resize) {
|
||||
if (!save_image(cur, output_path, opt, err_out)) {
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (opt.fit == "fill") {
|
||||
if (W <= 0 || H <= 0) {
|
||||
err_out = "fill fit requires both max_width and max_height";
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
const double sx = static_cast<double>(W) / static_cast<double>(iw);
|
||||
const double sy = static_cast<double>(H) / static_cast<double>(ih);
|
||||
if (vips_resize(cur, &next, sx, "vscale", sy, "kernel", vk, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
} else if (opt.fit == "inside" || opt.fit == "contain") {
|
||||
if (W <= 0 && H <= 0) {
|
||||
err_out = "inside/contain requires max_width and/or max_height";
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
double scale = 1.0;
|
||||
if (W > 0 && H > 0) {
|
||||
scale = std::min(static_cast<double>(W) / static_cast<double>(iw),
|
||||
static_cast<double>(H) / static_cast<double>(ih));
|
||||
} else if (W > 0) {
|
||||
scale = static_cast<double>(W) / static_cast<double>(iw);
|
||||
} else {
|
||||
scale = static_cast<double>(H) / static_cast<double>(ih);
|
||||
}
|
||||
if (opt.without_enlargement)
|
||||
scale = std::min(scale, 1.0);
|
||||
if (scale <= 0.0) {
|
||||
err_out = "invalid geometry";
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
if (vips_resize(cur, &next, scale, "kernel", vk, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
|
||||
if (opt.fit == "contain" && W > 0 && H > 0) {
|
||||
const int cw = vips_image_get_width(cur);
|
||||
const int ch = vips_image_get_height(cur);
|
||||
if (cw < W || ch < H) {
|
||||
double bg[3] = {255.0, 255.0, 255.0};
|
||||
if (!parse_rgb_background(opt.background, bg))
|
||||
parse_rgb_background("#ffffff", bg);
|
||||
const int left = (W - cw) / 2;
|
||||
const int top = (H - ch) / 2;
|
||||
VipsArrayDouble *bg_a = vips_array_double_newv(3, bg[0], bg[1], bg[2]);
|
||||
const int e = vips_embed(cur, &next, left, top, W, H, "extend", VIPS_EXTEND_BACKGROUND,
|
||||
"background", bg_a, NULL);
|
||||
vips_area_unref(VIPS_AREA(bg_a));
|
||||
if (e) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
}
|
||||
}
|
||||
} else if (opt.fit == "cover") {
|
||||
if (W <= 0 || H <= 0) {
|
||||
err_out = "cover fit requires max_width and max_height";
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
const double sx = static_cast<double>(W) / static_cast<double>(iw);
|
||||
const double sy = static_cast<double>(H) / static_cast<double>(ih);
|
||||
const double sc = std::max(sx, sy);
|
||||
if (vips_resize(cur, &next, sc, "kernel", vk, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
const int rw = vips_image_get_width(cur);
|
||||
const int rh = vips_image_get_height(cur);
|
||||
const int left = std::max(0, (rw - W) / 2);
|
||||
const int top = std::max(0, (rh - H) / 2);
|
||||
if (vi == VIPS_INTERESTING_ATTENTION) {
|
||||
if (vips_smartcrop(cur, &next, W, H, "interesting", VIPS_INTERESTING_ATTENTION, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (vips_extract_area(cur, &next, left, top, W, H, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
} else if (opt.fit == "outside") {
|
||||
if (W <= 0 || H <= 0) {
|
||||
err_out = "outside fit requires max_width and max_height";
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
const double sx = static_cast<double>(W) / static_cast<double>(iw);
|
||||
const double sy = static_cast<double>(H) / static_cast<double>(ih);
|
||||
double sc = std::max(sx, sy);
|
||||
if (opt.without_enlargement)
|
||||
sc = std::min(sc, 1.0);
|
||||
if (vips_resize(cur, &next, sc, "kernel", vk, NULL)) {
|
||||
err_out = vips_err();
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
cur = next;
|
||||
} else {
|
||||
err_out = std::string("unknown fit: ") + opt.fit;
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!save_image(cur, output_path, opt, err_out)) {
|
||||
g_object_unref(cur);
|
||||
return false;
|
||||
}
|
||||
g_object_unref(cur);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace media
|
||||
|
||||
@ -2,17 +2,58 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Sharp-like processing options (libvips backend).
|
||||
* @see https://sharp.pixelplumbing.com/api-resize
|
||||
* @see https://www.libvips.org/API/current/class.Image.html
|
||||
*/
|
||||
struct ResizeOptions {
|
||||
int max_width = 0; // 0 = no limit (respect max_height)
|
||||
int max_height = 0; // 0 = no limit
|
||||
/** "png", "jpg", "jpeg" — if empty, infer from output path extension */
|
||||
int max_width = 0; // 0 = unconstrained (use with max_height)
|
||||
int max_height = 0; // 0 = unconstrained
|
||||
|
||||
/** Output container: png, jpg, jpeg, webp, tif, tiff, avif, heic, … — empty = infer from output path */
|
||||
std::string format;
|
||||
|
||||
/**
|
||||
* How the image should fit the target box (max_width × max_height when both set).
|
||||
* inside — default; not larger than box (Sharp “inside” / contain bounds).
|
||||
* cover — fill box, crop overflow (centre or `position`).
|
||||
* contain — fit inside box; letterbox to exact WxH using `background` when both dimensions set.
|
||||
* fill — stretch to exact WxH (ignores aspect).
|
||||
* outside — at least as large as the box on both axes (no crop); may exceed WxH.
|
||||
*/
|
||||
std::string fit = "inside";
|
||||
|
||||
/** For `cover`: centre, attention, entropy, low, high — maps to libvips “interesting”. */
|
||||
std::string position = "centre";
|
||||
|
||||
/** nearest, cubic, mitchell, lanczos2, lanczos3 (default, closest to Sharp). */
|
||||
std::string kernel = "lanczos3";
|
||||
|
||||
int quality = 85; /**< JPEG / WebP / AVIF-style quality 1–100 */
|
||||
int png_compression = 6; /**< PNG DEFLATE 0–9 */
|
||||
|
||||
bool without_enlargement = true;
|
||||
bool autorotate = true; /**< Apply EXIF orientation (like Sharp default). */
|
||||
bool strip_metadata = true; /**< Strip EXIF etc. on save where supported */
|
||||
|
||||
/** Degrees: 0, 90, 180, 270 — applied after autorotate. */
|
||||
int rotate = 0;
|
||||
bool flip = false; /**< Vertical flip */
|
||||
bool flop = false; /**< Horizontal flip */
|
||||
|
||||
/** Letterbox / embed: `#rrggbb` or `#rgb` */
|
||||
std::string background = "#ffffff";
|
||||
};
|
||||
|
||||
/** Resize image on disk. At least one of max_width / max_height should be > 0. */
|
||||
bool resize_file(const std::string& input_path, const std::string& output_path,
|
||||
const ResizeOptions& opt, std::string& err_out);
|
||||
bool resize_file(const std::string& input_path, const std::string& output_path, const ResizeOptions& opt,
|
||||
std::string& err_out);
|
||||
|
||||
/** Merge JSON keys into `opt` (REST / IPC). Unknown keys ignored. */
|
||||
void apply_resize_options_from_json(const nlohmann::json& j, ResizeOptions& opt);
|
||||
|
||||
} // namespace media
|
||||
|
||||
@ -35,12 +35,7 @@ int run_server(const std::string& host, int port) {
|
||||
const std::string in = body["input"].get<std::string>();
|
||||
const std::string out = body["output"].get<std::string>();
|
||||
media::ResizeOptions opt;
|
||||
if (body.contains("max_width"))
|
||||
opt.max_width = body["max_width"].get<int>();
|
||||
if (body.contains("max_height"))
|
||||
opt.max_height = body["max_height"].get<int>();
|
||||
if (body.contains("format") && body["format"].is_string())
|
||||
opt.format = body["format"].get<std::string>();
|
||||
media::apply_resize_options_from_json(body, opt);
|
||||
|
||||
std::string err;
|
||||
if (!media::resize_file(in, out, opt, err)) {
|
||||
|
||||
@ -32,12 +32,7 @@ static int handle_session(asio::ip::tcp::socket sock) {
|
||||
return 0;
|
||||
}
|
||||
media::ResizeOptions opt;
|
||||
if (j.contains("max_width"))
|
||||
opt.max_width = j["max_width"].get<int>();
|
||||
if (j.contains("max_height"))
|
||||
opt.max_height = j["max_height"].get<int>();
|
||||
if (j.contains("format") && j["format"].is_string())
|
||||
opt.format = j["format"].get<std::string>();
|
||||
media::apply_resize_options_from_json(j, opt);
|
||||
std::string err;
|
||||
bool ok = media::resize_file(j["input"].get<std::string>(), j["output"].get<std::string>(), opt, err);
|
||||
nlohmann::json out = ok ? nlohmann::json{{"ok", true}} : nlohmann::json{{"ok", false}, {"error", err}};
|
||||
@ -88,12 +83,7 @@ int run_unix_server(const std::string& path) {
|
||||
continue;
|
||||
}
|
||||
media::ResizeOptions opt;
|
||||
if (j.contains("max_width"))
|
||||
opt.max_width = j["max_width"].get<int>();
|
||||
if (j.contains("max_height"))
|
||||
opt.max_height = j["max_height"].get<int>();
|
||||
if (j.contains("format") && j["format"].is_string())
|
||||
opt.format = j["format"].get<std::string>();
|
||||
media::apply_resize_options_from_json(j, opt);
|
||||
std::string err;
|
||||
bool ok = media::resize_file(j["input"].get<std::string>(), j["output"].get<std::string>(), opt, err);
|
||||
nlohmann::json out = ok ? nlohmann::json{{"ok", true}} : nlohmann::json{{"ok", false}, {"error", err}};
|
||||
|
||||
@ -62,13 +62,40 @@ int main(int argc, char **argv) {
|
||||
int max_w = 0;
|
||||
int max_h = 0;
|
||||
std::string format;
|
||||
std::string fit = "inside";
|
||||
std::string position = "centre";
|
||||
std::string kernel = "lanczos3";
|
||||
std::string background = "#ffffff";
|
||||
int quality = 85;
|
||||
int png_compression = 6;
|
||||
int rotate = 0;
|
||||
bool flip = false;
|
||||
bool flop = false;
|
||||
bool no_autorotate = false;
|
||||
bool no_strip = false;
|
||||
bool allow_enlargement = false;
|
||||
|
||||
auto *resize_cmd = app.add_subcommand("resize", "Resize an image file");
|
||||
auto *resize_cmd = app.add_subcommand("resize", "Resize / transform an image (libvips, Sharp-like options)");
|
||||
resize_cmd->add_option("input", in_path, "Input image path")->required();
|
||||
resize_cmd->add_option("output", out_path, "Output image path")->required();
|
||||
resize_cmd->add_option("--max-width", max_w, "Max width (0 = no limit)");
|
||||
resize_cmd->add_option("--max-height", max_h, "Max height (0 = no limit)");
|
||||
resize_cmd->add_option("--format", format, "Output format: png, jpg, jpeg (default: from extension)");
|
||||
resize_cmd->add_option("--max-width", max_w, "Target / max width (0 = no limit)");
|
||||
resize_cmd->add_option("--max-height", max_h, "Target / max height (0 = no limit)");
|
||||
resize_cmd->add_option("--format", format, "Output format (default: from extension)");
|
||||
resize_cmd
|
||||
->add_option("--fit", fit,
|
||||
"inside|cover|contain|fill|outside — see Sharp resize.fit")
|
||||
->default_val("inside");
|
||||
resize_cmd->add_option("--position", position, "For cover: centre|attention|entropy|…")->default_val("centre");
|
||||
resize_cmd->add_option("--kernel", kernel, "nearest|cubic|mitchell|lanczos2|lanczos3")->default_val("lanczos3");
|
||||
resize_cmd->add_option("-q,--quality", quality, "JPEG/WebP/AVIF quality 1–100")->default_val(85);
|
||||
resize_cmd->add_option("--png-compression", png_compression, "PNG DEFLATE 0–9")->default_val(6);
|
||||
resize_cmd->add_option("--background", background, "Letterbox colour #rrggbb (contain)");
|
||||
resize_cmd->add_option("--rotate", rotate, "Rotate 0|90|180|270 after EXIF autorotate")->default_val(0);
|
||||
resize_cmd->add_flag("--flip", flip, "Vertical flip");
|
||||
resize_cmd->add_flag("--flop", flop, "Horizontal flop");
|
||||
resize_cmd->add_flag("--no-autorotate", no_autorotate, "Disable EXIF orientation");
|
||||
resize_cmd->add_flag("--no-strip", no_strip, "Keep metadata on output");
|
||||
resize_cmd->add_flag("--allow-enlargement", allow_enlargement, "Allow upscaling (inside/contain/outside)");
|
||||
|
||||
std::string host = "127.0.0.1";
|
||||
int port = 8080;
|
||||
@ -94,6 +121,18 @@ int main(int argc, char **argv) {
|
||||
opt.max_width = max_w;
|
||||
opt.max_height = max_h;
|
||||
opt.format = format;
|
||||
opt.fit = fit;
|
||||
opt.position = position;
|
||||
opt.kernel = kernel;
|
||||
opt.background = background;
|
||||
opt.quality = quality;
|
||||
opt.png_compression = png_compression;
|
||||
opt.rotate = rotate;
|
||||
opt.flip = flip;
|
||||
opt.flop = flop;
|
||||
opt.autorotate = !no_autorotate;
|
||||
opt.strip_metadata = !no_strip;
|
||||
opt.without_enlargement = !allow_enlargement;
|
||||
std::string err;
|
||||
if (!media::resize_file(in_path, out_path, opt, err)) {
|
||||
std::cerr << err << "\n";
|
||||
|
||||
@ -23,7 +23,7 @@ node tests/assets/build-fixtures.mjs
|
||||
| `stripes-512x64.png` | 512×64 | Horizontal bands |
|
||||
| `checker-128x128.png` | 128×128 | Checkerboard (used by IPC Unix test on Linux/macOS) |
|
||||
|
||||
All are **RGB 8-bit** PNG (no alpha). **`media-img` uses STB**, not libvips.
|
||||
All are **RGB 8-bit** PNG (no alpha). **`media-img` uses libvips** for processing (same stack as Sharp).
|
||||
|
||||
## Other folders
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user