You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@poi.apache.org by fa...@apache.org on 2026/09/03 18:49:43 UTC
(poi) branch trunk updated: Reduce the per-run work done when opening a Word document (#1243)
This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git
The following commit(s) were added to refs/heads/trunk by this push:
new 961374b07a Reduce the per-run work done when opening a Word document (#1243)
961374b07a is described below
commit 961374b07af04784e112645d1a763c0822aff703
Author: PJ Fanning <pj...@users.noreply.github.com>
AuthorDate: Thu Sep 3 19:49:37 2026 +0100
Reduce the per-run work done when opening a Word document (#1243)
Opening a .docx eagerly builds an XWPFParagraph for every paragraph and
an XWPFRun for every run, so anything done per run in those
constructors is paid by every document.
XWPFRun built a ~200 character XPath query by string concatenation and
ran selectPath() on every run to look for a w:drawing inside an
mc:AlternateContent choice, even though almost every run is plain text.
The two query strings are now static constants, and the picture/drawing
block is skipped unless the run actually has a w:pict or w:drawing
child or an mc:AlternateContent element - the latter checked with a
plain DOM scan of the direct children, which is what the XPath matched
anyway. Runs that do hold picture content take exactly the same path as
before.
XWPFParagraph opened a cursor and ran selectPath("child::*") over the
children of every run to find footnote references, which are rare.
CTFtnEdnRef can only occur as a w:footnoteReference or w:endnoteReference
child of a run, so the walk is now skipped when the run has neither. The
cursor walk itself is unchanged, keeping document order for runs that
mix footnote and endnote references.
XWPFTable built a throw-away XWPFParagraph, and therefore all of its
XWPFRuns, for every paragraph of every cell purely to precompute the
text field - after XWPFTableRow had already built the same objects for
the same cells. Every table cell was parsed twice on open. It now reads
the paragraphs from the row it just constructed.
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
---
.../apache/poi/xwpf/usermodel/XWPFParagraph.java | 7 ++
.../org/apache/poi/xwpf/usermodel/XWPFRun.java | 76 +++++++++++++++++-----
.../org/apache/poi/xwpf/usermodel/XWPFTable.java | 12 ++--
3 files changed, 75 insertions(+), 20 deletions(-)
diff --git a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java
index b7014df5ff..b9ead68c2c 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFParagraph.java
@@ -71,6 +71,13 @@ public class XWPFParagraph implements IBodyElement, IRunBody, ISDTContents, Para
for (XWPFRun run : runs) {
CTR r = run.getCTR();
+ // Footnote/endnote references are rare - only pay for the cursor walk below
+ // when the run actually holds at least one of them. CTFtnEdnRef can only occur
+ // as a w:footnoteReference or a w:endnoteReference child of the run.
+ if (r.sizeOfFootnoteReferenceArray() == 0 && r.sizeOfEndnoteReferenceArray() == 0) {
+ continue;
+ }
+
// Check for bits that only apply when attached to a core document
// TODO Make this nicer by tracking the XWPFFootnotes directly
try (XmlCursor c = r.newCursor()) {
diff --git a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFRun.java b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFRun.java
index 0a0dc13ca4..f6e6b49b8b 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFRun.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFRun.java
@@ -75,6 +75,21 @@ import org.xml.sax.SAXException;
* XWPFRun object defines a region of text with a common set of properties
*/
public class XWPFRun implements ISDTContents, IRunElement, CharacterRun {
+ /**
+ * XPath selecting the {@code w:drawing} elements held in an {@code mc:AlternateContent} choice.
+ * Hoisted out of the constructor so that it is not rebuilt for every run of every document.
+ */
+ private static final String ALTERNATE_CONTENT_DRAWING_PATH =
+ "declare namespace w='" + XSSFRelation.NS_WORDPROCESSINGML + "' " +
+ "declare namespace mc='" + PackageNamespaces.MARKUP_COMPATIBILITY + "' " +
+ "./mc:AlternateContent/mc:Choice/w:drawing";
+
+ /**
+ * XPath selecting all the {@code w:t} elements below a picture or drawing.
+ */
+ private static final String PICTURE_TEXT_PATH =
+ "declare namespace w='" + XSSFRelation.NS_WORDPROCESSINGML + "' .//w:t";
+
private final CTR run;
private final String pictureText;
private final IRunBody parent;
@@ -93,30 +108,39 @@ public class XWPFRun implements ISDTContents, IRunElement, CharacterRun {
* reserve already occupied drawing ids, so reserving new ids later will
* not corrupt the document
*/
- for (CTDrawing ctDrawing : r.getDrawingArray()) {
- for (CTAnchor anchor : ctDrawing.getAnchorArray()) {
- if (anchor.getDocPr() != null) {
- getDocument().getDrawingIdManager().reserve(anchor.getDocPr().getId());
+ if (r.sizeOfDrawingArray() > 0) {
+ for (CTDrawing ctDrawing : r.getDrawingArray()) {
+ for (CTAnchor anchor : ctDrawing.getAnchorArray()) {
+ if (anchor.getDocPr() != null) {
+ getDocument().getDrawingIdManager().reserve(anchor.getDocPr().getId());
+ }
}
- }
- for (CTInline inline : ctDrawing.getInlineArray()) {
- if (inline.getDocPr() != null) {
- getDocument().getDrawingIdManager().reserve(inline.getDocPr().getId());
+ for (CTInline inline : ctDrawing.getInlineArray()) {
+ if (inline.getDocPr() != null) {
+ getDocument().getDrawingIdManager().reserve(inline.getDocPr().getId());
+ }
}
}
}
+ pictures = new ArrayList<>();
+ charts = new ArrayList<>();
+
+ // The vast majority of runs are plain text and hold neither a picture nor a drawing,
+ // so check for that cheaply first and skip the XPath queries altogether if we can.
+ if (!mayHavePictureContent(r)) {
+ pictureText = "";
+ return;
+ }
+
// Look for any text in any of our pictures or drawings
StringBuilder text = new StringBuilder();
List<XmlObject> pictTextObjs = new ArrayList<>();
pictTextObjs.addAll(Arrays.asList(r.getPictArray()));
pictTextObjs.addAll(Arrays.asList(r.getDrawingArray()));
- pictTextObjs.addAll(Arrays.asList(r.selectPath(
- "declare namespace w='" + XSSFRelation.NS_WORDPROCESSINGML + "' " +
- "declare namespace mc='" + PackageNamespaces.MARKUP_COMPATIBILITY + "' " +
- "./mc:AlternateContent/mc:Choice/w:drawing")));
+ pictTextObjs.addAll(Arrays.asList(r.selectPath(ALTERNATE_CONTENT_DRAWING_PATH)));
for (XmlObject o : pictTextObjs) {
- XmlObject[] ts = o.selectPath("declare namespace w='" + XSSFRelation.NS_WORDPROCESSINGML + "' .//w:t");
+ XmlObject[] ts = o.selectPath(PICTURE_TEXT_PATH);
for (XmlObject t : ts) {
NodeList kids = t.getDomNode().getChildNodes();
for (int n = 0; n < kids.getLength(); n++) {
@@ -134,8 +158,6 @@ public class XWPFRun implements ISDTContents, IRunElement, CharacterRun {
// Do we have any embedded pictures or charts?
// Pictures are a different CTPicture, under the drawingml namespace.
// Charts are relations and use the CTRelId type.
- pictures = new ArrayList<>();
- charts = new ArrayList<>();
for (XmlObject o : pictTextObjs) {
for (CTPicture pict : getCTPictures(o)) {
XWPFPicture picture = new XWPFPicture(pict, this);
@@ -153,6 +175,30 @@ public class XWPFRun implements ISDTContents, IRunElement, CharacterRun {
}
}
+ /**
+ * Cheap test for whether the run could possibly hold a picture, a drawing or an
+ * {@code mc:AlternateContent} element (which in turn may hold a {@code w:drawing}
+ * in one of its choices). Only if this returns {@code true} is it worth running the
+ * XPath queries that collect the picture text, pictures and charts.
+ *
+ * @param r the run to inspect
+ * @return {@code true} if the run may hold picture/drawing content
+ */
+ private static boolean mayHavePictureContent(CTR r) {
+ if (r.sizeOfPictArray() > 0 || r.sizeOfDrawingArray() > 0) {
+ return true;
+ }
+ // ./mc:AlternateContent - direct children only, matching the XPath below
+ for (Node node = r.getDomNode().getFirstChild(); node != null; node = node.getNextSibling()) {
+ if (node.getNodeType() == Node.ELEMENT_NODE
+ && "AlternateContent".equals(node.getLocalName())
+ && PackageNamespaces.MARKUP_COMPATIBILITY.equals(node.getNamespaceURI())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* @deprecated Use {@link XWPFRun#XWPFRun(CTR, IRunBody)}
*/
diff --git a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFTable.java b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFTable.java
index e6115bd0fb..d9703a61fb 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFTable.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/xwpf/usermodel/XWPFTable.java
@@ -35,7 +35,6 @@ import org.apache.xmlbeans.XmlObject;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTJcTable;
-import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtContentRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSdtRow;
@@ -45,7 +44,6 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblBorders;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblCellMar;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
-import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STBorder;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STJcTable;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
@@ -196,9 +194,13 @@ public class XWPFTable implements IBodyElement, ISDTContents {
StringBuilder rowText = new StringBuilder();
XWPFTableRow tableRow = new XWPFTableRow(row, this);
tableRows.add(tableRow);
- for (CTTc cell : row.getTcList()) {
- for (CTP ctp : cell.getPList()) {
- XWPFParagraph p = new XWPFParagraph(ctp, part);
+ // Reuse the paragraphs that XWPFTableRow has just built for the cells of this row,
+ // rather than parsing every cell a second time into throw-away XWPFParagraph/XWPFRun
+ // objects. The cells are the same CTTc elements in the same order (XWPFTableRow uses
+ // ctRow.getTcArray()) and each cell's paragraphs are its direct w:p children in
+ // document order, so the resulting text is identical.
+ for (XWPFTableCell cell : tableRow.getTableCells()) {
+ for (XWPFParagraph p : cell.getParagraphs()) {
if (!rowText.isEmpty()) {
rowText.append('\t');
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@poi.apache.org
For additional commands, e-mail: commits-help@poi.apache.org