From d3ff953cf71edfdbb409ab1110ef9976d88368c0 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 22 Nov 2021 00:01:31 +0000 Subject: [PATCH] #65694 - HSLF - handle date/time fields and formats git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1895248 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/xslf/usermodel/XSLFSlide.java | 8 +- .../apache/poi/hslf/model/HeadersFooters.java | 36 +- .../poi/hslf/record/DateTimeMCAtom.java | 125 +++ .../poi/hslf/record/HeadersFootersAtom.java | 30 +- .../poi/hslf/record/OEPlaceholderAtom.java | 6 +- .../apache/poi/hslf/record/RecordTypes.java | 2 +- .../usermodel/HSLFPlaceholderDetails.java | 5 +- .../HSLFShapePlaceholderDetails.java | 71 +- .../apache/poi/hslf/usermodel/HSLFSlide.java | 32 +- .../poi/hslf/util/LocaleDateFormat.java | 364 ++++++++ .../poi/hslf/usermodel/TestTextRun.java | 801 +++++++++--------- .../apache/poi/sl/draw/DrawMasterSheet.java | 2 +- .../apache/poi/sl/draw/DrawTextParagraph.java | 70 +- .../poi/sl/usermodel/PlaceholderDetails.java | 31 +- .../org/apache/poi/sl/usermodel/Slide.java | 25 +- poi/src/main/java9/module-info.class | Bin 3421 -> 3385 bytes test-data/slideshow/datetime.ppt | Bin 0 -> 114176 bytes 17 files changed, 1133 insertions(+), 475 deletions(-) create mode 100644 poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java create mode 100644 poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java create mode 100755 test-data/slideshow/datetime.ppt diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java index c126afdd7a..446ab8af7b 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java @@ -70,7 +70,7 @@ implements Slide { * Construct a SpreadsheetML slide from a package part * * @param part the package part holding the slide data, - * the content type must be application/vnd.openxmlformats-officedocument.slide+xml + * the content type must be {@code application/vnd.openxmlformats-officedocument.slide+xml} * * @since POI 3.14-Beta1 */ @@ -377,12 +377,6 @@ implements Slide { draw.draw(graphics); } - @Override - public boolean getDisplayPlaceholder(Placeholder placeholder) { - return false; - } - - @Override public void setHidden(boolean hidden) { CTSlide sld = getXmlObject(); diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java index 64b6dc6a35..51f647ea86 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java @@ -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 index 0000000000..5268b2a14d --- /dev/null +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java @@ -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> getGenericProperties() { + return GenericRecordUtil.getGenericProperties( + "position", this::getPosition, + "index", this::getIndex + ); + } + +} diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java index 552ec50ff0..25d2b39913 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java @@ -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> getGenericProperties() { return GenericRecordUtil.getGenericProperties( - "formatIndex", safeEnum(FormatIndex.values(), this::getFormatId), + "formatIndex", this::getFormatId, "flags", getBitsAsString(this::getMask, PLACEHOLDER_MASKS, PLACEHOLDER_NAMES) ); } diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java index bfc038747c..28247886a7 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java @@ -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.

* * 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. */ diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java index 295096da7e..f3cbf0b106 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java @@ -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), diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java index 906e51c44b..3c279e299f 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java @@ -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) { diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java index dc32a2d109..8a467dfedc 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java @@ -17,15 +17,29 @@ 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); + } } diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java index d63ea39721..6f205c9e57 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java @@ -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 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 index 0000000000..cad2d0541a --- /dev/null +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java @@ -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 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 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 formatFct; + + MapFormatBase(String datefmt, FormatStyle formatStyle, Function 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); + } + } +} diff --git a/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java b/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java index 0079c348dc..3e51fb8f5d 100644 --- a/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java +++ b/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java @@ -17,8 +17,10 @@ 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> textParas = slideOne.getTextParagraphs(); + void testGetText() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + HSLFSlide slideOne = ppt.getSlides().get(0); + List> 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))); - - // 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))); + // 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); + } // Now check on a rich text run - HSLFSlide slideOneR = ssRich.getSlides().get(0); - textParas = slideOneR.getTextParagraphs(); + try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) { + List> textParas = ppt.getSlides().get(0).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[] 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); + + 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> textRuns = slideOne.getTextParagraphs(); - HSLFTextParagraph run = textRuns.get(0).get(0); - HSLFTextRun tr = run.getTextRuns().get(0); + void testSetText() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + List> 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()); + // Check current text + assertEquals("This is a test title", tr.getRawText()); - // Change - String changeTo = "New test title"; - tr.setText(changeTo); - assertEquals(changeTo, 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()); + // 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 paras = slideOne.getTextParagraphs().get(0); - HSLFTextParagraph para = paras.get(0); + void testAdvancedSetText() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + List paras = ppt.getSlides().get(0).getTextParagraphs().get(0); + final 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; + 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]); } - - // 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; - } - - 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> textParass = slideOne.getTextParagraphs(); + void testGetRichTextNonRich() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + List> textParass = ppt.getSlides().get(0).getTextParagraphs(); - assertEquals(2, textParass.size()); + assertEquals(2, textParass.size()); - List trA = textParass.get(0); - List trB = textParass.get(1); + List trA = textParass.get(0); + List 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> textParass = slideOne.getTextParagraphs(); + void testGetRichText() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) { + List> textParass = ppt.getSlides().get(0).getTextParagraphs(); - assertEquals(2, textParass.size()); + assertEquals(2, textParass.size()); - List trA = textParass.get(0); - List trB = textParass.get(1); + List trA = textParass.get(0); + List 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> textParass = slideOne.getTextParagraphs(); - List trB = textParass.get(0); - assertEquals(1, trB.size()); + void testSetTextWhereNotRich() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + List 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()); + 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()); + // 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> textParass = slideOne.getTextParagraphs(); - List trB = textParass.get(1); - assertEquals(3, trB.size()); + void testSetTextWhereRich() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) { + List 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(); + 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); + 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"); + // 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() ); + // 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> textRuns = slideOne.getTextParagraphs(); - List trB = textRuns.get(1); - assertEquals(1, trB.get(0).getTextRuns().size()); + void testChangeTextInRichTextRunNonRich() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("basic_test_ppt_file.ppt")) { + List> textRuns = ppt.getSlides().get(0).getTextParagraphs(); + List 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()); + 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()); + // 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()); + // 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> textParass = slideOne.getTextParagraphs(); - List trB = textParass.get(1); - assertEquals(3, trB.size()); + void testChangeTextInRichTextRun() throws IOException { + try (HSLFSlideShow ppt = getSlideShow("Single_Coloured_Page.ppt")) { + HSLFSlide slideOne = ppt.getSlides().get(0); + List> textParass = slideOne.getTextParagraphs(); + List 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(); + // 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 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()); + // 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); + 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'), 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)); + 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 + // 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() - ); + // 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()); + // 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 rt; + try (HSLFSlideShow ppt = getSlideShow("bug-41015.ppt")) { + HSLFSlide sl = ppt.getSlides().get(0); + List> textParass = sl.getTextParagraphs(); + assertEquals(2, textParass.size()); - HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("bug-41015.ppt"); - HSLFSlide sl = ppt.getSlides().get(0); - List> textParass = sl.getTextParagraphs(); - assertEquals(2, textParass.size()); + List textParas = textParass.get(0); + List 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()); - List 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++; + 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(); + try (HSLFSlideShow ppt = new HSLFSlideShow()) { + HSLFSlide slide = ppt.createSlide(); - assertEquals(0, slide.getTextParagraphs().size()); + assertEquals(0, slide.getTextParagraphs().size()); - HSLFTextBox shape1 = new HSLFTextBox(); - List run1 = shape1.getTextParagraphs(); - shape1.setText("Text 1"); - slide.addShape(shape1); + HSLFTextBox shape1 = new HSLFTextBox(); + List 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> runs = slide.getTextParagraphs(); - assertNotNull(runs); - assertSame(run1, runs.get(0)); + //The array of Slide's text runs must be updated when new text shapes are added. + List> runs = slide.getTextParagraphs(); + assertNotNull(runs); + assertSame(run1, runs.get(0)); - HSLFTextBox shape2 = new HSLFTextBox(); - List run2 = shape2.getTextParagraphs(); - shape2.setText("Text 2"); - slide.addShape(shape2); + HSLFTextBox shape2 = new HSLFTextBox(); + List run2 = shape2.getTextParagraphs(); + shape2.setText("Text 2"); + slide.addShape(shape2); - runs = slide.getTextParagraphs(); - assertEquals(2, runs.size()); + runs = slide.getTextParagraphs(); + assertEquals(2, runs.size()); - assertSame(run1, runs.get(0)); - assertSame(run2, runs.get(1)); + assertSame(run1, runs.get(0)); + assertSame(run2, runs.get(1)); - // as getShapes() - List 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()); + // as getShapes() + List 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(); + // 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 slides = ppt1.getSlides(); - for(HSLFSlide slide : slides){ - for(HSLFShape sh : slide.getShapes()){ - if (!(sh instanceof HSLFTextShape)) continue; - HSLFTextShape tx = (HSLFTextShape)sh; - List 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 slides = ppt1.getSlides(); + for (HSLFSlide slide : slides) { + for (HSLFShape sh : slide.getShapes()) { + if (!(sh instanceof HSLFTextShape)) continue; + HSLFTextShape tx = (HSLFTextShape) sh; + List 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 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 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 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 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 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 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 phs = shapes.stream().map(HSLFSimpleShape::getPlaceholderDetails).collect(Collectors.toList()); + + for (Map.Entry 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(); + } + } } diff --git a/poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java b/poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java index 2dbbfef69b..01ab9dc0b7 100644 --- a/poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java +++ b/poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java @@ -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(); diff --git a/poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java b/poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java index b49f7276b8..8d8aa8213d 100644 --- a/poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java +++ b/poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java @@ -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); } - 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 ""; } @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 getAttributedString(Graphics2D graphics, StringBuilder text) { @@ -671,9 +671,11 @@ public class DrawTextParagraph implements Drawable { } /** - * Processing the glyphs is done in two steps. - *

  • 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: + *
      + *
    • 1. determine the font group - a text run can have different font groups. + *
    • 2. Depending on the chars, the correct font group needs to be used + *
    * * @see Office Open XML Themes, Schemes, and Fonts */ diff --git a/poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java b/poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java index 7c4418f164..5a4a947820 100644 --- a/poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java +++ b/poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java @@ -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; + } } diff --git a/poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java b/poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java index ade5b08ed6..bfd650cdf0 100644 --- a/poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java +++ b/poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java @@ -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, @@ -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)} + * */ - boolean getDisplayPlaceholder(Placeholder placeholder); + @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 + */ + default boolean getDisplayPlaceholder(SimpleShape placeholderRefShape) { + return false; + } /** * Sets the slide visibility diff --git a/poi/src/main/java9/module-info.class b/poi/src/main/java9/module-info.class index 7ca4ecd882a04d09234b79b8f0809aa79580adb5..a27868cdf4ab031ce77f7eff01d32dadd450c52a 100644 GIT binary patch literal 3385 zcmai$_nzBC5XXNTAV)4e-t^umA(3*)C3FIT90?EzF%UZJNo!kYSrU?L$JC?u-XZkP zBk%_gz*F!fd>D;f6iasQ^SQ5W?ab_yomu_!-(PXYIW!1D1_)hX)!Xkl1iKsEFA~LaX>aelj zI8ee8fvp*N%WpJ%FET9Ob+r{c^nJR7WdiF8g=}q=utH#U!K&kzuu5QQ&tyZzwSy%{ zfhFZYg^@n$#AO|qu;zU0u3xP>Ue%z_={fTBfH5^~OqH;n6gh&fKnWXrPi!64V?QWi zv%vB>yKXeSs2K!)7;9T%tH3hy&rDNdQaU8Ir#@$%nW>)=JJXEhm&|6QLrT4NC3Xw! zKHp|ne;{O5d@qh9E>A0xwP)KEi7V5vv(NR}I8;{bhZ0u_l!i8_1re$36WCm^U}kj9 z5h88vx^>TQdlFLx=A*>?L4hs9=4rvyE$zkK;13Dx7%7uU($xaSh2>1U*=Te^ZTTS; z>)Ha}dH+Fbt`}HeWVY+wjRG5rmU}xK$(OE{?2xXTs#*sEnn zqjxi{nhIlyV*=a9NpaUn+@3#rS=TH&P!cCDU@Pndu`hAw_%H*f<;Ss_b+yFF5n*OZ zdCv%IeG#*!-9U$ww0raCnuBP@4mD0$VB=6Gs39v+zy0pdm#-_31SWANpYY5&m5)ZG z#6tqRinmFun6r+j!cJ&SOFS}O(&BWgfM&*H0z1dpjAG^4DmlYXA!9y~MuY-3(jI>rUUBpd_z%C$(m`={n=J-UE<69^_gma zo%gXF4BiPQ@QuKpp`+G6#^$l!C3hBrSAWd*n>w&M$f61NToV$LohM!!I(O3H&B7HRMlv zAM|55UM|n{;^?dWkxx|5Cwg&gsNg)$<^e%3&*|R+Ttr_M;9^`tkN?F&emsdw=YdHq z=3h&h%b6>gCFW}4TC5{ZVgqv%^9Z)kC$NpVgSm@&8S@I}9_C)A!Q9V0z?^0tW?sX* zj(G#~Cg#n|qs-fwGtA@6JD7Jd?`EE2o@U<1e1Q2N^I_(r%*UBeGH01Kv%;(~=a??j zV+PES88chVv&^TN&oZB9&NE+PzQTNs`3CbX<~z*ym>)1dVt&H>jQIugD<-It=kP7Q V$B$Imb5!Y{>6gH-^jqNf{{aAB*t!4! literal 3421 zcmai$_mAg`xBE`vuUP2&C0t7+~gbrHy+SXc@ge2Qp>e74fy?35~ zKX?Ehgva188aL%y+3Y#{*}gM(H091*{qx^ne*-v(KTEjZz`{z{j$JkB`O3iZR#=5u9y0N;iAPL4YT_{ynd*M4JYkhhOqp;^ zR0)buBh(2Ef=8GpGzmVTMF5%O8B*e-%9wsgg*={$|cFb$}p~5 zPUN@^WksQv{To=2<<6RN63*$CoPry+T49h_Zs_~UO+EfTVPI{cMpd~6Ruzc6(7=*` zrIwR64j5QmiJUl5hrP6-(lagkVZH7Jb&EeIrdd;5>n{#|R#nnW!^pt0>1xwz2T41M z!Z=k`154QhyBD7=v1Kmh7VMrUy*S6$?RKJKPMGoy>?+(j(y(h`kS4v9<}FpLwO&a1 zMRhuj9XAbQ1EyxT5hc7~Q^Bg8+f^vx*Oq!+NzEnc<$$cIx$5)tyvv zwF({R>$#k%6&6eP?6UO)6|*D24P`P zxiS^n8OrU3@*@>Gz()qr8yy&Mke{rV(Zy{ zd&&!(cqVoy49unGO3s{!=h==qtR<-vRGsW7>K%}G>vYzrdNz6r(mlnFNc+?{pkyXV zRcrhd^ep5xiqqP1+&C<#M{j?Sja0qlJy8^gT*-9Cz{bKHBs_tfI-i);nmF<8YV35! z>)aREbro_zy1OjRHLpHxSn9RpaRvK^$IrkD-5CyjY)6uRl3to5;b|Q6?P5TmAbnBW z=PO@N+DK+QDc>jh%*)BSX|tol575s|PDyo~f{Q~_$UJRkchwwEtF;5UZ^zGa&e%Yy zV5;S`qO?9|;O*?XD#&)OK6*c~^`Ue$JjRX`Ysu)yUMccZCELyd+vVG<>-f#_%d2?g zbfeK@4eZve4$q-|WN_D>h{KLoRou#ZH03iQIK}mt3d80|eXp+WkDhE-e70F>gx!(8 zJ`M&=b1XakCsn{cWM1fIM=l?CykO||q`W9_50^DJdgFNThGHMBR3-<$5BKP48Mwml zS}i9!!=YKg;8*M3-#pCYFY|C7&gbQSae*u^#Qa&HgbU@@g8o|eu}Hoz7A|2f#WH3I z%Y`e1hp>`A0aHlARl?Q6HNv&Rb;9++4Z@AWO~TE>EyAtBZNlxs9m1W$UBZim7Yi>D zUMk!zyiB-9xL0TiZQ(v)S-4+#KsX^hD7;*Fh44z@Rl=)<*9fl_UMIX>c!The@J8Wb z;Z4Gug-3*w!drw#g~x=q3U3qME<7&0LwKj~gzzpQxSA(X!QHrrD|?dbeJ}qqaEgB$ GxbJ^K=i(&* diff --git a/test-data/slideshow/datetime.ppt b/test-data/slideshow/datetime.ppt new file mode 100755 index 0000000000000000000000000000000000000000..2cf67ef539fb6238e845077e402c620c85dd7329 GIT binary patch literal 114176 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 s_}JJDq