Browse Source

#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
tags/REL_5_2_0
Andreas Beeker 2 years ago
parent
commit
d3ff953cf7

+ 1
- 7
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFSlide.java 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();

+ 33
- 3
poi-scratchpad/src/main/java/org/apache/poi/hslf/model/HeadersFooters.java View File

@@ -50,11 +50,11 @@ public final class HeadersFooters {

public HeadersFooters(HSLFSheet sheet, short headerFooterType) {
_sheet = sheet;
@SuppressWarnings("resource")
HSLFSlideShow ppt = _sheet.getSlideShow();
Document doc = ppt.getDocumentRecord();
// detect if this ppt was saved in Office2007
String tag = ppt.getSlideMasters().get(0).getProgrammableTag();
_ppt2007 = _ppt2007tag.equals(tag);
@@ -72,7 +72,7 @@ public final class HeadersFooters {
}
}
}
if (hdd == null) {
hdd = new HeadersFootersContainer(headerFooterType);
Record lst = doc.findFirstOfType(RecordTypes.List.typeID);
@@ -206,6 +206,18 @@ public final class HeadersFooters {
return isVisible(HeadersFootersAtom.fHasUserDate, Placeholder.DATETIME);
}

public CString getHeaderAtom() {
return _container.getHeaderAtom();
}

public CString getFooterAtom() {
return _container.getFooterAtom();
}

public CString getUserDateAtom() {
return _container.getUserDateAtom();
}

/**
* whether the date is displayed in the footer.
*/
@@ -213,6 +225,20 @@ public final class HeadersFooters {
setFlag(HeadersFootersAtom.fHasUserDate, flag);
}

/**
* whether today's date is used.
*/
public boolean isTodayDateVisible(){
return isVisible(HeadersFootersAtom.fHasTodayDate, Placeholder.DATETIME);
}

/**
* whether the todays date is displayed in the footer.
*/
public void setTodayDateVisible(boolean flag){
setFlag(HeadersFootersAtom.fHasTodayDate, flag);
}

/**
* whether the slide number is displayed in the footer.
*/
@@ -282,4 +308,8 @@ public final class HeadersFooters {
public boolean isPpt2007() {
return _ppt2007;
}

public HeadersFootersContainer getContainer() {
return _container;
}
}

+ 125
- 0
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/DateTimeMCAtom.java 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
);
}

}

+ 3
- 27
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/HeadersFootersAtom.java 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)
);
}

+ 3
- 3
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/OEPlaceholderAtom.java 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.
*/

+ 1
- 1
poi-scratchpad/src/main/java/org/apache/poi/hslf/record/RecordTypes.java 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),

+ 3
- 2
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPlaceholderDetails.java 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) {

+ 67
- 4
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFShapePlaceholderDetails.java 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,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);
}
}

+ 28
- 4
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFSlide.java 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,18 +496,41 @@ public final class HSLFSlide extends HSLFSheet implements Slide<HSLFShape,HSLFTe
(slt == SlideLayoutType.TITLE_SLIDE || slt == SlideLayoutType.TITLE_ONLY || slt == SlideLayoutType.MASTER_TITLE);
switch (placeholder) {
case DATETIME:
return hf.isDateTimeVisible() && !isTitle;
return (hf.isDateTimeVisible() && (hf.isTodayDateVisible() || (hf.isUserDateVisible() && hf.getUserDateAtom() != null))) && !isTitle;
case SLIDE_NUMBER:
return hf.isSlideNumberVisible() && !isTitle;
case HEADER:
return hf.isHeaderVisible() && !isTitle;
return hf.isHeaderVisible() && hf.getHeaderAtom() != null && !isTitle;
case FOOTER:
return hf.isFooterVisible() && !isTitle;
return hf.isFooterVisible() && hf.getFooterAtom() != null && !isTitle;
default:
return false;
}
}

@Override
public boolean getDisplayPlaceholder(final SimpleShape<?,?> placeholderRef) {
Placeholder placeholder = placeholderRef.getPlaceholder();
if (placeholder == null) {
return false;
}

final HeadersFooters hf = getHeadersFooters();
final SlideLayoutType slt = getSlideRecord().getSlideAtom().getSSlideLayoutAtom().getGeometryType();
final boolean isTitle =
(slt == SlideLayoutType.TITLE_SLIDE || slt == SlideLayoutType.TITLE_ONLY || slt == SlideLayoutType.MASTER_TITLE);
switch (placeholder) {
case HEADER:
return hf.isHeaderVisible() && hf.getHeaderAtom() != null && !isTitle;
case FOOTER:
return hf.isFooterVisible() && hf.getFooterAtom() != null && !isTitle;
case DATETIME:
case SLIDE_NUMBER:
default:
return false;
}
}

@Override
public HSLFMasterSheet getSlideLayout(){
// TODO: find out how we can find the mastersheet base on the slide layout type, i.e.

+ 364
- 0
poi-scratchpad/src/main/java/org/apache/poi/hslf/util/LocaleDateFormat.java 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);
}
}
}

+ 451
- 416
poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestTextRun.java
File diff suppressed because it is too large
View File


+ 1
- 1
poi/src/main/java/org/apache/poi/sl/draw/DrawMasterSheet.java 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();

+ 36
- 34
poi/src/main/java/org/apache/poi/sl/draw/DrawTextParagraph.java 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) {
Slide<?,?> slide = (Slide<?,?>)graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
FieldType ft = tr.getFieldType();
if (ft == null) {
return getRenderableText(tr);
}
if (!tr.getRawText().isEmpty()) {
switch (ft) {
case SLIDE_NUMBER: {
Slide<?, ?> slide = (Slide<?, ?>) graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
}
case DATE_TIME: {
PlaceholderDetails pd = ((SimpleShape<?, ?>) this.getParagraphShape()).getPlaceholderDetails();
// refresh internal members
pd.getPlaceholder();
String uda = pd.getUserDate();
if (uda != null) {
return uda;
}
Calendar cal = LocaleUtil.getLocaleCalendar();
LocalDateTime now = LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
return now.format(pd.getDateFormat());
}
}
}
return getRenderableText(tr);
return "";
}

@Internal
@@ -550,30 +572,8 @@ public class DrawTextParagraph implements Drawable {
/**
* Helper method for paint style relative to bounds, e.g. gradient paint
*/
@SuppressWarnings("rawtypes")
private PlaceableShape<?,?> getParagraphShape() {
return new PlaceableShape(){
@Override
public ShapeContainer<?,?> getParent() { return null; }
@Override
public Rectangle2D getAnchor() { return paragraph.getParentShape().getAnchor(); }
@Override
public void setAnchor(Rectangle2D anchor) {}
@Override
public double getRotation() { return 0; }
@Override
public void setRotation(double theta) {}
@Override
public void setFlipHorizontal(boolean flip) {}
@Override
public void setFlipVertical(boolean flip) {}
@Override
public boolean getFlipHorizontal() { return false; }
@Override
public boolean getFlipVertical() { return false; }
@Override
public Sheet<?,?> getSheet() { return paragraph.getParentShape().getSheet(); }
};
return paragraph.getParentShape();
}

protected List<AttributedStringData> getAttributedString(Graphics2D graphics, StringBuilder text) {
@@ -671,9 +671,11 @@ public class DrawTextParagraph implements Drawable {
}

/**
* Processing the glyphs is done in two steps.
* <li>determine the font group - a text run can have different font groups. Depending on the chars,
* the correct font group needs to be used
* Processing the glyphs is done in two steps:
* <ul>
* <li>1. determine the font group - a text run can have different font groups.
* <li>2. Depending on the chars, the correct font group needs to be used
* </ul>
*
* @see <a href="https://blogs.msdn.microsoft.com/officeinteroperability/2013/04/22/office-open-xml-themes-schemes-and-fonts/">Office Open XML Themes, Schemes, and Fonts</a>
*/

+ 26
- 5
poi/src/main/java/org/apache/poi/sl/usermodel/PlaceholderDetails.java View File

@@ -18,6 +18,8 @@
package org.apache.poi.sl.usermodel;


import java.time.format.DateTimeFormatter;

/**
* Extended details about placholders
*
@@ -27,7 +29,7 @@ public interface PlaceholderDetails {
enum PlaceholderSize {
quarter, half, full
}
Placeholder getPlaceholder();

/**
@@ -40,13 +42,13 @@ public interface PlaceholderDetails {
* @param placeholder The shape to use as placeholder or null if no placeholder should be set.
*/
void setPlaceholder(Placeholder placeholder);
boolean isVisible();
void setVisible(boolean isVisible);
PlaceholderSize getSize();
void setSize(PlaceholderSize size);

/**
@@ -66,4 +68,23 @@ public interface PlaceholderDetails {
* @since POI 4.0.0
*/
void setText(String text);


/**
* @return the stored / fixed user specified date
*
* @since POI 5.2.0
*/
default String getUserDate() {
return null;
}

/**
* @return Get the date format for the datetime placeholder
*
* @since POI 5.2.0
*/
default DateTimeFormatter getDateFormat() {
return DateTimeFormatter.ISO_LOCAL_DATE;
}
}

+ 24
- 1
poi/src/main/java/org/apache/poi/sl/usermodel/Slide.java 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)}
*
*/
@Deprecated
@Removal(version = "6.0.0")
default boolean getDisplayPlaceholder(Placeholder placeholder) {
return false;
}


/**
* In XSLF, slidenumber and date shapes aren't marked as placeholders
* whereas in HSLF they are activated via a HeadersFooter configuration.
* This method is used to generalize that handling.
*
* @param placeholderRefShape the shape which references to the placeholder
* @return {@code true} if the placeholder should be displayed/rendered
* @since POI 5.2.0
*/
boolean getDisplayPlaceholder(Placeholder placeholder);
default boolean getDisplayPlaceholder(SimpleShape<?,?> placeholderRefShape) {
return false;
}

/**
* Sets the slide visibility

BIN
poi/src/main/java9/module-info.class View File


BIN
test-data/slideshow/datetime.ppt View File


Loading…
Cancel
Save