pdf-h5-react 0.0.0-stage → 0.3.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +455 -2
  3. package/dist/components/AnnotationLayer.d.ts +26 -0
  4. package/dist/components/LinkLayer.d.ts +17 -0
  5. package/dist/components/OutlinePanel.d.ts +8 -0
  6. package/dist/components/PageCanvas.d.ts +41 -0
  7. package/dist/components/PageThumbnail.d.ts +23 -0
  8. package/dist/components/PdfH5Reader.d.ts +101 -0
  9. package/dist/components/SearchPanel.d.ts +18 -0
  10. package/dist/components/StatusOverlay.d.ts +15 -0
  11. package/dist/components/TextLayer.d.ts +26 -0
  12. package/dist/components/ThumbnailPanel.d.ts +18 -0
  13. package/dist/components/Toolbar.d.ts +36 -0
  14. package/dist/core/annotations.d.ts +91 -0
  15. package/dist/core/cache.d.ts +96 -0
  16. package/dist/core/errors.d.ts +12 -0
  17. package/dist/core/eventEmitter.d.ts +43 -0
  18. package/dist/core/gestures.d.ts +71 -0
  19. package/dist/core/layout.d.ts +109 -0
  20. package/dist/core/layoutAdapter.d.ts +36 -0
  21. package/dist/core/lru.d.ts +57 -0
  22. package/dist/core/metrics.d.ts +22 -0
  23. package/dist/core/pdfjs.d.ts +51 -0
  24. package/dist/core/readerEvents.d.ts +116 -0
  25. package/dist/core/renderQueue.d.ts +45 -0
  26. package/dist/core/throttle.d.ts +40 -0
  27. package/dist/core/zoomAnimation.d.ts +33 -0
  28. package/dist/hooks/useElementSize.d.ts +18 -0
  29. package/dist/hooks/usePageMetrics.d.ts +27 -0
  30. package/dist/hooks/usePdfDocument.d.ts +19 -0
  31. package/dist/hooks/usePinchGesture.d.ts +41 -0
  32. package/dist/hooks/useVirtualPages.d.ts +48 -0
  33. package/dist/index.d.ts +46 -0
  34. package/dist/index.js +3226 -0
  35. package/dist/lib/highlight.d.ts +9 -0
  36. package/dist/lib/outline.d.ts +35 -0
  37. package/dist/lib/search.d.ts +38 -0
  38. package/dist/lib/text.d.ts +40 -0
  39. package/dist/lib/thumbnail.d.ts +18 -0
  40. package/dist/pdf-h5-react.css +1 -0
  41. package/dist/types.d.ts +145 -0
  42. package/package.json +84 -4
  43. package/scripts/copy-pdfjs-assets.mjs +100 -0
@@ -0,0 +1,9 @@
1
+ import { SearchHit } from '../types';
2
+ /**
3
+ * 在已渲染的文本层 DOM 上做搜索高亮。
4
+ *
5
+ * 为什么不在 React 里重渲染文本层:文本层由 pdf.js 直接生成(几百个 span),
6
+ * 交给 React 管理反而会破坏其精确定位。这里只对命中所在的 span 插入 <mark>,
7
+ * 并在清理时还原 —— 代价是 O(命中数),与页数无关。
8
+ */
9
+ export declare function highlightTextNodes(root: HTMLElement, hits: readonly SearchHit[], activeIndex: number): void;
@@ -0,0 +1,35 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ export interface OutlineEntry {
3
+ title: string;
4
+ bold: boolean;
5
+ italic: boolean;
6
+ pageNumber: number | null;
7
+ url: string | null;
8
+ depth: number;
9
+ key: string;
10
+ }
11
+ /**
12
+ * 解析 PDF 大纲(书签)。
13
+ *
14
+ * pdf.js 的 `dest` 可能是:
15
+ * - 字符串(命名目标)→ 需要 getDestination 解析;
16
+ * - 数组 [ref | pageIndex, {name: 'XYZ'}, ...];
17
+ * - null(只带 URL 的外部链接)。
18
+ * 这里统一拍平成带页码的线性列表,便于 H5 里做扁平列表 + 缩进。
19
+ */
20
+ export declare function flattenOutline(doc: PDFDocumentProxy): Promise<OutlineEntry[]>;
21
+ /** 供面板复用:加载 + 缓存大纲 */
22
+ export declare function useOutline(doc: PDFDocumentProxy | null): {
23
+ entries: OutlineEntry[];
24
+ loading: boolean;
25
+ };
26
+ /** 刻度尺式进度条:显示已解析页占比,让用户知道「还有多少没测完」 */
27
+ export declare function useOutlineStats(entries: OutlineEntry[]): {
28
+ located: number;
29
+ total: number;
30
+ maxDepth: number;
31
+ };
32
+ /** 面板通用:按 Esc 关闭 + 锁定背景滚动 */
33
+ export declare function usePanelDismiss(onClose: () => void): {
34
+ onBackdrop: (event: React.MouseEvent) => void;
35
+ };
@@ -0,0 +1,38 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ import { SearchHit } from '../types';
3
+ export interface PageText {
4
+ pageNumber: number;
5
+ /** 纯文本(已合并换行) */
6
+ text: string;
7
+ items: Array<{
8
+ str: string;
9
+ start: number;
10
+ }>;
11
+ }
12
+ /**
13
+ * 提取单页文本并记录每个文本项在整页字符串中的起始偏移。
14
+ * 记录偏移是为了把命中位置映射回具体文本项 —— 高亮时能定位到 span。
15
+ */
16
+ export declare function extractPageText(doc: PDFDocumentProxy, pageNumber: number): Promise<PageText>;
17
+ export interface SearchOptions {
18
+ /** 每页最多返回多少条 */
19
+ perPageLimit?: number;
20
+ /** 总量上限 */
21
+ totalLimit?: number;
22
+ /** 每页之间的让出主线程间隔(ms) */
23
+ yieldMs?: number;
24
+ signal?: AbortSignal;
25
+ onProgress?: (scanned: number, total: number) => void;
26
+ }
27
+ /**
28
+ * 全文档搜索。
29
+ *
30
+ * 性能考虑:
31
+ * 1. 逐页扫描,每页之间 await 一次宏任务,避免长任务把滚动卡住;
32
+ * 2. 页文本缓存(同一文档同一页只提取一次),重复搜索几乎零成本;
33
+ * 3. 命中数达到上限即停止,避免「搜 a 得到 8000 条」拖垮渲染;
34
+ * 4. 支持 AbortSignal,用户改关键词时立即中止。
35
+ */
36
+ export declare function searchDocument(doc: PDFDocumentProxy, query: string, options?: SearchOptions): Promise<SearchHit[]>;
37
+ /** 文档切换时清理文本缓存(避免跨文档串味 + 内存泄漏) */
38
+ export declare function clearSearchCache(): void;
@@ -0,0 +1,40 @@
1
+ import { SearchHit } from '../types';
2
+ export interface PageMetricsState {
3
+ /** index 0 = 第 1 页 */
4
+ metrics: Array<{
5
+ width: number;
6
+ height: number;
7
+ rotate: number;
8
+ userUnit: number;
9
+ } | undefined>;
10
+ /** 已经拿到的页数(用于渐进式布局:先按默认尺寸占位,再逐页校正) */
11
+ resolved: number;
12
+ }
13
+ /** 用最小/中位数估计未加载页的尺寸,避免滚动条跳变 */
14
+ export declare function estimateFallback(metrics: ReadonlyArray<{
15
+ width: number;
16
+ height: number;
17
+ } | undefined>, pageCount: number): {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ /** 文本片段:命中处用 \u0000 包裹,渲染时转成 <mark> */
22
+ export declare function makeSnippet(text: string, start: number, end: number, radius?: number): string;
23
+ export declare function splitSnippet(snippet: string): Array<{
24
+ text: string;
25
+ hit: boolean;
26
+ }>;
27
+ /** 在某一页的纯文本里找出全部命中 */
28
+ export declare function findInPage(pageNumber: number, items: ReadonlyArray<{
29
+ str: string;
30
+ start: number;
31
+ }>, joined: string, query: string, limit?: number): SearchHit[];
32
+ export interface OutlineNode {
33
+ title: string;
34
+ bold: boolean;
35
+ italic: boolean;
36
+ /** 解析后的目标页码(1 基),null 表示无法定位 */
37
+ pageNumber: number | null;
38
+ url: string | null;
39
+ children: OutlineNode[];
40
+ }
@@ -0,0 +1,18 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ import { RenderQueue } from '../core/renderQueue';
3
+ export interface ThumbnailResult {
4
+ dataUrl: string;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * 生成缩略图。
10
+ *
11
+ * 用低分辨率(默认 120px 宽)+ JPEG 压缩,单张约 3-8KB,
12
+ * 100 页的缩略图总占用可以控制在 1MB 以内 —— 因此可以放心缓存成 dataURL。
13
+ */
14
+ export declare function createThumbnailCanvas(doc: PDFDocumentProxy, pageNumber: number, options?: {
15
+ targetWidth?: number;
16
+ rotation?: number;
17
+ quality?: number;
18
+ }, queue?: RenderQueue): Promise<ThumbnailResult>;
@@ -0,0 +1 @@
1
+ .pdfh5-root{--pdfh5-bg: #f2f3f5;--pdfh5-page-bg: #ffffff;--pdfh5-text: #1f2329;--pdfh5-text-weak: #646a73;--pdfh5-border: rgba(0, 0, 0, .08);--pdfh5-toolbar-bg: rgba(255, 255, 255, .94);--pdfh5-toolbar-shadow: 0 -1px 0 rgba(0, 0, 0, .06), 0 -8px 24px rgba(0, 0, 0, .06);--pdfh5-accent: #3370ff;--pdfh5-accent-weak: rgba(51, 112, 255, .12);--pdfh5-hit: rgba(255, 196, 0, .42);--pdfh5-hit-active: rgba(255, 122, 0, .6);--pdfh5-shadow: 0 1px 3px rgba(0, 0, 0, .12);position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:0;overflow:hidden;background:var(--pdfh5-bg);color:var(--pdfh5-text);font-family:-apple-system,BlinkMacSystemFont,PingFang SC,Helvetica Neue,Microsoft YaHei,sans-serif;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;touch-action:manipulation}.pdfh5-root.pdfh5-theme-dark{--pdfh5-bg: #14161a;--pdfh5-page-bg: #1e2126;--pdfh5-text: #e6e8eb;--pdfh5-text-weak: #9aa0a6;--pdfh5-border: rgba(255, 255, 255, .1);--pdfh5-toolbar-bg: rgba(28, 31, 36, .96);--pdfh5-toolbar-shadow: 0 -1px 0 rgba(255, 255, 255, .08), 0 -8px 24px rgba(0, 0, 0, .4);--pdfh5-accent: #5b8cff;--pdfh5-accent-weak: rgba(91, 140, 255, .18);--pdfh5-hit: rgba(255, 208, 0, .32);--pdfh5-hit-active: rgba(255, 140, 0, .5);--pdfh5-shadow: 0 1px 3px rgba(0, 0, 0, .5)}.pdfh5-viewport{position:relative;flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y;scrollbar-width:thin}.pdfh5-root[data-mode=page] .pdfh5-viewport{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory}.pdfh5-content{position:relative;transform-origin:0 0;contain:layout size style}.pdfh5-page{position:absolute;top:0;left:0;background:var(--pdfh5-page-bg);box-shadow:var(--pdfh5-shadow);border-radius:2px;overflow:hidden;will-change:transform}.pdfh5-root[data-mode=page] .pdfh5-page{scroll-snap-align:center;scroll-snap-stop:always}.pdfh5-canvas{position:absolute;inset:0;background:var(--pdfh5-page-bg)}.pdfh5-canvas canvas{display:block;width:100%;height:100%}.pdfh5-pageNumber{position:absolute;right:6px;bottom:4px;padding:1px 6px;border-radius:999px;background:#0000000f;color:var(--pdfh5-text-weak);font-size:11px;line-height:1.6;pointer-events:none;user-select:none}.pdfh5-theme-dark .pdfh5-pageNumber{background:#ffffff14}.pdfh5-textLayerHost{position:absolute;inset:0;overflow:hidden;--scale-factor: 1;--total-scale-factor: calc(var(--scale-factor) * var(--user-unit, 1));--scale-round-x: 1px;--scale-round-y: 1px;line-height:1}.pdfh5-textLayerHost .textLayer{position:absolute;inset:0;overflow:clip;opacity:1;text-align:initial;transform-origin:0 0;caret-color:CanvasText;line-height:1;letter-spacing:normal;word-spacing:normal;text-size-adjust:none;forced-color-adjust:none;--min-font-size: 1;--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));--min-font-size-inv: calc(1 / var(--min-font-size))}.pdfh5-textLayerHost .textLayer :is(span,br){color:transparent;position:absolute;white-space:pre;cursor:text;transform-origin:0% 0%;user-select:text}.pdfh5-textLayerHost .textLayer>:not(.markedContent),.pdfh5-textLayerHost .textLayer .markedContent span:not(.markedContent){z-index:1;--font-height: 0;--scale-x: 1;--rotate: 0deg;font-size:calc(var(--text-scale-factor) * var(--font-height));transform:rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv))}.pdfh5-textLayerHost .textLayer .markedContent{display:contents}.pdfh5-textLayerHost .textLayer ::selection{background:#005aff40;color:transparent}.pdfh5-theme-dark .pdfh5-textLayerHost .textLayer ::selection{background:#78aaff59}.pdfh5-linkLayer{position:absolute;inset:0;pointer-events:none}.pdfh5-linkLayer .pdfh5-link{position:absolute;display:block;padding:0;border:0;background:transparent;pointer-events:auto;border-radius:2px;cursor:pointer}.pdfh5-linkLayer .pdfh5-link:active{background:var(--pdfh5-accent-weak)}.pdfh5-hud{position:absolute;right:12px;bottom:calc(64px + env(safe-area-inset-bottom,0px));display:flex;flex-direction:column;align-items:flex-end;gap:4px;padding:5px 10px;border-radius:10px;background:#0000008c;color:#fff;font-size:12px;font-variant-numeric:tabular-nums;pointer-events:none;opacity:.9;transition:opacity .2s ease}.pdfh5-hud[data-gesture]{opacity:.35}.pdfh5-hud-zoom{font-size:11px;opacity:.8}.pdfh5-hud-resolving{opacity:.6}.pdfh5-toolbar{position:relative;z-index:3;display:flex;align-items:center;gap:2px;flex:0 0 auto;padding:6px 8px calc(6px + env(safe-area-inset-bottom,0px));background:var(--pdfh5-toolbar-bg);box-shadow:var(--pdfh5-toolbar-shadow);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.pdfh5-toolbar::-webkit-scrollbar{display:none}.pdfh5-icon-btn{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;min-width:40px;height:40px;padding:0 6px;border:0;border-radius:10px;background:transparent;color:var(--pdfh5-text);font-size:19px;line-height:1;cursor:pointer;transition:background-color .15s ease}.pdfh5-icon-btn:active:not(:disabled){background:var(--pdfh5-accent-weak)}.pdfh5-icon-btn:disabled{opacity:.32;cursor:default}.pdfh5-icon-btn.is-active{background:var(--pdfh5-accent-weak);color:var(--pdfh5-accent)}.pdfh5-page-jump{display:flex;align-items:baseline;gap:2px;flex:0 0 auto;font-variant-numeric:tabular-nums}.pdfh5-page-input{width:46px;height:32px;padding:0 4px;border:1px solid var(--pdfh5-border);border-radius:8px;background:transparent;color:var(--pdfh5-text);font-size:14px;font-variant-numeric:tabular-nums;text-align:center;outline:none}.pdfh5-page-input:focus{border-color:var(--pdfh5-accent);box-shadow:0 0 0 2px var(--pdfh5-accent-weak)}.pdfh5-page-total{color:var(--pdfh5-text-weak);font-size:13px}.pdfh5-zoom-label{flex:0 0 auto;min-width:52px;height:40px;border:0;border-radius:10px;background:transparent;color:var(--pdfh5-text-weak);font-size:13px;font-variant-numeric:tabular-nums;cursor:pointer}.pdfh5-divider{flex:0 0 auto;width:1px;height:20px;margin:0 4px;background:var(--pdfh5-border)}.pdfh5-mode-tag{margin-left:auto;padding:2px 8px;border-radius:999px;background:var(--pdfh5-accent-weak);color:var(--pdfh5-accent);font-size:11px;white-space:nowrap}.pdfh5-panel-backdrop{position:absolute;inset:0;z-index:10;display:flex;background:#00000052;animation:pdfh5-fade-in .16s ease}.pdfh5-panel{display:flex;flex-direction:column;width:min(86%,380px);height:100%;background:var(--pdfh5-page-bg);color:var(--pdfh5-text);box-shadow:2px 0 16px #0000002e;animation:pdfh5-slide-in .2s cubic-bezier(.22,1,.36,1);padding-bottom:env(safe-area-inset-bottom,0px)}.pdfh5-panel-head{display:flex;align-items:center;justify-content:space-between;flex:0 0 auto;padding:10px 8px 10px 16px;border-bottom:1px solid var(--pdfh5-border)}.pdfh5-panel-head h2{margin:0;font-size:15px;font-weight:600}.pdfh5-outline-list,.pdfh5-search-list,.pdfh5-thumb-grid{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}.pdfh5-outline-item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;min-height:44px;padding:10px 16px;border:0;border-bottom:1px solid var(--pdfh5-border);background:transparent;color:var(--pdfh5-text);font-size:14px;text-align:left;cursor:pointer}.pdfh5-outline-item:active:not(:disabled){background:var(--pdfh5-accent-weak)}.pdfh5-outline-item:disabled{opacity:.45}.pdfh5-outline-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pdfh5-outline-page{flex:0 0 auto;color:var(--pdfh5-text-weak);font-size:12px;font-variant-numeric:tabular-nums}.pdfh5-thumb-grid{display:grid;grid-template-columns:repeat(2,1fr);justify-content:center;align-content:start;gap:12px;padding:14px}.pdfh5-thumb{position:relative;display:flex;align-items:center;justify-content:center;padding:0;border:2px solid transparent;border-radius:6px;background:var(--pdfh5-bg);overflow:hidden;cursor:pointer}.pdfh5-thumb.is-current{border-color:var(--pdfh5-accent)}.pdfh5-thumb img{display:block;width:100%;height:100%;object-fit:contain}.pdfh5-thumb-skeleton{display:block;width:100%;height:100%;background:linear-gradient(90deg,transparent,var(--pdfh5-border),transparent);background-size:200% 100%;animation:pdfh5-shimmer 1.2s infinite linear}.pdfh5-thumb-label{position:absolute;right:4px;bottom:2px;padding:0 5px;border-radius:999px;background:#00000080;color:#fff;font-size:10px;line-height:1.6}.pdfh5-search-bar{display:flex;align-items:center;gap:4px;flex:0 0 auto;padding:10px 12px 6px}.pdfh5-search-input{flex:1 1 auto;height:38px;padding:0 12px;border:1px solid var(--pdfh5-border);border-radius:10px;background:var(--pdfh5-bg);color:var(--pdfh5-text);font-size:15px;outline:none}.pdfh5-search-input:focus{border-color:var(--pdfh5-accent)}.pdfh5-search-summary{flex:0 0 auto;margin:0;padding:0 14px 8px;color:var(--pdfh5-text-weak);font-size:12px}.pdfh5-search-item{display:flex;flex-direction:column;gap:3px;width:100%;padding:10px 14px;border:0;border-bottom:1px solid var(--pdfh5-border);background:transparent;color:var(--pdfh5-text);text-align:left;cursor:pointer}.pdfh5-search-item.is-active{background:var(--pdfh5-accent-weak)}.pdfh5-search-page{color:var(--pdfh5-text-weak);font-size:11px;font-variant-numeric:tabular-nums}.pdfh5-search-snippet{font-size:13px;line-height:1.5;word-break:break-word}.pdfh5-search-snippet mark{background:var(--pdfh5-hit);color:inherit;border-radius:2px}.pdfh5-textLayerHost mark.pdfh5-hit{background:var(--pdfh5-hit);color:transparent;border-radius:2px}.pdfh5-textLayerHost mark.pdfh5-hit-active{background:var(--pdfh5-hit-active)}.pdfh5-perf{flex:0 0 auto;padding:10px 14px 14px;border-top:1px solid var(--pdfh5-border);font-size:12px}.pdfh5-perf h3{margin:0 0 6px;font-size:12px;font-weight:600;color:var(--pdfh5-text-weak)}.pdfh5-perf dl{display:grid;grid-template-columns:1fr auto;gap:3px 10px;margin:0 0 8px}.pdfh5-perf dl>div{display:contents}.pdfh5-perf dt{color:var(--pdfh5-text-weak)}.pdfh5-perf dd{margin:0;font-variant-numeric:tabular-nums;text-align:right}.pdfh5-empty{margin:0;padding:24px 16px;color:var(--pdfh5-text-weak);font-size:13px;text-align:center}.pdfh5-status{display:flex;flex:1 1 auto;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:32px 24px;text-align:center}.pdfh5-status-title{margin:0;font-size:15px;font-weight:500}.pdfh5-status-detail{margin:0;max-width:320px;color:var(--pdfh5-text-weak);font-size:13px;word-break:break-word}.pdfh5-spinner{width:28px;height:28px;border:2.5px solid var(--pdfh5-border);border-top-color:var(--pdfh5-accent);border-radius:50%;animation:pdfh5-spin .8s linear infinite}.pdfh5-progress{width:min(220px,60%);height:3px;border-radius:999px;background:var(--pdfh5-border);overflow:hidden}.pdfh5-progress span{display:block;height:100%;background:var(--pdfh5-accent);transition:width .18s ease}.pdfh5-password{display:flex;flex-direction:column;gap:12px;width:min(320px,100%)}.pdfh5-password h2{margin:0;font-size:16px;font-weight:600}.pdfh5-password input{height:44px;padding:0 12px;border:1px solid var(--pdfh5-border);border-radius:10px;background:var(--pdfh5-page-bg);color:var(--pdfh5-text);font-size:16px;outline:none}.pdfh5-password input:focus{border-color:var(--pdfh5-accent)}.pdfh5-password-actions{display:flex;gap:10px;justify-content:flex-end}.pdfh5-btn{min-height:40px;padding:0 18px;border:1px solid var(--pdfh5-border);border-radius:10px;background:var(--pdfh5-page-bg);color:var(--pdfh5-text);font-size:14px;cursor:pointer}.pdfh5-btn-primary{border-color:transparent;background:var(--pdfh5-accent);color:#fff}.pdfh5-btn-primary:disabled{opacity:.5}.pdfh5-btn-ghost{background:transparent}.pdfh5-loadingbar{position:absolute;top:0;left:0;right:0;z-index:20;height:2px;background:transparent;overflow:hidden;pointer-events:none}.pdfh5-loadingbar span{display:block;height:100%;background:var(--pdfh5-accent);transition:width .2s ease}.pdfh5-backtop{position:absolute;right:14px;bottom:calc(72px + env(safe-area-inset-bottom,0px));z-index:4;display:flex;align-items:center;justify-content:center;width:40px;height:40px;padding:0;border:0;border-radius:50%;background:var(--pdfh5-toolbar-bg);color:var(--pdfh5-text-weak);box-shadow:0 2px 10px #00000029;cursor:pointer;animation:pdfh5-pop .18s ease;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.pdfh5-backtop:active{background:var(--pdfh5-accent-weak);color:var(--pdfh5-accent)}.pdfh5-viewport[data-scroll-locked=true]{overflow:hidden;touch-action:none}@keyframes pdfh5-pop{0%{transform:scale(.8);opacity:0}}.pdfh5-root{--pdfh5-anno-hl: #ffd400;--pdfh5-anno-ink: #e5484d}.pdfh5-anno{position:absolute;inset:0;pointer-events:none;z-index:3}.pdfh5-anno-view{position:absolute;inset:0;pointer-events:none}.pdfh5-anno-hl{position:absolute;display:block;border-radius:2px;mix-blend-mode:multiply}.pdfh5-theme-dark .pdfh5-anno-hl{mix-blend-mode:screen}.pdfh5-anno-hl-draft{outline:1px dashed rgba(0,0,0,.35)}.pdfh5-anno-hit{position:absolute;inset:0;pointer-events:none}.pdfh5-anno-text{position:absolute;max-width:60%;padding:2px 4px;border-radius:4px;background:#ffffffdb;line-height:1.35;white-space:pre-wrap;word-break:break-word;box-shadow:0 1px 4px #0000001f;pointer-events:auto;cursor:pointer}.pdfh5-theme-dark .pdfh5-anno-text{background:#1e2126e6}.pdfh5-anno-ink{position:absolute;inset:0;pointer-events:none;overflow:visible}.pdfh5-anno-capture{position:absolute;inset:0;pointer-events:auto;cursor:crosshair;touch-action:none}.pdfh5-anno[data-mode=freetext] .pdfh5-anno-capture{cursor:text}.pdfh5-anno-editor{position:absolute;z-index:10;display:flex;flex-direction:column;gap:6px;width:max(180px,46%);padding:8px;border-radius:10px;background:var(--pdfh5-page-bg);box-shadow:0 6px 24px #00000038;pointer-events:auto;transform:translate(-4px,8px)}.pdfh5-anno-input{width:100%;min-height:52px;padding:6px 8px;border:1px solid var(--pdfh5-border);border-radius:8px;background:var(--pdfh5-bg);color:var(--pdfh5-text);font-size:14px;font-family:inherit;line-height:1.4;resize:vertical;outline:none}.pdfh5-anno-input:focus{border-color:var(--pdfh5-accent)}.pdfh5-anno-actions{display:flex;gap:8px;justify-content:flex-end}.pdfh5-anno-btn{min-height:32px;padding:0 12px;border:1px solid var(--pdfh5-border);border-radius:8px;background:transparent;color:var(--pdfh5-text);font-size:13px;cursor:pointer}.pdfh5-anno-btn-primary{border-color:transparent;background:var(--pdfh5-accent);color:#fff}.pdfh5-anno-menu-wrap{position:relative;flex:0 0 auto}.pdfh5-anno-badge{position:absolute;top:2px;right:2px;min-width:15px;height:15px;padding:0 3px;border-radius:999px;background:var(--pdfh5-accent);color:#fff;font-size:10px;line-height:15px;text-align:center}.pdfh5-anno-menu{position:absolute;bottom:calc(100% + 6px);right:0;z-index:30;display:flex;flex-direction:column;min-width:132px;padding:4px;border-radius:10px;background:var(--pdfh5-toolbar-bg);box-shadow:0 6px 24px #0003;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}.pdfh5-anno-menu button{display:flex;align-items:center;gap:8px;min-height:38px;padding:0 10px;border:0;border-radius:7px;background:transparent;color:var(--pdfh5-text);font-size:13px;text-align:left;white-space:nowrap;cursor:pointer}.pdfh5-anno-menu button:active{background:var(--pdfh5-accent-weak)}.pdfh5-anno-swatch{flex:0 0 auto;width:14px;height:14px;border-radius:3px}.pdfh5-anno-swatch-text{display:flex;align-items:center;justify-content:center;background:var(--pdfh5-accent-weak);color:var(--pdfh5-accent);font-size:10px;font-weight:700}.pdfh5-anno-menu-danger{margin-top:2px;border-top:1px solid var(--pdfh5-border)!important;border-radius:0 0 7px 7px!important;color:#e5484d!important}@keyframes pdfh5-spin{to{transform:rotate(360deg)}}@keyframes pdfh5-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@keyframes pdfh5-fade-in{0%{opacity:0}}@keyframes pdfh5-slide-in{0%{transform:translate(-8%);opacity:.4}}@media(prefers-reduced-motion:reduce){.pdfh5-panel,.pdfh5-panel-backdrop{animation:none}}@media(min-width:768px){.pdfh5-toolbar{justify-content:center}.pdfh5-mode-tag{margin-left:12px}}
@@ -0,0 +1,145 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ import { ReaderEvents, ReaderEventName, ReaderStatus } from './core/readerEvents';
3
+ import { Annotation, AnnotationMode } from './core/annotations';
4
+ /** PDF 源:URL / 字符串 / 二进制 / 自定义参数 */
5
+ export type PdfSource = string | URL | ArrayBuffer | Uint8Array | {
6
+ url?: string | URL;
7
+ data?: ArrayBuffer | Uint8Array;
8
+ httpHeaders?: Record<string, string>;
9
+ withCredentials?: boolean;
10
+ password?: string;
11
+ range?: unknown;
12
+ };
13
+ export type ViewerMode = 'scroll' | 'page';
14
+ export type FitMode = 'width' | 'page' | 'none';
15
+ export type ThemeMode = 'light' | 'dark' | 'auto';
16
+ export interface LoadProgress {
17
+ loaded: number;
18
+ total: number;
19
+ /** 0 - 1;total 为 0 时为 null */
20
+ ratio: number | null;
21
+ }
22
+ /** 单页的原始度量(PDF 点,未乘 userUnit 以外的任何缩放) */
23
+ export interface PageMetric {
24
+ pageNumber: number;
25
+ /** 已乘 userUnit 的宽(PDF 单位 → CSS px @ scale 1) */
26
+ width: number;
27
+ height: number;
28
+ /** PDF 自带的旋转角度 */
29
+ rotate: number;
30
+ userUnit: number;
31
+ }
32
+ export interface CacheStatsSnapshot {
33
+ bitmap: {
34
+ entries: number;
35
+ bytes: number;
36
+ maxBytes: number;
37
+ hits: number;
38
+ misses: number;
39
+ evictions: number;
40
+ };
41
+ text: {
42
+ entries: number;
43
+ bytes: number;
44
+ maxBytes: number;
45
+ hits: number;
46
+ misses: number;
47
+ evictions: number;
48
+ };
49
+ page: {
50
+ entries: number;
51
+ bytes: number;
52
+ maxBytes: number;
53
+ hits: number;
54
+ misses: number;
55
+ evictions: number;
56
+ };
57
+ thumbnail: {
58
+ entries: number;
59
+ bytes: number;
60
+ maxBytes: number;
61
+ hits: number;
62
+ misses: number;
63
+ evictions: number;
64
+ };
65
+ metrics: {
66
+ entries: number;
67
+ };
68
+ renders: {
69
+ started: number;
70
+ completed: number;
71
+ cancelled: number;
72
+ cacheHits: number;
73
+ cacheMisses: number;
74
+ };
75
+ /** 高频事件节流效果:产生的事件数 vs 实际处理数 */
76
+ throttle: {
77
+ scrollEvents: number;
78
+ scrollHandled: number;
79
+ sizeEvents: number;
80
+ sizeHandled: number;
81
+ };
82
+ }
83
+ export interface PdfH5ReaderHandle {
84
+ /** 当前文档代理(未加载完成时为 null) */
85
+ getDocument(): PDFDocumentProxy | null;
86
+ getCurrentPage(): number;
87
+ goToPage(page: number, options?: {
88
+ smooth?: boolean;
89
+ }): void;
90
+ nextPage(): void;
91
+ prevPage(): void;
92
+ setZoom(zoom: number, anchor?: {
93
+ x: number;
94
+ y: number;
95
+ }): void;
96
+ zoomIn(step?: number): void;
97
+ zoomOut(step?: number): void;
98
+ resetZoom(): void;
99
+ search(query: string): Promise<SearchHit[]>;
100
+ clearSearch(): void;
101
+ getCacheStats(): CacheStatsSnapshot;
102
+ clearCaches(): void;
103
+ /** 打开 / 关闭内置面板 */
104
+ openPanel(panel: 'outline' | 'thumbnails' | 'search' | null): void;
105
+ /**
106
+ * 监听事件,返回取消订阅函数。
107
+ *
108
+ * ```ts
109
+ * const off = ref.current.on('pageChange', ({ page, total }) => setPage(page));
110
+ * off(); // 取消
111
+ * ```
112
+ */
113
+ on<K extends ReaderEventName>(type: K, listener: (payload: ReaderEvents[K]) => void): () => void;
114
+ /** 只监听一次 */
115
+ once<K extends ReaderEventName>(type: K, listener: (payload: ReaderEvents[K]) => void): () => void;
116
+ /** 移除监听;不传 listener 时清空该事件 */
117
+ off<K extends ReaderEventName>(type: K, listener?: (payload: ReaderEvents[K]) => void): void;
118
+ /** 手动触发事件(调试 / 业务自定义事件透传) */
119
+ trigger<K extends ReaderEventName>(type: K, payload: ReaderEvents[K]): void;
120
+ /** 运行状态快照 */
121
+ getStatus(): ReaderStatus;
122
+ /** 当前全部批注 */
123
+ getAnnotations(): Annotation[];
124
+ /** 追加一条批注 */
125
+ addAnnotation(annotation: Annotation): void;
126
+ /** 按 id 删除 */
127
+ removeAnnotation(id: string): void;
128
+ /** 清空全部批注 */
129
+ clearAnnotations(): void;
130
+ /** 切换批注工具;'none' 表示退出批注模式 */
131
+ setAnnotationMode(mode: AnnotationMode): void;
132
+ /** 当前批注模式 */
133
+ getAnnotationMode(): AnnotationMode;
134
+ }
135
+ export interface SearchHit {
136
+ pageNumber: number;
137
+ /** 匹配到的纯文本 */
138
+ text: string;
139
+ /** 所在文本项的下标 */
140
+ itemIndex: number;
141
+ start: number;
142
+ end: number;
143
+ /** 上下文片段,命中处用 \u0000 包裹 */
144
+ snippet: string;
145
+ }
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "pdf-h5-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.1",
4
+ "description": "基于 pdf.js 的移动端优先 React PDF 阅读器组件(连续滚动 / 翻页 / 捏合缩放 / 文本层 / 搜索 / 缩略图 / 目录)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "superjunjie",
8
+ "homepage": "https://github.com/superjunjie/pdf-h5-react#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/superjunjie/pdf-h5-react.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/superjunjie/pdf-h5-react/issues"
15
+ },
16
+ "keywords": [
17
+ "pdf",
18
+ "pdfjs",
19
+ "pdf.js",
20
+ "react",
21
+ "h5",
22
+ "mobile",
23
+ "viewer",
24
+ "reader",
25
+ "pdf-viewer",
26
+ "react-pdf",
27
+ "mobile-pdf"
28
+ ],
29
+ "files": [
30
+ "dist",
31
+ "scripts/copy-pdfjs-assets.mjs",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "main": "./dist/index.js",
36
+ "module": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "import": "./dist/index.js"
42
+ },
43
+ "./style.css": "./dist/pdf-h5-react.css",
44
+ "./package.json": "./package.json"
45
+ },
46
+ "sideEffects": [
47
+ "**/*.css"
48
+ ],
49
+ "publishConfig": {
50
+ "registry": "https://registry.npmjs.org",
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "dev": "vite",
55
+ "build": "vite build --config vite.lib.config.ts && node scripts/clean-dist.mjs",
56
+ "build:demo": "vite build",
57
+ "preview": "vite preview",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "assets": "node scripts/copy-pdfjs-assets.mjs",
61
+ "samples": "node scripts/make-samples.mjs",
62
+ "verify": "node scripts/verify.mjs",
63
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
64
+ },
65
+ "peerDependencies": {
66
+ "pdfjs-dist": ">=5.0.0",
67
+ "react": ">=18.0.0",
68
+ "react-dom": ">=18.0.0"
69
+ },
70
+ "devDependencies": {
71
+ "@pdf-lib/fontkit": "^1.1.1",
72
+ "@types/node": "^24.19.2",
73
+ "@types/react": "^19.3.0",
74
+ "@types/react-dom": "^19.3.0",
75
+ "@vitejs/plugin-react": "^5.2.0",
76
+ "pdf-lib": "^1.17.1",
77
+ "pdfjs-dist": "6.5.438",
78
+ "puppeteer-core": "^25.17.0",
79
+ "react": "^19.3.1",
80
+ "react-dom": "^19.3.1",
81
+ "typescript": "^5.9.3",
82
+ "vite": "^7.3.8",
83
+ "vite-plugin-dts": "^4.5.4",
84
+ "vitest": "^4.1.12"
85
+ }
86
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 把 pdf.js 的运行时资源(worker / cmaps / 字体 / wasm / icc)拷贝到 public/pdfjs/,
3
+ * 用于「自托管」部署,避免依赖公网 CDN。
4
+ *
5
+ * 用法:
6
+ * node scripts/copy-pdfjs-assets.mjs # 拷到 public/pdfjs/
7
+ * node scripts/copy-pdfjs-assets.mjs out/assets # 拷到指定目录
8
+ *
9
+ * 配合:
10
+ * configurePdfjs({
11
+ * workerSrc: '/pdfjs/build/pdf.worker.min.mjs',
12
+ * cMapUrl: '/pdfjs/cmaps/',
13
+ * standardFontDataUrl: '/pdfjs/standard_fonts/',
14
+ * wasmUrl: '/pdfjs/wasm/',
15
+ * iccUrl: '/pdfjs/iccs/',
16
+ * })
17
+ */
18
+ import { cp, mkdir, readdir, stat, writeFile } from 'node:fs/promises';
19
+ import { existsSync } from 'node:fs';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { createRequire } from 'node:module';
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const root = resolve(here, '..');
26
+ const require = createRequire(import.meta.url);
27
+
28
+ const outDir = resolve(root, process.argv[2] ?? 'public/pdfjs');
29
+
30
+ function locatePdfjs() {
31
+ // 优先从 node_modules 解析,兼容 monorepo / pnpm 的符号链接
32
+ try {
33
+ return dirname(require.resolve('pdfjs-dist/package.json'));
34
+ } catch {
35
+ const fallback = join(root, 'node_modules/pdfjs-dist');
36
+ if (existsSync(fallback)) return fallback;
37
+ throw new Error('未找到 pdfjs-dist,请先 npm install');
38
+ }
39
+ }
40
+
41
+ async function sizeOf(path) {
42
+ let total = 0;
43
+ let files = 0;
44
+ const info = await stat(path);
45
+ if (info.isFile()) return { total: info.size, files: 1 };
46
+ const walk = async (dir) => {
47
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
48
+ const full = join(dir, entry.name);
49
+ if (entry.isDirectory()) await walk(full);
50
+ else {
51
+ total += (await stat(full)).size;
52
+ files += 1;
53
+ }
54
+ }
55
+ };
56
+ await walk(path);
57
+ return { total, files };
58
+ }
59
+
60
+ const pdfjsDir = locatePdfjs();
61
+ console.log(`pdfjs-dist: ${pdfjsDir}`);
62
+ console.log(`输出目录 : ${outDir}`);
63
+
64
+ await mkdir(outDir, { recursive: true });
65
+
66
+ // 只拷运行时真正需要的部分:
67
+ // - build/pdf.worker.min.mjs : worker 本体(约 1MB)
68
+ // - cmaps/ : 中日韩等 CJK 字体的预定义编码表
69
+ // - standard_fonts/ : 14 种标准字体(未内嵌字体的 PDF 需要)
70
+ // - wasm/ : JBIG2 / JPEG2000 / QCMS 解码器
71
+ // - iccs/ : ICC 色彩配置
72
+ const targets = [
73
+ ['build/pdf.worker.min.mjs', 'build/pdf.worker.min.mjs'],
74
+ ['cmaps', 'cmaps'],
75
+ ['standard_fonts', 'standard_fonts'],
76
+ ['wasm', 'wasm'],
77
+ ['iccs', 'iccs'],
78
+ ];
79
+
80
+ for (const [from, to] of targets) {
81
+ const src = join(pdfjsDir, from);
82
+ const dest = join(outDir, to);
83
+ if (!existsSync(src)) {
84
+ console.warn(`跳过(不存在): ${from}`);
85
+ continue;
86
+ }
87
+ await mkdir(dirname(dest), { recursive: true });
88
+ await cp(src, dest, { recursive: true });
89
+ const { total, files } = await sizeOf(dest);
90
+ console.log(`✓ ${to.padEnd(28)} ${files} 个文件 ${(total / 1024).toFixed(0)} KB`);
91
+ }
92
+
93
+ // 写一份版本说明,便于排查「库升级了但静态资源没更新」的问题
94
+ const pkg = require('pdfjs-dist/package.json');
95
+ await writeFile(
96
+ join(outDir, 'VERSION'),
97
+ `pdfjs-dist@${pkg.version}\nGenerated by scripts/copy-pdfjs-assets.mjs\n`,
98
+ 'utf8',
99
+ );
100
+ console.log(`\n完成。pdfjs-dist@${pkg.version} 资源已就绪:${outDir}`);