#60656 - EMF image support in slideshows

- extract option for embedded element in PPTX2PNG
- minor GenericRecordJsonWriter fixes
- fix EMF+ world transformations
- fix initialization emf pictures, which were partly unbounded -> excessive memory consumption
- change EMF+ brushes to continueable record

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1869272 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2019-11-01 17:18:13 +00:00
parent 3f0a01ae7c
commit f7c28ad08f
25 changed files with 908 additions and 536 deletions

View File

@ -1,46 +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.hslf.examples;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import javax.imageio.ImageIO;
import org.apache.poi.hslf.usermodel.HSLFSlide;
import org.apache.poi.hslf.usermodel.HSLFSlideShow;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.xslf.util.PPTX2PNG;
/**
* Demonstrates how you can use HSLF to convert each slide into a PNG image
*/
public final class PPT2PNG extends PPTX2PNG {
private static void usage(){
System.out.println("Usage: PPT2PNG [-scale <scale> -slide <num>] ppt");
}
}

View File

@ -94,12 +94,21 @@ public enum FileMagic {
PNG(new byte[]{ (byte)0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }),
/** TIFF Image */
TIFF("II*\u0000", "MM\u0000*" ),
/** WMF image with a placeable header */
WMF(new byte[]{ (byte)0xD7, (byte)0xCD, (byte)0xC6, (byte)0x9A }),
/** EMF image */
EMF(new byte[]{
1, 0, 0, 0,
'?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?',
'?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?',
' ', 'E', 'M', 'F'
}),
// keep UNKNOWN always as last enum!
/** UNKNOWN magic */
UNKNOWN(new byte[0]);
// update this if a longer pattern is added
final static int MAX_PATTERN_LENGTH = 12;
final static int MAX_PATTERN_LENGTH = 44;
final byte[][] magic;

View File

@ -0,0 +1,54 @@
/*
* ====================================================================
* 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.draw;
import java.util.Collections;
import java.util.function.Supplier;
import org.apache.poi.util.Beta;
@Beta
public interface EmbeddedExtractor {
class EmbeddedPart {
private String name;
private Supplier<byte[]> data;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Supplier<byte[]> getData() {
return data;
}
public void setData(Supplier<byte[]> data) {
this.data = data;
}
}
default Iterable<EmbeddedPart> getEmbeddings() {
return Collections.emptyList();
}
}

View File

@ -368,19 +368,19 @@ public class GenericRecordJsonWriter implements Closeable {
fw.append("{ \"type\": ");
switch (segType) {
case PathIterator.SEG_MOVETO:
fw.write("'move', \"x\": "+pnts[0]+", \"y\": "+pnts[1]);
fw.write("\"move\", \"x\": "+pnts[0]+", \"y\": "+pnts[1]);
break;
case PathIterator.SEG_LINETO:
fw.write("'lineto', \"x\": "+pnts[0]+", \"y\": "+pnts[1]);
fw.write("\"lineto\", \"x\": "+pnts[0]+", \"y\": "+pnts[1]);
break;
case PathIterator.SEG_QUADTO:
fw.write("'quad', \"x1\": "+pnts[0]+", \"y1\": "+pnts[1]+", \"x2\": "+pnts[2]+", \"y2\": "+pnts[3]);
fw.write("\"quad\", \"x1\": "+pnts[0]+", \"y1\": "+pnts[1]+", \"x2\": "+pnts[2]+", \"y2\": "+pnts[3]);
break;
case PathIterator.SEG_CUBICTO:
fw.write("'cubic', \"x1\": "+pnts[0]+", \"y1\": "+pnts[1]+", \"x2\": "+pnts[2]+", \"y2\": "+pnts[3]+", \"x3\": "+pnts[4]+", \"y3\": "+pnts[5]);
fw.write("\"cubic\", \"x1\": "+pnts[0]+", \"y1\": "+pnts[1]+", \"x2\": "+pnts[2]+", \"y2\": "+pnts[3]+", \"x3\": "+pnts[4]+", \"y3\": "+pnts[5]);
break;
case PathIterator.SEG_CLOSE:
fw.write("'close'");
fw.write("\"close\"");
break;
}
fw.append(" }");

View File

@ -107,17 +107,23 @@ public final class GenericRecordUtil {
}
public static Supplier<AnnotatedFlag> getBitsAsString(Supplier<Number> flags, final int[] masks, final String[] names) {
return () -> new AnnotatedFlag(flags, masks, names);
return () -> new AnnotatedFlag(flags, masks, names, false);
}
public static Supplier<AnnotatedFlag> getEnumBitsAsString(Supplier<Number> flags, final int[] masks, final String[] names) {
return () -> new AnnotatedFlag(flags, masks, names, true);
}
public static class AnnotatedFlag {
private final Supplier<Number> value;
private final Map<Integer,String> masks = new LinkedHashMap<>();
private final boolean exactMatch;
AnnotatedFlag(Supplier<Number> value, int[] masks, String[] names) {
AnnotatedFlag(Supplier<Number> value, int[] masks, String[] names, boolean exactMatch) {
assert(masks.length == names.length);
this.value = value;
this.exactMatch = exactMatch;
for (int i=0; i<masks.length; i++) {
this.masks.put(masks[i], names[i]);
}
@ -135,8 +141,8 @@ public final class GenericRecordUtil {
collect(Collectors.joining(" | "));
}
private static boolean match(final int val, int mask) {
return (val & mask) == mask;
private boolean match(final int val, int mask) {
return exactMatch ? (val == mask) : ((val & mask) == mask);
}
}
}

View File

@ -0,0 +1,114 @@
/*
* ====================================================================
* 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.util;
import java.awt.Graphics2D;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.DrawPictureShape;
import org.apache.poi.sl.draw.EmbeddedExtractor;
import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.util.Internal;
@Internal
class EMFHandler extends MFProxy {
private ImageRenderer imgr = null;
private InputStream is;
@Override
public void parse(File file) throws IOException {
// stream needs to be kept open
parse(file.toURI().toURL().openStream());
}
@Override
public void parse(InputStream is) throws IOException {
imgr = DrawPictureShape.getImageRenderer(null, getContentType());
if (imgr instanceof BitmapImageRenderer) {
throw new PPTX2PNG.NoScratchpadException();
}
// stream needs to be kept open
imgr.loadImage(is, getContentType());
if (ignoreParse) {
try {
imgr.getDimension();
} catch (Exception e) {
// if (!quite) {
// e.printStackTrace(System.err);
// }
}
}
}
protected String getContentType() {
return PictureData.PictureType.EMF.contentType;
}
@Override
public Dimension2D getSize() {
return imgr.getDimension();
}
@Override
public String getTitle() {
return "";
}
@Override
public void draw(Graphics2D ctx) {
Dimension2D dim = getSize();
imgr.drawImage(ctx, new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight()));
}
@Override
public void close() throws IOException {
if (is != null) {
try {
is.close();
} finally {
is = null;
}
}
}
@Override
public GenericRecord getRoot() {
return imgr.getGenericRecord();
}
@Override
public Iterable<EmbeddedPart> getEmbeddings(int slideNo) {
return (imgr instanceof EmbeddedExtractor)
? ((EmbeddedExtractor) imgr).getEmbeddings()
: Collections.emptyList();
}
}

View File

@ -0,0 +1,67 @@
/*
* ====================================================================
* 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.util;
import java.awt.Graphics2D;
import java.awt.geom.Dimension2D;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Set;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
import org.apache.poi.util.Internal;
@Internal
abstract class MFProxy implements Closeable {
boolean ignoreParse;
boolean quite;
void setIgnoreParse(boolean ignoreParse) {
this.ignoreParse = ignoreParse;
}
void setQuite(boolean quite) {
this.quite = quite;
}
abstract void parse(File file) throws IOException;
abstract void parse(InputStream is) throws IOException;
abstract Dimension2D getSize();
void setSlideNo(int slideNo) {}
abstract String getTitle();
abstract void draw(Graphics2D ctx);
int getSlideCount() { return 1; }
Set<Integer> slideIndexes(String range) {
return Collections.singleton(1);
}
abstract GenericRecord getRoot();
abstract Iterable<EmbeddedPart> getEmbeddings(int slideNo);
}

View File

@ -0,0 +1,180 @@
/*
* ====================================================================
* 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.util;
import static java.util.Spliterator.NONNULL;
import static java.util.Spliterator.ORDERED;
import java.awt.Graphics2D;
import java.awt.geom.Dimension2D;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
import org.apache.poi.sl.usermodel.ObjectData;
import org.apache.poi.sl.usermodel.ObjectShape;
import org.apache.poi.sl.usermodel.Shape;
import org.apache.poi.sl.usermodel.Slide;
import org.apache.poi.sl.usermodel.SlideShow;
import org.apache.poi.sl.usermodel.SlideShowFactory;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
/** Handler for ppt and pptx files */
@Internal
class PPTHandler extends MFProxy {
private SlideShow<?,?> ppt;
private Slide<?,?> slide;
@Override
public void parse(File file) throws IOException {
try {
ppt = SlideShowFactory.create(file, null, true);
} catch (IOException e) {
if (e.getMessage().contains("scratchpad")) {
throw new PPTX2PNG.NoScratchpadException(e);
} else {
throw e;
}
}
slide = ppt.getSlides().get(0);
}
@Override
public void parse(InputStream is) throws IOException {
try {
ppt = SlideShowFactory.create(is, null);
} catch (IOException e) {
if (e.getMessage().contains("scratchpad")) {
throw new PPTX2PNG.NoScratchpadException(e);
} else {
throw e;
}
}
slide = ppt.getSlides().get(0);
}
@Override
public Dimension2D getSize() {
return ppt.getPageSize();
}
@Override
public int getSlideCount() {
return ppt.getSlides().size();
}
@Override
public void setSlideNo(int slideNo) {
slide = ppt.getSlides().get(slideNo-1);
}
@Override
public String getTitle() {
return slide.getTitle();
}
private static final String RANGE_PATTERN = "(^|,)(?<from>\\d+)?(-(?<to>\\d+))?";
@Override
public Set<Integer> slideIndexes(String range) {
final Matcher matcher = Pattern.compile(RANGE_PATTERN).matcher(range);
Spliterator<Matcher> sp = new Spliterators.AbstractSpliterator<Matcher>(range.length(), ORDERED|NONNULL){
@Override
public boolean tryAdvance(Consumer<? super Matcher> action) {
boolean b = matcher.find();
if (b) {
action.accept(matcher);
}
return b;
}
};
return StreamSupport.stream(sp, false).
flatMap(this::range).
collect(Collectors.toCollection(TreeSet::new));
}
@Override
public void draw(Graphics2D ctx) {
slide.draw(ctx);
}
@Override
public void close() throws IOException {
if (ppt != null) {
ppt.close();
}
}
@Override
public GenericRecord getRoot() {
return (ppt instanceof GenericRecord) ? (GenericRecord)ppt : null;
}
private Stream<Integer> range(Matcher m) {
final int slideCount = ppt.getSlides().size();
String fromStr = m.group("from");
String toStr = m.group("to");
int from = (fromStr == null || fromStr.isEmpty() ? 1 : Integer.parseInt(fromStr));
int to = (toStr == null) ? from
: (toStr.isEmpty() || ((fromStr == null || fromStr.isEmpty()) && "1".equals(toStr))) ? slideCount
: Integer.parseInt(toStr);
return IntStream.rangeClosed(from, to).filter(i -> i <= slideCount).boxed();
}
@Override
public Iterable<EmbeddedPart> getEmbeddings(int slideNo) {
return () -> ppt.getSlides().get(slideNo).getShapes().stream().
filter(s -> s instanceof ObjectShape).
map(PPTHandler::fromObjectShape).
iterator()
;
}
private static EmbeddedPart fromObjectShape(Shape s) {
final ObjectShape os = (ObjectShape)s;
final ObjectData od = os.getObjectData();
EmbeddedPart embed = new EmbeddedPart();
embed.setName(od.getFileName());
embed.setData(() -> {
try (InputStream is = od.getInputStream()) {
return IOUtils.toByteArray(is);
} catch (IOException e) {
// TODO: change to custom runtime exception
throw new RuntimeException(e);
}
});
return embed;
}
}

View File

@ -19,51 +19,33 @@
package org.apache.poi.xslf.util;
import static java.util.Spliterator.NONNULL;
import static java.util.Spliterator.ORDERED;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators.AbstractSpliterator;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.imageio.ImageIO;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.DrawPictureShape;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.sl.usermodel.Slide;
import org.apache.poi.sl.usermodel.SlideShow;
import org.apache.poi.sl.usermodel.SlideShowFactory;
import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.GenericRecordJsonWriter;
/**
* An utility to convert slides of a .pptx slide show to a PNG image
*/
public class PPTX2PNG {
public final class PPTX2PNG {
private static final String INPUT_PAT_REGEX =
"(?<slideno>[^|]+)\\|(?<format>[^|]+)\\|(?<basename>.+)\\.(?<ext>[^.]++)";
@ -72,10 +54,9 @@ public class PPTX2PNG {
private static final String OUTPUT_PAT_REGEX = "${basename}-${slideno}.${format}";
private static void usage(String error){
String msg =
"Usage: PPTX2PNG [options] <ppt or pptx file>\n" +
"Usage: PPTX2PNG [options] <ppt or pptx file or 'stdin'>\n" +
(error == null ? "" : ("Error: "+error+"\n")) +
"Options:\n" +
" -scale <float> scale factor\n" +
@ -87,36 +68,52 @@ public class PPTX2PNG {
" -outpat <pattern> output filename pattern, defaults to '"+OUTPUT_PAT_REGEX+"'\n" +
" patterns: basename, slideno, format, ext\n" +
" -dump <file> dump the annotated records to a file\n" +
" -quiet do not write to console (for normal processing)";
" -quiet do not write to console (for normal processing)\n" +
" -ignoreParse ignore parsing error and continue with the records read until the error\n" +
" -extractEmbedded extract embedded parts";
System.out.println(msg);
// no System.exit here, as we also run in junit tests!
}
public static void main(String[] args) throws Exception {
PPTX2PNG p2p = new PPTX2PNG();
if (p2p.parseCommandLine(args)) {
p2p.processFile();
}
}
private String slidenumStr = "-1";
private float scale = 1;
private File file = null;
private String format = "png";
private File outdir = null;
private String outfile = null;
private boolean quiet = false;
private String outPattern = OUTPUT_PAT_REGEX;
private File dumpfile = null;
private String fixSide = "scale";
private boolean ignoreParse = false;
private boolean extractEmbedded = false;
private PPTX2PNG() {
}
private boolean parseCommandLine(String[] args) {
if (args.length == 0) {
usage(null);
return;
return false;
}
String slidenumStr = "-1";
float scale = 1;
File file = null;
String format = "png";
File outdir = null;
String outfile = null;
boolean quiet = false;
String outPattern = OUTPUT_PAT_REGEX;
File dumpfile = null;
String fixSide = "scale";
for (int i = 0; i < args.length; i++) {
String opt = (i+1 < args.length) ? args[i+1] : null;
switch (args[i]) {
case "-scale":
scale = Float.parseFloat(opt);
i++;
if (opt != null) {
scale = Float.parseFloat(opt);
i++;
}
break;
case "-slide":
slidenumStr = opt;
@ -127,8 +124,10 @@ public class PPTX2PNG {
i++;
break;
case "-outdir":
outdir = new File(opt);
i++;
if (opt != null) {
outdir = new File(opt);
i++;
}
break;
case "-outfile":
outfile = opt;
@ -142,12 +141,26 @@ public class PPTX2PNG {
quiet = true;
break;
case "-dump":
dumpfile = new File(opt);
i++;
if (opt != null) {
dumpfile = new File(opt);
i++;
} else {
dumpfile = new File("pptx2png.dump");
}
break;
case "-fixside":
fixSide = opt.toLowerCase(Locale.ROOT);
i++;
if (opt != null) {
fixSide = opt.toLowerCase(Locale.ROOT);
i++;
} else {
fixSide = "long";
}
break;
case "-ignoreParse":
ignoreParse = true;
break;
case "-extractEmbedded":
extractEmbedded = true;
break;
default:
file = new File(args[i]);
@ -155,40 +168,54 @@ public class PPTX2PNG {
}
}
if (file == null || !file.exists()) {
final boolean isStdin = file != null && "stdin".equalsIgnoreCase(file.getName());
if (!isStdin && (file == null || !file.exists())) {
usage("File not specified or it doesn't exist");
return;
return false;
}
if (format == null || !format.matches("^(png|gif|jpg|null)$")) {
usage("Invalid format given");
return;
return false;
}
if (outdir == null) {
outdir = file.getParentFile();
if (isStdin) {
usage("When reading from STDIN, you need to specify an outdir.");
return false;
} else {
outdir = file.getParentFile();
}
}
if (!outdir.exists()) {
usage("Outdir doesn't exist");
return false;
}
if (!"null".equals(format) && (outdir == null || !outdir.exists() || !outdir.isDirectory())) {
usage("Output directory doesn't exist");
return;
return false;
}
if (scale < 0) {
usage("Invalid scale given");
return;
return false;
}
if (!"long,short,width,height,scale".contains(fixSide)) {
usage("<fixside> must be one of long / short / width / height");
return;
return false;
}
return true;
}
private void processFile() throws IOException {
if (!quiet) {
System.out.println("Processing " + file);
}
try (MFProxy proxy = initProxy(file)) {
final Set<Integer> slidenum = proxy.slideIndexes(slidenumStr);
if (slidenum.isEmpty()) {
@ -196,30 +223,10 @@ public class PPTX2PNG {
return;
}
final Dimension2D pgsize = proxy.getSize();
final double lenSide;
switch (fixSide) {
default:
case "scale":
lenSide = 1;
break;
case "long":
lenSide = Math.max(pgsize.getWidth(), pgsize.getHeight());
break;
case "short":
lenSide = Math.min(pgsize.getWidth(), pgsize.getHeight());
break;
case "width":
lenSide = pgsize.getWidth();
break;
case "height":
lenSide = pgsize.getHeight();
break;
}
final int width = (int) Math.rint(pgsize.getWidth() * scale / lenSide);
final int height = (int) Math.rint(pgsize.getHeight() * scale / lenSide);
final Dimension2D dim = new Dimension2DDouble();
final double lenSide = getDimensions(proxy, dim);
final int width = (int)Math.rint(dim.getWidth());
final int height = (int)Math.rint(dim.getHeight());
for (int slideNo : slidenum) {
proxy.setSlideNo(slideNo);
@ -228,16 +235,9 @@ public class PPTX2PNG {
System.out.println("Rendering slide " + slideNo + (title == null ? "" : ": " + title.trim()));
}
GenericRecord gr = proxy.getRoot();
if (dumpfile != null) {
try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile)) {
if (gr == null) {
fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
} else {
fw.write(gr);
}
}
}
dumpRecords(proxy);
extractEmbedded(proxy, slideNo);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
@ -253,7 +253,7 @@ public class PPTX2PNG {
graphics.scale(scale / lenSide, scale / lenSide);
graphics.setComposite(AlphaComposite.Clear);
graphics.fillRect(0, 0, (int)width, (int)height);
graphics.fillRect(0, 0, width, height);
graphics.setComposite(AlphaComposite.SrcOver);
// draw stuff
@ -261,10 +261,7 @@ public class PPTX2PNG {
// save the result
if (!"null".equals(format)) {
String inname = String.format(Locale.ROOT, "%04d|%s|%s", slideNo, format, file.getName());
String outpat = (proxy.getSlideCount() > 1 ? outPattern : outPattern.replaceAll("-?\\$\\{slideno\\}", ""));
String outname = (outfile != null) ? outfile : INPUT_PATTERN.matcher(inname).replaceAll(outpat);
ImageIO.write(img, format, new File(outdir, outname));
ImageIO.write(img, format, new File(outdir, calcOutFile(proxy, slideNo)));
}
graphics.dispose();
@ -280,198 +277,114 @@ public class PPTX2PNG {
}
}
private static MFProxy initProxy(File file) throws IOException {
MFProxy proxy;
final String fileName = file.getName().toLowerCase(Locale.ROOT);
switch (fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.')) : "") {
case ".emf":
proxy = new EMFHandler();
break;
case ".wmf":
proxy = new WMFHandler();
break;
private double getDimensions(MFProxy proxy, Dimension2D dim) {
final Dimension2D pgsize = proxy.getSize();
final double lenSide;
switch (fixSide) {
default:
proxy = new PPTHandler();
case "scale":
lenSide = 1;
break;
case "long":
lenSide = Math.max(pgsize.getWidth(), pgsize.getHeight());
break;
case "short":
lenSide = Math.min(pgsize.getWidth(), pgsize.getHeight());
break;
case "width":
lenSide = pgsize.getWidth();
break;
case "height":
lenSide = pgsize.getHeight();
break;
}
proxy.parse(file);
dim.setSize(pgsize.getWidth() * scale / lenSide, pgsize.getHeight() * scale / lenSide);
return lenSide;
}
private void dumpRecords(MFProxy proxy) throws IOException {
if (dumpfile == null) {
return;
}
GenericRecord gr = proxy.getRoot();
try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile)) {
if (gr == null) {
fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
} else {
fw.write(gr);
}
}
}
private void extractEmbedded(MFProxy proxy, int slideNo) throws IOException {
if (!extractEmbedded) {
return;
}
for (EmbeddedPart ep : proxy.getEmbeddings(slideNo)) {
String filename = ep.getName();
// do some sanitizing for creative filenames ...
filename = new File(filename == null ? "dummy.dat" : filename).getName();
filename = calcOutFile(proxy, slideNo).replaceFirst("\\.\\w+$", "")+"_"+filename;
try (FileOutputStream fos = new FileOutputStream(new File(outdir, filename))) {
fos.write(ep.getData().get());
}
}
}
private MFProxy initProxy(File file) throws IOException {
MFProxy proxy;
final String fileName = file.getName().toLowerCase(Locale.ROOT);
if ("stdin".equals(fileName)) {
InputStream bis = FileMagic.prepareToCheckMagic(System.in);
FileMagic fm = FileMagic.valueOf(bis);
switch (fm) {
case EMF:
proxy = new EMFHandler();
break;
case WMF:
proxy = new WMFHandler();
break;
default:
proxy = new PPTHandler();
break;
}
proxy.setIgnoreParse(ignoreParse);
proxy.setQuite(quiet);
proxy.parse(bis);
} else {
switch (fileName.contains(".") ? fileName.substring(fileName.lastIndexOf('.')) : "") {
case ".emf":
proxy = new EMFHandler();
break;
case ".wmf":
proxy = new WMFHandler();
break;
default:
proxy = new PPTHandler();
break;
}
proxy.parse(file);
}
return proxy;
}
private interface MFProxy extends Closeable {
void parse(File file) throws IOException;
// Iterable<HwmfEmbedded> getEmbeddings();
Dimension2D getSize();
default void setSlideNo(int slideNo) {}
String getTitle();
void draw(Graphics2D ctx);
default int getSlideCount() { return 1; }
default Set<Integer> slideIndexes(String range) {
return Collections.singleton(1);
private String calcOutFile(MFProxy proxy, int slideNo) {
if (outfile != null) {
return outfile;
}
GenericRecord getRoot();
String inname = String.format(Locale.ROOT, "%04d|%s|%s", slideNo, format, file.getName());
String outpat = (proxy.getSlideCount() > 1 ? outPattern : outPattern.replaceAll("-?\\$\\{slideno}", ""));
return INPUT_PATTERN.matcher(inname).replaceAll(outpat);
}
/** Handler for ppt and pptx files */
private static class PPTHandler implements MFProxy {
SlideShow<?,?> ppt;
Slide<?,?> slide;
@Override
public void parse(File file) throws IOException {
try {
ppt = SlideShowFactory.create(file, null, true);
} catch (IOException e) {
if (e.getMessage().contains("scratchpad")) {
throw new NoScratchpadException(e);
} else {
throw e;
}
}
slide = ppt.getSlides().get(0);
static class NoScratchpadException extends IOException {
NoScratchpadException() {
}
@Override
public Dimension2D getSize() {
return ppt.getPageSize();
}
@Override
public int getSlideCount() {
return ppt.getSlides().size();
}
@Override
public void setSlideNo(int slideNo) {
slide = ppt.getSlides().get(slideNo-1);
}
@Override
public String getTitle() {
return slide.getTitle();
}
private static final String RANGE_PATTERN = "(^|,)(?<from>\\d+)?(-(?<to>\\d+))?";
@Override
public Set<Integer> slideIndexes(String range) {
final Matcher matcher = Pattern.compile(RANGE_PATTERN).matcher(range);
Spliterator<Matcher> sp = new AbstractSpliterator<Matcher>(range.length(), ORDERED|NONNULL){
@Override
public boolean tryAdvance(Consumer<? super Matcher> action) {
boolean b = matcher.find();
if (b) {
action.accept(matcher);
}
return b;
}
};
return StreamSupport.stream(sp, false).
flatMap(this::range).
collect(Collectors.toCollection(TreeSet::new));
}
@Override
public void draw(Graphics2D ctx) {
slide.draw(ctx);
}
@Override
public void close() throws IOException {
if (ppt != null) {
ppt.close();
}
}
@Override
public GenericRecord getRoot() {
return (ppt instanceof GenericRecord) ? (GenericRecord)ppt : null;
}
private Stream<Integer> range(Matcher m) {
final int slideCount = ppt.getSlides().size();
String fromStr = m.group("from");
String toStr = m.group("to");
int from = (fromStr == null || fromStr.isEmpty() ? 1 : Integer.parseInt(fromStr));
int to = (toStr == null) ? from
: (toStr.isEmpty() || ((fromStr == null || fromStr.isEmpty()) && "1".equals(toStr))) ? slideCount
: Integer.parseInt(toStr);
return IntStream.rangeClosed(from, to).filter(i -> i <= slideCount).boxed();
}
}
private static class EMFHandler implements MFProxy {
private ImageRenderer imgr = null;
private InputStream is;
@Override
public void parse(File file) throws IOException {
imgr = DrawPictureShape.getImageRenderer(null, getContentType());
if (imgr instanceof BitmapImageRenderer) {
throw new NoScratchpadException();
}
// stream needs to be kept open
is = file.toURI().toURL().openStream();
imgr.loadImage(is, getContentType());
}
protected String getContentType() {
return PictureData.PictureType.EMF.contentType;
}
@Override
public Dimension2D getSize() {
return imgr.getDimension();
}
@Override
public String getTitle() {
return "";
}
@Override
public void draw(Graphics2D ctx) {
Dimension2D dim = getSize();
imgr.drawImage(ctx, new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight()));
}
@Override
public void close() throws IOException {
if (is != null) {
try {
is.close();
} finally {
is = null;
}
}
}
@Override
public GenericRecord getRoot() {
return imgr.getGenericRecord();
}
}
private static class WMFHandler extends EMFHandler {
@Override
protected String getContentType() {
return PictureData.PictureType.WMF.contentType;
}
}
private static class NoScratchpadException extends IOException {
public NoScratchpadException() {
}
public NoScratchpadException(Throwable cause) {
NoScratchpadException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,31 @@
/*
* ====================================================================
* 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.util;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.util.Internal;
@Internal
class WMFHandler extends EMFHandler {
@Override
protected String getContentType() {
return PictureData.PictureType.WMF.contentType;
}
}

View File

@ -17,13 +17,17 @@
package org.apache.poi.hemf.draw;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle;
import org.apache.poi.hwmf.draw.HwmfDrawProperties;
public class HemfDrawProperties extends HwmfDrawProperties {
enum TransOperand { left, right }
/** Path for path bracket operations */
protected Path2D path = null;
@ -31,6 +35,8 @@ public class HemfDrawProperties extends HwmfDrawProperties {
private EmfPlusHatchStyle emfPlusBrushHatch;
private BufferedImage emfPlusImage;
private final List<AffineTransform> transXForm = new ArrayList<>();
private final List<TransOperand> transOper = new ArrayList<>();
public HemfDrawProperties() {
}
@ -43,6 +49,8 @@ public class HemfDrawProperties extends HwmfDrawProperties {
// TODO: check how to clone
clip = other.clip;
emfPlusImage = other.emfPlusImage;
transXForm.addAll(other.transXForm);
transOper.addAll(other.transOper);
}
/**
@ -88,4 +96,27 @@ public class HemfDrawProperties extends HwmfDrawProperties {
public void setEmfPlusImage(BufferedImage emfPlusImage) {
this.emfPlusImage = emfPlusImage;
}
public void addLeftTransform(AffineTransform transform) {
transXForm.add(transform);
transOper.add(TransOperand.left);
}
public void addRightTransform(AffineTransform transform) {
transXForm.add(transform);
transOper.add(TransOperand.right);
}
public void clearTransform() {
transXForm.clear();
transOper.clear();
}
List<AffineTransform> getTransXForm() {
return transXForm;
}
List<TransOperand> getTransOper() {
return transOper;
}
}

View File

@ -23,11 +23,14 @@ import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.function.Consumer;
import org.apache.poi.hemf.draw.HemfDrawProperties.TransOperand;
import org.apache.poi.hemf.record.emf.HemfComment.EmfComment;
import org.apache.poi.hemf.record.emf.HemfRecord;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
@ -336,4 +339,27 @@ public class HemfGraphics extends HwmfGraphics {
// TODO: use EmfPlusHatchBrushData
return super.getHatchedFill();
}
@Override
public void updateWindowMapMode() {
super.updateWindowMapMode();
HemfDrawProperties prop = getProperties();
List<AffineTransform> transXform = prop.getTransXForm();
List<TransOperand> transOper = prop.getTransOper();
assert(transXform.size() == transOper.size());
AffineTransform tx = graphicsCtx.getTransform();
for (int i=0; i<transXform.size(); i++) {
AffineTransform tx2 = transXform.get(i);
if (transOper.get(i) == TransOperand.left) {
tx.concatenate(tx2);
} else {
tx.preConcatenate(tx2);
}
}
graphicsCtx.setTransform(tx);
}
}

View File

@ -30,13 +30,15 @@ import java.io.InputStream;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hemf.usermodel.HemfPicture;
import org.apache.poi.hwmf.draw.HwmfImageRenderer;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.EmbeddedExtractor;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.util.Units;
@SuppressWarnings("unused")
public class HemfImageRenderer implements ImageRenderer {
public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
HemfPicture image;
double alpha;
@ -113,4 +115,9 @@ public class HemfImageRenderer implements ImageRenderer {
public GenericRecord getGenericRecord() {
return image;
}
@Override
public Iterable<EmbeddedPart> getEmbeddings() {
return HwmfImageRenderer.getEmbeddings(image.getEmbeddings());
}
}

View File

@ -90,6 +90,13 @@ public class HemfComment {
long init(LittleEndianInputStream leis, long dataSize) throws IOException;
/**
* Apply the record settings to the graphics context
*
* @param ctx the graphics context to modify
*/
default void draw(HemfGraphics ctx) {};
@Override
default Enum getGenericRecordType() {
return getCommentRecordType();
@ -117,13 +124,7 @@ public class HemfComment {
@Override
public void draw(HemfGraphics ctx) {
if (data instanceof EmfCommentDataPlus) {
if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) {
ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY);
}
((EmfCommentDataPlus)data).draw(ctx);
}
data.draw(ctx);
}
@Override
@ -297,7 +298,12 @@ public class HemfComment {
return Collections.unmodifiableList(records);
}
@Override
public void draw(HemfGraphics ctx) {
if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) {
ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY);
}
records.forEach(ctx::draw);
}

View File

@ -18,7 +18,7 @@
package org.apache.poi.hemf.record.emf;
import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds;
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString;
import java.awt.Shape;
import java.awt.geom.Arc2D;
@ -97,7 +97,7 @@ public class HemfDraw {
@Override
public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties(
"objectIndex", getBitsAsString(this::getObjectIndex, IDX_MASKS, IDX_NAMES)
"objectIndex", getEnumBitsAsString(this::getObjectIndex, IDX_MASKS, IDX_NAMES)
);
}

View File

@ -29,7 +29,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.poi.hemf.draw.HemfDrawProperties;
@ -754,10 +753,10 @@ public class HemfMisc {
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
prop.clearTransform();
prop.addLeftTransform(xForm);
ctx.updateWindowMapMode();
AffineTransform tx = ctx.getTransform();
tx.concatenate(xForm);
ctx.setTransform(tx);
}
@Override
@ -812,28 +811,23 @@ public class HemfMisc {
final HemfDrawProperties prop = ctx.getProperties();
final AffineTransform tx;
switch (modifyWorldTransformMode) {
case MWT_LEFTMULTIPLY:
tx = ctx.getTransform();
tx.concatenate(adaptXForm(xForm, ctx.getTransform()));
prop.addLeftTransform(xForm);
break;
case MWT_RIGHTMULTIPLY:
tx = ctx.getTransform();
tx.preConcatenate(adaptXForm(xForm, tx));
prop.addRightTransform(xForm);
break;
case MWT_IDENTITY:
ctx.updateWindowMapMode();
tx = ctx.getTransform();
prop.clearTransform();
break;
default:
case MWT_SET:
ctx.updateWindowMapMode();
tx = ctx.getTransform();
tx.concatenate(adaptXForm(xForm, tx));
prop.clearTransform();
prop.addLeftTransform(xForm);
break;
}
ctx.setTransform(tx);
ctx.updateWindowMapMode();
}
@Override
@ -950,22 +944,4 @@ public class HemfMisc {
}
}
/**
* adapt xform depending on the base transformation (... experimental ...)
*/
public static AffineTransform adaptXForm(AffineTransform xForm, AffineTransform other) {
// normalize signed zero
Function<Double,Double> nn = (d) -> (d == 0. ? 0. : d);
double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.;
double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.;
return new AffineTransform(
xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(),
yDiff * xForm.getShearY(),
xDiff * xForm.getShearX(),
xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(),
xForm.getTranslateX(),
xForm.getTranslateY()
);
}
}

View File

@ -219,7 +219,8 @@ public class HemfText {
return GenericRecordUtil.getGenericProperties(
"base", super::getGenericProperties,
"boundsIgnored", () -> boundsIgnored,
"graphicsMode", this::getGraphicsMode
"graphicsMode", this::getGraphicsMode,
"scale", this::getScale
);
}

View File

@ -26,6 +26,8 @@ import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
@ -49,6 +51,7 @@ import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
@ -312,25 +315,35 @@ public class HemfPlusBrush {
/** The EmfPlusBrush object specifies a graphics brush for filling regions. */
public static class EmfPlusBrush implements EmfPlusObjectData {
private static final int MAX_OBJECT_SIZE = 1_000_000;
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusBrushType brushType;
private EmfPlusBrushData brushData;
private byte[] brushBytes;
@Override
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException {
leis.mark(LittleEndianConsts.INT_SIZE);
long size = graphicsVersion.init(leis);
brushType = EmfPlusBrushType.valueOf(leis.readInt());
size += LittleEndianConsts.INT_SIZE;
assert(brushType != null);
if (isContinuedRecord()) {
leis.reset();
size = 0;
} else {
int brushInt = leis.readInt();
brushType = EmfPlusBrushType.valueOf(brushInt);
assert(brushType != null);
size += LittleEndianConsts.INT_SIZE;
}
size += (brushData = brushType.constructor.get()).init(leis, dataSize-size);
brushBytes = IOUtils.toByteArray(leis, dataSize-size, MAX_OBJECT_SIZE);
return size;
return dataSize;
}
@Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
EmfPlusBrushData brushData = getBrushData(continuedObjectData);
brushData.applyObject(ctx, continuedObjectData);
}
@ -344,10 +357,38 @@ public class HemfPlusBrush {
return GenericRecordJsonWriter.marshal(this);
}
public EmfPlusBrushData getBrushData() {
public byte[] getBrushBytes() {
return brushBytes;
}
public EmfPlusBrushData getBrushData(List<? extends EmfPlusObjectData> continuedObjectData) {
EmfPlusBrushData brushData = brushType.constructor.get();
byte[] buf = getRawData(continuedObjectData);
try {
brushData.init(new LittleEndianInputStream(new ByteArrayInputStream(buf)), buf.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
return brushData;
}
public byte[] getRawData(List<? extends EmfPlusObjectData> continuedObjectData) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
bos.write(getBrushBytes());
if (continuedObjectData != null) {
for (EmfPlusObjectData od : continuedObjectData) {
bos.write(((EmfPlusBrush)od).getBrushBytes());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return bos.toByteArray();
}
@Override
public EmfPlusBrushType getGenericRecordType() {
return brushType;
@ -357,7 +398,8 @@ public class HemfPlusBrush {
public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties(
"graphicsVersion", this::getGraphicsVersion,
"brushData", this::getBrushData
/* only return the first object data ... enough for now */
"brushData", () -> getBrushData(null)
);
}
}
@ -375,6 +417,8 @@ public class HemfPlusBrush {
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties();
prop.setBackgroundColor(new HwmfColorRef(solidColor));
prop.setBrushTransform(null);
prop.setBrushStyle(HwmfBrushStyle.BS_SOLID);
}
@Override
@ -696,10 +740,11 @@ public class HemfPlusBrush {
@Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
image.applyObject(ctx, continuedObjectData);
HemfDrawProperties prop = ctx.getProperties();
image.applyObject(ctx, null);
prop.setBrushBitmap(prop.getEmfPlusImage());
prop.setBrushStyle(HwmfBrushStyle.BS_PATTERN);
prop.setBrushTransform(transform);
}
@Override

View File

@ -17,7 +17,6 @@
package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.hemf.record.emf.HemfMisc.adaptXForm;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
@ -28,6 +27,7 @@ import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
import org.apache.poi.hemf.draw.HemfDrawProperties;
import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emf.HemfFill;
import org.apache.poi.util.BitField;
@ -174,6 +174,12 @@ public class HemfPlusMisc {
* The EmfPlusResetWorldTransform record resets the current world space transform to the identify matrix.
*/
public static class EmfPlusResetWorldTransform extends EmfPlusFlagOnly {
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
prop.clearTransform();
ctx.updateWindowMapMode();
}
}
@ -208,10 +214,10 @@ public class HemfPlusMisc {
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
prop.clearTransform();
prop.addLeftTransform(getMatrixData());
ctx.updateWindowMapMode();
AffineTransform tx = ctx.getTransform();
tx.concatenate(getMatrixData());
ctx.setTransform(tx);
}
@Override
@ -235,11 +241,9 @@ public class HemfPlusMisc {
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
prop.addLeftTransform(getMatrixData());
ctx.updateWindowMapMode();
AffineTransform tx = ctx.getTransform();
tx.preConcatenate(adaptXForm(getMatrixData(), tx));
tx.concatenate(getMatrixData());
ctx.setTransform(tx);
}
}

View File

@ -224,7 +224,8 @@ public class HemfPlusObject {
return GenericRecordUtil.getGenericProperties(
"flags", getBitsAsString(this::getFlags, FLAGS_MASKS, FLAGS_NAMES),
"objectId", this::getObjectId,
"continuedObjectData", this::getContinuedObject,
"objectData", () -> objectData.isContinuedRecord() ? null : getObjectData(),
"continuedObject", objectData::isContinuedRecord,
"totalObjectSize", () -> totalObjectSize
);
}

View File

@ -161,6 +161,8 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
HemfGraphics g = new HemfGraphics(ctx, emfBounds);
HemfDrawProperties prop = g.getProperties();
prop.setWindowOrg(emfBounds.getX(), emfBounds.getY());
prop.setWindowExt(emfBounds.getWidth(), emfBounds.getHeight());
prop.setViewportOrg(emfBounds.getX(), emfBounds.getY());
prop.setViewportExt(emfBounds.getWidth(), emfBounds.getHeight());

View File

@ -31,8 +31,8 @@ import java.util.List;
import org.apache.poi.hwmf.record.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfFont;
import org.apache.poi.hwmf.record.HwmfFill.WmfSetPolyfillMode.HwmfPolyfillMode;
import org.apache.poi.hwmf.record.HwmfFont;
import org.apache.poi.hwmf.record.HwmfHatchStyle;
import org.apache.poi.hwmf.record.HwmfMapMode;
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
@ -52,6 +52,7 @@ public class HwmfDrawProperties {
private HwmfColorRef brushColor;
private HwmfHatchStyle brushHatch;
private BufferedImage brushBitmap;
private final AffineTransform brushTransform = new AffineTransform();
private double penWidth;
private HwmfPenStyle penStyle;
private HwmfColorRef penColor;
@ -112,6 +113,7 @@ public class HwmfDrawProperties {
WritableRaster raster = other.brushBitmap.copyData(null);
this.brushBitmap = new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
this.brushTransform.setTransform(other.brushTransform);
this.penWidth = other.penWidth;
this.penStyle = (other.penStyle == null) ? null : other.penStyle.clone();
this.penColor = (other.penColor == null) ? null : other.penColor.clone();
@ -411,4 +413,16 @@ public class HwmfDrawProperties {
public void setClip(Shape clip) {
this.clip = clip;
}
public AffineTransform getBrushTransform() {
return brushTransform;
}
public void setBrushTransform(AffineTransform brushTransform) {
if (brushTransform == null) {
this.brushTransform.setToIdentity();
} else {
this.brushTransform.setTransform(brushTransform);
}
}
}

View File

@ -238,9 +238,11 @@ public class HwmfGraphics {
}
protected Paint getPatternPaint() {
BufferedImage bi = getProperties().getBrushBitmap();
return (bi == null) ? null
: new TexturePaint(bi, new Rectangle(0,0,bi.getWidth(),bi.getHeight()));
HwmfDrawProperties prop = getProperties();
BufferedImage bi = prop.getBrushBitmap();
Rectangle2D rect = new Rectangle2D.Double(0, 0, bi.getWidth(), bi.getHeight());
rect = prop.getBrushTransform().createTransformedShape(rect).getBounds2D();
return (bi == null) ? null : new TexturePaint(bi, rect);
}
/**
@ -355,7 +357,7 @@ public class HwmfGraphics {
Rectangle2D win = getProperties().getWindow();
Rectangle2D view = getProperties().getViewport();
HwmfMapMode mapMode = getProperties().getMapMode();
graphicsCtx.setTransform(initialAT);
graphicsCtx.setTransform(getInitTransform());
switch (mapMode) {
default:
@ -673,7 +675,7 @@ public class HwmfGraphics {
public void setClip(Shape clip, HwmfRegionMode regionMode, boolean useInitialAT) {
final AffineTransform at = graphicsCtx.getTransform();
if (useInitialAT) {
graphicsCtx.setTransform(initialAT);
graphicsCtx.setTransform(getInitTransform());
}
final Shape oldClip = graphicsCtx.getClip();

View File

@ -27,20 +27,24 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hwmf.usermodel.HwmfEmbedded;
import org.apache.poi.hwmf.usermodel.HwmfPicture;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.DrawPictureShape;
import org.apache.poi.sl.draw.EmbeddedExtractor;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData.PictureType;
import org.apache.poi.util.Internal;
import org.apache.poi.util.Units;
/**
* Helper class which is instantiated by {@link DrawPictureShape}
* via reflection
*/
public class HwmfImageRenderer implements ImageRenderer {
public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
HwmfPicture image;
double alpha;
@ -117,4 +121,33 @@ public class HwmfImageRenderer implements ImageRenderer {
public GenericRecord getGenericRecord() {
return image;
}
@Override
public Iterable<EmbeddedExtractor.EmbeddedPart> getEmbeddings() {
return getEmbeddings(image.getEmbeddings());
}
@Internal
public static Iterable<EmbeddedPart> getEmbeddings(Iterable<HwmfEmbedded> embs) {
return () -> {
final Iterator<HwmfEmbedded> embit = embs.iterator();
final int[] idx = { 1 };
return new Iterator<EmbeddedExtractor.EmbeddedPart>() {
@Override
public boolean hasNext() {
return embit.hasNext();
}
@Override
public EmbeddedExtractor.EmbeddedPart next() {
EmbeddedExtractor.EmbeddedPart ep = new EmbeddedExtractor.EmbeddedPart();
HwmfEmbedded emb = embit.next();
ep.setData(emb::getRawData);
ep.setName("embed_"+(idx[0]++)+emb.getEmbeddedType().extension);
return ep;
}
};
};
}
}

View File

@ -56,9 +56,10 @@ public class TestHemfPicture {
private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance();
private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance();
/* @Test
/*
@Test
@Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work")
public void paint() throws IOException {
public void paint() throws Exception {
final byte buf[] = new byte[50_000_000];
// good test samples to validate rendering:
@ -67,161 +68,56 @@ public class TestHemfPicture {
// emfs/govdocs1/844/844795.ppt_2.emf
// emfs/commoncrawl2/TO/TOYZSTNUSW5OFCFUQ6T5FBLIDLCRF3NH_0.emf
final boolean writeLog = false;
final boolean dumpRecords = false;
final boolean savePng = true;
final boolean dumpEmbedded = false;
// ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U_6 - map of italy - stroke problem
// 3QKAPISTXYHSFCTV6QTKTYLK6JTWJHQU_2 - text misplaced
// KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0 - dito
// KWG4VAU5GM3POSA4BPG6RSVQVS44SXOL_1.emf - processing freezes
Set<String> passed = new HashSet<>();
// F7GK5XOLERFURVTQALOCX3GJ6FH45LNQ strange colors
// ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U stroke wrong
// KWG4VAU5GM3POSA4BPG6RSVQVS44SXOL_1
try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt")
;BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt")
;BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt")
;SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))
try (SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))
) {
for (int idx=0;;idx++) {
SevenZArchiveEntry entry = sevenZFile.getNextEntry();
if (entry == null) break;
final String etName = entry.getName();
if (entry.isDirectory() || !etName.endsWith(".emf") || passed.contains(etName)) continue;
if (entry.isDirectory() || !etName.endsWith(".emf")) continue;
if (!(etName.contains("3QKAPISTXYHSFCTV6QTKTYLK6JTWJHQU_2")
)) continue;
// KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf takes ages, time is spent while drawing paths
// if (!etName.contains("KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf")) continue;
// F7GK5XOLERFURVTQALOCX3GJ6FH45LNQ strange colors
// ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U stroke wrong
// if (!etName.contains("ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U")) continue;
// || etName.contains("ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U_6")
// || etName.contains("KWG4VAU5GM3POSA4BPG6RSVQVS44SXOL_1")
System.out.println(etName);
int size = sevenZFile.read(buf);
ByteArrayInputStream bis = new ByteArrayInputStream(buf, 0, size);
System.setIn(bis);
HemfPicture emf = null;
try {
emf = new HemfPicture(new ByteArrayInputStream(buf, 0, size));
String lastName = etName.replaceFirst(".+/", "");
// initialize parsing
emf.getRecords();
} catch (Exception|AssertionError e) {
if (writeLog) {
parseError.write(etName+" "+hashException(e)+"\n");
parseError.flush();
}
System.out.println("parse error");
// continue with the read records up to the error anyway
if (emf.getRecords().isEmpty()) {
continue;
}
}
if (dumpRecords) {
dumpRecords(emf);
}
if (dumpEmbedded) {
int embIdx = 0;
for (HwmfEmbedded emb : emf.getEmbeddings()) {
final File embName = new File("build/tmp", "emb_"+etName.replaceFirst(".+/", "").replace(".emf", "_"+embIdx + emb.getEmbeddedType().extension) );
try (FileOutputStream fos = new FileOutputStream(embName)) {
fos.write(emb.getRawData());
}
embIdx++;
}
}
Graphics2D g = null;
try {
Dimension2D dim = emf.getSize();
double width = Units.pointsToPixel(dim.getWidth());
// keep aspect ratio for height
double height = Units.pointsToPixel(dim.getHeight());
double max = Math.max(width, height);
if (max > 1500.) {
width *= 1500. / max;
height *= 1500. / max;
}
width = Math.ceil(width);
height = Math.ceil(height);
BufferedImage bufImg = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB);
g = bufImg.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g.setComposite(AlphaComposite.Clear);
g.fillRect(0, 0, (int)width, (int)height);
g.setComposite(AlphaComposite.Src);
final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png"));
emf.draw(g, new Rectangle2D.Double(0, 0, width, height));
if (savePng) {
ImageIO.write(bufImg, "PNG", pngName);
}
} catch (Exception|AssertionError e) {
System.out.println("render error");
if (writeLog) {
// dumpRecords(emf.getRecords());
renderError.write(etName+" "+hashException(e)+"\n");
renderError.flush();
}
continue;
} finally {
if (g != null) g.dispose();
}
if (writeLog) {
sucWrite.write(etName + "\n");
sucWrite.flush();
}
String[] args = {
"-format", "png", // png,gif,jpg or null for test
"-outdir", new File("build/tmp/").getCanonicalPath(),
"-outfile", lastName.replace(".emf", ".png"),
"-fixside", "long",
"-scale", "800",
"-ignoreParse",
"-dump", new File("build/tmp/", lastName.replace(".emf",".json")).getCanonicalPath(),
// "-quiet",
// "-extractEmbedded",
"stdin"
};
PPTX2PNG.main(args);
}
}
}
private static int hashException(Throwable e) {
StringBuilder sb = new StringBuilder();
for (StackTraceElement se : e.getStackTrace()) {
sb.append(se.getClassName()+":"+se.getLineNumber());
}
return sb.toString().hashCode();
}
private static void dumpRecords(HemfPicture emf) throws IOException {
FileWriter fw = new FileWriter("record-list.txt");
int i = 0;
for (HemfRecord r : emf.getRecords()) {
if (r.getEmfRecordType() != HemfRecordType.comment) {
fw.write(i + " " + r.getEmfRecordType() + " " + r.toString() + "\n");
}
i++;
}
fw.close();
}
private static BufferedWriter parseEmfLog(Set<String> passed, String logFile) throws IOException {
Path log = Paths.get(logFile);
StandardOpenOption soo;
if (Files.exists(log)) {
soo = StandardOpenOption.APPEND;
try (Stream<String> stream = Files.lines(log)) {
stream.filter(s -> !s.startsWith("#")).forEach((s) -> passed.add(s.split("\\s")[0]));
}
} else {
soo = StandardOpenOption.CREATE;
}
return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo);
}*/
*/
@Test
public void testBasicWindows() throws Exception {
try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) {