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,101 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ import { ZoomEventPayload } from '../core/readerEvents';
3
+ import { Annotation, AnnotationStyle } from '../core/annotations';
4
+ import { PdfH5ReaderHandle, PdfSource, ThemeMode, ViewerMode, FitMode } from '../types';
5
+ export interface PdfH5ReaderProps {
6
+ /** PDF 来源:URL / 二进制 / 参数对象 */
7
+ src: PdfSource | null | undefined;
8
+ /** 连续滚动 / 单页翻页 */
9
+ mode?: ViewerMode;
10
+ /** 适配方式,默认 width */
11
+ fitMode?: FitMode;
12
+ initialPage?: number;
13
+ /** 初始缩放倍率(相对适配尺寸),默认 1 */
14
+ initialZoom?: number;
15
+ minZoom?: number;
16
+ maxZoom?: number;
17
+ /** 双击 / 双指点击的放大目标倍率,默认 2.5 */
18
+ doubleTapZoom?: number;
19
+ /** 缩放动画时长(ms),默认 300;设为 0 立即跳变 */
20
+ animationDuration?: number;
21
+ /** 是否允许缩放手势,默认 true */
22
+ zoomEnable?: boolean;
23
+ /** 是否允许滚动,默认 true */
24
+ scrollEnable?: boolean;
25
+ rotation?: number;
26
+ theme?: ThemeMode;
27
+ gap?: number;
28
+ padding?: number;
29
+ /** 文本层(可选中 / 可复制),默认开启 */
30
+ textLayer?: boolean;
31
+ /** 链接层,默认开启 */
32
+ links?: boolean;
33
+ /** 是否显示内置工具栏,默认 true */
34
+ toolbar?: boolean;
35
+ /** 是否显示页码 / 缩放浮层,默认 true */
36
+ showControls?: boolean;
37
+ /** 是否显示「回到顶部」悬浮按钮,默认 true */
38
+ backTop?: boolean;
39
+ /** 是否显示顶部加载进度条,默认 true */
40
+ loadingBar?: boolean;
41
+ /** 是否启用注释功能(工具栏出现批注按钮),默认 false */
42
+ annotation?: boolean;
43
+ /** 受控注释列表;不传则由组件内部维护 */
44
+ annotations?: Annotation[];
45
+ /** 注释样式覆盖 */
46
+ annotationStyle?: Partial<AnnotationStyle>;
47
+ /** 注释变化回调(新增 / 删除 / 清空) */
48
+ onAnnotationsChange?: (annotations: Annotation[]) => void;
49
+ /** 位图缓存上限(字节),默认 96MB */
50
+ bitmapCacheBytes?: number;
51
+ /** 文本层缓存条目上限,默认 24 */
52
+ textLayerCacheEntries?: number;
53
+ maxCanvasPixels?: number;
54
+ maxCanvasDimension?: number;
55
+ className?: string;
56
+ style?: React.CSSProperties;
57
+ onDocumentLoad?: (doc: PDFDocumentProxy) => void;
58
+ onPageChange?: (page: number, total: number) => void;
59
+ onProgress?: (loaded: number, total: number) => void;
60
+ onError?: (error: Error) => void;
61
+ onZoomChange?: (zoom: number) => void;
62
+ onInternalLink?: (dest: string | unknown[] | null) => void;
63
+ onExternalLink?: (url: string) => void;
64
+ /** 自定义空状态 / 加载失败渲染 */
65
+ renderError?: (error: Error, retry: () => void) => React.ReactNode;
66
+ /** 文档就绪,可拿到总页数 */
67
+ onReady?: (payload: {
68
+ doc: PDFDocumentProxy;
69
+ total: number;
70
+ }) => void;
71
+ /** 某一页渲染完成 */
72
+ onRender?: (payload: {
73
+ pageNumber: number;
74
+ pageTime: number;
75
+ totalTime: number;
76
+ }) => void;
77
+ /** 滚动(已节流) */
78
+ onScroll?: (payload: {
79
+ scrollTop: number;
80
+ scrollLeft: number;
81
+ currentPage: number;
82
+ progress: number;
83
+ }) => void;
84
+ /** 缩放结束 */
85
+ onZoomEnd?: (payload: ZoomEventPayload) => void;
86
+ /** 加载流程结束(成功或失败都会触发) */
87
+ onComplete?: (payload: {
88
+ status: 'success' | 'error';
89
+ message: string;
90
+ time: number;
91
+ }) => void;
92
+ }
93
+ /**
94
+ * 移动端优先的 PDF 阅读器。
95
+ *
96
+ * 三层性能结构:
97
+ * 文档层 usePdfDocument —— 加载 / 密码 / 进度(进度事件节流)
98
+ * 度量层 usePageMetrics —— 首屏 N 页真实尺寸 + 其余页渐进解析(rAF 批量校正)
99
+ * 视图层 useVirtualPages —— 只挂载可视区内的页面 DOM,位图走 LRU,渲染走双并发队列
100
+ */
101
+ export declare const PdfH5Reader: import('react').ForwardRefExoticComponent<PdfH5ReaderProps & import('react').RefAttributes<PdfH5ReaderHandle>>;
@@ -0,0 +1,18 @@
1
+ import { SearchHit, CacheStatsSnapshot } from '../types';
2
+ export interface SearchPanelProps {
3
+ hits: readonly SearchHit[];
4
+ index: number;
5
+ onSearch(query: string): Promise<SearchHit[]>;
6
+ onSelect(index: number): void;
7
+ onClose(): void;
8
+ /** 缓存统计(用于「性能」诊断区) */
9
+ stats?: CacheStatsSnapshot | null;
10
+ onClearCaches?(): void;
11
+ }
12
+ /**
13
+ * 搜索面板。
14
+ *
15
+ * 输入用防抖(260ms)避免逐字触发全文档扫描;
16
+ * 扫描本身在 lib/search 里逐页让出主线程,不会阻塞滚动。
17
+ */
18
+ export declare function SearchPanel({ hits, index, onSearch, onSelect, onClose, stats, onClearCaches }: SearchPanelProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ export interface StatusOverlayProps {
2
+ loading: boolean;
3
+ needPassword: boolean;
4
+ passwordReason: number;
5
+ error: Error | null;
6
+ progress: {
7
+ loaded: number;
8
+ total: number;
9
+ } | null;
10
+ onSubmitPassword(password: string): void;
11
+ onCancelPassword(): void;
12
+ renderError?: (error: Error, retry: () => void) => React.ReactNode;
13
+ }
14
+ /** 加载 / 密码 / 错误三态覆盖层 */
15
+ export declare function StatusOverlay({ loading, needPassword, passwordReason, error, progress, onSubmitPassword, onCancelPassword, renderError, }: StatusOverlayProps): import("react").JSX.Element | null;
@@ -0,0 +1,26 @@
1
+ import { PDFPageProxy } from 'pdfjs-dist';
2
+ import { TextLayerCache } from '../core/cache';
3
+ import { RenderQueue } from '../core/renderQueue';
4
+ export interface TextLayerProps {
5
+ page: PDFPageProxy | null;
6
+ pageNumber: number;
7
+ /** 与画布一致的显示尺寸 */
8
+ width: number;
9
+ height: number;
10
+ displayScale: number;
11
+ rotation: number;
12
+ textLayerCache: TextLayerCache;
13
+ renderQueue: RenderQueue;
14
+ /** 关闭时完全不构建文本层(省内存,代价是不可选中/搜索仍可用) */
15
+ enabled: boolean;
16
+ shouldBuild: boolean;
17
+ }
18
+ /**
19
+ * 文本层。
20
+ *
21
+ * 性能要点:
22
+ * 1. 文本层构建成本高(几百个 span + 字体测量),按页缓存在 LRU 里;
23
+ * 2. 缩放时优先走 TextLayer.update({ viewport }) 重排,而不是重建 DOM;
24
+ * 3. 一旦缓存里没有可用节点,才重新 streamTextContent 构建。
25
+ */
26
+ export declare function TextLayer({ page, pageNumber, width, height, displayScale, rotation, textLayerCache, renderQueue, enabled, shouldBuild, }: TextLayerProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { PDFDocumentProxy } from 'pdfjs-dist';
2
+ import { ThumbnailCache } from '../core/cache';
3
+ export interface ThumbnailPanelProps {
4
+ doc: PDFDocumentProxy;
5
+ cache: ThumbnailCache;
6
+ currentPage: number;
7
+ rotation: number;
8
+ onSelect(pageNumber: number): void;
9
+ onClose(): void;
10
+ }
11
+ /**
12
+ * 缩略图面板。
13
+ *
14
+ * 上百张缩略图不能一次性渲染:按容器滚动位置算出「可见行区间」,
15
+ * 只渲染该区间(±2 屏),配合 ThumbnailCache,回滚时命中 dataURL 零成本。
16
+ * 这里用位置区间而不是 IntersectionObserver —— 网格均质,算区间更稳且无观察者开销。
17
+ */
18
+ export declare function ThumbnailPanel({ doc, cache, currentPage, rotation, onSelect, onClose }: ThumbnailPanelProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ export type PanelKind = 'outline' | 'thumbnails' | 'search' | null;
2
+ export interface ToolbarProps {
3
+ mode: 'scroll' | 'page';
4
+ page: number;
5
+ total: number;
6
+ zoom: number;
7
+ minZoom: number;
8
+ maxZoom: number;
9
+ panel: PanelKind;
10
+ panelSupported: Record<'search' | 'outline' | 'thumbnails', boolean>;
11
+ /** 是否显示批注按钮 */
12
+ annotation?: boolean;
13
+ /** 当前批注模式 */
14
+ annotationMode?: 'none' | 'highlight' | 'freetext' | 'ink';
15
+ /** 当前页的批注数量(显示角标) */
16
+ annotationCount?: number;
17
+ onPrev(): void;
18
+ onNext(): void;
19
+ onJump(page: number): void;
20
+ onZoomIn(): void;
21
+ onZoomOut(): void;
22
+ onResetZoom(): void;
23
+ onTogglePanel(panel: PanelKind): void;
24
+ onLocate(): void;
25
+ onToggleAnnotation?(mode: 'highlight' | 'freetext' | 'ink'): void;
26
+ onClearAnnotations?(): void;
27
+ }
28
+ /**
29
+ * 底部工具栏(移动端优先)。
30
+ *
31
+ * 交互细节:
32
+ * - 页码输入框在移动端用 inputMode="numeric",聚焦时自动全选,输入完回车跳转;
33
+ * - 工具栏可折叠:滚动阅读时点中间区域收起,把屏幕让给正文;
34
+ * - 所有按钮 ≥ 40px 命中区,符合手指操作。
35
+ */
36
+ export declare function Toolbar({ mode, page, total, zoom, minZoom, maxZoom, panel, panelSupported, annotation, annotationMode, annotationCount, onPrev, onNext, onJump, onZoomIn, onZoomOut, onResetZoom, onTogglePanel, onLocate, onToggleAnnotation, onClearAnnotations, }: ToolbarProps): import("react").JSX.Element;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * 注释数据模型与坐标换算。
3
+ *
4
+ * 设计要点(这是注释功能最容易做错的地方):
5
+ *
6
+ * 1. **存储用归一化坐标**(0-1,相对页面宽高),而不是 CSS 像素。
7
+ * 否则缩放 / 旋转屏幕 / 切换 fitMode 之后,所有注释都会错位。
8
+ *
9
+ * 2. **不写回 PDF 字节**(默认)。把注释存在独立的数据层,导出时才考虑落盘。
10
+ * 改 PDF 需要 pdf-lib 之类的重量级依赖,且会破坏原文件的签名与增量更新语义。
11
+ *
12
+ * 3. 坐标以「页面显示盒」为参照系(即 .pdfh5-page 的左上角),
13
+ * 与 viewport 的旋转无关 —— 旋转在渲染层处理,注释层始终贴合最终显示区域。
14
+ */
15
+ /** 归一化点:x/y ∈ [0, 1],相对页面显示区域的左上角 */
16
+ export interface NormPoint {
17
+ x: number;
18
+ y: number;
19
+ }
20
+ /** 归一化矩形 */
21
+ export interface NormRect {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export type AnnotationKind = 'highlight' | 'freetext' | 'ink';
28
+ interface AnnotationBase {
29
+ id: string;
30
+ kind: AnnotationKind;
31
+ pageNumber: number;
32
+ /** 创建时间戳(ms) */
33
+ createdAt: number;
34
+ /** 作者标记,便于多人协作时区分 */
35
+ author?: string;
36
+ /** 任意业务附加数据(不会渲染) */
37
+ meta?: Record<string, unknown>;
38
+ }
39
+ /** 文本高亮:一个或多个矩形(跨行时会有多段) */
40
+ export interface HighlightAnnotation extends AnnotationBase {
41
+ kind: 'highlight';
42
+ rects: NormRect[];
43
+ /** 高亮到的文字(便于做批注列表 / 导出) */
44
+ text?: string;
45
+ color: string;
46
+ opacity: number;
47
+ }
48
+ /** 自由文本:一个锚点 + 字号(归一化到页面宽度,保证缩放后视觉比例一致) */
49
+ export interface FreetextAnnotation extends AnnotationBase {
50
+ kind: 'freetext';
51
+ anchor: NormPoint;
52
+ text: string;
53
+ /** 字号相对页面宽度的比例(例如 0.02 表示页面宽度的 2%) */
54
+ fontSize: number;
55
+ color: string;
56
+ }
57
+ /** 手写笔迹:归一化点序列 */
58
+ export interface InkAnnotation extends AnnotationBase {
59
+ kind: 'ink';
60
+ points: NormPoint[];
61
+ /** 线宽相对页面宽度的比例 */
62
+ thickness: number;
63
+ color: string;
64
+ }
65
+ export type Annotation = HighlightAnnotation | FreetextAnnotation | InkAnnotation;
66
+ export type AnnotationMode = 'none' | 'highlight' | 'freetext' | 'ink';
67
+ export interface AnnotationStyle {
68
+ highlightColor: string;
69
+ highlightOpacity: number;
70
+ textColor: string;
71
+ inkColor: string;
72
+ /** 相对页面宽度的比例 */
73
+ fontSize: number;
74
+ thickness: number;
75
+ }
76
+ export declare const DEFAULT_ANNOTATION_STYLE: AnnotationStyle;
77
+ /** 视口像素(相对页面盒左上角)→ 归一化 */
78
+ export declare function toNormalized(px: number, py: number, boxWidth: number, boxHeight: number): NormPoint;
79
+ /** 归一化 → 视口像素 */
80
+ export declare function toPixel(point: NormPoint, boxWidth: number, boxHeight: number): NormPoint;
81
+ export declare function rectToPixel(rect: NormRect, boxWidth: number, boxHeight: number): NormRect;
82
+ /** 由两个对角点构造归一化矩形(自动处理反向拖拽) */
83
+ export declare function rectFromPoints(a: NormPoint, b: NormPoint): NormRect;
84
+ export declare function createAnnotationId(prefix?: string): string;
85
+ /** 判断注释是否「有效」:空高亮、空文本、单击点都不入库 */
86
+ export declare function isAnnotationValid(annotation: Annotation): boolean;
87
+ /** 归一化坐标是否落在页面内(含少量容差) */
88
+ export declare function isPointInPage(point: NormPoint, tolerance?: number): boolean;
89
+ /** 按页分组,便于渲染时 O(1) 取用 */
90
+ export declare function groupByPage(annotations: readonly Annotation[]): Map<number, Annotation[]>;
91
+ export {};
@@ -0,0 +1,96 @@
1
+ import { PDFPageProxy } from 'pdfjs-dist';
2
+ import { LruCacheStats } from './lru';
3
+ export interface SizedCanvas {
4
+ canvas: HTMLCanvasElement | OffscreenCanvas;
5
+ width: number;
6
+ height: number;
7
+ bytes: number;
8
+ }
9
+ export declare function createCacheCanvas(width: number, height: number): HTMLCanvasElement;
10
+ export declare function disposeCanvas(canvas: HTMLCanvasElement | OffscreenCanvas | null | undefined): void;
11
+ export interface BitmapEntry extends SizedCanvas {
12
+ key: string;
13
+ pageNumber: number;
14
+ scale: number;
15
+ canvas: HTMLCanvasElement;
16
+ }
17
+ export interface BitmapCacheOptions {
18
+ maxBytes?: number;
19
+ onEvict?: (entry: BitmapEntry) => void;
20
+ }
21
+ /**
22
+ * 位图缓存。key = `${pageNumber}@${scaleBucket}`。
23
+ * 缩放时量化到固定档位,来回捏合不会反复触发渲染。
24
+ */
25
+ export declare class BitmapCache {
26
+ private readonly cache;
27
+ constructor(options?: BitmapCacheOptions);
28
+ static key(pageNumber: number, scale: number): string;
29
+ get(pageNumber: number, scale: number): BitmapEntry | undefined;
30
+ take(pageNumber: number, scale: number): BitmapEntry | undefined;
31
+ release(entry: BitmapEntry | undefined): void;
32
+ put(pageNumber: number, scale: number, canvas: HTMLCanvasElement): BitmapEntry;
33
+ has(pageNumber: number, scale: number): boolean;
34
+ /** 丢掉某一页的所有档位(例如页面被移出远区) */
35
+ dropPage(pageNumber: number): void;
36
+ clear(): void;
37
+ stats(): LruCacheStats;
38
+ }
39
+ export interface TextLayerEntry {
40
+ pageNumber: number;
41
+ /** 已构建好的 .textLayer 节点,可 detach / re-attach */
42
+ node: HTMLDivElement;
43
+ scale: number;
44
+ viewportScale: number;
45
+ bytes: number;
46
+ /** pdf.js TextLayer 实例,用于 update({viewport}) */
47
+ update: (viewport: unknown) => void;
48
+ destroy: () => void;
49
+ }
50
+ export interface TextLayerCacheOptions {
51
+ maxEntries?: number;
52
+ maxBytes?: number;
53
+ }
54
+ /**
55
+ * 文本层 DOM 缓存。构建一次成本高(几百个 span + 字体测量),
56
+ * 滚动离开视口后保留,回来后直接挂回 DOM。
57
+ */
58
+ export declare class TextLayerCache {
59
+ private readonly cache;
60
+ constructor(options?: TextLayerCacheOptions);
61
+ get(pageNumber: number): TextLayerEntry | undefined;
62
+ take(pageNumber: number): TextLayerEntry | undefined;
63
+ release(pageNumber: number): void;
64
+ put(entry: TextLayerEntry): void;
65
+ delete(pageNumber: number): void;
66
+ clear(): void;
67
+ stats(): LruCacheStats;
68
+ }
69
+ /** 页面代理缓存:避免反复调用 doc.getPage() */
70
+ export declare class PageProxyCache {
71
+ private readonly cache;
72
+ private readonly inflight;
73
+ private doc;
74
+ constructor(doc: {
75
+ getPage(n: number): Promise<PDFPageProxy>;
76
+ } | null, maxEntries?: number);
77
+ /** 同步读取(命中即返回,不发起请求) */
78
+ peek(pageNumber: number): PDFPageProxy | undefined;
79
+ get(pageNumber: number): Promise<PDFPageProxy>;
80
+ clear(): void;
81
+ stats(): LruCacheStats;
82
+ }
83
+ export interface ThumbnailEntry {
84
+ pageNumber: number;
85
+ dataUrl: string;
86
+ bytes: number;
87
+ }
88
+ /** 缩略图缓存(dataURL,体积小但数量多) */
89
+ export declare class ThumbnailCache {
90
+ private readonly cache;
91
+ constructor(maxEntries?: number);
92
+ get(pageNumber: number): ThumbnailEntry | undefined;
93
+ put(pageNumber: number, dataUrl: string): void;
94
+ clear(): void;
95
+ stats(): LruCacheStats;
96
+ }
@@ -0,0 +1,12 @@
1
+ export declare class PdfH5Error extends Error {
2
+ readonly cause?: unknown | undefined;
3
+ readonly code: 'load' | 'password' | 'render' | 'unknown';
4
+ readonly name = "PdfH5Error";
5
+ constructor(message: string, cause?: unknown | undefined, code?: 'load' | 'password' | 'render' | 'unknown');
6
+ }
7
+ /** 渲染任务被主动取消(离开视口 / 缩放变化),不应作为错误上报 */
8
+ export declare class RenderCancelledError extends Error {
9
+ readonly name = "RenderCancelledError";
10
+ constructor(message?: string);
11
+ }
12
+ export declare function isCancelledError(error: unknown): boolean;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 轻量事件总线。
3
+ *
4
+ * 设计参考 pdfh5 的 `on / off / trigger`:命令式 API 对「非 React 心智」的使用者更自然,
5
+ * 也便于在 ref 上挂载(无需把每个事件都写成一个 prop)。
6
+ * 同时保留 React 的 props 回调:两种风格各自可用,互不干扰。
7
+ *
8
+ * 与 pdfh5 的差异(都是踩过坑之后的选择):
9
+ * 1. 回调抛错不会中断其它监听者 —— 一个监听器写错不该让整个阅读器的事件链断掉;
10
+ * 2. `off` 支持只移除指定回调(pdfh5 只能按事件名全清,容易误伤);
11
+ * 3. 提供 `once`,且事件分发时对监听器数组做快照,避免回调里增删监听导致漏发。
12
+ */
13
+ export type Listener<T> = (payload: T) => void;
14
+ /**
15
+ * 事件表约束。
16
+ *
17
+ * 用 `Record<PropertyKey, unknown>` 而不是 `Record<string, unknown>`:
18
+ * 后者的索引签名隐含「所有 string key 都得有值」,与 `{ destroy: {} }` 这类
19
+ * 具体对象类型不兼容(会报 Type 'X' does not satisfy Record<string, unknown>)。
20
+ */
21
+ export declare class EventEmitter<Events extends object> {
22
+ private readonly listeners;
23
+ /** 注册监听,返回取消订阅函数 */
24
+ on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void;
25
+ /** 只触发一次 */
26
+ once<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void;
27
+ /**
28
+ * 移除监听。
29
+ * 不传 listener 时清空该事件的全部监听(兼容 pdfh5 的用法)。
30
+ */
31
+ off<K extends keyof Events>(type: K, listener?: Listener<Events[K]>): void;
32
+ /**
33
+ * 触发事件。
34
+ *
35
+ * 对数组做快照再遍历:回调内部调用 `off` 不会影响本轮分发(否则会漏发后续监听者)。
36
+ * 单个监听器抛错时打印告警并继续 —— 避免一个写错的回调导致别的功能静默失效。
37
+ */
38
+ emit<K extends keyof Events>(type: K, payload: Events[K]): void;
39
+ /** 某个事件当前的监听数量(调试用) */
40
+ listenerCount(type: keyof Events): number;
41
+ /** 清空全部监听(组件卸载时调用) */
42
+ clear(): void;
43
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 捏合 / 双击 / 滚轮缩放的纯数学部分(可单测,不碰 DOM)。
3
+ *
4
+ * 关键点:内容(页面 + 间距 + 内边距)对 zoom 严格线性,
5
+ * 因此捏合过程中可以用 CSS transform 做「预演」,松手后一次性提交真实 zoom,
6
+ * 两者在同一坐标系下等价,不会出现跳变。
7
+ */
8
+ export interface Point {
9
+ x: number;
10
+ y: number;
11
+ }
12
+ export interface ViewportMetrics {
13
+ /** 容器左上角在视口中的位置(getBoundingClientRect) */
14
+ left: number;
15
+ top: number;
16
+ /** 当前滚动位置 */
17
+ scrollLeft: number;
18
+ scrollTop: number;
19
+ }
20
+ export interface PinchGeometry {
21
+ /** 预演用的缩放比(相对手势开始时) */
22
+ k: number;
23
+ /** 预演用平移(px),配合 transform-origin: 0 0,作用在滚动容器内的内容元素上 */
24
+ tx: number;
25
+ ty: number;
26
+ }
27
+ /**
28
+ * 计算捏合预演 transform。
29
+ *
30
+ * 坐标系:transform 作用在「滚动容器内的内容元素」上(transform-origin: 0 0),
31
+ * 因此它以内容坐标系为基准;屏幕位置 = 内容坐标 - scroll。
32
+ *
33
+ * 约束:手势开始时手指中点下方的内容点 c0,必须始终落在当前两指中点处:
34
+ * k * c0 + t - scroll = v → t = v + scroll - k * c0
35
+ * 这样预演与「松手后提交 zoom + scroll」在数学上完全等价,松手不会跳变。
36
+ */
37
+ export declare function pinchTransform(startMidpoint: Point, currentMidpoint: Point, startViewport: ViewportMetrics, k: number): PinchGeometry;
38
+ export interface PinchCommit {
39
+ zoom: number;
40
+ scrollLeft: number;
41
+ scrollTop: number;
42
+ }
43
+ /**
44
+ * 松手后提交:给定新的 zoom,算出保持同一内容点位于当前中点所需的滚动位置。
45
+ */
46
+ export declare function pinchCommit(startMidpoint: Point, currentMidpoint: Point, startViewport: ViewportMetrics, ratio: number, options: {
47
+ minZoom: number;
48
+ maxZoom: number;
49
+ baseZoom: number;
50
+ maxScrollLeft: number;
51
+ maxScrollTop: number;
52
+ }): PinchCommit;
53
+ /**
54
+ * 双击 / 双指点击快速放大:以点击点为中心在 fit 与 targetZoom 之间切换。
55
+ */
56
+ export declare function doubleTapTarget(currentZoom: number, options: {
57
+ targetZoom: number;
58
+ minZoom: number;
59
+ maxZoom: number;
60
+ }): number;
61
+ /** 以某个视口点为中心缩放(滚轮 / 按钮 / 快捷键共用) */
62
+ export declare function zoomAtPoint(anchor: Point, viewport: ViewportMetrics, ratio: number, options: {
63
+ minZoom: number;
64
+ maxZoom: number;
65
+ baseZoom: number;
66
+ maxScrollLeft: number;
67
+ maxScrollTop: number;
68
+ }): PinchCommit;
69
+ export declare function clamp(value: number, min: number, max: number): number;
70
+ export declare function distance(a: Point, b: Point): number;
71
+ export declare function midpoint(a: Point, b: Point): Point;
@@ -0,0 +1,109 @@
1
+ import { FitMode, PageMetric, ViewerMode } from '../types';
2
+ export interface PageSize {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ export interface PageBox extends PageSize {
7
+ pageNumber: number;
8
+ top: number;
9
+ left: number;
10
+ /** 该页实际使用的缩放(翻页模式下每页独立适配) */
11
+ scale: number;
12
+ }
13
+ export interface ViewerLayout {
14
+ boxes: PageBox[];
15
+ contentWidth: number;
16
+ contentHeight: number;
17
+ /** 连续模式下页面宽度上限(用于把内容容器撑开) */
18
+ widest: number;
19
+ mode: ViewerMode;
20
+ }
21
+ export interface LayoutInput {
22
+ mode: ViewerMode;
23
+ /** 每页的度量,index 0 对应第 1 页;缺省用 fallback 顶替 */
24
+ metrics: ReadonlyArray<PageMetric | undefined>;
25
+ fallback: PageSize;
26
+ /** 连续模式下的统一缩放 */
27
+ scale: number;
28
+ /** 用户缩放倍率(1 = 适配),用于缩放间距,保证捏合锚点数学精确 */
29
+ zoom: number;
30
+ containerWidth: number;
31
+ containerHeight: number;
32
+ /** 基础间距(未乘 zoom) */
33
+ gap: number;
34
+ padding: number;
35
+ rotation: number;
36
+ fitMode: FitMode;
37
+ }
38
+ /** 旋转后的显示尺寸(未缩放) */
39
+ export declare function rotatedSize(metric: PageMetric, extraRotation: number): PageSize;
40
+ /** 适配缩放:fit-width 按宽度铺满,fit-page 整页可见 */
41
+ export declare function fitScale(size: PageSize, container: PageSize, fitMode: FitMode): number;
42
+ export declare function computeLayout(input: LayoutInput): ViewerLayout;
43
+ /**
44
+ * 二分查找「当前阅读页」:返回覆盖 offset 的页面;落在间隙里则取最近的一页。
45
+ */
46
+ export declare function pageAtOffset(boxes: ReadonlyArray<PageBox>, offset: number, axis?: 'y' | 'x'): number;
47
+ /** 滚动到指定页所需的 scrollTop / scrollLeft(让页面顶部对齐容器顶部) */
48
+ export declare function offsetForPage(layout: ViewerLayout, pageNumber: number, axis?: 'y' | 'x', align?: 'start' | 'center'): number;
49
+ /**
50
+ * 缩放锚点换算:保持 anchor 处的「内容坐标」不动。
51
+ *
52
+ * 因为内容(含间距 / 内边距)整体按 zoom 同比缩放,内容坐标可线性映射:
53
+ * content' = content * k, k = newZoom / oldZoom
54
+ * 于是新的滚动位置 = content' - anchor。
55
+ */
56
+ export declare function anchoredScroll(scroll: number, anchorViewportOffset: number, ratio: number): number;
57
+ /**
58
+ * 把渲染分辨率量化到固定档位,命中缓存(缩放来回时不重复渲染)。
59
+ * 返回「不小于 value 的最小档位」,没有更大档位时返回最后一档与 value 的较大者。
60
+ */
61
+ export declare const DEFAULT_SCALE_STEPS: readonly [0.25, 0.35, 0.5, 0.7, 0.85, 1, 1.25, 1.5, 1.75, 2, 2.5, 3, 3.5, 4, 5, 6, 8, 10, 12, 16];
62
+ export declare function quantizeScale(value: number, steps?: ReadonlyArray<number>): number;
63
+ /**
64
+ * 量化「设备像素比」,用于决定画布分辨率。
65
+ *
66
+ * 注意这里只处理 DPR —— 不掺任何页面缩放。
67
+ * 画布像素 = CSS 尺寸 × 本函数返回值,viewport scale 见 viewportScaleFor。
68
+ *
69
+ * 量化到固定档位是为了让缩放来回时能命中同一份位图缓存;
70
+ * 误差控制在 6% 内(视觉不可辨),超过则直接用精确值,宁可少一次命中也不牺牲画质。
71
+ */
72
+ export declare function quantizePixelRatio(devicePixelRatio: number, maxDpr?: number): number;
73
+ export interface CanvasLimitOptions {
74
+ /** 总像素上限,iOS 上建议 <= 16.7M */
75
+ maxPixels?: number;
76
+ /** 单边上限,iOS 上建议 <= 4096~8192 */
77
+ maxDimension?: number;
78
+ }
79
+ export interface CanvasSize {
80
+ width: number;
81
+ height: number;
82
+ /** 实际生效的缩放(可能因限幅而小于请求值) */
83
+ scale: number;
84
+ clamped: boolean;
85
+ }
86
+ /**
87
+ * 计算 canvas 的像素尺寸并在必要时限幅。
88
+ *
89
+ * 注意 scale 的语义:**画布像素 / CSS 像素**(不是 pdf.js 的 viewport scale)。
90
+ * 它不能直接传给 getViewport —— 中间还差一个「CSS 像素 / PDF 单位」的换算,
91
+ * 见 viewportScaleFor。
92
+ *
93
+ * 移动端 canvas 有硬上限(iOS Safari 约 16.7M 像素、单边 8192),超限会得到空白画布,
94
+ * 所以宁可降分辨率也要限幅。
95
+ */
96
+ export declare function clampCanvasSize(cssWidth: number, cssHeight: number, pixelRatio: number, options?: CanvasLimitOptions): CanvasSize;
97
+ /**
98
+ * 计算应当传给 `page.getViewport({ scale })` 的值。
99
+ *
100
+ * pdf.js 用「PDF 单位 × scale」算渲染尺寸,因此必须把
101
+ * 「画布像素 / CSS 像素」再乘上「CSS 像素 / PDF 单位」。
102
+ *
103
+ * 若直接把 clampCanvasSize 返回的 scale 传给 getViewport,
104
+ * 渲染内容会比画布大 1/displayScale 倍(窄屏 A4 约 1.63 倍),右侧被画布边界切掉,
105
+ * 表现就是「文字被截断」。
106
+ */
107
+ export declare function viewportScaleFor(cssWidth: number, pdfWidth: number, canvasPixelRatio: number): number;
108
+ /** 位图内存估算(RGBA) */
109
+ export declare function bitmapBytes(width: number, height: number): number;