mobile-pdf
Version:
A mobile-friendly PDF viewer based on pdfjs-dist.
434 lines (320 loc) • 20.8 kB
Markdown
# mobile-pdf
<div align="center">
<img src="https://img.shields.io/badge/node-20%2B-blue.svg" alt="Node.js version">
[](https://github.com/zhayes/mobile_pdf/blob/main/LICENSE)
[](https://www.npmjs.com/package/mobile-pdf)
A high-performance, mobile-first PDF viewer with rich gesture support that works seamlessly on desktop too.
**Language:** [English](#english) | [中文](#中文)
</div>
---
## <a name="english"></a>English
### 📦 Installation
```bash
# Using pnpm (recommended)
pnpm add mobile-pdf
# Using npm
npm install mobile-pdf
# Using yarn
yarn add mobile-pdf
```
### 🚀 Quick Start
#### 1. Prepare the HTML
First, you need a container element in your HTML to host the PDF viewer. This element should have a defined size.
```html
<!-- The viewer will be mounted here. Ensure it has a height. -->
<div id="pdf-viewer" style="width: 100vw; height: 100vh;"></div>
<!-- Example: An input for users to select a local PDF file -->
<input type="file" id="file-input" accept=".pdf" />
```
#### 2. Initialize the Viewer
The library's functionality is split across four core classes. Here’s how you initialize and connect them:
```javascript
// For compatibility with older browsers, it is recommended to additionally introduce:
import 'core-js/actual/promise/with-resolvers';
import { PDFViewer, MobilePDF, Transform, TouchManager } from 'mobile-pdf';
// Step 1: Create the viewer's DOM structure
const root_element = document.getElementById('pdf-viewer');
const pdf_viewer = new PDFViewer(root_element);
// Step 2: Set up gesture handling
const transform = new Transform(pdf_viewer.inner_div, pdf_viewer.wrap_div);
const touch_manager = new TouchManager(transform);
// Step 3: Create the PDF rendering engine
const mobile_pdf = new MobilePDF(pdf_viewer.wrap_div, pdf_viewer.inner_div, {
resolution_multiplier: 3, // Higher value for sharper rendering
hook_actions: {
start_loading: async () => console.log('Loading started...'),
begin_insert_pages: (total_pages) => {
transform.reset_transform(); // Reset zoom/pan for new document
console.log(`PDF parsed, ${total_pages} pages to be inserted.`);
},
complete_loading: async () => console.log('PDF document is fully loaded.'),
end_rendering: (page) => console.log(`Page ${page.page} has rendered.`),
},
});
// Step 4: Load a PDF
const file_input = document.getElementById('file-input');
file_input.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const file_buffer = await file.arrayBuffer();
await mobile_pdf.load_pdf(file_buffer);
}
});
// For Single Page Applications (SPAs) like React, Vue, etc.
// It is crucial to clean up event listeners and the PDF instance when the component unmounts.
//
// Example for a React component:
// useEffect(() => {
// // ... initialization code from above ...
// return () => {
// touch_manager.removeEventListener(); // Cleanup listeners
// mobile_pdf.cleanup_pdf(); // Cleanup PDF instance and DOM
// };
// }, []);
```
### 📚 API Reference
#### Architecture
1. **`PDFViewer`**: Builds the necessary DOM elements and automatically configures the container for touch or scroll interaction.
2. **`MobilePDF`**: Manages PDF loading and page rendering.
3. **`Transform`**: Controls CSS `transform` properties (scale and translate).
4. **`TouchManager`**: Captures user touch gestures and executes them via a `Transform` instance.
---
#### `new PDFViewer(rootEl: HTMLElement)`
Initializes the viewer's DOM structure. It intelligently detects if the device supports touch events.
- On **touch devices**, the main container (`wrap_div`) will have `overflow: hidden` to allow for gesture-based panning.
- On **non-touch devices** (like desktops), it will have `overflow: auto` to enable native scrollbars.
- **`rootEl`**: The HTML element where the viewer will be mounted.
- **Instance Properties**:
- `wrap_div: HTMLDivElement`: The outer container element (viewport).
- `inner_div: HTMLDivElement`: The inner content container that holds PDF pages and is the target for transformations.
---
#### `new MobilePDF(wrapper_dom, inner_dom, config?)`
The core class that handles the PDF rendering lifecycle.
- **`wrapper_dom`**: The outer container element (`PDFViewer.wrap_div`).
- **`inner_dom`**: The content container element (`PDFViewer.inner_div`).
- **`config`**: An optional configuration object (`MobilePDFViewerConfig`).
##### Methods
- **`load_pdf(source: PDFSourceDataOption): Promise<void>`**: Asynchronously loads a PDF document.
- `source`: Can be a URL (`string`), `ArrayBuffer`, `Uint8Array`, or other formats supported by `pdfjs-dist`.
- **`cleanup_pdf()`**: Destroys the internal PDF document instance, removes all rendered page elements from the DOM, and disconnects the `IntersectionObserver`. This is crucial for freeing up memory and preventing leaks when the viewer is no longer needed.
##### Configuration (`MobilePDFViewerConfig`)
| Key | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `resolution_multiplier`| `number` | `3` | The multiplier for canvas rendering resolution. Higher values produce sharper images but use more memory. |
| `pdf_container_class` | `string[]` | `[]` | Custom CSS classes for the main wrapper (`wrap_div`). |
| `transform_container_class`| `string[]` | `[]` | Custom CSS classes for the content container (`inner_div`). |
| `page_container_class` | `string[]` | `[]` | Custom CSS classes for the wrapper of each page. |
| `canvas_class` | `string[]` | `[]` | Custom CSS classes for each `<canvas>` element. |
| `hook_actions` | `HookActions`| `{}` | An object containing lifecycle callback functions. |
##### Lifecycle Hooks (`hook_actions`)
| Hook | Parameters | Description |
| :--- | :--- | :--- |
| `start_loading` | `() => Promise<void>` | **Async**. Fires when `load_pdf` is called. Ideal for showing a loading indicator. |
| `begin_insert_pages`| `(total_pages: number) => void`| Fires after the PDF is parsed. Recommended place to reset transforms. |
| `complete_loading`| `(pages, pdf_doc, total) => Promise<void>` | **Async**. Fires after all page placeholders are in the DOM. |
| `start_rendering` | `(page: PDFPage) => void` | Fires just before a specific page begins to render. |
| `end_rendering` | `(page: PDFPage) => void` | Triggered after a specific page has finished rendering. |
##### `PDFPage` Object Structure
The `PDFPage` object is passed as a parameter in several hooks and has the following structure:
| Key | Type | Description |
| :-------------- | :--------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `canvas` | `HTMLCanvasElement` or `null` | The canvas element used to render the page. It's `null` if the page is not currently rendered. |
| `canvas_wrapper`| `HTMLDivElement` | A wrapper element around the canvas. |
| `render_status` | `'pending'` | `'loading'` | `'complete'` | The current rendering status of the page. |
| `page` | `number` | The page number (1-based). |
| `key` | `string` | A unique key for the page. |
| `rendering_task` | `RenderTask` or `null` | A reference to the current rendering task from PDF.js. This is used to cancel the rendering if the page is scrolled out of view. |
---
#### `new Transform(transform_el, wrapper_el, boundary?)`
Manages the 2D transformations (zoom and pan) for the content element.
- **`transform_el`**: The element to which CSS transforms will be applied (`PDFViewer.inner_div`).
- **`wrapper_el`**: The outer boundary-defining element (`PDFViewer.wrap_div`).
- **`boundary`**: Optional object to configure movement boundaries and scale limits.
##### Boundary Configuration
The `boundary` object sets limits for panning and zooming.
| Key | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `left` | `number` | `50` | Maximum allowable pan distance from the left edge (in pixels). |
| `right` | `number` | `50` | Maximum allowable pan distance from the right edge (in pixels). |
| `top` | `number` | `50` | Maximum allowable pan distance from the top edge (in pixels). |
| `bottom` | `number` | `50` | Maximum allowable pan distance from the bottom edge (in pixels). |
| `min_scale` | `number` | `0.5` | The minimum allowed zoom-out scale. |
| `max_scale` | `number` | `4` | The maximum allowed zoom-in scale. |
**Note:** The `left`, `right`, `top`, and `bottom` boundary constraints only take effect when the content is zoomed in (`scale > 1`).
##### Methods
- **`set_dragging(value: boolean)`**: Sets the internal dragging state.
- **`set_pinching(value: boolean)`**: Sets the internal pinching state.
- **`get_dragging(): boolean`**: Returns `true` if a drag operation is active.
- **`get_pinching(): boolean`**: Returns `true` if a pinch operation is active.
- **`get_translate(): { translate_x: number, translate_y: number }`**: Returns the current translation values.
- **`get_scale(): number`**: Returns the current scale value.
- **`transform(position?)`**: Applies a transformation. `position` is an object with optional `translate_x`, `translate_y`, and `scale` properties.
- **`reset_transform()`**: Resets scale to `1` and translation to `(0, 0)`.
- **`constrain_boundary(x, y)`**: Calculates and returns new coordinates constrained within the defined boundaries.
---
#### `new TouchManager(transform_instance)`
Listens for user touch events and orchestrates gestures. The constructor automatically detects touch support. If touch is available, it calls `addEventListener()` to begin listening for events.
- **`transform_instance`**: An instance of the `Transform` class that this manager will control.
##### Methods
- **`addEventListener()`**: Attaches `touchstart`, `touchmove`, and `touchend` event listeners to the transform element. It is called by the constructor automatically on touch-enabled devices.
- **`removeEventListener()`**: Removes all event listeners. **This is crucial for cleanup in Single Page Applications** to prevent memory leaks when a component unmounts.
### 🙏 Contributing
Contributions, issues, and feature requests are welcome! Feel free to check the [issues page](https://github.com/zhayes/mobile_pdf/issues).
### 📄 License
This project is licensed under the MIT License.
---
## <a name="中文"></a>中文
### 📦 安装
```bash
# 使用 pnpm (推荐)
pnpm add mobile-pdf
# 使用 npm
npm install mobile-pdf
# 使用 yarn
yarn add mobile-pdf
```
### 🚀 快速上手
#### 1. 准备 HTML 结构
首先,您需要在 HTML 中准备一个容器元素来承载 PDF 阅读器。该元素应具有确定的尺寸。
```html
<!-- 阅读器将在此处挂载。请确保它具有高度。 -->
<div id="pdf-viewer" style="width: 100vw; height: 100vh;"></div>
<!-- 示例:一个供用户选择本地 PDF 文件的输入框 -->
<input type="file" id="file-input" accept=".pdf" />
```
#### 2. 初始化阅读器
该库的功能分散在四个核心类中。以下是如何初始化并连接它们:
```javascript
// 作为低版本浏览器兼容,建议额外引入:
import 'core-js/actual/promise/with-resolvers';
import { PDFViewer, MobilePDF, Transform, TouchManager } from 'mobile-pdf';
// 步骤 1: 创建阅读器的 DOM 结构
const root_element = document.getElementById('pdf-viewer');
const pdf_viewer = new PDFViewer(root_element);
// 步骤 2: 设置手势处理
const transform = new Transform(pdf_viewer.inner_div, pdf_viewer.wrap_div);
const touch_manager = new TouchManager(transform);
// 步骤 3: 创建 PDF 渲染引擎
const mobile_pdf = new MobilePDF(pdf_viewer.wrap_div, pdf_viewer.inner_div, {
resolution_multiplier: 3, // 更高的值可以获得更清晰的渲染效果
hook_actions: {
start_loading: async () => console.log('加载开始...'),
begin_insert_pages: (total_pages) => {
transform.reset_transform(); // 为新文档重置缩放和平移
console.log(`PDF 解析完成,总计 ${total_pages} 页待插入。`);
},
complete_loading: async () => console.log('PDF 文档已完全加载。'),
end_rendering: (page) => console.log(`第 ${page.page} 页已渲染。`),
},
});
// 步骤 4: 加载一个 PDF
const file_input = document.getElementById('file-input');
file_input.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const file_buffer = await file.arrayBuffer();
await mobile_pdf.load_pdf(file_buffer);
}
});
// 对于单页应用 (SPA) 如 React, Vue 等
// 在组件卸载时清理事件监听器和 PDF 实例至关重要。
//
// React 组件示例:
// useEffect(() => {
// // ... 以上初始化代码 ...
// return () => {
// touch_manager.removeEventListener(); // 清理监听器
// mobile_pdf.cleanup_pdf(); // 清理 PDF 实例和 DOM
// };
// }, []);
```
### 📚 API 参考
#### 架构
1. **`PDFViewer`**: 构建所需的 DOM 元素,并为触摸或滚动交互自动配置容器。
2. **`MobilePDF`**: 管理 PDF 的加载和页面渲染。
3. **`Transform`**: 控制 CSS `transform` 属性(缩放和平移)。
4. **`TouchManager`**: 捕获用户触摸手势,并通过 `Transform` 实例来执行操作。
---
#### `new PDFViewer(rootEl: HTMLElement)`
初始化阅读器的 DOM 结构。它能智能检测设备是否支持触摸事件。
- 在**触摸设备**上,主容器 (`wrap_div`) 将设置 `overflow: hidden` 以便进行手势平移。
- 在**非触摸设备**(如桌面电脑)上,它将设置 `overflow: auto` 以启用原生滚动条。
- **`rootEl`**: 用于挂载阅读器的 HTML 元素。
- **实例属性**:
- `wrap_div: HTMLDivElement`: 外层容器元素(视口)。
- `inner_div: HTMLDivElement`: 内部内容容器,用于存放 PDF 页面,也是变换的目标。
---
#### `new MobilePDF(wrapper_dom, inner_dom, config?)`
处理 PDF 渲染生命周期的核心类。
- **`wrapper_dom`**: 外层容器元素 (`PDFViewer.wrap_div`)。
- **`inner_dom`**: 内容容器元素 (`PDFViewer.inner_div`)。
- **`config`**: 可选的配置对象 (`MobilePDFViewerConfig`)。
##### 方法
- **`load_pdf(source: PDFSourceDataOption): Promise<void>`**: 异步加载一个 PDF 文档。
- `source`: 可以是 URL (`string`)、`ArrayBuffer`、`Uint8Array` 或 `pdfjs-dist` 支持的其他格式。
- **`cleanup_pdf()`**: 销毁内部的 PDF 文档实例,从 DOM 中移除所有已渲染的页面元素,并断开 `IntersectionObserver`。当不再需要查看器时,调用此方法对于释放内存和防止泄漏至关重要。
##### 配置 (`MobilePDFViewerConfig`)
| 键 | 类型 | 默认值 | 描述 |
| :--- | :--- | :--- | :--- |
| `resolution_multiplier`| `number` | `3` | 画布渲染分辨率的倍率。更高的值会带来更清晰的图像,但会增加内存使用量。 |
| `pdf_container_class` | `string[]` | `[]` | 添加到主包装器 (`wrap_div`) 的自定义 CSS 类名数组。 |
| `transform_container_class`| `string[]` | `[]` | 添加到可变换内容容器 (`inner_div`) 的自定义 CSS 类名数组。 |
| `page_container_class` | `string[]` | `[]` | 添加到每个独立页面包装器 `div` 的自定义 CSS 类名数组。 |
| `canvas_class` | `string[]` | `[]` | 添加到每个页面 `<canvas>` 元素的自定义 CSS 类名数组。 |
| `hook_actions` | `HookActions`| `{}` | 一个包含生命周期回调函数的对象。 |
##### 生命周期钩子 (`hook_actions`)
| 钩子 | 参数 | 描述 |
| :--- | :--- | :--- |
| `start_loading` | `() => Promise<void>` | **异步**。在调用 `load_pdf` 时触发。非常适合用于显示加载指示器。 |
| `begin_insert_pages`| `(total_pages: number) => void`| 在 PDF 解析完成后触发。建议在此处重置变换状态。 |
| `complete_loading`| `(pages, pdf_doc, total) => Promise<void>` | **异步**。在所有页面占位符都已添加到 DOM 后触发。 |
| `start_rendering` | `(page: PDFPage) => void` | 在特定页面即将开始渲染之前触发。 |
| `end_rendering` | `(page: PDFPage) => void` | 在特定页面完成渲染之后触发。 |
##### `PDFPage` 对象结构
`PDFPage` 对象作为参数在多个钩子中传递,包含以下属性:
| 键 | 类型 | 描述 |
| :--- | :--- | :--- |
| `canvas` | `HTMLCanvasElement` 或 `null` | 用于渲染页面的 canvas 元素。如果页面当前未渲染,则为 `null`。 |
| `canvas_wrapper` | `HTMLDivElement` | 包裹 canvas 的 div 元素。 |
| `render_status` | `'pending'` | `'loading'` | `'complete'` | 页面的当前渲染状态。 |
| `page` | `number` | 页码(从 1 开始)。 |
| `key` | `string` | 页面的唯一键。 |
| `rendering_task` | `RenderTask` 或 `null` | 来自 PDF.js 的当前渲染任务的引用。用于在页面滚出视野时取消渲染。 |
---
#### `new Transform(transform_el, wrapper_el, boundary?)`
管理内容元素的 2D 变换(缩放和平移)。
- **`transform_el`**: 将被应用 CSS 变换的元素 (`PDFViewer.inner_div`)。
- **`wrapper_el`**: 定义边界的外层元素 (`PDFViewer.wrap_div`)。
- **`boundary`**: 可选对象,用于配置移动边界和缩放限制。
##### Boundary (边界) 配置
`boundary` 对象为平移和缩放设置限制。
| 键 | 类型 | 默认值 | 描述 |
| :--- | :--- | :--- | :--- |
| `left` | `number` | `50` | 距离左边缘的最大可平移距离(单位:像素)。 |
| `right` | `number` | `50` | 距离右边缘的最大可平移距离(单位:像素)。 |
| `top` | `number` | `50` | 距离上边缘的最大可平移距离(单位:像素)。 |
| `bottom` | `number` | `50` | 距离下边缘的最大可平移距离(单位:像素)。 |
| `min_scale` | `number` | `0.5` | 允许的最小缩小比例。 |
| `max_scale` | `number` | `4` | 允许的最大放大比例。 |
**注意:** `left`、`right`、`top` 和 `bottom` 边界约束仅在内容被放大时 (`scale > 1`) 生效。
##### 方法
- **`set_dragging(value: boolean)`**: 设置内部的拖动状态。
- **`set_pinching(value: boolean)`**: 设置内部的双指缩放状态。
- **`get_dragging(): boolean`**: 如果拖动操作正在进行,则返回 `true`。
- **`get_pinching(): boolean`**: 如果双指缩放操作正在进行,则返回 `true`。
- **`get_translate(): { translate_x: number, translate_y: number }`**: 返回当前的平移值。
- **`get_scale(): number`**: 返回当前的缩放值。
- **`transform(position?)`**: 应用一个变换。`position` 是一个包含可选 `translate_x`、`translate_y` 和 `scale` 属性的对象。
- **`reset_transform()`**: 将缩放重置为 `1`,平移重置为 `(0, 0)`。
- **`constrain_boundary(x, y)`**: 计算并返回受边界约束的新坐标。
---
#### `new TouchManager(transform_instance)`
监听用户的触摸事件并协调手势。构造函数会自动检测触摸支持。如果支持触摸,它会调用 `addEventListener()` 开始监听事件。
- **`transform_instance`**: 此管理器将要控制的 `Transform` 类的实例。
##### 方法
- **`addEventListener()`**: 将 `touchstart`、`touchmove` 和 `touchend` 事件监听器附加到变换元素上。在支持触摸的设备上,构造函数会自动调用它。
- **`removeEventListener()`**: 移除所有事件监听器。**这对于在单页应用中进行清理至关重要**,以防止组件卸载时发生内存泄漏。
### 🙏 贡献
欢迎提交贡献、问题和功能请求!请随时查看 [issues 页面](https://github.com/zhayes/mobile_pdf/issues)。
### 📄 许可证
本项目基于 MIT 许可证授权。