diff options
Diffstat (limited to 'src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java')
| -rw-r--r-- | src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java new file mode 100644 index 0000000..43a275a --- /dev/null +++ b/src/main/java/org/zwobble/mammoth/internal/docx/BodyXmlReader.java @@ -0,0 +1,329 @@ +package org.zwobble.mammoth.internal.docx; + +import com.google.common.collect.ImmutableSet; +import org.zwobble.mammoth.internal.documents.*; +import org.zwobble.mammoth.results.Result; +import org.zwobble.mammoth.results.Warning; +import org.zwobble.mammoth.internal.util.InputStreamSupplier; +import org.zwobble.mammoth.internal.util.MammothOptionals; +import org.zwobble.mammoth.internal.xml.XmlElement; +import org.zwobble.mammoth.internal.xml.XmlElementLike; +import org.zwobble.mammoth.internal.xml.XmlElementList; +import org.zwobble.mammoth.internal.xml.XmlNode; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Iterables.transform; +import static org.zwobble.mammoth.internal.docx.ReadResult.EMPTY_SUCCESS; +import static org.zwobble.mammoth.internal.docx.ReadResult.success; +import static org.zwobble.mammoth.results.Warning.warning; +import static org.zwobble.mammoth.internal.util.MammothLists.list; +import static org.zwobble.mammoth.internal.util.MammothStrings.trimLeft; + +public class BodyXmlReader { + private static final Set<String> IMAGE_TYPES_SUPPORTED_BY_BROWSERS = ImmutableSet.of( + "image/png", "image/gif", "image/jpeg", "image/svg+xml", "image/tiff"); + + private final Styles styles; + private final Numbering numbering; + private final Relationships relationships; + private final ContentTypes contentTypes; + private final DocxFile file; + private final FileReader fileReader; + + public BodyXmlReader( + Styles styles, + Numbering numbering, + Relationships relationships, + ContentTypes contentTypes, + DocxFile file, + FileReader fileReader) + { + this.styles = styles; + this.numbering = numbering; + this.relationships = relationships; + this.contentTypes = contentTypes; + this.file = file; + this.fileReader = fileReader; + } + + public ReadResult readElement(XmlElement element) { + switch (element.getName()) { + case "w:t": + return success(new Text(element.innerText())); + case "w:r": + return readRun(element); + case "w:p": + return readParagraph(element); + + case "w:tab": + return success(Tab.TAB); + case "w:br": + return readBreak(element); + + case "w:tbl": + return readElements(element.children()).map(Table::new); + case "w:tr": + return readElements(element.children()).map(TableRow::new); + case "w:tc": + return readElements(element.children()).map(TableCell::new); + + case "w:hyperlink": + return readHyperlink(element); + case "w:bookmarkStart": + return readBookmark(element); + case "w:footnoteReference": + return readNoteReference(NoteType.FOOTNOTE, element); + case "w:endnoteReference": + return readNoteReference(NoteType.ENDNOTE, element); + + case "w:pict": + return readPict(element); + + case "v:imagedata": + return readImagedata(element); + + case "wp:inline": + case "wp:anchor": + return readInline(element); + + case "w:ins": + case "w:smartTag": + case "w:drawing": + case "v:roundrect": + case "v:shape": + case "v:textbox": + case "w:txbxContent": + return readElements(element.children()); + + case "office-word:wrap": + case "v:shadow": + case "v:shapetype": + case "w:bookmarkEnd": + case "w:sectPr": + case "w:proofErr": + case "w:lastRenderedPageBreak": + case "w:commentRangeStart": + case "w:commentRangeEnd": + case "w:commentReference": + case "w:del": + case "w:footnoteRef": + case "w:endnoteRef": + case "w:pPr": + case "w:rPr": + case "w:tblPr": + case "w:tblGrid": + case "w:tcPr": + return EMPTY_SUCCESS; + + default: + Warning warning = warning("An unrecognised element was ignored: " + element.getName()); + return ReadResult.emptyWithWarning(warning); + } + } + + private ReadResult readRun(XmlElement element) { + XmlElementLike properties = element.findChildOrEmpty("w:rPr"); + return ReadResult.map( + readRunStyle(properties), + readElements(element.children()), + (style, children) -> new Run( + isBold(properties), + isItalic(properties), + isUnderline(properties), + isStrikethrough(properties), + readVerticalAlignment(properties), + style, + children)); + } + + private boolean isBold(XmlElementLike properties) { + return properties.hasChild("w:b"); + } + + private boolean isItalic(XmlElementLike properties) { + return properties.hasChild("w:i"); + } + + private boolean isUnderline(XmlElementLike properties) { + return properties.hasChild("w:u"); + } + + private boolean isStrikethrough(XmlElementLike properties) { + return properties.hasChild("w:strike"); + } + + private VerticalAlignment readVerticalAlignment(XmlElementLike properties) { + String verticalAlignment = readVal(properties, "w:vertAlign").orElse(""); + switch (verticalAlignment) { + case "superscript": + return VerticalAlignment.SUPERSCRIPT; + case "subscript": + return VerticalAlignment.SUBSCRIPT; + default: + // TODO: warn if set? + return VerticalAlignment.BASELINE; + } + } + + private Result<Optional<Style>> readRunStyle(XmlElementLike properties) { + return readStyle(properties, "w:rStyle", "Run", styles::findCharacterStyleById); + } + + public ReadResult readElements(Iterable<XmlNode> nodes) { + return ReadResult.concat( + transform( + filter(nodes, XmlElement.class), + this::readElement)); + } + + private ReadResult readParagraph(XmlElement element) { + XmlElementLike properties = element.findChildOrEmpty("w:pPr"); + Optional<NumberingLevel> numbering = readNumbering(properties); + return ReadResult.map( + readParagraphStyle(properties), + readElements(element.children()), + (style, children) -> new Paragraph(style, numbering, children)).appendExtra(); + } + + private Result<Optional<Style>> readParagraphStyle(XmlElementLike properties) { + return readStyle(properties, "w:pStyle", "Paragraph", styles::findParagraphStyleById); + } + + private Result<Optional<Style>> readStyle( + XmlElementLike properties, + String styleTagName, + String styleType, + Function<String, Optional<Style>> findStyleById) + { + return readVal(properties, styleTagName) + .map(styleId -> findStyleById(styleType, styleId, findStyleById)) + .orElse(Result.empty()); + } + + private Result<Optional<Style>> findStyleById( + String styleType, + String styleId, + Function<String, Optional<Style>> findStyleById) + { + Optional<Style> style = findStyleById.apply(styleId); + if (style.isPresent()) { + return Result.success(style); + } else { + return new Result<>( + Optional.of(new Style(styleId, Optional.empty())), + list(warning(styleType + " style with ID " + styleId + " was referenced but not defined in the document"))); + } + + } + + private Optional<NumberingLevel> readNumbering(XmlElementLike properties) { + XmlElementLike numberingProperties = properties.findChildOrEmpty("w:numPr"); + return MammothOptionals.flatMap( + readVal(numberingProperties, "w:numId"), + readVal(numberingProperties, "w:ilvl"), + numbering::findLevel); + } + + private ReadResult readBreak(XmlElement element) { + String breakType = element.getAttributeOrNone("w:type").orElse(""); + if (breakType.equals("")) { + return success(LineBreak.LINE_BREAK); + } else { + return ReadResult.emptyWithWarning(warning("Unsupported break type: " + breakType)); + } + } + + private ReadResult readHyperlink(XmlElement element) { + Optional<String> relationshipId = element.getAttributeOrNone("r:id"); + Optional<String> anchor = element.getAttributeOrNone("w:anchor"); + ReadResult childrenResult = readElements(element.children()); + if (relationshipId.isPresent()) { + return childrenResult.map(children -> Hyperlink.href( + relationships.findRelationshipById(relationshipId.get()).getTarget(), children)); + } else if (anchor.isPresent()) { + return childrenResult.map(children -> Hyperlink.anchor( + anchor.get(), children)); + } else { + return childrenResult; + } + } + + private ReadResult readBookmark(XmlElement element) { + String name = element.getAttribute("w:name"); + if (name.equals("_GoBack")) { + return ReadResult.EMPTY_SUCCESS; + } else { + return success(new Bookmark(name)); + } + } + + private ReadResult readNoteReference(NoteType noteType, XmlElement element) { + String noteId = element.getAttribute("w:id"); + return success(new NoteReference(noteType, noteId)); + } + + private ReadResult readPict(XmlElement element) { + return readElements(element.children()).toExtra(); + } + + private ReadResult readImagedata(XmlElement element) { + Optional<String> title = element.getAttributeOrNone("o:title"); + String relationshipId = element.getAttribute("r:id"); + String imagePath = relationshipIdToDocxPath(relationshipId); + return readImage(imagePath, title, () -> DocxFile.getInputStream(file, imagePath)); + } + + private ReadResult readInline(XmlElement element) { + Optional<String> altText = element.findChildOrEmpty("wp:docPr").getAttributeOrNone("descr"); + XmlElementList blips = element.findChildren("a:graphic") + .findChildren("a:graphicData") + .findChildren("pic:pic") + .findChildren("pic:blipFill") + .findChildren("a:blip"); + return readBlips(blips, altText); + } + + private ReadResult readBlips(XmlElementList blips, Optional<String> altText) { + return ReadResult.concat(transform(blips, blip -> readBlip(blip, altText))); + } + + private ReadResult readBlip(XmlElement blip, Optional<String> altText) { + Optional<String> embedRelationshipId = blip.getAttributeOrNone("r:embed"); + Optional<String> linkRelationshipId = blip.getAttributeOrNone("r:link"); + if (embedRelationshipId.isPresent()) { + String imagePath = relationshipIdToDocxPath(embedRelationshipId.get()); + return readImage(imagePath, altText, () -> DocxFile.getInputStream(file, imagePath)); + } else if (linkRelationshipId.isPresent()) { + String imagePath = relationships.findRelationshipById(linkRelationshipId.get()).getTarget(); + return readImage(imagePath, altText, () -> fileReader.getInputStream(imagePath)); + } else { + // TODO: emit warning + return ReadResult.EMPTY_SUCCESS; + } + } + + private ReadResult readImage(String imagePath, Optional<String> altText, InputStreamSupplier open) { + Optional<String> contentType = contentTypes.findContentType(imagePath); + Image image = new Image(altText, contentType, open); + + String contentTypeString = contentType.orElse("(unknown)"); + if (IMAGE_TYPES_SUPPORTED_BY_BROWSERS.contains(contentTypeString)) { + return success(image); + } else { + return ReadResult.withWarning(image, warning("Image of type " + contentTypeString + " is unlikely to display in web browsers")); + } + } + + private String relationshipIdToDocxPath(String relationshipId) { + Relationship relationship = relationships.findRelationshipById(relationshipId); + return "word/" + trimLeft(relationship.getTarget(), '/'); + } + + private Optional<String> readVal(XmlElementLike element, String name) { + return element.findChildOrEmpty(name).getAttributeOrNone("w:val"); + } +} |
