/docx4j-export-fo/src/main/java/org/docx4j/convert/out/fo/LayoutMasterSetBuilder.java

http://github.com/plutext/docx4j · Java · 438 lines · 222 code · 92 blank · 124 comment · 29 complexity · 94abc592d291a8b93e0def2453035c01 MD5 · raw file

  1. /*
  2. Licensed to Plutext Pty Ltd under one or more contributor license agreements.
  3. * This file is part of docx4j.
  4. docx4j is licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. package org.docx4j.convert.out.fo;
  15. import org.docx4j.UnitsOfMeasurement;
  16. import org.docx4j.XmlUtils;
  17. import org.docx4j.convert.out.FOSettings;
  18. import org.docx4j.convert.out.common.AbstractWmlConversionContext;
  19. import org.docx4j.convert.out.common.ConversionSectionWrapper;
  20. import org.docx4j.convert.out.common.preprocess.PartialDeepCopy;
  21. import org.docx4j.events.EventFinished;
  22. import org.docx4j.events.StartEvent;
  23. import org.docx4j.events.WellKnownProcessSteps;
  24. import org.docx4j.jaxb.Context;
  25. import org.docx4j.model.structure.HeaderFooterPolicy;
  26. import org.docx4j.model.structure.PageDimensions;
  27. import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
  28. import org.docx4j.openpackaging.parts.WordprocessingML.FontTablePart;
  29. import org.docx4j.openpackaging.parts.relationships.Namespaces;
  30. import org.plutext.jaxb.xslfo.ConditionalPageMasterReference;
  31. import org.plutext.jaxb.xslfo.LayoutMasterSet;
  32. import org.plutext.jaxb.xslfo.ObjectFactory;
  33. import org.plutext.jaxb.xslfo.OddOrEvenType;
  34. import org.plutext.jaxb.xslfo.PagePositionType;
  35. import org.plutext.jaxb.xslfo.PageSequenceMaster;
  36. import org.plutext.jaxb.xslfo.RegionAfter;
  37. import org.plutext.jaxb.xslfo.RegionBefore;
  38. import org.plutext.jaxb.xslfo.RegionBody;
  39. import org.plutext.jaxb.xslfo.RepeatablePageMasterAlternatives;
  40. import org.plutext.jaxb.xslfo.SimplePageMaster;
  41. import org.slf4j.Logger;
  42. import org.slf4j.LoggerFactory;
  43. import org.w3c.dom.DocumentFragment;
  44. import org.w3c.dom.Node;
  45. import java.util.HashMap;
  46. import java.util.List;
  47. import java.util.Map;
  48. import java.util.Set;
  49. import java.util.TreeSet;
  50. /**
  51. * A description of how this stuff works
  52. * can be found at /docs/headers_footers.docx
  53. *
  54. * Its not possible to let FOP set the height (@extent) of the header and footer regions
  55. * automatically: http://apache-fop.1065347.n5.nabble.com/Auto-size-header-to-fit-with-content-td4455.html
  56. *
  57. * So we need to set the height (@extent) of the header and footer regions explicitly.
  58. *
  59. * We do that by creating a temp FO file which contains essentially just the headers/footers,
  60. * then interrogating FOP's area tree representation to find the height of each.
  61. *
  62. * So:
  63. * 1. create LayoutMasterSet (with large extents)
  64. * 2. generate area tree from that
  65. * 3. return LayoutMasterSet (with required extents)
  66. *
  67. * @author jharrop
  68. *
  69. */
  70. public class LayoutMasterSetBuilder {
  71. protected static Logger log = LoggerFactory.getLogger(LayoutMasterSetBuilder.class);
  72. private static org.plutext.jaxb.xslfo.ObjectFactory factory;
  73. public static DocumentFragment getLayoutMasterSetFragment(AbstractWmlConversionContext context) {
  74. LayoutMasterSet lms = getFoLayoutMasterSet(context);
  75. // Set suitable extents, for which we need area tree
  76. FOSettings foSettings = (FOSettings)context.getConversionSettings();
  77. if ( !foSettings.lsLayoutMasterSetCalculationInProgress()) // Avoid infinite loop
  78. // Can't just do it where foSettings.getApacheFopMime() is not MimeConstants.MIME_FOP_AREA_TREE,
  79. // since TOC functionality uses that.
  80. {
  81. fixExtents( lms, context, true);
  82. }
  83. org.w3c.dom.Document document = XmlUtils.marshaltoW3CDomDocument(lms, Context.getXslFoContext() );
  84. DocumentFragment docfrag = document.createDocumentFragment();
  85. docfrag.appendChild(document.getDocumentElement());
  86. return docfrag;
  87. }
  88. private static void fixExtents(LayoutMasterSet lms, AbstractWmlConversionContext context, boolean useXSLT) {
  89. WordprocessingMLPackage wordMLPackage = context.getWmlPackage();
  90. StartEvent startEvent = new StartEvent( wordMLPackage, WellKnownProcessSteps.FO_EXTENTS );
  91. startEvent.publish();
  92. // log.debug(wordMLPackage.getMainDocumentPart().getXML());
  93. if(log.isDebugEnabled()) {
  94. log.debug("incoming LMS: " + XmlUtils.marshaltoString(lms, Context.getXslFoContext()));
  95. }
  96. // Make a copy of it
  97. Set<String> relationshipTypes = new TreeSet<String>();
  98. relationshipTypes.add(Namespaces.DOCUMENT);
  99. relationshipTypes.add(Namespaces.HEADER);
  100. relationshipTypes.add(Namespaces.FOOTER);
  101. //those are probably not affected but get visited by the
  102. //default TraversalUtil.
  103. relationshipTypes.add(Namespaces.ENDNOTES);
  104. relationshipTypes.add(Namespaces.FOOTNOTES);
  105. relationshipTypes.add(Namespaces.COMMENTS);
  106. WordprocessingMLPackage hfPkg;
  107. try {
  108. hfPkg = (WordprocessingMLPackage) PartialDeepCopy.process(wordMLPackage, relationshipTypes);
  109. FOPAreaTreeHelper.trimContent(hfPkg);
  110. org.w3c.dom.Document areaTree = FOPAreaTreeHelper.getAreaTreeViaFOP( hfPkg, useXSLT);
  111. log.debug(XmlUtils.w3CDomNodeToString(areaTree));
  112. Map<String, Integer> headerBpda = new HashMap<String, Integer>();
  113. Map<String, Integer> footerBpda = new HashMap<String, Integer>();
  114. FOPAreaTreeHelper.calculateHFExtents(areaTree, headerBpda, footerBpda);
  115. FOPAreaTreeHelper.adjustLayoutMasterSet(lms, context.getSections(), headerBpda, footerBpda);
  116. /* Don't deleteEmbeddedFontTempFiles here, since they may be required by FOP later.
  117. * And they should get deleted later anyway...
  118. *
  119. FontTablePart ftp = hfPkg.getMainDocumentPart().getFontTablePart();
  120. if (ftp!=null) {
  121. ftp.deleteEmbeddedFontTempFiles();
  122. }
  123. */
  124. } catch (Exception e) {
  125. log.error(e.getMessage(), e);
  126. }
  127. if(log.isDebugEnabled()) {
  128. log.debug("resulting LMS: " + XmlUtils.marshaltoString(lms, Context.getXslFoContext()));
  129. }
  130. new EventFinished(startEvent).publish();
  131. }
  132. /**
  133. * For XSLFOExporterNonXSLT
  134. * @since 3.0
  135. *
  136. */
  137. public static void appendLayoutMasterSetFragment(AbstractWmlConversionContext context, Node foRoot) {
  138. LayoutMasterSet lms = getFoLayoutMasterSet(context);
  139. // Set suitable extents, for which we need area tree
  140. FOSettings foSettings = (FOSettings)context.getConversionSettings();
  141. if ( !foSettings.lsLayoutMasterSetCalculationInProgress()) // Avoid infinite loop
  142. // Can't just do it where foSettings.getApacheFopMime() is not MimeConstants.MIME_FOP_AREA_TREE,
  143. // since TOC functionality uses that.
  144. {
  145. fixExtents( lms, context, false);
  146. }
  147. org.w3c.dom.Document document = XmlUtils.marshaltoW3CDomDocument(lms, Context.getXslFoContext() );
  148. XmlUtils.treeCopy(document.getDocumentElement(), foRoot);
  149. }
  150. private static LayoutMasterSet getFoLayoutMasterSet(AbstractWmlConversionContext context) {
  151. LayoutMasterSet lms = getFactory().createLayoutMasterSet();
  152. List<ConversionSectionWrapper> sections = context.getSections().getList();
  153. ConversionSectionWrapper section = null;
  154. for(int i=0; i<sections.size(); i++) {
  155. section = sections.get(i);
  156. HeaderFooterPolicy hf = section.getHeaderFooterPolicy();
  157. String sectionName = "s" + Integer.toString(i + 1);
  158. // FIRST, create simple-page-masters
  159. // has first header or footer?
  160. if (hf.getFirstHeader()!=null || hf.getFirstFooter()!=null) {
  161. // per spec, HeaderFooterPolicy checks the titlePg elememt
  162. lms.getSimplePageMasterOrPageSequenceMaster().add(
  163. createSimplePageMaster(sectionName + "-firstpage",
  164. section.getPageDimensions(),
  165. "firstpage",
  166. (hf.getFirstHeader()!=null),
  167. (hf.getFirstFooter()!=null) ));
  168. }
  169. // has even or odd header or footer?
  170. /*
  171. * <w:headerReference w:type="even" r:id="rId12"/>
  172. * <w:headerReference w:type="default" r:id="rId13"/>
  173. *
  174. * the default one is treated as odd.
  175. */
  176. if (hf.getEvenHeader()!=null || hf.getEvenFooter()!=null) {
  177. lms.getSimplePageMasterOrPageSequenceMaster().add(
  178. createSimplePageMaster(sectionName + "-evenpage",
  179. section.getPageDimensions(),
  180. "evenpage",
  181. (hf.getEvenHeader()!=null),
  182. (hf.getEvenFooter()!=null) ));
  183. // the xslt outputs a "-default" page as the odd-page
  184. }
  185. if (hf.getDefaultHeader()!=null
  186. || hf.getDefaultFooter()!=null) {
  187. lms.getSimplePageMasterOrPageSequenceMaster().add(
  188. createSimplePageMaster(sectionName + "-default",
  189. section.getPageDimensions(),
  190. "default",
  191. (hf.getDefaultHeader()!=null),
  192. (hf.getDefaultFooter()!=null) ));
  193. }
  194. // simple: no headers and footers - after the first page anyway/
  195. // We still need this where there is just a first page header/footer,
  196. // since otherwise there'd be no page sequence for any content
  197. // after the first page, and you'd get:
  198. // org.apache.fop.fo.pagination.PageProductionException:
  199. // Subsequences exhausted in page-sequence-master ..., cannot recover.
  200. //
  201. // <w:sectPr>
  202. // <w:headerReference w:type="first" r:id="rId7"/>
  203. // </w:sectPr>
  204. if (
  205. (hf.getDefaultHeader() == null) && (hf.getDefaultFooter() == null)) {
  206. lms.getSimplePageMasterOrPageSequenceMaster().add(
  207. createSimplePageMaster(sectionName + "-simple",
  208. section.getPageDimensions(),
  209. "simple",
  210. true, true));
  211. }
  212. // SECOND, create page-sequence-masters
  213. lms.getSimplePageMasterOrPageSequenceMaster().add(
  214. createPageSequenceMaster(hf, sectionName ) );
  215. }
  216. //
  217. return lms;
  218. }
  219. private static PageSequenceMaster createPageSequenceMaster(HeaderFooterPolicy hf,
  220. String sectionName ) {
  221. boolean noHeadersFootersAfterFirstPage = true;
  222. PageSequenceMaster psm = getFactory().createPageSequenceMaster();
  223. psm.setMasterName(sectionName);
  224. RepeatablePageMasterAlternatives rpma = getFactory().createRepeatablePageMasterAlternatives();
  225. psm.getSinglePageMasterReferenceOrRepeatablePageMasterReferenceOrRepeatablePageMasterAlternatives().add(rpma);
  226. // has first header or footer?
  227. if (hf.getFirstHeader()!=null || hf.getFirstFooter()!=null) {
  228. ConditionalPageMasterReference cpmr1 = getFactory().createConditionalPageMasterReference();
  229. cpmr1.setMasterReference(sectionName+"-firstpage");
  230. cpmr1.setPagePosition(PagePositionType.FIRST);
  231. rpma.getConditionalPageMasterReference().add(cpmr1);
  232. }
  233. if (hf.getEvenHeader()!=null || hf.getEvenFooter()!=null) {
  234. ConditionalPageMasterReference cpmr2 = getFactory().createConditionalPageMasterReference();
  235. cpmr2.setMasterReference(sectionName+"-evenpage");
  236. //cpmr2.setPagePosition(PagePositionType.FIRST);
  237. cpmr2.setOddOrEven(OddOrEvenType.EVEN);
  238. rpma.getConditionalPageMasterReference().add(cpmr2);
  239. // the xslt outputs a "-default" page as the odd-page
  240. ConditionalPageMasterReference cpmr3 = getFactory().createConditionalPageMasterReference();
  241. cpmr3.setMasterReference(sectionName+"-default");
  242. //cpmr3.setPagePosition(PagePositionType.FIRST);
  243. cpmr3.setOddOrEven(OddOrEvenType.ODD);
  244. rpma.getConditionalPageMasterReference().add(cpmr3);
  245. noHeadersFootersAfterFirstPage = false;
  246. } else if (hf.getDefaultHeader()!=null || hf.getDefaultFooter()!=null) {
  247. ConditionalPageMasterReference cpmr4 = getFactory().createConditionalPageMasterReference();
  248. cpmr4.setMasterReference(sectionName+"-default");
  249. //cpmr4.setPagePosition(PagePositionType.FIRST);
  250. rpma.getConditionalPageMasterReference().add(cpmr4);
  251. noHeadersFootersAfterFirstPage = false;
  252. }
  253. if (noHeadersFootersAfterFirstPage) {
  254. ConditionalPageMasterReference cpmr5 = getFactory().createConditionalPageMasterReference();
  255. cpmr5.setMasterReference(sectionName+"-simple");
  256. //cpmr5.setPagePosition(PagePositionType.FIRST);
  257. rpma.getConditionalPageMasterReference().add(cpmr5);
  258. }
  259. return psm;
  260. }
  261. private static SimplePageMaster createSimplePageMaster(
  262. String masterName, PageDimensions page, String appendRegionName,
  263. boolean needBefore, boolean needAfter) {
  264. // This method uses dummy large extents
  265. // A later step fixes them.
  266. SimplePageMaster spm = getFactory().createSimplePageMaster();
  267. spm.setMasterName(masterName);
  268. // dimensions.
  269. // <w:pgSz w:w="12240" w:h="15840"/>
  270. // <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/>
  271. spm.setPageHeight( UnitsOfMeasurement.twipToBest(page.getPgSz().getH().intValue() ));
  272. spm.setPageWidth( UnitsOfMeasurement.twipToBest(page.getPgSz().getW().intValue() ));
  273. spm.setMarginLeft( UnitsOfMeasurement.twipToBest(page.getPgMar().getLeft().intValue() ) );
  274. spm.setMarginRight( UnitsOfMeasurement.twipToBest(page.getPgMar().getRight().intValue()) );
  275. /*
  276. * Region before & after live in region body margins:
  277. *
  278. * Per http://www.w3.org/TR/xsl/#fo_region-body
  279. *
  280. * The body region should be sized and positioned within the fo:simple-page-master
  281. * so that there is room for the areas returned by the flow that is assigned to the
  282. * fo:region-body and for any desired side regions, that is, fo:region-before,
  283. * fo:region-after, fo:region-start and fo:region-end's that are to be placed on the same page.
  284. *
  285. * These side regions are positioned within the content-rectangle of the page-reference-area.
  286. * The margins on the fo:region-body are used to position the region-viewport-area for the
  287. * fo:region-body and to leave space for the other regions that surround the fo:region-body.
  288. * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  289. *
  290. * The spacing between the last four regions and the fo:region-body is determined by subtracting
  291. * the relevant extent trait on the side regions from the trait that corresponds to the "margin-x"
  292. * property on the fo:region-body.
  293. */
  294. RegionBody rb = getFactory().createRegionBody();
  295. rb.setMarginLeft("0mm");
  296. rb.setMarginRight("0mm");
  297. rb.setColumnCount(String.valueOf(page.getColsNum())); //Number of Equal Width Columns
  298. rb.setColumnGap(UnitsOfMeasurement.twipToBest(page.getColsSpacing())); //Spacing Between Equal Width Columns
  299. float halfPageHeight = page.getPgSz().getH().intValue()/40; // convert from twips, then * 0.5
  300. String halfPageHeightPts = halfPageHeight + "pt";
  301. spm.setRegionBody(rb);
  302. if (needBefore) {
  303. //Header
  304. RegionBefore rBefore = getFactory().createRegionBefore();
  305. rBefore.setRegionName("xsl-region-before-"+appendRegionName);
  306. spm.setRegionBefore(rBefore);
  307. // Margin top on SPM is space between the page edge and the start of the header
  308. int marginTopTwips
  309. = page.getHeaderMargin();
  310. spm.setMarginTop( UnitsOfMeasurement.twipToBest(marginTopTwips ) );
  311. // Size header manually
  312. rBefore.setExtent( halfPageHeightPts); // A4 portrait is 297mm high
  313. // Leave room for this region in body margin
  314. rb.setMarginTop(halfPageHeightPts);
  315. } else {
  316. // No header
  317. spm.setMarginTop( UnitsOfMeasurement.twipToBest(page.getPgMar().getTop().intValue() ) );
  318. }
  319. if (needAfter) {
  320. // Footer
  321. RegionAfter rAfter = getFactory().createRegionAfter();
  322. rAfter.setRegionName("xsl-region-after-"+appendRegionName);
  323. spm.setRegionAfter(rAfter);
  324. int marginBottomTwips= page.getFooterMargin();
  325. spm.setMarginBottom( UnitsOfMeasurement.twipToBest(marginBottomTwips) );
  326. // Size footer manually
  327. rAfter.setExtent( halfPageHeightPts); // A4 portrait is 297mm high
  328. // Leave room for this region in body margin
  329. rb.setMarginBottom(halfPageHeightPts );
  330. } else {
  331. // No footer
  332. spm.setMarginBottom( UnitsOfMeasurement.twipToBest(page.getPgMar().getBottom().intValue()) );
  333. }
  334. return spm;
  335. }
  336. private static ObjectFactory getFactory() {
  337. if (factory == null) factory = new ObjectFactory();
  338. return factory;
  339. }
  340. }