Reportlab中bookmarkPage页面定位最优实现方案探讨
Reportlab自定义PDF大纲跳转视图的实现与优化疑问
我使用Python和Reportlab自动生成长文档,希望借助PDF大纲树实现便捷导航。根据官方文档,canvas.bookmarkPage提供多种跳转后的页面视图调整选项,默认是Fit(页面适配窗口),但从用户体验角度,我更倾向于FitH(宽度适配,目标位置在屏幕顶部)或XYZ(保留缩放比例,目标位置在屏幕顶部)。不过除基础Fit外,使用其他适配选项时必须提供对应坐标。
我未能找到相关说明、示例或代码片段,耗时许久才想出解决方案。现分享该方案,询问这是否为最优实现方式,或是我忽略了基础方法。
核心实现思路
核心是SmartParagraph类,它会记录自身绘制后的位置。最初我在afterFlowable()方法中调用flowable.canv.absolutePosition(0,0)获取位置,但返回始终是0, 0,显然此时flowable或canvas已丢失位置信息。研究源码后发现,在draw()方法执行后,Flowable仍能获取自身位置。
因此我实现了SmartParagraph作为Paragraph的子类,在绘制后存储自身位置,供后续流程使用。示例代码将生成一个测试PDF,包含2个使用FitH缩放的标题和2个使用基础Fit缩放的标题。
示例代码
import typing from reportlab.lib.styles import ParagraphStyle as PS from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus.flowables import Flowable from reportlab.platypus import PageBreak, Spacer from reportlab.platypus.paragraph import Paragraph from reportlab.platypus.doctemplate import SimpleDocTemplate from reportlab.lib.units import cm class SmartParagraph(Paragraph): def __init__(self, text, *args, **kwds): """This paragraph remembers its position on the canvas""" super(SmartParagraph, self).__init__(text, *args, **kwds) self._pos: typing.Tuple[int, int] = None def draw(self): super(SmartParagraph, self).draw() self._pos = self.canv.absolutePosition(0, 0) def get_pos(self) -> typing.Tuple[int, int]: return self._pos class CustomDocTemplate(SimpleDocTemplate): def __init__(self, filename, outline_levels: int = 4, **kwargs): super(CustomDocTemplate, self).__init__(filename, **kwargs) self._bookmark_keys = list() if not isinstance(outline_levels, int) and outline_levels < 1: raise ValueError("Outline levels must be integer and at least 1") self._outline_levels = {f'Heading{level+1}': level for level in range(outline_levels)} # Map of kind: Heading1 -> 0 # Heading 1 is level 0, I dont make the rules def afterFlowable(self, flowable: Flowable): """Registers TOC entries.""" if isinstance(flowable, Paragraph): flowable: Paragraph text = flowable.getPlainText() style = flowable.style.name if style in self._outline_levels: level = self._outline_levels[style] else: return if text not in self._bookmark_keys: key = text self._bookmark_keys.append(key) else: # There might headings with identical text, yet they need a different key # Keys are stored in a list and incremented if a duplicate is found cnt = 1 while True: key = text + str(cnt) if key not in self._bookmark_keys: self._bookmark_keys.append(key) break cnt += 1 if isinstance(flowable, SmartParagraph): # Only smart paragraphs know their own position x, y = flowable.get_pos() y += flowable.style.fontSize + 15 self.canv.bookmarkPage(key, fit="FitH", top=y) else: # Dumb paragraphs need to show the whole page self.canv.bookmarkPage(key) self.canv.addOutlineEntry(title=text, key=key, level=level) def _endBuild(self): """Override of parent function. Shows outline tree by default when opening PDF.""" super(CustomDocTemplate, self)._endBuild() self.canv.showOutline() story = list() story.append(SmartParagraph('First Smart Heading', getSampleStyleSheet()['h1'])) story.append(Paragraph('Text in first heading')) story.append(Spacer(1, 0.5 * cm)) story.append(SmartParagraph('First Sub Smart Heading', getSampleStyleSheet()['h2'])) story.append(Paragraph('Text in first sub heading')) story.append(Spacer(1, 0.5 * cm)) story.append(Paragraph('Second Sub Dumb Heading', getSampleStyleSheet()['h2'])) story.append(Paragraph('Text in second sub heading')) story.append(PageBreak()) story.append(Paragraph('Last Dumb Heading', getSampleStyleSheet()['h1'])) story.append(Paragraph('Text in last heading', PS('body'))) doc = CustomDocTemplate('mintoc.pdf') doc.multiBuild(story)
疑问
是否有更优的解决方案?
内容的提问来源于stack exchange,提问作者Aranvir
相关产品推荐
相关产品推荐

