From: Andreas Beeker Date: Fri, 28 Dec 2018 23:43:31 +0000 (+0000) Subject: #63028 - Provide font embedding for slideshows X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=033c0924151834140262cbf00d368b3958bdfbca;p=poi.git #63028 - Provide font embedding for slideshows git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1849898 13f79535-47bb-0310-9956-ffa450edef68 --- 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 0000000000..b00e97ab4d Binary files /dev/null and b/test-data/slideshow/font.fntdata differ