114 lines
4.6 KiB
TypeScript
114 lines
4.6 KiB
TypeScript
|
|
import { describe, expect, it } from "vitest";
|
|||
|
|
import { jpegExifOrientation, rotateRgba } from "@/utils/image-to-pdf";
|
|||
|
|
|
|||
|
|
/** สร้าง JPEG หลอง (SOI + marker ที่กำหนด + SOI-trailer) สำหรับทดสอบ parser */
|
|||
|
|
function fakeJpeg(markers: Buffer[]): Buffer {
|
|||
|
|
return Buffer.concat([Buffer.from([0xff, 0xd8]), ...markers]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** APP1 Exif ที่มีแค่ tag Orientation (274) ตาม byte layout จริง */
|
|||
|
|
function app1Exif(orientation: number): Buffer {
|
|||
|
|
const tiff = Buffer.alloc(26);
|
|||
|
|
tiff.write("II", 0, "ascii");
|
|||
|
|
tiff.writeUInt16LE(42, 2);
|
|||
|
|
tiff.writeUInt32LE(8, 4);
|
|||
|
|
tiff.writeUInt16LE(1, 8);
|
|||
|
|
tiff.writeUInt16LE(274, 10);
|
|||
|
|
tiff.writeUInt16LE(3, 12);
|
|||
|
|
tiff.writeUInt32LE(1, 14);
|
|||
|
|
tiff.writeUInt16LE(orientation, 18);
|
|||
|
|
const payload = Buffer.concat([Buffer.from("Exif\0\0", "latin1"), tiff]);
|
|||
|
|
const app1 = Buffer.alloc(4 + payload.length);
|
|||
|
|
app1[0] = 0xff;
|
|||
|
|
app1[1] = 0xe1;
|
|||
|
|
app1.writeUInt16BE(payload.length + 2, 2);
|
|||
|
|
payload.copy(app1, 4);
|
|||
|
|
return app1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe("jpegExifOrientation", () => {
|
|||
|
|
it("อ่าน orientation จาก APP1 ได้ (ค่า 1–8)", () => {
|
|||
|
|
for (const v of [1, 3, 6, 8]) {
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([app1Exif(v)]))).toBe(v);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("ไม่มี EXIF / ไม่ใช่ JPEG = 1", () => {
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([]))).toBe(1);
|
|||
|
|
expect(jpegExifOrientation(Buffer.from([0x89, 0x50]))).toBe(1); // PNG signature
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("ข้าม marker อื่นก่อนเจอ APP1 (เช่น APP0 JFIF)", () => {
|
|||
|
|
const app0 = Buffer.from([0xff, 0xe0, 0x00, 0x04, 0x4a, 0x46]); // len=4
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([app0, app1Exif(6)]))).toBe(6);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("APP1 ที่ไม่ใช่ Exif หรือ value ผิดพิสัย = 1", () => {
|
|||
|
|
const notExif = Buffer.from([0xff, 0xe1, 0x00, 0x0a, 0x58, 0x4d, 0x4c, 0, 0, 1, 2, 3]); // "XML..."
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([notExif]))).toBe(1);
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([app1Exif(99)]))).toBe(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("ทนไฟล์เสี้ยว — APP1 ถูกตัดกลางคันไม่โยน exception (คืน 1)", () => {
|
|||
|
|
const full = app1Exif(6);
|
|||
|
|
for (const cut of [10, 14, 20, 24]) {
|
|||
|
|
const truncated = full.subarray(0, cut);
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([truncated]))).toBe(1);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("ข้าม fill bytes (0xFF ซ้ำ) ก่อน marker ได้", () => {
|
|||
|
|
const fills = Buffer.from([0xff, 0xff, 0xff]);
|
|||
|
|
expect(jpegExifOrientation(fakeJpeg([fills, app1Exif(6)]))).toBe(6);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/** ตาราง 3×2 RGBA — pixel ระบุตำแหน่งด้วยค่าสี r=x, g=y เพื่อตรวจ mapping */
|
|||
|
|
const W = 3;
|
|||
|
|
const H = 2;
|
|||
|
|
const rgba = new Uint8Array(W * H * 4);
|
|||
|
|
for (let y = 0; y < H; y++) {
|
|||
|
|
for (let x = 0; x < W; x++) {
|
|||
|
|
const i = (y * W + x) * 4;
|
|||
|
|
rgba[i] = x; // r = ตำแหน่ง x เดิม
|
|||
|
|
rgba[i + 1] = y; // g = ตำแหน่ง y เดิม
|
|||
|
|
rgba[i + 2] = 0;
|
|||
|
|
rgba[i + 3] = 255;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const pixel = (data: Uint8Array, x: number, y: number, width: number) => {
|
|||
|
|
const i = (y * width + x) * 4;
|
|||
|
|
return [data[i], data[i + 1]];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
describe("rotateRgba", () => {
|
|||
|
|
it("orientation 1 = ไม่แปลง", () => {
|
|||
|
|
const out = rotateRgba(rgba, W, H, 1);
|
|||
|
|
expect(out.width).toBe(W);
|
|||
|
|
expect(pixel(out.data, 2, 1, W)).toEqual([2, 1]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("6 = หมุน 90°CW: สลับกว้าง-สูง, มุมซ้ายบนเดิมไปมุมขวาบน", () => {
|
|||
|
|
const out = rotateRgba(rgba, W, H, 6);
|
|||
|
|
expect(out.width).toBe(H);
|
|||
|
|
expect(out.height).toBe(W);
|
|||
|
|
expect(pixel(out.data, H - 1, 0, H)).toEqual([0, 0]); // (0,0) → (h-1, 0)
|
|||
|
|
expect(pixel(out.data, 0, W - 1, H)).toEqual([W - 1, H - 1]); // (w-1,h-1) → (0, w-1)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("5 = transpose และ 7 = anti-transpose (mirror + หมุน)", () => {
|
|||
|
|
const t5 = rotateRgba(rgba, W, H, 5);
|
|||
|
|
expect(t5.width).toBe(H);
|
|||
|
|
expect(pixel(t5.data, 1, 2, H)).toEqual([2, 1]); // (x,y) → (y,x)
|
|||
|
|
const t7 = rotateRgba(rgba, W, H, 7);
|
|||
|
|
expect(t7.width).toBe(H);
|
|||
|
|
expect(pixel(t7.data, H - 1 - 1, W - 1 - 2, H)).toEqual([2, 1]); // (x,y) → (h-1-y, w-1-x)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it("2 = flip แนวนอน, 3 = 180°, 4 = flip แนวตั้ง", () => {
|
|||
|
|
expect(pixel(rotateRgba(rgba, W, H, 2).data, W - 1, 0, W)).toEqual([0, 0]);
|
|||
|
|
expect(pixel(rotateRgba(rgba, W, H, 3).data, W - 1, H - 1, W)).toEqual([0, 0]);
|
|||
|
|
expect(pixel(rotateRgba(rgba, W, H, 4).data, 0, H - 1, W)).toEqual([0, 0]);
|
|||
|
|
});
|
|||
|
|
});
|