From 3b32a00aa7967d3b7ad61d7c0e67711335a924c5 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Wed, 30 Aug 2017 16:29:52 +0000 Subject: [PATCH] 61470 -- add extraction of content within ruby elements; allow users to concatenate or not concatenate phonetic strings. Default is to concatenate. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1806712 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/xwpf/extractor/XWPFWordExtractor.java | 16 +- .../apache/poi/xwpf/usermodel/XWPFRun.java | 177 +++++++++++++----- .../xwpf/extractor/TestXWPFWordExtractor.java | 12 ++ test-data/document/61470.docx | Bin 0 -> 12523 bytes 4 files changed, 153 insertions(+), 52 deletions(-) create mode 100644 test-data/document/61470.docx diff --git a/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java b/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java index 070f0c18a..f02982949 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java +++ b/src/ooxml/java/org/apache/poi/xwpf/extractor/XWPFWordExtractor.java @@ -33,6 +33,7 @@ import org.apache.poi.xwpf.usermodel.XWPFHyperlink; import org.apache.poi.xwpf.usermodel.XWPFHyperlinkRun; import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFRelation; +import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFSDT; import org.apache.poi.xwpf.usermodel.XWPFSDTCell; import org.apache.poi.xwpf.usermodel.XWPFTable; @@ -53,6 +54,7 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { private XWPFDocument document; private boolean fetchHyperlinks = false; + private boolean concatenatePhoneticRuns = true; public XWPFWordExtractor(OPCPackage container) throws XmlException, OpenXML4JException, IOException { this(new XWPFDocument(container)); @@ -86,6 +88,14 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { fetchHyperlinks = fetch; } + /** + * Should we concatenate phonetic runs in extraction. Default is true + * @param concatenatePhoneticRuns + */ + public void setConcatenatePhoneticRuns(boolean concatenatePhoneticRuns) { + this.concatenatePhoneticRuns = concatenatePhoneticRuns; + } + public String getText() { StringBuffer text = new StringBuffer(); XWPFHeaderFooterPolicy hfPolicy = document.getHeaderFooterPolicy(); @@ -130,7 +140,11 @@ public class XWPFWordExtractor extends POIXMLTextExtractor { for (IRunElement run : paragraph.getRuns()) { - text.append(run); + if (! concatenatePhoneticRuns && run instanceof XWPFRun) { + text.append(((XWPFRun)run).text()); + } else { + text.append(run); + } if (run instanceof XWPFHyperlinkRun && fetchHyperlinks) { XWPFHyperlink link = ((XWPFHyperlinkRun) run).getHyperlink(document); if (link != null) diff --git a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFRun.java b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFRun.java index 85289c502..6c16b1618 100644 --- a/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFRun.java +++ b/src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFRun.java @@ -68,6 +68,8 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPTab; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRuby; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRubyContent; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSignedHpsMeasure; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSignedTwipsMeasure; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; @@ -1042,10 +1044,15 @@ public class XWPFRun implements ISDTContents, IRunElement, CharacterRun { } /** - * Returns the string version of the text + * Returns the string version of the text and the phonetic string */ public String toString() { - return text(); + String phonetic = getPhonetic(); + if (phonetic.length() > 0) { + return text() +" ("+phonetic.toString()+")"; + } else { + return text(); + } } /** @@ -1061,71 +1068,139 @@ public class XWPFRun implements ISDTContents, IRunElement, CharacterRun { c.selectPath("./*"); while (c.toNextSelection()) { XmlObject o = c.getObject(); - if (o instanceof CTText) { + if (o instanceof CTRuby) { + handleRuby(o, text, false); + continue; + } + _getText(o, text); + } + c.dispose(); + return text.toString(); + + } + + /** + * + * @return the phonetic (ruby) string associated with this run or an empty String if none exists + */ + public String getPhonetic() { + StringBuffer text = new StringBuffer(); + + // Grab the text and tabs of the text run + // Do so in a way that preserves the ordering + XmlCursor c = run.newCursor(); + c.selectPath("./*"); + while (c.toNextSelection()) { + XmlObject o = c.getObject(); + if (o instanceof CTRuby) { + handleRuby(o, text, true); + } + } + c.dispose(); + return text.toString(); + } + + /** + * + * @param rubyObj rubyobject + * @param text buffer to which to append the content + * @param extractPhonetic extract the phonetic (rt) component or the base component + */ + private void handleRuby(XmlObject rubyObj, StringBuffer text, boolean extractPhonetic) { + XmlCursor c = rubyObj.newCursor(); + + //according to the spec, a ruby object + //has the phonetic (rt) first, then the actual text (base) + //second. + + c.selectPath(".//*"); + boolean inRT = false; + boolean inBase = false; + while (c.toNextSelection()) { + XmlObject o = c.getObject(); + if (o instanceof CTRubyContent) { String tagName = o.getDomNode().getNodeName(); - // Field Codes (w:instrText, defined in spec sec. 17.16.23) - // come up as instances of CTText, but we don't want them - // in the normal text output - if (!"w:instrText".equals(tagName)) { - text.append(((CTText) o).getStringValue()); + if ("w:rt".equals(tagName)) { + inRT = true; + } else if ("w:rubyBase".equals(tagName)) { + inRT = false; + inBase = true; + } + } else { + if (extractPhonetic && inRT) { + _getText(o, text); + } else if (! extractPhonetic && inBase) { + _getText(o, text); } } + } + c.dispose(); + } - // Complex type evaluation (currently only for extraction of check boxes) - if (o instanceof CTFldChar) { - CTFldChar ctfldChar = ((CTFldChar) o); - if (ctfldChar.getFldCharType() == STFldCharType.BEGIN) { - if (ctfldChar.getFfData() != null) { - for (CTFFCheckBox checkBox : ctfldChar.getFfData().getCheckBoxList()) { - if (checkBox.getDefault() != null && checkBox.getDefault().getVal() == STOnOff.X_1) { - text.append("|X|"); - } else { - text.append("|_|"); - } + private void _getText(XmlObject o, StringBuffer text) { + + if (o instanceof CTText) { + String tagName = o.getDomNode().getNodeName(); + // Field Codes (w:instrText, defined in spec sec. 17.16.23) + // come up as instances of CTText, but we don't want them + // in the normal text output + if (!"w:instrText".equals(tagName)) { + text.append(((CTText) o).getStringValue()); + } + } + + // Complex type evaluation (currently only for extraction of check boxes) + if (o instanceof CTFldChar) { + CTFldChar ctfldChar = ((CTFldChar) o); + if (ctfldChar.getFldCharType() == STFldCharType.BEGIN) { + if (ctfldChar.getFfData() != null) { + for (CTFFCheckBox checkBox : ctfldChar.getFfData().getCheckBoxList()) { + if (checkBox.getDefault() != null && checkBox.getDefault().getVal() == STOnOff.X_1) { + text.append("|X|"); + } else { + text.append("|_|"); } } } } - - if (o instanceof CTPTab) { - text.append("\t"); - } - if (o instanceof CTBr) { - text.append("\n"); - } - if (o instanceof CTEmpty) { - // Some inline text elements get returned not as - // themselves, but as CTEmpty, owing to some odd - // definitions around line 5642 of the XSDs - // This bit works around it, and replicates the above - // rules for that case - String tagName = o.getDomNode().getNodeName(); - if ("w:tab".equals(tagName) || "tab".equals(tagName)) { - text.append("\t"); - } - if ("w:br".equals(tagName) || "br".equals(tagName)) { - text.append("\n"); - } - if ("w:cr".equals(tagName) || "cr".equals(tagName)) { - text.append("\n"); - } - } - if (o instanceof CTFtnEdnRef) { - CTFtnEdnRef ftn = (CTFtnEdnRef) o; - String footnoteRef = ftn.getDomNode().getLocalName().equals("footnoteReference") ? - "[footnoteRef:" + ftn.getId().intValue() + "]" : "[endnoteRef:" + ftn.getId().intValue() + "]"; - text.append(footnoteRef); - } } - c.dispose(); + if (o instanceof CTPTab) { + text.append("\t"); + } + if (o instanceof CTBr) { + text.append("\n"); + } + if (o instanceof CTEmpty) { + // Some inline text elements get returned not as + // themselves, but as CTEmpty, owing to some odd + // definitions around line 5642 of the XSDs + // This bit works around it, and replicates the above + // rules for that case + String tagName = o.getDomNode().getNodeName(); + if ("w:tab".equals(tagName) || "tab".equals(tagName)) { + text.append("\t"); + } + if ("w:br".equals(tagName) || "br".equals(tagName)) { + text.append("\n"); + } + if ("w:cr".equals(tagName) || "cr".equals(tagName)) { + text.append("\n"); + } + } + if (o instanceof CTFtnEdnRef) { + CTFtnEdnRef ftn = (CTFtnEdnRef) o; + String footnoteRef = ftn.getDomNode().getLocalName().equals("footnoteReference") ? + "[footnoteRef:" + ftn.getId().intValue() + "]" : "[endnoteRef:" + ftn.getId().intValue() + "]"; + text.append(footnoteRef); + } + // Any picture text? if (pictureText != null && pictureText.length() > 0) { text.append("\n").append(pictureText); } - return text.toString(); } /** diff --git a/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java b/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java index b83b27d73..b7d0b03f6 100644 --- a/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java +++ b/src/ooxml/testcases/org/apache/poi/xwpf/extractor/TestXWPFWordExtractor.java @@ -421,4 +421,16 @@ public class TestXWPFWordExtractor extends TestCase { extractor.getText()); extractor.close(); } + + public void testPhonetic() throws IOException { + XWPFDocument doc = XWPFTestDataSamples.openSampleDocument("61470.docx"); + XWPFWordExtractor extractor = new XWPFWordExtractor(doc); + //expect: baseText (phoneticText) + assertEquals("\u6771\u4EAC (\u3068\u3046\u304D\u3087\u3046)", extractor.getText().trim()); + extractor.close(); + extractor = new XWPFWordExtractor(doc); + extractor.setConcatenatePhoneticRuns(false); + assertEquals("\u6771\u4EAC", extractor.getText().trim()); + } + } diff --git a/test-data/document/61470.docx b/test-data/document/61470.docx new file mode 100644 index 0000000000000000000000000000000000000000..6fc1afe45cfc6615874a5b56191a055751954b08 GIT binary patch literal 12523 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