Flutter中实现HtmlWidget渲染内容转PDF或HTML转PDF与Widget效果一致
Flutter中HTML渲染为Widget与PDF输出效果一致的解决方案
需求背景
需要将无JavaScript依赖的简单HTML(如菜谱、路线指引)同时渲染为Flutter Widget和PDF,且两者显示效果完全一致。当前使用flutter_widget_from_html的HtmlWidget组件渲染HTML,能很好支持自定义CSS类样式。
当前HTML渲染实现
HtmlWidget( "<html><body><span>a,\nb,c</span></body></html>", customStylesBuilder: (element) { if (element.classes.contains('a-css-class')) { return { 'color': '#5cb85c', 'padding': '3px' }; } return null; } );
现有PDF转换尝试及问题
使用printing、pdf、htmltopdfwidgets包转换HTML为PDF,代码如下:
import 'package:flutter/material.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as HTMLtoPDFWidgets; void main() async { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: const MyHomePage(), ); } } class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: Center( child: IconButton( icon: const Icon(Icons.print), onPressed: () async { await Printing.layoutPdf( onLayout: (PdfPageFormat format) async { String body = "<html><body><div><span>a,\nb,c</span></body></html>"; final pdf = pw.Document(); final widgets = await HTMLtoPDFWidgets.HTMLToPdf().convert(body, wrapInParagraph: true // 无效果 ); pdf.addPage(pw.MultiPage(build: (context) => widgets)); return await pdf.save(); }); }))); } }
转换效果存在两个核心问题:
<span>内的换行被原样保留,不符合行内元素的渲染规则- 无法像
HtmlWidget那样处理自定义CSS类样式
解决方案
方案一:复用flutter_widget_from_html解析逻辑生成PDF
flutter_widget_from_html的底层依赖fwfh_core包实现HTML解析和样式计算,可以复用这部分逻辑,将解析后的DOM节点和样式直接转换为pdf包的pw.Widget,从根源保证样式一致。
实现步骤
- 用
fwfh_core的解析器获取带样式的DOM节点树 - 递归遍历节点,将其转换为对应的PDF组件
- 复用原有的
customStylesBuilder逻辑处理自定义样式
示例代码
import 'package:fwfh_core/fwfh_core.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; Future<pw.Document> generatePdfFromHtml(String rawHtml) async { final pdf = pw.Document(); // 复用原样式解析逻辑 final parser = HtmlParser( rawHtml, customStylesBuilder: (element) { if (element.classes.contains('a-css-class')) { return { 'color': '#5cb85c', 'padding': '3px', }; } return null; }, ); final rootElement = await parser.parse(); // 递归转换DOM节点为PDF Widget pw.Widget convertToPdfWidget(StyledElement element) { if (element is TextElement) { // 行内元素的换行替换为空格,和HtmlWidget保持一致 final processedText = element.text.replaceAll('\n', ' '); final styles = element.styles; return pw.Text( processedText, style: pw.TextStyle( color: PdfColor.fromHex(styles.color ?? '#000000'), ), ); } else if (element is StyledElement) { final childrenWidgets = element.children.map(convertToPdfWidget).toList(); final styles = element.styles; // 处理padding等容器样式 double paddingValue = 0; if (styles.padding != null) { paddingValue = double.parse(styles.padding!.replaceAll('px', '')); } return pw.Container( padding: pw.EdgeInsets.all(paddingValue), child: pw.Column(children: childrenWidgets), ); } return pw.SizedBox.shrink(); } final pdfContent = convertToPdfWidget(rootElement); pdf.addPage(pw.MultiPage(build: (context) => [pdfContent])); return pdf; }
方案二:优化现有HTML转PDF工具的样式匹配
如果不想深度改造flutter_widget_from_html,可以针对htmltopdfwidgets的问题做预处理:
- 处理span内换行:将span标签内的换行替换为空格,符合行内元素渲染规则
- 注入自定义CSS:将
customStylesBuilder中的样式转换为内嵌CSS,添加到HTML头部
示例代码
void generatePdf() async { String rawHtml = "<html><body><span>a,\nb,c</span><div class='a-css-class'>测试样式</div></body></html>"; // 预处理1:替换span内的换行 rawHtml = rawHtml.replaceAll(RegExp(r'(?<=<span>)(.*?)\n(.*?)(?=</span>)'), r'$1 $2'); // 预处理2:注入自定义CSS final customCss = ''' <style> .a-css-class { color: #5cb85c; padding: 3px; } </style> '''; final processedHtml = rawHtml.replaceFirst('<head>', '<head>$customCss'); // 原PDF转换逻辑 final pdf = pw.Document(); final widgets = await HTMLtoPDFWidgets.HTMLToPdf().convert(processedHtml); pdf.addPage(pw.MultiPage(build: (context) => widgets)); await Printing.layoutPdf(onLayout: (_) => pdf.save()); }
方案三:Widget转图片再生成PDF(不推荐)
将HtmlWidget渲染为图片后插入PDF,这种方式简单但存在分辨率低、文本不可选中、分页困难等问题,仅适合内容简短的场景:
import 'package:flutter/rendering.dart'; import 'package:image/image.dart' as img; Future<pw.Document> generatePdfFromWidget(Widget widget, BuildContext context) async { final pdf = pw.Document(); final key = GlobalKey(); // 渲染Widget为图片 await Navigator.of(context).push(MaterialPageRoute( builder: (ctx) => RepaintBoundary(key: key, child: widget), )); final boundary = key.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ImageByteFormat.png); final pngBytes = byteData!.buffer.asUint8List(); final imageFile = img.decodePng(pngBytes)!; // 添加图片到PDF pdf.addPage(pw.Page( build: (context) => pw.Image(pw.MemoryImage(img.encodePng(imageFile))), )); return pdf; }
内容的提问来源于stack exchange,提问作者bliako
相关产品推荐
相关产品推荐

