Skip to content

Commit 56e2ff5

Browse files
tsukasa-artclaude
andcommitted
feat: Phase 2+3 — ops tests, CI (Linux + macOS matrix)
- fix pict_rotate orientation 6/8: swap CW/CCW formulas (EXIF was inverted) - add test/ops_precision.ts: 13 cases for rotate/remove_background/round_corners - fix test/*.ts: platform-aware libpict extension (.dylib/.so) - add .github/workflows/build.yml: cmake build + bun tests on ubuntu/macos Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7d68bb4 commit 56e2ff5

10 files changed

Lines changed: 859 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build & Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
name: ${{ matrix.os }}
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, macos-latest]
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Install dependencies (Linux)
22+
if: runner.os == 'Linux'
23+
run: |
24+
sudo apt-get update -qq
25+
sudo apt-get install -y \
26+
cmake pkg-config \
27+
libavif-dev libwebp-dev \
28+
libjpeg-dev libpng-dev libheif-dev
29+
30+
- name: Install dependencies (macOS)
31+
if: runner.os == 'macOS'
32+
run: |
33+
brew install libavif webp jpeg-turbo libpng libheif
34+
35+
- uses: oven-sh/setup-bun@v2
36+
with:
37+
bun-version: latest
38+
39+
- name: Build
40+
run: |
41+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
42+
cmake --build build --parallel
43+
44+
- name: Test — Lanczos-3 precision
45+
run: bun run test/lanczos_precision.ts
46+
47+
- name: Test — ops precision
48+
run: bun run test/ops_precision.ts

src/pict.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,9 @@ uint8_t *pict_rotate(
473473
case 3: dx = src_w - 1 - sx; dy = src_h - 1 - sy; break;
474474
case 4: dx = sx; dy = src_h - 1 - sy; break;
475475
case 5: dx = sy; dy = sx; break;
476-
case 6: dx = sy; dy = src_w - 1 - sx; break;
476+
case 6: dx = src_h - 1 - sy; dy = sx; break;
477477
case 7: dx = src_h - 1 - sy; dy = src_w - 1 - sx; break;
478-
case 8: dx = src_h - 1 - sy; dy = sx; break;
478+
case 8: dx = sy; dy = src_w - 1 - sx; break;
479479
default: dx = sx; dy = sy; break;
480480
}
481481
uint8_t *dp = dst + ((size_t)dy * dw + dx) * ch;

test/diag_c_only.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// C dylib のみをロードして uniform color resize の正確性を確認
2+
import { dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
3+
import { join } from "path";
4+
5+
const C_LIB = join(import.meta.dir, "../build/libpict.dylib");
6+
7+
const lib = dlopen(C_LIB, {
8+
pict_resize: {
9+
args: [FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.u8,
10+
FFIType.u32, FFIType.u32, FFIType.u32, FFIType.ptr],
11+
returns: FFIType.ptr,
12+
},
13+
pict_free_buffer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.void },
14+
});
15+
16+
// 4×4 RGB uniform (100, 150, 200) → 2×2
17+
{
18+
const src = new Uint8Array(4 * 4 * 3);
19+
for (let i = 0; i < 16; i++) { src[i*3]=100; src[i*3+1]=150; src[i*3+2]=200; }
20+
const outLen = new BigUint64Array(1);
21+
const r = lib.symbols.pict_resize(ptr(src), 4, 4, 3, 2, 2, 1, ptr(outLen));
22+
const out = new Uint8Array(toArrayBuffer(r!, 0, Number(outLen[0])).slice(0));
23+
lib.symbols.pict_free_buffer(r!, outLen[0]);
24+
console.log("4x4 uniform RGB → 2x2:");
25+
for (let i = 0; i < 4; i++) console.log(` [${i}] R=${out[i*3]} G=${out[i*3+1]} B=${out[i*3+2]} (expect 100,150,200)`);
26+
}
27+
28+
// gradient 64×48 → 64×48 identity: pixel(2,0) should be R=8,G=0,B=5
29+
{
30+
const SRC_W=64, SRC_H=48, CH=3;
31+
const src = new Uint8Array(SRC_W * SRC_H * CH);
32+
for (let y = 0; y < SRC_H; y++) for (let x = 0; x < SRC_W; x++) {
33+
const i = (y*SRC_W+x)*CH;
34+
src[i+0] = Math.round(x*255/(SRC_W-1));
35+
src[i+1] = Math.round(y*255/(SRC_H-1));
36+
src[i+2] = Math.round((x+y)*255/(SRC_W+SRC_H-2));
37+
}
38+
const outLen = new BigUint64Array(1);
39+
const r = lib.symbols.pict_resize(ptr(src), SRC_W, SRC_H, CH, SRC_W, SRC_H, 1, ptr(outLen));
40+
const out = new Uint8Array(toArrayBuffer(r!, 0, Number(outLen[0])).slice(0));
41+
lib.symbols.pict_free_buffer(r!, outLen[0]);
42+
console.log("\n64x48 identity:");
43+
console.log(` (0,0): R=${out[0]} G=${out[1]} B=${out[2]} (src: R=0,G=0,B=0)`);
44+
console.log(` (2,0): R=${out[6]} G=${out[7]} B=${out[8]} (src: R=8,G=0,B=5)`);
45+
console.log(` (0,1): R=${out[SRC_W*3]} G=${out[SRC_W*3+1]} B=${out[SRC_W*3+2]} (src: R=0,G=5,B=2)`);
46+
}
47+
48+
lib.close();

test/diag_simple.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// 単純なベリファイ: 4x4 均一色を 2x2 にリサイズして、全ピクセルが同じ値になるか確認
2+
import { dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
3+
4+
const ZIG_LIB = "/Users/tuki/Develop/Projects/zenpix/npm/zenpix-darwin-arm64/libpict.dylib";
5+
const C_LIB = "/Users/tuki/Develop/Projects/zenpix-c/build/libpict.dylib";
6+
7+
function makeLib(path: string) {
8+
return dlopen(path, {
9+
pict_resize: {
10+
args: [FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.u8,
11+
FFIType.u32, FFIType.u32, FFIType.u32, FFIType.ptr],
12+
returns: FFIType.ptr,
13+
},
14+
pict_free_buffer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.void },
15+
});
16+
}
17+
18+
const zig = makeLib(ZIG_LIB);
19+
const c = makeLib(C_LIB);
20+
21+
// 4x4 RGB, all pixels = (100, 150, 200)
22+
const src = new Uint8Array(4 * 4 * 3).fill(0);
23+
for (let i = 0; i < 4*4; i++) { src[i*3]=100; src[i*3+1]=150; src[i*3+2]=200; }
24+
25+
function resize(lib: ReturnType<typeof makeLib>, name: string, srcW: number, srcH: number, dstW: number, dstH: number, ch: number) {
26+
const pixels = new Uint8Array(srcW * srcH * ch);
27+
for (let y = 0; y < srcH; y++) for (let x = 0; x < srcW; x++) {
28+
const i = (y*srcW+x)*ch;
29+
pixels[i+0] = 100; if (ch>1) pixels[i+1] = 150; if (ch>2) pixels[i+2] = 200;
30+
}
31+
const outLen = new BigUint64Array(1);
32+
const result = lib.symbols.pict_resize(ptr(pixels), srcW, srcH, ch, dstW, dstH, 1, ptr(outLen));
33+
if (!result) { console.log(`${name}: NULL`); return; }
34+
const out = new Uint8Array(toArrayBuffer(result, 0, Number(outLen[0])).slice(0));
35+
lib.symbols.pict_free_buffer(result, outLen[0]);
36+
console.log(`${name} ${srcW}x${srcH}${dstW}x${dstH} ch=${ch}:`);
37+
for (let i = 0; i < Math.min(dstW*dstH, 4); i++) {
38+
if (ch===3) console.log(` [${i}] R=${out[i*3]} G=${out[i*3+1]} B=${out[i*3+2]}`);
39+
else if (ch===1) console.log(` [${i}] V=${out[i]}`);
40+
}
41+
}
42+
43+
resize(zig, "ZIG", 4, 4, 2, 2, 3);
44+
resize(c, "C ", 4, 4, 2, 2, 3);
45+
console.log("---");
46+
resize(zig, "ZIG", 4, 4, 2, 2, 1);
47+
resize(c, "C ", 4, 4, 2, 2, 1);
48+
49+
zig.close(); c.close();

test/diag_zig.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
2+
3+
const ZIG_LIB = "/Users/tuki/Develop/Projects/zenpix/npm/zenpix-darwin-arm64/libpict.dylib";
4+
const C_LIB = "/Users/tuki/Develop/Projects/zenpix-c/build/libpict.dylib";
5+
6+
function makeLib(path: string) {
7+
return dlopen(path, {
8+
pict_resize: {
9+
args: [FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.u8,
10+
FFIType.u32, FFIType.u32, FFIType.u32, FFIType.ptr],
11+
returns: FFIType.ptr,
12+
},
13+
pict_free_buffer: { args: [FFIType.ptr, FFIType.u64], returns: FFIType.void },
14+
});
15+
}
16+
17+
const SRC_W = 64, SRC_H = 48, CH = 3;
18+
const src = new Uint8Array(SRC_W * SRC_H * CH);
19+
for (let y = 0; y < SRC_H; y++)
20+
for (let x = 0; x < SRC_W; x++) {
21+
const i = (y * SRC_W + x) * CH;
22+
src[i+0] = Math.round(x * 255 / (SRC_W-1));
23+
src[i+1] = Math.round(y * 255 / (SRC_H-1));
24+
src[i+2] = Math.round((x+y) * 255 / (SRC_W+SRC_H-2));
25+
}
26+
27+
console.log("Source pixel (2,0): R=%d G=%d B=%d", src[6], src[7], src[8]);
28+
29+
function doResize(lib: ReturnType<typeof makeLib>, name: string, dstW: number, dstH: number) {
30+
const outLen = new BigUint64Array(1);
31+
const result = lib.symbols.pict_resize(ptr(src), SRC_W, SRC_H, CH, dstW, dstH, 1, ptr(outLen));
32+
if (!result) { console.log(`${name} ${dstW}x${dstH}: NULL`); return; }
33+
const got = new Uint8Array(toArrayBuffer(result, 0, Number(outLen[0])).slice(0));
34+
lib.symbols.pict_free_buffer(result, outLen[0]);
35+
// print first 6 pixels
36+
for (let i = 0; i < 6; i++) {
37+
const R = got[i*3], G = got[i*3+1], B = got[i*3+2];
38+
const x = i % dstW, y = Math.floor(i / dstW);
39+
console.log(` ${name} (${x},${y}): R=${R} G=${G} B=${B}`);
40+
}
41+
}
42+
43+
const zig = makeLib(ZIG_LIB);
44+
const c = makeLib(C_LIB);
45+
46+
console.log("\n--- identity 64x48 → 64x48 ---");
47+
doResize(zig, "ZIG", 64, 48);
48+
doResize(c, "C ", 64, 48);
49+
50+
console.log("\n--- downscale 64x48 → 20x15 ---");
51+
doResize(zig, "ZIG", 20, 15);
52+
doResize(c, "C ", 20, 15);
53+
54+
zig.close();
55+
c.close();

test/generate_reference.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* generate_reference.ts — one-time script to produce reference_data.bin
3+
*
4+
* Uses the Zig-built libpict.dylib to generate ground-truth resize output.
5+
* Commit the resulting reference_data.bin; future CI uses it without Zig.
6+
*
7+
* Run: bun run test/generate_reference.ts
8+
*/
9+
10+
import { dlopen, FFIType, ptr, toArrayBuffer } from "bun:ffi";
11+
import { writeFileSync } from "fs";
12+
import { join } from "path";
13+
14+
const ZIG_LIB = "/Users/tuki/Develop/Projects/zenpix/npm/zenpix-darwin-arm64/libpict.dylib";
15+
16+
const lib = dlopen(ZIG_LIB, {
17+
pict_resize: {
18+
args: [FFIType.ptr, FFIType.u32, FFIType.u32, FFIType.u8,
19+
FFIType.u32, FFIType.u32, FFIType.u32, FFIType.ptr],
20+
returns: FFIType.ptr,
21+
},
22+
pict_free_buffer: {
23+
args: [FFIType.ptr, FFIType.u64],
24+
returns: FFIType.void,
25+
},
26+
});
27+
28+
// ── Synthetic input: 64×48 RGB gradient ──────────────────────────────────────
29+
const SRC_W = 64, SRC_H = 48, CH = 3;
30+
const src = new Uint8Array(SRC_W * SRC_H * CH);
31+
for (let y = 0; y < SRC_H; y++) {
32+
for (let x = 0; x < SRC_W; x++) {
33+
const i = (y * SRC_W + x) * CH;
34+
src[i + 0] = Math.round(x * 255 / (SRC_W - 1)); // R: left→right
35+
src[i + 1] = Math.round(y * 255 / (SRC_H - 1)); // G: top→bottom
36+
src[i + 2] = Math.round((x + y) * 255 / (SRC_W + SRC_H - 2));// B: diagonal
37+
}
38+
}
39+
40+
// ── Resize cases ──────────────────────────────────────────────────────────────
41+
const cases = [
42+
{ dstW: 20, dstH: 15, label: "64×48 → 20×15 (downscale ×0.3)" },
43+
{ dstW: 128, dstH: 96, label: "64×48 → 128×96 (upscale ×2)" },
44+
{ dstW: 64, dstH: 48, label: "64×48 → 64×48 (identity)" },
45+
];
46+
47+
// ── Generate ──────────────────────────────────────────────────────────────────
48+
const parts: Buffer[] = [];
49+
50+
const countBuf = Buffer.alloc(4);
51+
countBuf.writeUInt32LE(cases.length, 0);
52+
parts.push(countBuf);
53+
54+
for (const { dstW, dstH, label } of cases) {
55+
const outLen = new BigUint64Array(1);
56+
const result = lib.symbols.pict_resize(
57+
ptr(src), SRC_W, SRC_H, CH,
58+
dstW, dstH,
59+
1, // n_threads = 1 (deterministic)
60+
ptr(outLen),
61+
);
62+
if (!result) throw new Error(`Zig pict_resize failed: ${label}`);
63+
64+
const pixels = new Uint8Array(toArrayBuffer(result, 0, Number(outLen[0])).slice(0));
65+
lib.symbols.pict_free_buffer(result, outLen[0]);
66+
67+
const header = Buffer.alloc(16);
68+
header.writeUInt32LE(dstW, 0);
69+
header.writeUInt32LE(dstH, 4);
70+
header.writeUInt32LE(CH, 8);
71+
header.writeUInt32LE(pixels.length, 12);
72+
73+
parts.push(header, Buffer.from(pixels));
74+
console.log(` ${label}: ${pixels.length} bytes`);
75+
}
76+
77+
lib.close();
78+
79+
// ── Write ─────────────────────────────────────────────────────────────────────
80+
const outPath = join(import.meta.dir, "reference_data.bin");
81+
writeFileSync(outPath, Buffer.concat(parts));
82+
console.log(`\nSaved ${outPath}`);

0 commit comments

Comments
 (0)