oximg 0.10.0

High-performance image compression: library, CLI, and self-hostable server (PoC).
Documentation
diff --git a/Makefile b/Makefile
index 44c456f..193a7de 100644
--- a/Makefile
+++ b/Makefile
@@ -72,6 +72,7 @@ k6:
 
 	docker compose run --rm k6 run -u $$(nproc) -d ${DURATION} \
 		-e FORMAT="${FORMAT}" \
+		-e OUT_FORMAT="${OUT_FORMAT}" \
 		-e TOOL="${TOOL}" \
 		-e WIDTH=${WIDTH} \
 		-e HEIGHT=${HEIGHT} \
diff --git a/docker-compose.yml b/docker-compose.yml
index 2e9bdba..082c1ec 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,6 +2,7 @@ version: "3"
 
 services:
   nginx:
+    cpuset: "0-1"
     container_name: nginx_benchmark
     image: nginx:latest
     restart: unless-stopped
@@ -18,12 +19,14 @@ services:
 
   k6:
     image: grafana/k6:latest
+    cpuset: "0-1"
     container_name: k6
     volumes:
       - ${PWD}:/bench:ro
     working_dir: /bench
 
   imgproxy:
+    cpuset: "0-1"
     container_name: imgproxy_benchmark
     image: darthsim/imgproxy:latest
     restart: unless-stopped
@@ -45,6 +48,7 @@ services:
       start_period: 10s
 
   thumbor:
+    cpuset: "0-1"
     container_name: thumbor_benchmark
     build: ${PWD}/thumbor
     restart: unless-stopped
@@ -63,6 +67,7 @@ services:
       start_period: 10s
 
   imagor:
+    cpuset: "0-1"
     container_name: imagor_benchmark
     image: ghcr.io/cshum/imagor:latest
     restart: unless-stopped
@@ -83,3 +88,23 @@ services:
       timeout: 5s
       start_period: 10s
 
+
+  oximg:
+    cpuset: "0-1"
+    container_name: oximg_benchmark
+    image: oximg:bench
+    restart: unless-stopped
+    tty: true
+    ports:
+      - 9005:80
+    environment:
+      PORT: "80"
+      OXIMG_SOURCE_BASE_URL: "http://nginx"
+    depends_on:
+      nginx:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD-SHELL", "timeout 5s bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1"]
+      interval: 5s
+      timeout: 5s
+      start_period: 10s
diff --git a/k6.js b/k6.js
index 73e99d5..1191412 100644
--- a/k6.js
+++ b/k6.js
@@ -33,6 +33,37 @@ switch (scenario.format) {
     throw `Unknown format ${scenario.format}`;
 }
 
+// Cross-format cells (oximg extension): OUT_FORMAT overrides the output
+// format while FORMAT keeps selecting the source files. Quality follows
+// the output format, matching the same-format mapping above; unknown
+// values fail fast like FORMAT does (a typo must not silently benchmark
+// a different workload per server).
+let outFormat = (__ENV.OUT_FORMAT || '').toLowerCase();
+switch (outFormat) {
+  case '':
+    break;
+
+  case 'jpg':
+  case 'jpeg':
+    outFormat = 'jpg';
+    scenario.quality = 80;
+    break;
+
+  case 'png':
+    break;
+
+  case 'webp':
+    scenario.quality = 75;
+    break;
+
+  case 'avif':
+    scenario.quality = 65;
+    break;
+
+  default:
+    throw `Unknown output format ${outFormat}`;
+}
+
 const getFilesList = (format) => {
   const datasetDir = (__ENV.DATASET_DIR || './dataset');
   const files = open(datasetDir+"/list.txt").split(/\r?\n/);
@@ -47,12 +78,15 @@ const getFormat = (filename) => {
 
 const formatUrl = (format, quality, filename) => {
   switch (scenario.tool) {
+    case 'oximg':
+      return `http://${scenario.tool}/resize/${scenario.width}/${scenario.height}/${filename}${outFormat && `@${outFormat}`}`;
+
     case 'imgproxy':
-      return `http://${scenario.tool}/unsafe/rs:fit:${scenario.width}:${scenario.height}/f:${format}/q:${quality}/plain/http://nginx/${filename}`;
+      return `http://${scenario.tool}/unsafe/rs:fit:${scenario.width}:${scenario.height}/f:${outFormat || format}/q:${quality}/plain/http://nginx/${filename}`;
 
     case 'thumbor':
     case 'imagor':
-      return `http://${scenario.tool}/unsafe/fit-in/${scenario.width}x${scenario.height}/filters:format(${format}):quality(${quality})/http://nginx/${filename}`;
+      return `http://${scenario.tool}/unsafe/fit-in/${scenario.width}x${scenario.height}/filters:format(${outFormat || format}):quality(${quality})/http://nginx/${filename}`;
 
     default:
       throw `Unknown tool ${tool}`;