diff options
author | Andreas Beeker <kiwiwings@apache.org> | 2021-11-22 00:01:31 +0000 |
---|---|---|
committer | Andreas Beeker <kiwiwings@apache.org> | 2021-11-22 00:01:31 +0000 |
commit | d3ff953cf71edfdbb409ab1110ef9976d88368c0 (patch) | |
tree | ce3699801533a6e51d2df1f6fc445b5f6eef5e19 /poi-scratchpad/src | |
parent | 84957d7bc4d1b6e774c0b306af023f64d94f6736 (diff) | |
download | poi-d3ff953cf71edfdbb409ab1110ef9976d88368c0.tar.gz poi-d3ff953cf71edfdbb409ab1110ef9976d88368c0.zip |
#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
Diffstat (limited to 'poi-scratchpad/src')
10 files changed, 1078 insertions, 460 deletions
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. */ @@ -214,6 +226,20 @@ public final class HeadersFooters { } /** + * 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. */ public boolean isSlideNumberVisible(){ @@ -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<String, Supplier<?>> 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<String, Supplier<?>> 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.<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. */ 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<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,19 +496,42 @@ 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. // getSlideRecord().getSlideAtom().getSSlideLayoutAtom().getGeometryType() 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<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); + } + } +} 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<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(); + } + } } |