import { createEngineModule, ENGINE_BUILD } from './engine-loader.js'; export const EC_BUILD = ENGINE_BUILD; const createEditCore = createEngineModule; const devBust = ''; import { applyType3LeftoverSurgery } from './type3surgery.js'; const RUN_SIZE = 60; const FMT_SIZE = 36; export const OBJ = { UNKNOWN: 0, TEXT: 1, PATH: 2, IMAGE: 3, SHADING: 4, FORM: 5, }; export function sfntCovers(bytes, cps) { try { const u16 = (o) => (bytes[o] << 8) | bytes[o + 1]; const u32 = (o) => ((bytes[o] << 24) | (bytes[o + 1] << 16) | (bytes[o + 2] << 8) | bytes[o + 3]) >>> 0; let base = 0; if (u32(0) === 0x74746366) base = u32(12); const num = u16(base + 4); let cmap = 0; for (let i = 0; i < num; i++) { const o = base + 12 + i * 16; if (u32(o) === 0x636d6170) { cmap = u32(o + 8); break; } } if (!cmap) return false; const nt = u16(cmap + 2); let sub = 0, fmt = -1, score = -1; for (let i = 0; i < nt; i++) { const rec = cmap + 4 + i * 8; const pid = u16(rec), eid = u16(rec + 2), off = cmap + u32(rec + 4); const f = u16(off); const sc = pid === 3 && eid === 10 ? 3 : pid === 3 && eid === 1 ? 2 : pid === 0 ? 1 : 0; if (sc > score && (f === 4 || f === 12)) { score = sc; sub = off; fmt = f; } } if (!sub) return false; const lookup4 = (cp) => { if (cp > 0xffff) return 0; const segX2 = u16(sub + 6); for (let s2 = 0; s2 < segX2; s2 += 2) { const end = u16(sub + 14 + s2); if (cp <= end) { const start = u16(sub + 16 + segX2 + s2); if (cp < start) return 0; const delta = u16(sub + 16 + segX2 * 2 + s2); const roBase = sub + 16 + segX2 * 3 + s2; const ro = u16(roBase); if (!ro) return (cp + delta) & 0xffff; const g = u16(roBase + ro + (cp - start) * 2); return g ? (g + delta) & 0xffff : 0; } } return 0; }; const lookup12 = (cp) => { const n = u32(sub + 12); let lo = 0, hi = n - 1; while (lo <= hi) { const mid = (lo + hi) >> 1, g = sub + 16 + mid * 12; const s0 = u32(g), e0 = u32(g + 4); if (cp < s0) hi = mid - 1; else if (cp > e0) lo = mid + 1; else return u32(g + 8) + (cp - s0); } return 0; }; for (const cp of cps) { if (cp === 0x20 || cp === 0x0a || cp === 0x0d) continue; if ((fmt === 12 ? lookup12(cp) : lookup4(cp)) === 0) return false; } return true; } catch { return false; } } export function scriptFallbackFamilies(cps) { const TABLE = [ [ [ [0x0600, 0x06ff], [0x0750, 0x077f], [0xfb50, 0xfdff], [0xfe70, 0xfeff], ], [ 'Geeza Pro', 'Al Nile', 'Damascus', 'Baghdad', 'Noto Naskh Arabic', 'Noto Sans Arabic', 'Arial Unicode MS', 'Arial', ], ], [ [ [0x0590, 0x05ff], [0xfb1d, 0xfb4f], ], [ 'Arial Hebrew', 'Lucida Grande', 'Noto Sans Hebrew', 'Arial Unicode MS', 'Arial', ], ], [ [[0x0900, 0x097f]], [ 'Kohinoor Devanagari', 'Devanagari MT', 'ITF Devanagari', 'Noto Sans Devanagari', 'Arial Unicode MS', ], ], [ [[0x0980, 0x09ff]], ['Kohinoor Bangla', 'Bangla MN', 'Noto Sans Bengali', 'Arial Unicode MS'], ], [ [[0x0a00, 0x0a7f]], ['Gurmukhi MN', 'Noto Sans Gurmukhi', 'Arial Unicode MS'], ], [ [[0x0a80, 0x0aff]], [ 'Kohinoor Gujarati', 'Gujarati MT', 'Noto Sans Gujarati', 'Arial Unicode MS', ], ], [ [[0x0b80, 0x0bff]], ['Tamil MN', 'InaiMathi', 'Noto Sans Tamil', 'Arial Unicode MS'], ], [ [[0x0c00, 0x0c7f]], ['Kohinoor Telugu', 'Telugu MN', 'Noto Sans Telugu', 'Arial Unicode MS'], ], [ [[0x0c80, 0x0cff]], ['Kannada MN', 'Noto Sans Kannada', 'Arial Unicode MS'], ], [ [[0x0d00, 0x0d7f]], ['Malayalam MN', 'Noto Sans Malayalam', 'Arial Unicode MS'], ], [ [[0x0e00, 0x0e7f]], ['Thonburi', 'Sukhumvit Set', 'Noto Sans Thai', 'Arial Unicode MS'], ], [ [ [0x3040, 0x30ff], [0x3400, 0x9fff], [0xf900, 0xfaff], ], [ 'PingFang SC', 'Hiragino Sans', 'Apple SD Gothic Neo', 'Noto Sans CJK SC', 'Arial Unicode MS', ], ], [ [[0xac00, 0xd7af]], ['Apple SD Gothic Neo', 'Noto Sans KR', 'Arial Unicode MS'], ], ]; const out = []; for (const cp of cps) { for (const [ranges, fams] of TABLE) { if (ranges.some(([a, b]) => cp >= a && cp <= b)) for (const f of fams) if (!out.includes(f)) out.push(f); } } return out; } const PAGE_CACHE_MAX = 6; export class PdfEngine { static localFonts = new Map(); constructor(module) { this.M = module; this.doc = 0; this.page = 0; this.session = 0; this.pageIndex = 0; this.pageCount = 0; this.pageWidth = 0; this.pageHeight = 0; this._docBuf = 0; this._providerPtr = 0; } _makeProvider() { const M = this.M; return M.addFunction( (ctx, familyPtr, bold, italic, cps, n, outData, outSize) => { let fam = M.UTF8ToString(familyPtr); fam = fam.replace(/ (serif|mono)$/, ''); const want = []; for (let i = 0; i < n; i++) want.push(M.HEAPU32[(cps >> 2) + i]); const sk = '|' + (bold ? 1 : 0) + (italic ? 1 : 0); const names = [fam, ...scriptFallbackFamilies(want)]; let bytes = null; for (const nm of names) { for (const k of [nm + sk, nm]) { const b = PdfEngine.localFonts.get(k); if (b && b.length && (!want.length || sfntCovers(b, want))) { bytes = b; break; } } if (bytes) break; } if (!bytes || !bytes.length) return 0; const buf = M._ec_buffer_alloc(bytes.length); if (!buf) return 0; M.HEAPU8.set(bytes, buf); M.HEAPU32[outData >> 2] = buf; M.HEAPU32[outSize >> 2] = bytes.length; return 1; }, 'iiiiiiiii' ); } static async create() { const M = await createEditCore( devBust ? { locateFile: (f, prefix) => `${prefix}${f}?v=${devBust}`, } : undefined ); const cfg = M._malloc(48); M.HEAPU8.fill(0, cfg, cfg + 48); M.HEAPU32[cfg >> 2] = 2; M._FPDF_InitLibraryWithConfig(cfg); M._free(cfg); return new PdfEngine(M); } static disambiguateSubsetFonts(bytes) { const s = new TextDecoder('latin1').decode(bytes); const fonts = []; for (const m of s.matchAll(/(\d+)\s+0\s+obj\b/g)) { const end = s.indexOf('endobj', m.index); if (end < 0) continue; const body = s.slice(m.index, end); if (!/\/Type\s*\/Font\b/.test(body)) continue; const bf = body.match(/\/BaseFont\s*\/([A-Z]{6})\+([^\s/\[\]<>()]+)/); if (!bf) continue; const fd = body.match(/\/FontDescriptor\s+(\d+)\s+0\s+R/); fonts.push({ obj: Number(m[1]), tagAt: m.index + bf.index + bf[0].indexOf(bf[1]), tag: bf[1], name: bf[2], desc: fd ? Number(fd[1]) : 0, }); } if (!fonts.length) return bytes; const containerFonts = new Set(); for (const m of s.matchAll(/(\d+)\s+0\s+obj\b/g)) { const end = s.indexOf('endobj', m.index); if (end < 0) continue; const body = s.slice(m.index, end); if (!/\/Subtype\s*\/Form\b/.test(body)) continue; const fi = body.indexOf('/Font'); if (fi < 0) continue; for (const r of body.slice(fi, fi + 800).matchAll(/(\d+)\s+0\s+R/g)) containerFonts.add(Number(r[1])); } if (!containerFonts.size) return bytes; const groups = new Map(); for (const f of fonts) { const k = f.tag + '+' + f.name; if (!groups.has(k)) groups.set(k, []); groups.get(k).push(f); } const taken = new Set(fonts.map((f) => f.tag)); const tagFor = (n) => { let t = ''; for (let i = 0; i < 6; i++) { t = String.fromCharCode(65 + (n % 26)) + t; n = Math.floor(n / 26); } return t; }; let seq = 0; const nextTag = () => { let t; do { t = tagFor(seq++); } while (taken.has(t)); taken.add(t); return t; }; const esc = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const faces = new Set(fonts.map((f) => f.name)); const edits = []; for (const [, list] of groups) { if (list.length < 2) continue; if ( !list.some((f) => containerFonts.has(f.obj)) || !list.some((f) => !containerFonts.has(f.obj)) ) continue; for (const f of list) { if (!containerFonts.has(f.obj) || !f.name.length) continue; let face = null; for (let c = 0; c < 26 && !face; c++) { const cand = f.name.slice(0, -1) + String.fromCharCode(65 + c); if (!faces.has(cand)) face = cand; } if (!face) continue; faces.add(face); const tag = nextTag(), lastCh = face[face.length - 1]; edits.push([f.tagAt, tag], [f.tagAt + 7 + f.name.length - 1, lastCh]); if (!f.desc) continue; const dm = s.match( new RegExp('(?:^|[^0-9])(' + f.desc + '\\s+0\\s+obj\\b)') ); if (!dm) continue; const dAt = dm.index + dm[0].indexOf(dm[1]); const dEnd = s.indexOf('endobj', dAt); const dBody = s.slice(dAt, dEnd < 0 ? undefined : dEnd); const fn = dBody.match( new RegExp('/FontName\\s*/(' + f.tag + ')\\+' + esc(f.name)) ); if (!fn) continue; const at = dAt + fn.index + fn[0].indexOf(fn[1]); edits.push([at, tag], [at + 7 + f.name.length - 1, lastCh]); } } if (!edits.length) return bytes; const out = new Uint8Array(bytes); for (const [at, text] of edits) { for (let i = 0; i < text.length; i++) out[at + i] = text.charCodeAt(i); } return out; } open(bytes, password, _reentrant) { bytes = PdfEngine.disambiguateSubsetFonts(bytes); this._originalBytes = bytes; this.close(); const M = this.M; const buf = M._malloc(bytes.length); M.HEAPU8.set(bytes, buf); let pw = 0; if (password) { const n = M.lengthBytesUTF8(password) + 1; pw = M._malloc(n); M.stringToUTF8(password, pw, n); } const doc = M._FPDF_LoadMemDocument64(buf, bytes.length, pw); if (pw) M._free(pw); if (!doc) { const err = M._FPDF_GetLastError ? M._FPDF_GetLastError() : -1; M._free(buf); this.lastOpenError = err; const e = new Error( err === 4 ? 'This PDF is password-protected.' : 'This file could not be opened as a PDF.' ); e.passwordRequired = err === 4; throw e; } this._docBuf = buf; this.doc = doc; if (!this._providerPtr) this._providerPtr = this._makeProvider(); this.session = M._ec_session_create(doc, this._providerPtr, 0); if (M._ec_set_flatten_forms) { const usesPattern = /\/Pattern\b/.test( new TextDecoder('latin1').decode( bytes.subarray(0, Math.min(bytes.length, 8 << 20)) ) ); this._usesPattern = usesPattern; M._ec_set_flatten_forms(this.session, usesPattern ? 0 : 1); } this.pageCount = M._FPDF_GetPageCount(doc); this._normalized = new Set(); this._reencoded = new Set(); this._regenHostile = new Set(); this._editedPages = new Set(); this._t3seg = null; this._t3state = {}; this._t3anchors = {}; this._fragilePages = null; this.loadPage(0); if (!_reentrant) { const info = this.documentInfo(); this.security = info; if (info.encrypted && !info.signatures) { const clear = this.save(); if (clear && clear.length) { this.open(clear, null, true); this.security = { ...info, strippedOnLoad: true }; } } } } reopen(bytes, pageIndex) { const keep = this.pageIndex; const orig = this._originalBytes; const edited = this._editedPages; const t3seg = this._t3seg; const fragile = this._fragilePages; this._splicePlans = null; this._spliceOk = false; this.open(bytes); if (orig) this._originalBytes = orig; if (edited) this._editedPages = edited; if (fragile) this.setFragilePages([...fragile]); if (t3seg) this._t3seg = t3seg; this.loadPage(Math.min(pageIndex ?? keep, this.pageCount - 1)); } setType3Seg(seg) { if (seg && Object.keys(seg).length) this._t3seg = seg; } _t3readState() { const ptr = this.M._ec_page_text_state(this.session, this.page); if (!ptr) return null; let st = null; try { st = JSON.parse(this.M.UTF8ToString(ptr)); } catch {} this.M._ec_string_free(ptr); return st; } _t3anchor(st) { const seg = this._t3seg?.[this.pageIndex]; if (!seg || !st) return; const anchors = ((this._t3anchors ||= {})[this.pageIndex] ||= {}); for (const o of st) { if (!o.t3 || !o.glyphs || !o.glyphs.length) continue; if (anchors[o.font] != null) continue; const oCodes = o.glyphs.map((g) => g[0]); const hit = seg.tjs.find( (tj) => Math.abs(tj.e - o.m[4]) < 0.05 && Math.abs(tj.f - o.m[5]) < 0.05 && tj.codes.length >= oCodes.length && oCodes.every((c, k) => c === tj.codes[k]) ); if (hit) anchors[o.font] = hit.font; } } close() { const M = this.M; if (this._pageCache) { for (const [, handle] of this._pageCache) if (handle && handle !== this.page) M._FPDF_ClosePage(handle); this._pageCache.clear(); } if (this.page) M._FPDF_ClosePage(this.page); if (this.session) M._ec_session_destroy(this.session); if (this.doc) M._FPDF_CloseDocument(this.doc); if (this._docBuf) M._free(this._docBuf); this.page = this.session = this.doc = this._docBuf = 0; } loadPage(index) { const M = this.M; this.renderParagraphLiveEnd(); if (this.page && this._pageDirty) this.generateContent(); const cache = (this._pageCache ||= new Map()); if (this.page) cache.set(this.pageIndex, this.page); let handle = cache.get(index); if (handle) cache.delete(index); else handle = M._FPDF_LoadPage(this.doc, index); cache.set(index, handle); this.page = handle; while (cache.size > PAGE_CACHE_MAX) { let victim = null; for (const key of cache.keys()) { if (key === index || this.pinnedPages?.has(key)) continue; victim = key; break; } if (victim === null) break; const dead = cache.get(victim); cache.delete(victim); if (dead) { this.M._ec_history_drop_page?.(this.session, dead); M._FPDF_ClosePage(dead); } this.onPageEvicted?.(victim); } this.pageIndex = index; this._pageDirty = false; this.pageWidth = M._FPDF_GetPageWidthF(this.page); this.pageHeight = M._FPDF_GetPageHeightF(this.page); if (this._fragilePages?.has(index)) { M._ec_mark_fonts_fragile(this.session, this.page); } this._readPageTransform(); } setFragilePages(pages) { this._fragilePages = new Set(pages || []); if (this.page && this._fragilePages.has(this.pageIndex)) { this.M._ec_mark_fonts_fragile(this.session, this.page); } } pageWasNormalized() { return !!this._reencoded?.has(this.pageIndex); } normalizeFontsForEdit() { if (!this.session || this._normalized?.has(this.pageIndex)) return; (this._normalized ||= new Set()).add(this.pageIndex); const lossy = this.M._ec_page_regen_is_lossy( this.session, this.page, this.pageIndex ); if (lossy === 2) { (this._regenHostile ||= new Set()).add(this.pageIndex); return 0; } if (lossy === 3) { return 0; } if (lossy) { const rebuilt = this.M._ec_reencode_page_fonts(this.session, this.page); this._pageDirty = true; if (rebuilt > 0) (this._reencoded ||= new Set()).add(this.pageIndex); return rebuilt; } return 0; } pageRegenHostile() { return !!this._regenHostile?.has(this.pageIndex); } documentInfo() { const M = this.M; const ptr = M._ec_document_info(this.doc); if (!ptr) return { pages: 0, signatures: 0 }; const j = M.UTF8ToString(ptr); M._ec_string_free(ptr); return JSON.parse(j); } metaText(tag) { const M = this.M; const kp = M._malloc(tag.length + 1); for (let i = 0; i < tag.length; i++) M.HEAPU8[kp + i] = tag.charCodeAt(i); M.HEAPU8[kp + tag.length] = 0; const n = M._FPDF_GetMetaText(this.doc, kp, 0, 0); let out = ''; if (n > 2) { const bp = M._malloc(n); M._FPDF_GetMetaText(this.doc, kp, bp, n); const chars = []; for (let i = 0; i + 1 < n - 1; i += 2) chars.push(M.HEAPU8[bp + i] | (M.HEAPU8[bp + i + 1] << 8)); out = String.fromCharCode(...chars); M._free(bp); } M._free(kp); return out; } static sniffPdfA(bytes) { const probe = 'pdfaid:part'; const limit = Math.min(bytes.length, 4 << 20); outer: for (let i = 0; i + probe.length < limit; i++) { if (bytes[i] !== 112) continue; for (let j = 0; j < probe.length; j++) { if (bytes[i + j] !== probe.charCodeAt(j)) continue outer; } return true; } return false; } buildModel() { const M = this.M; if (this._pageStale) { this.renderParagraphLiveEnd(); if (this._pageDirty) this.generateContent(); M._FPDF_ClosePage(this.page); this.page = M._FPDF_LoadPage(this.doc, this.pageIndex); this._pageCache?.set(this.pageIndex, this.page); this.onPageEvicted?.(this.pageIndex); this._pageStale = false; this._pageDirty = false; } if (this._t3seg && this._t3seg[this.pageIndex]) { const st = this._t3readState(); if (st) this._t3anchor(st); } const ptr = M._ec_build_page_model(this.session, this.page); if (!ptr) return []; const json = M.UTF8ToString(ptr); M._ec_string_free(ptr); this._readPageTransform(); return JSON.parse(json).paragraphs; } _readPageTransform() { const M = this.M; if (!this.session || !this.page) return; const cp = M._malloc(24); M._ec_page_transform(this.session, this.page, cp); this.pageT = Array.from( { length: 6 }, (_, i) => M.HEAPF32[(cp + i * 4) >> 2] ); M._free(cp); this.cropX = -this.pageT[4]; this.cropY = -this.pageT[5]; } toModel(x, y) { const t = this.pageT || [1, 0, 0, 1, 0, 0]; return { x: t[0] * x + t[2] * y + t[4], y: t[1] * x + t[3] * y + t[5] }; } toUser(x, y) { const t = this.pageT || [1, 0, 0, 1, 0, 0]; const det = t[0] * t[3] - t[1] * t[2] || 1; const dx = x - t[4], dy = y - t[5]; return { x: (t[3] * dx - t[2] * dy) / det, y: (-t[1] * dx + t[0] * dy) / det, }; } _allocStr(s) { const M = this.M; const n = M.lengthBytesUTF8(s || '') + 1; const p = M._malloc(n); M.stringToUTF8(s || '', p, n); return p; } _writeRuns(runs) { const M = this.M; const arr = M._malloc(RUN_SIZE * runs.length); const strs = []; runs.forEach((r, i) => { const b = arr + i * RUN_SIZE; const tp = this._allocStr(r.text); const fp = this._allocStr(r.family || ''); strs.push(tp, fp); M.HEAPU32[(b + 0) >> 2] = tp; M.HEAPU32[(b + 4) >> 2] = fp; M.HEAP32[(b + 8) >> 2] = r.bold === 2 ? 2 : r.bold ? 1 : 0; M.HEAP32[(b + 12) >> 2] = r.italic === 2 ? 2 : r.italic ? 1 : 0; M.HEAPF32[(b + 16) >> 2] = r.size; M.HEAPU32[(b + 20) >> 2] = r.rgba >>> 0; M.HEAP32[(b + 24) >> 2] = r.underline ? 1 : 0; M.HEAP32[(b + 28) >> 2] = r.strike ? 1 : 0; M.HEAP32[(b + 32) >> 2] = r.script | 0; M.HEAP32[(b + 36) >> 2] = r.sourceIndex == null ? -1 : r.sourceIndex; M.HEAP32[(b + 40) >> 2] = r.renderMode | 0; M.HEAPU32[(b + 44) >> 2] = (r.strokeRgba || 0) >>> 0; M.HEAPF32[(b + 48) >> 2] = r.strokeWidth || 1; M.HEAPF32[(b + 52) >> 2] = r.hScale || 1; M.HEAPF32[(b + 56) >> 2] = r.rise || 0; }); return { arr, strs, count: runs.length }; } _writeFmt(fmt) { const M = this.M; const p = M._malloc(FMT_SIZE); M.HEAP32[(p + 0) >> 2] = fmt.align | 0; M.HEAPF32[(p + 4) >> 2] = fmt.lineSpacing; M.HEAPF32[(p + 8) >> 2] = fmt.charSpacing || 0; M.HEAPF32[(p + 12) >> 2] = fmt.paraSpacing || 0; M.HEAPF32[(p + 16) >> 2] = fmt.wordSpacing || 0; M.HEAPF32[(p + 20) >> 2] = fmt.firstIndent || 0; M.HEAPF32[(p + 24) >> 2] = fmt.hangIndent || 0; M.HEAP32[(p + 28) >> 2] = fmt.dir | 0; M.HEAP32[(p + 32) >> 2] = fmt.listLevel | 0; return p; } _free(alloc) { for (const s of alloc.strs) this.M._free(s); this.M._free(alloc.arr); } _decode(ptr) { if (!ptr) return null; const j = this.M.UTF8ToString(ptr); this.M._ec_string_free(ptr); return JSON.parse(j); } previewParagraph(id, runs, fmt) { const a = this._writeRuns(runs); const f = this._writeFmt(fmt); const ptr = this.M._ec_preview_paragraph( this.session, this.page, id, a.arr, a.count, f ); this._free(a); this.M._free(f); if (!ptr) return null; const json = this.M.UTF8ToString(ptr); this.M._ec_string_free(ptr); try { return JSON.parse(json); } catch { return null; } } commitParagraph(id, runs, fmt) { this._pageDirty = true; const a = this._writeRuns(runs); const f = this._writeFmt(fmt); const out = this._decode( this.M._ec_commit_paragraph( this.session, this.page, id, a.arr, a.count, f ) ); this._free(a); this.M._free(f); return out; } renderParagraphLive(id, runs, fmt, scale, mx, my, mw, mh) { const M = this.M; const a = this._writeRuns(runs); const f = this._writeFmt(fmt); const wp = M._malloc(8); M.HEAP32[wp >> 2] = 0; M.HEAP32[(wp + 4) >> 2] = 0; const ptr = M._ec_render_paragraph_live( this.session, this.page, id, a.arr, a.count, f, scale, mx, my, mw, mh, wp, wp + 4 ); const w = M.HEAP32[wp >> 2], h = M.HEAP32[(wp + 4) >> 2]; M._free(wp); this._free(a); M._free(f); if (!ptr || !w || !h) return null; const data = new Uint8ClampedArray(M.HEAPU8.subarray(ptr, ptr + w * h * 4)); M._ec_string_free(ptr); return { width: w, height: h, data }; } renderPageRegion(fullW, fullH, px, py, pw, ph) { const M = this.M; const FPDFBitmap_BGRA = 4; const bmp = M._FPDFBitmap_CreateEx(pw, ph, FPDFBitmap_BGRA, 0, 0); if (!bmp) return null; M._FPDFBitmap_FillRect(bmp, 0, 0, pw, ph, 0xffffffff); const FPDF_ANNOT = 0x01, FPDF_LCD_TEXT = 0x02, FPDF_REVERSE_BYTE_ORDER = 0x10; const FLAGS = FPDF_ANNOT | FPDF_LCD_TEXT | FPDF_REVERSE_BYTE_ORDER; M._FPDF_RenderPageBitmap(bmp, this.page, -px, -py, fullW, fullH, 0, FLAGS); M._ec_form_draw( this.session, this.page, bmp, -px, -py, fullW, fullH, 0, FLAGS ); const buf = M._FPDFBitmap_GetBuffer(bmp); const stride = M._FPDFBitmap_GetStride(bmp); const out = new Uint8ClampedArray(pw * ph * 4); if (stride === pw * 4) { out.set(M.HEAPU8.subarray(buf, buf + pw * ph * 4)); } else { for (let y = 0; y < ph; y++) { out.set( M.HEAPU8.subarray(buf + y * stride, buf + y * stride + pw * 4), y * pw * 4 ); } } M._FPDFBitmap_Destroy(bmp); return { width: pw, height: ph, data: out }; } addParagraph(x, yTop, width, runs, fmt) { this._pageDirty = true; const a = this._writeRuns(runs); const f = this._writeFmt(fmt); const out = this._decode( this.M._ec_add_paragraph( this.session, this.page, x, yTop, width, a.arr, a.count, f ) ); this._free(a); this.M._free(f); return out; } synthRunFont(paraId, runIndex) { const M = this.M; const sp = M._malloc(4); M.HEAPU32[sp >> 2] = 0; const ptr = M._ec_synth_run_font( this.session, this.page, paraId, runIndex, sp ); const size = M.HEAPU32[sp >> 2]; M._free(sp); if (!ptr || !size) return null; if (size === 1) { const flag = M.HEAPU8[ptr]; M._ec_string_free(ptr); return flag === 0xdd ? 'dishonest' : null; } const bytes = M.HEAPU8.slice(ptr, ptr + size); M._ec_string_free(ptr); return bytes; } duplicateParagraph(id, dx, dy) { this._pageDirty = true; return this._decode( this.M._ec_duplicate_paragraph(this.session, this.page, id, dx, dy) ); } cloneMarker(srcId, dstId) { this._pageDirty = true; return this.M._ec_clone_marker(this.session, this.page, srcId, dstId) !== 0; } moveParagraph(id, dx, dy) { this._pageDirty = true; return this.M._ec_move_paragraph(this.session, this.page, id, dx, dy) !== 0; } resizeParagraph(id, width) { this._pageDirty = true; return this._decode( this.M._ec_resize_paragraph(this.session, this.page, id, width) ); } historyBeginStep(label) { const M = this.M; const lbl = this._allocStr(label || 'edit'); M._ec_history_begin(this.session, this.page, lbl); M._free(lbl); } historyEndStep() { const before = this.M._ec_history_depth(this.session, 0); this.M._ec_history_end(this.session, this.page); if (this._spliceOk === false) return; if (this.M._ec_history_depth(this.session, 0) <= before) return; const plan = this.lastSplicePlan(); if (plan) (this._splicePlans ||= []).push({ page: this.pageIndex, plan }); else this._spliceOk = false; } lastSplicePlan() { const ptr = this.M._ec_last_splice_plan(this.session, this.page); if (!ptr) return null; const json = this.M.UTF8ToString(ptr); this.M._ec_string_free(ptr); try { return JSON.parse(json); } catch { return null; } } editStep(label, fn) { this.historyBeginStep(label); try { return fn(); } finally { this.historyEndStep(); } } historyNoteMatrix(handle) { this.M._ec_history_note_matrix(this.session, this.page, handle); } historyNoteZOrder(handle) { this.M._ec_history_note_zorder(this.session, this.page, handle); } historyNoteInsert(handle) { this.M._ec_history_note_insert(this.session, this.page, handle); } historyRemoveObject(handle) { this._pageDirty = true; return ( this.M._ec_history_remove_object(this.session, this.page, handle) !== 0 ); } historyUndo() { this._pageDirty = true; const ok = this.M._ec_history_undo(this.session, this.page) !== 0; if (ok && this._splicePlans?.length) { const last = this._splicePlans[this._splicePlans.length - 1]; if (last.page === this.pageIndex) this._splicePlans.pop(); else this._spliceOk = false; } return ok; } historyRedo() { this._pageDirty = true; const ok = this.M._ec_history_redo(this.session, this.page) !== 0; if (ok && this._spliceOk !== false) { const plan = this.lastSplicePlan(); if (plan) (this._splicePlans ||= []).push({ page: this.pageIndex, plan }); else this._spliceOk = false; } return ok; } historyDepth() { return { undo: this.M._ec_history_depth(this.session, 0), redo: this.M._ec_history_depth(this.session, 1), }; } historyClear() { this.M._ec_history_clear(this.session); } spellLoad(bytes) { const M = this.M; const buf = M._malloc(bytes.length); M.HEAPU8.set(bytes, buf); const n = M._ec_spell_load(this.session, buf, bytes.length); M._free(buf); return n; } spellCheckPage() { const M = this.M; const ptr = M._ec_spell_check_page(this.session, this.page); if (!ptr) return null; const json = M.UTF8ToString(ptr); M._ec_string_free(ptr); try { return JSON.parse(json); } catch { return null; } } selectText(x0, y0, x1, y1, mode) { const M = this.M; const ptr = M._ec_select_text( this.session, this.page, x0, y0, x1, y1, mode | 0 ); if (!ptr) return null; const json = M.UTF8ToString(ptr); M._ec_string_free(ptr); try { return JSON.parse(json); } catch { return null; } } pageTextJson() { const M = this.M; const ptr = M._ec_page_text_json(this.session, this.page); if (!ptr) return null; const json = M.UTF8ToString(ptr); M._ec_string_free(ptr); try { return JSON.parse(json); } catch { return null; } } buildModelInRegion(x, y, w, h) { const M = this.M; const ptr = M._ec_build_page_model_region( this.session, this.page, x, y, w, h ); if (!ptr) return []; const json = M.UTF8ToString(ptr); M._ec_string_free(ptr); try { return JSON.parse(json).paragraphs || []; } catch { return []; } } deleteParagraph(id) { this._pageDirty = true; return this.M._ec_delete_paragraph(this.session, this.page, id) !== 0; } runFontData(paraId, runIndex) { const M = this.M; const needed = M._ec_get_run_font_data( this.session, this.page, paraId, runIndex, 0, 0 ); if (!needed) return null; const buf = M._malloc(needed); const written = M._ec_get_run_font_data( this.session, this.page, paraId, runIndex, buf, needed ); const out = written > 0 ? M.HEAPU8.slice(buf, buf + Math.min(needed, written || needed)) : null; M._free(buf); return out && out.length ? out : null; } paragraphObjects(id) { const M = this.M; const total = M._ec_get_paragraph_objects( this.session, this.page, id, 0, 0 ); if (total <= 0) return []; const out = M._malloc(total * 4); M._ec_get_paragraph_objects(this.session, this.page, id, out, total); const handles = []; for (let i = 0; i < total; i++) handles.push(M.HEAPU32[(out >> 2) + i]); M._free(out); return handles; } objectCount() { return this.M._FPDFPage_CountObjects(this.page); } objectAt(index) { const M = this.M; const h = M._FPDFPage_GetObject(this.page, index); if (!h) return null; return { handle: h, index, type: M._FPDFPageObj_GetType(h) }; } objectBounds(handle) { const b = this._rawBounds(handle); if (!b) return null; const p0 = this.toModel(b.x, b.y); const p1 = this.toModel(b.x + b.w, b.y + b.h); return { ...b, x: Math.min(p0.x, p1.x), y: Math.min(p0.y, p1.y), w: Math.abs(p1.x - p0.x), h: Math.abs(p1.y - p0.y), }; } _rawBounds(handle) { const M = this.M; const p = M._malloc(16); const ok = M._FPDFPageObj_GetBounds(handle, p, p + 4, p + 8, p + 12); const l = M.HEAPF32[p >> 2], b = M.HEAPF32[(p + 4) >> 2]; const r = M.HEAPF32[(p + 8) >> 2], t = M.HEAPF32[(p + 12) >> 2]; M._free(p); if (!ok) return null; return { x: l, y: b, w: r - l, h: t - b }; } deltaToUser(dx, dy) { const t = this.pageT || [1, 0, 0, 1, 0, 0]; const det = t[0] * t[3] - t[1] * t[2] || 1; return { x: (t[3] * dx - t[2] * dy) / det, y: (-t[1] * dx + t[0] * dy) / det, }; } translateObject(handle, dx, dy) { this._pageDirty = true; ({ x: dx, y: dy } = this.deltaToUser(dx, dy)); const M = this.M; const m = M._malloc(24); M.HEAPF32[(m + 0) >> 2] = 1; M.HEAPF32[(m + 4) >> 2] = 0; M.HEAPF32[(m + 8) >> 2] = 0; M.HEAPF32[(m + 12) >> 2] = 1; M.HEAPF32[(m + 16) >> 2] = dx; M.HEAPF32[(m + 20) >> 2] = dy; M._FPDFPageObj_TransformClipPath(handle, 1, 0, 0, 1, dx, dy); M._FPDFPageObj_TransformF(handle, m); M._free(m); } transformObjectAboutCenter(handle, a, b, c, d) { this._pageDirty = true; const M = this.M; const bounds = this._rawBounds(handle); if (!bounds) return; const cx = bounds.x + bounds.w / 2, cy = bounds.y + bounds.h / 2; const e = cx - a * cx - c * cy; const f = cy - b * cx - d * cy; const m = M._malloc(24); M.HEAPF32[(m + 0) >> 2] = a; M.HEAPF32[(m + 4) >> 2] = b; M.HEAPF32[(m + 8) >> 2] = c; M.HEAPF32[(m + 12) >> 2] = d; M.HEAPF32[(m + 16) >> 2] = e; M.HEAPF32[(m + 20) >> 2] = f; M._FPDFPageObj_TransformClipPath(handle, a, b, c, d, e, f); M._FPDFPageObj_TransformF(handle, m); M._free(m); } rotateObject(handle, deg) { this._pageDirty = true; const r = (deg * Math.PI) / 180; this.transformObjectAboutCenter( handle, Math.cos(r), Math.sin(r), -Math.sin(r), Math.cos(r) ); } rotateObjectsAbout(handles, deg, cx, cy) { ({ x: cx, y: cy } = this.toUser(cx, cy)); this._pageDirty = true; const M = this.M; const r = (deg * Math.PI) / 180, c = Math.cos(r), s = Math.sin(r); const m = M._malloc(24); M.HEAPF32[(m + 0) >> 2] = c; M.HEAPF32[(m + 4) >> 2] = s; M.HEAPF32[(m + 8) >> 2] = -s; M.HEAPF32[(m + 12) >> 2] = c; M.HEAPF32[(m + 16) >> 2] = cx - c * cx + s * cy; M.HEAPF32[(m + 20) >> 2] = cy - s * cx - c * cy; for (const h of handles) { M._FPDFPageObj_TransformClipPath( h, c, s, -s, c, cx - c * cx + s * cy, cy - s * cx - c * cy ); M._FPDFPageObj_TransformF(h, m); } M._free(m); } flipObject(handle, horizontal) { this._pageDirty = true; this.transformObjectAboutCenter( handle, horizontal ? -1 : 1, 0, 0, horizontal ? 1 : -1 ); } replaceImage(handle, bgra, width, height) { this._pageDirty = true; const M = this.M; const buf = M._malloc(bgra.length); M.HEAPU8.set(bgra, buf); const bmp = M._FPDFBitmap_CreateEx(width, height, 4, buf, width * 4); let ok = false; if (bmp) { const pages = M._malloc(4); M.HEAP32[pages >> 2] = this.page; ok = !!M._FPDFImageObj_SetBitmap(pages, 1, handle, bmp); M._free(pages); M._FPDFBitmap_Destroy(bmp); } M._free(buf); return ok; } renderImageObject(handle) { const M = this.M; const bmp = M._FPDFImageObj_GetRenderedBitmap(this.doc, this.page, handle); if (!bmp) return null; const w = M._FPDFBitmap_GetWidth(bmp), h = M._FPDFBitmap_GetHeight(bmp); const stride = M._FPDFBitmap_GetStride(bmp); const buf = M._FPDFBitmap_GetBuffer(bmp); const out = new Uint8ClampedArray(w * h * 4); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { const si = buf + y * stride + x * 4, di = (y * w + x) * 4; out[di] = M.HEAPU8[si + 2]; out[di + 1] = M.HEAPU8[si + 1]; out[di + 2] = M.HEAPU8[si]; out[di + 3] = M.HEAPU8[si + 3]; } } M._FPDFBitmap_Destroy(bmp); return { data: out, width: w, height: h }; } untaggedMarks() { const M = this.M; const tree = M._FPDF_StructTree_GetForPage(this.page); if (!tree) return []; const registered = new Set(); const walk = (el) => { const id = M._FPDF_StructElement_GetMarkedContentID(el); if (id >= 0) registered.add(id); const n = M._FPDF_StructElement_CountChildren(el); for (let i = 0; i < n; i++) { const ch = M._FPDF_StructElement_GetChildAtIndex(el, i); if (ch) walk(ch); } }; const n = M._FPDF_StructTree_CountChildren(tree); for (let i = 0; i < n; i++) walk(M._FPDF_StructTree_GetChildAtIndex(tree, i)); M._FPDF_StructTree_Close(tree); const out = new Map(); const key = M._malloc(8); M.stringToUTF8('MCID', key, 8); const val = M._malloc(4); for (let i = 0; i < this.objectCount(); i++) { const o = this.objectAt(i); if (!o) continue; const nm = M._FPDFPageObj_CountMarks(o.handle); for (let k = 0; k < nm; k++) { const mk = M._FPDFPageObj_GetMark(o.handle, k); if (!mk) continue; const nb = M._malloc(32), need = M._malloc(4); let name = ''; if (M._FPDFPageObjMark_GetName(mk, nb, 32, need)) { const len = M.HEAPU32[need >> 2]; for (let q = 0; q + 2 <= len; q += 2) { const c = M.HEAPU8[nb + q] | (M.HEAPU8[nb + q + 1] << 8); if (c) name += String.fromCharCode(c); } } M._free(nb); M._free(need); if (name !== 'P' && name !== 'Figure') continue; if (!M._FPDFPageObjMark_GetParamIntValue(mk, key, val)) continue; const mcid = M.HEAP32[val >> 2]; if (mcid >= 0 && !registered.has(mcid)) out.set(mcid, { mcid, type: name }); } } M._free(key); M._free(val); return [...out.values()].sort((a, b) => a.mcid - b.mcid); } objectMcid(handle) { const M = this.M; const key = M._malloc(8); M.stringToUTF8('MCID', key, 8); const val = M._malloc(4); let mcid = -1; const nm = M._FPDFPageObj_CountMarks(handle); for (let k = 0; k < nm && mcid < 0; k++) { const mk = M._FPDFPageObj_GetMark(handle, k); if (mk && M._FPDFPageObjMark_GetParamIntValue(mk, key, val)) { mcid = M.HEAP32[val >> 2]; } } M._free(key); M._free(val); return mcid; } tagObject(handle, name) { this._pageDirty = true; const M = this.M; let maxM = -1; for (let i = 0; i < this.objectCount(); i++) { const o = this.objectAt(i); if (o) maxM = Math.max(maxM, this.objectMcid(o.handle)); } const nbuf = M._malloc(name.length + 1); M.stringToUTF8(name, nbuf, name.length + 1); const mk = M._FPDFPageObj_AddMark(handle, nbuf); let ok = false; if (mk) { const key = M._malloc(8); M.stringToUTF8('MCID', key, 8); ok = !!M._FPDFPageObjMark_SetIntParam( this.doc, handle, mk, key, maxM + 1 ); M._free(key); } M._free(nbuf); return ok; } pageIsTagged() { const M = this.M; const tree = M._FPDF_StructTree_GetForPage(this.page); if (!tree) return false; const n = M._FPDF_StructTree_CountChildren(tree); M._FPDF_StructTree_Close(tree); return n > 0; } structAltFor(mcid) { const M = this.M; const tree = M._FPDF_StructTree_GetForPage(this.page); if (!tree) return null; let found = null; const walk = (el) => { if (found !== null) return; if (M._FPDF_StructElement_GetMarkedContentID(el) === mcid) { const need = M._FPDF_StructElement_GetAltText(el, 0, 0); let txt = ''; if (need > 2) { const buf = M._malloc(need); M._FPDF_StructElement_GetAltText(el, buf, need); for (let q = 0; q + 2 <= need - 2; q += 2) { const c = M.HEAPU8[buf + q] | (M.HEAPU8[buf + q + 1] << 8); if (c) txt += String.fromCharCode(c); } M._free(buf); } found = txt; return; } const n = M._FPDF_StructElement_CountChildren(el); for (let i = 0; i < n; i++) { const ch = M._FPDF_StructElement_GetChildAtIndex(el, i); if (ch) walk(ch); } }; const n = M._FPDF_StructTree_CountChildren(tree); for (let i = 0; i < n; i++) walk(M._FPDF_StructTree_GetChildAtIndex(tree, i)); M._FPDF_StructTree_Close(tree); return found; } collectShadings() { const out = []; let firstOther = -1; for (let i = 0; i < this.objectCount(); i++) { const o = this.objectAt(i); if (!o) continue; if (o.type === 4) out.push({ index: i }); else if (firstOther < 0) firstOther = i; } return out.map((s2) => ({ ...s2, isBackground: firstOther < 0 || s2.index < firstOther, })); } pathStyle(handle) { const M = this.M; const p = M._malloc(24); const st = { stroke: false, strokeRgba: 0, strokeWidth: 1, fill: false }; if (M._FPDFPath_GetDrawMode(handle, p, p + 4)) { st.fill = M.HEAP32[p >> 2] !== 0; st.stroke = M.HEAP32[(p + 4) >> 2] !== 0; } if (M._FPDFPageObj_GetStrokeColor(handle, p, p + 4, p + 8, p + 12)) { st.strokeRgba = (((M.HEAPU32[p >> 2] & 0xff) << 24) | ((M.HEAPU32[(p + 4) >> 2] & 0xff) << 16) | ((M.HEAPU32[(p + 8) >> 2] & 0xff) << 8) | (M.HEAPU32[(p + 12) >> 2] & 0xff)) >>> 0; } if (M._FPDFPageObj_GetStrokeWidth(handle, p)) { st.strokeWidth = M.HEAPF32[p >> 2]; } M._free(p); return st; } setPathStroke(handle, rgba, width) { this._pageDirty = true; const M = this.M; const p = M._malloc(8); let fill = 0; if (M._FPDFPath_GetDrawMode(handle, p, p + 4)) fill = M.HEAP32[p >> 2]; M._free(p); M._FPDFPageObj_SetStrokeColor( handle, (rgba >>> 24) & 0xff, (rgba >>> 16) & 0xff, (rgba >>> 8) & 0xff, rgba & 0xff ); M._FPDFPageObj_SetStrokeWidth(handle, width > 0 ? width : 1); M._FPDFPath_SetDrawMode(handle, fill, 1); } removeObject(handle) { this._pageDirty = true; if (this.M._FPDFPage_RemoveObject(this.page, handle)) { this.M._FPDFPageObj_Destroy(handle); return true; } return false; } scaleObject(handle, sx, sy, anchorX, anchorY) { ({ x: anchorX, y: anchorY } = this.toUser(anchorX, anchorY)); if (this.pageT && Math.abs(this.pageT[0]) < 0.5) { const t2 = sx; sx = sy; sy = t2; } this._pageDirty = true; const M = this.M; const m = M._malloc(24); M.HEAPF32[(m + 0) >> 2] = sx; M.HEAPF32[(m + 4) >> 2] = 0; M.HEAPF32[(m + 8) >> 2] = 0; M.HEAPF32[(m + 12) >> 2] = sy; M.HEAPF32[(m + 16) >> 2] = anchorX - sx * anchorX; M.HEAPF32[(m + 20) >> 2] = anchorY - sy * anchorY; M._FPDFPageObj_TransformClipPath( handle, sx, 0, 0, sy, anchorX - sx * anchorX, anchorY - sy * anchorY ); M._FPDFPageObj_TransformF(handle, m); M._free(m); } currentIndexOf(handle) { for (let i = 0; i < this.objectCount(); i++) if (this.M._FPDFPage_GetObject(this.page, i) === handle) return i; return -1; } arrangeObject(handle, op) { this._pageDirty = true; const M = this.M; const idx = this.currentIndexOf(handle); if (idx < 0) return false; const last = this.objectCount() - 1; const target = op === 'front' ? last : op === 'back' ? 0 : op === 'forward' ? Math.min(last, idx + 1) : Math.max(0, idx - 1); if (target === idx) return false; if (!M._FPDFPage_RemoveObject(this.page, handle)) return false; if (!M._FPDFPage_InsertObjectAtIndex(this.page, handle, target)) M._FPDFPage_InsertObject(this.page, handle); return true; } insertImage(rgba, pxW, pxH, x, y, w, h) { this._pageDirty = true; const M = this.M; const handle = M._FPDFPageObj_NewImageObj(this.doc); if (!handle) return 0; const FPDFBitmap_BGRA = 4; const bmp = M._FPDFBitmap_Create(pxW, pxH, 1); const buf = M._FPDFBitmap_GetBuffer(bmp); const stride = M._FPDFBitmap_GetStride(bmp); for (let row = 0; row < pxH; row++) { let src = row * pxW * 4, dst = buf + row * stride; for (let col = 0; col < pxW; col++) { M.HEAPU8[dst] = rgba[src + 2]; M.HEAPU8[dst + 1] = rgba[src + 1]; M.HEAPU8[dst + 2] = rgba[src]; M.HEAPU8[dst + 3] = rgba[src + 3]; src += 4; dst += 4; } } const pagesPtr = M._malloc(4); M.HEAPU32[pagesPtr >> 2] = this.page; M._FPDFImageObj_SetBitmap(pagesPtr, 1, handle, bmp); M._free(pagesPtr); M._FPDFBitmap_Destroy(bmp); const m = M._malloc(24); M.HEAPF32[(m + 0) >> 2] = w; M.HEAPF32[(m + 4) >> 2] = 0; M.HEAPF32[(m + 8) >> 2] = 0; M.HEAPF32[(m + 12) >> 2] = h; const at = this.toUser(x, y); M.HEAPF32[(m + 16) >> 2] = at.x; M.HEAPF32[(m + 20) >> 2] = at.y; M._FPDFPageObj_SetMatrix(handle, m); M._free(m); M._FPDFPage_InsertObject(this.page, handle); return handle; } duplicateImage(handle, dx, dy) { this._pageDirty = true; const M = this.M; const b = this.objectBounds(handle); if (!b) return 0; const sz = M._malloc(8); let iw = 0, ih = 0; if (M._FPDFImageObj_GetImagePixelSize(handle, sz, sz + 4)) { iw = M.HEAPU32[sz >> 2]; ih = M.HEAPU32[(sz + 4) >> 2]; } M._free(sz); const mp = M._malloc(24); let saved = null; if (M._FPDFPageObj_GetMatrix(handle, mp)) { saved = [0, 1, 2, 3, 4, 5].map((i) => M.HEAPF32[(mp >> 2) + i]); } const upright = saved && saved[1] === 0 && saved[2] === 0 && saved[0] > 0 && saved[3] > 0; let boosted = false; if (upright && iw > 0 && ih > 0 && (iw > saved[0] || ih > saved[3])) { const s = Math.min(1, 4096 / Math.max(iw, ih)); M.HEAPF32.set([iw * s, 0, 0, ih * s, saved[4], saved[5]], mp >> 2); M._FPDFPageObj_SetMatrix(handle, mp); boosted = true; } const bmp = M._FPDFImageObj_GetRenderedBitmap(this.doc, this.page, handle); if (boosted) { M.HEAPF32.set(saved, mp >> 2); M._FPDFPageObj_SetMatrix(handle, mp); } M._free(mp); if (!bmp) return 0; const w = M._FPDFBitmap_GetWidth(bmp), h = M._FPDFBitmap_GetHeight(bmp); const stride = M._FPDFBitmap_GetStride(bmp), buf = M._FPDFBitmap_GetBuffer(bmp); const rgba = new Uint8Array(w * h * 4); for (let row = 0; row < h; row++) { let src = buf + row * stride, dst = row * w * 4; for (let col = 0; col < w; col++) { rgba[dst] = M.HEAPU8[src + 2]; rgba[dst + 1] = M.HEAPU8[src + 1]; rgba[dst + 2] = M.HEAPU8[src]; rgba[dst + 3] = M.HEAPU8[src + 3]; src += 4; dst += 4; } } M._FPDFBitmap_Destroy(bmp); return this.insertImage(rgba, w, h, b.x + dx, b.y + dy, b.w, b.h); } objectFill(handle) { const M = this.M; const p = M._malloc(16); const ok = M._FPDFPageObj_GetFillColor(handle, p, p + 4, p + 8, p + 12); const r = M.HEAPU32[p >> 2], g = M.HEAPU32[(p + 4) >> 2]; const b = M.HEAPU32[(p + 8) >> 2], a = M.HEAPU32[(p + 12) >> 2]; M._free(p); return ok ? { r, g, b, a } : null; } setObjectFill(handle, r, g, b, a) { this._pageDirty = true; this.M._FPDFPageObj_SetFillColor(handle, r, g, b, a); } renderPage(scale, hideHandles = null) { const M = this.M; const s = Math.min( scale, 4096 / Math.max(1, this.pageWidth), 4096 / Math.max(1, this.pageHeight) ); scale = Math.max(0.05, s); const w = Math.max(1, Math.round(this.pageWidth * scale)); const h = Math.max(1, Math.round(this.pageHeight * scale)); let saved = null, mp = 0; if (hideHandles && hideHandles.length) { mp = M._malloc(24); saved = []; for (const handle of hideHandles) { if (!M._FPDFPageObj_GetMatrix(handle, mp)) continue; saved.push([handle, M.HEAPF32.slice(mp >> 2, (mp >> 2) + 6)]); M.HEAPF32[(mp + 0) >> 2] = 1e-6; M.HEAPF32[(mp + 4) >> 2] = 0; M.HEAPF32[(mp + 8) >> 2] = 0; M.HEAPF32[(mp + 12) >> 2] = 1e-6; M.HEAPF32[(mp + 16) >> 2] = -1e6; M.HEAPF32[(mp + 20) >> 2] = -1e6; M._FPDFPageObj_SetMatrix(handle, mp); } } const FPDFBitmap_BGRA = 4; const bmp = M._FPDFBitmap_CreateEx(w, h, FPDFBitmap_BGRA, 0, 0); M._FPDFBitmap_FillRect(bmp, 0, 0, w, h, 0xffffffff); const FPDF_ANNOT = 0x01, FPDF_LCD_TEXT = 0x02, FPDF_REVERSE_BYTE_ORDER = 0x10; const FLAGS = FPDF_ANNOT | FPDF_LCD_TEXT | FPDF_REVERSE_BYTE_ORDER; M._FPDF_RenderPageBitmap(bmp, this.page, 0, 0, w, h, 0, FLAGS); M._ec_form_draw(this.session, this.page, bmp, 0, 0, w, h, 0, FLAGS); if (saved) { for (const [handle, m] of saved) { M.HEAPF32.set(m, mp >> 2); M._FPDFPageObj_SetMatrix(handle, mp); } M._free(mp); } const buf = M._FPDFBitmap_GetBuffer(bmp); const stride = M._FPDFBitmap_GetStride(bmp); const out = new Uint8ClampedArray(w * h * 4); if (stride === w * 4) { out.set(M.HEAPU8.subarray(buf, buf + w * h * 4)); } else { for (let y = 0; y < h; y++) { out.set( M.HEAPU8.subarray(buf + y * stride, buf + y * stride + w * 4), y * w * 4 ); } } M._FPDFBitmap_Destroy(bmp); return { width: w, height: h, data: out }; } generateContent() { if (!this._pageDirty) return true; if (this._t3seg && this._t3seg[this.pageIndex]) { const st = this._t3readState(); if (st) { this._t3anchor(st); (this._t3state ||= {})[this.pageIndex] = st.filter( (o) => o.t3 && !o.inForm && o.glyphs && o.glyphs.length ); } } if (this.session) this.M._ec_dealias_page_fonts(this.session, this.page); this.M._ec_normalize_page_paint(this.page); const ok = this.M._FPDFPage_GenerateContent(this.page) !== 0; if (ok) this._spliceOk = false; if (ok && this._usesPattern) this._pageStale = true; if (ok) { this._pageDirty = false; (this._editedPages ||= new Set()).add(this.pageIndex); } return ok; } async saveSpliced(opts) { if ( this._spliceOk === false || !this._splicePlans?.length || !this._originalBytes || this._reencoded?.size || this._normalizedBytes ) return this.save(opts); try { const { applyPlan } = await import('./streamsplice.js'); const writeCount = this._splicePlans.filter( (e) => e.plan?.kind === 'write' ).length; if (writeCount > 1) return this.save(opts); let bytes = this._originalBytes; for (const { page, plan } of this._splicePlans) { const next = await applyPlan(bytes, page, plan); if (!next) return this.save(opts); bytes = next; } return bytes; } catch { return this.save(opts); } } renderParagraphLiveEnd() { if (this.M._ec_render_paragraph_live_end && this.session && this.page) { this.M._ec_render_paragraph_live_end(this.session, this.page); } } save(opts) { this.renderParagraphLiveEnd(); if (this._pageDirty && !opts?.noRegen) this.generateContent(); const M = this.M; const sizePtr = M._malloc(4); const hasT3 = this._t3seg && Object.keys(this._t3seg).length > 0; const incremental = (opts && opts.incremental) || hasT3; const outPtr = M._ec_save_document(this.doc, incremental ? 1 : 2, sizePtr); const size = M.HEAPU32[sizePtr >> 2]; M._free(sizePtr); if (!outPtr || !size) return null; let bytes = M.HEAPU8.slice(outPtr, outPtr + size); M._ec_string_free(outPtr); if (hasT3 && this._t3state) { try { const jobs = Object.entries(this._t3state).map(([pi, state]) => ({ pageIndex: Number(pi), seg: this._t3seg[pi], state, anchors: this._t3anchors?.[pi] || {}, })); const fixed = applyType3LeftoverSurgery(bytes, jobs); if (fixed) { bytes = fixed.bytes; for (const [pi, tjEntries] of Object.entries(fixed.injected)) { this._t3seg[pi]?.tjs?.push(...tjEntries); } } } catch (e) { console.warn('type3 surgery skipped:', e); } } return bytes; } }