在 React 中实现 RTF 与 PDF、HTML 的文档转换实践

在日常的文档处理工作中,RTF(富文本格式)凭借其良好的兼容性,常被用作跨软件交换文档的中间格式。而 PDF 以其版面固定、不易篡改的特点,成为正式交付和归档的首选;HTML 则在 Web 展示和内容发布中占据核心地位。在 React 前端项目中,若能做到这三种格式的按需转换,无疑能为文档管理类应用提供极大的灵活性。

本文介绍一种在 React 环境中将 RTF 文件转换为 PDF 和 HTML 的实现方案,该方案基于 WebAssembly 技术运行,无需依赖后端服务,可直接在浏览器端完成文档处理工作。

技术选型与实现思路

在浏览器端处理文档格式转换,目前主流的思路有两种:一是将文件上传至后端服务器进行处理,二是借助 WebAssembly 在本地完成转换。前者实现简单但存在数据隐私和服务器负载的问题,后者虽然技术门槛稍高,但能有效保护用户数据安全。

本文采用第二种思路,使用 Spire.Doc for JavaScript 作为处理工具。它是一个基于 WebAssembly 的文档处理库,支持在浏览器环境中读取和转换多种文档格式。

环境准备与模块加载

首先需要在 React 项目中安装核心依赖:

bash 复制代码
npm i spire.office

安装完成后,需要将 spire.doc.js 及其相关的 WASM 文件和资源复制到项目的 public 目录下。由于该库的运行依赖于 WebAssembly,我们需要在 React 组件加载时异步初始化 WASM 模块。这里的关键点是使用 webpackIgnore: true 来确保 Webpack 不会干预这个动态导入的过程,避免资源路径解析错误。

javascript 复制代码
import React, { useState, useEffect } from 'react';

function App() {
  const [wasmModule, setWasmModule] = useState(null);
  
  useEffect(() => {
    (async () => {
      try {
        const publicUrl = process.env.PUBLIC_URL || '';
        const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.doc.js`);
        const rawModule = spireModule.default || spireModule;
        window.wasmModule = typeof rawModule === 'function'
          ? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
          : rawModule;
        setWasmModule(window.wasmModule);
      } catch (error) {
        console.error('WASM module loading failed:', error);
      }
    })();
  }, []);
  
  // ... 转换逻辑
}

字体管理与文件加载

文档转换中最常见的问题是字体缺失导致的排版错乱。该工具使用虚拟文件系统(VFS)来管理字体资源。在转换前,需要将文档中可能用到的字体文件加载到 VFS 中。Times New Roman 是 RTF 文档常用的字体,这里以它为例进行说明。

javascript 复制代码
// 将字体文件加载到虚拟文件系统
await window.spire.FetchFileToVFS('times.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('timesbd.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('timesbi.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
await window.spire.FetchFileToVFS('timesi.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);

同时,目标 RTF 文件也需要通过 FetchFileToVFS 加载到 VFS 中,以便后续的 Document 实例能够读取到它。

javascript 复制代码
// 指定输入文件名
const inputFileName = 'input.rtf';
// 获取输入文件并添加到 VFS
await window.spire.FetchFileToVFS(inputFileName, '', `${process.env.PUBLIC_URL}/static/data/`);

核心转换实现

RTF 转 PDF

将 RTF 转换为 PDF 的流程如下:通过 Document.LoadFromFile() 加载源文件,然后使用 Document.SaveToFile() 指定输出格式为 PDF,最后从 VFS 中读取生成的文件并提供给用户下载。

javascript 复制代码
const convertRtfToPdf = async () => {
  const wasmModule = window.wasmModule?.spiredoc;
  if (!wasmModule) return;

  // 加载字体和 RTF 文件到 VFS
  await window.spire.FetchFileToVFS('times.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
  // ... 加载其他字体文件
  await window.spire.FetchFileToVFS('input.rtf', '', `${process.env.PUBLIC_URL}/static/data/`);

  // 创建文档实例并加载
  const doc = new wasmModule.Document();
  doc.LoadFromFile('input.rtf');

  // 定义输出文件名
  const outputFileName = "RtfToPdf.pdf";

  // 保存为 PDF
  doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.PDF });

  // 从 VFS 读取生成的 PDF 文件
  const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);

  // 创建 Blob 对象并触发下载
  const modifiedFile = new Blob([modifiedFileArray], { type: 'application/pdf' });
  const url = URL.createObjectURL(modifiedFile);
  const a = document.createElement('a');
  a.href = url;
  a.download = outputFileName;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);

  // 清理资源
  doc.Dispose();
};

如果需要确保 PDF 在不同设备上显示一致,可以设置字体嵌入参数:

javascript 复制代码
let parameters = new wasmModule.ToPdfParameterList();
parameters.IsEmbeddedAllFonts = true;
doc.SaveToFile({ fileName: outputFileName, paramList: parameters });

RTF 转 HTML

转换为 HTML 的过程与 PDF 类似,但在 SaveToFile 时需要指定 FileFormat.Html。为了便于传输和展示,可以通过设置 HtmlExportOptions 将 CSS 样式和图片内嵌到单个 HTML 文件中。

javascript 复制代码
const convertRtfToHtml = async () => {
  const wasmModule = window.wasmModule?.spiredoc;
  if (!wasmModule) return;

  // 加载字体和 RTF 文件到 VFS
  await window.spire.FetchFileToVFS('times.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/static/font/`);
  // ... 加载其他字体文件
  await window.spire.FetchFileToVFS('input.rtf', '', `${process.env.PUBLIC_URL}/static/data/`);

  // 创建文档实例并加载
  const doc = new wasmModule.Document();
  doc.LoadFromFile('input.rtf');

  // 配置 HTML 导出选项
  doc.HtmlExportOptions.CssStyleSheetType = wasmModule.CssStyleSheetType.Internal; // CSS 内嵌
  doc.HtmlExportOptions.ImageEmbedded = true; // 图片内嵌

  // 定义输出文件名
  const outputFileName = "RtfToHtml.html";

  // 保存为 HTML
  doc.SaveToFile({ fileName: outputFileName, fileFormat: wasmModule.FileFormat.Html });

  // 从 VFS 读取生成的 HTML 文件
  const modifiedFileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);

  // 创建 Blob 对象并触发下载
  const modifiedFile = new Blob([modifiedFileArray], { type: 'text/html' });
  const url = URL.createObjectURL(modifiedFile);
  const a = document.createElement('a');
  a.href = url;
  a.download = outputFileName;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);

  // 清理资源
  doc.Dispose();
};

完整组件示例

将上述逻辑整合到一个 React 组件中,可以提供两种转换功能:

javascript 复制代码
import React, { useState, useEffect } from 'react';

function App() {
  const [wasmModule, setWasmModule] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    (async () => {
      try {
        const publicUrl = process.env.PUBLIC_URL || '';
        const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.doc.js`);
        const rawModule = spireModule.default || spireModule;
        window.wasmModule = typeof rawModule === 'function'
          ? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
          : rawModule;
        setWasmModule(window.wasmModule);
      } catch (error) {
        console.error('WASM module loading failed:', error);
      }
    })();
  }, []);

  const convertRtfToPdf = async () => {
    setIsLoading(true);
    try {
      // 转换逻辑
    } finally {
      setIsLoading(false);
    }
  };

  const convertRtfToHtml = async () => {
    setIsLoading(true);
    try {
      // 转换逻辑
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div style={{ textAlign: 'center', padding: '50px' }}>
      <h1>RTF 文档格式转换</h1>
      <div style={{ display: 'flex', gap: '20px', justifyContent: 'center' }}>
        <button onClick={convertRtfToPdf} disabled={!wasmModule || isLoading}>
          {isLoading ? '转换中...' : '转换为 PDF'}
        </button>
        <button onClick={convertRtfToHtml} disabled={!wasmModule || isLoading}>
          {isLoading ? '转换中...' : '转换为 HTML'}
        </button>
      </div>
      {!wasmModule && <p>正在加载文档处理模块...</p>}
    </div>
  );
}

export default App;

方案优势与注意事项

方案优势:

  1. 数据本地化:所有处理在浏览器端完成,文档内容不会上传到任何服务器,对涉及敏感信息的文档处理场景更为合适。

  2. 降低服务器负载:转换任务由客户端承担,减少了后端服务器的计算压力。

  3. 实时响应:无需等待网络传输,转换完成后可立即预览或下载结果。

需要注意的问题:

  1. 字体资源管理:转换效果很大程度上取决于字体是否完整加载。如果文档使用了系统字体以外的特殊字体,需要提前准备对应的字体文件。不同操作系统和文档中使用的字体可能不同,需要根据实际需求补充字体资源。

  2. 文件体积控制:生成的 HTML 文件如果启用了图片内嵌,文件体积可能会显著增大。对于图片较多的文档,可以考虑将图片单独保存或使用外部链接。

  3. 内存管理 :每次转换完成后应调用 doc.Dispose() 释放资源,避免长时间运行导致的内存泄漏。对于大型文档,这一点尤为重要。

  4. 错误处理:实际项目中应增加更完善的错误处理逻辑,例如文件读取失败、转换超时、不支持的文档格式等场景。

  5. 性能考量:大型文档的转换可能需要一定时间,建议在转换过程中显示加载状态,并考虑添加超时机制,避免页面长时间无响应。

  6. 浏览器兼容性:WebAssembly 技术支持所有现代浏览器,但在较旧的浏览器版本中可能无法正常工作,需要根据目标用户群体进行评估。

总结

通过上述方案,可以在 React 前端实现 RTF 与 PDF、HTML 之间的格式转换。该方案基于 WebAssembly 技术,所有处理均在客户端完成,在保护用户数据隐私的同时,也减轻了后端服务的负担。

实现过程中的关键环节包括:正确配置 WebAssembly 模块的加载路径、管理虚拟文件系统中的字体和文档资源、根据需求选择合适的导出参数配置。开发者可以参考本文提供的代码示例,根据实际业务场景进行扩展和优化,例如支持更多输入格式、添加文档预览功能、或实现批量转换等。

相关推荐
3A Cloud1 小时前
Huashu Design:把 AI Agent 变成一间以 HTML 为画布的设计工作室
前端·人工智能·html
catastrophe_zy1 小时前
如何用 WebCodecs 在浏览器里实现高清录屏 —— 无插件、无水印、直接导出 MP4
前端·javascript·录屏
zhanghaha13141 小时前
Python进阶教程:24_Markdown 转 HTML 零基础超详细教程
开发语言·python·html
芳心粽伙饭1 小时前
HTML第七章 表格标签
前端·html
Moment2 小时前
太好了!NestJS 12 大版本转向 ESM,新项目默认构建换 Rspack
前端·javascript·后端
Eiceblue2 小时前
如何构建一个纯前端 Word 转 Markdown 转换器(React + WebAssembly)
前端·react.js·word
IMPYLH3 小时前
HTML 的 <option> 元素
服务器·数据库·html
平头哥~3 小时前
Day 15 | 不改一行 HTML,给页面加上引号、角标和标签
前端·javascript·css·html·css3·学习资料
Fluxart.ai4 小时前
GPT Image 2 国内怎么用?在 Flux Art 完成图片生成与编辑
前端·javascript·gpt