109 lines
5.3 KiB
TypeScript
109 lines
5.3 KiB
TypeScript
|
|
// N3 — decode TIFF ฝั่ง client: ทุกเฟรม (IFD) → RGBA → canvas → PNG blob URL ต่อเฟรม
|
|||
|
|
// (TIFF หลายเฟรม = หลายหน้าเหมือน PDF หลายหน้า)
|
|||
|
|
// utif2 ถูก dynamic import เฉพาะไฟล์ tif เท่านั้น (pattern invariant #6)
|
|||
|
|
// ขนาดหน้า normalize กว้าง imagePageWidth (scale contract เดียวกับ jpg/png และ export)
|
|||
|
|
// caller ต้อง revoke ทุก src ที่ไม่ null ตอน cleanup กัน memory leak (rules/pdf-viewer.md)
|
|||
|
|
|
|||
|
|
const MAX_FRAME_DIMENSION = 4096; // cap ด้านยาว decoded frame (สแกน 600dpi ~5000px — annotation เป็น % ไม่กระทบ)
|
|||
|
|
|
|||
|
|
type TiffIfd = import("utif2").IFD;
|
|||
|
|
|
|||
|
|
/** อ่านค่าตัวเลขแรกของ tag (คืน 0 ถ้าไม่ใช่ array ตัวเลข — เช่น tag ชนิดอื่น) */
|
|||
|
|
function tagNumber(ifd: TiffIfd, tag: "t256" | "t257" | "t274"): number {
|
|||
|
|
const value = ifd[tag];
|
|||
|
|
return Array.isArray(value) ? Number(value[0]) || 0 : 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type DecodedTiffPage = {
|
|||
|
|
/** PNG blob URL ของเฟรม (null = เฟรมนี้ decode ไม่ได้ → placeholder เทา แต่หน้ายังมีขนาดจาก header) */
|
|||
|
|
src: string | null;
|
|||
|
|
width: number;
|
|||
|
|
height: number;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** TIFF orientation tag (274) — ครบทุกค่า 1–8 ตามมาตรฐาน EXIF (หมุน + mirror)
|
|||
|
|
* โดย transform matrix มาตรฐาน (canvas ปลายทางสลับกว้าง-สูงสำหรับ 5–8)
|
|||
|
|
* ต้องตรงกับ rotateRgba ของ utils/image-to-pdf.ts ฝั่ง export เสมอ */
|
|||
|
|
function rgbaToCanvas(rgba: Uint8Array, w: number, h: number, orientation: number): HTMLCanvasElement {
|
|||
|
|
const swap = orientation >= 5 && orientation <= 8; // หมุน 90° → สลับกว้าง-สูง
|
|||
|
|
const targetW = swap ? h : w;
|
|||
|
|
const targetH = swap ? w : h;
|
|||
|
|
const scale = Math.min(1, MAX_FRAME_DIMENSION / Math.max(targetW, targetH));
|
|||
|
|
|
|||
|
|
// วาด RGBA ลง canvas ต้นทางก่อน แล้วค่อย transform (orientation + downscale) ลง canvas จริง
|
|||
|
|
const src = document.createElement("canvas");
|
|||
|
|
src.width = w;
|
|||
|
|
src.height = h;
|
|||
|
|
src.getContext("2d")!.putImageData(new ImageData(new Uint8ClampedArray(rgba), w, h), 0, 0);
|
|||
|
|
|
|||
|
|
const canvas = document.createElement("canvas");
|
|||
|
|
canvas.width = Math.max(1, Math.round(targetW * scale));
|
|||
|
|
canvas.height = Math.max(1, Math.round(targetH * scale));
|
|||
|
|
const ctx = canvas.getContext("2d")!;
|
|||
|
|
ctx.imageSmoothingQuality = "high";
|
|||
|
|
ctx.scale(scale, scale);
|
|||
|
|
switch (orientation) {
|
|||
|
|
case 2:
|
|||
|
|
ctx.transform(-1, 0, 0, 1, w, 0);
|
|||
|
|
break;
|
|||
|
|
case 3:
|
|||
|
|
ctx.transform(-1, 0, 0, -1, w, h);
|
|||
|
|
break;
|
|||
|
|
case 4:
|
|||
|
|
ctx.transform(1, 0, 0, -1, 0, h);
|
|||
|
|
break;
|
|||
|
|
case 5:
|
|||
|
|
ctx.transform(0, 1, 1, 0, 0, 0);
|
|||
|
|
break;
|
|||
|
|
case 6:
|
|||
|
|
ctx.transform(0, 1, -1, 0, h, 0);
|
|||
|
|
break;
|
|||
|
|
case 7:
|
|||
|
|
// anti-transpose: (x,y) → (h-1-y, w-1-x) — e/f เป็น h,w ตาม rotateRgba ฝั่ง export
|
|||
|
|
ctx.transform(0, -1, -1, 0, h, w);
|
|||
|
|
break;
|
|||
|
|
case 8:
|
|||
|
|
ctx.transform(0, -1, 1, 0, 0, w);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
ctx.drawImage(src, 0, 0);
|
|||
|
|
return canvas;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function decodeTiffPages(
|
|||
|
|
buffer: ArrayBuffer,
|
|||
|
|
imagePageWidth: number,
|
|||
|
|
): Promise<DecodedTiffPage[]> {
|
|||
|
|
const UTIF = await import("utif2");
|
|||
|
|
const ifds = UTIF.decode(buffer);
|
|||
|
|
// เอาเฉพาะ IFD ที่เป็นเฟรมรูป (มี tag ขนาด t256/t257) — ไม่ใช่ EXIF metadata IFD
|
|||
|
|
const frames = ifds.filter((ifd) => Array.isArray(ifd.t256) && Array.isArray(ifd.t257));
|
|||
|
|
if (frames.length === 0) throw new Error("ไม่พบเฟรมรูปในไฟล์ TIFF");
|
|||
|
|
|
|||
|
|
const pages: DecodedTiffPage[] = [];
|
|||
|
|
for (const ifd of frames) {
|
|||
|
|
const headerW = tagNumber(ifd, "t256");
|
|||
|
|
const headerH = tagNumber(ifd, "t257");
|
|||
|
|
try {
|
|||
|
|
UTIF.decodeImage(buffer, ifd);
|
|||
|
|
const rgba = UTIF.toRGBA8(ifd);
|
|||
|
|
const orientation = tagNumber(ifd, "t274") || 1;
|
|||
|
|
const canvas = rgbaToCanvas(rgba, ifd.width, ifd.height, orientation);
|
|||
|
|
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/png"));
|
|||
|
|
if (!blob) throw new Error("แปลง canvas เป็น PNG ไม่ได้");
|
|||
|
|
pages.push({
|
|||
|
|
src: URL.createObjectURL(blob),
|
|||
|
|
width: imagePageWidth,
|
|||
|
|
height: (canvas.height / canvas.width) * imagePageWidth,
|
|||
|
|
});
|
|||
|
|
} catch (err) {
|
|||
|
|
// เฟรมเดียวพัง (เช่น compression ที่ utif2 ไม่รอง) ไม่ทำให้ทั้งไฟล์พัง — placeholder เทา
|
|||
|
|
// แต่ขนาดหน้ายังเอาจาก header ได้ → page count/scroll/bookmark/deep link เสถียร
|
|||
|
|
console.error("decode เฟรม TIFF ไม่ได้ — แสดง placeholder", err);
|
|||
|
|
const ratio = headerW > 0 && headerH > 0 ? headerH / headerW : 1.4;
|
|||
|
|
pages.push({ src: null, width: imagePageWidth, height: imagePageWidth * ratio });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return pages;
|
|||
|
|
}
|