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_preview2.ts
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
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 { assertNonNullable } from '../lib/assert.js'
|
|
8
|
-
|
|
9
|
-
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
10
|
-
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
11
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
12
|
-
|
|
13
|
-
const _user32 = win.LoadLibrary('user32.dll')
|
|
14
|
-
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
15
|
-
const _comdlg32 = win.LoadLibrary('comdlg32.dll')
|
|
16
|
-
|
|
17
|
-
type MuPdf = typeof import('../vendor/mupdf-wasm/mupdf.js')
|
|
18
|
-
if (!(_user32 && _gdi32 && _comdlg32)) std.exit(0)
|
|
19
|
-
|
|
20
|
-
function loadProc(lib: win.HMODULE, name: string): number {
|
|
21
|
-
const ptr = win.GetProcAddress(lib, name)
|
|
22
|
-
if (!ptr) { std.printf('Error: cannot load %s\n', name); std.exit(1) }
|
|
23
|
-
return ptr
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const GetOpenFileNameW = loadProc(_comdlg32, 'GetOpenFileNameW')
|
|
27
|
-
const SetDIBitsToDevice = loadProc(_gdi32, 'SetDIBitsToDevice')
|
|
28
|
-
const GetDC = loadProc(_user32, 'GetDC')
|
|
29
|
-
const ReleaseDC = loadProc(_user32, 'ReleaseDC')
|
|
30
|
-
const PatBlt = loadProc(_gdi32, 'PatBlt')
|
|
31
|
-
const WHITENESS = 0x00FF0062
|
|
32
|
-
|
|
33
|
-
function makeBitmapInfo(w: number, h: number): ArrayBuffer {
|
|
34
|
-
const bmi = new ArrayBuffer(40)
|
|
35
|
-
const bv = new DataView(bmi)
|
|
36
|
-
bv.setUint32(0, 40, true)
|
|
37
|
-
bv.setInt32(4, w, true)
|
|
38
|
-
bv.setInt32(8, -h, true)
|
|
39
|
-
bv.setUint16(12, 1, true)
|
|
40
|
-
bv.setUint16(14, 24, true)
|
|
41
|
-
return bmi
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function strToWide(str: string): ArrayBuffer {
|
|
45
|
-
return new TextEncoder('utf-16le').encode(str + '\0').buffer
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function wideToStr(buf: ArrayBuffer): string {
|
|
49
|
-
const dv = new DataView(buf)
|
|
50
|
-
const chars: number[] = []
|
|
51
|
-
for (let i = 0; i < buf.byteLength; i += 2) {
|
|
52
|
-
const c = dv.getUint16(i, true); if (c === 0) break
|
|
53
|
-
chars.push(c)
|
|
54
|
-
}
|
|
55
|
-
return String.fromCharCode(...chars)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function setPtr(dv: DataView, off: number, ptr: number): void {
|
|
59
|
-
dv.setUint32(off, ptr & 0xFFFFFFFF, true)
|
|
60
|
-
dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface PixmapInfo {
|
|
64
|
-
data: ArrayBuffer; w: number; h: number
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let hwndMain: gui.HWND | null = null
|
|
68
|
-
let hwndEdit: gui.HWND | null = null
|
|
69
|
-
let hwndCanvas: gui.HWND | null = null
|
|
70
|
-
let hwndContent: gui.HWND | null = null
|
|
71
|
-
let currentPixmap: PixmapInfo | null = null
|
|
72
|
-
let currentPage = 0, totalPages = 0
|
|
73
|
-
let scrollX = 0, scrollY = 0
|
|
74
|
-
|
|
75
|
-
let cachedPath = ''
|
|
76
|
-
let cachedDoc: Document | null = null
|
|
77
|
-
|
|
78
|
-
function clearCachedDoc(): void {
|
|
79
|
-
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
80
|
-
cachedDoc = null; cachedPath = ''
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function loadMupdf(): Promise<MuPdf | null> {
|
|
84
|
-
const wasmPath = './vendor/mupdf-wasm/mupdf-wasm.wasm'
|
|
85
|
-
const fp = std.open(wasmPath, 'rb')
|
|
86
|
-
if (!fp) { std.printf('Error: cannot open %s\n', wasmPath); return null }
|
|
87
|
-
fp.seek(0, 2); const size = fp.tell(); fp.seek(0, 0)
|
|
88
|
-
const buf = new ArrayBuffer(size); fp.read(buf, 0, size); fp.close()
|
|
89
|
-
;(globalThis).$libmupdf_wasm_Module = { wasmBinary: buf, locateFile: (p: string) => p }
|
|
90
|
-
try { return await import('../vendor/mupdf-wasm/mupdf.js') }
|
|
91
|
-
catch (e) { std.printf('Error: mupdf load failed: %s\n', String(e)); return null }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function renderPdfPage(mupdf: MuPdf, filePath: string, pageIndex: number): PixmapInfo & { totalPages: number } | null {
|
|
95
|
-
if (filePath !== cachedPath) {
|
|
96
|
-
clearCachedDoc()
|
|
97
|
-
const fp = std.open(filePath, 'rb')
|
|
98
|
-
if (!fp) { std.printf('Error: cannot open %s\n', filePath); return null }
|
|
99
|
-
let buf: ArrayBuffer | null = null
|
|
100
|
-
try {
|
|
101
|
-
fp.seek(0, 2); const size = fp.tell(); fp.seek(0, 0)
|
|
102
|
-
buf = new ArrayBuffer(size); fp.read(buf, 0, size)
|
|
103
|
-
} finally { fp.close() }
|
|
104
|
-
if (!buf) return null
|
|
105
|
-
try {
|
|
106
|
-
cachedDoc = mupdf.Document.openDocument(new Uint8Array(buf), 'application/pdf')
|
|
107
|
-
cachedPath = filePath
|
|
108
|
-
} catch (e) { std.printf('Error opening document: %s\n', String(e)); return null }
|
|
109
|
-
}
|
|
110
|
-
if (!cachedDoc) return null
|
|
111
|
-
let page: Page | null = null; let pixmap: Pixmap | null = null
|
|
112
|
-
try {
|
|
113
|
-
page = cachedDoc.loadPage(pageIndex)
|
|
114
|
-
pixmap = page.toPixmap(mupdf.Matrix.scale(1.5, 1.5), mupdf.ColorSpace.DeviceRGB, false)
|
|
115
|
-
if (!pixmap) return null
|
|
116
|
-
const srcPixels = pixmap.getPixels()
|
|
117
|
-
const srcStride = pixmap.getStride()
|
|
118
|
-
const w = pixmap.getWidth(), h = pixmap.getHeight()
|
|
119
|
-
const dibStride = Math.floor((w * 3 + 3) / 4) * 4
|
|
120
|
-
const dib = new Uint8Array(h * dibStride)
|
|
121
|
-
for (let y = 0; y < h; y++) {
|
|
122
|
-
const srcOff = y * srcStride, dstOff = y * dibStride
|
|
123
|
-
for (let x = 0; x < w; x++) {
|
|
124
|
-
const sx = srcOff + x * 3, dx = dstOff + x * 3
|
|
125
|
-
dib[dx] = srcPixels[sx + 2]!; dib[dx + 1] = srcPixels[sx + 1]!; dib[dx + 2] = srcPixels[sx]!
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const totalPages = cachedDoc.countPages()
|
|
129
|
-
return { data: dib.buffer, w, h, totalPages }
|
|
130
|
-
} catch (e) { std.printf('Error rendering: %s\n', String(e)); clearCachedDoc(); return null }
|
|
131
|
-
finally { if (pixmap) try { pixmap.destroy() } catch {}; if (page) try { page.destroy() } catch {} }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function openPdfFileDialog(): string | null {
|
|
135
|
-
assertNonNullable(hwndMain)
|
|
136
|
-
const structBuf = new ArrayBuffer(152), sv = new DataView(structBuf)
|
|
137
|
-
const fileBuf = new ArrayBuffer(260 * 2)
|
|
138
|
-
const filterWide = strToWide('PDF Files\0*.pdf\0All Files\0*.*\0\0')
|
|
139
|
-
sv.setUint32(0, 152, true)
|
|
140
|
-
sv.setUint32(8, hwndMain & 0xFFFFFFFF, true)
|
|
141
|
-
sv.setUint32(12, Math.floor(hwndMain / 0x100000000), true)
|
|
142
|
-
setPtr(sv, 24, ffi.bufferPtr(filterWide))
|
|
143
|
-
setPtr(sv, 48, ffi.bufferPtr(fileBuf))
|
|
144
|
-
sv.setUint32(56, 260, true)
|
|
145
|
-
sv.setUint32(96, 0x1000 | 0x0800 | 0x0004, true)
|
|
146
|
-
const ret = ffi.ffiCall(GetOpenFileNameW, [FFI_PTR], [structBuf], FFI_U32)
|
|
147
|
-
return ret ? wideToStr(fileBuf) : null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function updateScrollRange(): void {
|
|
151
|
-
assertNonNullable(hwndCanvas); assertNonNullable(hwndContent)
|
|
152
|
-
if (!currentPixmap) return
|
|
153
|
-
const cr = gui.GetClientRect(hwndCanvas)
|
|
154
|
-
if (!cr) return
|
|
155
|
-
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
156
|
-
const maxX = Math.max(0, currentPixmap.w - cw)
|
|
157
|
-
const maxY = Math.max(0, currentPixmap.h - ch)
|
|
158
|
-
scrollX = Math.min(scrollX, maxX)
|
|
159
|
-
scrollY = Math.min(scrollY, maxY)
|
|
160
|
-
if (currentPixmap.w > cw) {
|
|
161
|
-
const style = gui.GetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE) | 0
|
|
162
|
-
if (!(style & gui.WindowStyle.HSCROLL)) {
|
|
163
|
-
gui.SetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE, style | gui.WindowStyle.HSCROLL)
|
|
164
|
-
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 0, 0, 0,
|
|
165
|
-
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOSIZE |
|
|
166
|
-
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_FRAMECHANGED)
|
|
167
|
-
}
|
|
168
|
-
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.HORZ, { min: 0, max: currentPixmap.w - 1, page: cw, pos: scrollX }, true)
|
|
169
|
-
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.HORZ, true)
|
|
170
|
-
} else {
|
|
171
|
-
scrollX = 0
|
|
172
|
-
const style = gui.GetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE) | 0
|
|
173
|
-
if (style & gui.WindowStyle.HSCROLL) {
|
|
174
|
-
gui.SetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE, style & ~gui.WindowStyle.HSCROLL)
|
|
175
|
-
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 0, 0, 0,
|
|
176
|
-
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOSIZE |
|
|
177
|
-
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_FRAMECHANGED)
|
|
178
|
-
}
|
|
179
|
-
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.HORZ, false)
|
|
180
|
-
}
|
|
181
|
-
if (currentPixmap.h > ch) {
|
|
182
|
-
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.VERT, { min: 0, max: currentPixmap.h - 1, page: ch, pos: scrollY }, true)
|
|
183
|
-
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.VERT, true)
|
|
184
|
-
} else { scrollY = 0; gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.VERT, false) }
|
|
185
|
-
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, -scrollX, -scrollY, 0, 0,
|
|
186
|
-
gui.SetWindowPosFlag.SWP_NOSIZE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function showPdf(mupdf: MuPdf, path: string, pageIdx: number): void {
|
|
190
|
-
assertNonNullable(hwndMain); assertNonNullable(hwndContent)
|
|
191
|
-
const pix = renderPdfPage(mupdf, path, pageIdx)
|
|
192
|
-
if (!pix) { gui.MessageBox('渲染 PDF 失败'); return }
|
|
193
|
-
currentPixmap = pix; currentPage = pageIdx; totalPages = pix.totalPages
|
|
194
|
-
gui.SetWindowText(hwndMain, 'PDF 预览 - 第 ' + (pageIdx + 1) + '/' + totalPages + ' 页')
|
|
195
|
-
scrollX = 0; scrollY = 0
|
|
196
|
-
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, 0, 0, pix.w, pix.h,
|
|
197
|
-
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_NOMOVE)
|
|
198
|
-
updateScrollRange()
|
|
199
|
-
gui.InvalidateRect(hwndContent, null, true)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function doScroll(dx: number, dy: number): void {
|
|
203
|
-
assertNonNullable(hwndCanvas); assertNonNullable(hwndContent)
|
|
204
|
-
const cr = gui.GetClientRect(hwndCanvas)
|
|
205
|
-
if (!cr || !currentPixmap) return
|
|
206
|
-
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
207
|
-
const maxX = Math.max(0, currentPixmap.w - cw)
|
|
208
|
-
const maxY = Math.max(0, currentPixmap.h - ch)
|
|
209
|
-
scrollX = Math.max(0, Math.min(maxX, scrollX + dx))
|
|
210
|
-
scrollY = Math.max(0, Math.min(maxY, scrollY + dy))
|
|
211
|
-
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, -scrollX, -scrollY, 0, 0,
|
|
212
|
-
gui.SetWindowPosFlag.SWP_NOSIZE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
213
|
-
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.HORZ, { pos: scrollX }, true)
|
|
214
|
-
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.VERT, { pos: scrollY }, true)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function main(): Promise<void> {
|
|
218
|
-
const mupdf = await loadMupdf()
|
|
219
|
-
if (!mupdf) { gui.MessageBox('加载 mupdf WASM 失败'); return }
|
|
220
|
-
|
|
221
|
-
const WS_CHILD = gui.WindowStyle.CHILD
|
|
222
|
-
const WS_VIS = gui.WindowStyle.VISIBLE
|
|
223
|
-
const WS_BORDER = gui.WindowStyle.BORDER
|
|
224
|
-
const ctrlY = 12, ctrlH = 26, gap = 4, btnW = 80, btnPageW = 72
|
|
225
|
-
|
|
226
|
-
let hwndBtnOpen: gui.HWND | null = null
|
|
227
|
-
let hwndBtnPrev: gui.HWND | null = null
|
|
228
|
-
let hwndBtnNext: gui.HWND | null = null
|
|
229
|
-
|
|
230
|
-
gui.RegisterClass('PdfViewer2', (hwnd, msg, wParam, lParam) => {
|
|
231
|
-
assertNonNullable(hwndCanvas); assertNonNullable(hwndEdit); assertNonNullable(hwndBtnOpen); assertNonNullable(hwndBtnPrev); assertNonNullable(hwndBtnNext)
|
|
232
|
-
if (msg === gui.WmMsg.DESTROY) { gui.PostQuitMessage(0); return 0 }
|
|
233
|
-
if (msg === gui.WmMsg.SIZE) {
|
|
234
|
-
const cr = gui.GetClientRect(hwnd)
|
|
235
|
-
if (cr) {
|
|
236
|
-
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
237
|
-
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 50, cw, Math.max(1, ch - 50), gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
238
|
-
gui.SetWindowPos(hwndEdit, gui.SetWindowPosHwnd.TOP, 0, 0, Math.max(100, cw - 320), 0,
|
|
239
|
-
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
240
|
-
}
|
|
241
|
-
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
242
|
-
}
|
|
243
|
-
if (msg === gui.WmMsg.COMMAND) {
|
|
244
|
-
const hCtrl = lParam
|
|
245
|
-
if (hCtrl === hwndBtnOpen) {
|
|
246
|
-
const path = openPdfFileDialog()
|
|
247
|
-
if (path) { gui.SetWindowText(hwndEdit, path); showPdf(mupdf, path, 0) }
|
|
248
|
-
} else if (hCtrl === hwndBtnPrev) {
|
|
249
|
-
const path = gui.GetWindowText(hwndEdit)
|
|
250
|
-
if (path && currentPage > 0) showPdf(mupdf, path, currentPage - 1)
|
|
251
|
-
} else if (hCtrl === hwndBtnNext) {
|
|
252
|
-
const path = gui.GetWindowText(hwndEdit)
|
|
253
|
-
if (path && currentPage < totalPages - 1) showPdf(mupdf, path, currentPage + 1)
|
|
254
|
-
}
|
|
255
|
-
return 0
|
|
256
|
-
}
|
|
257
|
-
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
258
|
-
})
|
|
259
|
-
hwndMain = gui.CreateWindow('PdfViewer2', 'PDF 预览 2',
|
|
260
|
-
gui.WindowStyle.OVERLAPPEDWINDOW | gui.WindowStyle.CLIPCHILDREN,
|
|
261
|
-
100, 100, 1100, 800, null, null)!
|
|
262
|
-
if (!hwndMain) { gui.MessageBox('创建主窗口失败'); return }
|
|
263
|
-
|
|
264
|
-
// 控件直接挂在主窗口上
|
|
265
|
-
hwndEdit = gui.CreateWindow('EDIT', '',
|
|
266
|
-
WS_CHILD | WS_VIS | WS_BORDER,
|
|
267
|
-
ctrlY + btnW + gap, ctrlY, 480, ctrlH, hwndMain, null)!
|
|
268
|
-
hwndBtnOpen = gui.CreateWindow('BUTTON', '打开 PDF',
|
|
269
|
-
WS_CHILD | WS_VIS,
|
|
270
|
-
ctrlY, ctrlY, btnW, ctrlH, hwndMain, null)!
|
|
271
|
-
hwndBtnPrev = gui.CreateWindow('BUTTON', '上一页',
|
|
272
|
-
WS_CHILD | WS_VIS,
|
|
273
|
-
ctrlY + btnW + gap + 480 + gap, ctrlY, btnPageW, ctrlH, hwndMain, null)!
|
|
274
|
-
hwndBtnNext = gui.CreateWindow('BUTTON', '下一页',
|
|
275
|
-
WS_CHILD | WS_VIS,
|
|
276
|
-
ctrlY + btnW + gap + 480 + gap + btnPageW + gap, ctrlY, btnPageW, ctrlH, hwndMain, null)!
|
|
277
|
-
|
|
278
|
-
// 画布(带滚动条)
|
|
279
|
-
hwndCanvas = gui.CreateWindow('STATIC', '',
|
|
280
|
-
WS_CHILD | WS_VIS | gui.WindowStyle.CLIPCHILDREN | gui.WindowStyle.VSCROLL | gui.WindowStyle.HSCROLL,
|
|
281
|
-
0, 50, 1100, 750, hwndMain, null)!
|
|
282
|
-
|
|
283
|
-
// 内容窗口(画 pixmap)
|
|
284
|
-
hwndContent = gui.CreateWindow('STATIC', '',
|
|
285
|
-
WS_CHILD | WS_VIS, 0, 0, 100, 100, hwndCanvas, null)!
|
|
286
|
-
|
|
287
|
-
assertNonNullable(hwndMain)
|
|
288
|
-
assertNonNullable(hwndEdit)
|
|
289
|
-
assertNonNullable(hwndCanvas)
|
|
290
|
-
assertNonNullable(hwndContent)
|
|
291
|
-
assertNonNullable(hwndBtnOpen)
|
|
292
|
-
assertNonNullable(hwndBtnPrev)
|
|
293
|
-
assertNonNullable(hwndBtnNext)
|
|
294
|
-
|
|
295
|
-
// 内容窗口 PAINT
|
|
296
|
-
gui.SetWindowProc(hwndContent, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number): number => {
|
|
297
|
-
if (msg === gui.WmMsg.ERASEBKGND) return 1
|
|
298
|
-
if (msg === gui.WmMsg.PAINT) {
|
|
299
|
-
gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
300
|
-
const pm = currentPixmap
|
|
301
|
-
if (!pm) return 0
|
|
302
|
-
const hdc = ffi.ffiCall(GetDC, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
303
|
-
if (hdc) {
|
|
304
|
-
const bmi = makeBitmapInfo(pm.w, pm.h)
|
|
305
|
-
ffi.ffiCall(SetDIBitsToDevice, [
|
|
306
|
-
ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
307
|
-
FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
308
|
-
FFI_PTR, FFI_PTR, FFI_U32
|
|
309
|
-
], [hdc, 0, 0, pm.w, pm.h, 0, 0, 0, pm.h, pm.data, bmi, 0], FFI_S32)
|
|
310
|
-
ffi.ffiCall(ReleaseDC, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
311
|
-
}
|
|
312
|
-
return 0
|
|
313
|
-
}
|
|
314
|
-
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
// 画布消息处理(PAINT 填白、滚动条、滚轮)
|
|
318
|
-
gui.SetWindowProc(hwndCanvas, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number): number => {
|
|
319
|
-
if (msg === gui.WmMsg.ERASEBKGND) return 1
|
|
320
|
-
if (msg === gui.WmMsg.PAINT) {
|
|
321
|
-
const cr = gui.GetClientRect(hwnd)
|
|
322
|
-
if (cr) {
|
|
323
|
-
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
324
|
-
const hdc = ffi.ffiCall(GetDC, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
325
|
-
if (hdc) {
|
|
326
|
-
ffi.ffiCall(PatBlt, [ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_S32, FFI_S32, FFI_U32],
|
|
327
|
-
[hdc, 0, 0, cw, ch, WHITENESS], FFI_U32)
|
|
328
|
-
ffi.ffiCall(ReleaseDC, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
332
|
-
return 0
|
|
333
|
-
}
|
|
334
|
-
if (msg === gui.WmMsg.VSCROLL || msg === gui.WmMsg.HSCROLL) {
|
|
335
|
-
const bar = msg === gui.WmMsg.VSCROLL ? gui.ScrollBar.VERT : gui.ScrollBar.HORZ
|
|
336
|
-
const info = gui.GetScrollInfo(hwnd, bar)
|
|
337
|
-
const maxPos = info.max - info.page + 1
|
|
338
|
-
const code = wParam & 0xFFFF
|
|
339
|
-
let newPos = bar === gui.ScrollBar.VERT ? scrollY : scrollX
|
|
340
|
-
if (code === gui.ScrollCmd.THUMBTRACK || code === gui.ScrollCmd.THUMBPOSITION)
|
|
341
|
-
newPos = Math.max(0, Math.min(maxPos, (wParam >> 16) & 0xFFFF))
|
|
342
|
-
else if (code === gui.ScrollCmd.LINEUP) newPos -= 20
|
|
343
|
-
else if (code === gui.ScrollCmd.LINEDOWN) newPos += 20
|
|
344
|
-
else if (code === gui.ScrollCmd.PAGEUP) newPos -= info.page
|
|
345
|
-
else if (code === gui.ScrollCmd.PAGEDOWN) newPos += info.page
|
|
346
|
-
else return 0
|
|
347
|
-
newPos = Math.max(0, Math.min(maxPos, newPos))
|
|
348
|
-
if (bar === gui.ScrollBar.VERT) doScroll(0, newPos - scrollY)
|
|
349
|
-
else doScroll(newPos - scrollX, 0)
|
|
350
|
-
return 0
|
|
351
|
-
}
|
|
352
|
-
if (msg === gui.WmMsg.MOUSEWHEEL) {
|
|
353
|
-
const raw = (wParam >>> 16) & 0xFFFF
|
|
354
|
-
const wheel = raw >= 0x8000 ? raw - 0x10000 : raw
|
|
355
|
-
const isHorz = (wParam & gui.MouseKeyFlag.MK_SHIFT) !== 0
|
|
356
|
-
const bar = isHorz ? gui.ScrollBar.HORZ : gui.ScrollBar.VERT
|
|
357
|
-
const info = gui.GetScrollInfo(hwnd, bar)
|
|
358
|
-
const maxPos = info.max - info.page + 1
|
|
359
|
-
let newPos = (isHorz ? scrollX : scrollY) - Math.round(wheel * 40 / 120)
|
|
360
|
-
newPos = Math.max(0, Math.min(maxPos, newPos))
|
|
361
|
-
if (isHorz) doScroll(newPos - scrollX, 0)
|
|
362
|
-
else doScroll(0, newPos - scrollY)
|
|
363
|
-
return 0
|
|
364
|
-
}
|
|
365
|
-
if (msg === gui.WmMsg.SIZE) { updateScrollRange(); return gui.DefWindowProc(hwnd, msg, wParam, lParam) }
|
|
366
|
-
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
gui.ShowWindow(hwndMain)
|
|
370
|
-
|
|
371
|
-
const test = std.open('example.pdf', 'rb')
|
|
372
|
-
if (test) { test.close(); showPdf(mupdf, 'example.pdf', 0) }
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
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 { assertNonNullable } from '../lib/assert.js'
|
|
8
|
+
|
|
9
|
+
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
10
|
+
const FFI_U32 = ffi.FFI_TYPE_UINT32
|
|
11
|
+
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
12
|
+
|
|
13
|
+
const _user32 = win.LoadLibrary('user32.dll')
|
|
14
|
+
const _gdi32 = win.LoadLibrary('gdi32.dll')
|
|
15
|
+
const _comdlg32 = win.LoadLibrary('comdlg32.dll')
|
|
16
|
+
|
|
17
|
+
type MuPdf = typeof import('../vendor/mupdf-wasm/mupdf.js')
|
|
18
|
+
if (!(_user32 && _gdi32 && _comdlg32)) std.exit(0)
|
|
19
|
+
|
|
20
|
+
function loadProc(lib: win.HMODULE, name: string): number {
|
|
21
|
+
const ptr = win.GetProcAddress(lib, name)
|
|
22
|
+
if (!ptr) { std.printf('Error: cannot load %s\n', name); std.exit(1) }
|
|
23
|
+
return ptr
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const GetOpenFileNameW = loadProc(_comdlg32, 'GetOpenFileNameW')
|
|
27
|
+
const SetDIBitsToDevice = loadProc(_gdi32, 'SetDIBitsToDevice')
|
|
28
|
+
const GetDC = loadProc(_user32, 'GetDC')
|
|
29
|
+
const ReleaseDC = loadProc(_user32, 'ReleaseDC')
|
|
30
|
+
const PatBlt = loadProc(_gdi32, 'PatBlt')
|
|
31
|
+
const WHITENESS = 0x00FF0062
|
|
32
|
+
|
|
33
|
+
function makeBitmapInfo(w: number, h: number): ArrayBuffer {
|
|
34
|
+
const bmi = new ArrayBuffer(40)
|
|
35
|
+
const bv = new DataView(bmi)
|
|
36
|
+
bv.setUint32(0, 40, true)
|
|
37
|
+
bv.setInt32(4, w, true)
|
|
38
|
+
bv.setInt32(8, -h, true)
|
|
39
|
+
bv.setUint16(12, 1, true)
|
|
40
|
+
bv.setUint16(14, 24, true)
|
|
41
|
+
return bmi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function strToWide(str: string): ArrayBuffer {
|
|
45
|
+
return new TextEncoder('utf-16le').encode(str + '\0').buffer
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function wideToStr(buf: ArrayBuffer): string {
|
|
49
|
+
const dv = new DataView(buf)
|
|
50
|
+
const chars: number[] = []
|
|
51
|
+
for (let i = 0; i < buf.byteLength; i += 2) {
|
|
52
|
+
const c = dv.getUint16(i, true); if (c === 0) break
|
|
53
|
+
chars.push(c)
|
|
54
|
+
}
|
|
55
|
+
return String.fromCharCode(...chars)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setPtr(dv: DataView, off: number, ptr: number): void {
|
|
59
|
+
dv.setUint32(off, ptr & 0xFFFFFFFF, true)
|
|
60
|
+
dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface PixmapInfo {
|
|
64
|
+
data: ArrayBuffer; w: number; h: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let hwndMain: gui.HWND | null = null
|
|
68
|
+
let hwndEdit: gui.HWND | null = null
|
|
69
|
+
let hwndCanvas: gui.HWND | null = null
|
|
70
|
+
let hwndContent: gui.HWND | null = null
|
|
71
|
+
let currentPixmap: PixmapInfo | null = null
|
|
72
|
+
let currentPage = 0, totalPages = 0
|
|
73
|
+
let scrollX = 0, scrollY = 0
|
|
74
|
+
|
|
75
|
+
let cachedPath = ''
|
|
76
|
+
let cachedDoc: Document | null = null
|
|
77
|
+
|
|
78
|
+
function clearCachedDoc(): void {
|
|
79
|
+
if (cachedDoc) { try { cachedDoc.destroy() } catch {} }
|
|
80
|
+
cachedDoc = null; cachedPath = ''
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function loadMupdf(): Promise<MuPdf | null> {
|
|
84
|
+
const wasmPath = './vendor/mupdf-wasm/mupdf-wasm.wasm'
|
|
85
|
+
const fp = std.open(wasmPath, 'rb')
|
|
86
|
+
if (!fp) { std.printf('Error: cannot open %s\n', wasmPath); return null }
|
|
87
|
+
fp.seek(0, 2); const size = fp.tell(); fp.seek(0, 0)
|
|
88
|
+
const buf = new ArrayBuffer(size); fp.read(buf, 0, size); fp.close()
|
|
89
|
+
;(globalThis).$libmupdf_wasm_Module = { wasmBinary: buf, locateFile: (p: string) => p }
|
|
90
|
+
try { return await import('../vendor/mupdf-wasm/mupdf.js') }
|
|
91
|
+
catch (e) { std.printf('Error: mupdf load failed: %s\n', String(e)); return null }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderPdfPage(mupdf: MuPdf, filePath: string, pageIndex: number): PixmapInfo & { totalPages: number } | null {
|
|
95
|
+
if (filePath !== cachedPath) {
|
|
96
|
+
clearCachedDoc()
|
|
97
|
+
const fp = std.open(filePath, 'rb')
|
|
98
|
+
if (!fp) { std.printf('Error: cannot open %s\n', filePath); return null }
|
|
99
|
+
let buf: ArrayBuffer | null = null
|
|
100
|
+
try {
|
|
101
|
+
fp.seek(0, 2); const size = fp.tell(); fp.seek(0, 0)
|
|
102
|
+
buf = new ArrayBuffer(size); fp.read(buf, 0, size)
|
|
103
|
+
} finally { fp.close() }
|
|
104
|
+
if (!buf) return null
|
|
105
|
+
try {
|
|
106
|
+
cachedDoc = mupdf.Document.openDocument(new Uint8Array(buf), 'application/pdf')
|
|
107
|
+
cachedPath = filePath
|
|
108
|
+
} catch (e) { std.printf('Error opening document: %s\n', String(e)); return null }
|
|
109
|
+
}
|
|
110
|
+
if (!cachedDoc) return null
|
|
111
|
+
let page: Page | null = null; let pixmap: Pixmap | null = null
|
|
112
|
+
try {
|
|
113
|
+
page = cachedDoc.loadPage(pageIndex)
|
|
114
|
+
pixmap = page.toPixmap(mupdf.Matrix.scale(1.5, 1.5), mupdf.ColorSpace.DeviceRGB, false)
|
|
115
|
+
if (!pixmap) return null
|
|
116
|
+
const srcPixels = pixmap.getPixels()
|
|
117
|
+
const srcStride = pixmap.getStride()
|
|
118
|
+
const w = pixmap.getWidth(), h = pixmap.getHeight()
|
|
119
|
+
const dibStride = Math.floor((w * 3 + 3) / 4) * 4
|
|
120
|
+
const dib = new Uint8Array(h * dibStride)
|
|
121
|
+
for (let y = 0; y < h; y++) {
|
|
122
|
+
const srcOff = y * srcStride, dstOff = y * dibStride
|
|
123
|
+
for (let x = 0; x < w; x++) {
|
|
124
|
+
const sx = srcOff + x * 3, dx = dstOff + x * 3
|
|
125
|
+
dib[dx] = srcPixels[sx + 2]!; dib[dx + 1] = srcPixels[sx + 1]!; dib[dx + 2] = srcPixels[sx]!
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const totalPages = cachedDoc.countPages()
|
|
129
|
+
return { data: dib.buffer, w, h, totalPages }
|
|
130
|
+
} catch (e) { std.printf('Error rendering: %s\n', String(e)); clearCachedDoc(); return null }
|
|
131
|
+
finally { if (pixmap) try { pixmap.destroy() } catch {}; if (page) try { page.destroy() } catch {} }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openPdfFileDialog(): string | null {
|
|
135
|
+
assertNonNullable(hwndMain)
|
|
136
|
+
const structBuf = new ArrayBuffer(152), sv = new DataView(structBuf)
|
|
137
|
+
const fileBuf = new ArrayBuffer(260 * 2)
|
|
138
|
+
const filterWide = strToWide('PDF Files\0*.pdf\0All Files\0*.*\0\0')
|
|
139
|
+
sv.setUint32(0, 152, true)
|
|
140
|
+
sv.setUint32(8, hwndMain & 0xFFFFFFFF, true)
|
|
141
|
+
sv.setUint32(12, Math.floor(hwndMain / 0x100000000), true)
|
|
142
|
+
setPtr(sv, 24, ffi.bufferPtr(filterWide))
|
|
143
|
+
setPtr(sv, 48, ffi.bufferPtr(fileBuf))
|
|
144
|
+
sv.setUint32(56, 260, true)
|
|
145
|
+
sv.setUint32(96, 0x1000 | 0x0800 | 0x0004, true)
|
|
146
|
+
const ret = ffi.ffiCall(GetOpenFileNameW, [FFI_PTR], [structBuf], FFI_U32)
|
|
147
|
+
return ret ? wideToStr(fileBuf) : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function updateScrollRange(): void {
|
|
151
|
+
assertNonNullable(hwndCanvas); assertNonNullable(hwndContent)
|
|
152
|
+
if (!currentPixmap) return
|
|
153
|
+
const cr = gui.GetClientRect(hwndCanvas)
|
|
154
|
+
if (!cr) return
|
|
155
|
+
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
156
|
+
const maxX = Math.max(0, currentPixmap.w - cw)
|
|
157
|
+
const maxY = Math.max(0, currentPixmap.h - ch)
|
|
158
|
+
scrollX = Math.min(scrollX, maxX)
|
|
159
|
+
scrollY = Math.min(scrollY, maxY)
|
|
160
|
+
if (currentPixmap.w > cw) {
|
|
161
|
+
const style = gui.GetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE) | 0
|
|
162
|
+
if (!(style & gui.WindowStyle.HSCROLL)) {
|
|
163
|
+
gui.SetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE, style | gui.WindowStyle.HSCROLL)
|
|
164
|
+
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 0, 0, 0,
|
|
165
|
+
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOSIZE |
|
|
166
|
+
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_FRAMECHANGED)
|
|
167
|
+
}
|
|
168
|
+
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.HORZ, { min: 0, max: currentPixmap.w - 1, page: cw, pos: scrollX }, true)
|
|
169
|
+
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.HORZ, true)
|
|
170
|
+
} else {
|
|
171
|
+
scrollX = 0
|
|
172
|
+
const style = gui.GetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE) | 0
|
|
173
|
+
if (style & gui.WindowStyle.HSCROLL) {
|
|
174
|
+
gui.SetWindowLongPtr(hwndCanvas, gui.Gwlp.STYLE, style & ~gui.WindowStyle.HSCROLL)
|
|
175
|
+
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 0, 0, 0,
|
|
176
|
+
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOSIZE |
|
|
177
|
+
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_FRAMECHANGED)
|
|
178
|
+
}
|
|
179
|
+
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.HORZ, false)
|
|
180
|
+
}
|
|
181
|
+
if (currentPixmap.h > ch) {
|
|
182
|
+
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.VERT, { min: 0, max: currentPixmap.h - 1, page: ch, pos: scrollY }, true)
|
|
183
|
+
gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.VERT, true)
|
|
184
|
+
} else { scrollY = 0; gui.ShowScrollBar(hwndCanvas, gui.ScrollBar.VERT, false) }
|
|
185
|
+
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, -scrollX, -scrollY, 0, 0,
|
|
186
|
+
gui.SetWindowPosFlag.SWP_NOSIZE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function showPdf(mupdf: MuPdf, path: string, pageIdx: number): void {
|
|
190
|
+
assertNonNullable(hwndMain); assertNonNullable(hwndContent)
|
|
191
|
+
const pix = renderPdfPage(mupdf, path, pageIdx)
|
|
192
|
+
if (!pix) { gui.MessageBox('渲染 PDF 失败'); return }
|
|
193
|
+
currentPixmap = pix; currentPage = pageIdx; totalPages = pix.totalPages
|
|
194
|
+
gui.SetWindowText(hwndMain, 'PDF 预览 - 第 ' + (pageIdx + 1) + '/' + totalPages + ' 页')
|
|
195
|
+
scrollX = 0; scrollY = 0
|
|
196
|
+
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, 0, 0, pix.w, pix.h,
|
|
197
|
+
gui.SetWindowPosFlag.SWP_NOZORDER | gui.SetWindowPosFlag.SWP_NOMOVE)
|
|
198
|
+
updateScrollRange()
|
|
199
|
+
gui.InvalidateRect(hwndContent, null, true)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function doScroll(dx: number, dy: number): void {
|
|
203
|
+
assertNonNullable(hwndCanvas); assertNonNullable(hwndContent)
|
|
204
|
+
const cr = gui.GetClientRect(hwndCanvas)
|
|
205
|
+
if (!cr || !currentPixmap) return
|
|
206
|
+
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
207
|
+
const maxX = Math.max(0, currentPixmap.w - cw)
|
|
208
|
+
const maxY = Math.max(0, currentPixmap.h - ch)
|
|
209
|
+
scrollX = Math.max(0, Math.min(maxX, scrollX + dx))
|
|
210
|
+
scrollY = Math.max(0, Math.min(maxY, scrollY + dy))
|
|
211
|
+
gui.SetWindowPos(hwndContent, gui.SetWindowPosHwnd.TOP, -scrollX, -scrollY, 0, 0,
|
|
212
|
+
gui.SetWindowPosFlag.SWP_NOSIZE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
213
|
+
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.HORZ, { pos: scrollX }, true)
|
|
214
|
+
gui.SetScrollInfo(hwndCanvas, gui.ScrollBar.VERT, { pos: scrollY }, true)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main(): Promise<void> {
|
|
218
|
+
const mupdf = await loadMupdf()
|
|
219
|
+
if (!mupdf) { gui.MessageBox('加载 mupdf WASM 失败'); return }
|
|
220
|
+
|
|
221
|
+
const WS_CHILD = gui.WindowStyle.CHILD
|
|
222
|
+
const WS_VIS = gui.WindowStyle.VISIBLE
|
|
223
|
+
const WS_BORDER = gui.WindowStyle.BORDER
|
|
224
|
+
const ctrlY = 12, ctrlH = 26, gap = 4, btnW = 80, btnPageW = 72
|
|
225
|
+
|
|
226
|
+
let hwndBtnOpen: gui.HWND | null = null
|
|
227
|
+
let hwndBtnPrev: gui.HWND | null = null
|
|
228
|
+
let hwndBtnNext: gui.HWND | null = null
|
|
229
|
+
|
|
230
|
+
gui.RegisterClass('PdfViewer2', (hwnd, msg, wParam, lParam) => {
|
|
231
|
+
assertNonNullable(hwndCanvas); assertNonNullable(hwndEdit); assertNonNullable(hwndBtnOpen); assertNonNullable(hwndBtnPrev); assertNonNullable(hwndBtnNext)
|
|
232
|
+
if (msg === gui.WmMsg.DESTROY) { gui.PostQuitMessage(0); return 0 }
|
|
233
|
+
if (msg === gui.WmMsg.SIZE) {
|
|
234
|
+
const cr = gui.GetClientRect(hwnd)
|
|
235
|
+
if (cr) {
|
|
236
|
+
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
237
|
+
gui.SetWindowPos(hwndCanvas, gui.SetWindowPosHwnd.TOP, 0, 50, cw, Math.max(1, ch - 50), gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
238
|
+
gui.SetWindowPos(hwndEdit, gui.SetWindowPosHwnd.TOP, 0, 0, Math.max(100, cw - 320), 0,
|
|
239
|
+
gui.SetWindowPosFlag.SWP_NOMOVE | gui.SetWindowPosFlag.SWP_NOZORDER)
|
|
240
|
+
}
|
|
241
|
+
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
242
|
+
}
|
|
243
|
+
if (msg === gui.WmMsg.COMMAND) {
|
|
244
|
+
const hCtrl = lParam
|
|
245
|
+
if (hCtrl === hwndBtnOpen) {
|
|
246
|
+
const path = openPdfFileDialog()
|
|
247
|
+
if (path) { gui.SetWindowText(hwndEdit, path); showPdf(mupdf, path, 0) }
|
|
248
|
+
} else if (hCtrl === hwndBtnPrev) {
|
|
249
|
+
const path = gui.GetWindowText(hwndEdit)
|
|
250
|
+
if (path && currentPage > 0) showPdf(mupdf, path, currentPage - 1)
|
|
251
|
+
} else if (hCtrl === hwndBtnNext) {
|
|
252
|
+
const path = gui.GetWindowText(hwndEdit)
|
|
253
|
+
if (path && currentPage < totalPages - 1) showPdf(mupdf, path, currentPage + 1)
|
|
254
|
+
}
|
|
255
|
+
return 0
|
|
256
|
+
}
|
|
257
|
+
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
258
|
+
})
|
|
259
|
+
hwndMain = gui.CreateWindow('PdfViewer2', 'PDF 预览 2',
|
|
260
|
+
gui.WindowStyle.OVERLAPPEDWINDOW | gui.WindowStyle.CLIPCHILDREN,
|
|
261
|
+
100, 100, 1100, 800, null, null)!
|
|
262
|
+
if (!hwndMain) { gui.MessageBox('创建主窗口失败'); return }
|
|
263
|
+
|
|
264
|
+
// 控件直接挂在主窗口上
|
|
265
|
+
hwndEdit = gui.CreateWindow('EDIT', '',
|
|
266
|
+
WS_CHILD | WS_VIS | WS_BORDER,
|
|
267
|
+
ctrlY + btnW + gap, ctrlY, 480, ctrlH, hwndMain, null)!
|
|
268
|
+
hwndBtnOpen = gui.CreateWindow('BUTTON', '打开 PDF',
|
|
269
|
+
WS_CHILD | WS_VIS,
|
|
270
|
+
ctrlY, ctrlY, btnW, ctrlH, hwndMain, null)!
|
|
271
|
+
hwndBtnPrev = gui.CreateWindow('BUTTON', '上一页',
|
|
272
|
+
WS_CHILD | WS_VIS,
|
|
273
|
+
ctrlY + btnW + gap + 480 + gap, ctrlY, btnPageW, ctrlH, hwndMain, null)!
|
|
274
|
+
hwndBtnNext = gui.CreateWindow('BUTTON', '下一页',
|
|
275
|
+
WS_CHILD | WS_VIS,
|
|
276
|
+
ctrlY + btnW + gap + 480 + gap + btnPageW + gap, ctrlY, btnPageW, ctrlH, hwndMain, null)!
|
|
277
|
+
|
|
278
|
+
// 画布(带滚动条)
|
|
279
|
+
hwndCanvas = gui.CreateWindow('STATIC', '',
|
|
280
|
+
WS_CHILD | WS_VIS | gui.WindowStyle.CLIPCHILDREN | gui.WindowStyle.VSCROLL | gui.WindowStyle.HSCROLL,
|
|
281
|
+
0, 50, 1100, 750, hwndMain, null)!
|
|
282
|
+
|
|
283
|
+
// 内容窗口(画 pixmap)
|
|
284
|
+
hwndContent = gui.CreateWindow('STATIC', '',
|
|
285
|
+
WS_CHILD | WS_VIS, 0, 0, 100, 100, hwndCanvas, null)!
|
|
286
|
+
|
|
287
|
+
assertNonNullable(hwndMain)
|
|
288
|
+
assertNonNullable(hwndEdit)
|
|
289
|
+
assertNonNullable(hwndCanvas)
|
|
290
|
+
assertNonNullable(hwndContent)
|
|
291
|
+
assertNonNullable(hwndBtnOpen)
|
|
292
|
+
assertNonNullable(hwndBtnPrev)
|
|
293
|
+
assertNonNullable(hwndBtnNext)
|
|
294
|
+
|
|
295
|
+
// 内容窗口 PAINT
|
|
296
|
+
gui.SetWindowProc(hwndContent, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number): number => {
|
|
297
|
+
if (msg === gui.WmMsg.ERASEBKGND) return 1
|
|
298
|
+
if (msg === gui.WmMsg.PAINT) {
|
|
299
|
+
gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
300
|
+
const pm = currentPixmap
|
|
301
|
+
if (!pm) return 0
|
|
302
|
+
const hdc = ffi.ffiCall(GetDC, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
303
|
+
if (hdc) {
|
|
304
|
+
const bmi = makeBitmapInfo(pm.w, pm.h)
|
|
305
|
+
ffi.ffiCall(SetDIBitsToDevice, [
|
|
306
|
+
ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
307
|
+
FFI_S32, FFI_S32, FFI_U32, FFI_U32,
|
|
308
|
+
FFI_PTR, FFI_PTR, FFI_U32
|
|
309
|
+
], [hdc, 0, 0, pm.w, pm.h, 0, 0, 0, pm.h, pm.data, bmi, 0], FFI_S32)
|
|
310
|
+
ffi.ffiCall(ReleaseDC, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
311
|
+
}
|
|
312
|
+
return 0
|
|
313
|
+
}
|
|
314
|
+
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// 画布消息处理(PAINT 填白、滚动条、滚轮)
|
|
318
|
+
gui.SetWindowProc(hwndCanvas, (hwnd: gui.HWND, msg: number, wParam: number, lParam: number): number => {
|
|
319
|
+
if (msg === gui.WmMsg.ERASEBKGND) return 1
|
|
320
|
+
if (msg === gui.WmMsg.PAINT) {
|
|
321
|
+
const cr = gui.GetClientRect(hwnd)
|
|
322
|
+
if (cr) {
|
|
323
|
+
const cw = cr.right - cr.left, ch = cr.bottom - cr.top
|
|
324
|
+
const hdc = ffi.ffiCall(GetDC, [ffi.FFI_TYPE_UINT64], [hwnd], ffi.FFI_TYPE_UINT64)
|
|
325
|
+
if (hdc) {
|
|
326
|
+
ffi.ffiCall(PatBlt, [ffi.FFI_TYPE_UINT64, FFI_S32, FFI_S32, FFI_S32, FFI_S32, FFI_U32],
|
|
327
|
+
[hdc, 0, 0, cw, ch, WHITENESS], FFI_U32)
|
|
328
|
+
ffi.ffiCall(ReleaseDC, [ffi.FFI_TYPE_UINT64, ffi.FFI_TYPE_UINT64], [hwnd, hdc], FFI_S32)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
332
|
+
return 0
|
|
333
|
+
}
|
|
334
|
+
if (msg === gui.WmMsg.VSCROLL || msg === gui.WmMsg.HSCROLL) {
|
|
335
|
+
const bar = msg === gui.WmMsg.VSCROLL ? gui.ScrollBar.VERT : gui.ScrollBar.HORZ
|
|
336
|
+
const info = gui.GetScrollInfo(hwnd, bar)
|
|
337
|
+
const maxPos = info.max - info.page + 1
|
|
338
|
+
const code = wParam & 0xFFFF
|
|
339
|
+
let newPos = bar === gui.ScrollBar.VERT ? scrollY : scrollX
|
|
340
|
+
if (code === gui.ScrollCmd.THUMBTRACK || code === gui.ScrollCmd.THUMBPOSITION)
|
|
341
|
+
newPos = Math.max(0, Math.min(maxPos, (wParam >> 16) & 0xFFFF))
|
|
342
|
+
else if (code === gui.ScrollCmd.LINEUP) newPos -= 20
|
|
343
|
+
else if (code === gui.ScrollCmd.LINEDOWN) newPos += 20
|
|
344
|
+
else if (code === gui.ScrollCmd.PAGEUP) newPos -= info.page
|
|
345
|
+
else if (code === gui.ScrollCmd.PAGEDOWN) newPos += info.page
|
|
346
|
+
else return 0
|
|
347
|
+
newPos = Math.max(0, Math.min(maxPos, newPos))
|
|
348
|
+
if (bar === gui.ScrollBar.VERT) doScroll(0, newPos - scrollY)
|
|
349
|
+
else doScroll(newPos - scrollX, 0)
|
|
350
|
+
return 0
|
|
351
|
+
}
|
|
352
|
+
if (msg === gui.WmMsg.MOUSEWHEEL) {
|
|
353
|
+
const raw = (wParam >>> 16) & 0xFFFF
|
|
354
|
+
const wheel = raw >= 0x8000 ? raw - 0x10000 : raw
|
|
355
|
+
const isHorz = (wParam & gui.MouseKeyFlag.MK_SHIFT) !== 0
|
|
356
|
+
const bar = isHorz ? gui.ScrollBar.HORZ : gui.ScrollBar.VERT
|
|
357
|
+
const info = gui.GetScrollInfo(hwnd, bar)
|
|
358
|
+
const maxPos = info.max - info.page + 1
|
|
359
|
+
let newPos = (isHorz ? scrollX : scrollY) - Math.round(wheel * 40 / 120)
|
|
360
|
+
newPos = Math.max(0, Math.min(maxPos, newPos))
|
|
361
|
+
if (isHorz) doScroll(newPos - scrollX, 0)
|
|
362
|
+
else doScroll(0, newPos - scrollY)
|
|
363
|
+
return 0
|
|
364
|
+
}
|
|
365
|
+
if (msg === gui.WmMsg.SIZE) { updateScrollRange(); return gui.DefWindowProc(hwnd, msg, wParam, lParam) }
|
|
366
|
+
return gui.DefWindowProc(hwnd, msg, wParam, lParam)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
gui.ShowWindow(hwndMain)
|
|
370
|
+
|
|
371
|
+
const test = std.open('example.pdf', 'rb')
|
|
372
|
+
if (test) { test.close(); showPdf(mupdf, 'example.pdf', 0) }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
main()
|