import '../lib/polyfill.js' import * as std from 'std' import * as gui from 'gui' import * as win from 'win' import * as ffi from 'ffi' import type { Document, Page, Pixmap } from '../vendor/mupdf-wasm/mupdf.js' import { useState } from 'react' import { render, Button, Input, ScrollView } from '../lib/react-qw/index.js' import { PdfCanvas } from './PdfCanvas.js' type MuPdf = typeof import('../vendor/mupdf-wasm/mupdf.js') const _user32 = win.LoadLibrary('user32.dll') const _gdi32 = win.LoadLibrary('gdi32.dll') const _comdlg32 = win.LoadLibrary('comdlg32.dll') if (!_user32 || !_gdi32 || !_comdlg32) { gui.MessageBox('Failed to load system libraries') std.exit(0) } function loadProc(lib: win.HMODULE, name: string): number { const ptr = win.GetProcAddress(lib, name) if (!ptr) throw new Error('Failed to load ' + name) return ptr } const GetOpenFileNameW_ = loadProc(_comdlg32, 'GetOpenFileNameW') const FFI_PTR = ffi.FFI_TYPE_POINTER const FFI_U32 = ffi.FFI_TYPE_UINT32 function strToWide(str: string): ArrayBuffer { const buf = new ArrayBuffer((str.length + 1) * 2) const dv = new DataView(buf) for (let i = 0; i < str.length; i++) dv.setUint16(i * 2, str.charCodeAt(i), true) return buf } function wideToStr(buf: ArrayBuffer): string { const dv = new DataView(buf) const chars: number[] = [] for (let i = 0; i < buf.byteLength; i += 2) { const c = dv.getUint16(i, true) if (c === 0) break chars.push(c) } return String.fromCharCode(...chars) } function setPtr(dv: DataView, off: number, ptr: number): void { dv.setUint32(off, ptr & 0xFFFFFFFF, true) dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true) } function openPdfFileDialog(owner: number): string | null { const structBuf = new ArrayBuffer(152) const sv = new DataView(structBuf) const fileBuf = new ArrayBuffer(260 * 2) const filterWide = strToWide('PDF Files\0*.pdf\0All Files\0*.*\0\0') sv.setUint32(0, 152, true) sv.setUint32(8, owner & 0xFFFFFFFF, true) sv.setUint32(12, Math.floor(owner / 0x100000000), true) setPtr(sv, 24, ffi.bufferPtr(filterWide)) setPtr(sv, 48, ffi.bufferPtr(fileBuf)) sv.setUint32(56, 260, true) sv.setUint32(96, 0x1000 | 0x0800 | 0x0004, true) const ret = ffi.ffiCall(GetOpenFileNameW_, [FFI_PTR], [structBuf], FFI_U32) if (!ret) return null const path = wideToStr(fileBuf) return path.length > 0 ? path : null } async function loadMupdf(): Promise { const wasmPath = './vendor/mupdf-wasm/mupdf-wasm.wasm' const fp = std.open(wasmPath, 'rb') if (!fp) { std.printf('Error: cannot open %s\n', wasmPath); return null } fp.seek(0, 2) const size = fp.tell() fp.seek(0, 0) const buf = new ArrayBuffer(size) fp.read(buf, 0, size) fp.close() ;(globalThis).$libmupdf_wasm_Module = { wasmBinary: buf, locateFile: (p: string) => p } try { return await import('../vendor/mupdf-wasm/mupdf.js') } catch (e) { std.printf('Error: mupdf load failed: %s\n', String(e)) return null } } interface PixmapInfo { data: ArrayBuffer w: number h: number } let cachedPath = '' let cachedDoc: Document | null = null let cachedTotalPages = 0 function renderPdfPage(mupdf: MuPdf, filePath: string, pageIndex: number): PixmapInfo & { totalPages: number } | null { if (filePath !== cachedPath) { if (cachedDoc) { try { cachedDoc.destroy() } catch {} } cachedDoc = null cachedPath = '' cachedTotalPages = 0 const fp = std.open(filePath, 'rb') if (!fp) { std.printf('Error: cannot open %s\n', filePath); return null } let buf: ArrayBuffer | null = null try { fp.seek(0, 2) const size = fp.tell() fp.seek(0, 0) buf = new ArrayBuffer(size) fp.read(buf, 0, size) } finally { fp.close() } if (!buf) return null try { cachedDoc = mupdf.Document.openDocument(new Uint8Array(buf), 'application/pdf') cachedTotalPages = cachedDoc.countPages() cachedPath = filePath } catch (e) { std.printf('Error opening document: %s\n', String(e)) return null } } if (!cachedDoc) return null if (pageIndex >= cachedTotalPages) return null let page: Page | null = null let pixmap: Pixmap | null = null try { page = cachedDoc.loadPage(pageIndex) const scale = 1.5 pixmap = page.toPixmap( mupdf.Matrix.scale(scale, scale), mupdf.ColorSpace.DeviceRGB, false ) if (!pixmap) return null const srcPixels = pixmap.getPixels() const srcStride = pixmap.getStride() const w = pixmap.getWidth() const h = pixmap.getHeight() const dibStride = Math.floor((w * 3 + 3) / 4) * 4 const dibSize = h * dibStride const dibBuffer = new ArrayBuffer(dibSize) const dib = new Uint8Array(dibBuffer) for (let y = 0; y < h; y++) { const srcOff = y * srcStride const dstOff = y * dibStride for (let x = 0; x < w; x++) { const sx = srcOff + x * 3 const dx = dstOff + x * 3 dib[dx] = srcPixels[sx + 2]! dib[dx + 1] = srcPixels[sx + 1]! dib[dx + 2] = srcPixels[sx]! } } return { data: dibBuffer, w, h, totalPages: cachedTotalPages } } catch (e) { std.printf('Error rendering: %s\n', String(e)) if (cachedDoc) { try { cachedDoc.destroy() } catch {} } cachedDoc = null; cachedPath = ''; cachedTotalPages = 0 return null } finally { if (pixmap) { try { pixmap.destroy() } catch {} } if (page) { try { page.destroy() } catch {} } } } function App({ mupdf, mainHwnd: hwnd }: { mupdf: MuPdf; mainHwnd: number }) { const [filePath, setFilePath] = useState('') const [currentPage, setCurrentPage] = useState(0) const [pixmap, setPixmap] = useState(undefined) const [totalPages, setTotalPages] = useState(0) function loadPage(path: string, page: number) { const result = renderPdfPage(mupdf, path, page) if (result) { setPixmap(result) setCurrentPage(page) setTotalPages(result.totalPages) } else { gui.MessageBox('Failed to render PDF page') } } function handleOpen() { const path = openPdfFileDialog(hwnd) if (path) { setFilePath(path) loadPage(path, 0) } } function handlePrev() { if (currentPage > 0) loadPage(filePath, currentPage - 1) } function handleNext() { if (currentPage < totalPages - 1) loadPage(filePath, currentPage + 1) } return ( 0 ? `Page ${currentPage + 1} / ${totalPages}` : 'No file'} style={{ width: 120 }} /> {pixmap ? ( ) : ( )} ) } async function main() { const mupdf = await loadMupdf() if (!mupdf) { gui.MessageBox('Loading mupdf WASM failed.\nEnsure vendor/mupdf-wasm/ is present.') return } gui.RegisterClass('PdfViewerWin') const hwnd = gui.CreateWindow( 'PdfViewerWin', 'PDF Viewer', gui.WindowStyle.OVERLAPPEDWINDOW, 100, 100, 1200, 800, null, null ) if (hwnd) { render(, hwnd) setTimeout(() => gui.ShowWindow(hwnd), 0) } } main()