]> source.dussan.org Git - poi.git/commitdiff
#65694 - HSLF - handle date/time fields and formats
authorAndreas Beeker <kiwiwings@apache.org>
Mon, 22 Nov 2021 00:01:31 +0000 (00:01 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Mon, 22 Nov 2021 00:01:31 +0000 (00:01 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1895248 13f79535-47bb-0310-9956-ffa450edef68

17 files changed:
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java [new file with mode: 0644]
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java [new file with mode: 0644]
poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java
poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java
poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java
poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java
poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java
poi/src/main/java9/module-info.class
test-data/slideshow/datetime.ppt [new file with mode: 0755]

index c126afdd7ad94940ff009d1c5e85c5ecbfd501d7..446ab8af7bcab7be60279893253145fafbc85b50 100644 (file)
@@ -70,7 +70,7 @@ implements Slide<XSLFShape,XSLFTextParagraph> {
      * Construct a SpreadsheetML slide from a package part
      *
      * @param part the package part holding the slide data,
-     * the content type must be <code>application/vnd.openxmlformats-officedocument.slide+xml</code>
+     * the content type must be {@code application/vnd.openxmlformats-officedocument.slide+xml}
      *
      * @since POI 3.14-Beta1
      */
@@ -377,12 +377,6 @@ implements Slide<XSLFShape,XSLFTextParagraph> {
         draw.draw(graphics);
     }
 
-    @Override
-    public boolean getDisplayPlaceholder(Placeholder placeholder) {
-        return false;
-    }
-
-
     @Override
     public void setHidden(boolean hidden) {
         CTSlide sld = getXmlObject();
index 64b6dc6a357596be0212b4312f47082be7f704c3..51f647ea86d12b1a1beba21f3ad7d431b629d828 100644 (file)
@@ -50,11 +50,11 @@ public final class HeadersFooters {
 
     public HeadersFooters(HSLFSheet sheet, short headerFooterType) {
         _sheet = sheet;
-        
+
         @SuppressWarnings("resource")
         HSLFSlideShow ppt = _sheet.getSlideShow();
         Document doc = ppt.getDocumentRecord();
-        
+
         // detect if this ppt was saved in Office2007
         String tag = ppt.getSlideMasters().get(0).getProgrammableTag();
         _ppt2007 = _ppt2007tag.equals(tag);
@@ -72,7 +72,7 @@ public final class HeadersFooters {
                 }
             }
         }
-        
+
         if (hdd == null) {
             hdd = new HeadersFootersContainer(headerFooterType);
             Record lst = doc.findFirstOfType(RecordTypes.List.typeID);
@@ -206,6 +206,18 @@ public final class HeadersFooters {
         return isVisible(HeadersFootersAtom.fHasUserDate, Placeholder.DATETIME);
     }
 
+    public CString getHeaderAtom() {
+        return _container.getHeaderAtom();
+    }
+
+    public CString getFooterAtom() {
+        return _container.getFooterAtom();
+    }
+
+    public CString getUserDateAtom() {
+        return _container.getUserDateAtom();
+    }
+
     /**
      * whether the date is displayed in the footer.
      */
@@ -213,6 +225,20 @@ public final class HeadersFooters {
         setFlag(HeadersFootersAtom.fHasUserDate, flag);
     }
 
+    /**
+     * whether today's date is used.
+     */
+    public boolean isTodayDateVisible(){
+        return isVisible(HeadersFootersAtom.fHasTodayDate, Placeholder.DATETIME);
+    }
+
+    /**
+     * whether the todays date is displayed in the footer.
+     */
+    public void setTodayDateVisible(boolean flag){
+        setFlag(HeadersFootersAtom.fHasTodayDate, flag);
+    }
+
     /**
      * whether the slide number is displayed in the footer.
      */
@@ -282,4 +308,8 @@ public final class HeadersFooters {
     public boolean isPpt2007() {
         return _ppt2007;
     }
+
+    public HeadersFootersContainer getContainer() {
+        return _container;
+    }
 }
diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java
new file mode 100644 (file)
index 0000000..5268b2a
--- /dev/null
@@ -0,0 +1,125 @@
+/* ====================================================================
+   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.record;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import org.apache.poi.util.GenericRecordUtil;
+import org.apache.poi.util.LittleEndian;
+
+public class DateTimeMCAtom extends RecordAtom {
+
+    /**
+     * Record header.
+     */
+    private final byte[] _header;
+
+    /**
+     * A TextPosition that specifies the position of the metacharacter in the corresponding text.
+     */
+    private int position;
+
+    /**
+     * An unsigned byte that specifies the Format ID used to stylize datetime. The identifier specified by
+     * the Format ID is converted based on the LCID [MS-LCID] into a value or string as specified in the
+     * following tables. The LCID is specified in TextSIException.lid. If no valid LCID is found in
+     * TextSIException.lid, TextSIException.altLid (if it exists) is used.
+     * The value MUST be greater than or equal to 0x0 and MUST be less than or equal to 0xC.
+     */
+    private int index;
+
+    private final byte[] unused = new byte[3];
+
+    protected DateTimeMCAtom() {
+        _header = new byte[8];
+        position = 0;
+        index = 0;
+
+        LittleEndian.putShort(_header, 2, (short)getRecordType());
+        LittleEndian.putInt(_header, 4, 8);
+    }
+
+    /**
+     * Constructs the datetime atom record from its source data.
+     *
+     * @param source the source data as a byte array.
+     * @param start the start offset into the byte array.
+     * @param len the length of the slice in the byte array.
+     */
+    protected DateTimeMCAtom(byte[] source, int start, int len) {
+        // Get the header.
+        _header = Arrays.copyOfRange(source, start, start+8);
+
+        position = LittleEndian.getInt(source, start+8);
+        index  = LittleEndian.getUByte(source, start+12);
+        System.arraycopy(source, start+13, unused, 0, 3);
+    }
+
+    /**
+     * Write the contents of the record back, so it can be written
+     * to disk
+     *
+     * @param out the output stream to write to.
+     * @throws IOException if an error occurs.
+     */
+    @Override
+    public void writeOut(OutputStream out) throws IOException {
+        out.write(_header);
+        LittleEndian.putInt(position, out);
+        out.write(index);
+        out.write(unused);
+    }
+
+    public int getPosition() {
+        return position;
+    }
+
+    public void setPosition(int position) {
+        this.position = position;
+    }
+
+    public int getIndex() {
+        return index;
+    }
+
+    public void setIndex(int index) {
+        this.index = index;
+    }
+
+    /**
+     * Gets the record type.
+     * @return the record type.
+     */
+    @Override
+    public long getRecordType() {
+        return RecordTypes.DateTimeMCAtom.typeID;
+    }
+
+    @Override
+    public Map<String, Supplier<?>> getGenericProperties() {
+        return GenericRecordUtil.getGenericProperties(
+            "position", this::getPosition,
+            "index", this::getIndex
+        );
+    }
+
+}
index 552ec50ff08ee50f87fa1585696b8eaddb7ed777..25d2b39913a7ec24bd070793c682a1b367965cf5 100644 (file)
@@ -18,7 +18,6 @@
 package org.apache.poi.hslf.record;
 
 import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
-import static org.apache.poi.util.GenericRecordUtil.safeEnum;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -37,30 +36,6 @@ import org.apache.poi.util.LittleEndian;
 
 public final class HeadersFootersAtom extends RecordAtom {
 
-    /** FormatIndex enum without LCID mapping */
-    public enum FormatIndex {
-        SHORT_DATE,
-        LONG_DATE,
-        LONG_DATE_WITHOUT_WEEKDAY,
-        ALTERNATE_SHORT_DATE,
-        ISO_STANDARD_DATE,
-        SHORT_DATE_WITH_ABBREVIATED_MONTH,
-        SHORT_DATE_WITH_SLASHES,
-        ALTERNATE_SHORT_DATE_WITH_ABBREVIATED_MONTH,
-        ENGLISH_DATE,
-        MONTH_AND_YEAR,
-        ABBREVIATED_MONTH_AND_YEAR,
-        DATE_AND_HOUR12_TIME,
-        DATE_AND_HOUR12_TIME_WITH_SECONDS,
-        HOUR12_TIME,
-        HOUR12_TIME_WITH_SECONDS,
-        HOUR24_TIME,
-        HOUR24_TIME_WITH_SECONDS,
-        CHINESE1,
-        CHINESE2,
-        CHINESE3
-    }
-
     /**
      * A bit that specifies whether the date is displayed in the footer.
      * @see #getMask()
@@ -136,7 +111,7 @@ public final class HeadersFootersAtom extends RecordAtom {
     /**
      * Build an instance of {@code HeadersFootersAtom} from on-disk data
      */
-    protected HeadersFootersAtom(byte[] source, int start, int len) {
+    HeadersFootersAtom(byte[] source, int start, int len) {
         // Get the header
         _header = Arrays.copyOfRange(source, start, start+8);
 
@@ -182,6 +157,7 @@ public final class HeadersFootersAtom extends RecordAtom {
         return LittleEndian.getShort(_recdata, 0);
     }
 
+
     /**
      * A signed integer that specifies the format ID to be used to style the datetime.
      *
@@ -258,7 +234,7 @@ public final class HeadersFootersAtom extends RecordAtom {
     @Override
     public Map<String, Supplier<?>> getGenericProperties() {
         return GenericRecordUtil.getGenericProperties(
-            "formatIndex", safeEnum(FormatIndex.values(), this::getFormatId),
+            "formatIndex", this::getFormatId,
             "flags", getBitsAsString(this::getMask, PLACEHOLDER_MASKS, PLACEHOLDER_NAMES)
         );
     }
index bfc038747c37e70a0ecf515c2837e52e8ea0e6d1..28247886a77112fc7de1ab2aabc2aab0f6d9ab1a 100644 (file)
@@ -52,7 +52,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
      */
     public static final int PLACEHOLDER_QUARTSIZE = 2;
 
-    private byte[] _header;
+    private final byte[] _header;
 
     private int placementId;
     private int placeholderId;
@@ -77,7 +77,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
     /**
      * Build an instance of {@code OEPlaceholderAtom} from on-disk data
      */
-    protected OEPlaceholderAtom(byte[] source, int start, int len) {
+    OEPlaceholderAtom(byte[] source, int start, int len) {
         _header = Arrays.copyOfRange(source, start, start+8);
         int offset = start+8;
 
@@ -135,7 +135,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
      * Sets the placeholder Id.<p>
      *
      * placeholder Id specifies the type of the placeholder shape.
-     * The value MUST be one of the static constants defined in this class
+     * The value MUST be one of the static constants defined in {@link Placeholder}
      *
      * @param id the placeholder Id.
      */
index 295096da7e6b5b463621beb3a354214c666082f8..f3cbf0b1068b6fa8e4014521a02a55909d09774c 100644 (file)
@@ -134,7 +134,7 @@ public enum RecordTypes {
     InteractiveInfoAtom(4083,InteractiveInfoAtom::new),
     UserEditAtom(4085,UserEditAtom::new),
     CurrentUserAtom(4086,null),
-    DateTimeMCAtom(4087,null),
+    DateTimeMCAtom(4087,DateTimeMCAtom::new),
     GenericDateMCAtom(4088,null),
     FooterMCAtom(4090,null),
     ExControlAtom(4091,ExControlAtom::new),
index 906e51c44b2204c62c898f705a096ea6124a6270..3c279e299f4969641aeef3082ab00689c75cefe5 100644 (file)
@@ -36,6 +36,7 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
     }
 
 
+    @Override
     public boolean isVisible() {
         final Placeholder ph = getPlaceholder();
         if (ph == null) {
@@ -46,13 +47,12 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
 
         switch (ph) {
             case HEADER:
+            case TITLE:
                 return headersFooters.isHeaderVisible();
             case FOOTER:
                 return headersFooters.isFooterVisible();
             case DATETIME:
                 return headersFooters.isDateTimeVisible();
-            case TITLE:
-                return headersFooters.isHeaderVisible();
             case SLIDE_NUMBER:
                 return headersFooters.isSlideNumberVisible();
             default:
@@ -60,6 +60,7 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
         }
     }
 
+    @Override
     public void setVisible(final boolean isVisible) {
         final Placeholder ph = getPlaceholder();
         if (ph == null) {
index dc32a2d109bde657fe11af566a91c5bebf4cc487..8a467dfedcf825c0691b74d4cdca403b9ace9aa0 100644 (file)
 
 package org.apache.poi.hslf.usermodel;
 
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+import java.util.stream.Stream;
+
 import org.apache.poi.ddf.EscherPropertyTypes;
 import org.apache.poi.ddf.EscherSpRecord;
 import org.apache.poi.hslf.exceptions.HSLFException;
+import org.apache.poi.hslf.model.HeadersFooters;
+import org.apache.poi.hslf.record.CString;
+import org.apache.poi.hslf.record.DateTimeMCAtom;
+import org.apache.poi.hslf.record.EscherTextboxWrapper;
 import org.apache.poi.hslf.record.HSLFEscherClientDataRecord;
+import org.apache.poi.hslf.record.HeadersFootersAtom;
 import org.apache.poi.hslf.record.OEPlaceholderAtom;
-import org.apache.poi.hslf.record.Record;
+import org.apache.poi.hslf.record.RecordTypes;
 import org.apache.poi.hslf.record.RoundTripHFPlaceholder12;
+import org.apache.poi.hslf.record.TextSpecInfoAtom;
+import org.apache.poi.hslf.record.TextSpecInfoRun;
+import org.apache.poi.hslf.util.LocaleDateFormat;
 import org.apache.poi.sl.usermodel.MasterSheet;
 import org.apache.poi.sl.usermodel.Placeholder;
+import org.apache.poi.util.LocaleID;
+import org.apache.poi.util.LocaleUtil;
 
 /**
  * Extended placeholder details for HSLF shapes
@@ -41,6 +55,7 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
     final HSLFSimpleShape shape;
     private OEPlaceholderAtom oePlaceholderAtom;
     private RoundTripHFPlaceholder12 roundTripHFPlaceholder12;
+    private DateTimeMCAtom localDateTime;
 
 
     HSLFShapePlaceholderDetails(final HSLFSimpleShape shape) {
@@ -61,6 +76,7 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
         }
     }
 
+    @Override
     public Placeholder getPlaceholder() {
         updatePlaceholderAtom(false);
         final int phId;
@@ -68,6 +84,8 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
             phId = oePlaceholderAtom.getPlaceholderId();
         } else if (roundTripHFPlaceholder12 != null) {
             phId = roundTripHFPlaceholder12.getPlaceholderId();
+        } else if (localDateTime != null) {
+            return Placeholder.DATETIME;
         } else {
             return null;
         }
@@ -85,6 +103,7 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
         }
     }
 
+    @Override
     public void setPlaceholder(final Placeholder placeholder) {
         final EscherSpRecord spRecord = shape.getEscherChild(EscherSpRecord.RECORD_ID);
         int flags = spRecord.getFlags();
@@ -111,16 +130,17 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
         roundTripHFPlaceholder12.setPlaceholderId(phId);
     }
 
+    @Override
     public PlaceholderSize getSize() {
         final Placeholder ph = getPlaceholder();
         if (ph == null) {
             return null;
         }
 
-        final int size = (oePlaceholderAtom != null) 
+        final int size = (oePlaceholderAtom != null)
             ? oePlaceholderAtom.getPlaceholderSize()
             : OEPlaceholderAtom.PLACEHOLDER_HALFSIZE;
-        
+
         switch (size) {
         case OEPlaceholderAtom.PLACEHOLDER_FULLSIZE:
             return PlaceholderSize.full;
@@ -132,13 +152,14 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
         }
     }
 
+    @Override
     public void setSize(final PlaceholderSize size) {
         final Placeholder ph = getPlaceholder();
         if (ph == null || size == null) {
             return;
         }
         updatePlaceholderAtom(true);
-        
+
         final byte ph_size;
         switch (size) {
         case full:
@@ -202,6 +223,14 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
     }
 
     private void updatePlaceholderAtom(final boolean create) {
+        localDateTime = null;
+        if (shape instanceof HSLFTextBox) {
+            EscherTextboxWrapper txtBox = ((HSLFTextBox)shape).getEscherTextboxWrapper();
+            if (txtBox != null) {
+                localDateTime = (DateTimeMCAtom)txtBox.findFirstOfType(RecordTypes.DateTimeMCAtom.typeID);
+            }
+        }
+
         final HSLFEscherClientDataRecord clientData = shape.getClientData(create);
         if (clientData == null) {
             oePlaceholderAtom = null;
@@ -237,4 +266,38 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
             clientData.addChild(roundTripHFPlaceholder12);
         }
     }
+
+    @Override
+    public String getUserDate() {
+        HeadersFooters hf = shape.getSheet().getHeadersFooters();
+        CString uda = hf.getUserDateAtom();
+        return hf.isUserDateVisible() && uda != null ? uda.getText() : null;
+    }
+
+    @Override
+    public DateTimeFormatter getDateFormat() {
+        int formatId;
+        if (localDateTime != null) {
+            formatId = localDateTime.getIndex();
+        } else {
+            HeadersFootersAtom hfAtom = shape.getSheet().getHeadersFooters().getContainer().getHeadersFootersAtom();
+            formatId = hfAtom.getFormatId();
+        }
+
+        LocaleID def = LocaleID.lookupByLanguageTag(LocaleUtil.getUserLocale().toLanguageTag());
+
+        // def = LocaleID.EN_US;
+
+        LocaleID lcid =
+            Stream.of(((HSLFTextShape)shape).getTextParagraphs().get(0).getRecords())
+            .filter(r -> r instanceof TextSpecInfoAtom)
+            .findFirst()
+            .map(r -> ((TextSpecInfoAtom)r).getTextSpecInfoRuns()[0])
+            .map(TextSpecInfoRun::getLangId)
+            .flatMap(lid -> Optional.ofNullable(LocaleID.lookupByLcid(lid)))
+            .orElse(def != null ? def : LocaleID.EN_US)
+        ;
+
+        return LocaleDateFormat.map(lcid, formatId, LocaleDateFormat.MapFormatId.PPT);
+    }
 }
index d63ea397210aa9eed0a590235312d3c2da91c543..6f205c9e578c1e5ca1c72e7ddb20b2681b09b4a0 100644 (file)
@@ -46,6 +46,7 @@ import org.apache.poi.sl.draw.Drawable;
 import org.apache.poi.sl.usermodel.Notes;
 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.Slide;
 import org.apache.poi.sl.usermodel.TextShape.TextPlaceholder;
 
@@ -259,7 +260,7 @@ public final class HSLFSlide extends HSLFSheet implements Slide<HSLFShape,HSLFTe
      * @return set of records inside {@code SlideListWithtext} container
      *  which hold text data for this slide (typically for placeholders).
      */
-    protected SlideAtomsSet getSlideAtomsSet() { return _atomSet;  }
+    public SlideAtomsSet getSlideAtomsSet() { return _atomSet;  }
 
     /**
      * Returns master sheet associated with this slide.
@@ -495,18 +496,41 @@ public final class HSLFSlide extends HSLFSheet implements Slide<HSLFShape,HSLFTe
             (slt == SlideLayoutType.TITLE_SLIDE || slt == SlideLayoutType.TITLE_ONLY || slt == SlideLayoutType.MASTER_TITLE);
         switch (placeholder) {
         case DATETIME:
-            return hf.isDateTimeVisible() && !isTitle;
+            return (hf.isDateTimeVisible() && (hf.isTodayDateVisible() || (hf.isUserDateVisible() && hf.getUserDateAtom() != null))) && !isTitle;
         case SLIDE_NUMBER:
             return hf.isSlideNumberVisible() && !isTitle;
         case HEADER:
-            return hf.isHeaderVisible() && !isTitle;
+            return hf.isHeaderVisible() && hf.getHeaderAtom() != null && !isTitle;
         case FOOTER:
-            return hf.isFooterVisible() && !isTitle;
+            return hf.isFooterVisible() && hf.getFooterAtom() != null && !isTitle;
         default:
             return false;
         }
     }
 
+    @Override
+    public boolean getDisplayPlaceholder(final SimpleShape<?,?> placeholderRef) {
+        Placeholder placeholder = placeholderRef.getPlaceholder();
+        if (placeholder == null) {
+            return false;
+        }
+
+        final HeadersFooters hf = getHeadersFooters();
+        final SlideLayoutType slt = getSlideRecord().getSlideAtom().getSSlideLayoutAtom().getGeometryType();
+        final boolean isTitle =
+            (slt == SlideLayoutType.TITLE_SLIDE || slt == SlideLayoutType.TITLE_ONLY || slt == SlideLayoutType.MASTER_TITLE);
+        switch (placeholder) {
+            case HEADER:
+                return hf.isHeaderVisible() && hf.getHeaderAtom() != null && !isTitle;
+            case FOOTER:
+                return hf.isFooterVisible() && hf.getFooterAtom() != null && !isTitle;
+            case DATETIME:
+            case SLIDE_NUMBER:
+            default:
+                return false;
+        }
+    }
+
     @Override
     public HSLFMasterSheet getSlideLayout(){
         // TODO: find out how we can find the mastersheet base on the slide layout type, i.e.
diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java
new file mode 100644 (file)
index 0000000..cad2d05
--- /dev/null
@@ -0,0 +1,364 @@
+/* ====================================================================
+   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.util;
+
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.FormatStyle;
+import java.util.AbstractMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.poi.util.Internal;
+import org.apache.poi.util.LocaleID;
+import org.apache.poi.util.SuppressForbidden;
+
+@Internal
+public final class LocaleDateFormat {
+
+    /**
+     * Enum to specify initial remapping of the FormatID based on thd LCID
+     */
+    public enum MapFormatId {
+        NONE, PPT
+    }
+
+    private enum MapFormatPPT {
+        EN_US(LocaleID.EN_US, "MM/dd/yyyy", 1, 8, "MMMM dd, yyyy", 5, 9, 10, 11, 12, 15, 16, "h:mm a", "h:mm:ss a"),
+        EN_AU(LocaleID.EN_AU, 0, 1, "d MMMM, yyy", 2, 5, 9, 10, 11, 12, 15, 16, 13, 14),
+        JA_JP(LocaleID.JA_JP, 4, 8, 7, 3, 0, 9, 5, 11, 12, "HH:mm", "HH:mm:ss", 15, 16),
+        ZH_TW(LocaleID.ZH_TW, 0, 1, 3, 7, 12, 9, 10, 4, 11, "HH:mm", "HH:mm:ss", "H:mm a", "H:mm:ss a"),
+        KO_KR(LocaleID.KO_KR, 0, 1, 6, 3, 4, 10, 7, 12, 11, "HH:mm", "HH:mm:ss", 13, 14 ),
+        AR_SA(LocaleID.AR_SA, 0, 1, 2, 3, 4, 5, 8, 7, 8, 1, 10, 11, 5),
+        HE_IL(LocaleID.HE_IL, 0, 1, 2, 6, 11, 5, 12, 7, 8, 9, 1, 11, 6),
+        SV_SE(LocaleID.SV_SE, 0, 1, 3, 2, 7, 9, 10, 11, 12, 15, 16, 13, 14),
+        ZH_CN(LocaleID.ZH_CN, 0, 1, 2, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
+        ZH_SG(LocaleID.ZH_SG, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
+        ZH_MO(LocaleID.ZH_MO, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
+        ZH_HK(LocaleID.ZH_HK, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
+        TH_TH(LocaleID.TH_TH, 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14),
+        VI_VN(LocaleID.VI_VN, 0, 1, 2, 3, 5, 6, 10, 11, 12, 13, 14, 15, 16),
+        HI_IN(LocaleID.HI_IN, 1, 2, 3, 5, 7, 11, 13, 0, 1, 5, 10, 11, 14),
+        SYR_SY(LocaleID.SYR_SY, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
+        NO_MAP(LocaleID.INVALID_O, 0, 1, 3, 2, 5, 9, 10, 11, 12, 15, 16, 13, 14, 4, 6, 7, 8)
+        ;
+
+        private final LocaleID lcid;
+        private final Object[] mapping;
+
+        private static final Map<LocaleID,MapFormatPPT> LCID_LOOKUP =
+            Stream.of(values()).collect(Collectors.toMap(MapFormatPPT::getLocaleID, Function.identity()));
+
+        MapFormatPPT(LocaleID lcid, Object... mapping) {
+            this.lcid = lcid;
+            this.mapping = mapping;
+        }
+
+        public LocaleID getLocaleID() {
+            return lcid;
+        }
+
+        public static Object mapFormatId(LocaleID lcid, int formatId) {
+            Object[] mapping = LCID_LOOKUP.getOrDefault(lcid, NO_MAP).mapping;
+            return (formatId >= 0 && formatId < mapping.length) ? mapping[formatId] : formatId;
+        }
+    }
+
+    private enum MapFormatException {
+        CHINESE(
+            new LocaleID[]{LocaleID.ZH, LocaleID.ZH_HANS, LocaleID.ZH_HANT, LocaleID.ZH_CN, LocaleID.ZH_SG, LocaleID.ZH_MO, LocaleID.ZH_HK, LocaleID.ZH_YUE_HK},
+            0,
+            1,
+            "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
+            "yyyy\u5E74M\u6708d\u65E5",
+            "yyyy/M/d",
+            "yy.M.d",
+            "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
+            "yyyy\u5E74M\u6708d\u65E5",
+            "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
+            "yyyy\u5E74M\u6708",
+            "yyyy\u5E74M\u6708",
+            "h\u65F6m\u5206s\u79D2",
+            "h\u65F6m\u5206",
+            "h\u65F6m\u5206",
+            "h\u65F6m\u5206",
+            "ah\u65F6m\u5206",
+            "ah\u65F6m\u5206",
+            // no lunar calendar support
+            "EEEE\u5E74O\u6708A\u65E5",
+            "EEEE\u5E74O\u6708A\u65E5\u661F\u671FW",
+            "EEEE\u5E74O\u6708"
+        ),
+        // no hindu calendar support
+        HINDI(
+            new LocaleID[]{LocaleID.HI, LocaleID.HI_IN},
+            "dd/M/g",
+            "dddd, d MMMM yyyy",
+            "dd MMMM yyyy",
+            "dd/M/yy",
+            "yy-M-dd",
+            "d-MMMM-yyyy",
+            "dd.M.g",
+            "dd MMMM. yy",
+            "dd MMMM yy",
+            "MMMM YY",
+            "MMMM-g",
+            "dd/M/g HH:mm",
+            "dd/M/g HH:mm:ss",
+            "HH:mm a",
+            "HH:mm:ss a",
+            "HH:mm",
+            "HH:mm:ss"
+        ),
+        // https://www.secondsite8.com/customdateformats.htm
+        // aa or gg or o, r, i, c -> lunar calendar not supported
+        // (aaa) -> lower case week names ... not supported
+        JAPANESE(
+            new LocaleID[]{LocaleID.JA, LocaleID.JA_JP, LocaleID.JA_PLOC_JP},
+            0,
+            1,
+            "EEEy\u5E74M\u6708d\u65E5",
+            "yyyy\u5E74M\u6708d\u65E5",
+            "yyyy/M/d",
+            "yyyy\u5E74M\u6708d\u65E5",
+            "yy\u5E74M\u6708d\u65E5",
+            "yyyy\u5E74M\u6708d\u65E5",
+            "yyyy\u5E74M\u6708d\u65E5(EEE)",
+            "yyyy\u5E74M\u6708",
+            "yyyy\u5E74M\u6708",
+            "yy/M/d H\u6642m\u5206",
+            "yy/M/d H\u6642m\u5206s\u79D2",
+            "a h\u6642m\u5206",
+            "a h\u6642m\u5206s\u79D2",
+            "H\u6642m\u5206",
+            "H\u6642m\u5206s\u79D2",
+            "yyyy\u5E74M\u6708d\u65E5 EEE\u66DC\u65E5"
+        ),
+        KOREAN(
+            new LocaleID[]{LocaleID.KO,LocaleID.KO_KR},
+            0,
+            1,
+            "yyyy\uB144 M\uC6D4 d\uC77C EEE\uC694\uC77C",
+            "yyyy\uB144 M\uC6D4 d\uC77C",
+            "yyyy/M/d",
+            "yyMMdd",
+            "yyyy\uB144 M\uC6D4 d\uC77C",
+            "yyyy\uB144 M\uC6D4",
+            "yyyy\uB144 M\uC6D4 d\uC77C",
+            "yyyy",
+            "yyyy\uB144 M\uC6D4",
+            "yyyy\uB144 M\uC6D4 d\uC77C a h\uC2DC m\uBD84",
+            "yy\uB144 M\uC6D4 d\uC77C H\uC2DC m\uBD84 s\uCD08",
+            "a h\uC2DC m\uBD84",
+            "a h\uC2DC m\uBD84 s\uCD08",
+            "H\uC2DC m\uBD84",
+            "H\uC2DC m\uBD84 S\uCD08"
+        ),
+        HUNGARIAN(
+            new LocaleID[]{LocaleID.HU, LocaleID.HU_HU},
+            0, 1, 2, 3, 4, 5, 6, "yy. MMM. dd.", "\u2019yy MMM.", "MMMM \u2019yy", 10, 11, 12, "a h:mm", "a h:mm:ss", 15, 16
+        ),
+        BOKMAL(
+            new LocaleID[]{LocaleID.NB_NO},
+            0, 1, 2, 3, 4, "d. MMM. yyyy", "d/m yyyy", "MMM. yy", "yyyy.mm.dd", 9, "d. MMM.", 11, 12, 13, 14, 15, 16
+        ),
+        CZECH(new LocaleID[]{LocaleID.CS, LocaleID.CS_CZ}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
+        DANISH(new LocaleID[]{LocaleID.DA, LocaleID.DA_DK}, 0, "d. MMMM yyyy", "yy-MM-dd", "yyyy.MM.dd", 4, "MMMM yyyy", "d.M.yy", "d/M yyyy", "dd.MM.yyyy", "d.M.yyyy", "dd/MM yyyy", 11, 12, 13, 14, 15, 16 ),
+        DUTCH(new LocaleID[]{LocaleID.NL,LocaleID.NL_BE,LocaleID.NL_NL}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
+        FINISH(new LocaleID[]{LocaleID.FI, LocaleID.FI_FI}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
+        FRENCH_CANADIAN(new LocaleID[]{LocaleID.FR_CA}, 0, 1, 2, "yy MM dd", 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16),
+        GERMAN(new LocaleID[]{LocaleID.DE,LocaleID.DE_AT,LocaleID.DE_CH,LocaleID.DE_DE,LocaleID.DE_LI,LocaleID.DE_LU}, 0, 1, 2, 3, 4, "yy-MM-dd", 6, "dd. MMM. yyyy", 8, 9, 10, 11, 12, 13, 14, 15, 16),
+        ITALIAN(new LocaleID[]{LocaleID.IT,LocaleID.IT_IT,LocaleID.IT_CH}, 0, 1, 2, 3, 4, "d-MMM.-yy", 6, "d. MMM. yy", "MMM. \u2019yy", "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
+        NO_MAP(new LocaleID[]{LocaleID.INVALID_O}, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+        // TODO: add others from [MS-OSHARED] chapter 2.4.4.4
+        ;
+
+
+        private final LocaleID[] lcid;
+        private final Object[] mapping;
+
+        private static final Map<LocaleID, MapFormatException> LCID_LOOKUP =
+            Stream.of(values()).flatMap(m -> Stream.of(m.lcid).map(l -> new AbstractMap.SimpleEntry<>(l, m)))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        MapFormatException(LocaleID[] lcid, Object... mapping) {
+            this.lcid = lcid;
+            this.mapping = mapping;
+        }
+
+        public static Object mapFormatId(LocaleID lcid, int formatId) {
+            Object[] mapping = LCID_LOOKUP.getOrDefault(lcid, NO_MAP).mapping;
+            return (formatId >= 0 && formatId < mapping.length) ? mapping[formatId] : formatId;
+        }
+    }
+
+    /**
+     * This enum lists and describes the format indices that can be used as inputs to the algorithm. The
+     * descriptions given are generalized; the actual format produced can vary from the description,
+     * depending on the input locale.
+     */
+    @SuppressForbidden("DateTimeFormatter::ofLocalizedDate and others will be localized in mapFormatId")
+    private enum MapFormatBase {
+        /** 0 - Base short date **/
+        SHORT_DATE(null,  FormatStyle.MEDIUM, DateTimeFormatter::ofLocalizedDate),
+        /** 1 - Base long date. **/
+        LONG_DATE(null, FormatStyle.FULL, DateTimeFormatter::ofLocalizedDate),
+        /**
+         * 2 - Do the following to base long date:
+         * - Remove occurrences of "dddd".
+         * - Remove the comma symbol (0x002C) and space following "dddd" if present.
+         * - Change occurrences of "dd" to "d".
+         **/
+        LONG_DATE_WITHOUT_WEEKDAY("d. MMMM yyyy", null, null),
+        /**
+         * 3 - Do the following to base short date:
+         * - Change occurrences of "yyyy" to "yy".
+         * - Change occurrences of "yy" to "yyyy".
+         */
+        ALTERNATE_SHORT_DATE("dd/MM/yy", null, null),
+        /**
+         * 4 - yyyy-MM-dd
+         */
+        ISO_STANDARD_DATE("yyyy-MM-dd", null, null),
+        /**
+         * 5 - If the symbol "y" occurs before the symbol "M" occurs in the base short date, the format is
+         * "yy-MMM-d". Otherwise, the format is "d-MMM-yy".
+         */
+        SHORT_DATE_WITH_ABBREVIATED_MONTH("d-MMM-yy", null, null),
+        /**
+         * 6 - If the forward slash symbol (0x002F) occurs in the base short date, the slash symbol is the
+         * period symbol (0x002E). Otherwise, the slash symbol is the forward slash (0x002F).
+         * A group is an uninterrupted sequence of qualified symbols where a qualified symbol is "d",
+         * "M", or "Y".
+         * Identify the first three groups that occur in the base short date. The format is formed by
+         * appending the three groups together with single slash symbols separating the groups.
+         */
+        SHORT_DATE_WITH_SLASHES("d/M/y", null, null),
+        /**
+         * 7 - Do the following to base long date:
+         * - Remove occurrences of "dddd".
+         * - Remove the comma symbol (0x002C) and space following "dddd" if present.
+         * - Change occurrences of "dd" to "d".
+         * - For all right-to-left locales and Lao, change a sequence of any length of "M" to "MMM".
+         * - For all other locales, change a sequence of any length of "M" to "MMM".
+         * - Change occurrences of "yyyy" to "yy".
+         */
+        ALTERNATE_SHORT_DATE_WITH_ABBREVIATED_MONTH("d. MMM yy", null, null),
+        /**
+         * 8 - For American English and Arabic, the format is "d MMMM yyyy".
+         * For Hebrew, the format is "d MMMM, yyyy".
+         * For all other locales, the format is the same as format 6 with the following additional step:
+         * Change occurrences of "yyyy" to "yy".
+         */
+        ENGLISH_DATE("d MMMM yyyy", null, null),
+        /**
+         * 9 - Do the following to base long date:
+         * - Remove all symbols that occur before the first occurrence of either the "y" symbol or the "M" symbol.
+         * - Remove all "d" symbols.
+         * - For all locales except Lithuanian, remove all period symbols (0x002E).
+         * - Remove all comma symbols (0x002C).
+         * - Change occurrences of "yyyy" to "yy".
+         */
+        MONTH_AND_YEAR("MMMM yy", null, null),
+        /**
+         * 10 - MMM-yy
+         */
+        ABBREVIATED_MONTH_AND_YEAR("LLL-yy", null, null),
+        /**
+         * 11 - Base short date followed by a space, followed by base time with seconds removed.
+         * Seconds are removed by removing all "s" symbols and any symbol that directly precedes an
+         * "s" symbol that is not an "h" or "m" symbol.
+         */
+        DATE_AND_HOUR12_TIME(null, FormatStyle.MEDIUM, (fs) -> new DateTimeFormatterBuilder().appendLocalized(FormatStyle.SHORT, null).appendLiteral("  ").appendLocalized(null, FormatStyle.SHORT).toFormatter()),
+        /**
+         * 12 - Base short date followed by a space, followed by base time.
+         */
+        DATE_AND_HOUR12_TIME_WITH_SECONDS(null, FormatStyle.MEDIUM, (fs) -> new DateTimeFormatterBuilder().appendLocalized(FormatStyle.SHORT, null).appendLiteral("  ").appendLocalized(null, fs).toFormatter()),
+        /**
+         * 13 - For Hungarian, the format is "am/pm h:mm".
+         * For all other locales, the format is "h:mm am/pm".
+         * In both cases, replace occurrences of the colon symbol (0x003A) with the time separator.
+         */
+        HOUR12_TIME("K:mm", null, null),
+        /**
+         * 14 - For Hungarian, the format is "am/pm h:mm:ss".
+         * For all other locales, the format is "h:mm:ss am/pm".
+         * In both cases, replace occurrences of the colon symbol (0x003A) with the time separator.
+         */
+        HOUR12_TIME_WITH_SECONDS("K:mm:ss", null, null),
+        /**
+         * 15 - "HH" followed by the time separator, followed by "mm".
+         */
+        HOUR24_TIME("HH:mm", null, null),
+        /**
+         * 16 - "HH" followed by the time separator, followed by "mm", followed by the time separator
+         * followed by "ss".
+         */
+        HOUR24_TIME_WITH_SECONDS("HH:mm:ss", null, null),
+        // CHINESE1(null, null, null),
+        // CHINESE2(null, null, null),
+        // CHINESE3(null, null, null)
+        ;
+
+
+        private final String datefmt;
+        private final FormatStyle formatStyle;
+        private final Function<FormatStyle,DateTimeFormatter> formatFct;
+
+        MapFormatBase(String datefmt, FormatStyle formatStyle, Function<FormatStyle,DateTimeFormatter> formatFct) {
+            this.formatStyle = formatStyle;
+            this.datefmt = datefmt;
+            this.formatFct = formatFct;
+        }
+
+        public static DateTimeFormatter mapFormatId(Locale loc, int formatId) {
+            MapFormatBase[] mfb = MapFormatBase.values();
+            if (formatId < 0 || formatId >= mfb.length) {
+                return DateTimeFormatter.BASIC_ISO_DATE;
+            }
+            MapFormatBase mf = mfb[formatId];
+            return (mf.datefmt == null)
+                ? mf.formatFct.apply(mf.formatStyle).withLocale(loc)
+                : DateTimeFormatter.ofPattern(mf.datefmt, loc);
+        }
+    }
+
+    private LocaleDateFormat() {}
+
+    public static DateTimeFormatter map(LocaleID lcid, int formatID, MapFormatId mapFormatId) {
+        final Locale loc = Locale.forLanguageTag(lcid.getLanguageTag());
+        int mappedFormatId = formatID;
+        if (mapFormatId == MapFormatId.PPT) {
+            Object mappedFormat = MapFormatPPT.mapFormatId(lcid, formatID);
+            if (mappedFormat instanceof String) {
+                return DateTimeFormatter.ofPattern((String)mappedFormat,loc);
+            } else {
+                mappedFormatId = (Integer)mappedFormat;
+            }
+        }
+        Object mappedFormat = MapFormatException.mapFormatId(lcid, mappedFormatId);
+        if (mappedFormat instanceof String) {
+            return DateTimeFormatter.ofPattern((String)mappedFormat,loc);
+        } else {
+            return MapFormatBase.mapFormatId(loc, (Integer)mappedFormat);
+        }
+    }
+}
index 0079c348dc02904fcf5dad2cf13e7d5742218d7c..3e51fb8f5d49069a6398b5d6d7fbbe2d39d19df0 100644 (file)
 
 package org.apache.poi.hslf.usermodel;
 
-import static org.apache.poi.sl.usermodel.BaseTestSlideShow.getColor;
+import static org.apache.poi.hslf.HSLFTestDataSamples.getSlideShow;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -27,15 +29,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.awt.Color;
 import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.apache.poi.hslf.HSLFTestDataSamples;
 import org.apache.poi.hslf.model.textproperties.TextPropCollection;
+import org.apache.poi.hslf.record.DateTimeMCAtom;
 import org.apache.poi.hslf.record.TextBytesAtom;
 import org.apache.poi.hslf.record.TextCharsAtom;
-import org.apache.poi.hslf.record.TextHeaderAtom;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import org.apache.poi.sl.usermodel.BaseTestSlideShow;
+import org.apache.poi.sl.usermodel.PlaceholderDetails;
+import org.apache.poi.util.LocaleUtil;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -43,76 +52,62 @@ import org.junit.jupiter.api.Test;
  */
 @SuppressWarnings("UnusedAssignment")
 public final class TestTextRun {
-    // SlideShow primed on the test data
-    private HSLFSlideShow ss;
-    private HSLFSlideShow ssRich;
-
-    @BeforeEach
-    void setUp() throws IOException {
-        // Basic (non rich) test file
-        ss = HSLFTestDataSamples.getSlideShow("basic_test_ppt_file.ppt");
-
-        // Rich test file
-        ssRich = HSLFTestDataSamples.getSlideShow("Single_Coloured_Page.ppt");
-    }
-
-    @AfterEach
-    void tearDown() throws IOException {
-        ssRich.close();
-        ss.close();
-    }
-
     /**
      * Test to ensure that getting the text works correctly
      */
     @Test
-    void testGetText() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParas = slideOne.getTextParagraphs();
-
-        assertEquals(2, textParas.size());
-
-        // Get text works with \n
-        assertEquals("This is a test title", HSLFTextParagraph.getText(textParas.get(0)));
-        assertEquals("This is a test subtitle\nThis is on page 1", HSLFTextParagraph.getText(textParas.get(1)));
+    void testGetText() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            HSLFSlide slideOne = ppt.getSlides().get(0);
+            List<List<HSLFTextParagraph>> textParas = slideOne.getTextParagraphs();
+
+            // Get text works with \n
+            String[] exp1 = { "This is a test title", "This is a test subtitle\nThis is on page 1" };
+            String[] act1 = textParas.stream().map(HSLFTextParagraph::getText).toArray(String[]::new);
+            assertArrayEquals(exp1, act1);
+
+            // Raw text has \r instead
+            String[] exp2 = { "This is a test title", "This is a test subtitle\rThis is on page 1" };
+            String[] act2 = textParas.stream().map(HSLFTextParagraph::getRawText).toArray(String[]::new);
+            assertArrayEquals(exp2, act2);
+        }
 
-        // Raw text has \r instead
-        assertEquals("This is a test title", HSLFTextParagraph.getRawText(textParas.get(0)));
-        assertEquals("This is a test subtitle\rThis is on page 1", HSLFTextParagraph.getRawText(textParas.get(1)));
+        // Now check on a rich text run
+        try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) {
+            List<List<HSLFTextParagraph>> textParas = ppt.getSlides().get(0).getTextParagraphs();
 
+            String[] exp1 = { "This is a title, it\u2019s in black", "This is the subtitle, in bold\nThis bit is blue and italic\nThis bit is red (normal)" };
+            String[] act1 = textParas.stream().map(HSLFTextParagraph::getText).toArray(String[]::new);
+            assertArrayEquals(exp1, act1);
 
-        // Now check on a rich text run
-        HSLFSlide slideOneR = ssRich.getSlides().get(0);
-        textParas = slideOneR.getTextParagraphs();
-
-        assertEquals(2, textParas.size());
-        assertEquals("This is a title, it\u2019s in black", HSLFTextParagraph.getText(textParas.get(0)));
-        assertEquals("This is the subtitle, in bold\nThis bit is blue and italic\nThis bit is red (normal)", HSLFTextParagraph.getText(textParas.get(1)));
-        assertEquals("This is a title, it\u2019s in black", HSLFTextParagraph.getRawText(textParas.get(0)));
-        assertEquals("This is the subtitle, in bold\rThis bit is blue and italic\rThis bit is red (normal)", HSLFTextParagraph.getRawText(textParas.get(1)));
+            String[] exp2 = { "This is a title, it\u2019s in black", "This is the subtitle, in bold\rThis bit is blue and italic\rThis bit is red (normal)" };
+            String[] act2 = textParas.stream().map(HSLFTextParagraph::getRawText).toArray(String[]::new);
+            assertArrayEquals(exp2, act2);
+        }
     }
 
     /**
      * Test to ensure changing non rich text bytes->bytes works correctly
      */
     @Test
-    void testSetText() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textRuns = slideOne.getTextParagraphs();
-        HSLFTextParagraph run = textRuns.get(0).get(0);
-        HSLFTextRun tr =  run.getTextRuns().get(0);
-
-        // Check current text
-        assertEquals("This is a test title", tr.getRawText());
-
-        // Change
-        String changeTo = "New test title";
-        tr.setText(changeTo);
-        assertEquals(changeTo, tr.getRawText());
-
-        // Ensure trailing \n's are NOT stripped, it is legal to set a text with a trailing '\r'
-        tr.setText(changeTo + "\n");
-        assertEquals(changeTo + "\r", tr.getRawText());
+    void testSetText() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            List<List<HSLFTextParagraph>> textRuns = ppt.getSlides().get(0).getTextParagraphs();
+            HSLFTextParagraph run = textRuns.get(0).get(0);
+            HSLFTextRun tr = run.getTextRuns().get(0);
+
+            // Check current text
+            assertEquals("This is a test title", tr.getRawText());
+
+            // Change
+            String changeTo = "New test title";
+            tr.setText(changeTo);
+            assertEquals(changeTo, tr.getRawText());
+
+            // Ensure trailing \n's are NOT stripped, it is legal to set a text with a trailing '\r'
+            tr.setText(changeTo + "\n");
+            assertEquals(changeTo + "\r", tr.getRawText());
+        }
     }
 
     /**
@@ -121,74 +116,53 @@ public final class TestTextRun {
      */
     @SuppressWarnings("unused")
     @Test
-    void testAdvancedSetText() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<HSLFTextParagraph> paras = slideOne.getTextParagraphs().get(0);
-        HSLFTextParagraph para = paras.get(0);
-
-        TextHeaderAtom tha = null;
-        TextBytesAtom tba = null;
-        TextCharsAtom tca = null;
-        for ( org.apache.poi.hslf.record.Record r : para.getRecords()) {
-            if (r instanceof TextHeaderAtom) tha = (TextHeaderAtom)r;
-            else if (r instanceof TextBytesAtom) tba = (TextBytesAtom)r;
-            else if (r instanceof TextCharsAtom) tca = (TextCharsAtom)r;
-        }
-
-        // Bytes -> Bytes
-        assertNull(tca);
-        assertNotNull(tba);
-        // assertFalse(run._isUnicode);
-        assertEquals("This is a test title", para.getTextRuns().get(0).getRawText());
-
-        String changeBytesOnly = "New Test Title";
-        HSLFTextParagraph.setText(paras, changeBytesOnly);
-        para = paras.get(0);
-        tha = null; tba = null; tca = null;
-        for ( org.apache.poi.hslf.record.Record r : para.getRecords()) {
-            if (r instanceof TextHeaderAtom) tha = (TextHeaderAtom)r;
-            else if (r instanceof TextBytesAtom) tba = (TextBytesAtom)r;
-            else if (r instanceof TextCharsAtom) tca = (TextCharsAtom)r;
-        }
-
-        assertEquals(changeBytesOnly, HSLFTextParagraph.getRawText(paras));
-        assertNull(tca);
-        assertNotNull(tba);
-
-        // Bytes -> Chars
-        assertEquals(changeBytesOnly, HSLFTextParagraph.getRawText(paras));
-
-        String changeByteChar = "This is a test title with a '\u0121' g with a dot";
-        HSLFTextParagraph.setText(paras, changeByteChar);
-        para = paras.get(0);
-        tha = null; tba = null; tca = null;
-        for ( org.apache.poi.hslf.record.Record r : para.getRecords()) {
-            if (r instanceof TextHeaderAtom) tha = (TextHeaderAtom)r;
-            else if (r instanceof TextBytesAtom) tba = (TextBytesAtom)r;
-            else if (r instanceof TextCharsAtom) tca = (TextCharsAtom)r;
-        }
-
-        assertEquals(changeByteChar, HSLFTextParagraph.getRawText(paras));
-        assertNotNull(tca);
-        assertNull(tba);
-
-        // Chars -> Chars
-        assertNotNull(tca);
-        assertEquals(changeByteChar, HSLFTextParagraph.getRawText(paras));
-
-        String changeCharChar = "This is a test title with a '\u0147' N with a hat";
-        HSLFTextParagraph.setText(paras, changeCharChar);
-        para = paras.get(0);
-        tha = null; tba = null; tca = null;
-        for ( org.apache.poi.hslf.record.Record r : para.getRecords()) {
-            if (r instanceof TextHeaderAtom) tha = (TextHeaderAtom)r;
-            else if (r instanceof TextBytesAtom) tba = (TextBytesAtom)r;
-            else if (r instanceof TextCharsAtom) tca = (TextCharsAtom)r;
+    void testAdvancedSetText() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            List<HSLFTextParagraph> paras = ppt.getSlides().get(0).getTextParagraphs().get(0);
+            final HSLFTextParagraph para = paras.get(0);
+
+            final TextBytesAtom[] tba = { null };
+            final TextCharsAtom[] tca = { null };
+            Runnable extract = () -> {
+                tba[0] = null;
+                tca[0] = null;
+                Stream.of(para.getRecords()).forEach(r -> {
+                    if (r instanceof TextBytesAtom) tba[0] = (TextBytesAtom) r;
+                    else if (r instanceof TextCharsAtom) tca[0] = (TextCharsAtom) r;
+                });
+            };
+
+            // Bytes -> Bytes
+            extract.run();
+            assertNull(tca[0]);
+            assertNotNull(tba);
+            // assertFalse(run._isUnicode);
+            assertEquals("This is a test title", para.getTextRuns().get(0).getRawText());
+
+            String changeBytesOnly = "New Test Title";
+            HSLFTextParagraph.setText(paras, changeBytesOnly);
+            extract.run();
+            assertEquals(changeBytesOnly, HSLFTextParagraph.getRawText(paras));
+            assertNull(tca[0]);
+            assertNotNull(tba);
+
+            // Bytes -> Chars
+            String changeByteChar = "This is a test title with a '\u0121' g with a dot";
+            HSLFTextParagraph.setText(paras, changeByteChar);
+            extract.run();
+            assertEquals(changeByteChar, HSLFTextParagraph.getRawText(paras));
+            assertNotNull(tca[0]);
+            assertNull(tba[0]);
+
+            // Chars -> Chars
+            String changeCharChar = "This is a test title with a '\u0147' N with a hat";
+            HSLFTextParagraph.setText(paras, changeCharChar);
+            extract.run();
+
+            assertEquals(changeCharChar, HSLFTextParagraph.getRawText(paras));
+            assertNotNull(tca[0]);
+            assertNull(tba[0]);
         }
-
-        assertEquals(changeCharChar, HSLFTextParagraph.getRawText(paras));
-        assertNotNull(tca);
-        assertNull(tba);
     }
 
     /**
@@ -196,61 +170,63 @@ public final class TestTextRun {
      *  set up for it
      */
     @Test
-    void testGetRichTextNonRich() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
+    void testGetRichTextNonRich() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            List<List<HSLFTextParagraph>> textParass = ppt.getSlides().get(0).getTextParagraphs();
 
-        assertEquals(2, textParass.size());
+            assertEquals(2, textParass.size());
 
-        List<HSLFTextParagraph> trA = textParass.get(0);
-        List<HSLFTextParagraph> trB = textParass.get(1);
+            List<HSLFTextParagraph> trA = textParass.get(0);
+            List<HSLFTextParagraph> trB = textParass.get(1);
 
-        assertEquals(1, trA.size());
-        assertEquals(2, trB.size());
+            assertEquals(1, trA.size());
+            assertEquals(2, trB.size());
 
-        HSLFTextRun rtrA = trA.get(0).getTextRuns().get(0);
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrA = trA.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
 
-        assertEquals(HSLFTextParagraph.getRawText(trA), rtrA.getRawText());
-        assertEquals(HSLFTextParagraph.getRawText(trB.subList(0, 1)), rtrB.getRawText());
+            assertEquals(HSLFTextParagraph.getRawText(trA), rtrA.getRawText());
+            assertEquals(HSLFTextParagraph.getRawText(trB.subList(0, 1)), rtrB.getRawText());
+        }
     }
 
     /**
      * Tests to ensure that the rich text runs are built up correctly
      */
     @Test
-    void testGetRichText() {
-        HSLFSlide slideOne = ssRich.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
+    void testGetRichText() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) {
+            List<List<HSLFTextParagraph>> textParass = ppt.getSlides().get(0).getTextParagraphs();
 
-        assertEquals(2, textParass.size());
+            assertEquals(2, textParass.size());
 
-        List<HSLFTextParagraph> trA = textParass.get(0);
-        List<HSLFTextParagraph> trB = textParass.get(1);
+            List<HSLFTextParagraph> trA = textParass.get(0);
+            List<HSLFTextParagraph> trB = textParass.get(1);
 
-        assertEquals(1, trA.size());
-        assertEquals(3, trB.size());
+            assertEquals(1, trA.size());
+            assertEquals(3, trB.size());
 
-        HSLFTextRun rtrA = trA.get(0).getTextRuns().get(0);
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
-        HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
-        HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
+            HSLFTextRun rtrA = trA.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
+            HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
 
-        assertEquals(HSLFTextParagraph.getRawText(trA), rtrA.getRawText());
+            assertEquals(HSLFTextParagraph.getRawText(trA), rtrA.getRawText());
 
-        String trBstr = HSLFTextParagraph.getRawText(trB);
-        assertEquals(trBstr.substring(0, 30), rtrB.getRawText());
-        assertEquals(trBstr.substring(30,58), rtrC.getRawText());
-        assertEquals(trBstr.substring(58,82), rtrD.getRawText());
+            String trBstr = HSLFTextParagraph.getRawText(trB);
+            assertEquals(trBstr.substring(0, 30), rtrB.getRawText());
+            assertEquals(trBstr.substring(30, 58), rtrC.getRawText());
+            assertEquals(trBstr.substring(58, 82), rtrD.getRawText());
 
-        // Same paragraph styles
-        assertEquals(trB.get(0).getParagraphStyle(), trB.get(1).getParagraphStyle());
-        assertEquals(trB.get(0).getParagraphStyle(), trB.get(2).getParagraphStyle());
+            // Same paragraph styles
+            assertEquals(trB.get(0).getParagraphStyle(), trB.get(1).getParagraphStyle());
+            assertEquals(trB.get(0).getParagraphStyle(), trB.get(2).getParagraphStyle());
 
-        // Different char styles
-        assertNotEquals(rtrB.getCharacterStyle(), rtrC.getCharacterStyle());
-        assertNotEquals(rtrB.getCharacterStyle(), rtrD.getCharacterStyle());
-        assertNotEquals(rtrC.getCharacterStyle(), rtrD.getCharacterStyle());
+            // Different char styles
+            assertNotEquals(rtrB.getCharacterStyle(), rtrC.getCharacterStyle());
+            assertNotEquals(rtrB.getCharacterStyle(), rtrD.getCharacterStyle());
+            assertNotEquals(rtrC.getCharacterStyle(), rtrD.getCharacterStyle());
+        }
     }
 
     /**
@@ -258,20 +234,20 @@ public final class TestTextRun {
      *  ensuring that everything stays with the same default styling
      */
     @Test
-    void testSetTextWhereNotRich() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
-        List<HSLFTextParagraph> trB = textParass.get(0);
-        assertEquals(1, trB.size());
-
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
-        assertEquals(HSLFTextParagraph.getText(trB), rtrB.getRawText());
-
-        // Change text via normal
-        HSLFTextParagraph.setText(trB, "Test Foo Test");
-        rtrB = trB.get(0).getTextRuns().get(0);
-        assertEquals("Test Foo Test", HSLFTextParagraph.getRawText(trB));
-        assertEquals("Test Foo Test", rtrB.getRawText());
+    void testSetTextWhereNotRich() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            List<HSLFTextParagraph> trB = ppt.getSlides().get(0).getTextParagraphs().get(0);
+            assertEquals(1, trB.size());
+
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            assertEquals(HSLFTextParagraph.getText(trB), rtrB.getRawText());
+
+            // Change text via normal
+            HSLFTextParagraph.setText(trB, "Test Foo Test");
+            rtrB = trB.get(0).getTextRuns().get(0);
+            assertEquals("Test Foo Test", HSLFTextParagraph.getRawText(trB));
+            assertEquals("Test Foo Test", rtrB.getRawText());
+        }
     }
 
     /**
@@ -279,48 +255,48 @@ public final class TestTextRun {
      *  sets everything to the same styling
      */
     @Test
-    void testSetTextWhereRich() {
-        HSLFSlide slideOne = ssRich.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
-        List<HSLFTextParagraph> trB = textParass.get(1);
-        assertEquals(3, trB.size());
-
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
-        HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
-        HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
-        TextPropCollection tpBP = rtrB.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpBC = rtrB.getCharacterStyle();
-        TextPropCollection tpCP = rtrC.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpCC = rtrC.getCharacterStyle();
-        TextPropCollection tpDP = rtrD.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpDC = rtrD.getCharacterStyle();
+    void testSetTextWhereRich() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) {
+            List<HSLFTextParagraph> trB = ppt.getSlides().get(0).getTextParagraphs().get(1);
+            assertEquals(3, trB.size());
+
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
+            HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
+            TextPropCollection tpBP = rtrB.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpBC = rtrB.getCharacterStyle();
+            TextPropCollection tpCP = rtrC.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpCC = rtrC.getCharacterStyle();
+            TextPropCollection tpDP = rtrD.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpDC = rtrD.getCharacterStyle();
 
 //      assertEquals(trB.getRawText().substring(0, 30), rtrB.getRawText());
-        assertNotNull(tpBP);
-        assertNotNull(tpBC);
-        assertNotNull(tpCP);
-        assertNotNull(tpCC);
-        assertNotNull(tpDP);
-        assertNotNull(tpDC);
-        assertEquals(tpBP,tpCP);
-        assertEquals(tpBP,tpDP);
-        assertEquals(tpCP,tpDP);
-        assertNotEquals(tpBC,tpCC);
-        assertNotEquals(tpBC,tpDC);
-        assertNotEquals(tpCC,tpDC);
-
-        // Change text via normal
-        HSLFTextParagraph.setText(trB, "Test Foo Test");
-
-        // Ensure now have first style
-        assertEquals(1, trB.get(0).getTextRuns().size());
-        rtrB = trB.get(0).getTextRuns().get(0);
-        assertEquals("Test Foo Test", HSLFTextParagraph.getRawText(trB));
-        assertEquals("Test Foo Test", rtrB.getRawText());
-        assertNotNull(rtrB.getCharacterStyle());
-        assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
-        assertEquals( tpBP, rtrB.getTextParagraph().getParagraphStyle() );
-        assertEquals( tpBC, rtrB.getCharacterStyle() );
+            assertNotNull(tpBP);
+            assertNotNull(tpBC);
+            assertNotNull(tpCP);
+            assertNotNull(tpCC);
+            assertNotNull(tpDP);
+            assertNotNull(tpDC);
+            assertEquals(tpBP, tpCP);
+            assertEquals(tpBP, tpDP);
+            assertEquals(tpCP, tpDP);
+            assertNotEquals(tpBC, tpCC);
+            assertNotEquals(tpBC, tpDC);
+            assertNotEquals(tpCC, tpDC);
+
+            // Change text via normal
+            HSLFTextParagraph.setText(trB, "Test Foo Test");
+
+            // Ensure now have first style
+            assertEquals(1, trB.get(0).getTextRuns().size());
+            rtrB = trB.get(0).getTextRuns().get(0);
+            assertEquals("Test Foo Test", HSLFTextParagraph.getRawText(trB));
+            assertEquals("Test Foo Test", rtrB.getRawText());
+            assertNotNull(rtrB.getCharacterStyle());
+            assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
+            assertEquals(tpBP, rtrB.getTextParagraph().getParagraphStyle());
+            assertEquals(tpBC, rtrB.getCharacterStyle());
+        }
     }
 
     /**
@@ -328,25 +304,26 @@ public final class TestTextRun {
      *  in a rich text run, that doesn't happen to actually be rich
      */
     @Test
-    void testChangeTextInRichTextRunNonRich() {
-        HSLFSlide slideOne = ss.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textRuns = slideOne.getTextParagraphs();
-        List<HSLFTextParagraph> trB = textRuns.get(1);
-        assertEquals(1, trB.get(0).getTextRuns().size());
-
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
-        assertEquals(HSLFTextParagraph.getRawText(trB.subList(0, 1)), rtrB.getRawText());
-        assertNotNull(rtrB.getCharacterStyle());
-        assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
-
-        // Change text via rich
-        rtrB.setText("Test Test Test");
-        assertEquals("Test Test Test", HSLFTextParagraph.getRawText(trB.subList(0, 1)));
-        assertEquals("Test Test Test", rtrB.getRawText());
-
-        // Will now have dummy props
-        assertNotNull(rtrB.getCharacterStyle());
-        assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
+    void testChangeTextInRichTextRunNonRich() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) {
+            List<List<HSLFTextParagraph>> textRuns = ppt.getSlides().get(0).getTextParagraphs();
+            List<HSLFTextParagraph> trB = textRuns.get(1);
+            assertEquals(1, trB.get(0).getTextRuns().size());
+
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            assertEquals(HSLFTextParagraph.getRawText(trB.subList(0, 1)), rtrB.getRawText());
+            assertNotNull(rtrB.getCharacterStyle());
+            assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
+
+            // Change text via rich
+            rtrB.setText("Test Test Test");
+            assertEquals("Test Test Test", HSLFTextParagraph.getRawText(trB.subList(0, 1)));
+            assertEquals("Test Test Test", rtrB.getRawText());
+
+            // Will now have dummy props
+            assertNotNull(rtrB.getCharacterStyle());
+            assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
+        }
     }
 
     /**
@@ -354,76 +331,78 @@ public final class TestTextRun {
      *  correctly
      */
     @Test
-    void testChangeTextInRichTextRun() {
-        HSLFSlide slideOne = ssRich.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
-        List<HSLFTextParagraph> trB = textParass.get(1);
-        assertEquals(3, trB.size());
-
-        // We start with 3 text runs, each with their own set of styles,
-        // but all sharing the same paragraph styles
-        HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
-        HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
-        HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
-        TextPropCollection tpBP = rtrB.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpBC = rtrB.getCharacterStyle();
-        TextPropCollection tpCP = rtrC.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpCC = rtrC.getCharacterStyle();
-        TextPropCollection tpDP = rtrD.getTextParagraph().getParagraphStyle();
-        TextPropCollection tpDC = rtrD.getCharacterStyle();
-
-        // Check text and stylings
-        assertEquals(HSLFTextParagraph.getRawText(trB).substring(0, 30), rtrB.getRawText());
-        assertNotNull(tpBP);
-        assertNotNull(tpBC);
-        assertNotNull(tpCP);
-        assertNotNull(tpCC);
-        assertNotNull(tpDP);
-        assertNotNull(tpDC);
-        assertEquals(tpBP, tpCP);
-        assertEquals(tpBP, tpDP);
-        assertEquals(tpCP, tpDP);
-        assertNotEquals(tpBC, tpCC);
-        assertNotEquals(tpBC, tpDC);
-        assertNotEquals(tpCC, tpDC);
-
-        // Check text in the rich runs
-        assertEquals("This is the subtitle, in bold\r", rtrB.getRawText());
-        assertEquals("This bit is blue and italic\r", rtrC.getRawText());
-        assertEquals("This bit is red (normal)", rtrD.getRawText());
-
-        String newBText = "New Subtitle, will still be bold\n";
-        String newCText = "New blue and italic text\n";
-        String newDText = "Funky new normal red text";
-        rtrB.setText(newBText);
-        rtrC.setText(newCText);
-        rtrD.setText(newDText);
-        HSLFTextParagraph.storeText(trB);
-
-        assertEquals(newBText.replace('\n','\r'), rtrB.getRawText());
-        assertEquals(newCText.replace('\n','\r'), rtrC.getRawText());
-        assertEquals(newDText.replace('\n','\r'), rtrD.getRawText());
-
-        assertEquals(newBText.replace('\n','\r') + newCText.replace('\n','\r') + newDText.replace('\n','\r'), HSLFTextParagraph.getRawText(trB));
-
-        // The styles should have been updated for the new sizes
-        assertEquals(newBText.length(), tpBC.getCharactersCovered());
-        assertEquals(newCText.length(), tpCC.getCharactersCovered());
-        assertEquals(newDText.length()+1, tpDC.getCharactersCovered()); // Last one is always one larger
-
-        // Paragraph style should be sum of text length
-        assertEquals(
-            newBText.length() + newCText.length() + newDText.length() +1,
-            tpBP.getCharactersCovered() + tpCP.getCharactersCovered() + tpDP.getCharactersCovered()
-        );
-
-        // Check stylings still as expected
-        TextPropCollection ntpBC = rtrB.getCharacterStyle();
-        TextPropCollection ntpCC = rtrC.getCharacterStyle();
-        TextPropCollection ntpDC = rtrD.getCharacterStyle();
-        assertEquals(tpBC.getTextPropList(), ntpBC.getTextPropList());
-        assertEquals(tpCC.getTextPropList(), ntpCC.getTextPropList());
-        assertEquals(tpDC.getTextPropList(), ntpDC.getTextPropList());
+    void testChangeTextInRichTextRun() throws IOException {
+        try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) {
+            HSLFSlide slideOne = ppt.getSlides().get(0);
+            List<List<HSLFTextParagraph>> textParass = slideOne.getTextParagraphs();
+            List<HSLFTextParagraph> trB = textParass.get(1);
+            assertEquals(3, trB.size());
+
+            // We start with 3 text runs, each with their own set of styles,
+            // but all sharing the same paragraph styles
+            HSLFTextRun rtrB = trB.get(0).getTextRuns().get(0);
+            HSLFTextRun rtrC = trB.get(1).getTextRuns().get(0);
+            HSLFTextRun rtrD = trB.get(2).getTextRuns().get(0);
+            TextPropCollection tpBP = rtrB.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpBC = rtrB.getCharacterStyle();
+            TextPropCollection tpCP = rtrC.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpCC = rtrC.getCharacterStyle();
+            TextPropCollection tpDP = rtrD.getTextParagraph().getParagraphStyle();
+            TextPropCollection tpDC = rtrD.getCharacterStyle();
+
+            // Check text and stylings
+            assertEquals(HSLFTextParagraph.getRawText(trB).substring(0, 30), rtrB.getRawText());
+            assertNotNull(tpBP);
+            assertNotNull(tpBC);
+            assertNotNull(tpCP);
+            assertNotNull(tpCC);
+            assertNotNull(tpDP);
+            assertNotNull(tpDC);
+            assertEquals(tpBP, tpCP);
+            assertEquals(tpBP, tpDP);
+            assertEquals(tpCP, tpDP);
+            assertNotEquals(tpBC, tpCC);
+            assertNotEquals(tpBC, tpDC);
+            assertNotEquals(tpCC, tpDC);
+
+            // Check text in the rich runs
+            assertEquals("This is the subtitle, in bold\r", rtrB.getRawText());
+            assertEquals("This bit is blue and italic\r", rtrC.getRawText());
+            assertEquals("This bit is red (normal)", rtrD.getRawText());
+
+            String newBText = "New Subtitle, will still be bold\n";
+            String newCText = "New blue and italic text\n";
+            String newDText = "Funky new normal red text";
+            rtrB.setText(newBText);
+            rtrC.setText(newCText);
+            rtrD.setText(newDText);
+            HSLFTextParagraph.storeText(trB);
+
+            assertEquals(newBText.replace('\n', '\r'), rtrB.getRawText());
+            assertEquals(newCText.replace('\n', '\r'), rtrC.getRawText());
+            assertEquals(newDText.replace('\n', '\r'), rtrD.getRawText());
+
+            assertEquals(newBText.replace('\n', '\r') + newCText.replace('\n', '\r') + newDText.replace('\n', '\r'), HSLFTextParagraph.getRawText(trB));
+
+            // The styles should have been updated for the new sizes
+            assertEquals(newBText.length(), tpBC.getCharactersCovered());
+            assertEquals(newCText.length(), tpCC.getCharactersCovered());
+            assertEquals(newDText.length() + 1, tpDC.getCharactersCovered()); // Last one is always one larger
+
+            // Paragraph style should be sum of text length
+            assertEquals(
+                newBText.length() + newCText.length() + newDText.length() + 1,
+                tpBP.getCharactersCovered() + tpCP.getCharactersCovered() + tpDP.getCharactersCovered()
+            );
+
+            // Check stylings still as expected
+            TextPropCollection ntpBC = rtrB.getCharacterStyle();
+            TextPropCollection ntpCC = rtrC.getCharacterStyle();
+            TextPropCollection ntpDC = rtrD.getCharacterStyle();
+            assertEquals(tpBC.getTextPropList(), ntpBC.getTextPropList());
+            assertEquals(tpCC.getTextPropList(), ntpCC.getTextPropList());
+            assertEquals(tpDC.getTextPropList(), ntpDC.getTextPropList());
+        }
     }
 
 
@@ -436,29 +415,27 @@ public final class TestTextRun {
      */
     @Test
     void testBug41015() throws IOException {
-        List<HSLFTextRun> rt;
-
-        HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("bug-41015.ppt");
-        HSLFSlide sl = ppt.getSlides().get(0);
-        List<List<HSLFTextParagraph>> textParass = sl.getTextParagraphs();
-        assertEquals(2, textParass.size());
-
-        List<HSLFTextParagraph> textParas = textParass.get(0);
-        rt = textParass.get(0).get(0).getTextRuns();
-        assertEquals(1, rt.size());
-        assertEquals(0, textParass.get(0).get(0).getIndentLevel());
-        assertEquals("sdfsdfsdf", rt.get(0).getRawText());
-
-        textParas = textParass.get(1);
-        String[] texts = {"Sdfsdfsdf\r", "Dfgdfg\r", "Dfgdfgdfg\r", "Sdfsdfs\r", "Sdfsdf\r"};
-        int[] indents = {0, 0, 0, 1, 1};
-        int i=0;
-        for (HSLFTextParagraph p : textParas) {
-            assertEquals(texts[i], p.getTextRuns().get(0).getRawText());
-            assertEquals(indents[i], p.getIndentLevel());
-            i++;
+        try (HSLFSlideShow ppt = getSlideShow("bug-41015.ppt")) {
+            HSLFSlide sl = ppt.getSlides().get(0);
+            List<List<HSLFTextParagraph>> textParass = sl.getTextParagraphs();
+            assertEquals(2, textParass.size());
+
+            List<HSLFTextParagraph> textParas = textParass.get(0);
+            List<HSLFTextRun> rt = textParass.get(0).get(0).getTextRuns();
+            assertEquals(1, rt.size());
+            assertEquals(0, textParass.get(0).get(0).getIndentLevel());
+            assertEquals("sdfsdfsdf", rt.get(0).getRawText());
+
+            textParas = textParass.get(1);
+            String[] texts = {"Sdfsdfsdf\r", "Dfgdfg\r", "Dfgdfgdfg\r", "Sdfsdfs\r", "Sdfsdf\r"};
+            int[] indents = {0, 0, 0, 1, 1};
+            int i = 0;
+            for (HSLFTextParagraph p : textParas) {
+                assertEquals(texts[i], p.getTextRuns().get(0).getRawText());
+                assertEquals(indents[i], p.getIndentLevel());
+                i++;
+            }
         }
-        ppt.close();
     }
 
     /**
@@ -466,110 +443,106 @@ public final class TestTextRun {
      */
     @Test
     void testAddTextRun() throws IOException {
-        HSLFSlideShow ppt = new HSLFSlideShow();
-        HSLFSlide slide = ppt.createSlide();
-
-        assertEquals(0, slide.getTextParagraphs().size());
-
-        HSLFTextBox shape1 = new HSLFTextBox();
-        List<HSLFTextParagraph> run1 = shape1.getTextParagraphs();
-        shape1.setText("Text 1");
-        slide.addShape(shape1);
-
-        //The array of Slide's text runs must be updated when new text shapes are added.
-        List<List<HSLFTextParagraph>> runs = slide.getTextParagraphs();
-        assertNotNull(runs);
-        assertSame(run1, runs.get(0));
-
-        HSLFTextBox shape2 = new HSLFTextBox();
-        List<HSLFTextParagraph> run2 = shape2.getTextParagraphs();
-        shape2.setText("Text 2");
-        slide.addShape(shape2);
-
-        runs = slide.getTextParagraphs();
-        assertEquals(2, runs.size());
-
-        assertSame(run1, runs.get(0));
-        assertSame(run2, runs.get(1));
-
-        // as getShapes()
-        List<HSLFShape> sh = slide.getShapes();
-        assertEquals(2, sh.size());
-        assertTrue(sh.get(0) instanceof HSLFTextBox);
-        HSLFTextBox box1 = (HSLFTextBox)sh.get(0);
-        assertSame(run1, box1.getTextParagraphs());
-        HSLFTextBox box2 = (HSLFTextBox)sh.get(1);
-        assertSame(run2, box2.getTextParagraphs());
-
-        // test Table - a complex group of shapes containing text objects
-        HSLFSlide slide2 = ppt.createSlide();
-        assertTrue(slide2.getTextParagraphs().isEmpty());
-        HSLFTable table = new HSLFTable(2, 2);
-        slide2.addShape(table);
-        runs = slide2.getTextParagraphs();
-        assertNotNull(runs);
-        assertEquals(4, runs.size());
-        ppt.close();
+        try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+            HSLFSlide slide = ppt.createSlide();
+
+            assertEquals(0, slide.getTextParagraphs().size());
+
+            HSLFTextBox shape1 = new HSLFTextBox();
+            List<HSLFTextParagraph> run1 = shape1.getTextParagraphs();
+            shape1.setText("Text 1");
+            slide.addShape(shape1);
+
+            //The array of Slide's text runs must be updated when new text shapes are added.
+            List<List<HSLFTextParagraph>> runs = slide.getTextParagraphs();
+            assertNotNull(runs);
+            assertSame(run1, runs.get(0));
+
+            HSLFTextBox shape2 = new HSLFTextBox();
+            List<HSLFTextParagraph> run2 = shape2.getTextParagraphs();
+            shape2.setText("Text 2");
+            slide.addShape(shape2);
+
+            runs = slide.getTextParagraphs();
+            assertEquals(2, runs.size());
+
+            assertSame(run1, runs.get(0));
+            assertSame(run2, runs.get(1));
+
+            // as getShapes()
+            List<HSLFShape> sh = slide.getShapes();
+            assertEquals(2, sh.size());
+            assertTrue(sh.get(0) instanceof HSLFTextBox);
+            HSLFTextBox box1 = (HSLFTextBox) sh.get(0);
+            assertSame(run1, box1.getTextParagraphs());
+            HSLFTextBox box2 = (HSLFTextBox) sh.get(1);
+            assertSame(run2, box2.getTextParagraphs());
+
+            // test Table - a complex group of shapes containing text objects
+            HSLFSlide slide2 = ppt.createSlide();
+            assertTrue(slide2.getTextParagraphs().isEmpty());
+            HSLFTable table = new HSLFTable(2, 2);
+            slide2.addShape(table);
+            runs = slide2.getTextParagraphs();
+            assertNotNull(runs);
+            assertEquals(4, runs.size());
+        }
     }
 
     @Test
     void test48916() throws IOException {
-        HSLFSlideShow ppt1 = HSLFTestDataSamples.getSlideShow("SampleShow.ppt");
-        List<HSLFSlide> slides = ppt1.getSlides();
-        for(HSLFSlide slide : slides){
-            for(HSLFShape sh : slide.getShapes()){
-                if (!(sh instanceof HSLFTextShape)) continue;
-                HSLFTextShape tx = (HSLFTextShape)sh;
-                List<HSLFTextParagraph> paras = tx.getTextParagraphs();
-                //verify that records cached in  TextRun and EscherTextboxWrapper are the same
-                 org.apache.poi.hslf.record.Record[] runChildren = paras.get(0).getRecords();
-                 org.apache.poi.hslf.record.Record[] txboxChildren = tx.getEscherTextboxWrapper().getChildRecords();
-                assertEquals(runChildren.length, txboxChildren.length);
-                for(int i=0; i < txboxChildren.length; i++){
-                    assertSame(txboxChildren[i], runChildren[i]);
-                }
-                // caused NPE prior to fix of Bugzilla #48916
-                for (HSLFTextParagraph p : paras) {
-                    for (HSLFTextRun rt : p.getTextRuns()) {
-                        rt.setBold(true);
-                        rt.setFontColor(Color.RED);
+        try (HSLFSlideShow ppt1 = getSlideShow("SampleShow.ppt")) {
+            List<HSLFSlide> slides = ppt1.getSlides();
+            for (HSLFSlide slide : slides) {
+                for (HSLFShape sh : slide.getShapes()) {
+                    if (!(sh instanceof HSLFTextShape)) continue;
+                    HSLFTextShape tx = (HSLFTextShape) sh;
+                    List<HSLFTextParagraph> paras = tx.getTextParagraphs();
+                    //verify that records cached in  TextRun and EscherTextboxWrapper are the same
+                    org.apache.poi.hslf.record.Record[] runChildren = paras.get(0).getRecords();
+                    org.apache.poi.hslf.record.Record[] txboxChildren = tx.getEscherTextboxWrapper().getChildRecords();
+                    assertEquals(runChildren.length, txboxChildren.length);
+                    for (int i = 0; i < txboxChildren.length; i++) {
+                        assertSame(txboxChildren[i], runChildren[i]);
+                    }
+                    // caused NPE prior to fix of Bugzilla #48916
+                    for (HSLFTextParagraph p : paras) {
+                        for (HSLFTextRun rt : p.getTextRuns()) {
+                            rt.setBold(true);
+                            rt.setFontColor(Color.RED);
+                        }
                     }
+                    // tx.storeText();
                 }
-                // tx.storeText();
             }
-        }
 
-        HSLFSlideShow ppt2 = HSLFTestDataSamples.writeOutAndReadBack(ppt1);
-        for(HSLFSlide slide : ppt2.getSlides()){
-            for(HSLFShape sh : slide.getShapes()){
-                if(sh instanceof HSLFTextShape){
-                    HSLFTextShape tx = (HSLFTextShape)sh;
-                    List<HSLFTextParagraph> run = tx.getTextParagraphs();
-                    HSLFTextRun rt = run.get(0).getTextRuns().get(0);
-                    assertTrue(rt.isBold());
-                    assertEquals(Color.RED, getColor(rt.getFontColor()));
-                }
+            try (HSLFSlideShow ppt2 = HSLFTestDataSamples.writeOutAndReadBack(ppt1)) {
+                List<HSLFTextRun> runs = ppt2.getSlides().stream()
+                    .flatMap(s -> s.getShapes().stream())
+                    .filter(s -> s instanceof HSLFTextShape)
+                    .map(s -> ((HSLFTextShape) s).getTextParagraphs().get(0).getTextRuns().get(0))
+                    .collect(Collectors.toList());
+
+                assertFalse(runs.isEmpty());
+                assertTrue(runs.stream().allMatch(HSLFTextRun::isBold));
+                assertTrue(runs.stream().map(HSLFTextRun::getFontColor)
+                    .map(BaseTestSlideShow::getColor).allMatch(Color.RED::equals));
             }
         }
-        ppt2.close();
-        ppt1.close();
     }
 
     @Test
     void test52244() throws IOException {
-        HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("52244.ppt");
-        HSLFSlide slide = ppt.getSlides().get(0);
+        try (HSLFSlideShow ppt = getSlideShow("52244.ppt")) {
+            HSLFSlide slide = ppt.getSlides().get(0);
 
-        int[] sizes = {36, 24, 12, 32, 12, 12};
+            List<HSLFTextRun> runs = slide.getTextParagraphs().stream().map(tp -> tp.get(0).getTextRuns().get(0)).collect(Collectors.toList());
+            assertTrue(runs.stream().map(HSLFTextRun::getFontFamily).allMatch("Arial"::equals));
 
-        int i=0;
-        for (List<HSLFTextParagraph> textParas : slide.getTextParagraphs()) {
-            HSLFTextRun first = textParas.get(0).getTextRuns().get(0);
-            assertEquals("Arial", first.getFontFamily());
-            assertNotNull(first.getFontSize());
-            assertEquals(sizes[i++], first.getFontSize().intValue());
+            int[] exp = {36, 24, 12, 32, 12, 12};
+            int[] act = runs.stream().map(HSLFTextRun::getFontSize).mapToInt(Double::intValue).toArray();
+            assertArrayEquals(exp, act);
         }
-        ppt.close();
     }
 
     @Test
@@ -583,4 +556,66 @@ public final class TestTextRun {
             assertEquals("\npara", title.getText());
         }
     }
+
+    @Test
+    void datetimeFormats() throws IOException {
+        LocalDateTime ldt = LocalDateTime.of(2012, 3, 4, 23, 45, 26);
+        final Map<Locale, String[]> formats = new HashMap<>();
+        formats.put(Locale.GERMANY, new String[]{
+            "04.03.2012",
+            "Sonntag, 4. M\u00e4rz 2012",
+            "04/03/12",
+            "4. M\u00e4rz 2012",
+            "12-03-04",
+            "M\u00e4rz 12",
+            "M\u00e4r-12",
+            "04.03.12  23:45",
+            "04.03.12  23:45:26",
+            "23:45",
+            "23:45:26",
+            "11:45",
+            "11:45:26"
+        });
+        formats.put(Locale.US, new String[]{
+            "03/04/2012",
+            "Sunday, March 4, 2012",
+            "4 March 2012",
+            "March 04, 2012",
+            "4-Mar-12",
+            "March 12",
+            "Mar-12",
+            "3/4/12  11:45 PM",
+            "3/4/12  11:45:26 PM",
+            "23:45",
+            "23:45:26",
+            "11:45 PM",
+            "11:45:26 PM"
+        });
+
+
+        try (HSLFSlideShow ppt = getSlideShow("datetime.ppt")) {
+            List<HSLFTextShape> shapes = ppt.getSlides().get(0).getShapes()
+                .stream().map(HSLFTextShape.class::cast).collect(Collectors.toList());
+
+            int[] expFormatId = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
+            int[] actFormatId = shapes.stream().flatMap(tp -> Stream.of(tp.getTextParagraphs().get(0).getRecords()))
+                .filter(r -> r instanceof DateTimeMCAtom)
+                .mapToInt(r -> ((DateTimeMCAtom)r).getIndex()).toArray();
+            assertArrayEquals(expFormatId, actFormatId);
+
+            List<HSLFShapePlaceholderDetails> phs = shapes.stream().map(HSLFSimpleShape::getPlaceholderDetails).collect(Collectors.toList());
+
+            for (Map.Entry<Locale,String[]> me : formats.entrySet()) {
+                LocaleUtil.setUserLocale(me.getKey());
+
+                // refresh internal members
+                phs.forEach(PlaceholderDetails::getPlaceholder);
+
+                String[] actDate = phs.stream().map(PlaceholderDetails::getDateFormat).map(ldt::format).toArray(String[]::new);
+                assertArrayEquals(me.getValue(), actDate);
+            }
+        } finally {
+            LocaleUtil.resetUserLocale();
+        }
+    }
 }
index 2dbbfef69ba28b531c768c1aa8d9de46ef41d6db..01ab9dc0b7b32cea824cab3dc0a220fd796e7bb3 100644 (file)
@@ -45,7 +45,7 @@ public class DrawMasterSheet extends DrawSheet {
             // in XSLF, slidenumber and date shapes aren't marked as placeholders opposed to HSLF
             Placeholder ph = ((SimpleShape<?,?>)shape).getPlaceholder();
             if (ph != null) {
-                return slide.getDisplayPlaceholder(ph);
+                return slide.getDisplayPlaceholder((SimpleShape<?, ?>)shape);
             }
         }
         return slide.getFollowMasterGraphics();
index b49f7276b8563f31adf105cbf1fc22ed09d64b45..8d8aa8213d1d8874e6a7f2d19c0e1893f47619e8 100644 (file)
@@ -17,6 +17,8 @@
 
 package org.apache.poi.sl.draw;
 
+import static org.apache.logging.log4j.util.Unbox.box;
+
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics2D;
@@ -30,8 +32,10 @@ import java.io.InvalidObjectException;
 import java.text.AttributedCharacterIterator;
 import java.text.AttributedCharacterIterator.Attribute;
 import java.text.AttributedString;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Calendar;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -47,8 +51,8 @@ import org.apache.poi.sl.usermodel.Hyperlink;
 import org.apache.poi.sl.usermodel.Insets2D;
 import org.apache.poi.sl.usermodel.PaintStyle;
 import org.apache.poi.sl.usermodel.PlaceableShape;
-import org.apache.poi.sl.usermodel.ShapeContainer;
-import org.apache.poi.sl.usermodel.Sheet;
+import org.apache.poi.sl.usermodel.PlaceholderDetails;
+import org.apache.poi.sl.usermodel.SimpleShape;
 import org.apache.poi.sl.usermodel.Slide;
 import org.apache.poi.sl.usermodel.TextParagraph;
 import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle;
@@ -61,8 +65,6 @@ import org.apache.poi.util.Internal;
 import org.apache.poi.util.LocaleUtil;
 import org.apache.poi.util.Units;
 
-import static org.apache.logging.log4j.util.Unbox.box;
-
 public class DrawTextParagraph implements Drawable {
     private static final Logger LOG = LogManager.getLogger(DrawTextParagraph.class);
 
@@ -397,11 +399,31 @@ public class DrawTextParagraph implements Drawable {
     }
 
     protected String getRenderableText(Graphics2D graphics, TextRun tr) {
-        if (tr.getFieldType() == FieldType.SLIDE_NUMBER) {
-            Slide<?,?> slide = (Slide<?,?>)graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
-            return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
+        FieldType ft = tr.getFieldType();
+        if (ft == null) {
+            return getRenderableText(tr);
+        }
+        if (!tr.getRawText().isEmpty()) {
+            switch (ft) {
+                case SLIDE_NUMBER: {
+                    Slide<?, ?> slide = (Slide<?, ?>) graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
+                    return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
+                }
+                case DATE_TIME: {
+                    PlaceholderDetails pd = ((SimpleShape<?, ?>) this.getParagraphShape()).getPlaceholderDetails();
+                    // refresh internal members
+                    pd.getPlaceholder();
+                    String uda = pd.getUserDate();
+                    if (uda != null) {
+                        return uda;
+                    }
+                    Calendar cal = LocaleUtil.getLocaleCalendar();
+                    LocalDateTime now = LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
+                    return now.format(pd.getDateFormat());
+                }
+            }
         }
-        return getRenderableText(tr);
+        return "";
     }
 
     @Internal
@@ -550,30 +572,8 @@ public class DrawTextParagraph implements Drawable {
     /**
      * Helper method for paint style relative to bounds, e.g. gradient paint
      */
-    @SuppressWarnings("rawtypes")
     private PlaceableShape<?,?> getParagraphShape() {
-        return new PlaceableShape(){
-            @Override
-            public ShapeContainer<?,?> getParent() { return null; }
-            @Override
-            public Rectangle2D getAnchor() { return paragraph.getParentShape().getAnchor(); }
-            @Override
-            public void setAnchor(Rectangle2D anchor) {}
-            @Override
-            public double getRotation() { return 0; }
-            @Override
-            public void setRotation(double theta) {}
-            @Override
-            public void setFlipHorizontal(boolean flip) {}
-            @Override
-            public void setFlipVertical(boolean flip) {}
-            @Override
-            public boolean getFlipHorizontal() { return false; }
-            @Override
-            public boolean getFlipVertical() { return false; }
-            @Override
-            public Sheet<?,?> getSheet() { return paragraph.getParentShape().getSheet(); }
-        };
+        return paragraph.getParentShape();
     }
 
     protected List<AttributedStringData> getAttributedString(Graphics2D graphics, StringBuilder text) {
@@ -671,9 +671,11 @@ public class DrawTextParagraph implements Drawable {
     }
 
     /**
-     * Processing the glyphs is done in two steps.
-     * <li>determine the font group - a text run can have different font groups. Depending on the chars,
-     * the correct font group needs to be used
+     * Processing the glyphs is done in two steps:
+     * <ul>
+     * <li>1. determine the font group - a text run can have different font groups.
+     * <li>2. Depending on the chars, the correct font group needs to be used
+     * </ul>
      *
      * @see <a href="https://blogs.msdn.microsoft.com/officeinteroperability/2013/04/22/office-open-xml-themes-schemes-and-fonts/">Office Open XML Themes, Schemes, and Fonts</a>
      */
index 7c4418f1645c473b65d9b7bb93244fe1a4f9cc01..5a4a9478206150210e3a61a0efffd039324dfd7e 100644 (file)
@@ -18,6 +18,8 @@
 package org.apache.poi.sl.usermodel;
 
 
+import java.time.format.DateTimeFormatter;
+
 /**
  * Extended details about placholders
  *
@@ -27,7 +29,7 @@ public interface PlaceholderDetails {
     enum PlaceholderSize {
         quarter, half, full
     }
-    
+
     Placeholder getPlaceholder();
 
     /**
@@ -40,13 +42,13 @@ public interface PlaceholderDetails {
      * @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);
 
     /**
@@ -66,4 +68,23 @@ public interface PlaceholderDetails {
      * @since POI 4.0.0
      */
     void setText(String text);
+
+
+    /**
+     * @return the stored / fixed user specified date
+     *
+     * @since POI 5.2.0
+     */
+    default String getUserDate() {
+        return null;
+    }
+
+    /**
+     * @return Get the date format for the datetime placeholder
+     *
+     * @since POI 5.2.0
+     */
+    default DateTimeFormatter getDateFormat() {
+        return DateTimeFormatter.ISO_LOCAL_DATE;
+    }
 }
index ade5b08ed670f6ee6a932756c183f201a12db209..bfd650cdf041055ec044814ca5bc40501c89fe92 100644 (file)
@@ -19,6 +19,8 @@ package org.apache.poi.sl.usermodel;
 
 import java.util.List;
 
+import org.apache.poi.util.Removal;
+
 @SuppressWarnings("unused")
 public interface Slide<
     S extends Shape<S,P>,
@@ -54,8 +56,29 @@ public interface Slide<
      * @param placeholder the placeholder type
      * @return {@code true} if the placeholder should be displayed/rendered
      * @since POI 3.16-beta2
+     *
+     * @deprecated in POI 5.2.0 - use {@link #getDisplayPlaceholder(SimpleShape)}
+     *
+     */
+    @Deprecated
+    @Removal(version = "6.0.0")
+    default boolean getDisplayPlaceholder(Placeholder placeholder) {
+        return false;
+    }
+
+
+    /**
+     * In XSLF, slidenumber and date shapes aren't marked as placeholders
+     * whereas in HSLF they are activated via a HeadersFooter configuration.
+     * This method is used to generalize that handling.
+     *
+     * @param placeholderRefShape the shape which references to the placeholder
+     * @return {@code true} if the placeholder should be displayed/rendered
+     * @since POI 5.2.0
      */
-    boolean getDisplayPlaceholder(Placeholder placeholder);
+    default boolean getDisplayPlaceholder(SimpleShape<?,?> placeholderRefShape) {
+        return false;
+    }
 
     /**
      * Sets the slide visibility
index 7ca4ecd882a04d09234b79b8f0809aa79580adb5..a27868cdf4ab031ce77f7eff01d32dadd450c52a 100644 (file)
Binary files a/poi/src/main/java9/module-info.class and b/poi/src/main/java9/module-info.class differ
diff --git a/test-data/slideshow/datetime.ppt b/test-data/slideshow/datetime.ppt
new file mode 100755 (executable)
index 0000000..2cf67ef
Binary files /dev/null and b/test-data/slideshow/datetime.ppt differ