quickwin 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +204 -204
- package/README.md +203 -203
- package/examples/PdfCanvas.tsx +83 -83
- package/examples/exec_server.ts +35 -35
- package/examples/pdf_preview2.ts +375 -375
- package/examples/pdf_viewer.tsx +279 -279
- package/examples/test_gallery.tsx +285 -285
- package/examples/test_react_lazy.tsx +63 -63
- package/examples/test_react_listbox.tsx +71 -71
- package/examples/test_root_window.tsx +41 -41
- package/examples/test_tab.tsx +75 -75
- package/examples/tray_demo.tsx +81 -81
- package/lib/assert.ts +6 -6
- package/lib/cache-utils.ts +40 -40
- package/lib/dumpRects.ts +25 -25
- package/lib/fetch.ts +502 -502
- package/lib/http-common.ts +399 -399
- package/lib/http-server.ts +424 -424
- package/lib/polyfill.ts +171 -171
- package/lib/react-qw/components/index.ts +17 -17
- package/lib/react-qw/index.ts +90 -90
- package/lib/react-qw/jsx.d.ts +34 -34
- package/lib/react-qw/layout.ts +111 -111
- package/lib/react-qw/props.ts +36 -36
- package/lib/react-qw/reconciler.ts +373 -373
- package/lib/stream.ts +21 -21
- package/lib/text-codec.ts +171 -171
- package/lib/text-measure.ts +54 -54
- package/lib/url.ts +201 -201
- package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
- package/lib/vendor/web-streams/ponyfill.mjs +8 -8
- package/lib/websocket.js +8 -0
- package/lib/websocket.ts +563 -555
- package/package.json +50 -44
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/run.ts +96 -96
- package/test/test_basic.ts +9 -9
- package/test/test_basic_fetch.ts +33 -33
- package/test/test_brotli.ts +52 -52
- package/test/test_fetch_cache.ts +187 -187
- package/test/test_ffi.ts +174 -174
- package/test/test_frame_encoding.ts +132 -132
- package/test/test_helper.ts +80 -80
- package/test/test_http_import.ts +74 -74
- package/test/test_http_server.ts +136 -136
- package/test/test_import_dir.ts +5 -5
- package/test/test_ipv6.ts +129 -129
- package/test/test_meta_url.ts +4 -4
- package/test/test_mupdf_render.ts +74 -74
- package/test/test_mupdf_twice.ts +81 -81
- package/test/test_mupdf_wasm.ts +30 -30
- package/test/test_net_event.ts +55 -55
- package/test/test_net_fetch.ts +238 -238
- package/test/test_net_websocket.ts +164 -164
- package/test/test_polyfill.ts +159 -159
- package/test/test_url.ts +183 -183
- package/test/test_wasm_basic.ts +73 -73
- package/test/test_wasm_import_global.ts +45 -45
- package/test/test_wasm_sjlj.ts +125 -125
- package/test/test_wasm_types.ts +124 -124
- package/test/test_worker.ts +24 -24
- package/test/test_worker_fetch.ts +27 -27
- package/test/test_worker_net.ts +54 -54
- package/test/test_worker_wasm_concurrent.ts +45 -45
- package/test/worker_fetch.ts +13 -13
- package/test/worker_helper.ts +1 -1
- package/test/worker_http.ts +20 -20
- package/test/worker_import_lib.ts +13 -13
- package/test/worker_module.ts +17 -17
- package/test/worker_wasm_concurrent.ts +22 -22
- package/tsconfig.json +27 -27
- package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
- package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
- package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
- package/vendor/mupdf-wasm/mupdf.js +3317 -3317
package/examples/pdf_viewer.tsx
CHANGED
|
@@ -1,279 +1,279 @@
|
|
|
1
|
-
import '../lib/polyfill.js'
|
|
2
|
-
import * as std from 'std'
|
|
3
|
-
import * as gui from 'gui'
|
|
4
|
-
import * as win from 'win'
|
|
5
|
-
import * as ffi from 'ffi'
|
|
6
|
-
import type { Document, Page, Pixmap } from '../vendor/mupdf-wasm/mupdf.js'
|
|
7
|
-
import { useState } from 'react'
|
|
8
|
-
import { render, Button, Input, ScrollView } from '../lib/react-qw/index.js'
|
|
9
|
-
import { PdfCanvas } from './PdfCanvas.js'
|
|
10
|
-
|
|
11
|
-
type MuPdf = typeof import('../vendor/mupdf-wasm/mupdf.js')
|
|
12
|
-
|
|
13
|
-
const _user32 = win.LoadLibrary('user32.dll')
|
|
14
|
-
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
15
|
-
const _comdlg32 = win.LoadLibrary('comdlg32.dll')
|
|
16
|
-
if (!_user32 || !_gdi32 || !_comdlg32) {
|
|
17
|
-
gui.MessageBox('Failed to load system libraries')
|
|
18
|
-
std.exit(0)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function loadProc(lib: win.HMODULE, name: string): number {
|
|
22
|
-
const ptr = win.GetProcAddress(lib, name)
|
|
23
|
-
if (!ptr) throw new Error('Failed to load ' + name)
|
|
24
|
-
return ptr
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const GetOpenFileNameW_ = loadProc(_comdlg32, 'GetOpenFileNameW')
|
|
28
|
-
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
29
|
-
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
30
|
-
|
|
31
|
-
function strToWide(str: string): ArrayBuffer {
|
|
32
|
-
const buf = new ArrayBuffer((str.length + 1) * 2)
|
|
33
|
-
const dv = new DataView(buf)
|
|
34
|
-
for (let i = 0; i < str.length; i++) dv.setUint16(i * 2, str.charCodeAt(i), true)
|
|
35
|
-
return buf
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function wideToStr(buf: ArrayBuffer): string {
|
|
39
|
-
const dv = new DataView(buf)
|
|
40
|
-
const chars: number[] = []
|
|
41
|
-
for (let i = 0; i < buf.byteLength; i += 2) {
|
|
42
|
-
const c = dv.getUint16(i, true)
|
|
43
|
-
if (c === 0) break
|
|
44
|
-
chars.push(c)
|
|
45
|
-
}
|
|
46
|
-
return String.fromCharCode(...chars)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function setPtr(dv: DataView, off: number, ptr: number): void {
|
|
50
|
-
dv.setUint32(off, ptr & 0xFFFFFFFF, true)
|
|
51
|
-
dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function openPdfFileDialog(owner: number): string | null {
|
|
55
|
-
const structBuf = new ArrayBuffer(152)
|
|
56
|
-
const sv = new DataView(structBuf)
|
|
57
|
-
const fileBuf = new ArrayBuffer(260 * 2)
|
|
58
|
-
const filterWide = strToWide('PDF Files\0*.pdf\0All Files\0*.*\0\0')
|
|
59
|
-
|
|
60
|
-
sv.setUint32(0, 152, true)
|
|
61
|
-
sv.setUint32(8, owner & 0xFFFFFFFF, true)
|
|
62
|
-
sv.setUint32(12, Math.floor(owner / 0x100000000), true)
|
|
63
|
-
|
|
64
|
-
setPtr(sv, 24, ffi.bufferPtr(filterWide))
|
|
65
|
-
setPtr(sv, 48, ffi.bufferPtr(fileBuf))
|
|
66
|
-
sv.setUint32(56, 260, true)
|
|
67
|
-
|
|
68
|
-
sv.setUint32(96, 0x1000 | 0x0800 | 0x0004, true)
|
|
69
|
-
|
|
70
|
-
const ret = ffi.ffiCall(GetOpenFileNameW_, [FFI_PTR], [structBuf], FFI_U32)
|
|
71
|
-
if (!ret) return null
|
|
72
|
-
|
|
73
|
-
const path = wideToStr(fileBuf)
|
|
74
|
-
return path.length > 0 ? path : null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function loadMupdf(): Promise<MuPdf | null> {
|
|
78
|
-
const wasmPath = './vendor/mupdf-wasm/mupdf-wasm.wasm'
|
|
79
|
-
const fp = std.open(wasmPath, 'rb')
|
|
80
|
-
if (!fp) { std.printf('Error: cannot open %s\n', wasmPath); return null }
|
|
81
|
-
fp.seek(0, 2)
|
|
82
|
-
const size = fp.tell()
|
|
83
|
-
fp.seek(0, 0)
|
|
84
|
-
const buf = new ArrayBuffer(size)
|
|
85
|
-
fp.read(buf, 0, size)
|
|
86
|
-
fp.close()
|
|
87
|
-
|
|
88
|
-
;(globalThis).$libmupdf_wasm_Module = {
|
|
89
|
-
wasmBinary: buf,
|
|
90
|
-
locateFile: (p: string) => p
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
return await import('../vendor/mupdf-wasm/mupdf.js')
|
|
94
|
-
} catch (e) {
|
|
95
|
-
std.printf('Error: mupdf load failed: %s\n', String(e))
|
|
96
|
-
return null
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface PixmapInfo {
|
|
101
|
-
data: ArrayBuffer
|
|
102
|
-
w: number
|
|
103
|
-
h: number
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let cachedPath = ''
|
|
107
|
-
let cachedDoc: Document | null = null
|
|
108
|
-
let cachedTotalPages = 0
|
|
109
|
-
|
|
110
|
-
function renderPdfPage(mupdf: MuPdf, filePath: string, pageIndex: number): PixmapInfo & { totalPages: number } | null {
|
|
111
|
-
if (filePath !== cachedPath) {
|
|
112
|
-
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
113
|
-
cachedDoc = null
|
|
114
|
-
cachedPath = ''
|
|
115
|
-
cachedTotalPages = 0
|
|
116
|
-
|
|
117
|
-
const fp = std.open(filePath, 'rb')
|
|
118
|
-
if (!fp) { std.printf('Error: cannot open %s\n', filePath); return null }
|
|
119
|
-
let buf: ArrayBuffer | null = null
|
|
120
|
-
try {
|
|
121
|
-
fp.seek(0, 2)
|
|
122
|
-
const size = fp.tell()
|
|
123
|
-
fp.seek(0, 0)
|
|
124
|
-
buf = new ArrayBuffer(size)
|
|
125
|
-
fp.read(buf, 0, size)
|
|
126
|
-
} finally {
|
|
127
|
-
fp.close()
|
|
128
|
-
}
|
|
129
|
-
if (!buf) return null
|
|
130
|
-
try {
|
|
131
|
-
cachedDoc = mupdf.Document.openDocument(new Uint8Array(buf), 'application/pdf')
|
|
132
|
-
cachedTotalPages = cachedDoc.countPages()
|
|
133
|
-
cachedPath = filePath
|
|
134
|
-
} catch (e) {
|
|
135
|
-
std.printf('Error opening document: %s\n', String(e))
|
|
136
|
-
return null
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!cachedDoc) return null
|
|
141
|
-
if (pageIndex >= cachedTotalPages) return null
|
|
142
|
-
|
|
143
|
-
let page: Page | null = null
|
|
144
|
-
let pixmap: Pixmap | null = null
|
|
145
|
-
try {
|
|
146
|
-
page = cachedDoc.loadPage(pageIndex)
|
|
147
|
-
const scale = 1.5
|
|
148
|
-
pixmap = page.toPixmap(
|
|
149
|
-
mupdf.Matrix.scale(scale, scale),
|
|
150
|
-
mupdf.ColorSpace.DeviceRGB,
|
|
151
|
-
false
|
|
152
|
-
)
|
|
153
|
-
if (!pixmap) return null
|
|
154
|
-
|
|
155
|
-
const srcPixels = pixmap.getPixels()
|
|
156
|
-
const srcStride = pixmap.getStride()
|
|
157
|
-
const w = pixmap.getWidth()
|
|
158
|
-
const h = pixmap.getHeight()
|
|
159
|
-
const dibStride = Math.floor((w * 3 + 3) / 4) * 4
|
|
160
|
-
|
|
161
|
-
const dibSize = h * dibStride
|
|
162
|
-
const dibBuffer = new ArrayBuffer(dibSize)
|
|
163
|
-
const dib = new Uint8Array(dibBuffer)
|
|
164
|
-
|
|
165
|
-
for (let y = 0; y < h; y++) {
|
|
166
|
-
const srcOff = y * srcStride
|
|
167
|
-
const dstOff = y * dibStride
|
|
168
|
-
for (let x = 0; x < w; x++) {
|
|
169
|
-
const sx = srcOff + x * 3
|
|
170
|
-
const dx = dstOff + x * 3
|
|
171
|
-
dib[dx] = srcPixels[sx + 2]!
|
|
172
|
-
dib[dx + 1] = srcPixels[sx + 1]!
|
|
173
|
-
dib[dx + 2] = srcPixels[sx]!
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { data: dibBuffer, w, h, totalPages: cachedTotalPages }
|
|
178
|
-
} catch (e) {
|
|
179
|
-
std.printf('Error rendering: %s\n', String(e))
|
|
180
|
-
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
181
|
-
cachedDoc = null; cachedPath = ''; cachedTotalPages = 0
|
|
182
|
-
return null
|
|
183
|
-
} finally {
|
|
184
|
-
if (pixmap) { try { pixmap.destroy() } catch {} }
|
|
185
|
-
if (page) { try { page.destroy() } catch {} }
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function App({ mupdf, mainHwnd: hwnd }: { mupdf: MuPdf; mainHwnd: number }) {
|
|
190
|
-
const [filePath, setFilePath] = useState('')
|
|
191
|
-
const [currentPage, setCurrentPage] = useState(0)
|
|
192
|
-
const [pixmap, setPixmap] = useState<PixmapInfo | undefined>(undefined)
|
|
193
|
-
const [totalPages, setTotalPages] = useState(0)
|
|
194
|
-
|
|
195
|
-
function loadPage(path: string, page: number) {
|
|
196
|
-
const result = renderPdfPage(mupdf, path, page)
|
|
197
|
-
if (result) {
|
|
198
|
-
setPixmap(result)
|
|
199
|
-
setCurrentPage(page)
|
|
200
|
-
setTotalPages(result.totalPages)
|
|
201
|
-
} else {
|
|
202
|
-
gui.MessageBox('Failed to render PDF page')
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function handleOpen() {
|
|
207
|
-
const path = openPdfFileDialog(hwnd)
|
|
208
|
-
if (path) {
|
|
209
|
-
setFilePath(path)
|
|
210
|
-
loadPage(path, 0)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function handlePrev() {
|
|
215
|
-
if (currentPage > 0) loadPage(filePath, currentPage - 1)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function handleNext() {
|
|
219
|
-
if (currentPage < totalPages - 1) loadPage(filePath, currentPage + 1)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<w type="STATIC"
|
|
224
|
-
ws={gui.WindowStyle.VISIBLE | gui.WindowStyle.CLIPCHILDREN}
|
|
225
|
-
style={{ flexDirection: 'column', gap: 4, width: 1180, height: 720 }}
|
|
226
|
-
>
|
|
227
|
-
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
228
|
-
style={{ flexDirection: 'row', gap: 6, alignItems: 'stretch', height: 28 }}
|
|
229
|
-
>
|
|
230
|
-
<w type="STATIC" style={{ width: 6 }} />
|
|
231
|
-
<Button onClick={handleOpen} style={{ width: 80 }}>Open</Button>
|
|
232
|
-
<Input value={filePath} placeholder="File path..." readonly style={{ flexGrow: 1 }} />
|
|
233
|
-
<Button onClick={handlePrev} disabled={currentPage <= 0} style={{ width: 60 }}>Prev</Button>
|
|
234
|
-
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
235
|
-
text={totalPages > 0 ? `Page ${currentPage + 1} / ${totalPages}` : 'No file'}
|
|
236
|
-
style={{ width: 120 }}
|
|
237
|
-
/>
|
|
238
|
-
<Button onClick={handleNext} disabled={currentPage >= totalPages - 1} style={{ width: 60 }}>Next</Button>
|
|
239
|
-
<w type="STATIC" style={{ width: 6 }} />
|
|
240
|
-
</w>
|
|
241
|
-
|
|
242
|
-
<ScrollView style={{ flexGrow: 1 }}>
|
|
243
|
-
{pixmap ? (
|
|
244
|
-
<PdfCanvas
|
|
245
|
-
pixmap={pixmap}
|
|
246
|
-
style={{ width: pixmap.w, height: pixmap.h }}
|
|
247
|
-
/>
|
|
248
|
-
) : (
|
|
249
|
-
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
250
|
-
text="Open a PDF file to preview"
|
|
251
|
-
style={{ width: 300, height: 30 }}
|
|
252
|
-
/>
|
|
253
|
-
)}
|
|
254
|
-
</ScrollView>
|
|
255
|
-
</w>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function main() {
|
|
260
|
-
const mupdf = await loadMupdf()
|
|
261
|
-
if (!mupdf) {
|
|
262
|
-
gui.MessageBox('Loading mupdf WASM failed.\nEnsure vendor/mupdf-wasm/ is present.')
|
|
263
|
-
return
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
gui.RegisterClass('PdfViewerWin')
|
|
267
|
-
const hwnd = gui.CreateWindow(
|
|
268
|
-
'PdfViewerWin', 'PDF Viewer',
|
|
269
|
-
gui.WindowStyle.OVERLAPPEDWINDOW,
|
|
270
|
-
100, 100, 1200, 800, null, null
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
if (hwnd) {
|
|
274
|
-
render(<App mupdf={mupdf} mainHwnd={hwnd} />, hwnd)
|
|
275
|
-
setTimeout(() => gui.ShowWindow(hwnd), 0)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
main()
|
|
1
|
+
import '../lib/polyfill.js'
|
|
2
|
+
import * as std from 'std'
|
|
3
|
+
import * as gui from 'gui'
|
|
4
|
+
import * as win from 'win'
|
|
5
|
+
import * as ffi from 'ffi'
|
|
6
|
+
import type { Document, Page, Pixmap } from '../vendor/mupdf-wasm/mupdf.js'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { render, Button, Input, ScrollView } from '../lib/react-qw/index.js'
|
|
9
|
+
import { PdfCanvas } from './PdfCanvas.js'
|
|
10
|
+
|
|
11
|
+
type MuPdf = typeof import('../vendor/mupdf-wasm/mupdf.js')
|
|
12
|
+
|
|
13
|
+
const _user32 = win.LoadLibrary('user32.dll')
|
|
14
|
+
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
15
|
+
const _comdlg32 = win.LoadLibrary('comdlg32.dll')
|
|
16
|
+
if (!_user32 || !_gdi32 || !_comdlg32) {
|
|
17
|
+
gui.MessageBox('Failed to load system libraries')
|
|
18
|
+
std.exit(0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadProc(lib: win.HMODULE, name: string): number {
|
|
22
|
+
const ptr = win.GetProcAddress(lib, name)
|
|
23
|
+
if (!ptr) throw new Error('Failed to load ' + name)
|
|
24
|
+
return ptr
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GetOpenFileNameW_ = loadProc(_comdlg32, 'GetOpenFileNameW')
|
|
28
|
+
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
29
|
+
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
30
|
+
|
|
31
|
+
function strToWide(str: string): ArrayBuffer {
|
|
32
|
+
const buf = new ArrayBuffer((str.length + 1) * 2)
|
|
33
|
+
const dv = new DataView(buf)
|
|
34
|
+
for (let i = 0; i < str.length; i++) dv.setUint16(i * 2, str.charCodeAt(i), true)
|
|
35
|
+
return buf
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function wideToStr(buf: ArrayBuffer): string {
|
|
39
|
+
const dv = new DataView(buf)
|
|
40
|
+
const chars: number[] = []
|
|
41
|
+
for (let i = 0; i < buf.byteLength; i += 2) {
|
|
42
|
+
const c = dv.getUint16(i, true)
|
|
43
|
+
if (c === 0) break
|
|
44
|
+
chars.push(c)
|
|
45
|
+
}
|
|
46
|
+
return String.fromCharCode(...chars)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setPtr(dv: DataView, off: number, ptr: number): void {
|
|
50
|
+
dv.setUint32(off, ptr & 0xFFFFFFFF, true)
|
|
51
|
+
dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function openPdfFileDialog(owner: number): string | null {
|
|
55
|
+
const structBuf = new ArrayBuffer(152)
|
|
56
|
+
const sv = new DataView(structBuf)
|
|
57
|
+
const fileBuf = new ArrayBuffer(260 * 2)
|
|
58
|
+
const filterWide = strToWide('PDF Files\0*.pdf\0All Files\0*.*\0\0')
|
|
59
|
+
|
|
60
|
+
sv.setUint32(0, 152, true)
|
|
61
|
+
sv.setUint32(8, owner & 0xFFFFFFFF, true)
|
|
62
|
+
sv.setUint32(12, Math.floor(owner / 0x100000000), true)
|
|
63
|
+
|
|
64
|
+
setPtr(sv, 24, ffi.bufferPtr(filterWide))
|
|
65
|
+
setPtr(sv, 48, ffi.bufferPtr(fileBuf))
|
|
66
|
+
sv.setUint32(56, 260, true)
|
|
67
|
+
|
|
68
|
+
sv.setUint32(96, 0x1000 | 0x0800 | 0x0004, true)
|
|
69
|
+
|
|
70
|
+
const ret = ffi.ffiCall(GetOpenFileNameW_, [FFI_PTR], [structBuf], FFI_U32)
|
|
71
|
+
if (!ret) return null
|
|
72
|
+
|
|
73
|
+
const path = wideToStr(fileBuf)
|
|
74
|
+
return path.length > 0 ? path : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadMupdf(): Promise<MuPdf | null> {
|
|
78
|
+
const wasmPath = './vendor/mupdf-wasm/mupdf-wasm.wasm'
|
|
79
|
+
const fp = std.open(wasmPath, 'rb')
|
|
80
|
+
if (!fp) { std.printf('Error: cannot open %s\n', wasmPath); return null }
|
|
81
|
+
fp.seek(0, 2)
|
|
82
|
+
const size = fp.tell()
|
|
83
|
+
fp.seek(0, 0)
|
|
84
|
+
const buf = new ArrayBuffer(size)
|
|
85
|
+
fp.read(buf, 0, size)
|
|
86
|
+
fp.close()
|
|
87
|
+
|
|
88
|
+
;(globalThis).$libmupdf_wasm_Module = {
|
|
89
|
+
wasmBinary: buf,
|
|
90
|
+
locateFile: (p: string) => p
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
return await import('../vendor/mupdf-wasm/mupdf.js')
|
|
94
|
+
} catch (e) {
|
|
95
|
+
std.printf('Error: mupdf load failed: %s\n', String(e))
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface PixmapInfo {
|
|
101
|
+
data: ArrayBuffer
|
|
102
|
+
w: number
|
|
103
|
+
h: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let cachedPath = ''
|
|
107
|
+
let cachedDoc: Document | null = null
|
|
108
|
+
let cachedTotalPages = 0
|
|
109
|
+
|
|
110
|
+
function renderPdfPage(mupdf: MuPdf, filePath: string, pageIndex: number): PixmapInfo & { totalPages: number } | null {
|
|
111
|
+
if (filePath !== cachedPath) {
|
|
112
|
+
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
113
|
+
cachedDoc = null
|
|
114
|
+
cachedPath = ''
|
|
115
|
+
cachedTotalPages = 0
|
|
116
|
+
|
|
117
|
+
const fp = std.open(filePath, 'rb')
|
|
118
|
+
if (!fp) { std.printf('Error: cannot open %s\n', filePath); return null }
|
|
119
|
+
let buf: ArrayBuffer | null = null
|
|
120
|
+
try {
|
|
121
|
+
fp.seek(0, 2)
|
|
122
|
+
const size = fp.tell()
|
|
123
|
+
fp.seek(0, 0)
|
|
124
|
+
buf = new ArrayBuffer(size)
|
|
125
|
+
fp.read(buf, 0, size)
|
|
126
|
+
} finally {
|
|
127
|
+
fp.close()
|
|
128
|
+
}
|
|
129
|
+
if (!buf) return null
|
|
130
|
+
try {
|
|
131
|
+
cachedDoc = mupdf.Document.openDocument(new Uint8Array(buf), 'application/pdf')
|
|
132
|
+
cachedTotalPages = cachedDoc.countPages()
|
|
133
|
+
cachedPath = filePath
|
|
134
|
+
} catch (e) {
|
|
135
|
+
std.printf('Error opening document: %s\n', String(e))
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!cachedDoc) return null
|
|
141
|
+
if (pageIndex >= cachedTotalPages) return null
|
|
142
|
+
|
|
143
|
+
let page: Page | null = null
|
|
144
|
+
let pixmap: Pixmap | null = null
|
|
145
|
+
try {
|
|
146
|
+
page = cachedDoc.loadPage(pageIndex)
|
|
147
|
+
const scale = 1.5
|
|
148
|
+
pixmap = page.toPixmap(
|
|
149
|
+
mupdf.Matrix.scale(scale, scale),
|
|
150
|
+
mupdf.ColorSpace.DeviceRGB,
|
|
151
|
+
false
|
|
152
|
+
)
|
|
153
|
+
if (!pixmap) return null
|
|
154
|
+
|
|
155
|
+
const srcPixels = pixmap.getPixels()
|
|
156
|
+
const srcStride = pixmap.getStride()
|
|
157
|
+
const w = pixmap.getWidth()
|
|
158
|
+
const h = pixmap.getHeight()
|
|
159
|
+
const dibStride = Math.floor((w * 3 + 3) / 4) * 4
|
|
160
|
+
|
|
161
|
+
const dibSize = h * dibStride
|
|
162
|
+
const dibBuffer = new ArrayBuffer(dibSize)
|
|
163
|
+
const dib = new Uint8Array(dibBuffer)
|
|
164
|
+
|
|
165
|
+
for (let y = 0; y < h; y++) {
|
|
166
|
+
const srcOff = y * srcStride
|
|
167
|
+
const dstOff = y * dibStride
|
|
168
|
+
for (let x = 0; x < w; x++) {
|
|
169
|
+
const sx = srcOff + x * 3
|
|
170
|
+
const dx = dstOff + x * 3
|
|
171
|
+
dib[dx] = srcPixels[sx + 2]!
|
|
172
|
+
dib[dx + 1] = srcPixels[sx + 1]!
|
|
173
|
+
dib[dx + 2] = srcPixels[sx]!
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { data: dibBuffer, w, h, totalPages: cachedTotalPages }
|
|
178
|
+
} catch (e) {
|
|
179
|
+
std.printf('Error rendering: %s\n', String(e))
|
|
180
|
+
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
181
|
+
cachedDoc = null; cachedPath = ''; cachedTotalPages = 0
|
|
182
|
+
return null
|
|
183
|
+
} finally {
|
|
184
|
+
if (pixmap) { try { pixmap.destroy() } catch {} }
|
|
185
|
+
if (page) { try { page.destroy() } catch {} }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function App({ mupdf, mainHwnd: hwnd }: { mupdf: MuPdf; mainHwnd: number }) {
|
|
190
|
+
const [filePath, setFilePath] = useState('')
|
|
191
|
+
const [currentPage, setCurrentPage] = useState(0)
|
|
192
|
+
const [pixmap, setPixmap] = useState<PixmapInfo | undefined>(undefined)
|
|
193
|
+
const [totalPages, setTotalPages] = useState(0)
|
|
194
|
+
|
|
195
|
+
function loadPage(path: string, page: number) {
|
|
196
|
+
const result = renderPdfPage(mupdf, path, page)
|
|
197
|
+
if (result) {
|
|
198
|
+
setPixmap(result)
|
|
199
|
+
setCurrentPage(page)
|
|
200
|
+
setTotalPages(result.totalPages)
|
|
201
|
+
} else {
|
|
202
|
+
gui.MessageBox('Failed to render PDF page')
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleOpen() {
|
|
207
|
+
const path = openPdfFileDialog(hwnd)
|
|
208
|
+
if (path) {
|
|
209
|
+
setFilePath(path)
|
|
210
|
+
loadPage(path, 0)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handlePrev() {
|
|
215
|
+
if (currentPage > 0) loadPage(filePath, currentPage - 1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function handleNext() {
|
|
219
|
+
if (currentPage < totalPages - 1) loadPage(filePath, currentPage + 1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<w type="STATIC"
|
|
224
|
+
ws={gui.WindowStyle.VISIBLE | gui.WindowStyle.CLIPCHILDREN}
|
|
225
|
+
style={{ flexDirection: 'column', gap: 4, width: 1180, height: 720 }}
|
|
226
|
+
>
|
|
227
|
+
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
228
|
+
style={{ flexDirection: 'row', gap: 6, alignItems: 'stretch', height: 28 }}
|
|
229
|
+
>
|
|
230
|
+
<w type="STATIC" style={{ width: 6 }} />
|
|
231
|
+
<Button onClick={handleOpen} style={{ width: 80 }}>Open</Button>
|
|
232
|
+
<Input value={filePath} placeholder="File path..." readonly style={{ flexGrow: 1 }} />
|
|
233
|
+
<Button onClick={handlePrev} disabled={currentPage <= 0} style={{ width: 60 }}>Prev</Button>
|
|
234
|
+
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
235
|
+
text={totalPages > 0 ? `Page ${currentPage + 1} / ${totalPages}` : 'No file'}
|
|
236
|
+
style={{ width: 120 }}
|
|
237
|
+
/>
|
|
238
|
+
<Button onClick={handleNext} disabled={currentPage >= totalPages - 1} style={{ width: 60 }}>Next</Button>
|
|
239
|
+
<w type="STATIC" style={{ width: 6 }} />
|
|
240
|
+
</w>
|
|
241
|
+
|
|
242
|
+
<ScrollView style={{ flexGrow: 1 }}>
|
|
243
|
+
{pixmap ? (
|
|
244
|
+
<PdfCanvas
|
|
245
|
+
pixmap={pixmap}
|
|
246
|
+
style={{ width: pixmap.w, height: pixmap.h }}
|
|
247
|
+
/>
|
|
248
|
+
) : (
|
|
249
|
+
<w type="STATIC" ws={gui.WindowStyle.VISIBLE}
|
|
250
|
+
text="Open a PDF file to preview"
|
|
251
|
+
style={{ width: 300, height: 30 }}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
</ScrollView>
|
|
255
|
+
</w>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function main() {
|
|
260
|
+
const mupdf = await loadMupdf()
|
|
261
|
+
if (!mupdf) {
|
|
262
|
+
gui.MessageBox('Loading mupdf WASM failed.\nEnsure vendor/mupdf-wasm/ is present.')
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
gui.RegisterClass('PdfViewerWin')
|
|
267
|
+
const hwnd = gui.CreateWindow(
|
|
268
|
+
'PdfViewerWin', 'PDF Viewer',
|
|
269
|
+
gui.WindowStyle.OVERLAPPEDWINDOW,
|
|
270
|
+
100, 100, 1200, 800, null, null
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if (hwnd) {
|
|
274
|
+
render(<App mupdf={mupdf} mainHwnd={hwnd} />, hwnd)
|
|
275
|
+
setTimeout(() => gui.ShowWindow(hwnd), 0)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
main()
|