This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new fe5ba58bc1 TIKA-4966: apply font formatting styles in docx (#3316)
fe5ba58bc1 is described below
commit fe5ba58bc1bee4f37217c32a05a9b74db7fa51ba
Author: Tim Allison <[email protected]>
AuthorDate: Sat Oct 17 13:27:09 2026 +0100
TIKA-4966: apply font formatting styles in docx (#3316)
---
CHANGES.txt | 8 ++
.../tika/parser/microsoft/OfficeParserConfig.java | 18 +++
.../tika/parser/microsoft/WordExtractor.java | 25 +++-
.../microsoft/ooxml/FormattingTagManager.java | 35 +++++-
.../microsoft/ooxml/OOXMLTikaBodyPartHandler.java | 28 ++++-
.../ooxml/OOXMLWordAndPowerPointTextHandler.java | 12 +-
.../tika/parser/microsoft/ooxml/RunProperties.java | 45 ++++++++
.../microsoft/ooxml/xwpf/XWPFStylesShim.java | 127 +++++++++++++++++----
.../tika/parser/microsoft/WordParserTest.java | 14 +++
.../parser/microsoft/ooxml/OOXMLDocxSAXTest.java | 37 ++++++
.../test-documents/testWORD_characterStyles.docx | Bin 0 -> 1944 bytes
11 files changed, 314 insertions(+), 35 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index e2e97146b8..e81aa603bb 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,13 @@
Release 4.2.0 - unreleased
+ * The docx parser now applies bold and italics set by a run's character
style
+ (e.g. Word's built-in Strong and Emphasis), following basedOn chains;
previously
+ only direct formatting was emitted. The new OfficeParserConfig option
+ includeCharacterStyleClasses wraps such runs in <span class="..."> named
after
+ the style. Style-derived classes (paragraph and character) now replace
every
+ character other than letters, digits, '_' and '-' with '_' and are capped
at 128
+ characters, so a style name can no longer yield extra class tokens
(TIKA-4966).
+
* OpenAI-compatible VLM parser can send the output token limit as
max_completion_tokens for models that reject max_tokens. The new
useMaxCompletionTokens setting is opt-in; max_tokens remains the default.
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java
index 5b174caaaa..6e8708baff 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/OfficeParserConfig.java
@@ -46,6 +46,7 @@ public class OfficeParserConfig implements Serializable {
private int rtfEmbeddedMaxBytesInKb = 2 * 1024 * 1024; // 2 GB
private boolean includeGlossary = true;
+ private boolean includeCharacterStyleClasses = false;
private String dateOverrideFormat = null;
private int maxOverride = 0;//ignore
@@ -203,6 +204,23 @@ public class OfficeParserConfig implements Serializable {
this.includeGlossary = includeGlossary;
}
+ public boolean isIncludeCharacterStyleClasses() {
+ return includeCharacterStyleClasses;
+ }
+
+ /**
+ * Whether to wrap docx runs that carry a character style in
+ * <code><span class="..."></code>, named after the style the same
way
+ * paragraph styles are.
+ * <p/>
+ * Default: <code>false</code>
+ *
+ * @param includeCharacterStyleClasses whether to emit character style
classes
+ */
+ public void setIncludeCharacterStyleClasses(boolean
includeCharacterStyleClasses) {
+ this.includeCharacterStyleClasses = includeCharacterStyleClasses;
+ }
+
public boolean isIncludeMissingRows() {
return includeMissingRows;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WordExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WordExtractor.java
index 346de85dcc..444e6100ef 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WordExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/WordExtractor.java
@@ -29,6 +29,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Pattern;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.HWPFOldDocument;
@@ -78,6 +79,8 @@ public class WordExtractor extends AbstractPOIFSExtractor {
private static final String LIST_DELIMITER = " ";
private static final Map<String, TagAndStyle> fixedParagraphStyles = new
HashMap<>();
private static final TagAndStyle defaultParagraphStyle = new
TagAndStyle("p", null);
+ private static final int MAX_STYLE_CLASS_LENGTH = 128;
+ private static final Pattern NON_STYLE_CLASS_CHARS =
Pattern.compile("[^\\p{L}\\p{N}_-]");
private static final Logger LOG =
LoggerFactory.getLogger(WordExtractor.class);
static {
@@ -170,14 +173,30 @@ public class WordExtractor extends AbstractPOIFSExtractor
{
// Turn it into a H1 - H6 (H7+ isn't valid!)
tag = "h" + Math.min(num, 6);
} else {
- styleClass = styleName.replace(' ', '_');
- styleClass =
- styleClass.substring(0, 1).toLowerCase(Locale.ROOT) +
styleClass.substring(1);
+ styleClass = toStyleClass(styleName);
}
return new TagAndStyle(tag, styleClass);
}
+ /**
+ * @return the XHTML class for a style name, e.g. "List Bullet" ->
"list_Bullet",
+ * or null for an empty name. Anything but letters, digits, '_' and '-'
becomes '_',
+ * so a style name can never yield more than one class token.
+ */
+ public static String toStyleClass(String styleName) {
+ if (styleName == null || styleName.isEmpty()) {
+ return null;
+ }
+ // sanitize last: lowercasing can expand a letter into a disallowed
combining mark
+ String styleClass =
+ styleName.substring(0, 1).toLowerCase(Locale.ROOT) +
styleName.substring(1);
+ if (styleClass.length() > MAX_STYLE_CLASS_LENGTH) {
+ styleClass = styleClass.substring(0, MAX_STYLE_CLASS_LENGTH);
+ }
+ return NON_STYLE_CLASS_CHARS.matcher(styleClass).replaceAll("_");
+ }
+
protected void parse(POIFSFileSystem filesystem, XHTMLContentHandler xhtml)
throws IOException, SAXException, TikaException {
parse(filesystem.getRoot(), xhtml);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
index 3e8ae2f0de..8717b524aa 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/FormattingTagManager.java
@@ -44,8 +44,9 @@ class FormattingTagManager {
private final XHTMLContentHandler xhtml;
- // Outermost to innermost: hyperlink > bold > italic > strike > underline
+ // Outermost to innermost: hyperlink > span > bold > italic > strike >
underline
private String currentHyperlink = null;
+ private String currentSpanClass = null;
private boolean wrapperHyperlinkActive = false;
private boolean isBold = false;
private boolean isItalics = false;
@@ -65,7 +66,7 @@ class FormattingTagManager {
if (url == null) {
return;
}
- closeFormattingTags();
+ closeInlineTags();
if (currentHyperlink != null) {
xhtml.endElement("a");
}
@@ -79,7 +80,7 @@ class FormattingTagManager {
*/
void closeHyperlink() throws SAXException {
if (currentHyperlink != null && wrapperHyperlinkActive) {
- closeFormattingTags();
+ closeInlineTags();
xhtml.endElement("a");
currentHyperlink = null;
wrapperHyperlinkActive = false;
@@ -98,11 +99,19 @@ class FormattingTagManager {
* opening and closing XHTML tags as needed to maintain proper nesting.
*/
void applyFormatting(RunProperties runProperties) throws SAXException {
+ applyFormatting(runProperties, null);
+ }
+
+ /**
+ * As {@link #applyFormatting(RunProperties)}, additionally wrapping the
run in
+ * {@code <span class="spanClass">} when spanClass is non-null.
+ */
+ void applyFormatting(RunProperties runProperties, String spanClass) throws
SAXException {
// Run-property hyperlinks only when no wrapper is active
if (!wrapperHyperlinkActive) {
String newHyperlink = runProperties.getHlinkClickUrl();
if (!Objects.equals(newHyperlink, currentHyperlink)) {
- closeFormattingTags();
+ closeInlineTags();
if (currentHyperlink != null) {
xhtml.endElement("a");
}
@@ -113,6 +122,14 @@ class FormattingTagManager {
}
}
+ if (!Objects.equals(spanClass, currentSpanClass)) {
+ closeInlineTags();
+ if (spanClass != null) {
+ xhtml.startElement("span", "class", spanClass);
+ }
+ currentSpanClass = spanClass;
+ }
+
if (runProperties.isBold() != isBold) {
// Close inner tags before flipping <b>. Nesting is <b><i><s><u>
// (outermost to innermost), so close innermost first: u, s, i.
@@ -182,7 +199,7 @@ class FormattingTagManager {
* Closes all currently open tags in proper nesting order.
*/
void closeAll() throws SAXException {
- closeFormattingTags();
+ closeInlineTags();
if (currentHyperlink != null) {
xhtml.endElement("a");
currentHyperlink = null;
@@ -190,6 +207,14 @@ class FormattingTagManager {
}
}
+ private void closeInlineTags() throws SAXException {
+ closeFormattingTags();
+ if (currentSpanClass != null) {
+ xhtml.endElement("span");
+ currentSpanClass = null;
+ }
+ }
+
private void closeFormattingTags() throws SAXException {
if (isUnderline) {
xhtml.endElement("u");
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
index 0366ab9670..7df2342fda 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLTikaBodyPartHandler.java
@@ -49,6 +49,8 @@ public class OOXMLTikaBodyPartHandler
private final XWPFListManager listManager;
private final boolean includeDeletedText;
private final boolean includeMoveFromText;
+ private final boolean includeCharacterStyleClasses;
+ private final RunProperties effectiveRunProperties = new RunProperties();
private final XWPFStylesShim styles;
private final Metadata metadata;
@@ -95,6 +97,7 @@ public class OOXMLTikaBodyPartHandler
this.listManager = XWPFListManager.EMPTY_LIST;
this.includeDeletedText = false;
this.includeMoveFromText = false;
+ this.includeCharacterStyleClasses = false;
}
public OOXMLTikaBodyPartHandler(XHTMLContentHandler xhtml, XWPFStylesShim
styles,
@@ -113,6 +116,7 @@ public class OOXMLTikaBodyPartHandler
this.listManager = listManager;
this.includeDeletedText = parserConfig.isIncludeDeletedContent();
this.includeMoveFromText = parserConfig.isIncludeMoveFromContent();
+ this.includeCharacterStyleClasses =
parserConfig.isIncludeCharacterStyleClasses();
}
/**
@@ -128,10 +132,32 @@ public class OOXMLTikaBodyPartHandler
@Override
public void run(RunProperties runProperties, String contents) throws
SAXException {
- formattingTags.applyFormatting(runProperties);
+ if (contents.isEmpty()) {
+ return;
+ }
+ String styleId = runProperties.getStyleID();
+ if (styleId == null || styles == null) {
+ formattingTags.applyFormatting(runProperties);
+ } else {
+ effectiveRunProperties.copyFrom(runProperties);
+ if (!runProperties.isBoldSet()) {
+ effectiveRunProperties.bold = styles.isBold(styleId);
+ }
+ if (!runProperties.isItalicsSet()) {
+ effectiveRunProperties.italics = styles.isItalics(styleId);
+ }
+ formattingTags.applyFormatting(effectiveRunProperties,
getSpanClass(styleId));
+ }
xhtml.characters(contents);
}
+ private String getSpanClass(String styleId) {
+ if (!includeCharacterStyleClasses) {
+ return null;
+ }
+ return WordExtractor.toStyleClass(styles.getStyleName(styleId));
+ }
+
@Override
public void hyperlinkStart(String link) throws SAXException {
formattingTags.openHyperlink(link);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
index e9aa3f001a..f1411245fd 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/OOXMLWordAndPowerPointTextHandler.java
@@ -20,7 +20,6 @@ package org.apache.tika.parser.microsoft.ooxml;
import java.util.Date;
import java.util.Map;
-import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -56,6 +55,7 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
private final static String RPR = "rPr";
private final static String P = "p";
private final static String P_STYLE = "pStyle";
+ private final static String R_STYLE = "rStyle";
private final static String PPR = "pPr";
private final static String T = "t";
private final static String TAB = "tab";
@@ -290,6 +290,10 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
} else if (P_STYLE.equals(localName)) {
String styleId = atts.getValue(W_NS, "val");
currPProperties.setStyleID(styleId);
+ } else if (R_STYLE.equals(localName)) {
+ if (inR && inRPr) {
+ currRunProperties.setStyleID(atts.getValue(W_NS, "val"));
+ }
} else if (I.equals(localName)) { //TODO: add iCs
//rprs don't have to be inR; ignore those that aren't
if (inR && inRPr) {
@@ -630,11 +634,7 @@ public class OOXMLWordAndPowerPointTextHandler extends
DefaultHandler {
bodyContentsHandler.run(currRunProperties, runBuffer.toString());
inR = false;
runBuffer.setLength(0);
- currRunProperties.setBold(false);
- currRunProperties.setItalics(false);
- currRunProperties.setStrike(false);
- currRunProperties.setUnderline(UnderlinePatterns.NONE.name());
- currRunProperties.setHlinkClickUrl(null);
+ currRunProperties.reset();
}
@Override
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
index efed9c1348..56fbc6176e 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/RunProperties.java
@@ -33,12 +33,22 @@ public class RunProperties {
// PPTX hlinkClick hyperlink URL — set from <a:hlinkClick> inside <a:rPr>
String hlinkClickUrl = null;
+ // docx character style (w:rStyle); direct b/i, once set, override it
+ String styleID = null;
+ boolean boldSet = false;
+ boolean italicsSet = false;
+
public boolean isItalics() {
return italics;
}
public void setItalics(boolean italics) {
this.italics = italics;
+ this.italicsSet = true;
+ }
+
+ public boolean isItalicsSet() {
+ return italicsSet;
}
public boolean isBold() {
@@ -47,6 +57,19 @@ public class RunProperties {
public void setBold(boolean bold) {
this.bold = bold;
+ this.boldSet = true;
+ }
+
+ public boolean isBoldSet() {
+ return boldSet;
+ }
+
+ public String getStyleID() {
+ return styleID;
+ }
+
+ public void setStyleID(String styleID) {
+ this.styleID = styleID;
}
public boolean isStrikeThrough() {
@@ -79,4 +102,26 @@ public class RunProperties {
public void setHlinkClickUrl(String url) {
this.hlinkClickUrl = url;
}
+
+ void copyFrom(RunProperties other) {
+ italics = other.italics;
+ bold = other.bold;
+ strikeThrough = other.strikeThrough;
+ underline = other.underline;
+ hlinkClickUrl = other.hlinkClickUrl;
+ styleID = other.styleID;
+ boldSet = other.boldSet;
+ italicsSet = other.italicsSet;
+ }
+
+ public void reset() {
+ italics = false;
+ bold = false;
+ strikeThrough = false;
+ underline = UnderlinePatterns.NONE;
+ hlinkClickUrl = null;
+ styleID = null;
+ boldSet = false;
+ italicsSet = false;
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
index 2698c058cc..c8f77dff47 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFStylesShim.java
@@ -32,10 +32,8 @@ import
org.apache.tika.parser.microsoft.ooxml.OOXMLWordAndPowerPointTextHandler;
import org.apache.tika.utils.XMLReaderUtils;
/**
- * For Tika, all we need (so far) is a mapping between styleId and a style's
name.
- * <p>
- * This class uses SAX to scrape that info out of the styles.xml file. If
- * either the styleId or the style's name is null, no information is recorded.
+ * Scrapes from styles.xml each style's name, its basedOn parent and whether
+ * its own run properties set bold or italics.
*/
public class XWPFStylesShim {
@@ -44,7 +42,12 @@ public class XWPFStylesShim {
*/
public static final XWPFStylesShim EMPTY_STYLES = new EmptyXWPFStyles();
- private final Map<String, String> styles = new HashMap<>();
+ // bounds basedOn chains, including cyclic ones
+ private static final int MAX_BASED_ON_DEPTH = 32;
+ // Word itself caps style names at 253 characters
+ private static final int MAX_STYLE_NAME_LENGTH = 255;
+
+ private final Map<String, StyleInfo> styles = new HashMap<>();
private XWPFStylesShim() {
@@ -72,7 +75,45 @@ public class XWPFStylesShim {
if (styleId == null) {
return null;
}
- return styles.get(styleId);
+ StyleInfo info = styles.get(styleId);
+ return info == null ? null : info.name;
+ }
+
+ /**
+ * @return whether the style, or the nearest style it is based on that says
+ * either way, sets bold; false if none does
+ */
+ public boolean isBold(String styleId) {
+ StyleInfo info = styles.get(styleId);
+ for (int i = 0; info != null && i < MAX_BASED_ON_DEPTH; i++) {
+ if (info.bold != null) {
+ return info.bold;
+ }
+ info = info.basedOn == null ? null : styles.get(info.basedOn);
+ }
+ return false;
+ }
+
+ /**
+ * @return whether the style, or the nearest style it is based on that says
+ * either way, sets italics; false if none does
+ */
+ public boolean isItalics(String styleId) {
+ StyleInfo info = styles.get(styleId);
+ for (int i = 0; info != null && i < MAX_BASED_ON_DEPTH; i++) {
+ if (info.italics != null) {
+ return info.italics;
+ }
+ info = info.basedOn == null ? null : styles.get(info.basedOn);
+ }
+ return false;
+ }
+
+ private static class StyleInfo {
+ String name;
+ String basedOn;
+ Boolean bold;
+ Boolean italics;
}
private static class EmptyXWPFStyles extends XWPFStylesShim {
@@ -81,35 +122,81 @@ public class XWPFStylesShim {
public String getStyleName(String styleId) {
return null;
}
+
+ @Override
+ public boolean isBold(String styleId) {
+ return false;
+ }
+
+ @Override
+ public boolean isItalics(String styleId) {
+ return false;
+ }
}
private class StylesStripper extends DefaultHandler {
- String currentStyleId = null;
+ StyleInfo current = null;
+ // depth below the current <w:style>; its own rPr is at 1, toggles at 2
+ int depth = 0;
+ boolean inStyleRPr = false;
@Override
public void startElement(String uri, String localName, String qName,
Attributes atts)
throws SAXException {
- if (uri == null ||
OOXMLWordAndPowerPointTextHandler.W_NS.equals(uri)) {
- if ("style".equals(localName)) {
- currentStyleId =
-
atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS, "styleId");
- } else if ("name".equals(localName)) {
- String name =
atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS, "val");
- if (currentStyleId != null && name != null) {
- styles.put(currentStyleId, name);
- }
+ if (current != null) {
+ depth++;
+ }
+ if (uri != null &&
!OOXMLWordAndPowerPointTextHandler.W_NS.equals(uri)) {
+ return;
+ }
+ if ("style".equals(localName)) {
+ String styleId =
+ atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS,
"styleId");
+ if (styleId != null) {
+ current = new StyleInfo();
+ styles.put(styleId, current);
+ depth = 0;
}
+ } else if (current == null) {
+ return;
+ } else if (depth == 1 && "name".equals(localName)) {
+ String name = getVal(atts);
+ current.name = name != null && name.length() >
MAX_STYLE_NAME_LENGTH
+ ? name.substring(0, MAX_STYLE_NAME_LENGTH) : name;
+ } else if (depth == 1 && "basedOn".equals(localName)) {
+ current.basedOn = getVal(atts);
+ } else if (depth == 1 && "rPr".equals(localName)) {
+ inStyleRPr = true;
+ } else if (depth == 2 && inStyleRPr && "b".equals(localName)) {
+ current.bold = getOnOff(atts);
+ } else if (depth == 2 && inStyleRPr && "i".equals(localName)) {
+ current.italics = getOnOff(atts);
}
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
- if (uri == null ||
OOXMLWordAndPowerPointTextHandler.W_NS.equals(uri)) {
- if ("style".equals(localName)) {
- currentStyleId = null;
- }
+ if (current == null) {
+ return;
+ }
+ if (depth == 0) {
+ current = null;
+ return;
}
+ if (depth == 1) {
+ inStyleRPr = false;
+ }
+ depth--;
+ }
+
+ private String getVal(Attributes atts) {
+ return atts.getValue(OOXMLWordAndPowerPointTextHandler.W_NS,
"val");
+ }
+
+ private boolean getOnOff(Attributes atts) {
+ String v = getVal(atts);
+ return v == null || !("0".equals(v) || "false".equals(v) ||
"off".equals(v));
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
index 27825f560d..8fa036da9b 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/WordParserTest.java
@@ -51,6 +51,20 @@ import org.apache.tika.sax.BodyContentHandler;
public class WordParserTest extends TikaTest {
+ @Test
+ public void testToStyleClass() {
+ assertEquals("list_Bullet", WordExtractor.toStyleClass("List Bullet"));
+ assertEquals("x_omml", WordExtractor.toStyleClass("x\tomml"));
+ assertEquals("table_Text___3__", WordExtractor.toStyleClass("Table
Text (.3\")"));
+ assertEquals("überschrift_1", WordExtractor.toStyleClass("Überschrift
1"));
+ assertEquals(128,
WordExtractor.toStyleClass("a".repeat(10_000)).length());
+ // lowercasing U+0130 yields "i" + U+0307; sanitizing must still have
the last word
+ assertEquals("i_stanbul", WordExtractor.toStyleClass("İstanbul"));
+ assertEquals(128,
WordExtractor.toStyleClass("İ".repeat(200)).length());
+ assertNull(WordExtractor.toStyleClass(""));
+ assertNull(WordExtractor.toStyleClass(null));
+ }
+
@Test
public void testWordParser() throws Exception {
try (TikaInputStream tis =
getResourceAsStream("/test-documents/testWORD.doc")) {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
index 92cb938058..08767ac8f5 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/ooxml/OOXMLDocxSAXTest.java
@@ -152,6 +152,43 @@ public class OOXMLDocxSAXTest extends TikaTest {
assertContains("<a href=\"http://tika.apache.org/\"><b>hyper</b>
link</a>; bold", xml);
}
+ @Test
+ public void testCharacterStyleFormatting() throws Exception {
+ String xml = getXML("testWORD_characterStyles.docx").xml;
+ assertContains("<p>plain <b>strongStyle</b></p>", xml);
+ assertContains("<p><i>emphasisStyle</i></p>", xml);
+ assertContains("<p><b><i>inheritsEmphasis</i></b></p>", xml);
+ assertContains("<p>italicOff</p>", xml);
+ assertContains("<p>directOffStrong</p>", xml);
+ assertContains("<p><b>directBold</b></p>", xml);
+ assertContains("<p>cycle</p>", xml);
+ assertContains("<p>tableOnly</p>", xml);
+ assertContains("<p>unknownStyle</p>", xml);
+ // paragraph-level formatting is conveyed by the paragraph's class,
not <b>
+ assertContains("<p class=\"bold_Para\">boldParaText</p>", xml);
+ assertNotContained("<span", xml);
+ }
+
+ @Test
+ public void testCharacterStyleClasses() throws Exception {
+ OfficeParserConfig config = new OfficeParserConfig();
+ config.setIncludeCharacterStyleClasses(true);
+ ParseContext parseContext = new ParseContext();
+ parseContext.set(OfficeParserConfig.class, config);
+ String xml = getXML("testWORD_characterStyles.docx", parseContext).xml;
+ assertContains("<p>plain <span
class=\"strong\"><b>strongStyle</b></span></p>", xml);
+ assertContains("<span
class=\"inherits_Emphasis\"><b><i>inheritsEmphasis</i></b></span>",
+ xml);
+ assertContains("<p><b>directBold</b></p>", xml);
+ assertContains("<p>unknownStyle</p>", xml);
+ // adjacent runs sharing a style share one span
+ assertContains("<span class=\"plain\">ab</span><span
class=\"strong\"><b>c</b></span>",
+ xml);
+ // a tab in a style name must not split into a second, forged class
token
+ assertContains("<p><span class=\"x_omml\">forged</span></p>", xml);
+ assertContains("<p><span class=\"l" + "L".repeat(127) +
"\">longName</span></p>", xml);
+ }
+
@Test
public void testLongForIntExceptionInSummaryDetails() throws Exception {
assertContains("bold",
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testWORD_characterStyles.docx
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testWORD_characterStyles.docx
new file mode 100644
index 0000000000..f539eb08ef
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/resources/test-documents/testWORD_characterStyles.docx
differ