/* global Office, console */ /** * ดึงไฟล์ .docx ปัจจุบันผ่าน File/Slice API (FR-2 ขั้นตอนที่ 5) * กติกา: Word เปิด File object พร้อมกันได้แค่ 2 ตัว → เรียก closeAsync เสมอใน finally * พอร์ตดิบจาก D:\word-addin-poc\addin\src\services\fileService.ts */ export const DOCX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; /** ขนาด slice ต่อครั้ง — 1MB กันกิน memory ยักษ์ในเครื่องรายงานน้อย */ export const PDF_CONTENT_TYPE = "application/pdf"; const SLICE_SIZE = 1024 * 1024; function officeError(result: Office.AsyncResult, fallback: string): Error { return new Error(result.error?.message || fallback); } function getFileAsync(fileType: Office.FileType): Promise { return new Promise((resolve, reject) => { try { Office.context.document.getFileAsync(fileType, { sliceSize: SLICE_SIZE }, (result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { resolve(result.value); } else { reject(officeError(result, "เปิดไฟล์เอกสารไม่สำเร็จ")); } }); } catch (error) { reject(error instanceof Error ? error : new Error("เปิดไฟล์เอกสารไม่สำเร็จ")); } }); } /** * host แต่ละตัวส่งข้อมูล slice มาคนละรูปแบบ (เอกสาร Office.js ระบุแค่ "byte array"): * - Word บน Windows ส่วนใหญ่ = ArrayBuffer / typed-array view * - Word บน Mac = base64 string หรือ byte array ธรรมดา (number[]) * ปรับเป็น Uint8Array ให้หมดก่อนประกอบเป็น Blob */ export function sliceDataToBytes(data: unknown): Uint8Array { if (data instanceof ArrayBuffer) { return new Uint8Array(data); } if (ArrayBuffer.isView(data)) { const view = data as ArrayBufferView; return new Uint8Array(view.buffer as ArrayBuffer, view.byteOffset, view.byteLength); } if (typeof data === "string") { // base64 (บาง host ใส่ data-url prefix หรือขึ้นบรรทัดมาด้วย — ตัดทิ้งก่อน decode) const base64 = data.replace(/^data:[^,]*?;base64,/, "").replace(/\s/g, ""); const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index++) { bytes[index] = binary.charCodeAt(index); } return bytes; } if (Array.isArray(data) && (data.length === 0 || typeof data[0] === "number")) { return Uint8Array.from(data); } throw new Error(`รูปแบบข้อมูลของ slice ไม่รองรับ (ได้รับ ${typeof data})`); } /** อ่าน slice หนึ่งชิ้นแล้ว normalize เป็น Uint8Array */ function getSliceAsync(file: Office.File, sliceIndex: number): Promise { return new Promise((resolve, reject) => { try { file.getSliceAsync(sliceIndex, (result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { try { resolve(sliceDataToBytes(result.value.data)); } catch (error) { reject( error instanceof Error ? error : new Error(`อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`) ); } } else { reject(officeError(result, `อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`)); } }); } catch (error) { reject(error instanceof Error ? error : new Error(`อ่าน slice ที่ ${sliceIndex} ไม่สำเร็จ`)); } }); } /** ปิด File object — fail ก็แค่ warn ไม่ทับ error ของ flow หลัก */ function closeFileQuietly(file: Office.File): Promise { return new Promise((resolve) => { try { file.closeAsync(() => resolve()); } catch (error) { console.warn("closeAsync failed:", error); resolve(); } }); } /** * ประกอบไฟล์ทั้งไฟล์เป็น Blob — อ่าน slice ทีละชิ้นตามลำดับ * เรียก closeAsync เสมอ (สำเร็จหรือ fail ก็ตาม) */ async function readFileAsBlob(fileType: Office.FileType, contentType: string): Promise { const file = await getFileAsync(fileType); try { const chunks: BlobPart[] = []; for (let index = 0; index < file.sliceCount; index++) { chunks.push(await getSliceAsync(file, index)); } return new Blob(chunks, { type: contentType }); } finally { await closeFileQuietly(file); } } export async function getCurrentDocumentFile(): Promise { return readFileAsBlob(Office.FileType.Compressed, DOCX_CONTENT_TYPE); } export async function getCurrentDocumentPdf(): Promise { if (!Office.context.requirements.isSetSupported("PdfFile", "1.1")) { throw new Error("Word นี้ไม่รองรับการสร้าง PDF"); } return readFileAsBlob(Office.FileType.Pdf, PDF_CONTENT_TYPE); }