From 4ba2d64a505ac2418d44518c4a20644f77919890 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Wed, 18 Apr 2018 15:02:02 +0000 Subject: [PATCH] Bug 62092 - Text not extracted from grouped text shapes in HSLF git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1829453 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/sl/draw/DrawTextParagraph.java | 4 +- .../poi/sl/extractor/SlideShowExtractor.java | 307 +++++++++++++++ .../org/apache/poi/sl/usermodel/Comment.java | 85 +++++ .../poi/sl/usermodel/PlaceholderDetails.java | 69 ++++ .../org/apache/poi/sl/usermodel/Sheet.java | 12 + .../apache/poi/sl/usermodel/SimpleShape.java | 18 + .../org/apache/poi/sl/usermodel/Slide.java | 9 +- .../apache/poi/sl/usermodel/SlideShow.java | 8 + .../extractor/XSLFPowerPointExtractor.java | 348 +++++++++--------- .../poi/xslf/usermodel/XMLSlideShow.java | 7 +- .../poi/xslf/usermodel/XSLFComment.java | 126 +++++++ .../poi/xslf/usermodel/XSLFComments.java | 25 +- .../poi/xslf/usermodel/XSLFNotesMaster.java | 27 +- .../usermodel/XSLFPlaceholderDetails.java | 203 ++++++++++ .../apache/poi/xslf/usermodel/XSLFShape.java | 57 ++- .../apache/poi/xslf/usermodel/XSLFSheet.java | 57 ++- .../poi/xslf/usermodel/XSLFSimpleShape.java | 11 - .../apache/poi/xslf/usermodel/XSLFSlide.java | 49 ++- .../poi/xslf/usermodel/XSLFSlideLayout.java | 17 +- .../poi/xslf/usermodel/XSLFSlideMaster.java | 50 +-- .../poi/xslf/usermodel/XSLFTextParagraph.java | 262 +++++++------ .../poi/xslf/usermodel/XSLFTextRun.java | 76 +--- .../poi/xslf/usermodel/XSLFTextShape.java | 8 +- .../poi/xslf/usermodel/TestXMLSlideShow.java | 26 +- .../poi/xslf/usermodel/TestXSLFTextRun.java | 20 +- .../poi/xslf/usermodel/TestXSLFTextShape.java | 22 +- .../hslf/extractor/PowerPointExtractor.java | 271 +++----------- .../org/apache/poi/hslf/model/Comment.java | 75 ---- .../apache/poi/hslf/model/HeadersFooters.java | 7 + .../hslf/record/DocumentEncryptionAtom.java | 11 +- .../record/HSLFEscherClientDataRecord.java | 14 +- .../org/apache/poi/hslf/record/Record.java | 52 +-- .../apache/poi/hslf/record/RecordTypes.java | 217 ++++------- .../poi/hslf/usermodel/HSLFComment.java | 104 ++++++ .../poi/hslf/usermodel/HSLFMasterSheet.java | 12 +- .../apache/poi/hslf/usermodel/HSLFNotes.java | 27 ++ .../usermodel/HSLFPlaceholderDetails.java | 157 ++++++++ .../HSLFShapePlaceholderDetails.java | 240 ++++++++++++ .../apache/poi/hslf/usermodel/HSLFSheet.java | 18 + .../poi/hslf/usermodel/HSLFSimpleShape.java | 153 +------- .../apache/poi/hslf/usermodel/HSLFSlide.java | 113 +++--- .../poi/hslf/usermodel/HSLFSlideShow.java | 6 + .../poi/hslf/usermodel/HSLFTextShape.java | 29 +- .../poi/hslf/extractor/TestExtractor.java | 62 +++- .../poi/hslf/record/TestRecordTypes.java | 15 +- test-data/slideshow/bug62092.ppt | Bin 0 -> 161792 bytes 46 files changed, 2196 insertions(+), 1290 deletions(-) create mode 100644 src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java create mode 100644 src/java/org/apache/poi/sl/usermodel/Comment.java create mode 100644 src/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComment.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java delete mode 100644 src/scratchpad/src/org/apache/poi/hslf/model/Comment.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFComment.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java create mode 100644 test-data/slideshow/bug62092.ppt diff --git a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java index fbf6988c4..938d46230 100644 --- a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java +++ b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java @@ -52,6 +52,7 @@ import org.apache.poi.sl.usermodel.TextRun; import org.apache.poi.sl.usermodel.TextRun.FieldType; import org.apache.poi.sl.usermodel.TextShape; import org.apache.poi.sl.usermodel.TextShape.TextDirection; +import org.apache.poi.util.Internal; import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -381,7 +382,8 @@ public class DrawTextParagraph implements Drawable { return getRenderableText(tr); } - private String getRenderableText(final TextRun tr) { + @Internal + public String getRenderableText(final TextRun tr) { final String txtSpace = tr.getRawText().replace("\t", tab2space(tr)).replace('\u000b', '\n'); final Locale loc = LocaleUtil.getUserLocale(); diff --git a/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java b/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java new file mode 100644 index 000000000..44f0df19c --- /dev/null +++ b/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java @@ -0,0 +1,307 @@ +package org.apache.poi.sl.extractor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.POITextExtractor; +import org.apache.poi.sl.usermodel.Comment; +import org.apache.poi.sl.usermodel.MasterSheet; +import org.apache.poi.sl.usermodel.Notes; +import org.apache.poi.sl.usermodel.ObjectShape; +import org.apache.poi.sl.usermodel.Placeholder; +import org.apache.poi.sl.usermodel.PlaceholderDetails; +import org.apache.poi.sl.usermodel.Shape; +import org.apache.poi.sl.usermodel.ShapeContainer; +import org.apache.poi.sl.usermodel.Sheet; +import org.apache.poi.sl.usermodel.Slide; +import org.apache.poi.sl.usermodel.SlideShow; +import org.apache.poi.sl.usermodel.TableCell; +import org.apache.poi.sl.usermodel.TableShape; +import org.apache.poi.sl.usermodel.TextParagraph; +import org.apache.poi.sl.usermodel.TextRun; +import org.apache.poi.sl.usermodel.TextShape; +import org.apache.poi.util.LocaleUtil; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * Common SlideShow extractor + * + * @since POI 4.0.0 + */ +public class SlideShowExtractor< + S extends Shape, + P extends TextParagraph +> extends POITextExtractor { + private static final POILogger LOG = POILogFactory.getLogger(SlideShowExtractor.class); + + private SlideShow slideshow; + + private boolean slidesByDefault = true; + private boolean notesByDefault; + private boolean commentsByDefault; + private boolean masterByDefault; + + + public SlideShowExtractor(final SlideShow slideshow) { + setFilesystem(slideshow); + this.slideshow = slideshow; + } + + /** + * Should a call to getText() return slide text? Default is yes + */ + public void setSlidesByDefault(final boolean slidesByDefault) { + this.slidesByDefault = slidesByDefault; + } + + /** + * Should a call to getText() return notes text? Default is no + */ + public void setNotesByDefault(final boolean notesByDefault) { + this.notesByDefault = notesByDefault; + } + + /** + * Should a call to getText() return comments text? Default is no + */ + public void setCommentsByDefault(final boolean commentsByDefault) { + this.commentsByDefault = commentsByDefault; + } + + /** + * Should a call to getText() return text from master? Default is no + */ + public void setMasterByDefault(final boolean masterByDefault) { + this.masterByDefault = masterByDefault; + } + + @Override + public POITextExtractor getMetadataTextExtractor() { + return slideshow.getMetadataTextExtractor(); + } + + /** + * Fetches all the slide text from the slideshow, but not the notes, unless + * you've called setSlidesByDefault() and setNotesByDefault() to change this + */ + @Override + public String getText() { + final StringBuilder sb = new StringBuilder(); + + if (masterByDefault) { + for (final MasterSheet master : slideshow.getSlideMasters()) { + for (final Shape shape : master) { + if (shape instanceof TextShape) { + final TextShape ts = (TextShape)shape; + final String text = ts.getText(); + if (text == null || text.isEmpty() || "*".equals(text)) { + continue; + } + if (ts.isPlaceholder()) { + // don't bother about boiler plate text on master sheets + LOG.log(POILogger.INFO, "Ignoring boiler plate (placeholder) text on slide master:", text); + continue; + } + sb.append(text); + if (!text.endsWith("\n")) { + sb.append("\n"); + } + + } + } + } + } + + for (final Slide slide : slideshow.getSlides()) { + sb.append(getText(slide)); + } + + return sb.toString(); + } + + public String getText(final Slide slide) { + final StringBuilder sb = new StringBuilder(); + + if (slidesByDefault) { + printShapeText(slide, sb); + } + + if (commentsByDefault) { + printComments(slide, sb); + } + + if (notesByDefault) { + printNotes(slide, sb); + } + + return sb.toString(); + } + + private String printHeaderReturnFooter(final Sheet sheet, final StringBuilder sb) { + final Sheet m = (sheet instanceof Slide) ? sheet.getMasterSheet() : sheet; + final StringBuilder footer = new StringBuilder("\n"); + addSheetPlaceholderDatails(sheet, Placeholder.HEADER, sb); + addSheetPlaceholderDatails(sheet, Placeholder.FOOTER, footer); + + if (masterByDefault) { + // write header texts and determine footer text + for (Shape s : m) { + if (!(s instanceof TextShape)) { + continue; + } + final TextShape ts = (TextShape) s; + final PlaceholderDetails pd = ts.getPlaceholderDetails(); + if (pd == null || !pd.isVisible()) { + continue; + } + switch (pd.getPlaceholder()) { + case HEADER: + sb.append(ts.getText()); + sb.append('\n'); + break; + case SLIDE_NUMBER: + if (sheet instanceof Slide) { + footer.append(ts.getText().replace("‹#›", Integer.toString(((Slide) sheet).getSlideNumber() + 1))); + footer.append('\n'); + } + break; + case FOOTER: + footer.append(ts.getText()); + footer.append('\n'); + break; + case DATETIME: + // currently not supported + default: + break; + } + } + } + + return (footer.length() > 1) ? footer.toString() : ""; + } + + private void addSheetPlaceholderDatails(final Sheet sheet, final Placeholder placeholder, final StringBuilder sb) { + final PlaceholderDetails headerPD = sheet.getPlaceholderDetails(placeholder); + if (headerPD == null) { + return; + } + final String headerStr = headerPD.getText(); + if (headerStr == null) { + return; + } + sb.append(headerStr); + } + + private void printShapeText(final Sheet sheet, final StringBuilder sb) { + final String footer = printHeaderReturnFooter(sheet, sb); + printShapeText((ShapeContainer)sheet, sb); + sb.append(footer); + } + + @SuppressWarnings("unchecked") + private void printShapeText(final ShapeContainer container, final StringBuilder sb) { + for (Shape shape : container) { + if (shape instanceof TextShape) { + printShapeText((TextShape)shape, sb); + } else if (shape instanceof TableShape) { + printShapeText((TableShape)shape, sb); + } else if (shape instanceof ShapeContainer) { + printShapeText((ShapeContainer)shape, sb); + } + } + } + + private void printShapeText(final TextShape shape, final StringBuilder sb) { + final List

paraList = shape.getTextParagraphs(); + if (paraList.isEmpty()) { + sb.append('\n'); + return; + } + for (final P para : paraList) { + final int oldLen = sb.length(); + for (final TextRun tr : para) { + final String str = tr.getRawText().replace("\r", ""); + final String newStr; + switch (tr.getTextCap()) { + case ALL: + newStr = str.toUpperCase(LocaleUtil.getUserLocale()); + break; + case SMALL: + newStr = str.toLowerCase(LocaleUtil.getUserLocale()); + break; + default: + case NONE: + newStr = str; + break; + } + sb.append(newStr); + } + sb.append('\n'); + } + } + + @SuppressWarnings("Duplicates") + private void printShapeText(final TableShape shape, final StringBuilder sb) { + final int nrows = shape.getNumberOfRows(); + final int ncols = shape.getNumberOfColumns(); + for (int row = 0; row < nrows; row++){ + for (int col = 0; col < ncols; col++){ + TableCell cell = shape.getCell(row, col); + //defensive null checks; don't know if they're necessary + if (cell != null){ + String txt = cell.getText(); + txt = (txt == null) ? "" : txt; + sb.append(txt); + if (col < ncols-1){ + sb.append('\t'); + } + } + } + sb.append('\n'); + } + } + + private void printComments(final Slide slide, final StringBuilder sb) { + for (final Comment comment : slide.getComments()) { + sb.append(comment.getAuthor()); + sb.append(" - "); + sb.append(comment.getText()); + sb.append("\n"); + } + } + + private void printNotes(final Slide slide, final StringBuilder sb) { + final Notes notes = slide.getNotes(); + if (notes == null) { + return; + } + + final String footer = printHeaderReturnFooter(notes, sb); + + printShapeText(notes, sb); + + sb.append(footer); + } + + public List> getOLEShapes() { + final List> oleShapes = new ArrayList<>(); + + for (final Slide slide : slideshow.getSlides()) { + addOLEShapes(oleShapes, slide); + } + + return oleShapes; + } + + @SuppressWarnings("unchecked") + private void addOLEShapes(final List> oleShapes, ShapeContainer container) { + for (Shape shape : container) { + if (shape instanceof ShapeContainer) { + addOLEShapes(oleShapes, (ShapeContainer)shape); + } else if (shape instanceof ObjectShape) { + oleShapes.add((ObjectShape)shape); + } + } + } +} diff --git a/src/java/org/apache/poi/sl/usermodel/Comment.java b/src/java/org/apache/poi/sl/usermodel/Comment.java new file mode 100644 index 000000000..43e46efec --- /dev/null +++ b/src/java/org/apache/poi/sl/usermodel/Comment.java @@ -0,0 +1,85 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.sl.usermodel; + +import java.awt.geom.Point2D; +import java.util.Date; + +/** + * Common interface for comments + * + * @since POI 4.0.0 + */ +public interface Comment { + /** + * Get the Author of this comment + */ + String getAuthor(); + + /** + * Set the Author of this comment. + * if the author wasn't registered before, create a new entry + */ + void setAuthor(String author); + + /** + * Get the Author's Initials of this comment + */ + String getAuthorInitials(); + + /** + * Set the Author's Initials of this comment. + * if the author wasn't registered before via {@link #setAuthor(String)} + * this has no effect + */ + void setAuthorInitials(String initials); + + /** + * Get the text of this comment + */ + String getText(); + + /** + * Set the text of this comment + */ + void setText(String text); + + /** + * Gets the date the comment was made. + * @return the comment date. + */ + Date getDate(); + + /** + * Sets the date the comment was made. + * @param date the comment date. + */ + void setDate(Date date); + + /** + * Gets the offset of the comment on the page. + * @return the offset. + */ + Point2D getOffset(); + + /** + * Sets the offset of the comment on the page. + * @param offset the offset. + */ + void setOffset(Point2D offset); +} diff --git a/src/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java b/src/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java new file mode 100644 index 000000000..377b5c5b7 --- /dev/null +++ b/src/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java @@ -0,0 +1,69 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.sl.usermodel; + + +/** + * Extended details about placholders + * + * @since POI 4.0.0 + */ +public interface PlaceholderDetails { + enum PlaceholderSize { + quarter, half, full; + } + + Placeholder getPlaceholder(); + + /** + * Specifies that the corresponding shape should be represented by the generating application + * as a placeholder. When a shape is considered a placeholder by the generating application + * it can have special properties to alert the user that they may enter content into the shape. + * Different types of placeholders are allowed and can be specified by using the placeholder + * type attribute for this element + * + * @param placeholder The shape to use as placeholder or null if no placeholder should be set. + */ + void setPlaceholder(Placeholder placeholder); + + boolean isVisible(); + + void setVisible(boolean isVisible); + + PlaceholderSize getSize(); + + void setSize(PlaceholderSize size); + + /** + * If the placeholder shape or object stores text, this text is returned otherwise {@code null}. + * + * @return the text of the shape / placeholder + * + * @since POI 4.0.0 + */ + String getText(); + + /** + * If the placeholder shape or object stores text, the given text is stored otherwise this is a no-op. + * + * @param text the placeholder text + * + * @since POI 4.0.0 + */ + void setText(String text); +} diff --git a/src/java/org/apache/poi/sl/usermodel/Sheet.java b/src/java/org/apache/poi/sl/usermodel/Sheet.java index 923dac378..ae7cd179b 100644 --- a/src/java/org/apache/poi/sl/usermodel/Sheet.java +++ b/src/java/org/apache/poi/sl/usermodel/Sheet.java @@ -46,4 +46,16 @@ public interface Sheet< * @param graphics */ void draw(Graphics2D graphics); + + /** + * Get the placeholder details for the given placeholder type. Not all placeholders are also shapes - + * this is especially true for old HSLF slideshows, which notes have header/footers elements which + * aren't shapes. + * + * @param placeholder the placeholder type + * @return the placeholder details or {@code null}, if the placeholder isn't contained in the sheet + * + * @since POI 4.0.0 + */ + PlaceholderDetails getPlaceholderDetails(Placeholder placeholder); } diff --git a/src/java/org/apache/poi/sl/usermodel/SimpleShape.java b/src/java/org/apache/poi/sl/usermodel/SimpleShape.java index 7bbbddfd0..ec31b5cc6 100644 --- a/src/java/org/apache/poi/sl/usermodel/SimpleShape.java +++ b/src/java/org/apache/poi/sl/usermodel/SimpleShape.java @@ -65,6 +65,24 @@ public interface SimpleShape< */ void setPlaceholder(Placeholder placeholder); + /** + * @return an accessor for placeholder details + * + * @since POI 4.0.0 + */ + PlaceholderDetails getPlaceholderDetails(); + + /** + * Checks if the shape is a placeholder. + * (placeholders aren't normal shapes, they are visible only in the Edit Master mode) + * + * @return {@code true} if the shape is a placeholder + * + * @since POI 4.0.0 + */ + boolean isPlaceholder(); + + Shadow getShadow(); /** diff --git a/src/java/org/apache/poi/sl/usermodel/Slide.java b/src/java/org/apache/poi/sl/usermodel/Slide.java index 74c9d6b0f..f5aed9273 100644 --- a/src/java/org/apache/poi/sl/usermodel/Slide.java +++ b/src/java/org/apache/poi/sl/usermodel/Slide.java @@ -17,6 +17,8 @@ package org.apache.poi.sl.usermodel; +import java.util.List; + public interface Slide< S extends Shape, P extends TextParagraph @@ -48,7 +50,7 @@ public interface Slide< * whereas in HSLF they are activated via a HeadersFooter configuration. * This method is used to generalize that handling. * - * @param placeholder + * @param placeholder the placeholder type * @return {@code true} if the placeholder should be displayed/rendered * @since POI 3.16-beta2 */ @@ -69,4 +71,9 @@ public interface Slide< * @since POI 4.0.0 */ boolean isHidden(); + + /** + * @return the comment(s) for this slide + */ + List getComments(); } diff --git a/src/java/org/apache/poi/sl/usermodel/SlideShow.java b/src/java/org/apache/poi/sl/usermodel/SlideShow.java index 90d81df3b..8ddd96217 100644 --- a/src/java/org/apache/poi/sl/usermodel/SlideShow.java +++ b/src/java/org/apache/poi/sl/usermodel/SlideShow.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import org.apache.poi.POITextExtractor; import org.apache.poi.sl.usermodel.PictureData.PictureType; public interface SlideShow< @@ -118,4 +119,11 @@ public interface SlideShow< * OutputStream */ void write(OutputStream out) throws IOException; + + /** + * @return an extractor for the slideshow metadata + * + * @since POI 4.0.0 + */ + POITextExtractor getMetadataTextExtractor(); } diff --git a/src/ooxml/java/org/apache/poi/xslf/extractor/XSLFPowerPointExtractor.java b/src/ooxml/java/org/apache/poi/xslf/extractor/XSLFPowerPointExtractor.java index 6e96f0e2b..ef9097f15 100644 --- a/src/ooxml/java/org/apache/poi/xslf/extractor/XSLFPowerPointExtractor.java +++ b/src/ooxml/java/org/apache/poi/xslf/extractor/XSLFPowerPointExtractor.java @@ -21,202 +21,188 @@ import java.io.IOException; import org.apache.poi.POIXMLTextExtractor; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.sl.extractor.SlideShowExtractor; +import org.apache.poi.util.Removal; import org.apache.poi.xslf.usermodel.XMLSlideShow; -import org.apache.poi.xslf.usermodel.XSLFCommentAuthors; -import org.apache.poi.xslf.usermodel.XSLFComments; -import org.apache.poi.xslf.usermodel.XSLFNotes; import org.apache.poi.xslf.usermodel.XSLFRelation; import org.apache.poi.xslf.usermodel.XSLFShape; -import org.apache.poi.xslf.usermodel.XSLFShapeContainer; import org.apache.poi.xslf.usermodel.XSLFSlide; -import org.apache.poi.xslf.usermodel.XSLFSlideLayout; -import org.apache.poi.xslf.usermodel.XSLFSlideMaster; import org.apache.poi.xslf.usermodel.XSLFSlideShow; -import org.apache.poi.xslf.usermodel.XSLFTable; -import org.apache.poi.xslf.usermodel.XSLFTableCell; -import org.apache.poi.xslf.usermodel.XSLFTableRow; -import org.apache.poi.xslf.usermodel.XSLFTextShape; +import org.apache.poi.xslf.usermodel.XSLFTextParagraph; import org.apache.xmlbeans.XmlException; -import org.openxmlformats.schemas.presentationml.x2006.main.CTComment; -import org.openxmlformats.schemas.presentationml.x2006.main.CTCommentAuthor; +/** + * Extractor for XSLF SlideShows + * + * @deprecated use {@link SlideShowExtractor} + */ +@Deprecated +@Removal(version="4.2.0") public class XSLFPowerPointExtractor extends POIXMLTextExtractor { - public static final XSLFRelation[] SUPPORTED_TYPES = new XSLFRelation[] { - XSLFRelation.MAIN, XSLFRelation.MACRO, XSLFRelation.MACRO_TEMPLATE, - XSLFRelation.PRESENTATIONML, XSLFRelation.PRESENTATIONML_TEMPLATE, - XSLFRelation.PRESENTATION_MACRO - }; - - private XMLSlideShow slideshow; - private boolean slidesByDefault = true; - private boolean notesByDefault; - private boolean masterByDefault; - - public XSLFPowerPointExtractor(XMLSlideShow slideshow) { - super(slideshow); - this.slideshow = slideshow; - } - public XSLFPowerPointExtractor(XSLFSlideShow slideshow) throws XmlException, IOException { - this(new XMLSlideShow(slideshow.getPackage())); - } - public XSLFPowerPointExtractor(OPCPackage container) throws XmlException, OpenXML4JException, IOException { - this(new XSLFSlideShow(container)); - } + public static final XSLFRelation[] SUPPORTED_TYPES = new XSLFRelation[]{ + XSLFRelation.MAIN, XSLFRelation.MACRO, XSLFRelation.MACRO_TEMPLATE, + XSLFRelation.PRESENTATIONML, XSLFRelation.PRESENTATIONML_TEMPLATE, + XSLFRelation.PRESENTATION_MACRO + }; - public static void main(String[] args) throws Exception { - if(args.length < 1) { - System.err.println("Use:"); - System.err.println(" XSLFPowerPointExtractor "); - System.exit(1); - } - POIXMLTextExtractor extractor = - new XSLFPowerPointExtractor( - new XSLFSlideShow(args[0])); - System.out.println(extractor.getText()); - extractor.close(); - } + private final SlideShowExtractor delegate; - /** - * Should a call to getText() return slide text? - * Default is yes - */ - public void setSlidesByDefault(boolean slidesByDefault) { - this.slidesByDefault = slidesByDefault; - } - /** - * Should a call to getText() return notes text? - * Default is no - */ - public void setNotesByDefault(boolean notesByDefault) { - this.notesByDefault = notesByDefault; - } - - /** - * Should a call to getText() return text from master? Default is no - */ - public void setMasterByDefault(boolean masterByDefault) { - this.masterByDefault = masterByDefault; - } - - /** - * Gets the slide text, but not the notes text - */ - @Override + + private boolean slidesByDefault = true; + private boolean notesByDefault; + private boolean commentsByDefault; + private boolean masterByDefault; + + @SuppressWarnings("WeakerAccess") + public XSLFPowerPointExtractor(XMLSlideShow slideShow) { + super(slideShow); + delegate = new SlideShowExtractor<>(slideShow); + } + + public XSLFPowerPointExtractor(XSLFSlideShow slideShow) { + this(new XMLSlideShow(slideShow.getPackage())); + } + + public XSLFPowerPointExtractor(OPCPackage container) throws XmlException, OpenXML4JException, IOException { + this(new XSLFSlideShow(container)); + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Use:"); + System.err.println(" XSLFPowerPointExtractor "); + System.exit(1); + } + POIXMLTextExtractor extractor = + new XSLFPowerPointExtractor( + new XSLFSlideShow(args[0])); + System.out.println(extractor.getText()); + extractor.close(); + } + + /** + * Should a call to getText() return slide text? + * Default is yes + */ + @SuppressWarnings("WeakerAccess") + public void setSlidesByDefault(final boolean slidesByDefault) { + this.slidesByDefault = slidesByDefault; + delegate.setSlidesByDefault(slidesByDefault); + } + + /** + * Should a call to getText() return notes text? + * Default is no + */ + @SuppressWarnings("WeakerAccess") + public void setNotesByDefault(final boolean notesByDefault) { + this.notesByDefault = notesByDefault; + delegate.setNotesByDefault(notesByDefault); + } + + /** + * Should a call to getText() return comments text? Default is no + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public void setCommentsByDefault(final boolean commentsByDefault) { + this.commentsByDefault = commentsByDefault; + delegate.setCommentsByDefault(commentsByDefault); + } + + /** + * Should a call to getText() return text from master? Default is no + */ + @SuppressWarnings("WeakerAccess") + public void setMasterByDefault(final boolean masterByDefault) { + this.masterByDefault = masterByDefault; + delegate.setMasterByDefault(masterByDefault); + } + + /** + * Gets the slide text, but not the notes text + */ + @Override public String getText() { - return getText(slidesByDefault, notesByDefault); - } - - /** - * Gets the requested text from the file - * @param slideText Should we retrieve text from slides? - * @param notesText Should we retrieve text from notes? - */ - public String getText(boolean slideText, boolean notesText) { - return getText(slideText, notesText, masterByDefault); - } - - /** - * Gets the requested text from the file - * - * @param slideText Should we retrieve text from slides? - * @param notesText Should we retrieve text from notes? - * @param masterText Should we retrieve text from master slides? - * - * @return the extracted text - */ - public String getText(boolean slideText, boolean notesText, boolean masterText) { - StringBuilder text = new StringBuilder(); + return delegate.getText(); + } - for (XSLFSlide slide : slideshow.getSlides()) { - text.append(getText(slide, slideText, notesText, masterText)); - } + /** + * Gets the requested text from the file + * + * @param slideText Should we retrieve text from slides? + * @param notesText Should we retrieve text from notes? + */ + public String getText(final boolean slideText, final boolean notesText) { + return getText(slideText, notesText, commentsByDefault, masterByDefault); + } - return text.toString(); - } + /** + * Gets the requested text from the file + * + * @param slideText Should we retrieve text from slides? + * @param notesText Should we retrieve text from notes? + * @param masterText Should we retrieve text from master slides? + * @return the extracted text + */ + public String getText(boolean slideText, boolean notesText, boolean masterText) { + return getText(slideText, notesText, commentsByDefault, masterText); + } - /** - * Gets the requested text from the slide - * - * @param slide the slide to retrieve the text from - * @param slideText Should we retrieve text from slides? - * @param notesText Should we retrieve text from notes? - * @param masterText Should we retrieve text from master slides? - * - * @return the extracted text - */ - public static String getText(XSLFSlide slide, boolean slideText, boolean notesText, boolean masterText) { - StringBuilder text = new StringBuilder(); - XSLFCommentAuthors commentAuthors = slide.getSlideShow().getCommentAuthors(); + /** + * Gets the requested text from the file + * + * @param slideText Should we retrieve text from slides? + * @param notesText Should we retrieve text from notes? + * @param commentText Should we retrieve text from comments? + * @param masterText Should we retrieve text from master slides? + * @return the extracted text + */ + @SuppressWarnings("Duplicates") + public String getText(boolean slideText, boolean notesText, boolean commentText, boolean masterText) { + delegate.setSlidesByDefault(slideText); + delegate.setNotesByDefault(notesText); + delegate.setCommentsByDefault(commentText); + delegate.setMasterByDefault(masterText); + try { + return delegate.getText(); + } finally { + delegate.setSlidesByDefault(slidesByDefault); + delegate.setNotesByDefault(notesByDefault); + delegate.setCommentsByDefault(commentsByDefault); + delegate.setMasterByDefault(masterByDefault); + } + } - XSLFNotes notes = slide.getNotes(); - XSLFComments comments = slide.getComments(); - XSLFSlideLayout layout = slide.getSlideLayout(); - XSLFSlideMaster master = layout.getSlideMaster(); + /** + * Gets the requested text from the slide + * + * @param slide the slide to retrieve the text from + * @param slideText Should we retrieve text from slides? + * @param notesText Should we retrieve text from notes? + * @param masterText Should we retrieve text from master slides? + * @return the extracted text + */ + public static String getText(XSLFSlide slide, boolean slideText, boolean notesText, boolean masterText) { + return getText(slide, slideText, notesText, false, masterText); + } - // TODO Do the slide's name - // (Stored in docProps/app.xml) - - // Do the slide's text if requested - if (slideText) { - extractText(slide, false, text); - - // If requested, get text from the master and it's layout - if(masterText) { - assert (layout != null); - extractText(layout, true, text); - assert (master != null); - extractText(master, true, text); - } - - // If the slide has comments, do those too - if (comments != null) { - for (CTComment comment : comments.getCTCommentsList().getCmArray()) { - // Do the author if we can - if (commentAuthors != null) { - CTCommentAuthor author = commentAuthors.getAuthorById(comment.getAuthorId()); - if(author != null) { - text.append(author.getName() + ": "); - } - } - - // Then the comment text, with a new line afterwards - text.append(comment.getText()); - text.append("\n"); - } - } - } - - // Do the notes if requested - if (notesText && notes != null) { - extractText(notes, false, text); - } - - return text.toString(); - } - - private static void extractText(XSLFShapeContainer data, boolean skipPlaceholders, StringBuilder text) { - for (XSLFShape s : data) { - if (s instanceof XSLFShapeContainer) { - extractText((XSLFShapeContainer)s, skipPlaceholders, text); - } else if (s instanceof XSLFTextShape) { - XSLFTextShape ts = (XSLFTextShape)s; - // Skip non-customised placeholder text - if (!(skipPlaceholders && ts.isPlaceholder())) { - text.append(ts.getText()); - text.append("\n"); - } - } else if (s instanceof XSLFTable) { - XSLFTable ts = (XSLFTable)s; - // Skip non-customised placeholder text - for (XSLFTableRow r : ts) { - for (XSLFTableCell c : r) { - text.append(c.getText()); - text.append("\t"); - } - text.append("\n"); - } - } - } - } + /** + * Gets the requested text from the slide + * + * @param slide the slide to retrieve the text from + * @param slideText Should we retrieve text from slides? + * @param notesText Should we retrieve text from notes? + * @param commentText Should we retrieve text from comments? + * @param masterText Should we retrieve text from master slides? + * @return the extracted text + */ + public static String getText(XSLFSlide slide, boolean slideText, boolean notesText, boolean commentText, boolean masterText) { + final SlideShowExtractor ex = new SlideShowExtractor<>(slide.getSlideShow()); + ex.setSlidesByDefault(slideText); + ex.setNotesByDefault(notesText); + ex.setCommentsByDefault(commentText); + ex.setMasterByDefault(masterText); + return ex.getText(slide); + } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java index a935174f1..8734f3603 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java @@ -35,10 +35,10 @@ import java.util.regex.Pattern; import org.apache.poi.POIXMLDocument; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.POIXMLException; +import org.apache.poi.POIXMLPropertiesTextExtractor; import org.apache.poi.openxml4j.exceptions.OpenXML4JException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; -import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.sl.usermodel.MasterSheet; import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.sl.usermodel.Resources; @@ -626,4 +626,9 @@ public class XMLSlideShow extends POIXMLDocument // TODO: implement! throw new UnsupportedOperationException(); } + + @Override + public POIXMLPropertiesTextExtractor getMetadataTextExtractor() { + return new POIXMLPropertiesTextExtractor(this); + } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComment.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComment.java new file mode 100644 index 000000000..917df0dd9 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComment.java @@ -0,0 +1,126 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.xslf.usermodel; + +import java.awt.geom.Point2D; +import java.util.Calendar; +import java.util.Date; + +import org.apache.poi.sl.usermodel.Comment; +import org.apache.poi.util.LocaleUtil; +import org.apache.poi.util.Units; +import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; +import org.openxmlformats.schemas.presentationml.x2006.main.CTComment; +import org.openxmlformats.schemas.presentationml.x2006.main.CTCommentAuthor; +import org.openxmlformats.schemas.presentationml.x2006.main.CTCommentAuthorList; + +/** + * XSLF Comment + * + * @since POI 4.0.0 + */ +public class XSLFComment implements Comment { + + final CTComment comment; + final XSLFCommentAuthors authors; + + XSLFComment(final CTComment comment, final XSLFCommentAuthors authors) { + this.comment = comment; + this.authors = authors; + } + + @Override + public String getAuthor() { + return authors.getAuthorById(comment.getAuthorId()).getName(); + } + + @Override + public void setAuthor(final String author) { + if (author == null) { + throw new IllegalArgumentException("author must not be null"); + } + final CTCommentAuthorList list = authors.getCTCommentAuthorsList(); + long maxId = -1; + for (final CTCommentAuthor aut : list.getCmAuthorArray()) { + maxId = Math.max(aut.getId(), maxId); + if (author.equals(aut.getName())) { + comment.setAuthorId(aut.getId()); + return; + } + } + // author not found -> add new author + final CTCommentAuthor newAuthor = list.addNewCmAuthor(); + newAuthor.setName(author); + newAuthor.setId(maxId+1); + newAuthor.setInitials(author.replaceAll( "\\s*(\\w)\\S*", "$1").toUpperCase(LocaleUtil.getUserLocale())); + comment.setAuthorId(maxId+1); + } + + @Override + public String getAuthorInitials() { + final CTCommentAuthor aut = authors.getAuthorById(comment.getAuthorId()); + return aut == null ? null : aut.getInitials(); + } + + @Override + public void setAuthorInitials(final String initials) { + final CTCommentAuthor aut = authors.getAuthorById(comment.getAuthorId()); + if (aut != null) { + aut.setInitials(initials); + } + } + + @Override + public String getText() { + return comment.getText(); + } + + @Override + public void setText(final String text) { + comment.setText(text); + } + + @Override + public Date getDate() { + final Calendar cal = comment.getDt(); + return (cal == null) ? null : cal.getTime(); + } + + @Override + public void setDate(final Date date) { + final Calendar cal = LocaleUtil.getLocaleCalendar(); + cal.setTime(date); + comment.setDt(cal); + } + + @Override + public Point2D getOffset() { + final CTPoint2D pos = comment.getPos(); + return new Point2D.Double(Units.toPoints(pos.getX()), Units.toPoints(pos.getY())); + } + + @Override + public void setOffset(final Point2D offset) { + CTPoint2D pos = comment.getPos(); + if (pos == null) { + pos = comment.addNewPos(); + } + pos.setX(Units.toEMU(offset.getX())); + pos.setY(Units.toEMU(offset.getY())); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComments.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComments.java index 04e09c865..7a281d5bc 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComments.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFComments.java @@ -31,42 +31,37 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CmLstDocument; @Beta public class XSLFComments extends POIXMLDocumentPart { - private final CTCommentList _comments; - + private final CmLstDocument doc; + /** * Create a new set of slide comments */ XSLFComments() { - super(); - CmLstDocument doc = CmLstDocument.Factory.newInstance(); - _comments = doc.addNewCmLst(); + doc = CmLstDocument.Factory.newInstance(); } /** * Construct a SpreadsheetML slide comments from a package part * * @param part the package part holding the comments data, - * the content type must be application/vnd.openxmlformats-officedocument.comments+xml - * + * the content type must be application/vnd.openxmlformats-officedocument.comments+xml * @since POI 3.14-Beta1 */ XSLFComments(PackagePart part) throws IOException, XmlException { super(part); - CmLstDocument doc = - CmLstDocument.Factory.parse(getPackagePart().getInputStream(), DEFAULT_XML_OPTIONS); - _comments = doc.getCmLst(); + doc = CmLstDocument.Factory.parse(getPackagePart().getInputStream(), DEFAULT_XML_OPTIONS); } public CTCommentList getCTCommentsList() { - return _comments; + return doc.getCmLst(); } - + public int getNumberOfComments() { - return _comments.sizeOfCmArray(); + return doc.getCmLst().sizeOfCmArray(); } - + public CTComment getCommentAt(int pos) { - return _comments.getCmArray(pos); + return doc.getCmLst().getCmArray(pos); } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFNotesMaster.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFNotesMaster.java index 59d8d6746..b142416b3 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFNotesMaster.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFNotesMaster.java @@ -51,7 +51,6 @@ import org.openxmlformats.schemas.presentationml.x2006.main.NotesMasterDocument; public class XSLFNotesMaster extends XSLFSheet implements MasterSheet { private CTNotesMaster _slide; - private XSLFTheme _theme; XSLFNotesMaster() { super(); @@ -100,21 +99,15 @@ import org.openxmlformats.schemas.presentationml.x2006.main.NotesMasterDocument; public MasterSheet getMasterSheet() { return null; } - + + @Override - public XSLFTheme getTheme() { - if (_theme == null) { - for (POIXMLDocumentPart p : getRelations()) { - if (p instanceof XSLFTheme) { - _theme = (XSLFTheme) p; - CTColorMapping cmap = _slide.getClrMap(); - if (cmap != null) { - _theme.initColorMap(cmap); - } - break; - } - } - } - return _theme; - } + boolean isSupportTheme() { + return true; + } + + @Override + CTColorMapping getColorMapping() { + return _slide.getClrMap(); + } } \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java new file mode 100644 index 000000000..25fbd6344 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java @@ -0,0 +1,203 @@ +package org.apache.poi.xslf.usermodel; + +import static org.apache.poi.xslf.usermodel.XSLFShape.PML_NS; + +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.poi.sl.usermodel.MasterSheet; +import org.apache.poi.sl.usermodel.Placeholder; +import org.apache.poi.sl.usermodel.PlaceholderDetails; +import org.openxmlformats.schemas.presentationml.x2006.main.CTApplicationNonVisualDrawingProps; +import org.openxmlformats.schemas.presentationml.x2006.main.CTHeaderFooter; +import org.openxmlformats.schemas.presentationml.x2006.main.CTNotesMaster; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; +import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMaster; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderSize; +import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; + +/** + * XSLF Placeholder Details + * + * @since POI 4.0.0 + */ +public class XSLFPlaceholderDetails implements PlaceholderDetails { + + private final XSLFShape shape; + private CTPlaceholder _ph; + + XSLFPlaceholderDetails(final XSLFShape shape) { + this.shape = shape; + } + + @Override + public Placeholder getPlaceholder() { + final CTPlaceholder ph = getCTPlaceholder(false); + if (ph == null || !(ph.isSetType() || ph.isSetIdx())) { + return null; + } + return Placeholder.lookupOoxml(ph.getType().intValue()); + } + + @Override + public void setPlaceholder(final Placeholder placeholder) { + CTPlaceholder ph = getCTPlaceholder(placeholder != null); + if (ph != null) { + if (placeholder != null) { + ph.setType(STPlaceholderType.Enum.forInt(placeholder.ooxmlId)); + } else { + getNvProps().unsetPh(); + } + } + } + + @Override + public boolean isVisible() { + final CTPlaceholder ph = getCTPlaceholder(false); + if (ph == null || !ph.isSetType()) { + return true; + } + final CTHeaderFooter hf = getHeaderFooter(false); + if (hf == null) { + return false; + } + + final Placeholder pl = Placeholder.lookupOoxml(ph.getType().intValue()); + if (pl == null) { + return true; + } + switch (pl) { + case DATETIME: + return !hf.isSetDt() || hf.getDt(); + case FOOTER: + return !hf.isSetFtr() || hf.getFtr(); + case HEADER: + return !hf.isSetHdr() || hf.getHdr(); + case SLIDE_NUMBER: + return !hf.isSetSldNum() || hf.getSldNum(); + default: + return true; + } + } + + @Override + public void setVisible(final boolean isVisible) { + final Placeholder ph = getPlaceholder(); + if (ph == null) { + return; + } + final Function> fun; + switch (ph) { + case DATETIME: + fun = (hf) -> hf::setDt; + break; + case FOOTER: + fun = (hf) -> hf::setFtr; + break; + case HEADER: + fun = (hf) -> hf::setHdr; + break; + case SLIDE_NUMBER: + fun = (hf) -> hf::setSldNum; + break; + default: + return; + } + // only create a header, if we need to, i.e. the placeholder type is eligible + final CTHeaderFooter hf = getHeaderFooter(true); + if (hf == null) { + return; + } + fun.apply(hf).accept(isVisible); + } + + @Override + public PlaceholderSize getSize() { + final CTPlaceholder ph = getCTPlaceholder(false); + if (ph == null || !ph.isSetSz()) { + return null; + } + switch (ph.getSz().intValue()) { + case STPlaceholderSize.INT_FULL: + return PlaceholderSize.full; + case STPlaceholderSize.INT_HALF: + return PlaceholderSize.half; + case STPlaceholderSize.INT_QUARTER: + return PlaceholderSize.quarter; + default: + return null; + } + } + + @Override + public void setSize(final PlaceholderSize size) { + final CTPlaceholder ph = getCTPlaceholder(false); + if (ph == null) { + return; + } + if (size == null) { + ph.unsetSz(); + return; + } + switch (size) { + case full: + ph.setSz(STPlaceholderSize.FULL); + break; + case half: + ph.setSz(STPlaceholderSize.HALF); + break; + case quarter: + ph.setSz(STPlaceholderSize.QUARTER); + break; + } + } + + /** + * Gets or creates a new placeholder element + * + * @param create if {@code true} creates the element if it hasn't existed before + * @return the placeholder or {@code null} if the shape doesn't support placeholders + */ + CTPlaceholder getCTPlaceholder(final boolean create) { + if (_ph != null) { + return _ph; + } + + final CTApplicationNonVisualDrawingProps nv = getNvProps(); + if (nv == null) { + // shape doesn't support CTApplicationNonVisualDrawingProps + return null; + } + + _ph = (nv.isSetPh() || !create) ? nv.getPh() : nv.addNewPh(); + return _ph; + } + + private CTApplicationNonVisualDrawingProps getNvProps() { + final String xquery = "declare namespace p='" + PML_NS + "' .//*/p:nvPr"; + return shape.selectProperty(CTApplicationNonVisualDrawingProps.class, xquery); + } + + private CTHeaderFooter getHeaderFooter(final boolean create) { + final XSLFSheet sheet = shape.getSheet(); + final XSLFSheet master = (sheet instanceof MasterSheet && !(sheet instanceof XSLFSlideLayout)) ? sheet : (XSLFSheet)sheet.getMasterSheet(); + if (master instanceof XSLFSlideMaster) { + final CTSlideMaster ct = ((XSLFSlideMaster) master).getXmlObject(); + return (ct.isSetHf() || !create) ? ct.getHf() : ct.addNewHf(); + } else if (master instanceof XSLFNotesMaster) { + final CTNotesMaster ct = ((XSLFNotesMaster) master).getXmlObject(); + return (ct.isSetHf() || !create) ? ct.getHf() : ct.addNewHf(); + } else { + return null; + } + } + + @Override + public String getText() { + return null; + } + + @Override + public void setText(String text) { + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java index 91b5abd63..35a54e96e 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java @@ -38,7 +38,9 @@ import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint; import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint; import org.apache.poi.sl.usermodel.PlaceableShape; import org.apache.poi.sl.usermodel.Placeholder; +import org.apache.poi.sl.usermodel.PlaceholderDetails; import org.apache.poi.sl.usermodel.Shape; +import org.apache.poi.sl.usermodel.SimpleShape; import org.apache.poi.util.Beta; import org.apache.poi.util.Internal; import org.apache.poi.xslf.model.PropertyFetcher; @@ -58,7 +60,6 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTSolidColorFillPropertie import org.openxmlformats.schemas.drawingml.x2006.main.CTStyleMatrix; import org.openxmlformats.schemas.drawingml.x2006.main.CTStyleMatrixReference; import org.openxmlformats.schemas.drawingml.x2006.main.STPathShadeType; -import org.openxmlformats.schemas.presentationml.x2006.main.CTApplicationNonVisualDrawingProps; import org.openxmlformats.schemas.presentationml.x2006.main.CTBackgroundProperties; import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; import org.openxmlformats.schemas.presentationml.x2006.main.CTShape; @@ -69,7 +70,7 @@ import org.openxmlformats.schemas.presentationml.x2006.main.STPlaceholderType; */ @Beta public abstract class XSLFShape implements Shape { - protected static final String PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"; + static final String PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"; private final XmlObject _shape; private final XSLFSheet _sheet; @@ -77,7 +78,6 @@ public abstract class XSLFShape implements Shape { private CTShapeStyle _spStyle; private CTNonVisualDrawingProps _nvPr; - private CTPlaceholder _ph; protected XSLFShape(XmlObject shape, XSLFSheet sheet) { _shape = shape; @@ -238,45 +238,32 @@ public abstract class XSLFShape implements Shape { cur.dispose(); return child; } - - protected CTPlaceholder getCTPlaceholder() { - if (_ph == null) { - String xquery = "declare namespace p='"+PML_NS+"' .//*/p:nvPr/p:ph"; - _ph = selectProperty(CTPlaceholder.class, xquery); - } - return _ph; + + public boolean isPlaceholder() { + return getPlaceholderDetails().getCTPlaceholder(false) != null; } + /** + * @see PlaceholderDetails#getPlaceholder() + */ public Placeholder getPlaceholder() { - CTPlaceholder ph = getCTPlaceholder(); - if (ph == null || !(ph.isSetType() || ph.isSetIdx())) { - return null; - } - return Placeholder.lookupOoxml(ph.getType().intValue()); + return getPlaceholderDetails().getPlaceholder(); } /** - * Specifies that the corresponding shape should be represented by the generating application - * as a placeholder. When a shape is considered a placeholder by the generating application - * it can have special properties to alert the user that they may enter content into the shape. - * Different types of placeholders are allowed and can be specified by using the placeholder - * type attribute for this element - * - * @param placeholder The shape to use as placeholder or null if no placeholder should be set. + * @see PlaceholderDetails#setPlaceholder(Placeholder) */ - protected void setPlaceholder(Placeholder placeholder) { - String xquery = "declare namespace p='"+PML_NS+"' .//*/p:nvPr"; - CTApplicationNonVisualDrawingProps nv = selectProperty(CTApplicationNonVisualDrawingProps.class, xquery); - if (nv == null) return; - if(placeholder == null) { - if (nv.isSetPh()) nv.unsetPh(); - _ph = null; - } else { - nv.addNewPh().setType(STPlaceholderType.Enum.forInt(placeholder.ooxmlId)); - } + public void setPlaceholder(final Placeholder placeholder) { + getPlaceholderDetails().setPlaceholder(placeholder); } - - + + /** + * @see SimpleShape#getPlaceholderDetails() + */ + public XSLFPlaceholderDetails getPlaceholderDetails() { + return new XSLFPlaceholderDetails(this); + } + /** * As there's no xmlbeans hierarchy, but XSLF works with subclassing, not all * child classes work with a {@link CTShape} object, but often contain the same @@ -315,7 +302,7 @@ public abstract class XSLFShape implements Shape { return true; } - CTPlaceholder ph = getCTPlaceholder(); + final CTPlaceholder ph = getPlaceholderDetails().getCTPlaceholder(false); if (ph == null) { return false; } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java index 8936f209b..e8b4f2cc4 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java @@ -18,6 +18,7 @@ package org.apache.poi.xslf.usermodel; import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; +import javax.xml.namespace.QName; import java.awt.Dimension; import java.awt.Graphics2D; import java.io.IOException; @@ -28,8 +29,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; - -import javax.xml.namespace.QName; +import java.util.Optional; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.POIXMLException; @@ -56,6 +56,7 @@ import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl; +import org.openxmlformats.schemas.drawingml.x2006.main.CTColorMapping; import org.openxmlformats.schemas.presentationml.x2006.main.CTConnector; import org.openxmlformats.schemas.presentationml.x2006.main.CTGraphicalObjectFrame; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape; @@ -72,6 +73,7 @@ implements XSLFShapeContainer, Sheet { private XSLFDrawing _drawing; private List _shapes; private CTGroupShape _spTree; + private XSLFTheme _theme; private List_placeholders; private Map _placeholderByIdMap; @@ -456,7 +458,36 @@ implements XSLFShapeContainer, Sheet { * Sheets that support the notion of themes (slides, masters, layouts, etc.) should override this * method and return the corresponding package part. */ - XSLFTheme getTheme(){ + public XSLFTheme getTheme() { + if (_theme != null || !isSupportTheme()) { + return _theme; + } + + final Optional t = + getRelations().stream().filter((p) -> p instanceof XSLFTheme).map((p) -> (XSLFTheme) p).findAny(); + if (t.isPresent()) { + _theme = t.get(); + final CTColorMapping cmap = getColorMapping(); + if (cmap != null) { + _theme.initColorMap(cmap); + } + } + return _theme; + } + + + + /** + * @return {@code true} if this class supports themes + */ + boolean isSupportTheme() { + return false; + } + + /** + * @return the color mapping for this slide type + */ + CTColorMapping getColorMapping() { return null; } @@ -488,16 +519,16 @@ implements XSLFShapeContainer, Sheet { return shape; } - void initPlaceholders() { + private void initPlaceholders() { if(_placeholders == null) { _placeholders = new ArrayList<>(); _placeholderByIdMap = new HashMap<>(); _placeholderByTypeMap = new HashMap<>(); - for(XSLFShape sh : getShapes()){ + for(final XSLFShape sh : getShapes()){ if(sh instanceof XSLFTextShape){ - XSLFTextShape sShape = (XSLFTextShape)sh; - CTPlaceholder ph = sShape.getCTPlaceholder(); + final XSLFTextShape sShape = (XSLFTextShape)sh; + final CTPlaceholder ph = sShape.getPlaceholderDetails().getCTPlaceholder(false); if(ph != null) { _placeholders.add(sShape); if(ph.isSetIdx()) { @@ -513,7 +544,7 @@ implements XSLFShapeContainer, Sheet { } } - XSLFSimpleShape getPlaceholderById(int id) { + private XSLFSimpleShape getPlaceholderById(int id) { initPlaceholders(); return _placeholderByIdMap.get(id); } @@ -574,7 +605,7 @@ implements XSLFShapeContainer, Sheet { /** * Render this sheet into the supplied graphics object * - * @param graphics + * @param graphics the graphics context to draw to */ @Override public void draw(Graphics2D graphics){ @@ -645,4 +676,12 @@ implements XSLFShapeContainer, Sheet { void removePictureRelation(XSLFPictureShape pictureShape) { removeRelation(pictureShape.getBlipId()); } + + + @Override + public XSLFPlaceholderDetails getPlaceholderDetails(Placeholder placeholder) { + final XSLFSimpleShape ph = getPlaceholder(placeholder); + return (ph == null) ? null : new XSLFPlaceholderDetails(ph); + } + } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java index 751bd9b92..0326250db 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSimpleShape.java @@ -36,7 +36,6 @@ import org.apache.poi.sl.usermodel.LineDecoration.DecorationShape; import org.apache.poi.sl.usermodel.LineDecoration.DecorationSize; import org.apache.poi.sl.usermodel.PaintStyle; import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint; -import org.apache.poi.sl.usermodel.Placeholder; import org.apache.poi.sl.usermodel.ShapeType; import org.apache.poi.sl.usermodel.SimpleShape; import org.apache.poi.sl.usermodel.StrokeStyle; @@ -980,11 +979,6 @@ public abstract class XSLFSimpleShape extends XSLFShape return ds; } - public boolean isPlaceholder() { - CTPlaceholder ph = getCTPlaceholder(); - return ph != null; - } - @Override public Guide getAdjustValue(String name) { XSLFGeometryProperties gp = XSLFPropertiesDelegate.getGeometryDelegate(getShapeProperties()); @@ -1105,11 +1099,6 @@ public abstract class XSLFSimpleShape extends XSLFShape } } - @Override - public void setPlaceholder(Placeholder placeholder) { - super.setPlaceholder(placeholder); - } - @Override public XSLFHyperlink getHyperlink() { CTNonVisualDrawingProps cNvPr = getCNvPr(); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java index 7c3d1f66c..ce650c152 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlide.java @@ -20,6 +20,8 @@ import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; import java.awt.Graphics2D; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import org.apache.poi.POIXMLDocumentPart; import org.apache.poi.openxml4j.opc.PackagePart; @@ -38,6 +40,7 @@ import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps; import org.openxmlformats.schemas.drawingml.x2006.main.CTPoint2D; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; import org.openxmlformats.schemas.presentationml.x2006.main.CTBackground; +import org.openxmlformats.schemas.presentationml.x2006.main.CTComment; import org.openxmlformats.schemas.presentationml.x2006.main.CTCommonSlideData; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShape; import org.openxmlformats.schemas.presentationml.x2006.main.CTGroupShapeNonVisual; @@ -52,6 +55,7 @@ implements Slide { private final CTSlide _slide; private XSLFSlideLayout _layout; private XSLFComments _comments; + private XSLFCommentAuthors _commentAuthors; private XSLFNotes _notes; /** @@ -155,22 +159,55 @@ implements Slide { return getSlideLayout().getSlideMaster(); } - public XSLFComments getComments() { + /** + * @return the comments part or {@code null} if there weren't any comments + * @since POI 4.0.0 + */ + public XSLFComments getCommentsPart() { if(_comments == null) { for (POIXMLDocumentPart p : getRelations()) { if (p instanceof XSLFComments) { _comments = (XSLFComments)p; + break; } } } - if(_comments == null) { - // This slide lacks comments - // Not all have them, sorry... - return null; - } + return _comments; } + /** + * @return the comment authors part or {@code null} if there weren't any comments + * @since POI 4.0.0 + */ + public XSLFCommentAuthors getCommentAuthorsPart() { + if(_commentAuthors == null) { + for (POIXMLDocumentPart p : getRelations()) { + if (p instanceof XSLFCommentAuthors) { + _commentAuthors = (XSLFCommentAuthors)p; + return _commentAuthors; + } + } + } + + return null; + } + + + @Override + public List getComments() { + final List comments = new ArrayList<>(); + final XSLFComments xComments = getCommentsPart(); + final XSLFCommentAuthors xAuthors = getCommentAuthorsPart(); + if (xComments != null) { + for (final CTComment xc : xComments.getCTCommentsList().getCmArray()) { + comments.add(new XSLFComment(xc, xAuthors)); + } + } + + return comments; + } + @Override public XSLFNotes getNotes() { if(_notes == null) { diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java index 14684b4c6..bf5ef5ad2 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideLayout.java @@ -28,21 +28,15 @@ import org.apache.poi.util.Beta; import org.apache.poi.util.Internal; import org.apache.xmlbeans.XmlException; import org.openxmlformats.schemas.presentationml.x2006.main.CTBackground; -import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideLayout; import org.openxmlformats.schemas.presentationml.x2006.main.SldLayoutDocument; @Beta public class XSLFSlideLayout extends XSLFSheet implements MasterSheet { - private CTSlideLayout _layout; + private final CTSlideLayout _layout; private XSLFSlideMaster _master; - XSLFSlideLayout() { - super(); - _layout = CTSlideLayout.Factory.newInstance(); - } - /** * @since POI 3.14-Beta1 */ @@ -111,14 +105,7 @@ implements MasterSheet { */ @Override protected boolean canDraw(XSLFShape shape) { - if (shape instanceof XSLFSimpleShape) { - XSLFSimpleShape txt = (XSLFSimpleShape) shape; - CTPlaceholder ph = txt.getCTPlaceholder(); - if (ph != null) { - return false; - } - } - return true; + return !(shape instanceof XSLFSimpleShape) || !shape.isPlaceholder(); } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java index 967441a93..2f3be06f6 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSlideMaster.java @@ -32,7 +32,6 @@ import org.apache.xmlbeans.XmlException; import org.openxmlformats.schemas.drawingml.x2006.main.CTColorMapping; import org.openxmlformats.schemas.drawingml.x2006.main.CTTextListStyle; import org.openxmlformats.schemas.presentationml.x2006.main.CTBackground; -import org.openxmlformats.schemas.presentationml.x2006.main.CTPlaceholder; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMaster; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMasterTextStyles; import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; @@ -43,7 +42,6 @@ import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; * Within a slide master slide are contained all elements * that describe the objects and their corresponding formatting * for within a presentation slide. -*

*

* Within a slide master slide are two main elements. * The cSld element specifies the common slide elements such as shapes and @@ -52,21 +50,12 @@ import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; * within a slide master slide specify other properties for within a presentation slide * such as color information, headers and footers, as well as timing and * transition information for all corresponding presentation slides. -*

- * - * @author Yegor Kozlov */ @Beta public class XSLFSlideMaster extends XSLFSheet implements MasterSheet { private CTSlideMaster _slide; private Map _layouts; - private XSLFTheme _theme; - - XSLFSlideMaster() { - super(); - _slide = CTSlideMaster.Factory.newInstance(); - } /** * @since POI 3.14-Beta1 @@ -142,23 +131,9 @@ import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; } - @Override - public XSLFTheme getTheme(){ - if(_theme == null){ - for (POIXMLDocumentPart p : getRelations()) { - if (p instanceof XSLFTheme){ - _theme = (XSLFTheme)p; - CTColorMapping cmap = _slide.getClrMap(); - if(cmap != null){ - _theme.initColorMap(cmap); - } - break; - } - } - } - return _theme; - } + + @SuppressWarnings(value = "unused") protected CTTextListStyle getTextProperties(Placeholder textType) { CTTextListStyle props; CTSlideMasterTextStyles txStyles = getXmlObject().getTxStyles(); @@ -183,15 +158,8 @@ import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; * */ @Override - protected boolean canDraw(XSLFShape shape){ - if(shape instanceof XSLFSimpleShape){ - XSLFSimpleShape txt = (XSLFSimpleShape)shape; - CTPlaceholder ph = txt.getCTPlaceholder(); - if(ph != null) { - return false; - } - } - return true; + protected boolean canDraw(XSLFShape shape) { + return !(shape instanceof XSLFSimpleShape) || !shape.isPlaceholder(); } @Override @@ -203,4 +171,14 @@ import org.openxmlformats.schemas.presentationml.x2006.main.SldMasterDocument; return null; } } + + @Override + boolean isSupportTheme() { + return true; + } + + @Override + CTColorMapping getColorMapping() { + return _slide.getClrMap(); + } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java index a51bc79f3..9d2f71470 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextParagraph.java @@ -20,6 +20,9 @@ import java.awt.Color; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; import org.apache.poi.sl.draw.DrawPaint; import org.apache.poi.sl.usermodel.AutoNumberingScheme; @@ -49,6 +52,12 @@ public class XSLFTextParagraph implements TextParagraph _runs; private final XSLFTextShape _shape; + @FunctionalInterface + private interface Procedure { + void accept(); + } + + XSLFTextParagraph(CTTextParagraph p, XSLFTextShape shape){ _p = p; _runs = new ArrayList<>(); @@ -79,14 +88,6 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ public boolean fetch(CTTextParagraphProperties props){ @@ -214,6 +217,7 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ public boolean fetch(CTTextParagraphProperties props){ @@ -237,6 +242,7 @@ public class XSLFTextParagraph implements TextParagraphnull value means to use the text font color. */ + @SuppressWarnings("WeakerAccess") public PaintStyle getBulletFontColor(){ final XSLFTheme theme = getParentShape().getSheet().getTheme(); ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ @@ -265,6 +272,7 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ public boolean fetch(CTTextParagraphProperties props){ @@ -326,6 +336,7 @@ public class XSLFTextParagraph implements TextParagraph */ + @SuppressWarnings("WeakerAccess") public void setBulletFontSize(double bulletSize){ CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); @@ -343,6 +354,7 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { public boolean fetch(CTTextParagraphProperties props) { @@ -363,6 +375,7 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()) { public boolean fetch(CTTextParagraphProperties props) { @@ -487,10 +500,11 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetTabLst()){ + if (props.isSetTabLst()) { CTTextTabStopList tabStops = props.getTabLst(); if(idx < tabStops.sizeOfTabArray() ) { CTTextTabStop ts = tabStops.getTabArray(idx); @@ -506,6 +520,7 @@ public class XSLFTextParagraph implements TextParagraph= 0) { - (spc.isSetSpcPct() ? spc.getSpcPct() : spc.addNewSpcPct()).setVal((int)(lineSpacing*1000)); - if (spc.isSetSpcPts()) spc.unsetSpcPts(); - } else { - (spc.isSetSpcPts() ? spc.getSpcPts() : spc.addNewSpcPts()).setVal((int)(-lineSpacing*100)); - if (spc.isSetSpcPct()) spc.unsetSpcPct(); - } - } + setSpacing(lineSpacing, props -> props::getLnSpc, props -> props::addNewLnSpc, props -> props::unsetLnSpc); } @Override public Double getLineSpacing(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetLnSpc()){ - CTTextSpacing spc = props.getLnSpc(); - - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - - Double lnSpc = fetcher.getValue(); + final Double lnSpc = getSpacing(props -> props::getLnSpc); if (lnSpc != null && lnSpc > 0) { // check if the percentage value is scaled - CTTextNormalAutofit normAutofit = getParentShape().getTextBodyPr().getNormAutofit(); - if(normAutofit != null) { - double scale = 1 - (double)normAutofit.getLnSpcReduction() / 100000; - lnSpc *= scale; + final CTTextNormalAutofit normAutofit = getParentShape().getTextBodyPr().getNormAutofit(); + if (normAutofit != null) { + final double scale = 1 - (double)normAutofit.getLnSpcReduction() / 100000; + return lnSpc * scale; } } @@ -561,84 +549,82 @@ public class XSLFTextParagraph implements TextParagraph= 0) { - spc.addNewSpcPct().setVal((int)(spaceBefore*1000)); - } else { - spc.addNewSpcPts().setVal((int)(-spaceBefore*100)); - } - pr.setSpcBef(spc); + setSpacing(spaceBefore, props -> props::getSpcBef, props -> props::addNewSpcBef, props -> props::unsetSpcBef); } @Override public Double getSpaceBefore(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetSpcBef()){ - CTTextSpacing spc = props.getSpcBef(); - - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); - return true; - } - return false; - } - }; - fetchParagraphProperty(fetcher); - - return fetcher.getValue(); + return getSpacing(props -> props::getSpcBef); } @Override public void setSpaceAfter(Double spaceAfter){ - if (spaceAfter == null && !_p.isSetPPr()) { + setSpacing(spaceAfter, props -> props::getSpcAft, props -> props::addNewSpcAft, props -> props::unsetSpcAft); + } + + @Override + public Double getSpaceAfter() { + return getSpacing(props -> props::getSpcAft); + } + + private void setSpacing(final Double space, + final Function> getSpc, + final Function> addSpc, + final Function unsetSpc + ) { + final CTTextParagraphProperties pPr = (space == null || _p.isSetPPr()) ? _p.getPPr() : _p.addNewPPr(); + if (pPr == null) { return; } - // unset the space before on null input - if (spaceAfter == null) { - if(_p.getPPr().isSetSpcAft()) { - _p.getPPr().unsetSpcAft(); + CTTextSpacing spc = getSpc.apply(pPr).get(); + + if (space == null) { + if (spc != null) { + // unset the space before on null input + unsetSpc.apply(pPr).accept(); } return; } - CTTextParagraphProperties pr = _p.isSetPPr() ? _p.getPPr() : _p.addNewPPr(); - CTTextSpacing spc = CTTextSpacing.Factory.newInstance(); - - if(spaceAfter >= 0) { - spc.addNewSpcPct().setVal((int)(spaceAfter*1000)); - } else { - spc.addNewSpcPts().setVal((int)(-spaceAfter*100)); + if (spc == null) { + spc = addSpc.apply(pPr).get(); + } + + if (space >= 0) { + if (spc.isSetSpcPts()) { + spc.unsetSpcPts(); + } + final CTTextSpacingPercent pct = spc.isSetSpcPct() ? spc.getSpcPct() : spc.addNewSpcPct(); + pct.setVal((int)(space*1000)); + } else { + if (spc.isSetSpcPct()) { + spc.unsetSpcPct(); + } + final CTTextSpacingPoint pts = spc.isSetSpcPts() ? spc.getSpcPts() : spc.addNewSpcPts(); + pts.setVal((int)(-space*100)); } - pr.setSpcAft(spc); } - @Override - public Double getSpaceAfter(){ - ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ - public boolean fetch(CTTextParagraphProperties props){ - if(props.isSetSpcAft()){ - CTTextSpacing spc = props.getSpcAft(); + private Double getSpacing(final Function> getSpc) { + final ParagraphPropertyFetcher fetcher = new ParagraphPropertyFetcher(getIndentLevel()){ + public boolean fetch(final CTTextParagraphProperties props){ + final CTTextSpacing spc = getSpc.apply(props).get(); - if(spc.isSetSpcPct()) setValue( spc.getSpcPct().getVal()*0.001 ); - else if (spc.isSetSpcPts()) setValue( -spc.getSpcPts().getVal()*0.01 ); + if (spc == null) { + return false; + } + + if (spc.isSetSpcPct()) { + setValue( spc.getSpcPct().getVal()*0.001 ); return true; } + + if (spc.isSetSpcPts()) { + setValue( -spc.getSpcPts().getVal()*0.01 ); + return true; + } + return false; } }; @@ -713,6 +699,7 @@ public class XSLFTextParagraph implements TextParagraph boolean fetchParagraphProperty(ParagraphPropertyFetcher visitor){ - boolean ok = false; + private void fetchParagraphProperty(final ParagraphPropertyFetcher visitor){ final XSLFTextShape shape = getParentShape(); final XSLFSheet sheet = shape.getSheet(); if (!(sheet instanceof XSLFSlideMaster)) { - if(_p.isSetPPr()) ok = visitor.fetch(_p.getPPr()); - if (ok) return true; - - ok = shape.fetchShapeProperty(visitor); - if (ok) return true; - - - CTPlaceholder ph = shape.getCTPlaceholder(); - if(ph == null){ - // if it is a plain text box then take defaults from presentation.xml - @SuppressWarnings("resource") - XMLSlideShow ppt = sheet.getSlideShow(); - CTTextParagraphProperties themeProps = ppt.getDefaultParagraphStyle(getIndentLevel()); - if (themeProps != null) ok = visitor.fetch(themeProps); + if (_p.isSetPPr() && visitor.fetch(_p.getPPr())) { + return; + } + + if (shape.fetchShapeProperty(visitor)) { + return; + } + + if (fetchThemeProperty(visitor)) { + return; } - if (ok) return true; } - // defaults for placeholders are defined in the slide master - CTTextParagraphProperties defaultProps = getDefaultMasterStyle(); - // TODO: determine master shape - if(defaultProps != null) ok = visitor.fetch(defaultProps); - if (ok) return true; + fetchMasterProperty(visitor); + } - return false; + boolean fetchMasterProperty(final ParagraphPropertyFetcher visitor) { + // defaults for placeholders are defined in the slide master + final CTTextParagraphProperties defaultProps = getDefaultMasterStyle(); + // TODO: determine master shape + return defaultProps != null && visitor.fetch(defaultProps); + } + + boolean fetchThemeProperty(final ParagraphPropertyFetcher visitor) { + final XSLFTextShape shape = getParentShape(); + + if (shape.isPlaceholder()) { + return false; + } + + // if it is a plain text box then take defaults from presentation.xml + @SuppressWarnings("resource") + final XMLSlideShow ppt = shape.getSheet().getSlideShow(); + final CTTextParagraphProperties themeProps = ppt.getDefaultParagraphStyle(getIndentLevel()); + return themeProps != null && visitor.fetch(themeProps); } void copy(XSLFTextParagraph other){ @@ -873,40 +869,40 @@ public class XSLFTextParagraph implements TextParagraph fetcher = new CharacterPropertyFetcher(_p.getIndentLevel()){ @@ -255,6 +222,7 @@ public class XSLFTextRun implements TextRun { * * @param spc character spacing in points. */ + @SuppressWarnings("WeakerAccess") public void setCharacterSpacing(double spc){ CTTextCharacterProperties rPr = getRPr(true); if(spc == 0.0) { @@ -357,9 +325,8 @@ public class XSLFTextRun implements TextRun { * The size is specified using a percentage. * Positive values indicate superscript, negative values indicate subscript. *

- * - * @param baselineOffset */ + @SuppressWarnings("WeakerAccess") public void setBaselineOffset(double baselineOffset){ getRPr(true).setBaseline((int) baselineOffset * 1000); } @@ -370,6 +337,7 @@ public class XSLFTextRun implements TextRun { * * @see #setBaselineOffset(double) */ + @SuppressWarnings("WeakerAccess") public void setSuperscript(boolean flag){ setBaselineOffset(flag ? 30. : 0.); } @@ -380,6 +348,7 @@ public class XSLFTextRun implements TextRun { * * @see #setBaselineOffset(double) */ + @SuppressWarnings("WeakerAccess") public void setSubscript(boolean flag){ setBaselineOffset(flag ? -25.0 : 0.); } @@ -544,38 +513,23 @@ public class XSLFTextRun implements TextRun { return new XSLFHyperlink(hl, _p.getParentShape().getSheet()); } - private boolean fetchCharacterProperty(CharacterPropertyFetcher fetcher){ + private void fetchCharacterProperty(final CharacterPropertyFetcher visitor){ XSLFTextShape shape = _p.getParentShape(); - XSLFSheet sheet = shape.getSheet(); CTTextCharacterProperties rPr = getRPr(false); - if (rPr != null && fetcher.fetch(rPr)) { - return true; + if (rPr != null && visitor.fetch(rPr)) { + return; } - if (shape.fetchShapeProperty(fetcher)) { - return true; + if (shape.fetchShapeProperty(visitor)) { + return; } - CTPlaceholder ph = shape.getCTPlaceholder(); - if (ph == null){ - // if it is a plain text box then take defaults from presentation.xml - @SuppressWarnings("resource") - XMLSlideShow ppt = sheet.getSlideShow(); - // TODO: determine master shape - CTTextParagraphProperties themeProps = ppt.getDefaultParagraphStyle(_p.getIndentLevel()); - if (themeProps != null && fetcher.fetch(themeProps)) { - return true; - } + if (_p.fetchThemeProperty(visitor)) { + return; } - // TODO: determine master shape - CTTextParagraphProperties defaultProps = _p.getDefaultMasterStyle(); - if(defaultProps != null && fetcher.fetch(defaultProps)) { - return true; - } - - return false; + _p.fetchMasterProperty(visitor); } void copy(XSLFTextRun r){ @@ -630,14 +584,14 @@ public class XSLFTextRun implements TextRun { } - private class XSLFFontInfo implements FontInfo { + private final class XSLFFontInfo implements FontInfo { private final FontGroup fontGroup; private XSLFFontInfo(FontGroup fontGroup) { this.fontGroup = (fontGroup != null) ? fontGroup : FontGroup.getFontGroupFirst(getRawText()); } - public void copyFrom(FontInfo fontInfo) { + void copyFrom(FontInfo fontInfo) { CTTextFont tf = getXmlObject(true); setTypeface(fontInfo.getTypeface()); setCharset(fontInfo.getCharset()); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java index b6787a9ea..4904d0c73 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextShape.java @@ -590,13 +590,7 @@ public abstract class XSLFTextShape extends XSLFSimpleShape } public Placeholder getTextType(){ - CTPlaceholder ph = getCTPlaceholder(); - if (ph == null) { - return null; - } - - int val = ph.getType().intValue(); - return Placeholder.lookupOoxml(val); + return getPlaceholder(); } @Override diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java index 2864e3903..2d70d352f 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java @@ -19,6 +19,7 @@ package org.apache.poi.xslf.usermodel; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -51,7 +52,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { } @After - public void tearDown() throws IOException { + public void tearDown() { pack.revert(); } @@ -149,7 +150,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { assertEquals(null, xml.getCommentAuthors()); for (XSLFSlide slide : xml.getSlides()) { - assertEquals(null, slide.getComments()); + assertTrue(slide.getComments().isEmpty()); } // Try another with comments @@ -166,17 +167,18 @@ public class TestXMLSlideShow extends BaseTestSlideShow { i++; if(i == 0) { - assertNotNull(slide.getComments()); - assertEquals(1, slide.getComments().getNumberOfComments()); - assertEquals("testdoc", slide.getComments().getCommentAt(0).getText()); - assertEquals(0, slide.getComments().getCommentAt(0).getAuthorId()); + assertNotNull(slide.getCommentsPart()); + assertEquals(1, slide.getCommentsPart().getNumberOfComments()); + assertEquals("testdoc", slide.getCommentsPart().getCommentAt(0).getText()); + assertEquals(0, slide.getCommentsPart().getCommentAt(0).getAuthorId()); } else if (i == 1) { assertNotNull(slide.getComments()); - assertEquals(1, slide.getComments().getNumberOfComments()); - assertEquals("test phrase", slide.getComments().getCommentAt(0).getText()); - assertEquals(0, slide.getComments().getCommentAt(0).getAuthorId()); + assertEquals(1, slide.getCommentsPart().getNumberOfComments()); + assertEquals("test phrase", slide.getCommentsPart().getCommentAt(0).getText()); + assertEquals(0, slide.getCommentsPart().getCommentAt(0).getAuthorId()); } else { - assertEquals(null, slide.getComments()); + assertNull(slide.getCommentsPart()); + assertTrue(slide.getComments().isEmpty()); } } @@ -188,7 +190,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { return reopen((XMLSlideShow)show); } - public static XMLSlideShow reopen(XMLSlideShow show) { + private static XMLSlideShow reopen(XMLSlideShow show) { try { BufAccessBAOS bos = new BufAccessBAOS(); show.write(bos); @@ -200,7 +202,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { } private static class BufAccessBAOS extends ByteArrayOutputStream { - public byte[] getBuf() { + byte[] getBuf() { return buf; } } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextRun.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextRun.java index 9c0c1b6b5..8e77f59f2 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextRun.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextRun.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import java.awt.Color; import java.io.IOException; +import org.apache.poi.sl.draw.DrawTextParagraph; import org.junit.Test; /** @@ -65,17 +66,17 @@ public class TestXSLFTextRun { r.setFontSize(13.0); assertEquals(13.0, r.getFontSize(), 0); - assertEquals(false, r.isSuperscript()); + assertFalse(r.isSuperscript()); r.setSuperscript(true); - assertEquals(true, r.isSuperscript()); + assertTrue(r.isSuperscript()); r.setSuperscript(false); - assertEquals(false, r.isSuperscript()); + assertFalse(r.isSuperscript()); - assertEquals(false, r.isSubscript()); + assertFalse(r.isSubscript()); r.setSubscript(true); - assertEquals(true, r.isSubscript()); + assertTrue(r.isSubscript()); r.setSubscript(false); - assertEquals(false, r.isSubscript()); + assertFalse(r.isSubscript()); ppt.close(); } @@ -94,8 +95,11 @@ public class TestXSLFTextRun { try (XMLSlideShow ppt = new XMLSlideShow()) { XSLFSlide slide = ppt.createSlide(); XSLFTextShape sh = slide.createAutoShape(); - XSLFTextRun r = sh.addNewTextParagraph().addNewTextRun(); - assertEquals(unicodeSurrogates, r.getRenderableText(unicodeSurrogates)); + XSLFTextParagraph p = sh.addNewTextParagraph(); + XSLFTextRun r = p.addNewTextRun(); + r.setText(unicodeSurrogates); + + assertEquals(unicodeSurrogates, new DrawTextParagraph(p).getRenderableText(r)); } } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java index 8856a2a64..49dc0a3bf 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFTextShape.java @@ -83,7 +83,7 @@ public class TestXSLFTextShape { assertEquals("Title Slide",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.CTR_TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -113,7 +113,7 @@ public class TestXSLFTextShape { assertTrue(sameColor(Color.black, r1.getFontColor())); XSLFTextShape shape2 = (XSLFTextShape)shapes.get(1); - CTPlaceholder ph2 = shape2.getCTPlaceholder(); + CTPlaceholder ph2 = shape2.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.SUB_TITLE, ph2.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape2).getXfrm()); @@ -149,7 +149,7 @@ public class TestXSLFTextShape { assertEquals("Title and Content",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -182,7 +182,7 @@ public class TestXSLFTextShape { assertTrue(sameColor(Color.black, r1.getFontColor())); XSLFTextShape shape2 = (XSLFTextShape)shapes.get(1); - CTPlaceholder ph2 = shape2.getCTPlaceholder(); + CTPlaceholder ph2 = shape2.getPlaceholderDetails().getCTPlaceholder(false); assertFalse(ph2.isSetType()); // assertTrue(ph2.isSetIdx()); assertEquals(1, ph2.getIdx()); @@ -262,7 +262,7 @@ public class TestXSLFTextShape { assertEquals("Section Header",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -296,7 +296,7 @@ public class TestXSLFTextShape { assertFalse(r1.isUnderlined()); XSLFTextShape shape2 = (XSLFTextShape)shapes.get(1); - CTPlaceholder ph2 = shape2.getCTPlaceholder(); + CTPlaceholder ph2 = shape2.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.BODY, ph2.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape2).getXfrm()); @@ -333,7 +333,7 @@ public class TestXSLFTextShape { assertEquals("Two Content",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -367,7 +367,7 @@ public class TestXSLFTextShape { assertTrue(sameColor(Color.black, r1.getFontColor())); XSLFTextShape shape2 = (XSLFTextShape)shapes.get(1); - CTPlaceholder ph2 = shape2.getCTPlaceholder(); + CTPlaceholder ph2 = shape2.getPlaceholderDetails().getCTPlaceholder(false); assertFalse(ph2.isSetType()); assertTrue(ph2.isSetIdx()); assertEquals(1, ph2.getIdx()); // @@ -448,7 +448,7 @@ public class TestXSLFTextShape { assertEquals("Blank",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -516,7 +516,7 @@ public class TestXSLFTextShape { assertEquals("Content with Caption",layout.getName()); XSLFTextShape shape1 = (XSLFTextShape)shapes.get(0); - CTPlaceholder ph1 = shape1.getCTPlaceholder(); + CTPlaceholder ph1 = shape1.getPlaceholderDetails().getCTPlaceholder(false); assertEquals(STPlaceholderType.TITLE, ph1.getType()); // anchor is not defined in the shape assertNull(getSpPr(shape1).getXfrm()); @@ -549,7 +549,7 @@ public class TestXSLFTextShape { assertTrue(r1.isBold()); XSLFTextShape shape2 = (XSLFTextShape)shapes.get(1); - CTPlaceholder ph2 = shape2.getCTPlaceholder(); + CTPlaceholder ph2 = shape2.getPlaceholderDetails().getCTPlaceholder(false); assertFalse(ph2.isSetType()); assertTrue(ph2.isSetIdx()); assertEquals(1, ph2.getIdx()); diff --git a/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java b/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java index c34080874..156747eac 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java +++ b/src/scratchpad/src/org/apache/poi/hslf/extractor/PowerPointExtractor.java @@ -20,47 +20,36 @@ package org.apache.poi.hslf.extractor; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import org.apache.poi.POIOLE2TextExtractor; -import org.apache.poi.hslf.model.Comment; -import org.apache.poi.hslf.model.HSLFMetroShape; -import org.apache.poi.hslf.model.HeadersFooters; -import org.apache.poi.hslf.usermodel.HSLFMasterSheet; -import org.apache.poi.hslf.usermodel.HSLFNotes; +import org.apache.poi.hslf.usermodel.HSLFObjectShape; import org.apache.poi.hslf.usermodel.HSLFShape; -import org.apache.poi.hslf.usermodel.HSLFSlide; -import org.apache.poi.hslf.usermodel.HSLFSlideMaster; import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.hslf.usermodel.HSLFSlideShowImpl; -import org.apache.poi.hslf.usermodel.HSLFTable; -import org.apache.poi.hslf.usermodel.HSLFTableCell; import org.apache.poi.hslf.usermodel.HSLFTextParagraph; -import org.apache.poi.hslf.usermodel.HSLFTextShape; -import org.apache.poi.hslf.usermodel.HSLFObjectShape; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; import org.apache.poi.poifs.filesystem.POIFSFileSystem; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; +import org.apache.poi.sl.extractor.SlideShowExtractor; +import org.apache.poi.sl.usermodel.SlideShowFactory; /** * This class can be used to extract text from a PowerPoint file. Can optionally * also get the notes from one. + * + * @deprecated in POI 4.0.0, use {@link SlideShowExtractor} instead */ +@SuppressWarnings("WeakerAccess") +@Deprecated public final class PowerPointExtractor extends POIOLE2TextExtractor { - private static final POILogger LOG = POILogFactory.getLogger(PowerPointExtractor.class); - - private final HSLFSlideShow _show; - private final List _slides; + private final SlideShowExtractor delegate; - private boolean _slidesByDefault = true; - private boolean _notesByDefault; - private boolean _commentsByDefault; - private boolean _masterByDefault; + private boolean slidesByDefault = true; + private boolean notesByDefault; + private boolean commentsByDefault; + private boolean masterByDefault; /** * Basic extractor. Returns all the text, and optionally all the notes @@ -92,13 +81,19 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { ppe.close(); } + public PowerPointExtractor(final HSLFSlideShow slideShow) { + super(slideShow.getSlideShowImpl()); + setFilesystem(slideShow); + delegate = new SlideShowExtractor<>(slideShow); + } + /** * Creates a PowerPointExtractor, from a file * * @param fileName The name of the file to extract from */ public PowerPointExtractor(String fileName) throws IOException { - this(new NPOIFSFileSystem(new File(fileName))); + this((HSLFSlideShow)SlideShowFactory.create(new File(fileName), Biff8EncryptionKey.getCurrentUserPassword(), true)); } /** @@ -107,7 +102,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param iStream The input stream containing the PowerPoint document */ public PowerPointExtractor(InputStream iStream) throws IOException { - this(new POIFSFileSystem(iStream)); + this((HSLFSlideShow)SlideShowFactory.create(iStream, Biff8EncryptionKey.getCurrentUserPassword())); } /** @@ -116,7 +111,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param fs the POIFSFileSystem containing the PowerPoint document */ public PowerPointExtractor(POIFSFileSystem fs) throws IOException { - this(fs.getRoot()); + this((HSLFSlideShow)SlideShowFactory.create(fs, Biff8EncryptionKey.getCurrentUserPassword())); } /** @@ -125,8 +120,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param fs the NPOIFSFileSystem containing the PowerPoint document */ public PowerPointExtractor(NPOIFSFileSystem fs) throws IOException { - this(fs.getRoot()); - setFilesystem(fs); + this((HSLFSlideShow)SlideShowFactory.create(fs, Biff8EncryptionKey.getCurrentUserPassword())); } /** @@ -136,7 +130,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param dir the POIFS Directory containing the PowerPoint document */ public PowerPointExtractor(DirectoryNode dir) throws IOException { - this(new HSLFSlideShowImpl(dir)); + this(new HSLFSlideShow(dir)); } /** @@ -145,37 +139,39 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param ss the HSLFSlideShow to extract text from */ public PowerPointExtractor(HSLFSlideShowImpl ss) { - super(ss); - _show = new HSLFSlideShow(ss); - _slides = _show.getSlides(); + this(new HSLFSlideShow(ss)); } /** * Should a call to getText() return slide text? Default is yes */ - public void setSlidesByDefault(boolean slidesByDefault) { - this._slidesByDefault = slidesByDefault; + public void setSlidesByDefault(final boolean slidesByDefault) { + this.slidesByDefault = slidesByDefault; + delegate.setSlidesByDefault(slidesByDefault); } /** * Should a call to getText() return notes text? Default is no */ - public void setNotesByDefault(boolean notesByDefault) { - this._notesByDefault = notesByDefault; + public void setNotesByDefault(final boolean notesByDefault) { + this.notesByDefault = notesByDefault; + delegate.setNotesByDefault(notesByDefault); } /** * Should a call to getText() return comments text? Default is no */ - public void setCommentsByDefault(boolean commentsByDefault) { - this._commentsByDefault = commentsByDefault; + public void setCommentsByDefault(final boolean commentsByDefault) { + this.commentsByDefault = commentsByDefault; + delegate.setCommentsByDefault(commentsByDefault); } /** * Should a call to getText() return text from master? Default is no */ - public void setMasterByDefault(boolean masterByDefault) { - this._masterByDefault = masterByDefault; + public void setMasterByDefault(final boolean masterByDefault) { + this.masterByDefault = masterByDefault; + delegate.setMasterByDefault(masterByDefault); } /** @@ -184,28 +180,7 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { */ @Override public String getText() { - return getText(_slidesByDefault, _notesByDefault, _commentsByDefault, _masterByDefault); - } - - /** - * Fetches all the notes text from the slideshow, but not the slide text - */ - public String getNotes() { - return getText(false, true); - } - - public List getOLEShapes() { - List list = new ArrayList<>(); - - for (HSLFSlide slide : _slides) { - for (HSLFShape shape : slide.getShapes()) { - if (shape instanceof HSLFObjectShape) { - list.add((HSLFObjectShape) shape); - } - } - } - - return list; + return delegate.getText(); } /** @@ -217,159 +192,33 @@ public final class PowerPointExtractor extends POIOLE2TextExtractor { * @param getNoteText fetch note text */ public String getText(boolean getSlideText, boolean getNoteText) { - return getText(getSlideText, getNoteText, _commentsByDefault, _masterByDefault); + return getText(getSlideText,getNoteText,commentsByDefault,masterByDefault); } public String getText(boolean getSlideText, boolean getNoteText, boolean getCommentText, boolean getMasterText) { - StringBuffer ret = new StringBuffer(); - - if (getSlideText) { - if (getMasterText) { - for (HSLFSlideMaster master : _show.getSlideMasters()) { - for(HSLFShape sh : master.getShapes()){ - if(sh instanceof HSLFTextShape){ - HSLFTextShape hsh = (HSLFTextShape)sh; - final String text = hsh.getText(); - if (text == null || text.isEmpty() || "*".equals(text)) { - continue; - } - - if (HSLFMasterSheet.isPlaceholder(sh)) { - // check for metro shape of complex placeholder - boolean isMetro = new HSLFMetroShape(sh).hasMetroBlob(); - - if (!isMetro) { - // don't bother about boiler plate text on master sheets - LOG.log(POILogger.INFO, "Ignoring boiler plate (placeholder) text on slide master:", text); - continue; - } - } - - ret.append(text); - if (!text.endsWith("\n")) { - ret.append("\n"); - } - } - } - } - } - - for (HSLFSlide slide : _slides) { - String headerText = ""; - String footerText = ""; - HeadersFooters hf = slide.getHeadersFooters(); - if (hf != null) { - if (hf.isHeaderVisible()) { - headerText = safeLine(hf.getHeaderText()); - } - if (hf.isFooterVisible()) { - footerText = safeLine(hf.getFooterText()); - } - } - - // Slide header, if set - ret.append(headerText); - - // Slide text - textRunsToText(ret, slide.getTextParagraphs()); - - // Table text - for (HSLFShape shape : slide.getShapes()){ - if (shape instanceof HSLFTable){ - extractTableText(ret, (HSLFTable)shape); - } - } - // Slide footer, if set - ret.append(footerText); - - // Comments, if requested and present - if (getCommentText) { - for (Comment comment : slide.getComments()) { - ret.append(comment.getAuthor() + " - " + comment.getText() + "\n"); - } - } - } - if (getNoteText) { - ret.append('\n'); - } + delegate.setSlidesByDefault(getSlideText); + delegate.setNotesByDefault(getNoteText); + delegate.setCommentsByDefault(getCommentText); + delegate.setMasterByDefault(getMasterText); + try { + return delegate.getText(); + } finally { + delegate.setSlidesByDefault(slidesByDefault); + delegate.setNotesByDefault(notesByDefault); + delegate.setCommentsByDefault(commentsByDefault); + delegate.setMasterByDefault(masterByDefault); } - - if (getNoteText) { - // Not currently using _notes, as that can have the notes of - // master sheets in. Grab Slide list, then work from there, - // but ensure no duplicates - Set seenNotes = new HashSet<>(); - String headerText = ""; - String footerText = ""; - HeadersFooters hf = _show.getNotesHeadersFooters(); - if (hf != null) { - if (hf.isHeaderVisible()) { - headerText = safeLine(hf.getHeaderText()); - } - if (hf.isFooterVisible()) { - footerText = safeLine(hf.getFooterText()); - } - } - - - for (HSLFSlide slide : _slides) { - HSLFNotes notes = slide.getNotes(); - if (notes == null) { - continue; - } - Integer id = Integer.valueOf(notes._getSheetNumber()); - if (seenNotes.contains(id)) { - continue; - } - seenNotes.add(id); - - // Repeat the Notes header, if set - ret.append(headerText); - - // Notes text - textRunsToText(ret, notes.getTextParagraphs()); - - // Repeat the notes footer, if set - ret.append(footerText); - } - } - - return ret.toString(); - } - - private static String safeLine(String text) { - return (text == null) ? "" : (text+'\n'); } - private void extractTableText(StringBuffer ret, HSLFTable table) { - final int nrows = table.getNumberOfRows(); - final int ncols = table.getNumberOfColumns(); - for (int row = 0; row < nrows; row++){ - for (int col = 0; col < ncols; col++){ - HSLFTableCell cell = table.getCell(row, col); - //defensive null checks; don't know if they're necessary - if (cell != null){ - String txt = cell.getText(); - txt = (txt == null) ? "" : txt; - ret.append(txt); - if (col < ncols-1){ - ret.append('\t'); - } - } - } - ret.append('\n'); - } - } - private void textRunsToText(StringBuffer ret, List> paragraphs) { - if (paragraphs==null) { - return; - } + /** + * Fetches all the notes text from the slideshow, but not the slide text + */ + public String getNotes() { + return getText(false, true, false, false); + } - for (List lp : paragraphs) { - ret.append(HSLFTextParagraph.getText(lp)); - if (ret.length() > 0 && ret.charAt(ret.length()-1) != '\n') { - ret.append('\n'); - } - } - } + @SuppressWarnings("unchecked") + public List getOLEShapes() { + return (List)delegate.getOLEShapes(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Comment.java b/src/scratchpad/src/org/apache/poi/hslf/model/Comment.java deleted file mode 100644 index 27afa47c8..000000000 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Comment.java +++ /dev/null @@ -1,75 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -package org.apache.poi.hslf.model; - -import org.apache.poi.hslf.record.Comment2000; - -/** - * - * @author Nick Burch - */ -public final class Comment { - private Comment2000 _comment2000; - - public Comment(Comment2000 comment2000) { - _comment2000 = comment2000; - } - - protected Comment2000 getComment2000() { - return _comment2000; - } - - /** - * Get the Author of this comment - */ - public String getAuthor() { - return _comment2000.getAuthor(); - } - /** - * Set the Author of this comment - */ - public void setAuthor(String author) { - _comment2000.setAuthor(author); - } - - /** - * Get the Author's Initials of this comment - */ - public String getAuthorInitials() { - return _comment2000.getAuthorInitials(); - } - /** - * Set the Author's Initials of this comment - */ - public void setAuthorInitials(String initials) { - _comment2000.setAuthorInitials(initials); - } - - /** - * Get the text of this comment - */ - public String getText() { - return _comment2000.getText(); - } - /** - * Set the text of this comment - */ - public void setText(String text) { - _comment2000.setText(text); - } -} diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java b/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java index ab96c7dbe..64b6dc6a3 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/HeadersFooters.java @@ -275,4 +275,11 @@ public final class HeadersFooters { private void setFlag(int type, boolean flag) { _container.getHeadersFootersAtom().setFlag(type, flag); } + + /** + * @return true, if this is a ppt 2007 document and header/footer are stored as placeholder shapes + */ + public boolean isPpt2007() { + return _ppt2007; + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java index 57f0f31ed..488893cd7 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentEncryptionAtom.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Map; +import org.apache.poi.EncryptedDocumentException; import org.apache.poi.poifs.crypt.CipherAlgorithm; import org.apache.poi.poifs.crypt.EncryptionInfo; import org.apache.poi.poifs.crypt.EncryptionMode; @@ -46,15 +47,17 @@ public final class DocumentEncryptionAtom extends PositionDependentRecordAtom { /** * For the Document Encryption Atom */ - protected DocumentEncryptionAtom(byte[] source, int start, int len) throws IOException { + protected DocumentEncryptionAtom(byte[] source, int start, int len) { // Get the header _header = new byte[8]; System.arraycopy(source,start,_header,0,8); ByteArrayInputStream bis = new ByteArrayInputStream(source, start+8, len-8); - LittleEndianInputStream leis = new LittleEndianInputStream(bis); - ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI); - leis.close(); + try (LittleEndianInputStream leis = new LittleEndianInputStream(bis)) { + ei = new EncryptionInfo(leis, EncryptionMode.cryptoAPI); + } catch (IOException e) { + throw new EncryptedDocumentException(e); + } } public DocumentEncryptionAtom() { diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/HSLFEscherClientDataRecord.java b/src/scratchpad/src/org/apache/poi/hslf/record/HSLFEscherClientDataRecord.java index 6bcfd2a9a..f6094950c 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/HSLFEscherClientDataRecord.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/HSLFEscherClientDataRecord.java @@ -20,7 +20,6 @@ package org.apache.poi.hslf.record; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import org.apache.poi.ddf.EscherClientDataRecord; @@ -49,12 +48,7 @@ public class HSLFEscherClientDataRecord extends EscherClientDataRecord { } public void removeChild(Class childClass) { - Iterator iter = _childRecords.iterator(); - while (iter.hasNext()) { - if (childClass.isInstance(iter.next())) { - iter.remove(); - } - } + _childRecords.removeIf(childClass::isInstance); } public void addChild(Record childRecord) { @@ -109,8 +103,10 @@ public class HSLFEscherClientDataRecord extends EscherClientDataRecord { _childRecords.clear(); int offset = 0; while (offset < remainingData.length) { - Record r = Record.buildRecordAtOffset(remainingData, offset); - _childRecords.add(r); + final Record r = Record.buildRecordAtOffset(remainingData, offset); + if (r != null) { + _childRecords.add(r); + } long rlen = LittleEndian.getUInt(remainingData,offset+4); offset += 8 + rlen; } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/Record.java b/src/scratchpad/src/org/apache/poi/hslf/record/Record.java index 97f2eb7d8..bec722cb8 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/Record.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/Record.java @@ -24,6 +24,7 @@ import java.util.List; import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException; import org.apache.poi.hslf.exceptions.HSLFException; +import org.apache.poi.hslf.record.RecordTypes.RecordConstructor; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -122,15 +123,13 @@ public abstract class Record // Abort if first record is of type 0000 and length FFFF, // as that's a sign of a screwed up record - if(pos == 0 && type == 0l && rleni == 0xffff) { + if(pos == 0 && type == 0L && rleni == 0xffff) { throw new CorruptPowerPointFileException("Corrupt document - starts with record of type 0000 and length 0xFFFF"); } Record r = createRecordForType(type,b,pos,8+rleni); if(r != null) { children.add(r); - } else { - // Record was horribly corrupt } pos += 8; pos += rleni; @@ -150,43 +149,32 @@ public abstract class Record * passing in corrected lengths */ public static Record createRecordForType(long type, byte[] b, int start, int len) { - Record toReturn = null; - - // Handle case of a corrupt last record, whose claimed length - // would take us passed the end of the file - if(start + len > b.length) { - logger.log(POILogger.WARN, "Warning: Skipping record of type " + type + " at position " + start + " which claims to be longer than the file! (" + len + " vs " + (b.length-start) + ")"); - return null; - } - // We use the RecordTypes class to provide us with the right // class to use for a given type // A spot of reflection gets us the (byte[],int,int) constructor // From there, we instanciate the class // Any special record handling occurs once we have the class - Class c = null; + RecordConstructor c = RecordTypes.forTypeID((short)type).recordConstructor; + if (c == null) { + // How odd. RecordTypes normally substitutes in + // a default handler class if it has heard of the record + // type but there's no support for it. Explicitly request + // that now + c = RecordTypes.UnknownRecordPlaceholder.recordConstructor; + } + + final Record toReturn; try { - c = RecordTypes.forTypeID((short)type).handlingClass; - if(c == null) { - // How odd. RecordTypes normally substitutes in - // a default handler class if it has heard of the record - // type but there's no support for it. Explicitly request - // that now - c = RecordTypes.UnknownRecordPlaceholder.handlingClass; + toReturn = c.apply(b, start, len); + } catch(RuntimeException e) { + // Handle case of a corrupt last record, whose claimed length + // would take us passed the end of the file + if(start + len > b.length ) { + logger.log(POILogger.WARN, "Warning: Skipping record of type " + type + " at position " + start + " which claims to be longer than the file! (" + len + " vs " + (b.length-start) + ")"); + return null; } - // Grab the right constructor - java.lang.reflect.Constructor con = c.getDeclaredConstructor(new Class[] { byte[].class, Integer.TYPE, Integer.TYPE }); - // Instantiate - toReturn = con.newInstance(new Object[] { b, start, len }); - } catch(InstantiationException ie) { - throw new HSLFException("Couldn't instantiate the class for type with id " + type + " on class " + c + " : " + ie, ie); - } catch(java.lang.reflect.InvocationTargetException ite) { - throw new HSLFException("Couldn't instantiate the class for type with id " + type + " on class " + c + " : " + ite + "\nCause was : " + ite.getCause(), ite); - } catch(IllegalAccessException iae) { - throw new HSLFException("Couldn't access the constructor for type with id " + type + " on class " + c + " : " + iae, iae); - } catch(NoSuchMethodException nsme) { - throw new HSLFException("Couldn't access the constructor for type with id " + type + " on class " + c + " : " + nsme, nsme); + throw new HSLFException("Couldn't instantiate the class for type with id " + type + " on class " + c + " : " + e, e); } // Handling for special kinds of records follow diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java index 3fec22b1a..233d54a67 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java @@ -29,141 +29,141 @@ import java.util.Map; */ public enum RecordTypes { Unknown(0,null), - UnknownRecordPlaceholder(-1, UnknownRecordPlaceholder.class), - Document(1000,Document.class), - DocumentAtom(1001,DocumentAtom.class), + UnknownRecordPlaceholder(-1, UnknownRecordPlaceholder::new), + Document(1000,Document::new), + DocumentAtom(1001,DocumentAtom::new), EndDocument(1002,null), - Slide(1006,Slide.class), - SlideAtom(1007,SlideAtom.class), - Notes(1008,Notes.class), - NotesAtom(1009,NotesAtom.class), - Environment(1010,Environment.class), - SlidePersistAtom(1011,SlidePersistAtom.class), + Slide(1006,Slide::new), + SlideAtom(1007,SlideAtom::new), + Notes(1008,Notes::new), + NotesAtom(1009,NotesAtom::new), + Environment(1010,Environment::new), + SlidePersistAtom(1011,SlidePersistAtom::new), SSlideLayoutAtom(1015,null), - MainMaster(1016,MainMaster.class), - SSSlideInfoAtom(1017,SSSlideInfoAtom.class), + MainMaster(1016,MainMaster::new), + SSSlideInfoAtom(1017,SSSlideInfoAtom::new), SlideViewInfo(1018,null), GuideAtom(1019,null), ViewInfo(1020,null), ViewInfoAtom(1021,null), SlideViewInfoAtom(1022,null), - VBAInfo(1023,VBAInfoContainer.class), - VBAInfoAtom(1024,VBAInfoAtom.class), + VBAInfo(1023,VBAInfoContainer::new), + VBAInfoAtom(1024,VBAInfoAtom::new), SSDocInfoAtom(1025,null), Summary(1026,null), DocRoutingSlip(1030,null), OutlineViewInfo(1031,null), SorterViewInfo(1032,null), - ExObjList(1033,ExObjList.class), - ExObjListAtom(1034,ExObjListAtom.class), - PPDrawingGroup(1035,PPDrawingGroup.class), - PPDrawing(1036,PPDrawing.class), + ExObjList(1033,ExObjList::new), + ExObjListAtom(1034,ExObjListAtom::new), + PPDrawingGroup(1035,PPDrawingGroup::new), + PPDrawing(1036,PPDrawing::new), NamedShows(1040,null), NamedShow(1041,null), NamedShowSlides(1042,null), SheetProperties(1044,null), RoundTripCustomTableStyles12Atom(1064,null), - List(2000,DocInfoListContainer.class), - FontCollection(2005,FontCollection.class), + List(2000,DocInfoListContainer::new), + FontCollection(2005,FontCollection::new), BookmarkCollection(2019,null), - SoundCollection(2020,SoundCollection.class), + SoundCollection(2020,SoundCollection::new), SoundCollAtom(2021,null), - Sound(2022,Sound.class), - SoundData(2023,SoundData.class), + Sound(2022,Sound::new), + SoundData(2023,SoundData::new), BookmarkSeedAtom(2025,null), - ColorSchemeAtom(2032,ColorSchemeAtom.class), - ExObjRefAtom(3009,ExObjRefAtom.class), - OEPlaceholderAtom(3011,OEPlaceholderAtom.class), + ColorSchemeAtom(2032,ColorSchemeAtom::new), + ExObjRefAtom(3009,ExObjRefAtom::new), + OEPlaceholderAtom(3011,OEPlaceholderAtom::new), GPopublicintAtom(3024,null), GRatioAtom(3031,null), - OutlineTextRefAtom(3998,OutlineTextRefAtom.class), - TextHeaderAtom(3999,TextHeaderAtom.class), - TextCharsAtom(4000,TextCharsAtom.class), - StyleTextPropAtom(4001, StyleTextPropAtom.class),//0x0fa1 RT_StyleTextPropAtom - MasterTextPropAtom(4002, MasterTextPropAtom.class), - TxMasterStyleAtom(4003,TxMasterStyleAtom.class), + OutlineTextRefAtom(3998,OutlineTextRefAtom::new), + TextHeaderAtom(3999,TextHeaderAtom::new), + TextCharsAtom(4000,TextCharsAtom::new), + StyleTextPropAtom(4001, StyleTextPropAtom::new),//0x0fa1 RT_StyleTextPropAtom + MasterTextPropAtom(4002, MasterTextPropAtom::new), + TxMasterStyleAtom(4003,TxMasterStyleAtom::new), TxCFStyleAtom(4004,null), TxPFStyleAtom(4005,null), - TextRulerAtom(4006,TextRulerAtom.class), + TextRulerAtom(4006,TextRulerAtom::new), TextBookmarkAtom(4007,null), - TextBytesAtom(4008,TextBytesAtom.class), + TextBytesAtom(4008,TextBytesAtom::new), TxSIStyleAtom(4009,null), - TextSpecInfoAtom(4010, TextSpecInfoAtom.class), + TextSpecInfoAtom(4010, TextSpecInfoAtom::new), DefaultRulerAtom(4011,null), - StyleTextProp9Atom(4012, StyleTextProp9Atom.class), //0x0FAC RT_StyleTextProp9Atom - FontEntityAtom(4023,FontEntityAtom.class), + StyleTextProp9Atom(4012, StyleTextProp9Atom::new), //0x0FAC RT_StyleTextProp9Atom + FontEntityAtom(4023,FontEntityAtom::new), FontEmbeddedData(4024,null), - CString(4026,CString.class), + CString(4026,CString::new), MetaFile(4033,null), - ExOleObjAtom(4035,ExOleObjAtom.class), + ExOleObjAtom(4035,ExOleObjAtom::new), SrKinsoku(4040,null), - HandOut(4041,DummyPositionSensitiveRecordWithChildren.class), - ExEmbed(4044,ExEmbed.class), - ExEmbedAtom(4045,ExEmbedAtom.class), + HandOut(4041,DummyPositionSensitiveRecordWithChildren::new), + ExEmbed(4044,ExEmbed::new), + ExEmbedAtom(4045,ExEmbedAtom::new), ExLink(4046,null), BookmarkEntityAtom(4048,null), ExLinkAtom(4049,null), SrKinsokuAtom(4050,null), - ExHyperlinkAtom(4051,ExHyperlinkAtom.class), - ExHyperlink(4055,ExHyperlink.class), + ExHyperlinkAtom(4051,ExHyperlinkAtom::new), + ExHyperlink(4055,ExHyperlink::new), SlideNumberMCAtom(4056,null), - HeadersFooters(4057,HeadersFootersContainer.class), - HeadersFootersAtom(4058,HeadersFootersAtom.class), - TxInteractiveInfoAtom(4063,TxInteractiveInfoAtom.class), + HeadersFooters(4057,HeadersFootersContainer::new), + HeadersFootersAtom(4058,HeadersFootersAtom::new), + TxInteractiveInfoAtom(4063,TxInteractiveInfoAtom::new), CharFormatAtom(4066,null), ParaFormatAtom(4067,null), RecolorInfoAtom(4071,null), ExQuickTimeMovie(4074,null), ExQuickTimeMovieData(4075,null), - ExControl(4078,ExControl.class), - SlideListWithText(4080,SlideListWithText.class), - InteractiveInfo(4082,InteractiveInfo.class), - InteractiveInfoAtom(4083,InteractiveInfoAtom.class), - UserEditAtom(4085,UserEditAtom.class), + ExControl(4078,ExControl::new), + SlideListWithText(4080,SlideListWithText::new), + InteractiveInfo(4082,InteractiveInfo::new), + InteractiveInfoAtom(4083,InteractiveInfoAtom::new), + UserEditAtom(4085,UserEditAtom::new), CurrentUserAtom(4086,null), DateTimeMCAtom(4087,null), GenericDateMCAtom(4088,null), FooterMCAtom(4090,null), - ExControlAtom(4091,ExControlAtom.class), - ExMediaAtom(4100,ExMediaAtom.class), - ExVideoContainer(4101,ExVideoContainer.class), - ExAviMovie(4102,ExAviMovie.class), - ExMCIMovie(4103,ExMCIMovie.class), + ExControlAtom(4091,ExControlAtom::new), + ExMediaAtom(4100,ExMediaAtom::new), + ExVideoContainer(4101,ExVideoContainer::new), + ExAviMovie(4102,ExAviMovie::new), + ExMCIMovie(4103,ExMCIMovie::new), ExMIDIAudio(4109,null), ExCDAudio(4110,null), ExWAVAudioEmbedded(4111,null), ExWAVAudioLink(4112,null), - ExOleObjStg(4113,ExOleObjStg.class), + ExOleObjStg(4113,ExOleObjStg::new), ExCDAudioAtom(4114,null), ExWAVAudioEmbeddedAtom(4115,null), - AnimationInfo(4116,AnimationInfo.class), - AnimationInfoAtom(4081,AnimationInfoAtom.class), + AnimationInfo(4116,AnimationInfo::new), + AnimationInfoAtom(4081,AnimationInfoAtom::new), RTFDateTimeMCAtom(4117,null), - ProgTags(5000,DummyPositionSensitiveRecordWithChildren.class), + ProgTags(5000,DummyPositionSensitiveRecordWithChildren::new), ProgStringTag(5001,null), - ProgBinaryTag(5002,DummyPositionSensitiveRecordWithChildren.class), - BinaryTagData(5003, BinaryTagDataBlob.class),//0x138b RT_BinaryTagDataBlob + ProgBinaryTag(5002,DummyPositionSensitiveRecordWithChildren::new), + BinaryTagData(5003, BinaryTagDataBlob::new),//0x138b RT_BinaryTagDataBlob PrpublicintOptions(6000,null), - PersistPtrFullBlock(6001,PersistPtrHolder.class), - PersistPtrIncrementalBlock(6002,PersistPtrHolder.class), + PersistPtrFullBlock(6001,PersistPtrHolder::new), + PersistPtrIncrementalBlock(6002,PersistPtrHolder::new), GScalingAtom(10001,null), GRColorAtom(10002,null), // Records ~12000 seem to be related to the Comments used in PPT 2000/XP // (Comments in PPT97 are normal Escher text boxes) - Comment2000(12000,Comment2000.class), - Comment2000Atom(12001,Comment2000Atom.class), + Comment2000(12000,Comment2000::new), + Comment2000Atom(12001,Comment2000Atom::new), Comment2000Summary(12004,null), Comment2000SummaryAtom(12005,null), // Records ~12050 seem to be related to Document Encryption - DocumentEncryptionAtom(12052,DocumentEncryptionAtom.class), + DocumentEncryptionAtom(12052,DocumentEncryptionAtom::new), OriginalMainMasterId(1052,null), CompositeMasterId(1052,null), RoundTripContentMasterInfo12(1054,null), RoundTripShapeId12(1055,null), - RoundTripHFPlaceholder12(1056,RoundTripHFPlaceholder12.class), + RoundTripHFPlaceholder12(1056,RoundTripHFPlaceholder12::new), RoundTripContentMasterId(1058,null), RoundTripOArtTextStyles12(1059,null), RoundTripShapeCheckSumForCustomLayouts12(1062,null), @@ -207,6 +207,11 @@ public enum RecordTypes { // same as EscherTertiaryOptRecord.RECORD_ID EscherUserDefined(0xf122,null); + @FunctionalInterface + public interface RecordConstructor { + T apply(byte[] source, int start, int len); + } + private static final Map LOOKUP; static { @@ -217,85 +222,15 @@ public enum RecordTypes { } public final short typeID; - public final Class handlingClass; + public final RecordConstructor recordConstructor; - private RecordTypes(int typeID, Class handlingClass) { + RecordTypes(int typeID, RecordConstructor recordConstructor) { this.typeID = (short)typeID; - this.handlingClass = handlingClass; + this.recordConstructor = recordConstructor; } public static RecordTypes forTypeID(int typeID) { RecordTypes rt = LOOKUP.get((short)typeID); return (rt != null) ? rt : UnknownRecordPlaceholder; } - - - - /** - * Returns name of the record by its type - * - * @param type section of the record header - * @return name of the record - */ -// public static String recordName(int type) { -// String name = typeToName.get(Integer.valueOf(type)); -// return (name == null) ? ("Unknown" + type) : name; -// } - - /** - * Returns the class handling a record by its type. - * If given an un-handled PowerPoint record, will return a dummy - * placeholder class. If given an unknown PowerPoint record, or - * and Escher record, will return null. - * - * @param type section of the record header - * @return class to handle the record, or null if an unknown (eg Escher) record - */ -// public static Class recordHandlingClass(int type) { -// Class c = typeToClass.get(Integer.valueOf(type)); -// return c; -// } -// -// static { -// typeToName = new HashMap(); -// typeToClass = new HashMap>(); -// try { -// Field[] f = RecordTypes.class.getFields(); -// for (int i = 0; i < f.length; i++){ -// Object val = f[i].get(null); -// -// // Escher record, only store ID -> Name -// if (val instanceof Integer) { -// typeToName.put((Integer)val, f[i].getName()); -// } -// // PowerPoint record, store ID -> Name and ID -> Class -// if (val instanceof Type) { -// Type t = (Type)val; -// Class c = t.handlingClass; -// Integer id = Integer.valueOf(t.typeID); -// if(c == null) { c = UnknownRecordPlaceholder.class; } -// -// typeToName.put(id, f[i].getName()); -// typeToClass.put(id, c); -// } -// } -// } catch (IllegalAccessException e){ -// throw new HSLFException("Failed to initialize records types"); -// } -// } - - - /** - * Wrapper for the details of a PowerPoint or Escher record type. - * Contains both the type, and the handling class (if any), and - * offers methods to get either back out. - */ -// public static class Type { -// public final int typeID; -// public final Class handlingClass; -// public Type(int typeID, Class handlingClass) { -// this.typeID = typeID; -// this.handlingClass = handlingClass; -// } -// } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFComment.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFComment.java new file mode 100644 index 000000000..0548fca45 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFComment.java @@ -0,0 +1,104 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hslf.usermodel; + +import org.apache.poi.hslf.record.Comment2000; +import org.apache.poi.sl.usermodel.Comment; +import org.apache.poi.util.Units; + +import java.awt.geom.Point2D; +import java.util.Date; + +public final class HSLFComment implements Comment { + private Comment2000 _comment2000; + + public HSLFComment(Comment2000 comment2000) { + _comment2000 = comment2000; + } + + protected Comment2000 getComment2000() { + return _comment2000; + } + + /** + * Get the Author of this comment + */ + public String getAuthor() { + return _comment2000.getAuthor(); + } + + /** + * Set the Author of this comment + */ + public void setAuthor(String author) { + _comment2000.setAuthor(author); + } + + /** + * Get the Author's Initials of this comment + */ + public String getAuthorInitials() { + return _comment2000.getAuthorInitials(); + } + + /** + * Set the Author's Initials of this comment + */ + public void setAuthorInitials(String initials) { + _comment2000.setAuthorInitials(initials); + } + + /** + * Get the text of this comment + */ + public String getText() { + return _comment2000.getText(); + } + + /** + * Set the text of this comment + */ + public void setText(String text) { + _comment2000.setText(text); + } + + @Override + public Date getDate() { + return _comment2000.getComment2000Atom().getDate(); + } + + @Override + public void setDate(Date date) { + _comment2000.getComment2000Atom().setDate(date); + } + + @Override + public Point2D getOffset() { + final double x = Units.masterToPoints(_comment2000.getComment2000Atom().getXOffset()); + final double y = Units.masterToPoints(_comment2000.getComment2000Atom().getYOffset()); + return new Point2D.Double(x, y); + } + + @Override + public void setOffset(Point2D offset) { + final int x = Units.pointsToMaster(offset.getX()); + final int y = Units.pointsToMaster(offset.getY()); + _comment2000.getComment2000Atom().setXOffset(x); + _comment2000.getComment2000Atom().setYOffset(y); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFMasterSheet.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFMasterSheet.java index 526a3ceed..0548bf802 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFMasterSheet.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFMasterSheet.java @@ -21,6 +21,8 @@ import org.apache.poi.hslf.model.textproperties.TextPropCollection; import org.apache.poi.hslf.record.SheetContainer; import org.apache.poi.hslf.record.TextHeaderAtom; import org.apache.poi.sl.usermodel.MasterSheet; +import org.apache.poi.sl.usermodel.SimpleShape; +import org.apache.poi.util.Removal; /** * The superclass of all master sheets - Slide masters, Notes masters, etc. @@ -52,11 +54,13 @@ public abstract class HSLFMasterSheet extends HSLFSheet implements MasterSheet)shape).isPlaceholder(); } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFNotes.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFNotes.java index d922d7a89..70f15e021 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFNotes.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFNotes.java @@ -20,7 +20,10 @@ package org.apache.poi.hslf.usermodel; import java.util.ArrayList; import java.util.List; +import org.apache.poi.hslf.model.HeadersFooters; +import org.apache.poi.hslf.record.HeadersFootersContainer; import org.apache.poi.sl.usermodel.Notes; +import org.apache.poi.sl.usermodel.Placeholder; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -72,4 +75,28 @@ public final class HSLFNotes extends HSLFSheet implements Notes clRecords = getClientRecords(); - if (clRecords == null) { - return null; - } - int phSource; - HSLFSheet sheet = getSheet(); - if (sheet instanceof HSLFSlideMaster) { - phSource = 1; - } else if (sheet instanceof HSLFNotes) { - phSource = 2; - } else if (sheet instanceof MasterSheet) { - // notes master aren't yet supported ... - phSource = 3; - } else { - phSource = 0; - } - - for (Record r : clRecords) { - int phId; - if (r instanceof OEPlaceholderAtom) { - phId = ((OEPlaceholderAtom)r).getPlaceholderId(); - } else if (r instanceof RoundTripHFPlaceholder12) { - //special case for files saved in Office 2007 - phId = ((RoundTripHFPlaceholder12)r).getPlaceholderId(); - } else { - continue; - } - - switch (phSource) { - case 0: - return Placeholder.lookupNativeSlide(phId); - default: - case 1: - return Placeholder.lookupNativeSlideMaster(phId); - case 2: - return Placeholder.lookupNativeNotes(phId); - case 3: - return Placeholder.lookupNativeNotesMaster(phId); - } - } - - return null; + return getPlaceholderDetails().getPlaceholder(); } @Override public void setPlaceholder(Placeholder placeholder) { - EscherSpRecord spRecord = getEscherChild(EscherSpRecord.RECORD_ID); - int flags = spRecord.getFlags(); - if (placeholder == null) { - flags ^= EscherSpRecord.FLAG_HAVEMASTER; - } else { - flags |= EscherSpRecord.FLAG_HAVEANCHOR | EscherSpRecord.FLAG_HAVEMASTER; - } - spRecord.setFlags(flags); - - // Placeholders can't be grouped - setEscherProperty(EscherProperties.PROTECTION__LOCKAGAINSTGROUPING, (placeholder == null ? -1 : 262144)); - - HSLFEscherClientDataRecord clientData = getClientData(false); - if (placeholder == null) { - if (clientData != null) { - clientData.removeChild(OEPlaceholderAtom.class); - clientData.removeChild(RoundTripHFPlaceholder12.class); - // remove client data if the placeholder was the only child to be carried - if (clientData.getChildRecords().isEmpty()) { - getSpContainer().removeChildRecord(clientData); - } - } - return; - } - - if (clientData == null) { - clientData = getClientData(true); - } - - // OEPlaceholderAtom tells powerpoint that this shape is a placeholder - OEPlaceholderAtom oep = null; - RoundTripHFPlaceholder12 rtp = null; - for (Record r : clientData.getHSLFChildRecords()) { - if (r instanceof OEPlaceholderAtom) { - oep = (OEPlaceholderAtom)r; - break; - } - if (r instanceof RoundTripHFPlaceholder12) { - rtp = (RoundTripHFPlaceholder12)r; - break; - } - } - - /** - * Extract from MSDN: - * - * There is a special case when the placeholder does not have a position in the layout. - * This occurs when the user has moved the placeholder from its original position. - * In this case the placeholder ID is -1. - */ - byte phId; - HSLFSheet sheet = getSheet(); - // TODO: implement/switch NotesMaster - if (sheet instanceof HSLFSlideMaster) { - phId = (byte)placeholder.nativeSlideMasterId; - } else if (sheet instanceof HSLFNotes) { - phId = (byte)placeholder.nativeNotesId; - } else { - phId = (byte)placeholder.nativeSlideId; - } - - if (phId == -2) { - throw new HSLFException("Placeholder "+placeholder.name()+" not supported for this sheet type ("+sheet.getClass()+")"); - } - - switch (placeholder) { - case HEADER: - case FOOTER: - if (rtp == null) { - rtp = new RoundTripHFPlaceholder12(); - rtp.setPlaceholderId(phId); - clientData.addChild(rtp); - } - if (oep != null) { - clientData.removeChild(OEPlaceholderAtom.class); - } - break; - default: - if (rtp != null) { - clientData.removeChild(RoundTripHFPlaceholder12.class); - } - if (oep == null) { - oep = new OEPlaceholderAtom(); - oep.setPlaceholderSize((byte)OEPlaceholderAtom.PLACEHOLDER_FULLSIZE); - // TODO: placement id only "SHOULD" be unique ... check other placeholders on sheet for unique id - oep.setPlacementId(-1); - oep.setPlaceholderId(phId); - clientData.addChild(oep); - } - break; - } + getPlaceholderDetails().setPlaceholder(placeholder); } @@ -727,4 +596,10 @@ public abstract class HSLFSimpleShape extends HSLFShape implements SimpleShape= path.length) { + return root; + } + final RecordContainer newRoot = (RecordContainer) root.findFirstOfType(path[index].typeID); + return selectContainer(newRoot, index+1, path); + } + /** * Get the comment(s) for this slide. * Note - for now, only works on PPT 2000 and * PPT 2003 files. Doesn't work for PPT 97 * ones, as they do their comments oddly. */ - public Comment[] getComments() { + public List getComments() { + final List comments = new ArrayList<>(); // If there are any, they're in // ProgTags -> ProgBinaryTag -> BinaryTagData - RecordContainer progTags = (RecordContainer) - getSheetContainer().findFirstOfType( - RecordTypes.ProgTags.typeID - ); - if(progTags != null) { - RecordContainer progBinaryTag = (RecordContainer) - progTags.findFirstOfType( - RecordTypes.ProgBinaryTag.typeID - ); - if(progBinaryTag != null) { - RecordContainer binaryTags = (RecordContainer) - progBinaryTag.findFirstOfType( - RecordTypes.BinaryTagData.typeID - ); - if(binaryTags != null) { - // This is where they'll be - int count = 0; - for(int i=0; i { } final String text = ((tba != null) ? tba.getText() : tca.getText()); - + StyleTextPropAtom sta = (StyleTextPropAtom)_txtbox.findFirstOfType(StyleTextPropAtom._type); TextPropCollection paraStyle = null, charStyle = null; if (sta == null) { @@ -305,7 +305,7 @@ implements TextShape { htp.setParagraphStyle(paraStyle); htp.setParentShape(this); _paragraphs.add(htp); - + HSLFTextRun htr = new HSLFTextRun(htp); htr.setCharacterStyle(charStyle); htr.setText(text); @@ -317,7 +317,7 @@ implements TextShape { public Rectangle2D resizeToFitText() { return resizeToFitText(null); } - + @Override public Rectangle2D resizeToFitText(Graphics2D graphics) { Rectangle2D anchor = getAnchor(); @@ -649,7 +649,7 @@ implements TextShape { } else { _paragraphs = pList; } - + if (_paragraphs.isEmpty()) { LOG.log(POILogger.WARN, "TextRecord didn't contained any text lines"); } @@ -701,18 +701,13 @@ implements TextShape { @Override public boolean isPlaceholder() { - OEPlaceholderAtom oep = getPlaceholderAtom(); - if (oep != null) { - return true; - } - - //special case for files saved in Office 2007 - RoundTripHFPlaceholder12 hldr = getHFPlaceholderAtom(); - if (hldr != null) { - return true; - } - - return false; + return + ((getPlaceholderAtom() != null) || + //special case for files saved in Office 2007 + (getHFPlaceholderAtom() != null)) && + // check for metro shape of complex placeholder + (!new HSLFMetroShape(this).hasMetroBlob()) + ; } @@ -738,7 +733,7 @@ implements TextShape { public double getTextHeight() { return getTextHeight(null); } - + @Override public double getTextHeight(Graphics2D graphics) { DrawFactory drawFact = DrawFactory.getInstance(graphics); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java index f2700d4fa..6a34d1af3 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java @@ -33,9 +33,9 @@ import java.io.InputStream; import java.util.List; import org.apache.poi.POIDataSamples; +import org.apache.poi.hslf.usermodel.HSLFObjectShape; import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.hslf.usermodel.HSLFSlideShowImpl; -import org.apache.poi.hslf.usermodel.HSLFObjectShape; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.poifs.filesystem.DirectoryNode; @@ -89,12 +89,12 @@ public final class TestExtractor { public void testReadSheetText() throws IOException { // Basic 2 page example PowerPointExtractor ppe = openExtractor("basic_test_ppt_file.ppt"); - ensureTwoStringsTheSame(expectText, ppe.getText()); + assertEquals(expectText, ppe.getText()); ppe.close(); // 1 page example with text boxes PowerPointExtractor ppe2 = openExtractor("with_textbox.ppt"); - ensureTwoStringsTheSame(expectText2, ppe2.getText()); + assertEquals(expectText2, ppe2.getText()); ppe2.close(); } @@ -103,15 +103,15 @@ public final class TestExtractor { // Basic 2 page example PowerPointExtractor ppe = openExtractor("basic_test_ppt_file.ppt"); String notesText = ppe.getNotes(); - String expText = "These are the notes for page 1\nThese are the notes on page two, again lacking formatting\n"; - ensureTwoStringsTheSame(expText, notesText); + String expText = "\nThese are the notes for page 1\n\nThese are the notes on page two, again lacking formatting\n"; + assertEquals(expText, notesText); ppe.close(); // Other one doesn't have notes PowerPointExtractor ppe2 = openExtractor("with_textbox.ppt"); notesText = ppe2.getNotes(); expText = ""; - ensureTwoStringsTheSame(expText, notesText); + assertEquals(expText, notesText); ppe2.close(); } @@ -122,8 +122,8 @@ public final class TestExtractor { "This is the title on page 2\nThis is page two\nIt has several blocks of text\nNone of them have formatting\n" }; String[] ntText = new String[]{ - "These are the notes for page 1\n", - "These are the notes on page two, again lacking formatting\n" + "\nThese are the notes for page 1\n", + "\nThese are the notes on page two, again lacking formatting\n" }; PowerPointExtractor ppe = openExtractor("basic_test_ppt_file.ppt"); @@ -137,7 +137,7 @@ public final class TestExtractor { ppe.setSlidesByDefault(true); ppe.setNotesByDefault(true); - assertEquals(slText[0] + slText[1] + "\n" + ntText[0] + ntText[1], ppe.getText()); + assertEquals(slText[0] + ntText[0] + slText[1] + ntText[1], ppe.getText()); ppe.close(); } @@ -166,16 +166,6 @@ public final class TestExtractor { ppe.close(); } - private void ensureTwoStringsTheSame(String exp, String act) { - assertEquals(exp.length(), act.length()); - char[] expC = exp.toCharArray(); - char[] actC = act.toCharArray(); - for (int i = 0; i < expC.length; i++) { - assertEquals("Char " + i, expC[i], actC[i]); - } - assertEquals(exp, act); - } - @Test public void testExtractFromEmbeded() throws IOException { InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("excel_with_embeded.xls"); @@ -454,4 +444,38 @@ public final class TestExtractor { assertContains(text, "Prague"); ppe.close(); } + + @Test + public void testExtractGroupedShapeText() throws Exception { + try (final PowerPointExtractor ppe = openExtractor("bug62092.ppt")) { + final String text = ppe.getText(); + + //this tests that we're ignoring text shapes at depth=0 + //i.e. POI has already included them in the slide's getTextParagraphs() + assertContains(text, "Text box1"); + assertEquals(1, countMatches(text,"Text box1")); + + + //the WordArt and text box count tests will fail + //if this content is available via getTextParagraphs() of the slide in POI + //i.e. when POI is fixed, these tests will fail, and + //we'll have to remove the workaround in HSLFExtractor's extractGroupText(...) + assertEquals(1, countMatches(text,"WordArt1")); + assertEquals(1, countMatches(text,"WordArt2")); + assertEquals(1, countMatches(text,"Ungrouped text box"));//should only be 1 + assertContains(text, "Text box2"); + assertContains(text, "Text box3"); + assertContains(text, "Text box4"); + assertContains(text, "Text box5"); + + //see below -- need to extract hyperlinks + assertContains(text, "tika"); + assertContains(text, "MyTitle"); + + } + } + + private static int countMatches(final String base, final String find) { + return base.split(find).length-1; + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestRecordTypes.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestRecordTypes.java index 814259ca1..dbf89692b 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestRecordTypes.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestRecordTypes.java @@ -18,10 +18,10 @@ package org.apache.poi.hslf.record; -import static org.junit.Assert.assertEquals; - import org.junit.Test; +import static org.junit.Assert.assertEquals; + /** * Tests that RecordTypes returns the right records and classes when asked */ @@ -42,20 +42,15 @@ public final class TestRecordTypes { @Test public void testPPTClassLookups() { - assertEquals(Slide.class, RecordTypes.Slide.handlingClass); - assertEquals(TextCharsAtom.class, RecordTypes.TextCharsAtom.handlingClass); - assertEquals(TextBytesAtom.class, RecordTypes.TextBytesAtom.handlingClass); - assertEquals(SlideListWithText.class, RecordTypes.SlideListWithText.handlingClass); - // If this record is ever implemented, change to one that isn't! // This is checking the "unhandled default" stuff works - assertEquals(UnknownRecordPlaceholder.class, RecordTypes.forTypeID(-10).handlingClass); + assertEquals(RecordTypes.UnknownRecordPlaceholder, RecordTypes.forTypeID(-10)); } @Test public void testEscherClassLookups() { // Should all come back with null, as DDF handles them - assertEquals(null, RecordTypes.EscherDggContainer.handlingClass); - assertEquals(null, RecordTypes.EscherBStoreContainer.handlingClass); + assertEquals(null, RecordTypes.EscherDggContainer.recordConstructor); + assertEquals(null, RecordTypes.EscherBStoreContainer.recordConstructor); } } diff --git a/test-data/slideshow/bug62092.ppt b/test-data/slideshow/bug62092.ppt new file mode 100644 index 0000000000000000000000000000000000000000..769049581092689ba993249ed78b101e5edf9bd0 GIT binary patch literal 161792 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%0D55O{ggJN? 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 literal 0 HcmV?d00001