如何使用 CSS 页面规则在 itext html2pdf 中启用页面方向支持

How to enable support of page orientation in itext html2pdf using CSS page rule

我们有 HTML 的内容,我们将打印布局与混合横向和纵向页面结合在一起。页面方向与页面规则一起应用,以保持页面清洁和可维护。

html2pdf 似乎完全忽略了这一点,例如:


<html>
  <head>
    <title>UnitTest</title>
    <style type="text/css">* {
  box-sizing: border-box;
}

body {
  font-family: Arial, sans-serif;
}

@page page-portrait {
  size: A4;
  orientation: portait;
}
@page page-landscape {
  size: A4;
  orientation: landscape;
}

.page-portrait {
  page-break-after: always;
  page: page-portrait;
}
.page-landscapes {
  page-break-after: always;
  page: page-landscape;
}

</style>
  </head>
  
  <body>
    
    <div class="page-portrait">
        <p>This should be a portrait page</p>
    </div>
    
    <div class="page-landscapes">
        <p>This should be a landscape page</p>
    </div>
    
    <div class="page-portrait">
        <p>This should be a portrait page</p>
    </div>
    
    <div class="page-landscapes">
        <p>This should be a landscape page</p>
    </div>
    
  </body>
</html>

pdfHTML 目前不支持命名页面。您的 HTML 代码也有不一致之处:您希望以下块位于横向页面上

    <div class="page-landscapes">
        <p>This should be a landscape page</p>
    </div>

在上一个(纵向)块之后,您对纵向页面进行了分页。

奇偶 landscape/portrait 页来自 pdfHTML

如果您的目标是交替显示横向和纵向页面,您可以使用如下方法:

<html>
<head>
  <title>UnitTest</title>
  <style type="text/css">* {
    box-sizing: border-box;
  }

  body {
    font-family: Arial, sans-serif;
  }

  @page:left {
    size: A4 landscape;
  }
  @page:right {
    size: A4 portrait;
  }

  .page-portrait {
    page-break-after: always;
  }
  .page-landscapes {
    page-break-after: always;
  }

  </style>
</head>

<body>

<div class="page-portrait">
  <p>This should be a portrait page</p>
</div>

<div class="page-landscapes">
  <p>This should be a landscape page</p>
</div>

<div class="page-portrait">
  <p>This should be a portrait page</p>
</div>

<div class="page-landscapes">
  <p>This should be a landscape page</p>
</div>

</body>
</html>

中间转换为元素的自定义元素处理

您可以利用 HtmlConverter.convertToElements 获取中间元素列表并逐个处理它们,根据需要更改默认页面大小。请注意,只有当进入新页面的元素是 HTML.

中的顶级元素时,它才会起作用

我的回答将基于以下 HTML:

<html>
<head>
  <title>UnitTest</title>
  <style type="text/css">* {
    box-sizing: border-box;
  }

  body {
    font-family: Arial, sans-serif;
  }

  .page-portrait {
    page-break-after: always;
    page: page-portrait;
  }
  .page-landscapes {
    page-break-after: always;
    page: page-landscape;
  }

  </style>
</head>

<body>

<div class="page-landscapes">
  <p>This should be a portrait page</p>
</div>

<div class="page-portrait">
  <p>This should be a landscape page</p>
</div>

<div class="page-landscapes">
  <p>This should be a portrait page</p>
</div>

<div>
  <p>This should be a landscape page</p>
</div>

</body>
</html>

首先,让我们定义一个自定义标签工作者工厂,它将把自定义标签工作者分配给 <div> 元素,这些元素也可以包含我们负责分页的特殊 类作为 <div> 元素本身的标签工作者,它将负责将我们的特殊 类 传播到结果 layout 级别元素:

private static class CustomDivTagWorker extends DivTagWorker {
    public CustomDivTagWorker(IElementNode element, ProcessorContext context) {
        super(element, context);
    }

    @Override
    public void processEnd(IElementNode element, ProcessorContext context) {
        super.processEnd(element, context);
        IPropertyContainer result = getElementResult();
        if (result != null) {
            if ("page-portrait".equals(element.getAttribute("class"))) {
                result.setProperty(PAGE_BREAK_AFTER_RPOPERTY, "portrait");
            } else if ("page-landscapes".equals(element.getAttribute("class"))) {
                result.setProperty(PAGE_BREAK_AFTER_RPOPERTY, "landscape");
            }
        }
    }
}

private static class CustomTagWorkerFactory extends DefaultTagWorkerFactory {
    @Override
    public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
        if ("div".equals(tag.name())) {
            return new CustomDivTagWorker(tag, context);
        }
        return super.getCustomTagWorker(tag, context);
    }
}

现在主要代码将 HTML 转换为元素,将根元素逐一添加到结果 Document (来自布局模块),然后处理特殊 [=43= 的存在] 通过相应地更改页面大小。完整代码:

PdfDocument pdfDocument = new PdfDocument(new PdfWriter("path/to/out.pdf"));
ConverterProperties properties = new ConverterProperties();
properties.setTagWorkerFactory(new CustomTagWorkerFactory());
List<IElement> elements = HtmlConverter.convertToElements(new FileInputStream(sourceHTML), properties);
Document document = new Document(pdfDocument);
for (IElement element : elements) {
    if (element instanceof IBlockElement) {
        document.add((IBlockElement) element);
    } else if (element instanceof AreaBreak) {
        document.add((AreaBreak) element);
    } else {
        throw new RuntimeException();
    }

    if (element.hasProperty(PAGE_BREAK_AFTER_RPOPERTY)) {
        String prop = element.getProperty(PAGE_BREAK_AFTER_RPOPERTY);
        if ("portrait".equals(prop)) {
            document.getPdfDocument().setDefaultPageSize(PageSize.A4);
        } else {
            document.getPdfDocument().setDefaultPageSize(PageSize.A4.rotate());
        }
    }

}

pdfDocument.close();

到目前为止我们设法找到的最佳解决方案如下,它基于 DIV 级别 CSS class 分配来控制页面方向。它可以扩展到其他 HTML 标签 and/or CSS classes.

该解决方案旨在通知页面侦听器应用的正确方向,并根据需要旋转页面。标记工作人员检查 HTML 并查找特定的 CSS classes 以向页面侦听器发送已找到方向的信号。

这种方法的好处是它允许保留东西 'simple' 并使用 'HtmlConverter.convertToPdf' 方法避免丢失底层实现中的某些特性。

PageOrientationsEventHandler eventHandler = new PageOrientationsEventHandler(); pdfDocument.addEventHandler(PdfDocumentEvent.START_PAGE, eventHandler);

converterProperties.setTagWorkerFactory(

new DefaultTagWorkerFactory() {                 
    @Override
    public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
            if (HTML_DIV.equalsIgnoreCase(tag.name())) {
                return new CustomDivTagWorker(tag, context, pdfDocument, eventHandler);
            }
        return null;
    }
});

// Run the conversion
HtmlConverter.convertToPdf(fis, pdfDocument, converterProperties);

以及以下引用常量:

private static final String CSS_PAGE_PORTRAIT = "page";
private static final String CSS_PAGE_LANDSCAPE = "page-landscape";

以及以下引用的 classes:

private static final class PageOrientationsEventHandler implements IEventHandler {
    
    public static final PdfNumber PORTRAIT = new PdfNumber(0);
    public static final PdfNumber LANDSCAPE = new PdfNumber(90);
    
    private PdfNumber orientation = PORTRAIT;
    private Map<Integer, PdfNumber> orientationHistory = new HashMap<Integer, PdfNumber>();
    
    public void setOrientation(PdfNumber orientation) {
        this.orientation = orientation;
    }

    @Override
    public void handleEvent(Event currentEvent) {
    
        PdfDocumentEvent docEvent = (PdfDocumentEvent) currentEvent;
        PdfPage page = docEvent.getPage();
        
        // Check if we already rendered this page before
        int pageNr = docEvent.getDocument().getPageNumber(page);
        if(orientationHistory.containsKey(pageNr)) {
            // We did, use the same orientation
            // as we re-render we have lost track of the right orientation instrucrion
            // since it only works well on 1st pass, not on subsequent renders
            orientation = orientationHistory.get(pageNr);
        } else {
            
            // First render pass, store orientation
            orientationHistory.put(pageNr, orientation);
        }
        
        // Toggle orientation
        page.setIgnorePageRotationForContent(LANDSCAPE.equals(orientation));
        page.put(PdfName.Rotate, orientation);
    }
}

private static final class CustomDivTagWorker extends DivTagWorker {
    
    private IElementNode element;
    private PageOrientationsEventHandler eventHandler;

    public CustomDivTagWorker(IElementNode element, ProcessorContext context, PdfDocument pdfDocument, PageOrientationsEventHandler eventHandler) {
        super(element, context);
        this.element = element;
        this.eventHandler = eventHandler;
    }

    @Override
    public IPropertyContainer getElementResult() {
        
        IPropertyContainer baseElementResult = super.getElementResult();
        
        // We are interested in Divs only
        if (baseElementResult instanceof Div) {
            
            // Check landscape based on class identifier
            boolean landscape = false;
            String cssClass = element.getAttribute(AttributeConstants.CLASS);
            if (CSS_PAGE_LANDSCAPE.equals(cssClass)) {
                landscape=true;
            } else  if (CSS_PAGE_PORTRAIT.equals(cssClass)) {
                landscape=false;
            }
            
            // Flag requested orientation to our start page handler
            if(cssClass!=null && cssClass.length()>0)  {
                eventHandler.setOrientation(landscape?PageOrientationsEventHandler.LANDSCAPE:PageOrientationsEventHandler.PORTRAIT);
            }
        }
        return baseElementResult;
    }
}