#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
This commit is contained in:
Andreas Beeker 2021-11-22 00:01:31 +00:00
parent 84957d7bc4
commit d3ff953cf7
17 changed files with 1133 additions and 475 deletions

View File

@ -70,7 +70,7 @@ implements Slide<XSLFShape,XSLFTextParagraph> {
* Construct a SpreadsheetML slide from a package part
*
* @param part the package part holding the slide data,
* the content type must be <code>application/vnd.openxmlformats-officedocument.slide+xml</code>
* the content type must be {@code application/vnd.openxmlformats-officedocument.slide+xml}
*
* @since POI 3.14-Beta1
*/
@ -377,12 +377,6 @@ implements Slide<XSLFShape,XSLFTextParagraph> {
draw.draw(graphics);
}
@Override
public boolean getDisplayPlaceholder(Placeholder placeholder) {
return false;
}
@Override
public void setHidden(boolean hidden) {
CTSlide sld = getXmlObject();

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -18,7 +18,6 @@
package org.apache.poi.hslf.record;
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
import static org.apache.poi.util.GenericRecordUtil.safeEnum;
import java.io.IOException;
import java.io.OutputStream;
@ -37,30 +36,6 @@ import org.apache.poi.util.LittleEndian;
public final class HeadersFootersAtom extends RecordAtom {
/** FormatIndex enum without LCID mapping */
public enum FormatIndex {
SHORT_DATE,
LONG_DATE,
LONG_DATE_WITHOUT_WEEKDAY,
ALTERNATE_SHORT_DATE,
ISO_STANDARD_DATE,
SHORT_DATE_WITH_ABBREVIATED_MONTH,
SHORT_DATE_WITH_SLASHES,
ALTERNATE_SHORT_DATE_WITH_ABBREVIATED_MONTH,
ENGLISH_DATE,
MONTH_AND_YEAR,
ABBREVIATED_MONTH_AND_YEAR,
DATE_AND_HOUR12_TIME,
DATE_AND_HOUR12_TIME_WITH_SECONDS,
HOUR12_TIME,
HOUR12_TIME_WITH_SECONDS,
HOUR24_TIME,
HOUR24_TIME_WITH_SECONDS,
CHINESE1,
CHINESE2,
CHINESE3
}
/**
* A bit that specifies whether the date is displayed in the footer.
* @see #getMask()
@ -136,7 +111,7 @@ public final class HeadersFootersAtom extends RecordAtom {
/**
* Build an instance of {@code HeadersFootersAtom} from on-disk data
*/
protected HeadersFootersAtom(byte[] source, int start, int len) {
HeadersFootersAtom(byte[] source, int start, int len) {
// Get the header
_header = Arrays.copyOfRange(source, start, start+8);
@ -182,6 +157,7 @@ public final class HeadersFootersAtom extends RecordAtom {
return LittleEndian.getShort(_recdata, 0);
}
/**
* A signed integer that specifies the format ID to be used to style the datetime.
*
@ -258,7 +234,7 @@ public final class HeadersFootersAtom extends RecordAtom {
@Override
public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties(
"formatIndex", safeEnum(FormatIndex.values(), this::getFormatId),
"formatIndex", this::getFormatId,
"flags", getBitsAsString(this::getMask, PLACEHOLDER_MASKS, PLACEHOLDER_NAMES)
);
}

View File

@ -52,7 +52,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
*/
public static final int PLACEHOLDER_QUARTSIZE = 2;
private byte[] _header;
private final byte[] _header;
private int placementId;
private int placeholderId;
@ -77,7 +77,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
/**
* Build an instance of {@code OEPlaceholderAtom} from on-disk data
*/
protected OEPlaceholderAtom(byte[] source, int start, int len) {
OEPlaceholderAtom(byte[] source, int start, int len) {
_header = Arrays.copyOfRange(source, start, start+8);
int offset = start+8;
@ -135,7 +135,7 @@ public final class OEPlaceholderAtom extends RecordAtom{
* Sets the placeholder Id.<p>
*
* placeholder Id specifies the type of the placeholder shape.
* The value MUST be one of the static constants defined in this class
* The value MUST be one of the static constants defined in {@link Placeholder}
*
* @param id the placeholder Id.
*/

View File

@ -134,7 +134,7 @@ public enum RecordTypes {
InteractiveInfoAtom(4083,InteractiveInfoAtom::new),
UserEditAtom(4085,UserEditAtom::new),
CurrentUserAtom(4086,null),
DateTimeMCAtom(4087,null),
DateTimeMCAtom(4087,DateTimeMCAtom::new),
GenericDateMCAtom(4088,null),
FooterMCAtom(4090,null),
ExControlAtom(4091,ExControlAtom::new),

View File

@ -36,6 +36,7 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
}
@Override
public boolean isVisible() {
final Placeholder ph = getPlaceholder();
if (ph == null) {
@ -46,13 +47,12 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
switch (ph) {
case HEADER:
case TITLE:
return headersFooters.isHeaderVisible();
case FOOTER:
return headersFooters.isFooterVisible();
case DATETIME:
return headersFooters.isDateTimeVisible();
case TITLE:
return headersFooters.isHeaderVisible();
case SLIDE_NUMBER:
return headersFooters.isSlideNumberVisible();
default:
@ -60,6 +60,7 @@ public class HSLFPlaceholderDetails implements PlaceholderDetails {
}
}
@Override
public void setVisible(final boolean isVisible) {
final Placeholder ph = getPlaceholder();
if (ph == null) {

View File

@ -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,6 +130,7 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
roundTripHFPlaceholder12.setPlaceholderId(phId);
}
@Override
public PlaceholderSize getSize() {
final Placeholder ph = getPlaceholder();
if (ph == null) {
@ -132,6 +152,7 @@ public class HSLFShapePlaceholderDetails extends HSLFPlaceholderDetails {
}
}
@Override
public void setSize(final PlaceholderSize size) {
final Placeholder ph = getPlaceholder();
if (ph == null || size == null) {
@ -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);
}
}

View File

@ -46,6 +46,7 @@ import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.usermodel.Notes;
import org.apache.poi.sl.usermodel.Placeholder;
import org.apache.poi.sl.usermodel.ShapeType;
import org.apache.poi.sl.usermodel.SimpleShape;
import org.apache.poi.sl.usermodel.Slide;
import org.apache.poi.sl.usermodel.TextShape.TextPlaceholder;
@ -259,7 +260,7 @@ public final class HSLFSlide extends HSLFSheet implements Slide<HSLFShape,HSLFTe
* @return set of records inside {@code SlideListWithtext} container
* which hold text data for this slide (typically for placeholders).
*/
protected SlideAtomsSet getSlideAtomsSet() { return _atomSet; }
public SlideAtomsSet getSlideAtomsSet() { return _atomSet; }
/**
* Returns master sheet associated with this slide.
@ -495,13 +496,36 @@ 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;
}

View File

@ -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);
}
}
}

View File

@ -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,62 +52,47 @@ 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);
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();
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)));
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
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)));
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<List<HSLFTextParagraph>> 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<List<HSLFTextParagraph>> textRuns = slideOne.getTextParagraphs();
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);
@ -114,6 +108,7 @@ public final class TestTextRun {
tr.setText(changeTo + "\n");
assertEquals(changeTo + "\r", tr.getRawText());
}
}
/**
* Test to ensure that changing non rich text between bytes and
@ -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);
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);
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
assertNull(tca);
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);
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;
}
extract.run();
assertEquals(changeBytesOnly, HSLFTextParagraph.getRawText(paras));
assertNull(tca);
assertNull(tca[0]);
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;
}
extract.run();
assertEquals(changeByteChar, HSLFTextParagraph.getRawText(paras));
assertNotNull(tca);
assertNull(tba);
assertNotNull(tca[0]);
assertNull(tba[0]);
// 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;
}
extract.run();
assertEquals(changeCharChar, HSLFTextParagraph.getRawText(paras));
assertNotNull(tca);
assertNull(tba);
assertNotNull(tca[0]);
assertNull(tba[0]);
}
}
/**
@ -196,9 +170,9 @@ 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());
@ -214,14 +188,15 @@ public final class TestTextRun {
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());
@ -252,16 +227,16 @@ public final class TestTextRun {
assertNotEquals(rtrB.getCharacterStyle(), rtrD.getCharacterStyle());
assertNotEquals(rtrC.getCharacterStyle(), rtrD.getCharacterStyle());
}
}
/**
* Tests to ensure that setting the text where the text isn't rich,
* 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);
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);
@ -273,16 +248,16 @@ public final class TestTextRun {
assertEquals("Test Foo Test", HSLFTextParagraph.getRawText(trB));
assertEquals("Test Foo Test", rtrB.getRawText());
}
}
/**
* Tests to ensure that setting the text where the text is rich
* 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);
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);
@ -322,15 +297,16 @@ public final class TestTextRun {
assertEquals(tpBP, rtrB.getTextParagraph().getParagraphStyle());
assertEquals(tpBC, rtrB.getCharacterStyle());
}
}
/**
* Test to ensure the right stuff happens if we change the text
* 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();
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());
@ -348,14 +324,16 @@ public final class TestTextRun {
assertNotNull(rtrB.getCharacterStyle());
assertNotNull(rtrB.getTextParagraph().getParagraphStyle());
}
}
/**
* Tests to ensure changing the text within rich text runs works
* correctly
*/
@Test
void testChangeTextInRichTextRun() {
HSLFSlide slideOne = ssRich.getSlides().get(0);
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());
@ -425,6 +403,7 @@ public final class TestTextRun {
assertEquals(tpCC.getTextPropList(), ntpCC.getTextPropList());
assertEquals(tpDC.getTextPropList(), ntpDC.getTextPropList());
}
}
/**
@ -436,15 +415,13 @@ public final class TestTextRun {
*/
@Test
void testBug41015() throws IOException {
List<HSLFTextRun> rt;
HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("bug-41015.ppt");
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);
rt = textParass.get(0).get(0).getTextRuns();
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());
@ -458,7 +435,7 @@ public final class TestTextRun {
assertEquals(indents[i], p.getIndentLevel());
i++;
}
ppt.close();
}
}
/**
@ -466,7 +443,7 @@ public final class TestTextRun {
*/
@Test
void testAddTextRun() throws IOException {
HSLFSlideShow ppt = new HSLFSlideShow();
try (HSLFSlideShow ppt = new HSLFSlideShow()) {
HSLFSlide slide = ppt.createSlide();
assertEquals(0, slide.getTextParagraphs().size());
@ -509,12 +486,12 @@ public final class TestTextRun {
runs = slide2.getTextParagraphs();
assertNotNull(runs);
assertEquals(4, runs.size());
ppt.close();
}
}
@Test
void test48916() throws IOException {
HSLFSlideShow ppt1 = HSLFTestDataSamples.getSlideShow("SampleShow.ppt");
try (HSLFSlideShow ppt1 = getSlideShow("SampleShow.ppt")) {
List<HSLFSlide> slides = ppt1.getSlides();
for (HSLFSlide slide : slides) {
for (HSLFShape sh : slide.getShapes()) {
@ -539,37 +516,33 @@ public final class TestTextRun {
}
}
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");
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();
}
}
}

View File

@ -45,7 +45,7 @@ public class DrawMasterSheet extends DrawSheet {
// in XSLF, slidenumber and date shapes aren't marked as placeholders opposed to HSLF
Placeholder ph = ((SimpleShape<?,?>)shape).getPlaceholder();
if (ph != null) {
return slide.getDisplayPlaceholder(ph);
return slide.getDisplayPlaceholder((SimpleShape<?, ?>)shape);
}
}
return slide.getFollowMasterGraphics();

View File

@ -17,6 +17,8 @@
package org.apache.poi.sl.draw;
import static org.apache.logging.log4j.util.Unbox.box;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
@ -30,8 +32,10 @@ import java.io.InvalidObjectException;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@ -47,8 +51,8 @@ import org.apache.poi.sl.usermodel.Hyperlink;
import org.apache.poi.sl.usermodel.Insets2D;
import org.apache.poi.sl.usermodel.PaintStyle;
import org.apache.poi.sl.usermodel.PlaceableShape;
import org.apache.poi.sl.usermodel.ShapeContainer;
import org.apache.poi.sl.usermodel.Sheet;
import org.apache.poi.sl.usermodel.PlaceholderDetails;
import org.apache.poi.sl.usermodel.SimpleShape;
import org.apache.poi.sl.usermodel.Slide;
import org.apache.poi.sl.usermodel.TextParagraph;
import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle;
@ -61,8 +65,6 @@ import org.apache.poi.util.Internal;
import org.apache.poi.util.LocaleUtil;
import org.apache.poi.util.Units;
import static org.apache.logging.log4j.util.Unbox.box;
public class DrawTextParagraph implements Drawable {
private static final Logger LOG = LogManager.getLogger(DrawTextParagraph.class);
@ -397,11 +399,31 @@ public class DrawTextParagraph implements Drawable {
}
protected String getRenderableText(Graphics2D graphics, TextRun tr) {
if (tr.getFieldType() == FieldType.SLIDE_NUMBER) {
FieldType ft = tr.getFieldType();
if (ft == null) {
return getRenderableText(tr);
}
if (!tr.getRawText().isEmpty()) {
switch (ft) {
case SLIDE_NUMBER: {
Slide<?, ?> slide = (Slide<?, ?>) graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
}
return getRenderableText(tr);
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<AttributedStringData> getAttributedString(Graphics2D graphics, StringBuilder text) {
@ -671,9 +671,11 @@ public class DrawTextParagraph implements Drawable {
}
/**
* Processing the glyphs is done in two steps.
* <li>determine the font group - a text run can have different font groups. Depending on the chars,
* the correct font group needs to be used
* Processing the glyphs is done in two steps:
* <ul>
* <li>1. determine the font group - a text run can have different font groups.
* <li>2. Depending on the chars, the correct font group needs to be used
* </ul>
*
* @see <a href="https://blogs.msdn.microsoft.com/officeinteroperability/2013/04/22/office-open-xml-themes-schemes-and-fonts/">Office Open XML Themes, Schemes, and Fonts</a>
*/

View File

@ -18,6 +18,8 @@
package org.apache.poi.sl.usermodel;
import java.time.format.DateTimeFormatter;
/**
* Extended details about placholders
*
@ -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;
}
}

View File

@ -19,6 +19,8 @@ package org.apache.poi.sl.usermodel;
import java.util.List;
import org.apache.poi.util.Removal;
@SuppressWarnings("unused")
public interface Slide<
S extends Shape<S,P>,
@ -54,8 +56,29 @@ public interface Slide<
* @param placeholder the placeholder type
* @return {@code true} if the placeholder should be displayed/rendered
* @since POI 3.16-beta2
*
* @deprecated in POI 5.2.0 - use {@link #getDisplayPlaceholder(SimpleShape)}
*
*/
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

Binary file not shown.

BIN
test-data/slideshow/datetime.ppt Executable file

Binary file not shown.