1. 项目概述
OFD(Open Fixed-layout Document)是一种开放版式文档格式,类似于PDF,但具有更高的灵活性和可扩展性。开发一个OFD阅读器需要解析OFD文件的结构,并将其内容渲染到屏幕上。本文将详细介绍如何使用TypeScript开发一个简单的OFD阅读器。
开发一款ofd web阅读器有很大的挑战性,本人开发过一款完善的ofd web阅读器,见文章《ofd轻阅读---采用Typescript全新开发,让阅读、批注更方便!》。本文则从最基本处理逻辑谈起,由易入难,让读者对开发web阅读器有个初步的的认识。
2. 项目结构
首先,我们需要确定项目的基本结构。一个典型的OFD阅读器项目可能包含以下模块:
文件解析模块:负责解析OFD文件的结构,提取文档内容。
渲染模块:负责将解析后的内容渲染到屏幕上。
用户交互模块:处理用户的交互操作,如翻页、缩放等。
工具模块:提供一些辅助功能,如日志记录、错误处理等。
3. 文件解析模块
OFD文件实际上是一个ZIP压缩包,里面包含了多个XML文件和其他资源文件。我们需要先解压这个ZIP包,然后解析其中的XML文件。
3.1 解压OFD文件
我们可以使用JSZip库来解压OFD文件。首先,安装JSZip:
npm install jszip
然后,编写解压代码:
import JSZip from 'jszip';
async function unzipOFD(file: File): Promise<JSZip> {
const zip = new JSZip();
const content = await zip.loadAsync(file);
return content;
}
3.2 解析XML文件
OFD文件中的XML文件描述了文档的结构。我们可以使用DOMParser来解析这些XML文件。
function parseXML(xmlString: string): Document {
const parser = new DOMParser();
return parser.parseFromString(xmlString, 'application/xml');
}
3.3 解析OFD文档结构
OFD文档的结构通常包括以下几个部分:
Document.xml:描述文档的基本信息。
Pages/:包含各个页面的描述文件。
Res/:包含资源文件,如图片、字体等。
我们可以编写一个函数来解析这些文件:
interface OFDDocument {
documentXML: Document;
pages: Document[];
resources: Map<string, Blob>;
}
async function parseOFD(zip: JSZip): Promise<OFDDocument> {
const documentXML = parseXML(await zip.file('Document.xml').async('text'));
const pages: Document[] = [];
const resources = new Map<string, Blob>();
// 解析页面
const pageFiles = zip.folder('Pages').filter((relativePath, file) => !file.dir);
for (const pageFile of pageFiles) {
const pageXML = parseXML(await pageFile.async('text'));
pages.push(pageXML);
}
// 解析资源
const resourceFiles = zip.folder('Res').filter((relativePath, file) => !file.dir);
for (const resourceFile of resourceFiles) {
const resourceBlob = await resourceFile.async('blob');
resources.set(resourceFile.name, resourceBlob);
}
return { documentXML, pages, resources };
}
4. 渲染模块
渲染模块负责将解析后的OFD文档内容渲染到屏幕上。我们可以使用HTML5的Canvas来实现这一功能。
4.1 创建Canvas
首先,我们需要在HTML中创建一个Canvas元素:
<canvas id="ofd-canvas"></canvas>
运行 HTML
然后,在TypeScript中获取这个Canvas元素并设置其大小:
const canvas = document.getElementById('ofd-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
function setCanvasSize(width: number, height: number) {
canvas.width = width;
canvas.height = height;
}
4.2 渲染页面
我们可以编写一个函数来渲染单个页面。假设每个页面的内容是一个简单的矩形,我们可以这样实现:
function renderPage(ctx: CanvasRenderingContext2D, pageXML: Document) {
// 假设页面内容是一个矩形
const rect = pageXML.querySelector('Rect');
if (rect) {
const x = parseFloat(rect.getAttribute('x'));
const y = parseFloat(rect.getAttribute('y'));
const width = parseFloat(rect.getAttribute('width'));
const height = parseFloat(rect.getAttribute('height'));
const color = rect.getAttribute('color') || 'black';
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
}
4.3 渲染整个文档
我们可以编写一个函数来渲染整个文档:
function renderDocument(ctx: CanvasRenderingContext2D, ofdDocument: OFDDocument) {
// 清空Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 渲染每个页面
for (const pageXML of ofdDocument.pages) {
renderPage(ctx, pageXML);
}
}
5. 用户交互模块
用户交互模块负责处理用户的翻页、缩放等操作。
5.1 翻页功能
我们可以通过监听键盘事件来实现翻页功能:
let currentPage = 0;
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft' && currentPage > 0) {
currentPage--;
renderPage(ctx, ofdDocument.pages[currentPage]);
} else if (event.key === 'ArrowRight' && currentPage < ofdDocument.pages.length - 1) {
currentPage++;
renderPage(ctx, ofdDocument.pages[currentPage]);
}
});
5.2 缩放功能
我们可以通过监听鼠标滚轮事件来实现缩放功能:
let scale = 1;
canvas.addEventListener('wheel', (event) => {
event.preventDefault();
scale += event.deltaY * -0.01;
scale = Math.min(Math.max(0.1, scale), 4);
ctx.setTransform(scale, 0, 0, scale, 0, 0);
renderPage(ctx, ofdDocument.pages[currentPage]);
});
6. 工具模块
工具模块提供一些辅助功能,如日志记录、错误处理等。
6.1 日志记录
我们可以编写一个简单的日志记录函数:
function log(message: string, level: 'info' | 'warn' | 'error' = 'info') {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
}
6.2 错误处理
我们可以编写一个错误处理函数,用于捕获和处理异常:
function handleError(error: Error) {
log(error.message, 'error');
// 可以在这里添加更多的错误处理逻辑,如显示错误提示等
}
7. 完整代码
以下是完整的TypeScript代码:
import JSZip from 'jszip';
// 解压OFD文件
async function unzipOFD(file: File): Promise<JSZip> {
const zip = new JSZip();
const content = await zip.loadAsync(file);
return content;
}
// 解析XML文件
function parseXML(xmlString: string): Document {
const parser = new DOMParser();
return parser.parseFromString(xmlString, 'application/xml');
}
// 解析OFD文档结构
interface OFDDocument {
documentXML: Document;
pages: Document[];
resources: Map<string, Blob>;
}
async function parseOFD(zip: JSZip): Promise<OFDDocument> {
const documentXML = parseXML(await zip.file('Document.xml').async('text'));
const pages: Document[] = [];
const resources = new Map<string, Blob>();
// 解析页面
const pageFiles = zip.folder('Pages').filter((relativePath, file) => !file.dir);
for (const pageFile of pageFiles) {
const pageXML = parseXML(await pageFile.async('text'));
pages.push(pageXML);
}
// 解析资源
const resourceFiles = zip.folder('Res').filter((relativePath, file) => !file.dir);
for (const resourceFile of resourceFiles) {
const resourceBlob = await resourceFile.async('blob');
resources.set(resourceFile.name, resourceBlob);
}
return { documentXML, pages, resources };
}
// 创建Canvas
const canvas = document.getElementById('ofd-canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d');
function setCanvasSize(width: number, height: number) {
canvas.width = width;
canvas.height = height;
}
// 渲染页面
function renderPage(ctx: CanvasRenderingContext2D, pageXML: Document) {
// 假设页面内容是一个矩形
const rect = pageXML.querySelector('Rect');
if (rect) {
const x = parseFloat(rect.getAttribute('x'));
const y = parseFloat(rect.getAttribute('y'));
const width = parseFloat(rect.getAttribute('width'));
const height = parseFloat(rect.getAttribute('height'));
const color = rect.getAttribute('color') || 'black';
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
}
// 渲染整个文档
function renderDocument(ctx: CanvasRenderingContext2D, ofdDocument: OFDDocument) {
// 清空Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 渲染每个页面
for (const pageXML of ofdDocument.pages) {
renderPage(ctx, pageXML);
}
}
// 翻页功能
let currentPage = 0;
document.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft' && currentPage > 0) {
currentPage--;
renderPage(ctx, ofdDocument.pages[currentPage]);
} else if (event.key === 'ArrowRight' && currentPage < ofdDocument.pages.length - 1) {
currentPage++;
renderPage(ctx, ofdDocument.pages[currentPage]);
}
});
// 缩放功能
let scale = 1;
canvas.addEventListener('wheel', (event) => {
event.preventDefault();
scale += event.deltaY * -0.01;
scale = Math.min(Math.max(0.1, scale), 4);
ctx.setTransform(scale, 0, 0, scale, 0, 0);
renderPage(ctx, ofdDocument.pages[currentPage]);
});
// 日志记录
function log(message: string, level: 'info' | 'warn' | 'error' = 'info') {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
}
// 错误处理
function handleError(error: Error) {
log(error.message, 'error');
// 可以在这里添加更多的错误处理逻辑,如显示错误提示等
}
// 主函数
async function main() {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.addEventListener('change', async (event) => {
const file = (event.target as HTMLInputElement).files[0];
if (file) {
try {
const zip = await unzipOFD(file);
const ofdDocument = await parseOFD(zip);
renderDocument(ctx, ofdDocument);
} catch (error) {
handleError(error);
}
}
});
}
main();
8. 总结
本文介绍了如何使用TypeScript开发一个简单的OFD阅读器。我们首先解析了OFD文件的结构,然后使用Canvas将文档内容渲染到屏幕上。最后,我们实现了翻页和缩放功能,并添加了日志记录和错误处理功能。这个项目只是一个基础版本,实际应用中还需要处理更多的细节,如复杂的页面布局、字体渲染、图像处理等。希望本文能为开发OFD阅读器提供一些思路和参考。