From 033c0924151834140262cbf00d368b3958bdfbca Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 28 Dec 2018 23:43:31 +0000 Subject: [PATCH] #63028 - Provide font embedding for slideshows git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1849898 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/poi/TestAllFiles.java | 2 +- .../common/usermodel/fonts/FontCharset.java | 2 +- .../poi/common/usermodel/fonts/FontFacet.java | 75 +++++ .../common/usermodel/fonts/FontHeader.java | 227 +++++++++++++ .../poi/common/usermodel/fonts/FontInfo.java | 68 +++- .../org/apache/poi/sl/draw/DrawFontInfo.java | 48 --- .../poi/sl/extractor/SlideShowExtractor.java | 305 +++++++++++------- .../poi/sl/usermodel/FontCollection.java | 22 -- .../apache/poi/sl/usermodel/Resources.java | 29 -- .../org/apache/poi/sl/usermodel/Slide.java | 3 +- .../apache/poi/sl/usermodel/SlideShow.java | 29 +- .../org/apache/poi/sl/usermodel/TextRun.java | 8 + .../poi/xslf/usermodel/XMLSlideShow.java | 144 ++++----- .../poi/xslf/usermodel/XSLFFontData.java | 84 +++++ .../poi/xslf/usermodel/XSLFFontInfo.java | 285 ++++++++++++++++ .../poi/xslf/usermodel/XSLFRelation.java | 7 + .../poi/xslf/usermodel/XSLFTextRun.java | 16 +- .../TestXSLFPowerPointExtractor.java | 14 +- .../poi/xslf/usermodel/TestXMLSlideShow.java | 32 +- .../org/apache/poi/hslf/record/Document.java | 36 +-- .../apache/poi/hslf/record/DocumentAtom.java | 59 ++-- .../poi/hslf/record/FontCollection.java | 76 ++++- .../poi/hslf/record/FontEmbeddedData.java | 116 +++++++ .../poi/hslf/record/FontEntityAtom.java | 8 +- .../apache/poi/hslf/record/RecordTypes.java | 26 +- .../poi/hslf/usermodel/HSLFFontInfo.java | 27 ++ .../usermodel/HSLFFontInfoPredefined.java | 30 -- .../poi/hslf/usermodel/HSLFSlideShow.java | 27 +- .../poi/hslf/usermodel/HSLFTextRun.java | 30 +- .../org/apache/poi/hwmf/record/HwmfFont.java | 36 +-- .../poi/hslf/extractor/TestExtractor.java | 65 ++-- .../poi/hslf/usermodel/TestHSLFSlideShow.java | 10 +- .../poi/sl/usermodel/BaseTestSlideShow.java | 91 +++--- test-data/slideshow/font.fntdata | Bin 0 -> 17759 bytes 34 files changed, 1481 insertions(+), 556 deletions(-) create mode 100644 src/java/org/apache/poi/common/usermodel/fonts/FontFacet.java create mode 100644 src/java/org/apache/poi/common/usermodel/fonts/FontHeader.java delete mode 100644 src/java/org/apache/poi/sl/usermodel/FontCollection.java delete mode 100644 src/java/org/apache/poi/sl/usermodel/Resources.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontData.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontInfo.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/record/FontEmbeddedData.java create mode 100644 test-data/slideshow/font.fntdata diff --git a/src/integrationtest/org/apache/poi/TestAllFiles.java b/src/integrationtest/org/apache/poi/TestAllFiles.java index 2255e8bbf9..33bd696195 100644 --- a/src/integrationtest/org/apache/poi/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/TestAllFiles.java @@ -187,11 +187,11 @@ public class TestAllFiles { HANDLERS.put(".tif", new NullFileHandler()); HANDLERS.put(".tiff", new NullFileHandler()); HANDLERS.put(".wav", new NullFileHandler()); - HANDLERS.put(".pfx", new NullFileHandler()); HANDLERS.put(".xml", new NullFileHandler()); HANDLERS.put(".csv", new NullFileHandler()); HANDLERS.put(".ods", new NullFileHandler()); HANDLERS.put(".ttf", new NullFileHandler()); + HANDLERS.put(".fntdata", new NullFileHandler()); // VBA source files HANDLERS.put(".vba", new NullFileHandler()); HANDLERS.put(".bas", new NullFileHandler()); diff --git a/src/java/org/apache/poi/common/usermodel/fonts/FontCharset.java b/src/java/org/apache/poi/common/usermodel/fonts/FontCharset.java index aeeca9284c..32915149f2 100644 --- a/src/java/org/apache/poi/common/usermodel/fonts/FontCharset.java +++ b/src/java/org/apache/poi/common/usermodel/fonts/FontCharset.java @@ -70,7 +70,7 @@ public enum FontCharset { /** Specifies the Russian Cyrillic character set. */ RUSSIAN(0x000000CC, "Cp1251"), /** Specifies the Thai character set. */ - THAI_(0x000000DE, "x-windows-874"), + THAI(0x000000DE, "x-windows-874"), /** Specifies a Eastern European character set. */ EASTEUROPE(0x000000EE, "Cp1250"), /** diff --git a/src/java/org/apache/poi/common/usermodel/fonts/FontFacet.java b/src/java/org/apache/poi/common/usermodel/fonts/FontFacet.java new file mode 100644 index 0000000000..5fb16908cd --- /dev/null +++ b/src/java/org/apache/poi/common/usermodel/fonts/FontFacet.java @@ -0,0 +1,75 @@ +/* ==================================================================== +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.common.usermodel.fonts; + +import org.apache.poi.util.Beta; + +/** + * A FontFacet holds the font data for a shape of a font, i.e. a regular, + * italic, bold or bold-italic version of a Font. + */ +@SuppressWarnings("unused") +@Beta +public interface FontFacet { + /** + * Get the font weight.

+ * + * The weight of the font in the range 0 through 1000. + * For example, 400 is normal and 700 is bold. + * If this value is zero, a default weight is used. + * + * @return the font weight + * + * @since POI 4.1.0 + */ + default int getWeight() { + return FontHeader.REGULAR_WEIGHT; + } + + /** + * Set the font weight + * + * @param weight the font weight + */ + default void setWeight(int weight) { + throw new UnsupportedOperationException("FontFacet is read-only."); + } + + /** + * @return {@code true}, if the font is italic + */ + default boolean isItalic() { + return false; + } + + /** + * Set the font posture + * + * @param italic {@code true} for italic, {@code false} for regular + */ + default void setItalic(boolean italic) { + throw new UnsupportedOperationException("FontFacet is read-only."); + } + + /** + * @return the wrapper object holding the font data + */ + default Object getFontData() { + return null; + } +} diff --git a/src/java/org/apache/poi/common/usermodel/fonts/FontHeader.java b/src/java/org/apache/poi/common/usermodel/fonts/FontHeader.java new file mode 100644 index 0000000000..9777f0cc75 --- /dev/null +++ b/src/java/org/apache/poi/common/usermodel/fonts/FontHeader.java @@ -0,0 +1,227 @@ +/* ==================================================================== + 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.common.usermodel.fonts; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndianByteArrayInputStream; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianInputStream; + + +/** + * The header data of an EOT font.

+ * + * Currently only version 1 fields are read to identify a stream to be embedded. + * + * @see Embedded OpenType (EOT) File Format + */ +@SuppressWarnings({"FieldCanBeLocal", "unused", "Duplicates"}) +public class FontHeader implements FontInfo { + /** + * Fonts with a font weight of 400 are regarded as regular weighted. + * Higher font weights (up to 1000) are bold - lower weights are thin. + */ + public static final int REGULAR_WEIGHT = 400; + + private int eotSize; + private int fontDataSize; + private int version; + private int flags; + private final byte[] panose = new byte[10]; + private byte charset; + private byte italic; + private int weight; + private int fsType; + private int magic; + private int unicodeRange1; + private int unicodeRange2; + private int unicodeRange3; + private int unicodeRange4; + private int codePageRange1; + private int codePageRange2; + private int checkSumAdjustment; + private String familyName; + private String styleName; + private String versionName; + private String fullName; + + public void init(byte[] source, int offset, int length) { + init(new LittleEndianByteArrayInputStream(source, offset, length)); + } + + public void init(LittleEndianInput leis) { + eotSize = leis.readInt(); + fontDataSize = leis.readInt(); + version = leis.readInt(); + if (version != 0x00010000 && version != 0x00020001 && version != 0x00020002) { + throw new RuntimeException("not a EOT font data stream"); + } + flags = leis.readInt(); + leis.readFully(panose); + charset = leis.readByte(); + italic = leis.readByte(); + weight = leis.readInt(); + fsType = leis.readUShort(); + magic = leis.readUShort(); + if (magic != 0x504C) { + throw new RuntimeException("not a EOT font data stream"); + } + unicodeRange1 = leis.readInt(); + unicodeRange2 = leis.readInt(); + unicodeRange3 = leis.readInt(); + unicodeRange4 = leis.readInt(); + codePageRange1 = leis.readInt(); + codePageRange2 = leis.readInt(); + checkSumAdjustment = leis.readInt(); + int reserved1 = leis.readInt(); + int reserved2 = leis.readInt(); + int reserved3 = leis.readInt(); + int reserved4 = leis.readInt(); + familyName = readName(leis); + styleName = readName(leis); + versionName = readName(leis); + fullName = readName(leis); + + } + + public InputStream bufferInit(InputStream fontStream) throws IOException { + LittleEndianInputStream is = new LittleEndianInputStream(fontStream); + is.mark(1000); + init(is); + is.reset(); + return is; + } + + private String readName(LittleEndianInput leis) { + // padding + leis.readShort(); + int nameSize = leis.readUShort(); + byte[] nameBuf = IOUtils.safelyAllocate(nameSize, 1000); + leis.readFully(nameBuf); + // may be 0-terminated, just trim it away + return new String(nameBuf, 0, nameSize, StandardCharsets.UTF_16LE).trim(); + } + + public boolean isItalic() { + return italic != 0; + } + + public int getWeight() { + return weight; + } + + public boolean isBold() { + return getWeight() > REGULAR_WEIGHT; + } + + public byte getCharsetByte() { + return charset; + } + + public FontCharset getCharset() { + return FontCharset.valueOf(getCharsetByte()); + } + + public FontPitch getPitch() { + byte familyKind = panose[0]; + switch (familyKind) { + default: + // Any + case 0: + // No Fit + case 1: + return FontPitch.VARIABLE; + + // Latin Text + case 2: + // Latin Decorative + case 4: + byte proportion = panose[3]; + return proportion == 9 ? FontPitch.FIXED : FontPitch.VARIABLE; + + // Latin Hand Written + case 3: + // Latin Symbol + case 5: + byte spacing = panose[3]; + return spacing == 3 ? FontPitch.FIXED : FontPitch.VARIABLE; + } + + } + + public FontFamily getFamily() { + switch (panose[0]) { + // Any + case 0: + // No Fit + case 1: + return FontFamily.FF_DONTCARE; + // Latin Text + case 2: + byte serifStyle = panose[1]; + return (10 <= serifStyle && serifStyle <= 15) + ? FontFamily.FF_SWISS : FontFamily.FF_ROMAN; + // Latin Hand Written + case 3: + return FontFamily.FF_SCRIPT; + // Latin Decorative + default: + case 4: + return FontFamily.FF_DECORATIVE; + // Latin Symbol + case 5: + return FontFamily.FF_MODERN; + } + } + + public String getFamilyName() { + return familyName; + } + + public String getStyleName() { + return styleName; + } + + public String getVersionName() { + return versionName; + } + + public String getFullName() { + return fullName; + } + + public byte[] getPanose() { + return panose; + } + + @Override + public String getTypeface() { + return getFamilyName(); + } + + public int getFlags() { + return flags; + } +} + + + diff --git a/src/java/org/apache/poi/common/usermodel/fonts/FontInfo.java b/src/java/org/apache/poi/common/usermodel/fonts/FontInfo.java index ecb5a69687..b47b02e53b 100644 --- a/src/java/org/apache/poi/common/usermodel/fonts/FontInfo.java +++ b/src/java/org/apache/poi/common/usermodel/fonts/FontInfo.java @@ -17,6 +17,11 @@ limitations under the License. package org.apache.poi.common.usermodel.fonts; +import java.util.Collections; +import java.util.List; + +import org.apache.poi.util.Beta; + /** * A FontInfo object holds information about a font configuration. * It is roughly an equivalent to the LOGFONT structure in Windows GDI.

@@ -30,6 +35,7 @@ package org.apache.poi.common.usermodel.fonts; * * @see LOGFONT structure */ +@SuppressWarnings("unused") public interface FontInfo { /** @@ -37,7 +43,9 @@ public interface FontInfo { * @return unique index number of the underlying record this Font represents * (probably you don't care unless you're comparing which one is which) */ - Integer getIndex(); + default Integer getIndex() { + return null; + } /** * Sets the index within the collection of Font objects @@ -46,7 +54,9 @@ public interface FontInfo { * * @throws UnsupportedOperationException if unsupported */ - void setIndex(int index); + default void setIndex(int index) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } /** @@ -60,36 +70,48 @@ public interface FontInfo { * @param typeface the full name of the font, when {@code null} removes the font definition - * removal is implementation specific */ - void setTypeface(String typeface); + default void setTypeface(String typeface) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } /** * @return the font charset */ - FontCharset getCharset(); + default FontCharset getCharset() { + return FontCharset.ANSI; + } /** * Sets the charset * * @param charset the charset */ - void setCharset(FontCharset charset); + default void setCharset(FontCharset charset) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } /** * @return the family class */ - FontFamily getFamily(); + default FontFamily getFamily() { + return FontFamily.FF_DONTCARE; + } /** * Sets the font family class * * @param family the font family class */ - void setFamily(FontFamily family); + default void setFamily(FontFamily family) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } /** * @return the font pitch or {@code null} if unsupported */ - FontPitch getPitch(); + default FontPitch getPitch() { + return null; + } /** * Set the font pitch @@ -98,5 +120,33 @@ public interface FontInfo { * * @throws UnsupportedOperationException if unsupported */ - void setPitch(FontPitch pitch); + default void setPitch(FontPitch pitch) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } + + /** + * @return panose info in binary form or {@code null} if unknown + */ + default byte[] getPanose() { + return null; + } + + /** + * Set the panose in binary form + * @param panose the panose bytes + */ + default void setPanose(byte[] panose) { + throw new UnsupportedOperationException("FontInfo is read-only."); + } + + + /** + * If font facets are embedded in the document, return the list of embedded facets. + * The font embedding is experimental, therefore the API can change. + * @return the list of embedded EOT font data + */ + @Beta + default List getFacets() { + return Collections.emptyList(); + } } \ No newline at end of file diff --git a/src/java/org/apache/poi/sl/draw/DrawFontInfo.java b/src/java/org/apache/poi/sl/draw/DrawFontInfo.java index dc7afb4e24..da1979cb9e 100644 --- a/src/java/org/apache/poi/sl/draw/DrawFontInfo.java +++ b/src/java/org/apache/poi/sl/draw/DrawFontInfo.java @@ -19,10 +19,7 @@ package org.apache.poi.sl.draw; -import org.apache.poi.common.usermodel.fonts.FontCharset; -import org.apache.poi.common.usermodel.fonts.FontFamily; import org.apache.poi.common.usermodel.fonts.FontInfo; -import org.apache.poi.common.usermodel.fonts.FontPitch; import org.apache.poi.util.Internal; /** @@ -37,53 +34,8 @@ import org.apache.poi.util.Internal; this.typeface = typeface; } - @Override - public Integer getIndex() { - return null; - } - - @Override - public void setIndex(int index) { - throw new UnsupportedOperationException("DrawFontManagers FontInfo can't be changed."); - } - @Override public String getTypeface() { return typeface; } - - @Override - public void setTypeface(String typeface) { - throw new UnsupportedOperationException("DrawFontManagers FontInfo can't be changed."); - } - - @Override - public FontCharset getCharset() { - return FontCharset.ANSI; - } - - @Override - public void setCharset(FontCharset charset) { - throw new UnsupportedOperationException("DrawFontManagers FontInfo can't be changed."); - } - - @Override - public FontFamily getFamily() { - return FontFamily.FF_SWISS; - } - - @Override - public void setFamily(FontFamily family) { - throw new UnsupportedOperationException("DrawFontManagers FontInfo can't be changed."); - } - - @Override - public FontPitch getPitch() { - return FontPitch.VARIABLE; - } - - @Override - public void setPitch(FontPitch pitch) { - throw new UnsupportedOperationException("DrawFontManagers FontInfo can't be changed."); - } } diff --git a/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java b/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java index dee4d44a03..7173c24e97 100644 --- a/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java +++ b/src/java/org/apache/poi/sl/extractor/SlideShowExtractor.java @@ -18,10 +18,14 @@ package org.apache.poi.sl.extractor; import java.util.ArrayList; +import java.util.BitSet; +import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.poi.extractor.POITextExtractor; -import org.apache.poi.sl.usermodel.Comment; import org.apache.poi.sl.usermodel.MasterSheet; import org.apache.poi.sl.usermodel.Notes; import org.apache.poi.sl.usermodel.ObjectShape; @@ -52,6 +56,10 @@ public class SlideShowExtractor< > extends POITextExtractor { private static final POILogger LOG = POILogFactory.getLogger(SlideShowExtractor.class); + // placeholder text for slide numbers + private static final String SLIDE_NUMBER_PH = "‹#›"; + + private SlideShow slideshow; private boolean slidesByDefault = true; @@ -59,7 +67,8 @@ public class SlideShowExtractor< private boolean commentsByDefault; private boolean masterByDefault; - + private Predicate filter = o -> true; + public SlideShowExtractor(final SlideShow slideshow) { setFilesystem(slideshow); this.slideshow = slideshow; @@ -115,9 +124,8 @@ public class SlideShowExtractor< @Override public String getText() { final StringBuilder sb = new StringBuilder(); - for (final Slide slide : slideshow.getSlides()) { - sb.append(getText(slide)); + getText(slide, sb::append); } return sb.toString(); @@ -125,34 +133,37 @@ public class SlideShowExtractor< public String getText(final Slide slide) { final StringBuilder sb = new StringBuilder(); + getText(slide, sb::append); + return sb.toString(); + } + + private void getText(final Slide slide, final Consumer consumer) { if (slidesByDefault) { - printShapeText(slide, sb); + printShapeText(slide, consumer); } if (masterByDefault) { final MasterSheet ms = slide.getMasterSheet(); - printSlideMaster(ms, sb); + printSlideMaster(ms, consumer); // only print slide layout, if it's a different instance final MasterSheet sl = slide.getSlideLayout(); if (sl != ms) { - printSlideMaster(sl, sb); + printSlideMaster(sl, consumer); } } if (commentsByDefault) { - printComments(slide, sb); + printComments(slide, consumer); } if (notesByDefault) { - printNotes(slide, sb); + printNotes(slide, consumer); } - - return sb.toString(); } - private void printSlideMaster(final MasterSheet master, final StringBuilder sb) { + private void printSlideMaster(final MasterSheet master, final Consumer consumer) { if (master == null) { return; } @@ -163,163 +174,140 @@ public class SlideShowExtractor< if (text == null || text.isEmpty() || "*".equals(text)) { continue; } + if (ts.isPlaceholder()) { // don't bother about boiler plate text on master sheets LOG.log(POILogger.INFO, "Ignoring boiler plate (placeholder) text on slide master:", text); continue; } - sb.append(text); - if (!text.endsWith("\n")) { - sb.append("\n"); - } + printTextParagraphs(ts.getTextParagraphs(), consumer); } } } - private String printHeaderReturnFooter(final Sheet sheet, final StringBuilder sb) { - final Sheet m = (sheet instanceof Slide) ? sheet.getMasterSheet() : sheet; - final StringBuilder footer = new StringBuilder("\n"); - addSheetPlaceholderDatails(sheet, Placeholder.HEADER, sb); - addSheetPlaceholderDatails(sheet, Placeholder.FOOTER, footer); + private void printTextParagraphs(final List

paras, final Consumer consumer) { + printTextParagraphs(paras, consumer, "\n"); + } - if (masterByDefault) { - // write header texts and determine footer text - for (Shape s : m) { - if (!(s instanceof TextShape)) { - continue; - } - final TextShape ts = (TextShape) s; - final PlaceholderDetails pd = ts.getPlaceholderDetails(); - if (pd == null || !pd.isVisible() || pd.getPlaceholder() == null) { - continue; - } - switch (pd.getPlaceholder()) { - case HEADER: - sb.append(ts.getText()); - sb.append('\n'); - break; - case SLIDE_NUMBER: - if (sheet instanceof Slide) { - footer.append(ts.getText().replace("‹#›", Integer.toString(((Slide) sheet).getSlideNumber() + 1))); - footer.append('\n'); - } - break; - case FOOTER: - footer.append(ts.getText()); - footer.append('\n'); - break; - case DATETIME: - // currently not supported - default: - break; + + private void printTextParagraphs(final List

paras, final Consumer consumer, String trailer) { + printTextParagraphs(paras, consumer, trailer, SlideShowExtractor::replaceTextCap); + } + + private void printTextParagraphs(final List

paras, final Consumer consumer, String trailer, final Function converter) { + for (P p : paras) { + for (TextRun r : p) { + if (filter.test(r)) { + consumer.accept(converter.apply(r)); } } + if (!trailer.isEmpty() && filter.test(trailer)) { + consumer.accept(trailer); + } } - - return (footer.length() > 1) ? footer.toString() : ""; } - private void addSheetPlaceholderDatails(final Sheet sheet, final Placeholder placeholder, final StringBuilder sb) { - final PlaceholderDetails headerPD = sheet.getPlaceholderDetails(placeholder); - if (headerPD == null) { + private void printHeaderFooter(final Sheet sheet, final Consumer consumer, final Consumer footerCon) { + final Sheet m = (sheet instanceof Slide) ? sheet.getMasterSheet() : sheet; + addSheetPlaceholderDatails(sheet, Placeholder.HEADER, consumer); + addSheetPlaceholderDatails(sheet, Placeholder.FOOTER, footerCon); + + if (!masterByDefault) { return; } - final String headerStr = headerPD.getText(); - if (headerStr == null) { - return; + + // write header texts and determine footer text + for (Shape s : m) { + if (!(s instanceof TextShape)) { + continue; + } + final TextShape ts = (TextShape) s; + final PlaceholderDetails pd = ts.getPlaceholderDetails(); + if (pd == null || !pd.isVisible() || pd.getPlaceholder() == null) { + continue; + } + switch (pd.getPlaceholder()) { + case HEADER: + printTextParagraphs(ts.getTextParagraphs(), consumer); + break; + case FOOTER: + printTextParagraphs(ts.getTextParagraphs(), footerCon); + break; + case SLIDE_NUMBER: + printTextParagraphs(ts.getTextParagraphs(), footerCon, "\n", SlideShowExtractor::replaceSlideNumber); + break; + case DATETIME: + // currently not supported + default: + break; + } } - sb.append(headerStr); } - private void printShapeText(final Sheet sheet, final StringBuilder sb) { - final String footer = printHeaderReturnFooter(sheet, sb); - printShapeText((ShapeContainer)sheet, sb); - sb.append(footer); + + private void addSheetPlaceholderDatails(final Sheet sheet, final Placeholder placeholder, final Consumer consumer) { + final PlaceholderDetails headerPD = sheet.getPlaceholderDetails(placeholder); + final String headerStr = (headerPD != null) ? headerPD.getText() : null; + if (headerStr != null && filter.test(headerPD)) { + consumer.accept(headerStr); + } + } + + private void printShapeText(final Sheet sheet, final Consumer consumer) { + final List footer = new LinkedList<>(); + printHeaderFooter(sheet, consumer, footer::add); + printShapeText((ShapeContainer)sheet, consumer); + footer.forEach(consumer); } @SuppressWarnings("unchecked") - private void printShapeText(final ShapeContainer container, final StringBuilder sb) { + private void printShapeText(final ShapeContainer container, final Consumer consumer) { for (Shape shape : container) { if (shape instanceof TextShape) { - printShapeText((TextShape)shape, sb); + printTextParagraphs(((TextShape)shape).getTextParagraphs(), consumer); } else if (shape instanceof TableShape) { - printShapeText((TableShape)shape, sb); + printShapeText((TableShape)shape, consumer); } else if (shape instanceof ShapeContainer) { - printShapeText((ShapeContainer)shape, sb); + printShapeText((ShapeContainer)shape, consumer); } } } - private void printShapeText(final TextShape shape, final StringBuilder sb) { - final List

paraList = shape.getTextParagraphs(); - if (paraList.isEmpty()) { - sb.append('\n'); - return; - } - for (final P para : paraList) { - for (final TextRun tr : para) { - final String str = tr.getRawText().replace("\r", ""); - final String newStr; - switch (tr.getTextCap()) { - case ALL: - newStr = str.toUpperCase(LocaleUtil.getUserLocale()); - break; - case SMALL: - newStr = str.toLowerCase(LocaleUtil.getUserLocale()); - break; - default: - case NONE: - newStr = str; - break; - } - sb.append(newStr); - } - sb.append('\n'); - } - } - @SuppressWarnings("Duplicates") - private void printShapeText(final TableShape shape, final StringBuilder sb) { + private void printShapeText(final TableShape shape, final Consumer consumer) { final int nrows = shape.getNumberOfRows(); final int ncols = shape.getNumberOfColumns(); - for (int row = 0; row < nrows; row++){ + for (int row = 0; row < nrows; row++) { + String trailer = ""; for (int col = 0; col < ncols; col++){ TableCell cell = shape.getCell(row, col); //defensive null checks; don't know if they're necessary - if (cell != null){ - String txt = cell.getText(); - txt = (txt == null) ? "" : txt; - sb.append(txt); - if (col < ncols-1){ - sb.append('\t'); - } + if (cell != null) { + trailer = col < ncols-1 ? "\t" : "\n"; + printTextParagraphs(cell.getTextParagraphs(), consumer, trailer); } } - sb.append('\n'); + if (!trailer.equals("\n") && filter.test("\n")) { + consumer.accept("\n"); + } } } - private void printComments(final Slide slide, final StringBuilder sb) { - for (final Comment comment : slide.getComments()) { - sb.append(comment.getAuthor()); - sb.append(" - "); - sb.append(comment.getText()); - sb.append("\n"); - } + private void printComments(final Slide slide, final Consumer consumer) { + slide.getComments().stream().filter(filter).map(c -> c.getAuthor()+" - "+c.getText()).forEach(consumer); } - private void printNotes(final Slide slide, final StringBuilder sb) { + private void printNotes(final Slide slide, final Consumer consumer) { final Notes notes = slide.getNotes(); if (notes == null) { return; } - final String footer = printHeaderReturnFooter(notes, sb); - - printShapeText(notes, sb); - - sb.append(footer); + List footer = new LinkedList<>(); + printHeaderFooter(notes, consumer, footer::add); + printShapeText(notes, consumer); + footer.forEach(consumer); } public List> getOLEShapes() { @@ -342,4 +330,83 @@ public class SlideShowExtractor< } } } + + private static String replaceSlideNumber(TextRun tr) { + String raw = tr.getRawText(); + + if (!raw.contains(SLIDE_NUMBER_PH)) { + return raw; + } + + TextParagraph tp = tr.getParagraph(); + TextShape ps = (tp != null) ? tp.getParentShape() : null; + Sheet sh = (ps != null) ? ps.getSheet() : null; + String slideNr = (sh instanceof Slide) ? Integer.toString(((Slide)sh).getSlideNumber() + 1) : ""; + + return raw.replace(SLIDE_NUMBER_PH, slideNr); + } + + private static String replaceTextCap(TextRun tr) { + final TextParagraph tp = tr.getParagraph(); + final TextShape sh = (tp != null) ? tp.getParentShape() : null; + final Placeholder ph = (sh != null) ? sh.getPlaceholder() : null; + + // 0xB acts like cariage return in page titles and like blank in the others + final char sep = ( + ph == Placeholder.TITLE || + ph == Placeholder.CENTERED_TITLE || + ph == Placeholder.SUBTITLE + ) ? '\n' : ' '; + + // PowerPoint seems to store files with \r as the line break + // The messes things up on everything but a Mac, so translate them to \n + String txt = tr.getRawText(); + txt = txt.replace('\r', '\n'); + txt = txt.replace((char) 0x0B, sep); + + switch (tr.getTextCap()) { + case ALL: + txt = txt.toUpperCase(LocaleUtil.getUserLocale()); + case SMALL: + txt = txt.toLowerCase(LocaleUtil.getUserLocale()); + } + + return txt; + } + + /** + * Extract the used codepoints for font embedding / subsetting + * @param typeface the typeface/font family of the textruns to examine + * @param italic use {@code true} for italic TextRuns, {@code false} for non-italic ones and + * {@code null} if it doesn't matter + * @param bold use {@code true} for bold TextRuns, {@code false} for non-bold ones and + * {@code null} if it doesn't matter + * @return a bitset with the marked/used codepoints + */ + public BitSet getCodepoints(String typeface, Boolean italic, Boolean bold) { + final BitSet glyphs = new BitSet(); + + Predicate filterOld = filter; + try { + filter = o -> filterFonts(o, typeface, italic, bold); + slideshow.getSlides().forEach(slide -> + getText(slide, s -> s.codePoints().forEach(glyphs::set)) + ); + } finally { + filter = filterOld; + } + + return glyphs; + } + + private static boolean filterFonts(Object o, String typeface, Boolean italic, Boolean bold) { + if (!(o instanceof TextRun)) { + return false; + } + TextRun tr = (TextRun)o; + return + typeface.equalsIgnoreCase(tr.getFontFamily()) && + (italic == null || tr.isItalic() == italic) && + (bold == null || tr.isBold() == bold); + } } diff --git a/src/java/org/apache/poi/sl/usermodel/FontCollection.java b/src/java/org/apache/poi/sl/usermodel/FontCollection.java deleted file mode 100644 index 61278f4618..0000000000 --- a/src/java/org/apache/poi/sl/usermodel/FontCollection.java +++ /dev/null @@ -1,22 +0,0 @@ -/* ==================================================================== - 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.sl.usermodel; - -public interface FontCollection { - -} diff --git a/src/java/org/apache/poi/sl/usermodel/Resources.java b/src/java/org/apache/poi/sl/usermodel/Resources.java deleted file mode 100644 index 96170e50bd..0000000000 --- a/src/java/org/apache/poi/sl/usermodel/Resources.java +++ /dev/null @@ -1,29 +0,0 @@ -/* ==================================================================== - 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.sl.usermodel; - -/** - * Common SlideShow resources, such as fonts, pictures - * and multimedia data - */ -public interface Resources { - public FontCollection getFontCollection(); - - public PictureData[] getPictureData(); - public int addPictureData(PictureData pict); -} diff --git a/src/java/org/apache/poi/sl/usermodel/Slide.java b/src/java/org/apache/poi/sl/usermodel/Slide.java index 91b80f107e..7c0d566138 100644 --- a/src/java/org/apache/poi/sl/usermodel/Slide.java +++ b/src/java/org/apache/poi/sl/usermodel/Slide.java @@ -19,6 +19,7 @@ package org.apache.poi.sl.usermodel; import java.util.List; +@SuppressWarnings("unused") public interface Slide< S extends Shape, P extends TextParagraph @@ -82,7 +83,7 @@ public interface Slide< * * @since POI 4.0.0 */ - MasterSheet getSlideLayout(); + MasterSheet getSlideLayout(); /** * @return the slide name, defaults to "Slide[slideNumber]" diff --git a/src/java/org/apache/poi/sl/usermodel/SlideShow.java b/src/java/org/apache/poi/sl/usermodel/SlideShow.java index 175ad2b00e..751379de92 100644 --- a/src/java/org/apache/poi/sl/usermodel/SlideShow.java +++ b/src/java/org/apache/poi/sl/usermodel/SlideShow.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.extractor.POITextExtractor; import org.apache.poi.sl.usermodel.PictureData.PictureType; @@ -44,8 +45,6 @@ public interface SlideShow< */ List> getSlideMasters(); - Resources getResources(); - /** * Returns the current page size * @@ -135,4 +134,30 @@ public interface SlideShow< * @since POI 4.0.0 */ Object getPersistDocument(); + + /** + * Add an EOT font to the slideshow. + * An EOT or MTX font is a transformed True-Type (.ttf) or Open-Type (.otf) font. + * To transform a True-Type font use the sfntly library (see "see also" below)

+ * + * (Older?) Powerpoint versions handle embedded fonts by converting them to .ttf files + * and put them into the Windows fonts directory. If the user is not allowed to install + * fonts, the slideshow can't be opened. While the slideshow is opened, its possible + * to copy the extracted .ttfs from the fonts directory. When the slideshow is closed, + * they will be removed. + * + * @param fontData the EOT font as stream + * @return the font info object containing the new font data + * @throws IOException if the fontData can't be saved or if the fontData is no EOT font + * + * @see EOT specification + * @see googles sfntly library + * @see Example on how to subset and embed fonts + */ + FontInfo addFont(InputStream fontData) throws IOException; + + /** + * @return a list of registered fonts + */ + List getFonts(); } diff --git a/src/java/org/apache/poi/sl/usermodel/TextRun.java b/src/java/org/apache/poi/sl/usermodel/TextRun.java index 394166071c..7dfd4933d8 100644 --- a/src/java/org/apache/poi/sl/usermodel/TextRun.java +++ b/src/java/org/apache/poi/sl/usermodel/TextRun.java @@ -27,6 +27,7 @@ import org.apache.poi.util.Internal; /** * Some text. */ +@SuppressWarnings("unused") public interface TextRun { /** * Type of text capitals @@ -243,4 +244,11 @@ public interface TextRun { */ @Internal FieldType getFieldType(); + + /** + * @return the paragraph which contains this TextRun + * + * @since POI 4.1.0 + */ + TextParagraph getParagraph(); } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java index d596993fe8..ff12d6aad2 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java @@ -30,19 +30,20 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.OptionalLong; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.apache.poi.ooxml.POIXMLDocument; import org.apache.poi.ooxml.POIXMLDocumentPart; import org.apache.poi.ooxml.POIXMLException; import org.apache.poi.ooxml.extractor.POIXMLPropertiesTextExtractor; import org.apache.poi.ooxml.util.PackageHelper; -import org.apache.poi.openxml4j.exceptions.OpenXML4JException; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.sl.usermodel.MasterSheet; import org.apache.poi.sl.usermodel.PictureData.PictureType; -import org.apache.poi.sl.usermodel.Resources; import org.apache.poi.sl.usermodel.SlideShow; import org.apache.poi.util.Beta; import org.apache.poi.util.IOUtils; @@ -60,7 +61,6 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CTNotesMasterIdListE import org.openxmlformats.schemas.presentationml.x2006.main.CTPresentation; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideIdList; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideIdListEntry; -import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMasterIdListEntry; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideSize; import org.openxmlformats.schemas.presentationml.x2006.main.PresentationDocument; @@ -70,6 +70,7 @@ import org.openxmlformats.schemas.presentationml.x2006.main.PresentationDocument * they are reading or writing a slideshow. It is also the * top level object for creating new slides/etc. */ +@SuppressWarnings("WeakerAccess") @Beta public class XMLSlideShow extends POIXMLDocument implements SlideShow { @@ -78,10 +79,10 @@ public class XMLSlideShow extends POIXMLDocument private static final int MAX_RECORD_LENGTH = 1_000_000; private CTPresentation _presentation; - private List _slides; - private List _masters; - private List _pictures; - private List _charts; + private final List _slides = new ArrayList<>(); + private final List _masters = new ArrayList<>(); + private final List _pictures = new ArrayList<>(); + private final List _charts = new ArrayList<>(); private XSLFTableStyles _tableStyles; private XSLFNotesMaster _notesMaster; private XSLFCommentAuthors _commentAuthors; @@ -153,27 +154,26 @@ public class XMLSlideShow extends POIXMLDocument } } - _charts = new ArrayList<>(chartMap.size()); - for (XSLFChart chart : chartMap.values()) { - _charts.add(chart); - } + _charts.clear(); + _charts.addAll(chartMap.values()); - _masters = new ArrayList<>(masterMap.size()); - for (CTSlideMasterIdListEntry masterId : _presentation.getSldMasterIdLst().getSldMasterIdList()) { - XSLFSlideMaster master = masterMap.get(masterId.getId2()); - _masters.add(master); + _masters.clear(); + if (_presentation.isSetSldMasterIdLst()) { + _presentation.getSldMasterIdLst().getSldMasterIdList().forEach( + id -> _masters.add(masterMap.get(id.getId2())) + ); } - _slides = new ArrayList<>(shIdMap.size()); + _slides.clear(); if (_presentation.isSetSldIdLst()) { - for (CTSlideIdListEntry slId : _presentation.getSldIdLst().getSldIdList()) { - XSLFSlide sh = shIdMap.get(slId.getId2()); + _presentation.getSldIdLst().getSldIdList().forEach(id -> { + XSLFSlide sh = shIdMap.get(id.getId2()); if (sh == null) { - LOG.log(POILogger.WARN, "Slide with r:id " + slId.getId() + " was defined, but didn't exist in package, skipping"); - continue; + LOG.log(POILogger.WARN, "Slide with r:id " + id.getId() + " was defined, but didn't exist in package, skipping"); + } else { + _slides.add(sh); } - _slides.add(sh); - } + }); } } catch (XmlException e) { throw new POIXMLException(e); @@ -192,7 +192,7 @@ public class XMLSlideShow extends POIXMLDocument * Get the document's embedded files. */ @Override - public List getAllEmbeddedParts() throws OpenXML4JException { + public List getAllEmbeddedParts() { return Collections.unmodifiableList( getPackage().getPartsByName(Pattern.compile("/ppt/embeddings/.*?")) ); @@ -200,14 +200,12 @@ public class XMLSlideShow extends POIXMLDocument @Override public List getPictureData() { - if (_pictures == null) { - List mediaParts = getPackage().getPartsByName(Pattern.compile("/ppt/media/.*?")); - _pictures = new ArrayList<>(mediaParts.size()); - for (PackagePart part : mediaParts) { + if (_pictures.isEmpty()) { + getPackage().getPartsByName(Pattern.compile("/ppt/media/.*?")).forEach(part -> { XSLFPictureData pd = new XSLFPictureData(part); pd.setIndex(_pictures.size()); _pictures.add(pd); - } + }); } return Collections.unmodifiableList(_pictures); } @@ -219,20 +217,16 @@ public class XMLSlideShow extends POIXMLDocument * @return created slide */ public XSLFSlide createSlide(XSLFSlideLayout layout) { - int slideNumber = 256, cnt = 1; - CTSlideIdList slideList; - XSLFRelation relationType = XSLFRelation.SLIDE; - if (!_presentation.isSetSldIdLst()) { - slideList = _presentation.addNewSldIdLst(); - } else { - slideList = _presentation.getSldIdLst(); - for (CTSlideIdListEntry slideId : slideList.getSldIdArray()) { - slideNumber = (int) Math.max(slideId.getId() + 1, slideNumber); - cnt++; - } + CTSlideIdList slideList = _presentation.isSetSldIdLst() + ? _presentation.getSldIdLst() : _presentation.addNewSldIdLst(); - cnt = findNextAvailableFileNameIndex(relationType, cnt); - } + @SuppressWarnings("deprecation") + OptionalLong maxId = Stream.of(slideList.getSldIdArray()) + .mapToLong(CTSlideIdListEntry::getId).max(); + + final XSLFRelation relationType = XSLFRelation.SLIDE; + final int slideNumber = (int)(Math.max(maxId.orElse(0),255)+1); + final int cnt = findNextAvailableFileNameIndex(relationType); RelationPart rp = createRelationship (relationType, XSLFFactory.getInstance(), cnt, false); @@ -250,33 +244,14 @@ public class XMLSlideShow extends POIXMLDocument return slide; } - private int findNextAvailableFileNameIndex(XSLFRelation relationType, int idx) { + private int findNextAvailableFileNameIndex(XSLFRelation relationType) { // Bug 55791: We also need to check that the resulting file name is not already taken // this can happen when removing/adding slides, notes or charts - while (true) { - String fileName = relationType.getFileName(idx); - boolean found = false; - for (POIXMLDocumentPart relation : getRelations()) { - if (relation.getPackagePart() != null && - fileName.equals(relation.getPackagePart().getPartName().getName())) { - // name is taken => try next one - found = true; - break; - } - } - - if (!found && - getPackage().getPartsByName(Pattern.compile(Pattern.quote(fileName))).size() > 0) { - // name is taken => try next one - found = true; - } - - if (!found) { - break; - } - idx++; + try { + return getPackage().getUnusedPartIndex(relationType.getDefaultFileName()); + } catch (InvalidFormatException e) { + throw new RuntimeException(e); } - return idx; } /** @@ -313,8 +288,8 @@ public class XMLSlideShow extends POIXMLDocument * @since POI 4.1.0 */ public XSLFChart createChart() { - int chartIdx = findNextAvailableFileNameIndex(XSLFRelation.CHART, _charts.size() + 1); - XSLFChart chart = (XSLFChart) createRelationship(XSLFRelation.CHART, XSLFFactory.getInstance(), chartIdx, true).getDocumentPart(); + int chartIdx = findNextAvailableFileNameIndex(XSLFRelation.CHART); + XSLFChart chart = createRelationship(XSLFRelation.CHART, XSLFFactory.getInstance(), chartIdx, true).getDocumentPart(); chart.setChartIndex(chartIdx); _charts.add(chart); return chart; @@ -341,10 +316,8 @@ public class XMLSlideShow extends POIXMLDocument createNotesMaster(); } - int slideIndex = XSLFRelation.SLIDE.getFileNameIndex(slide); - XSLFRelation relationType = XSLFRelation.NOTES; - slideIndex = findNextAvailableFileNameIndex(relationType, slideIndex); + int slideIndex = findNextAvailableFileNameIndex(relationType); // add notes slide to presentation XSLFNotes notesSlide = (XSLFNotes) createRelationship @@ -453,6 +426,7 @@ public class XMLSlideShow extends POIXMLDocument // fix ordering in the low-level xml CTSlideIdList sldIdLst = _presentation.getSldIdLst(); + @SuppressWarnings("deprecation") CTSlideIdListEntry[] entries = sldIdLst.getSldIdArray(); CTSlideIdListEntry oldEntry = entries[oldIndex]; if (oldIndex < newIndex) { @@ -517,14 +491,21 @@ public class XMLSlideShow extends POIXMLDocument return img; } - int imageNumber = _pictures.size(); + XSLFRelation relType = XSLFPictureData.getRelationForType(format); if (relType == null) { throw new IllegalArgumentException("Picture type " + format + " is not supported."); } - img = createRelationship(relType, XSLFFactory.getInstance(), imageNumber + 1, true).getDocumentPart(); - img.setIndex(imageNumber); + int imageNumber; + try { + imageNumber = getPackage().getUnusedPartIndex("/ppt/media/image#\\..+"); + } catch (InvalidFormatException e) { + imageNumber = _pictures.size() + 1; + } + + img = createRelationship(relType, XSLFFactory.getInstance(), imageNumber, true).getDocumentPart(); + img.setIndex(_pictures.size()); _pictures.add(img); try (OutputStream out = img.getPackagePart().getOutputStream()) { @@ -624,18 +605,13 @@ public class XMLSlideShow extends POIXMLDocument return null; } + @SuppressWarnings("RedundantThrows") @Override public MasterSheet createMasterSheet() throws IOException { // TODO: implement! throw new UnsupportedOperationException(); } - @Override - public Resources getResources() { - // TODO: implement! - throw new UnsupportedOperationException(); - } - @Override public POIXMLPropertiesTextExtractor getMetadataTextExtractor() { return new POIXMLPropertiesTextExtractor(this); @@ -645,4 +621,14 @@ public class XMLSlideShow extends POIXMLDocument public Object getPersistDocument() { return this; } + + @Override + public XSLFFontInfo addFont(InputStream fontStream) throws IOException { + return XSLFFontInfo.addFontToSlideShow(this, fontStream); + } + + @Override + public List getFonts() { + return XSLFFontInfo.getFonts(this); + } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontData.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontData.java new file mode 100644 index 0000000000..8b2cb09ebc --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontData.java @@ -0,0 +1,84 @@ +/* + * ==================================================================== + * 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.xslf.usermodel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.util.Beta; + +/** + * A container for fontdata files, i.e. MTX fonts derived from + * true (TTF) or open (OTF) type fonts. + * + * @since POI 4.1.0 + */ +@Beta +public class XSLFFontData extends POIXMLDocumentPart { + /** + * Create a new XSLFFontData node + */ + @SuppressWarnings("unused") + protected XSLFFontData() { + super(); + } + + /** + * Construct XSLFFontData from a package part + * + * @param part the package part holding the ole data + */ + @SuppressWarnings("unused") + public XSLFFontData(final PackagePart part) { + super(part); + } + + public InputStream getInputStream() throws IOException { + return getPackagePart().getInputStream(); + } + + public OutputStream getOutputStream() { + final PackagePart pp = getPackagePart(); + pp.clear(); + return pp.getOutputStream(); + } + + /** + * XSLFFontData objects store the actual content in the part directly without keeping a + * copy like all others therefore we need to handle them differently. + */ + @Override + protected void prepareForCommit() { + // do not clear the part here + } + + + public void setData(final byte[] data) throws IOException { + try (final OutputStream os = getPackagePart().getOutputStream()) { + os.write(data); + } + } + + + +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontInfo.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontInfo.java new file mode 100644 index 0000000000..1e7462113e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFFontInfo.java @@ -0,0 +1,285 @@ +/* + * ==================================================================== + * 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.xslf.usermodel; + +import java.awt.Font; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.poi.common.usermodel.fonts.FontCharset; +import org.apache.poi.common.usermodel.fonts.FontFacet; +import org.apache.poi.common.usermodel.fonts.FontFamily; +import org.apache.poi.common.usermodel.fonts.FontHeader; +import org.apache.poi.common.usermodel.fonts.FontInfo; +import org.apache.poi.common.usermodel.fonts.FontPitch; +import org.apache.poi.ooxml.POIXMLDocumentPart; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.util.IOUtils; +import org.openxmlformats.schemas.drawingml.x2006.main.CTTextFont; +import org.openxmlformats.schemas.presentationml.x2006.main.CTEmbeddedFontDataId; +import org.openxmlformats.schemas.presentationml.x2006.main.CTEmbeddedFontList; +import org.openxmlformats.schemas.presentationml.x2006.main.CTEmbeddedFontListEntry; +import org.openxmlformats.schemas.presentationml.x2006.main.CTPresentation; + +@SuppressWarnings("WeakerAccess") +public class XSLFFontInfo implements FontInfo { + final XMLSlideShow ppt; + final String typeface; + final CTEmbeddedFontListEntry fontListEntry; + + public XSLFFontInfo(XMLSlideShow ppt, String typeface) { + this.ppt = ppt; + this.typeface = typeface; + + final CTPresentation pres = ppt.getCTPresentation(); + CTEmbeddedFontList fontList = pres.isSetEmbeddedFontLst() + ? pres.getEmbeddedFontLst() : pres.addNewEmbeddedFontLst(); + + for (CTEmbeddedFontListEntry fe : fontList.getEmbeddedFontArray()) { + if (typeface.equalsIgnoreCase(fe.getFont().getTypeface())) { + fontListEntry = fe; + return; + } + } + + fontListEntry = fontList.addNewEmbeddedFont(); + fontListEntry.addNewFont().setTypeface(typeface); + } + + public XSLFFontInfo(XMLSlideShow ppt, CTEmbeddedFontListEntry fontListEntry) { + this.ppt = ppt; + this.typeface = fontListEntry.getFont().getTypeface(); + this.fontListEntry = fontListEntry; + } + + @Override + public String getTypeface() { + return getFont().getTypeface(); + } + + @Override + public void setTypeface(String typeface) { + getFont().setTypeface(typeface); + } + + @Override + public FontCharset getCharset() { + return FontCharset.valueOf(getFont().getCharset()); + } + + @Override + public void setCharset(FontCharset charset) { + getFont().setCharset((byte)charset.getNativeId()); + } + + @Override + public FontFamily getFamily() { + return FontFamily.valueOfPitchFamily(getFont().getPitchFamily()); + } + + @Override + public void setFamily(FontFamily family) { + byte pitchAndFamily = getFont().getPitchFamily(); + FontPitch pitch = FontPitch.valueOfPitchFamily(pitchAndFamily); + getFont().setPitchFamily(FontPitch.getNativeId(pitch, family)); + } + + @Override + public FontPitch getPitch() { + return FontPitch.valueOfPitchFamily(getFont().getPitchFamily()); + } + + @Override + public void setPitch(FontPitch pitch) { + byte pitchAndFamily = getFont().getPitchFamily(); + FontFamily family = FontFamily.valueOfPitchFamily(pitchAndFamily); + getFont().setPitchFamily(FontPitch.getNativeId(pitch, family)); + } + + @Override + public byte[] getPanose() { + return getFont().getPanose(); + } + + @Override + public List getFacets() { + List facetList = new ArrayList<>(); + if (fontListEntry.isSetRegular()) { + facetList.add(new XSLFFontFacet((fontListEntry.getRegular()))); + } + if (fontListEntry.isSetItalic()) { + facetList.add(new XSLFFontFacet((fontListEntry.getItalic()))); + } + if (fontListEntry.isSetBold()) { + facetList.add(new XSLFFontFacet((fontListEntry.getBold()))); + } + if (fontListEntry.isSetBoldItalic()) { + facetList.add(new XSLFFontFacet((fontListEntry.getBoldItalic()))); + } + return facetList; + } + + public FontFacet addFacet(InputStream fontData) throws IOException { + FontHeader header = new FontHeader(); + InputStream is = header.bufferInit(fontData); + + final CTPresentation pres = ppt.getCTPresentation(); + pres.setEmbedTrueTypeFonts(true); + pres.setSaveSubsetFonts(true); + + final CTEmbeddedFontDataId dataId; + final int style = + (header.getWeight() > 400 ? Font.BOLD : Font.PLAIN) | + (header.isItalic() ? Font.ITALIC : Font.PLAIN); + switch (style) { + case Font.PLAIN: + dataId = fontListEntry.isSetRegular() + ? fontListEntry.getRegular() : fontListEntry.addNewRegular(); + break; + case Font.BOLD: + dataId = fontListEntry.isSetBold() + ? fontListEntry.getBold() : fontListEntry.addNewBold(); + break; + case Font.ITALIC: + dataId = fontListEntry.isSetItalic() + ? fontListEntry.getItalic() : fontListEntry.addNewItalic(); + break; + default: + dataId = fontListEntry.isSetBoldItalic() + ? fontListEntry.getBoldItalic() : fontListEntry.addNewBoldItalic(); + break; + } + + XSLFFontFacet facet = new XSLFFontFacet(dataId); + facet.setFontData(is); + return facet; + } + + private final class XSLFFontFacet implements FontFacet { + private final CTEmbeddedFontDataId fontEntry; + private final FontHeader header = new FontHeader(); + + private XSLFFontFacet(CTEmbeddedFontDataId fontEntry) { + this.fontEntry = fontEntry; + } + + @Override + public int getWeight() { + init(); + return header.getWeight(); + } + + @Override + public boolean isItalic() { + init(); + return header.isItalic(); + } + + @Override + public XSLFFontData getFontData() { + return ppt.getRelationPartById(fontEntry.getId()).getDocumentPart(); + } + + void setFontData(InputStream is) throws IOException { + final XSLFRelation fntRel = XSLFRelation.FONT; + final String relId = fontEntry.getId(); + final XSLFFontData fntData; + if (relId == null || relId.isEmpty()) { + final int fntDataIdx; + try { + fntDataIdx = ppt.getPackage().getUnusedPartIndex(fntRel.getDefaultFileName()); + } catch (InvalidFormatException e) { + throw new RuntimeException(e); + } + + POIXMLDocumentPart.RelationPart rp = ppt.createRelationship(fntRel, XSLFFactory.getInstance(), fntDataIdx, false); + fntData = rp.getDocumentPart(); + fontEntry.setId(rp.getRelationship().getId()); + } else { + fntData = (XSLFFontData)ppt.getRelationById(relId); + } + + assert (fntData != null); + try (OutputStream os = fntData.getOutputStream()) { + IOUtils.copy(is, os); + } + } + + private void init() { + if (header.getFamilyName() == null) { + try (InputStream is = getFontData().getInputStream()) { + byte[] buf = IOUtils.toByteArray(is, 1000); + header.init(buf, 0, buf.length); + } catch (IOException e) { + // TODO: better exception class + throw new RuntimeException(e); + } + } + } + } + + + private CTTextFont getFont() { + return fontListEntry.getFont(); + } + + + + /** + * Adds or updates a (MTX-) font + * @param ppt the slideshow which will contain the font + * @param fontStream the (MTX) font data as stream + * @return a font data object + * @throws IOException if the font data can't be stored + * + * @since POI 4.1.0 + */ + public static XSLFFontInfo addFontToSlideShow(XMLSlideShow ppt, InputStream fontStream) + throws IOException { + FontHeader header = new FontHeader(); + InputStream is = header.bufferInit(fontStream); + + XSLFFontInfo fontInfo = new XSLFFontInfo(ppt, header.getFamilyName()); + fontInfo.addFacet(is); + return fontInfo; + } + + /** + * Return all registered fonts + * @param ppt the slideshow containing the fonts + * @return the list of registered fonts + */ + public static List getFonts(XMLSlideShow ppt) { + final CTPresentation pres = ppt.getCTPresentation(); + + //noinspection deprecation + return pres.isSetEmbeddedFontLst() + ? Stream.of(pres.getEmbeddedFontLst().getEmbeddedFontArray()) + .map(fe -> new XSLFFontInfo(ppt, fe)).collect(Collectors.toList()) + : Collections.emptyList(); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRelation.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRelation.java index 9b9fc9c844..ca2d9d7ba2 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRelation.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFRelation.java @@ -260,6 +260,13 @@ public final class XSLFRelation extends POIXMLRelation { XSLFObjectData.class ); + public static final XSLFRelation FONT = new XSLFRelation( + "application/x-fontdata", + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font", + "/ppt/fonts/font#.fntdata", + XSLFFontData.class + ); + private XSLFRelation(String type, String rel, String defaultName, Class cls) { super(type, rel, defaultName, cls); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java index be714cbd85..6d2d215181 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFTextRun.java @@ -28,6 +28,7 @@ import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.sl.draw.DrawPaint; import org.apache.poi.sl.usermodel.PaintStyle; import org.apache.poi.sl.usermodel.PaintStyle.SolidPaint; +import org.apache.poi.sl.usermodel.TextParagraph; import org.apache.poi.sl.usermodel.TextRun; import org.apache.poi.util.Beta; import org.apache.poi.util.POILogFactory; @@ -628,16 +629,6 @@ public class XSLFTextRun implements TextRun { } } - @Override - public Integer getIndex() { - return null; - } - - @Override - public void setIndex(int index) { - throw new UnsupportedOperationException("setIndex not supported by XSLFFontInfo."); - } - @Override public String getTypeface() { CTTextFont tf = getXmlObject(false); @@ -829,4 +820,9 @@ public class XSLFTextRun implements TextRun { return font; } } + + @Override + public XSLFTextParagraph getParagraph() { + return _p; + } } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/extractor/TestXSLFPowerPointExtractor.java b/src/ooxml/testcases/org/apache/poi/xslf/extractor/TestXSLFPowerPointExtractor.java index e63762d290..3f71533dd5 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/extractor/TestXSLFPowerPointExtractor.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/extractor/TestXSLFPowerPointExtractor.java @@ -59,7 +59,7 @@ public class TestXSLFPowerPointExtractor { // Check Basics assertStartsWith(text, "Lorem ipsum dolor sit amet\n"); - assertContains(text, "amet\n\n"); + assertContains(text, "amet\n"); // Our placeholder master text // This shouldn't show up in the output @@ -96,7 +96,7 @@ public class TestXSLFPowerPointExtractor { extractor.setSlidesByDefault(false); extractor.setNotesByDefault(true); text = extractor.getText(); - assertEquals("\n\n1\n\n\n2\n", text); + assertEquals("\n1\n\n2\n", text); // Both extractor.setSlidesByDefault(true); @@ -105,14 +105,14 @@ public class TestXSLFPowerPointExtractor { String bothText = "Lorem ipsum dolor sit amet\n" + "Nunc at risus vel erat tempus posuere. Aenean non ante.\n" + - "\n\n\n1\n" + + "\n\n1\n" + "Lorem ipsum dolor sit amet\n" + "Lorem\n" + "ipsum\n" + "dolor\n" + "sit\n" + "amet\n" + - "\n\n\n2\n"; + "\n\n2\n"; assertEquals(bothText, text); // With Slides and Master Text @@ -141,21 +141,21 @@ public class TestXSLFPowerPointExtractor { String snmText = "Lorem ipsum dolor sit amet\n" + "Nunc at risus vel erat tempus posuere. Aenean non ante.\n" + - "\n\n\n1\n" + + "\n\n1\n" + "Lorem ipsum dolor sit amet\n" + "Lorem\n" + "ipsum\n" + "dolor\n" + "sit\n" + "amet\n" + - "\n\n\n2\n"; + "\n\n2\n"; assertEquals(snmText, text); // Via set defaults extractor.setSlidesByDefault(false); extractor.setNotesByDefault(true); text = extractor.getText(); - assertEquals("\n\n1\n\n\n2\n", text); + assertEquals("\n1\n\n2\n", text); } } diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java index afaf8d65b5..bb6167ee2f 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXMLSlideShow.java @@ -38,7 +38,7 @@ import org.openxmlformats.schemas.presentationml.x2006.main.CTNotesMasterIdListE import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideIdListEntry; import org.openxmlformats.schemas.presentationml.x2006.main.CTSlideMasterIdListEntry; -public class TestXMLSlideShow extends BaseTestSlideShow { +public class TestXMLSlideShow extends BaseTestSlideShow { private OPCPackage pack; @Override @@ -81,6 +81,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { xml.close(); } + @SuppressWarnings("deprecation") @Test public void testSlideBasics() throws IOException { XMLSlideShow xml = new XMLSlideShow(pack); @@ -136,7 +137,7 @@ public class TestXMLSlideShow extends BaseTestSlideShow { assertEquals(0, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getCharacters()); assertEquals(0, xml.getProperties().getExtendedProperties().getUnderlyingProperties().getLines()); - assertEquals(null, xml.getProperties().getCoreProperties().getTitle()); + assertNull(xml.getProperties().getCoreProperties().getTitle()); assertFalse(xml.getProperties().getCoreProperties().getUnderlyingProperties().getSubjectProperty().isPresent()); xml.close(); @@ -146,8 +147,8 @@ public class TestXMLSlideShow extends BaseTestSlideShow { public void testComments() throws Exception { // Default sample file has none XMLSlideShow xml = new XMLSlideShow(pack); - - assertEquals(null, xml.getCommentAuthors()); + + assertNull(xml.getCommentAuthors()); for (XSLFSlide slide : xml.getSlides()) { assertTrue(slide.getComments().isEmpty()); @@ -186,19 +187,16 @@ public class TestXMLSlideShow extends BaseTestSlideShow { xml.close(); } - public SlideShow reopen(SlideShow show) { - return reopen((XMLSlideShow)show); - } - - private static XMLSlideShow reopen(XMLSlideShow show) { - try { - BufAccessBAOS bos = new BufAccessBAOS(); - show.write(bos); - return new XMLSlideShow(new ByteArrayInputStream(bos.getBuf())); - } catch (IOException e) { - fail(e.getMessage()); - return null; - } + @Override + public XMLSlideShow reopen(SlideShow show) { + try { + BufAccessBAOS bos = new BufAccessBAOS(); + show.write(bos); + return new XMLSlideShow(new ByteArrayInputStream(bos.getBuf())); + } catch (IOException e) { + fail(e.getMessage()); + return null; + } } private static class BufAccessBAOS extends ByteArrayOutputStream { diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/Document.java b/src/scratchpad/src/org/apache/poi/hslf/record/Document.java index 5e4019b001..4fd37a9b35 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/Document.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/Document.java @@ -87,11 +87,11 @@ public final class Document extends PositionDependentRecordContainer * Master Slides */ public SlideListWithText getMasterSlideListWithText() { - for (int i = 0; i < slwts.length; i++) { - if(slwts[i].getInstance() == SlideListWithText.MASTER) { - return slwts[i]; - } - } + for (SlideListWithText slwt : slwts) { + if (slwt.getInstance() == SlideListWithText.MASTER) { + return slwt; + } + } return null; } @@ -100,11 +100,11 @@ public final class Document extends PositionDependentRecordContainer * Slides, or null if there isn't one */ public SlideListWithText getSlideSlideListWithText() { - for (int i = 0; i < slwts.length; i++) { - if(slwts[i].getInstance() == SlideListWithText.SLIDES) { - return slwts[i]; - } - } + for (SlideListWithText slwt : slwts) { + if (slwt.getInstance() == SlideListWithText.SLIDES) { + return slwt; + } + } return null; } /** @@ -112,11 +112,11 @@ public final class Document extends PositionDependentRecordContainer * notes, or null if there isn't one */ public SlideListWithText getNotesSlideListWithText() { - for (int i = 0; i < slwts.length; i++) { - if(slwts[i].getInstance() == SlideListWithText.NOTES) { - return slwts[i]; - } - } + for (SlideListWithText slwt : slwts) { + if (slwt.getInstance() == SlideListWithText.NOTES) { + return slwt; + } + } return null; } @@ -124,7 +124,7 @@ public final class Document extends PositionDependentRecordContainer /** * Set things up, and find our more interesting children */ - protected Document(byte[] source, int start, int len) { + /* package */ Document(byte[] source, int start, int len) { // Grab the header _header = new byte[8]; System.arraycopy(source,start,_header,0,8); @@ -186,7 +186,7 @@ public final class Document extends PositionDependentRecordContainer // The new SlideListWithText should go in // just before the EndDocumentRecord Record endDoc = _children[_children.length - 1]; - if(endDoc.getRecordType() == RecordTypes.RoundTripCustomTableStyles12Atom.typeID) { + if(endDoc.getRecordType() == RecordTypes.RoundTripCustomTableStyles12.typeID) { // last record can optionally be a RoundTripCustomTableStyles12Atom endDoc = _children[_children.length - 2]; } @@ -213,7 +213,7 @@ public final class Document extends PositionDependentRecordContainer removeChild(slwt); } } - slwts = lst.toArray(new SlideListWithText[lst.size()]); + slwts = lst.toArray(new SlideListWithText[0]); } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentAtom.java index e2c49a9f35..cf01f0b2c9 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/DocumentAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/DocumentAtom.java @@ -17,25 +17,25 @@ package org.apache.poi.hslf.record; -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.LittleEndian; import java.io.IOException; import java.io.OutputStream; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndianByteArrayInputStream; + /** * A Document Atom (type 1001). Holds misc information on the PowerPoint * document, lots of them size and scale related. - * - * @author Nick Burch */ +@SuppressWarnings({"WeakerAccess", "unused"}) public final class DocumentAtom extends RecordAtom { //arbitrarily selected; may need to increase private static final int MAX_RECORD_LENGTH = 1_000_000; - private byte[] _header; - private static long _type = 1001l; + private final byte[] _header = new byte[8]; + private static long _type = RecordTypes.DocumentAtom.typeID; private long slideSizeX; // PointAtom, assume 1st 4 bytes = X private long slideSizeY; // PointAtom, assume 2nd 4 bytes = Y @@ -87,6 +87,11 @@ public final class DocumentAtom extends RecordAtom return saveWithFonts != 0; } + /** Set the font embedding state */ + public void setSaveWithFonts(boolean saveWithFonts) { + this.saveWithFonts = (byte)(saveWithFonts ? 1 : 0); + } + /** Have the placeholders on the title slide been omitted? */ public boolean getOmitTitlePlace() { return omitTitlePlace != 0; @@ -108,41 +113,41 @@ public final class DocumentAtom extends RecordAtom /** * For the Document Atom */ - protected DocumentAtom(byte[] source, int start, int len) { - // Sanity Checking - if(len < 48) { len = 48; } + /* package */ DocumentAtom(byte[] source, int start, int len) { + final int maxLen = Math.max(len, 48); + LittleEndianByteArrayInputStream leis = + new LittleEndianByteArrayInputStream(source, start, maxLen); // Get the header - _header = new byte[8]; - System.arraycopy(source,start,_header,0,8); + leis.readFully(_header); // Get the sizes and zoom ratios - slideSizeX = LittleEndian.getInt(source,start+0+8); - slideSizeY = LittleEndian.getInt(source,start+4+8); - notesSizeX = LittleEndian.getInt(source,start+8+8); - notesSizeY = LittleEndian.getInt(source,start+12+8); - serverZoomFrom = LittleEndian.getInt(source,start+16+8); - serverZoomTo = LittleEndian.getInt(source,start+20+8); + slideSizeX = leis.readInt(); + slideSizeY = leis.readInt(); + notesSizeX = leis.readInt(); + notesSizeY = leis.readInt(); + serverZoomFrom = leis.readInt(); + serverZoomTo = leis.readInt(); // Get the master persists - notesMasterPersist = LittleEndian.getInt(source,start+24+8); - handoutMasterPersist = LittleEndian.getInt(source,start+28+8); + notesMasterPersist = leis.readInt(); + handoutMasterPersist = leis.readInt(); // Get the ID of the first slide - firstSlideNum = LittleEndian.getShort(source,start+32+8); + firstSlideNum = leis.readShort(); // Get the slide size type - slideSizeType = LittleEndian.getShort(source,start+34+8); + slideSizeType = leis.readShort(); // Get the booleans as bytes - saveWithFonts = source[start+36+8]; - omitTitlePlace = source[start+37+8]; - rightToLeft = source[start+38+8]; - showComments = source[start+39+8]; + saveWithFonts = leis.readByte(); + omitTitlePlace = leis.readByte(); + rightToLeft = leis.readByte(); + showComments = leis.readByte(); // If there's any other bits of data, keep them about - reserved = IOUtils.safelyAllocate(len-40-8, MAX_RECORD_LENGTH); - System.arraycopy(source,start+48,reserved,0,reserved.length); + reserved = IOUtils.safelyAllocate(maxLen-48, MAX_RECORD_LENGTH); + leis.readFully(reserved); } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/FontCollection.java b/src/scratchpad/src/org/apache/poi/hslf/record/FontCollection.java index fef1e797d4..bddb79efc6 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/FontCollection.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/FontCollection.java @@ -18,13 +18,19 @@ package org.apache.poi.hslf.record; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import org.apache.poi.common.usermodel.fonts.FontHeader; import org.apache.poi.common.usermodel.fonts.FontInfo; +import org.apache.poi.common.usermodel.fonts.FontPitch; import org.apache.poi.hslf.usermodel.HSLFFontInfo; import org.apache.poi.hslf.usermodel.HSLFFontInfoPredefined; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.POILogger; /** @@ -32,11 +38,12 @@ import org.apache.poi.util.POILogger; * about all the fonts in the presentation. */ +@SuppressWarnings("WeakerAccess") public final class FontCollection extends RecordContainer { private final Map fonts = new LinkedHashMap<>(); private byte[] _header; - protected FontCollection(byte[] source, int start, int len) { + /* package */ FontCollection(byte[] source, int start, int len) { _header = new byte[8]; System.arraycopy(source,start,_header,0,8); @@ -44,8 +51,13 @@ public final class FontCollection extends RecordContainer { for (Record r : _children){ if(r instanceof FontEntityAtom) { - HSLFFontInfo fi = new HSLFFontInfo((FontEntityAtom)r); - fonts.put(fi.getTypeface(), fi); + HSLFFontInfo fi = new HSLFFontInfo((FontEntityAtom) r); + fonts.put(fi.getTypeface(), fi); + } else if (r instanceof FontEmbeddedData) { + FontEmbeddedData fed = (FontEmbeddedData)r; + FontHeader fontHeader = fed.getFontHeader(); + HSLFFontInfo fi = addFont(fontHeader); + fi.addFacet(fed); } else { logger.log(POILogger.WARN, "Warning: FontCollection child wasn't a FontEntityAtom, was " + r.getClass().getSimpleName()); } @@ -88,7 +100,7 @@ public final class FontCollection extends RecordContainer { fi = new HSLFFontInfo(fontInfo); fi.setIndex(fonts.size()); fonts.put(fi.getTypeface(), fi); - + FontEntityAtom fnt = fi.createRecord(); // Append new child to the end @@ -98,6 +110,58 @@ public final class FontCollection extends RecordContainer { return fi; } + public HSLFFontInfo addFont(InputStream fontData) throws IOException { + FontHeader fontHeader = new FontHeader(); + InputStream is = fontHeader.bufferInit(fontData); + + HSLFFontInfo fi = addFont(fontHeader); + + // always overwrite the font info properties when a font data given + // as the font info properties are assigned generically when only a typeface is given + FontEntityAtom fea = fi.getFontEntityAtom(); + assert (fea != null); + fea.setCharSet(fontHeader.getCharsetByte()); + fea.setPitchAndFamily(FontPitch.getNativeId(fontHeader.getPitch(),fontHeader.getFamily())); + + // always activate subsetting + fea.setFontFlags(1); + // true type font and no font substitution + fea.setFontType(12); + + Record after = fea; + + final int insertIdx = getFacetIndex(fontHeader.isItalic(), fontHeader.isBold()); + + FontEmbeddedData newChild = null; + for (FontEmbeddedData fed : fi.getFacets()) { + FontHeader fh = fed.getFontHeader(); + final int curIdx = getFacetIndex(fh.isItalic(), fh.isBold()); + + if (curIdx == insertIdx) { + newChild = fed; + break; + } else if (curIdx > insertIdx) { + // the new facet needs to be inserted before the current facet + break; + } + + after = fed; + } + + if (newChild == null) { + newChild = new FontEmbeddedData(); + addChildAfter(newChild, after); + fi.addFacet(newChild); + } + + newChild.setFontData(IOUtils.toByteArray(is)); + return fi; + } + + private static int getFacetIndex(boolean isItalic, boolean isBold) { + return (isItalic ? 2 : 0) | (isBold ? 1 : 0); + } + /** * Lookup a FontInfo object by its typeface @@ -132,4 +196,8 @@ public final class FontCollection extends RecordContainer { public int getNumberOfFonts() { return fonts.size(); } + + public List getFonts() { + return new ArrayList<>(fonts.values()); + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/FontEmbeddedData.java b/src/scratchpad/src/org/apache/poi/hslf/record/FontEmbeddedData.java new file mode 100644 index 0000000000..f60ba4a697 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hslf/record/FontEmbeddedData.java @@ -0,0 +1,116 @@ +/* ==================================================================== + 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 org.apache.poi.common.usermodel.fonts.FontFacet; +import org.apache.poi.common.usermodel.fonts.FontHeader; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; + +@SuppressWarnings("WeakerAccess") +public class FontEmbeddedData extends RecordAtom implements FontFacet { + //arbitrarily selected; may need to increase + private static final int MAX_RECORD_LENGTH = 1_000_000; + + /** + * Record header. + */ + private byte[] _header; + + /** + * Record data - An EOT Font + */ + private byte[] _data; + + /** + * Constructs a brand new font embedded record. + */ + /* package */ FontEmbeddedData() { + _header = new byte[8]; + _data = new byte[4]; + + LittleEndian.putShort(_header, 2, (short)getRecordType()); + LittleEndian.putInt(_header, 4, _data.length); + } + + /** + * Constructs the font embedded 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. + */ + /* package */ FontEmbeddedData(byte[] source, int start, int len) { + // Get the header. + _header = new byte[8]; + System.arraycopy(source,start,_header,0,8); + + // Get the record data. + _data = IOUtils.safelyAllocate(len-8, MAX_RECORD_LENGTH); + System.arraycopy(source,start+8,_data,0,len-8); + + // Must be at least 4 bytes long + if(_data.length < 4) { + throw new IllegalArgumentException("The length of the data for a ExObjListAtom must be at least 4 bytes, but was only " + _data.length); + } + } + + @Override + public long getRecordType() { + return RecordTypes.FontEmbeddedData.typeID; + } + + @Override + public void writeOut(OutputStream out) throws IOException { + out.write(_header); + out.write(_data); + } + + public void setFontData(byte[] fontData) { + _data = fontData.clone(); + LittleEndian.putInt(_header, 4, _data.length); + } + + public FontHeader getFontHeader() { + FontHeader h = new FontHeader(); + h.init(_data, 0, _data.length); + return h; + } + + @Override + public int getWeight() { + return getFontHeader().getWeight(); + } + + @Override + public boolean isItalic() { + return getFontHeader().isItalic(); + } + + public String getTypeface() { + return getFontHeader().getFamilyName(); + } + + @Override + public Object getFontData() { + return this; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/FontEntityAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/FontEntityAtom.java index 4f3cdd89bc..2d172d7b5e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/FontEntityAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/FontEntityAtom.java @@ -43,7 +43,7 @@ public final class FontEntityAtom extends RecordAtom { /** * record header */ - private byte[] _header; + private final byte[] _header = new byte[8]; /** * record data @@ -53,9 +53,8 @@ public final class FontEntityAtom extends RecordAtom { /** * Build an instance of FontEntityAtom from on-disk data */ - protected FontEntityAtom(byte[] source, int start, int len) { + /* package */ FontEntityAtom(byte[] source, int start, int len) { // Get the header - _header = new byte[8]; System.arraycopy(source,start,_header,0,8); // Grab the record data @@ -69,7 +68,6 @@ public final class FontEntityAtom extends RecordAtom { public FontEntityAtom() { _recdata = new byte[68]; - _header = new byte[8]; LittleEndian.putShort(_header, 2, (short)getRecordType()); LittleEndian.putInt(_header, 4, _recdata.length); } @@ -108,7 +106,7 @@ public final class FontEntityAtom extends RecordAtom { byte[] bytes = StringUtil.getToUnicodeLE(name); System.arraycopy(bytes, 0, _recdata, 0, bytes.length); // null the remaining bytes - Arrays.fill(_recdata, 64-bytes.length, 64, (byte)0); + Arrays.fill(_recdata, bytes.length, 64, (byte)0); } public void setFontIndex(int idx){ diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java index 233d54a67f..a1697d511e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java @@ -62,7 +62,18 @@ public enum RecordTypes { NamedShow(1041,null), NamedShowSlides(1042,null), SheetProperties(1044,null), - RoundTripCustomTableStyles12Atom(1064,null), + OriginalMainMasterId(1052,null), + CompositeMasterId(1052,null), + RoundTripContentMasterInfo12(1054,null), + RoundTripShapeId12(1055,null), + RoundTripHFPlaceholder12(1056,RoundTripHFPlaceholder12::new), + RoundTripContentMasterId(1058,null), + RoundTripOArtTextStyles12(1059,null), + RoundTripShapeCheckSumForCustomLayouts12(1062,null), + RoundTripNotesMasterTextStyles12(1063,null), + RoundTripCustomTableStyles12(1064,null), + + List(2000,DocInfoListContainer::new), FontCollection(2005,FontCollection::new), BookmarkCollection(2019,null), @@ -92,7 +103,7 @@ public enum RecordTypes { DefaultRulerAtom(4011,null), StyleTextProp9Atom(4012, StyleTextProp9Atom::new), //0x0FAC RT_StyleTextProp9Atom FontEntityAtom(4023,FontEntityAtom::new), - FontEmbeddedData(4024,null), + FontEmbeddedData(4024,FontEmbeddedData::new), CString(4026,CString::new), MetaFile(4033,null), ExOleObjAtom(4035,ExOleObjAtom::new), @@ -159,17 +170,6 @@ public enum RecordTypes { // Records ~12050 seem to be related to Document Encryption DocumentEncryptionAtom(12052,DocumentEncryptionAtom::new), - OriginalMainMasterId(1052,null), - CompositeMasterId(1052,null), - RoundTripContentMasterInfo12(1054,null), - RoundTripShapeId12(1055,null), - RoundTripHFPlaceholder12(1056,RoundTripHFPlaceholder12::new), - RoundTripContentMasterId(1058,null), - RoundTripOArtTextStyles12(1059,null), - RoundTripShapeCheckSumForCustomLayouts12(1062,null), - RoundTripNotesMasterTextStyles12(1063,null), - RoundTripCustomTableStyles12(1064,null), - // records greater then 0xF000 belong to with Microsoft Office Drawing format also known as Escher EscherDggContainer(0xF000,null), EscherDgg(0xf006,null), diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfo.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfo.java index 57c473173a..3b509276ca 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfo.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfo.java @@ -17,13 +17,18 @@ package org.apache.poi.hslf.usermodel; +import java.util.ArrayList; +import java.util.List; + import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.common.usermodel.fonts.FontFamily; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.common.usermodel.fonts.FontPitch; +import org.apache.poi.hslf.record.FontEmbeddedData; import org.apache.poi.hslf.record.FontEntityAtom; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.Internal; /** * Represents a Font used in a presentation.

@@ -32,6 +37,7 @@ import org.apache.poi.util.BitFieldFactory; * * @since POI 3.17-beta2 */ +@SuppressWarnings("WeakerAccess") public class HSLFFontInfo implements FontInfo { public enum FontRenderType { @@ -53,6 +59,8 @@ public class HSLFFontInfo implements FontInfo { private FontPitch pitch = FontPitch.VARIABLE; private boolean isSubsetted; private boolean isSubstitutable = true; + private final List facets = new ArrayList<>(); + private FontEntityAtom fontEntityAtom; /** * Creates a new instance of HSLFFontInfo with more or sensible defaults.

@@ -70,6 +78,7 @@ public class HSLFFontInfo implements FontInfo { * Creates a new instance of HSLFFontInfo and initialize it from the supplied font atom */ public HSLFFontInfo(FontEntityAtom fontAtom){ + fontEntityAtom = fontAtom; setIndex(fontAtom.getFontIndex()); setTypeface(fontAtom.getFontName()); setCharset(FontCharset.valueOf(fontAtom.getCharSet())); @@ -187,7 +196,11 @@ public class HSLFFontInfo implements FontInfo { } public FontEntityAtom createRecord() { + assert(fontEntityAtom == null); + FontEntityAtom fnt = new FontEntityAtom(); + fontEntityAtom = fnt; + fnt.setFontIndex(getIndex() << 4); fnt.setFontName(getTypeface()); fnt.setCharSet(getCharset().getNativeId()); @@ -212,4 +225,18 @@ public class HSLFFontInfo implements FontInfo { fnt.setPitchAndFamily(FontPitch.getNativeId(pitch, family)); return fnt; } + + public void addFacet(FontEmbeddedData facet) { + facets.add(facet); + } + + @Override + public List getFacets() { + return facets; + } + + @Internal + public FontEntityAtom getFontEntityAtom() { + return fontEntityAtom; + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfoPredefined.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfoPredefined.java index 1f016f99e5..d2dc44095e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfoPredefined.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFontInfoPredefined.java @@ -44,54 +44,24 @@ public enum HSLFFontInfoPredefined implements FontInfo { this.pitch = pitch; this.family = family; } - - @Override - public Integer getIndex() { - return -1; - } - - @Override - public void setIndex(int index) { - throw new UnsupportedOperationException("Predefined enum can't be changed."); - } @Override public String getTypeface() { return typeface; } - @Override - public void setTypeface(String typeface) { - throw new UnsupportedOperationException("Predefined enum can't be changed."); - } - @Override public FontCharset getCharset() { return charset; } - @Override - public void setCharset(FontCharset charset) { - throw new UnsupportedOperationException("Predefined enum can't be changed."); - } - @Override public FontFamily getFamily() { return family; } - @Override - public void setFamily(FontFamily family) { - throw new UnsupportedOperationException("Predefined enum can't be changed."); - } - @Override public FontPitch getPitch() { return pitch; } - - @Override - public void setPitch(FontPitch pitch) { - throw new UnsupportedOperationException("Predefined enum can't be changed."); - } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java index de90334840..2343ccc489 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFSlideShow.java @@ -50,7 +50,6 @@ import org.apache.poi.poifs.filesystem.Ole10Native; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.sl.usermodel.MasterSheet; import org.apache.poi.sl.usermodel.PictureData.PictureType; -import org.apache.poi.sl.usermodel.Resources; import org.apache.poi.sl.usermodel.SlideShow; import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; @@ -891,6 +890,21 @@ public final class HSLFSlideShow implements SlideShow getFonts() { + return getDocumentRecord().getEnvironment().getFontCollection().getFonts(); + } + /** * Return Header / Footer settings for slides * @@ -1127,12 +1146,6 @@ public final class HSLFSlideShow implements SlideShow T getMasterProp(final String name) { + private T getMasterProp() { final int txtype = parentParagraph.getRunType(); final HSLFSheet sheet = parentParagraph.getSheet(); if (sheet == null) { @@ -155,7 +156,8 @@ public final class HSLFTextRun implements TextRun { logger.log(POILogger.WARN, "MasterSheet is not available"); return null; } - + + String name = CharFlagsTextProp.NAME; final TextPropCollection col = master.getPropCollection(txtype, parentParagraph.getIndentLevel(), name, true); return (col == null) ? null : col.findByName(name); } @@ -302,7 +304,7 @@ public final class HSLFTextRun implements TextRun { @Override public void setFontFamily(String typeface) { - setFontInfo(new HSLFFontInfo(typeface), FontGroup.LATIN); + setFontFamily(typeface, FontGroup.LATIN); } @Override @@ -330,7 +332,7 @@ public final class HSLFTextRun implements TextRun { switch (fg) { default: case LATIN: - propName = "font.index"; + propName = "ansi.font.index"; break; case COMPLEX_SCRIPT: // TODO: implement TextCFException10 structure @@ -350,6 +352,7 @@ public final class HSLFTextRun implements TextRun { } + setCharTextPropVal("font.index", fontIdx); setCharTextPropVal(propName, fontIdx); } @@ -435,8 +438,8 @@ public final class HSLFTextRun implements TextRun { setFontColor(rgb); } - protected void setFlag(int index, boolean value) { - BitMaskTextProp prop = (BitMaskTextProp)characterStyle.addWithName(CharFlagsTextProp.NAME); + private void setFlag(int index, boolean value) { + BitMaskTextProp prop = characterStyle.addWithName(CharFlagsTextProp.NAME); prop.setSubValue(value, index); } @@ -469,7 +472,7 @@ public final class HSLFTextRun implements TextRun { * * @param link the hyperlink */ - protected void setHyperlink(HSLFHyperlink link) { + /* package */ void setHyperlink(HSLFHyperlink link) { this.link = link; } @@ -521,4 +524,9 @@ public final class HSLFTextRun implements TextRun { private FontGroup safeFontGroup(FontGroup fontGroup) { return (fontGroup != null) ? fontGroup : FontGroup.getFontGroupFirst(getRawText()); } + + @Override + public HSLFTextParagraph getParagraph() { + return parentParagraph; + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java index 621fa6a095..211c9fc263 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.common.usermodel.fonts.FontFamily; +import org.apache.poi.common.usermodel.fonts.FontHeader; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.common.usermodel.fonts.FontPitch; import org.apache.poi.util.BitField; @@ -32,6 +33,7 @@ import org.apache.poi.util.LittleEndianInputStream; /** * The Font object specifies the attributes of a logical font */ +@SuppressWarnings({"unused", "Duplicates"}) public class HwmfFont implements FontInfo { /** @@ -289,7 +291,7 @@ public class HwmfFont implements FontInfo { /** * An 8-bit unsigned integer that defines the character set. - * It SHOULD be set to a value in the {@link WmfCharset} Enumeration. + * It SHOULD be set to a value in the {@link FontCharset} Enumeration. * * The DEFAULT_CHARSET value MAY be used to allow the name and size of a font to fully * describe the logical font. If the specified font name does not exist, a font in another character @@ -373,7 +375,7 @@ public class HwmfFont implements FontInfo { height = -12; width = 0; escapement = 0; - weight = 400; + weight = FontHeader.REGULAR_WEIGHT; italic = false; underline = false; strikeOut = false; @@ -437,51 +439,21 @@ public class HwmfFont implements FontInfo { return FontFamily.valueOf(pitchAndFamily & 0xF); } - @Override - public void setFamily(FontFamily family) { - throw new UnsupportedOperationException("setCharset not supported by HwmfFont."); - } - @Override public FontPitch getPitch() { return FontPitch.valueOf((pitchAndFamily >>> 6) & 3); } - @Override - public void setPitch(FontPitch pitch) { - throw new UnsupportedOperationException("setPitch not supported by HwmfFont."); - } - - @Override - public Integer getIndex() { - return null; - } - - @Override - public void setIndex(int index) { - throw new UnsupportedOperationException("setIndex not supported by HwmfFont."); - } - @Override public String getTypeface() { return facename; } - @Override - public void setTypeface(String typeface) { - throw new UnsupportedOperationException("setTypeface not supported by HwmfFont."); - } - @Override public FontCharset getCharset() { return charSet; } - @Override - public void setCharset(FontCharset charset) { - throw new UnsupportedOperationException("setCharset not supported by HwmfFont."); - } - @Override public String toString() { return "{ height: "+height+ diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java index 78ca26ee3b..9fa47b4107 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/extractor/TestExtractor.java @@ -29,7 +29,9 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.BitSet; import java.util.List; +import java.util.stream.Collectors; import org.apache.poi.POIDataSamples; import org.apache.poi.hslf.usermodel.HSLFObjectShape; @@ -52,12 +54,22 @@ public final class TestExtractor { /** * Extractor primed on the 2 page basic test data */ - private static final String expectText = "This is a test title\nThis is a test subtitle\nThis is on page 1\nThis is the title on page 2\nThis is page two\nIt has several blocks of text\nNone of them have formatting\n"; + private static final String EXPECTED_PAGE1 = + "This is a test title\n" + + "This is a test subtitle\n\n" + + "This is on page 1\n"; - /** - * Extractor primed on the 1 page but text-box'd test data - */ - private static final String expectText2 = "Hello, World!!!\nI am just a poor boy\nThis is Times New Roman\nPlain Text \n"; + private static final String EXPECTED_PAGE2 = + "This is the title on page 2\n" + + "This is page two\n\n" + + "It has several blocks of text\n\n" + + "None of them have formatting\n"; + + private static final String NOTES_PAGE1 = + "\nThese are the notes for page 1\n"; + + private static final String NOTES_PAGE2 = + "\nThese are the notes on page two, again lacking formatting\n"; /** * Where our embeded files live @@ -75,9 +87,16 @@ public final class TestExtractor { public void testReadSheetText() throws IOException { // Basic 2 page example try (SlideShowExtractor ppe = openExtractor("basic_test_ppt_file.ppt")) { - assertEquals(expectText, ppe.getText()); + assertEquals(EXPECTED_PAGE1+EXPECTED_PAGE2, ppe.getText()); } + // Extractor primed on the 1 page but text-box'd test data + final String expectText2 = + "Hello, World!!!\n" + + "I am just a poor boy\n" + + "This is Times New Roman\n" + + "Plain Text \n"; + // 1 page example with text boxes try (SlideShowExtractor ppe = openExtractor("with_textbox.ppt")) { assertEquals(expectText2, ppe.getText()); @@ -92,8 +111,7 @@ public final class TestExtractor { ppe.setSlidesByDefault(false); ppe.setMasterByDefault(false); String notesText = ppe.getText(); - String expText = "\nThese are the notes for page 1\n\nThese are the notes on page two, again lacking formatting\n"; - assertEquals(expText, notesText); + assertEquals(NOTES_PAGE1+NOTES_PAGE2, notesText); } // Other one doesn't have notes @@ -109,14 +127,8 @@ public final class TestExtractor { @Test public void testReadBoth() throws IOException { - String[] slText = new String[]{ - "This is a test title\nThis is a test subtitle\nThis is on page 1\n", - "This is the title on page 2\nThis is page two\nIt has several blocks of text\nNone of them have formatting\n" - }; - String[] ntText = new String[]{ - "\nThese are the notes for page 1\n", - "\nThese are the notes on page two, again lacking formatting\n" - }; + String[] slText = { EXPECTED_PAGE1, EXPECTED_PAGE2 }; + String[] ntText = { NOTES_PAGE1, NOTES_PAGE2 }; try (SlideShowExtractor ppe = openExtractor("basic_test_ppt_file.ppt")) { ppe.setSlidesByDefault(true); @@ -165,8 +177,8 @@ public final class TestExtractor { final DirectoryNode root = fs.getRoot(); final String[] TEST_SET = { - "MBD0000A3B6", "Sample PowerPoint file\nThis is the 1st file\nNot much too it\n", - "MBD0000A3B3", "Sample PowerPoint file\nThis is the 2nd file\nNot much too it either\n" + "MBD0000A3B6", "Sample PowerPoint file\nThis is the 1st file\n\nNot much too it\n", + "MBD0000A3B3", "Sample PowerPoint file\nThis is the 2nd file\n\nNot much too it either\n" }; for (int i=0; i ppt = SlideShowFactory.create(npoifs.getRoot()); SlideShowExtractor extractor = new SlideShowExtractor<>(ppt)) { - assertEquals(expectText, extractor.getText()); + assertEquals(EXPECTED_PAGE1+EXPECTED_PAGE2, extractor.getText()); } } } @@ -457,4 +469,19 @@ public final class TestExtractor { private static int countMatches(final String base, final String find) { return base.split(find).length-1; } + + @Test + public void glyphCounting() throws IOException { + String[] expected = { + "Times New Roman", "\t\n ,-./01234679:ABDEFGILMNOPRSTVWabcdefghijklmnoprstuvwxyz\u00F3\u201C\u201D", + "Arial", " Lacdilnost" + }; + try (SlideShowExtractor ppt = openExtractor("45543.ppt")) { + for (int i=0; i { @Override public HSLFSlideShow createSlideShow() { return new HSLFSlideShow(); @@ -39,11 +39,7 @@ public class TestHSLFSlideShow extends BaseTestSlideShow { assertNotNull(createSlideShow()); } - public SlideShow reopen(SlideShow show) { - return reopen((HSLFSlideShow)show); - } - - public static HSLFSlideShow reopen(HSLFSlideShow show) { + public HSLFSlideShow reopen(SlideShow show) { try { BufAccessBAOS bos = new BufAccessBAOS(); show.write(bos); @@ -55,7 +51,7 @@ public class TestHSLFSlideShow extends BaseTestSlideShow { } private static class BufAccessBAOS extends ByteArrayOutputStream { - public byte[] getBuf() { + byte[] getBuf() { return buf; } } diff --git a/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java b/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java index c4daa50b3f..c12c96559f 100644 --- a/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java +++ b/src/testcases/org/apache/poi/sl/usermodel/BaseTestSlideShow.java @@ -17,6 +17,7 @@ package org.apache.poi.sl.usermodel; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -29,20 +30,24 @@ import java.io.InputStream; import java.util.List; import org.apache.poi.POIDataSamples; +import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.sl.usermodel.TabStop.TabStopType; import org.junit.Test; -public abstract class BaseTestSlideShow { +public abstract class BaseTestSlideShow< + S extends Shape, + P extends TextParagraph +> { protected static final POIDataSamples slTests = POIDataSamples.getSlideShowInstance(); - public abstract SlideShow createSlideShow(); + public abstract SlideShow createSlideShow(); - public abstract SlideShow reopen(SlideShow show); + public abstract SlideShow reopen(SlideShow show); @Test public void addPicture_File() throws IOException { - SlideShow show = createSlideShow(); + SlideShow show = createSlideShow(); File f = slTests.getFile("clock.jpg"); assertEquals(0, show.getPictureData().size()); @@ -55,26 +60,18 @@ public abstract class BaseTestSlideShow { @Test public void addPicture_Stream() throws IOException { - SlideShow show = createSlideShow(); - try { - InputStream stream = slTests.openResourceAsStream("clock.jpg"); - try { - assertEquals(0, show.getPictureData().size()); - PictureData picture = show.addPicture(stream, PictureType.JPEG); - assertEquals(1, show.getPictureData().size()); - assertSame(picture, show.getPictureData().get(0)); - - } finally { - stream.close(); - } - } finally { - show.close(); + try (SlideShow show = createSlideShow(); + InputStream stream = slTests.openResourceAsStream("clock.jpg")) { + assertEquals(0, show.getPictureData().size()); + PictureData picture = show.addPicture(stream, PictureType.JPEG); + assertEquals(1, show.getPictureData().size()); + assertSame(picture, show.getPictureData().get(0)); } } @Test public void addPicture_ByteArray() throws IOException { - SlideShow show = createSlideShow(); + SlideShow show = createSlideShow(); byte[] data = slTests.readFile("clock.jpg"); assertEquals(0, show.getPictureData().size()); @@ -87,7 +84,7 @@ public abstract class BaseTestSlideShow { @Test public void findPicture() throws IOException { - SlideShow show = createSlideShow(); + SlideShow show = createSlideShow(); byte[] data = slTests.readFile("clock.jpg"); assertNull(show.findPictureData(data)); @@ -101,11 +98,11 @@ public abstract class BaseTestSlideShow { @Test public void addTabStops() throws IOException { - try (final SlideShow show1 = createSlideShow()) { + try (final SlideShow show1 = createSlideShow()) { // first set the TabStops in the Master sheet - final MasterSheet master1 = show1.getSlideMasters().get(0); - final AutoShape master1_as = (AutoShape)master1.getPlaceholder(Placeholder.BODY); - final TextParagraph master1_tp = master1_as.getTextParagraphs().get(0); + final MasterSheet master1 = show1.getSlideMasters().get(0); + final AutoShape master1_as = (AutoShape)master1.getPlaceholder(Placeholder.BODY); + final P master1_tp = master1_as.getTextParagraphs().get(0); master1_tp.clearTabStops(); int i1 = 0; for (final TabStopType tst : TabStopType.values()) { @@ -114,11 +111,11 @@ public abstract class BaseTestSlideShow { } // then set it on a normal slide - final Slide slide1 = show1.createSlide(); - final AutoShape slide1_as = slide1.createAutoShape(); + final Slide slide1 = show1.createSlide(); + final AutoShape slide1_as = slide1.createAutoShape(); slide1_as.setText("abc"); slide1_as.setAnchor(new Rectangle2D.Double(100,100,100,100)); - final TextParagraph slide1_tp = slide1_as.getTextParagraphs().get(0); + final P slide1_tp = slide1_as.getTextParagraphs().get(0); slide1_tp.getTextRuns().get(0).setFontColor(new Color(0x563412)); slide1_tp.clearTabStops(); int i2 = 0; @@ -127,10 +124,10 @@ public abstract class BaseTestSlideShow { i2++; } - try (final SlideShow show2 = reopen(show1)) { - final MasterSheet master2 = show2.getSlideMasters().get(0); - final AutoShape master2_as = (AutoShape)master2.getPlaceholder(Placeholder.BODY); - final TextParagraph master2_tp = master2_as.getTextParagraphs().get(0); + try (final SlideShow show2 = reopen(show1)) { + final MasterSheet master2 = show2.getSlideMasters().get(0); + final AutoShape master2_as = (AutoShape)master2.getPlaceholder(Placeholder.BODY); + final P master2_tp = master2_as.getTextParagraphs().get(0); final List master2_tabStops = master2_tp.getTabStops(); assertNotNull(master2_tabStops); int i3 = 0; @@ -142,9 +139,10 @@ public abstract class BaseTestSlideShow { } - final Slide slide2 = show2.getSlides().get(0); - final AutoShape slide2_as = (AutoShape)slide2.getShapes().get(0); - final TextParagraph slide2_tp = slide2_as.getTextParagraphs().get(0); + final Slide slide2 = show2.getSlides().get(0); + @SuppressWarnings("unchecked") + final AutoShape slide2_as = (AutoShape)slide2.getShapes().get(0); + final P slide2_tp = slide2_as.getTextParagraphs().get(0); final List slide2_tabStops = slide2_tp.getTabStops(); assertNotNull(slide2_tabStops); int i4 = 0; @@ -162,18 +160,35 @@ public abstract class BaseTestSlideShow { public void shapeAndSlideName() throws IOException { final String file = "SampleShow.ppt"+(getClass().getSimpleName().contains("XML")?"x":""); try (final InputStream is = slTests.openResourceAsStream(file); - final SlideShow ppt = SlideShowFactory.create(is)) { - final List shapes1 = ppt.getSlides().get(0).getShapes(); + final SlideShow ppt = SlideShowFactory.create(is)) { + final List shapes1 = ppt.getSlides().get(0).getShapes(); assertEquals("The Title", shapes1.get(0).getShapeName()); assertEquals("Another Subtitle", shapes1.get(1).getShapeName()); - final List shapes2 = ppt.getSlides().get(1).getShapes(); + final List shapes2 = ppt.getSlides().get(1).getShapes(); assertEquals("Title 1", shapes2.get(0).getShapeName()); assertEquals("Content Placeholder 2", shapes2.get(1).getShapeName()); - for (final Slide slide : ppt.getSlides()) { + for (final Slide slide : ppt.getSlides()) { final String expected = slide.getSlideNumber()==1 ? "FirstSlide" : "Slide2"; assertEquals(expected, slide.getSlideName()); } } } + + @Test + public void addFont() throws IOException { + try (SlideShow ppt = createSlideShow()) { + ppt.createSlide(); + try (InputStream fontData = slTests.openResourceAsStream("font.fntdata")) { + ppt.addFont(fontData); + } + + try (SlideShow ppt2 = reopen(ppt)) { + List fonts = ppt2.getFonts(); + assertFalse(fonts.isEmpty()); + FontInfo fi = fonts.get(fonts.size()-1); + assertEquals("Harlow Solid Italic", fi.getTypeface()); + } + } + } } diff --git a/test-data/slideshow/font.fntdata b/test-data/slideshow/font.fntdata new file mode 100644 index 0000000000000000000000000000000000000000..b00e97ab4d50f7e71b2dd06f23bd7d786d56c287 GIT binary patch literal 17759 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 literal 0 HcmV?d00001 -- 2.39.5