mirror of https://github.com/apache/poi.git
#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:
parent
3f0a01ae7c
commit
f7c28ad08f
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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(" }");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,7 +219,8 @@ public class HemfText {
|
|||
return GenericRecordUtil.getGenericProperties(
|
||||
"base", super::getGenericProperties,
|
||||
"boundsIgnored", () -> boundsIgnored,
|
||||
"graphicsMode", this::getGraphicsMode
|
||||
"graphicsMode", this::getGraphicsMode,
|
||||
"scale", this::getScale
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")) {
|
||||
|
|
Loading…
Reference in New Issue