git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hemf@1841712 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_1_0
@@ -105,7 +105,6 @@ public abstract class SlideShowHandler extends POIFSFileHandler { | |||
for (Slide<?,?> s : ss.getSlides()) { | |||
BufferedImage img = new BufferedImage(pgsize.width, pgsize.height, BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D graphics = img.createGraphics(); | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
// default rendering options | |||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
@@ -40,6 +40,7 @@ import javax.imageio.ImageTypeSpecifier; | |||
import javax.imageio.stream.ImageInputStream; | |||
import javax.imageio.stream.MemoryCacheImageInputStream; | |||
import org.apache.poi.sl.usermodel.PictureData.PictureType; | |||
import org.apache.poi.util.IOUtils; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
@@ -48,10 +49,23 @@ import org.apache.poi.util.POILogger; | |||
* For now this class renders only images supported by the javax.imageio.ImageIO framework. | |||
**/ | |||
public class BitmapImageRenderer implements ImageRenderer { | |||
private final static POILogger LOG = POILogFactory.getLogger(ImageRenderer.class); | |||
private final static POILogger LOG = POILogFactory.getLogger(BitmapImageRenderer.class); | |||
protected BufferedImage img; | |||
@Override | |||
public boolean canRender(String contentType) { | |||
PictureType[] pts = { | |||
PictureType.JPEG, PictureType.PNG, PictureType.BMP, PictureType.GIF | |||
}; | |||
for (PictureType pt : pts) { | |||
if (pt.contentType.equalsIgnoreCase(contentType)) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
@Override | |||
public void loadImage(InputStream data, String contentType) throws IOException { | |||
img = readImage(data, contentType); |
@@ -22,8 +22,6 @@ import java.awt.font.TextLayout; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Rectangle2D; | |||
import java.text.AttributedString; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import org.apache.poi.sl.usermodel.Background; | |||
import org.apache.poi.sl.usermodel.ConnectorShape; | |||
@@ -40,18 +38,18 @@ import org.apache.poi.sl.usermodel.TableShape; | |||
import org.apache.poi.sl.usermodel.TextBox; | |||
import org.apache.poi.sl.usermodel.TextParagraph; | |||
import org.apache.poi.sl.usermodel.TextShape; | |||
import org.apache.poi.util.JvmBugs; | |||
public class DrawFactory { | |||
protected static final ThreadLocal<DrawFactory> defaultFactory = new ThreadLocal<>(); | |||
private static final ThreadLocal<DrawFactory> defaultFactory = new ThreadLocal<>(); | |||
/** | |||
* Set a custom draw factory for the current thread. | |||
* This is a fallback, for operations where usercode can't set a graphics context. | |||
* Preferably use the rendering hint {@link Drawable#DRAW_FACTORY} to set the factory. | |||
* | |||
* @param factory | |||
* @param factory the custom factory | |||
*/ | |||
@SuppressWarnings("unused") | |||
public static void setDefaultFactory(DrawFactory factory) { | |||
defaultFactory.set(factory); | |||
} | |||
@@ -170,6 +168,7 @@ public class DrawFactory { | |||
return new DrawBackground(shape); | |||
} | |||
@SuppressWarnings("WeakerAccess") | |||
public DrawTextFragment getTextFragment(TextLayout layout, AttributedString str) { | |||
return new DrawTextFragment(layout, str); | |||
} | |||
@@ -213,35 +212,6 @@ public class DrawFactory { | |||
} | |||
/** | |||
* Replace font families for Windows JVM 6, which contains a font rendering error. | |||
* This is likely to be removed, when POI upgrades to JDK 7 | |||
* | |||
* @param graphics the graphics context which will contain the font mapping | |||
*/ | |||
public void fixFonts(Graphics2D graphics) { | |||
if (!JvmBugs.hasLineBreakMeasurerBug()) return; | |||
@SuppressWarnings("unchecked") | |||
Map<String,String> fontMap = (Map<String,String>)graphics.getRenderingHint(Drawable.FONT_MAP); | |||
if (fontMap == null) { | |||
fontMap = new HashMap<>(); | |||
graphics.setRenderingHint(Drawable.FONT_MAP, fontMap); | |||
} | |||
String fonts[][] = { | |||
{ "Calibri", "Lucida Sans" }, | |||
{ "Cambria", "Lucida Bright" }, | |||
{ "Times New Roman", "Lucida Bright" }, | |||
{ "serif", "Lucida Bright" } | |||
}; | |||
for (String f[] : fonts) { | |||
if (!fontMap.containsKey(f[0])) { | |||
fontMap.put(f[0], f[1]); | |||
} | |||
} | |||
} | |||
/** | |||
* Return a FontManager, either registered beforehand or a default implementation | |||
* |
@@ -22,19 +22,19 @@ import java.awt.Graphics2D; | |||
import java.awt.Insets; | |||
import java.awt.geom.Rectangle2D; | |||
import java.io.IOException; | |||
import java.util.ServiceLoader; | |||
import org.apache.poi.sl.usermodel.PictureData; | |||
import org.apache.poi.sl.usermodel.PictureData.PictureType; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
import org.apache.poi.sl.usermodel.PictureShape; | |||
import org.apache.poi.sl.usermodel.RectAlign; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
public class DrawPictureShape extends DrawSimpleShape { | |||
private static final POILogger LOG = POILogFactory.getLogger(DrawPictureShape.class); | |||
private static final String WMF_IMAGE_RENDERER = "org.apache.poi.hwmf.draw.HwmfSLImageRenderer"; | |||
private static final ServiceLoader<ImageRenderer> rendererLoader = ServiceLoader.load(ImageRenderer.class); | |||
public DrawPictureShape(PictureShape<?,?> shape) { | |||
super(shape); | |||
} | |||
@@ -59,28 +59,26 @@ public class DrawPictureShape extends DrawSimpleShape { | |||
/** | |||
* Returns an ImageRenderer for the PictureData | |||
* | |||
* @param graphics | |||
* @param graphics the graphics context | |||
* @return the image renderer | |||
*/ | |||
@SuppressWarnings("WeakerAccess") | |||
public static ImageRenderer getImageRenderer(Graphics2D graphics, String contentType) { | |||
ImageRenderer renderer = (ImageRenderer)graphics.getRenderingHint(Drawable.IMAGE_RENDERER); | |||
if (renderer != null) { | |||
return renderer; | |||
} | |||
if (PictureType.WMF.contentType.equals(contentType)) { | |||
try { | |||
@SuppressWarnings("unchecked") | |||
Class<? extends ImageRenderer> irc = (Class<? extends ImageRenderer>) | |||
DrawPictureShape.class.getClassLoader().loadClass(WMF_IMAGE_RENDERER); | |||
return irc.newInstance(); | |||
} catch (Exception e) { | |||
// WMF image renderer is not on the classpath, continuing with BitmapRenderer | |||
// although this doesn't make much sense ... | |||
LOG.log(POILogger.ERROR, "WMF image renderer is not on the classpath - include poi-scratchpad jar!", e); | |||
for (ImageRenderer ir : rendererLoader) { | |||
if (ir.canRender(contentType)) { | |||
return ir; | |||
} | |||
} | |||
LOG.log(POILogger.ERROR, "No suiteable image renderer found for content-type '"+ | |||
contentType+"' - include poi-scratchpad jar!"); | |||
// falling back to BitmapImageRenderer although this doesn't make much sense ... | |||
return new BitmapImageRenderer(); | |||
} | |||
@@ -254,7 +254,6 @@ public class DrawTextParagraph implements Drawable { | |||
lines.clear(); | |||
DrawFactory fact = DrawFactory.getInstance(graphics); | |||
fact.fixFonts(graphics); | |||
StringBuilder text = new StringBuilder(); | |||
AttributedString at = getAttributedString(graphics, text); | |||
boolean emptyParagraph = text.toString().trim().isEmpty(); | |||
@@ -635,13 +634,6 @@ public class DrawTextParagraph implements Drawable { | |||
* <li>determine the font group - a text run can have different font groups. Depending on the chars, | |||
* the correct font group needs to be used | |||
* | |||
* @param graphics | |||
* @param dfm | |||
* @param attList | |||
* @param beginIndex | |||
* @param run | |||
* @param runText | |||
* | |||
* @see <a href="https://blogs.msdn.microsoft.com/officeinteroperability/2013/04/22/office-open-xml-themes-schemes-and-fonts/">Office Open XML Themes, Schemes, and Fonts</a> | |||
*/ | |||
private void processGlyphs(Graphics2D graphics, DrawFontManager dfm, List<AttributedStringData> attList, final int beginIndex, TextRun run, String runText) { |
@@ -40,8 +40,6 @@ public class DrawTextShape extends DrawSimpleShape { | |||
@Override | |||
public void drawContent(Graphics2D graphics) { | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
TextShape<?,?> s = getShape(); | |||
Rectangle2D anchor = DrawShape.getAnchor(graphics, s); | |||
@@ -219,10 +217,9 @@ public class DrawTextShape extends DrawSimpleShape { | |||
graphics.addRenderingHints(oldGraphics.getRenderingHints()); | |||
graphics.setTransform(oldGraphics.getTransform()); | |||
} | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
return drawParagraphs(graphics, 0, 0); | |||
} | |||
@Override | |||
protected TextShape<?,? extends TextParagraph<?,?,? extends TextRun>> getShape() { | |||
return (TextShape<?,? extends TextParagraph<?,?,? extends TextRun>>)shape; |
@@ -75,6 +75,13 @@ import java.io.InputStream; | |||
* </pre> | |||
*/ | |||
public interface ImageRenderer { | |||
/** | |||
* Determines if this image renderer implementation supports the given contentType | |||
* @param contentType the image content type | |||
* @return if the content type is supported | |||
*/ | |||
boolean canRender(String contentType); | |||
/** | |||
* Load and buffer the image | |||
* |
@@ -139,7 +139,6 @@ public class PPTX2PNG { | |||
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D graphics = img.createGraphics(); | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
// default rendering options | |||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
@@ -131,8 +131,6 @@ public class TestFonts { | |||
graphics.setRenderingHint(Drawable.FONT_FALLBACK, fallbackMap); | |||
graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
tb.resizeToFitText(graphics); | |||
graphics.dispose(); | |||
@@ -361,7 +361,6 @@ public class TestXSLFSimpleShape { | |||
BufferedImage img = new BufferedImage(pgsize.width, pgsize.height, BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D graphics = img.createGraphics(); | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
// default rendering options | |||
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
@@ -0,0 +1 @@ | |||
org.apache.poi.sl.draw.BitmapImageRenderer |
@@ -0,0 +1,2 @@ | |||
org.apache.poi.hwmf.draw.HwmfImageRenderer | |||
org.apache.poi.hemf.draw.HemfImageRenderer |
@@ -17,14 +17,29 @@ | |||
package org.apache.poi.hemf.draw; | |||
import java.awt.geom.Path2D; | |||
import org.apache.poi.hwmf.draw.HwmfDrawProperties; | |||
public class HemfDrawProperties extends HwmfDrawProperties { | |||
/** Path for path bracket operations */ | |||
protected final Path2D path; | |||
public HemfDrawProperties() { | |||
path = new Path2D.Double(); | |||
} | |||
public HemfDrawProperties(HemfDrawProperties other) { | |||
super(other); | |||
path = (Path2D)other.path.clone(); | |||
} | |||
/** | |||
* @return the current path used for bracket operations | |||
*/ | |||
public Path2D getPath() { | |||
return path; | |||
} | |||
} |
@@ -18,14 +18,23 @@ | |||
package org.apache.poi.hemf.draw; | |||
import java.awt.Graphics2D; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Rectangle2D; | |||
import java.util.ArrayDeque; | |||
import java.util.Deque; | |||
import org.apache.poi.hwmf.draw.HwmfDrawProperties; | |||
import org.apache.poi.hemf.record.emf.HemfBounded; | |||
import org.apache.poi.hemf.record.emf.HemfRecord; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics; | |||
public class HemfGraphics extends HwmfGraphics { | |||
private final Deque<AffineTransform> transforms = new ArrayDeque<>(); | |||
public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { | |||
super(graphicsCtx,bbox); | |||
// add dummy entry for object index 0, as emf is 1-based | |||
addObjectTableEntry((ctx)->{}); | |||
} | |||
@Override | |||
@@ -42,4 +51,43 @@ public class HemfGraphics extends HwmfGraphics { | |||
propStack.add(prop); | |||
prop = new HemfDrawProperties((HemfDrawProperties)prop); | |||
} | |||
@Override | |||
public void updateWindowMapMode() { | |||
// ignore window settings | |||
} | |||
public void draw(HemfRecord r) { | |||
if (r instanceof HemfBounded) { | |||
saveTransform(); | |||
final HemfBounded bounded = (HemfBounded)r; | |||
final Rectangle2D tgt = bounded.getRecordBounds(); | |||
if (tgt != null && !tgt.isEmpty()) { | |||
final Rectangle2D src = bounded.getShapeBounds(this); | |||
if (src != null && !src.isEmpty()) { | |||
graphicsCtx.translate(tgt.getCenterX() - src.getCenterX(), tgt.getCenterY() - src.getCenterY()); | |||
graphicsCtx.translate(src.getCenterX(), src.getCenterY()); | |||
graphicsCtx.scale(tgt.getWidth() / src.getWidth(), tgt.getHeight() / src.getHeight()); | |||
graphicsCtx.translate(-src.getCenterX(), -src.getCenterY()); | |||
} | |||
} | |||
} | |||
r.draw(this); | |||
if (r instanceof HemfBounded) { | |||
restoreTransform(); | |||
} | |||
} | |||
/** saves the current affine transform on the stack */ | |||
private void saveTransform() { | |||
transforms.push(graphicsCtx.getTransform()); | |||
} | |||
/** restore the last saved affine transform */ | |||
private void restoreTransform() { | |||
graphicsCtx.setTransform(transforms.pop()); | |||
} | |||
} |
@@ -0,0 +1,126 @@ | |||
/* ==================================================================== | |||
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.hemf.draw; | |||
import java.awt.Dimension; | |||
import java.awt.Graphics2D; | |||
import java.awt.Insets; | |||
import java.awt.RenderingHints; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
import java.awt.image.RescaleOp; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import org.apache.poi.hemf.usermodel.HemfPicture; | |||
import org.apache.poi.sl.draw.ImageRenderer; | |||
import org.apache.poi.sl.usermodel.PictureData; | |||
import org.apache.poi.util.Units; | |||
public class HemfImageRenderer implements ImageRenderer { | |||
HemfPicture image; | |||
double alpha; | |||
@Override | |||
public boolean canRender(String contentType) { | |||
return PictureData.PictureType.EMF.contentType.equalsIgnoreCase(contentType); | |||
} | |||
@Override | |||
public void loadImage(InputStream data, String contentType) throws IOException { | |||
if (!PictureData.PictureType.EMF.contentType.equals(contentType)) { | |||
throw new IOException("Invalid picture type"); | |||
} | |||
image = new HemfPicture(data); | |||
} | |||
@Override | |||
public void loadImage(byte[] data, String contentType) throws IOException { | |||
if (!PictureData.PictureType.EMF.contentType.equals(contentType)) { | |||
throw new IOException("Invalid picture type"); | |||
} | |||
image = new HemfPicture(new ByteArrayInputStream(data)); | |||
} | |||
@Override | |||
public Dimension getDimension() { | |||
int width = 0, height = 0; | |||
if (image != null) { | |||
Dimension2D dim = image.getSize(); | |||
width = Units.pointsToPixel(dim.getWidth()); | |||
// keep aspect ratio for height | |||
height = Units.pointsToPixel(dim.getHeight()); | |||
} | |||
return new Dimension(width, height); | |||
} | |||
@Override | |||
public void setAlpha(double alpha) { | |||
this.alpha = alpha; | |||
} | |||
@Override | |||
public BufferedImage getImage() { | |||
return getImage(getDimension()); | |||
} | |||
@Override | |||
public BufferedImage getImage(Dimension dim) { | |||
if (image == null) { | |||
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); | |||
} | |||
BufferedImage bufImg = new BufferedImage((int)dim.getWidth(), (int)dim.getHeight(), BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D g = bufImg.createGraphics(); | |||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); | |||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); | |||
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); | |||
image.draw(g, new Rectangle2D.Double(0,0,dim.getWidth(),dim.getHeight())); | |||
g.dispose(); | |||
if (alpha != 0) { | |||
BufferedImage newImg = new BufferedImage((int)dim.getWidth(), (int)dim.getHeight(), BufferedImage.TYPE_INT_ARGB); | |||
g = newImg.createGraphics(); | |||
RescaleOp op = new RescaleOp(new float[]{1.0f, 1.0f, 1.0f, (float)alpha}, new float[]{0,0,0,0}, null); | |||
g.drawImage(bufImg, op, 0, 0); | |||
g.dispose(); | |||
bufImg = newImg; | |||
} | |||
return bufImg; | |||
} | |||
@Override | |||
public boolean drawImage(Graphics2D graphics, Rectangle2D anchor) { | |||
return drawImage(graphics, anchor, null); | |||
} | |||
@Override | |||
public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) { | |||
if (image == null) { | |||
return false; | |||
} else { | |||
image.draw(graphics, anchor); | |||
return true; | |||
} | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
/* ==================================================================== | |||
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.hemf.record.emf; | |||
import java.awt.geom.Rectangle2D; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
/** | |||
* In EMF, shape records bring their own bounding. | |||
* The record bounding is in the same space as the global drawing context, | |||
* but the specified shape points can have a different space and therefore | |||
* need to be translated/normalized | |||
*/ | |||
public interface HemfBounded { | |||
/** | |||
* Getter for the outer bounds which are given in the record | |||
* | |||
* @return the bounds specified in the record | |||
*/ | |||
Rectangle2D getRecordBounds(); | |||
/** | |||
* Getter for the inner bounds which are calculated by the shape points | |||
* | |||
* @param ctx the graphics context | |||
* @return the bounds of the shape points | |||
*/ | |||
Rectangle2D getShapeBounds(HemfGraphics ctx); | |||
} |
@@ -21,6 +21,8 @@ import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL; | |||
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; | |||
import java.awt.Color; | |||
import java.awt.geom.Arc2D; | |||
import java.awt.geom.Area; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.PathIterator; | |||
@@ -30,7 +32,6 @@ import java.io.IOException; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics; | |||
import org.apache.poi.hwmf.record.HwmfColorRef; | |||
import org.apache.poi.hwmf.record.HwmfDraw; | |||
import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject; | |||
@@ -187,7 +188,7 @@ public class HemfDraw { | |||
/** The EMR_POLYBEZIER record specifies one or more Bezier curves. */ | |||
public static class EmfPolyBezier extends HwmfDraw.WmfPolygon implements HemfRecord { | |||
public static class EmfPolyBezier extends HwmfDraw.WmfPolygon implements HemfRecord, HemfBounded { | |||
private final Rectangle2D bounds = new Rectangle2D.Double(); | |||
@Override | |||
@@ -238,9 +239,13 @@ public class HemfDraw { | |||
for (int i=0; i+3<pointCnt; i+=3) { | |||
// x (4 bytes): A 32-bit signed integer that defines the horizontal (x) coordinate of the point. | |||
// y (4 bytes): A 32-bit signed integer that defines the vertical (y) coordinate of the point. | |||
if (i==0 && hasStartPoint()) { | |||
size += readPoint(leis, pnt[0]); | |||
poly.moveTo(pnt[0].getX(),pnt[0].getY()); | |||
if (i==0) { | |||
if (hasStartPoint()) { | |||
size += readPoint(leis, pnt[0]); | |||
poly.moveTo(pnt[0].getX(), pnt[0].getY()); | |||
} else { | |||
poly.moveTo(0, 0); | |||
} | |||
} | |||
size += readPoint(leis, pnt[0]); | |||
@@ -257,6 +262,19 @@ public class HemfDraw { | |||
return size; | |||
} | |||
@Override | |||
public Rectangle2D getRecordBounds() { | |||
return bounds; | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
if (!hasStartPoint()) { | |||
throw new IllegalStateException("shape bounds not valid for path bracket based record: "+getClass().getName()); | |||
} | |||
return poly.getBounds2D(); | |||
} | |||
/** | |||
* @return true, if start point is in the list of points. false, if start point is taken from the context | |||
*/ | |||
@@ -285,7 +303,7 @@ public class HemfDraw { | |||
* The EMR_POLYGON record specifies a polygon consisting of two or more vertexes connected by | |||
* straight lines. | |||
*/ | |||
public static class EmfPolygon extends HwmfDraw.WmfPolygon implements HemfRecord { | |||
public static class EmfPolygon extends HwmfDraw.WmfPolygon implements HemfRecord, HemfBounded { | |||
private final Rectangle2D bounds = new Rectangle2D.Double(); | |||
@Override | |||
@@ -310,21 +328,34 @@ public class HemfDraw { | |||
for (int i=0; i<points; i++) { | |||
size += readPoint(leis, pnt); | |||
if (i==0) { | |||
poly.moveTo(pnt.getX(), pnt.getY()); | |||
if (hasStartPoint()) { | |||
continue; | |||
poly.moveTo(pnt.getX(), pnt.getY()); | |||
} else { | |||
// if this path is connected to the current position (= has no start point) | |||
// the first entry is a dummy entry and will be skipped later | |||
poly.moveTo(0,0); | |||
} | |||
// if this path is connected to the current position (= has no start point) | |||
// the first entry is a dummy entry and will be skipped later | |||
} else { | |||
poly.lineTo(pnt.getX(), pnt.getY()); | |||
} | |||
poly.lineTo(pnt.getX(), pnt.getY()); | |||
} | |||
return size; | |||
} | |||
@Override | |||
public Rectangle2D getRecordBounds() { | |||
return bounds; | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
if (!hasStartPoint()) { | |||
throw new IllegalStateException("shape bounds not valid for path bracket based record: "+getClass().getName()); | |||
} | |||
return poly.getBounds2D(); | |||
} | |||
/** | |||
* @return true, if start point is in the list of points. false, if start point is taken from the context | |||
*/ | |||
@@ -398,8 +429,15 @@ public class HemfDraw { | |||
} | |||
@Override | |||
protected Path2D getShape(HwmfGraphics ctx) { | |||
return polyTo(ctx, poly); | |||
public void draw(HemfGraphics ctx) { | |||
polyTo(ctx, poly); | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
// should be called in a beginPath/endPath bracket, so the shape bounds | |||
// of this path segment are irrelevant | |||
return null; | |||
} | |||
} | |||
@@ -432,8 +470,15 @@ public class HemfDraw { | |||
} | |||
@Override | |||
protected Path2D getShape(HwmfGraphics ctx) { | |||
return polyTo(ctx, poly); | |||
public void draw(HemfGraphics ctx) { | |||
polyTo(ctx, poly); | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
// should be called in a beginPath/endPath bracket, so the shape bounds | |||
// of this path segment are irrelevant | |||
return null; | |||
} | |||
} | |||
@@ -458,7 +503,7 @@ public class HemfDraw { | |||
/** | |||
* The EMR_POLYPOLYGON record specifies a series of closed polygons. | |||
*/ | |||
public static class EmfPolyPolygon extends HwmfDraw.WmfPolyPolygon implements HemfRecord { | |||
public static class EmfPolyPolygon extends HwmfDraw.WmfPolyPolygon implements HemfRecord, HemfBounded { | |||
private final Rectangle2D bounds = new Rectangle2D.Double(); | |||
@Override | |||
@@ -519,6 +564,17 @@ public class HemfDraw { | |||
protected boolean isClosed() { | |||
return true; | |||
} | |||
@Override | |||
public Rectangle2D getRecordBounds() { | |||
return bounds; | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
Area area = getShape(ctx); | |||
return area == null ? bounds : area.getBounds2D(); | |||
} | |||
} | |||
/** | |||
@@ -601,6 +657,14 @@ public class HemfDraw { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return readPointL(leis, point); | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
path.moveTo(point.getX(), point.getY()); | |||
prop.setLocation(point); | |||
} | |||
} | |||
/** | |||
@@ -736,6 +800,14 @@ public class HemfDraw { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return readPointL(leis, point); | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
path.lineTo(point.getX(), point.getY()); | |||
prop.setLocation(point); | |||
} | |||
} | |||
/** | |||
@@ -758,13 +830,15 @@ public class HemfDraw { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
super.draw(ctx); | |||
final Path2D path = ctx.getProperties().getPath(); | |||
Arc2D arc = getShape(); | |||
path.append(arc, true); | |||
ctx.getProperties().setLocation(endPoint); | |||
} | |||
} | |||
/** The EMR_POLYDRAW record specifies a set of line segments and Bezier curves. */ | |||
public static class EmfPolyDraw extends HwmfDraw.WmfPolygon implements HemfRecord { | |||
public static class EmfPolyDraw extends HwmfDraw.WmfPolygon implements HemfRecord, HemfBounded { | |||
private final Rectangle2D bounds = new Rectangle2D.Double(); | |||
@Override | |||
@@ -837,6 +911,16 @@ public class HemfDraw { | |||
size += count; | |||
return size; | |||
} | |||
@Override | |||
public Rectangle2D getRecordBounds() { | |||
return bounds; | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
return poly.getBounds2D(); | |||
} | |||
} | |||
public static class EmfPolyDraw16 extends EmfPolyDraw { | |||
@@ -850,6 +934,16 @@ public class HemfDraw { | |||
} | |||
} | |||
/** | |||
* This record opens a path bracket in the current playback device context. | |||
* | |||
* After a path bracket is open, an application can begin processing records to define | |||
* the points that lie in the path. An application MUST close an open path bracket by | |||
* processing the EMR_ENDPATH record. | |||
* | |||
* When an application processes the EMR_BEGINPATH record, all previous paths | |||
* MUST be discarded from the playback device context. | |||
*/ | |||
public static class EmfBeginPath implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -860,8 +954,19 @@ public class HemfDraw { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return 0; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
path.reset(); | |||
} | |||
} | |||
/** | |||
* This record closes a path bracket and selects the path defined by the bracket into | |||
* the playback device context. | |||
*/ | |||
public static class EmfEndPath implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -874,6 +979,9 @@ public class HemfDraw { | |||
} | |||
} | |||
/** | |||
* This record aborts a path bracket or discards the path from a closed path bracket. | |||
*/ | |||
public static class EmfAbortPath implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -884,8 +992,34 @@ public class HemfDraw { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return 0; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
path.reset(); | |||
} | |||
} | |||
/** | |||
* This record closes an open figure in a path. | |||
* | |||
* Processing the EMR_CLOSEFIGURE record MUST close the figure by drawing a line | |||
* from the current position to the first point of the figure, and then it MUST connect | |||
* the lines by using the line join style. If a figure is closed by processing the | |||
* EMR_LINETO record instead of the EMR_CLOSEFIGURE record, end caps are | |||
* used to create the corner instead of a join. | |||
* | |||
* The EMR_CLOSEFIGURE record SHOULD only be used if there is an open path | |||
* bracket in the playback device context. | |||
* | |||
* A figure in a path is open unless it is explicitly closed by processing this record. | |||
* Note: A figure can be open even if the current point and the starting point of the | |||
* figure are the same. | |||
* | |||
* After processing the EMR_CLOSEFIGURE record, adding a line or curve to the path | |||
* MUST start a new figure. | |||
*/ | |||
public static class EmfCloseFigure implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -896,8 +1030,22 @@ public class HemfDraw { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return 0; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
path.closePath(); | |||
prop.setLocation(path.getCurrentPoint()); | |||
} | |||
} | |||
/** | |||
* This record transforms any curves in the selected path into the playback device | |||
* context; each curve MUST be turned into a sequence of lines. | |||
*/ | |||
public static class EmfFlattenPath implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -910,6 +1058,10 @@ public class HemfDraw { | |||
} | |||
} | |||
/** | |||
* This record redefines the current path as the area that would be painted if the path | |||
* were drawn using the pen currently selected into the playback device context. | |||
*/ | |||
public static class EmfWidenPath implements HemfRecord { | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
@@ -988,16 +1140,15 @@ public class HemfDraw { | |||
return 2*LittleEndianConsts.INT_SIZE; | |||
} | |||
private static Path2D polyTo(HwmfGraphics ctx, Path2D poly) { | |||
Path2D polyCopy = new Path2D.Double(); | |||
Point2D start = ctx.getProperties().getLocation(); | |||
polyCopy.moveTo(start.getX(), start.getY()); | |||
PathIterator iter = poly.getPathIterator(null); | |||
iter.next(); | |||
polyCopy.append(iter, true); | |||
return polyCopy; | |||
private static void polyTo(HemfGraphics ctx, Path2D poly) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
PathIterator pi = poly.getPathIterator(null); | |||
// ignore dummy start point (moveTo) | |||
pi.next(); | |||
assert(!pi.isDone()); | |||
path.append(pi, true); | |||
prop.setLocation(path.getCurrentPoint()); | |||
} | |||
@@ -17,12 +17,13 @@ | |||
package org.apache.poi.hemf.record.emf; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; | |||
import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Area; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.io.ByteArrayInputStream; | |||
@@ -31,8 +32,11 @@ import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics; | |||
import org.apache.poi.hwmf.record.HwmfBitmapDib; | |||
import org.apache.poi.hwmf.record.HwmfBrushStyle; | |||
import org.apache.poi.hwmf.record.HwmfColorRef; | |||
import org.apache.poi.hwmf.record.HwmfDraw; | |||
import org.apache.poi.hwmf.record.HwmfFill; | |||
@@ -515,7 +519,7 @@ public class HemfFill { | |||
static long readBitmap(final LittleEndianInputStream leis, final HwmfBitmapDib bitmap, | |||
final int startIdx, final int offBmiSrc, final int cbBmiSrc, final int offBitsSrc, int cbBitsSrc) | |||
throws IOException { | |||
final int offCurr = leis.getReadIndex()-(startIdx-HEADER_SIZE); | |||
final int offCurr = leis.getReadIndex()-startIdx; | |||
final int undefinedSpace1 = offBmiSrc-offCurr; | |||
assert(undefinedSpace1 >= 0); | |||
@@ -622,7 +626,7 @@ public class HemfFill { | |||
* The EMR_FILLPATH record closes any open figures in the current path and fills the path's interior by | |||
* using the current brush and polygon-filling mode. | |||
*/ | |||
public static class EmfFillPath implements HemfRecord { | |||
public static class EmfFillPath implements HemfRecord, HemfBounded { | |||
protected final Rectangle2D bounds = new Rectangle2D.Double(); | |||
@Override | |||
@@ -635,5 +639,29 @@ public class HemfFill { | |||
// A 128-bit WMF RectL object, which specifies bounding rectangle, in device units | |||
return readRectL(leis, bounds); | |||
} | |||
@Override | |||
public Rectangle2D getRecordBounds() { | |||
return bounds; | |||
} | |||
@Override | |||
public Rectangle2D getShapeBounds(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
return path.getBounds2D(); | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
final HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = (Path2D)prop.getPath().clone(); | |||
path.setWindingRule(ctx.getProperties().getWindingRule()); | |||
if (prop.getBrushStyle() == HwmfBrushStyle.BS_NULL) { | |||
ctx.draw(path); | |||
} else { | |||
ctx.fill(path); | |||
} | |||
} | |||
} | |||
} |
@@ -111,12 +111,16 @@ public class HemfHeader implements HemfRecord { | |||
return hasExtension2; | |||
} | |||
public long getMicrometersX() { | |||
return (long)microDimension.getWidth(); | |||
public Dimension2D getDeviceDimension() { | |||
return deviceDimension; | |||
} | |||
public long getMicrometersY() { | |||
return (long)microDimension.getHeight(); | |||
public Dimension2D getMilliDimension() { | |||
return milliDimension; | |||
} | |||
public Dimension2D getMicroDimension() { | |||
return microDimension; | |||
} | |||
@Override | |||
@@ -135,8 +139,9 @@ public class HemfHeader implements HemfRecord { | |||
", offPixelFormat=" + offPixelFormat + | |||
", bOpenGL=" + bOpenGL + | |||
", hasExtension2=" + hasExtension2 + | |||
", micrometersX=" + getMicrometersX() + | |||
", micrometersY=" + getMicrometersY() + | |||
", deviceDimension=" + deviceDimension + | |||
", microDimension=" + microDimension + | |||
", milliDimension=" + milliDimension + | |||
'}'; | |||
} | |||
@@ -19,8 +19,10 @@ package org.apache.poi.hemf.record.emf; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; | |||
import static org.apache.poi.hemf.record.emf.HemfFill.readBitmap; | |||
import static org.apache.poi.hemf.record.emf.HemfFill.readXForm; | |||
import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Point2D; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
@@ -54,34 +56,40 @@ public class HemfMisc { | |||
@Override | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
final int startIdx = leis.getReadIndex(); | |||
// A 32-bit unsigned integer that specifies the number of palette entries. | |||
int nPalEntries = (int) leis.readUInt(); | |||
final int nPalEntries = (int) leis.readUInt(); | |||
// A 32-bit unsigned integer that specifies the offset to the palette entries from the start of this record. | |||
int offPalEntries = (int) leis.readUInt(); | |||
final int offPalEntries = (int) leis.readUInt(); | |||
int size = 2 * LittleEndianConsts.INT_SIZE; | |||
int undefinedSpace1 = (int) (offPalEntries - size - HEADER_SIZE); | |||
assert (undefinedSpace1 >= 0); | |||
leis.skipFully(undefinedSpace1); | |||
size += undefinedSpace1; | |||
for (int i = 0; i < nPalEntries; i++) { | |||
PaletteEntry pe = new PaletteEntry(); | |||
size += pe.init(leis); | |||
} | |||
int undefinedSpace2 = (int) (recordSize - size - LittleEndianConsts.INT_SIZE); | |||
assert (undefinedSpace2 >= 0); | |||
leis.skipFully(undefinedSpace2); | |||
size += undefinedSpace2; | |||
if (offPalEntries > 0) { | |||
int undefinedSpace1 = (int) (offPalEntries - size - HEADER_SIZE); | |||
assert (undefinedSpace1 >= 0); | |||
leis.skipFully(undefinedSpace1); | |||
size += undefinedSpace1; | |||
for (int i = 0; i < nPalEntries; i++) { | |||
PaletteEntry pe = new PaletteEntry(); | |||
size += pe.init(leis); | |||
} | |||
int undefinedSpace2 = (int) (recordSize - size - LittleEndianConsts.INT_SIZE); | |||
assert (undefinedSpace2 >= 0); | |||
leis.skipFully(undefinedSpace2); | |||
size += undefinedSpace2; | |||
} | |||
// A 32-bit unsigned integer that MUST be the same as Size and MUST be the | |||
// last field of the record and hence the metafile. | |||
// LogPaletteEntry objects, if they exist, MUST precede this field. | |||
long sizeLast = leis.readUInt(); | |||
size += LittleEndianConsts.INT_SIZE; | |||
assert ((sizeLast - HEADER_SIZE) == recordSize && recordSize == size); | |||
// some files store the whole file size in sizeLast, other just the last record size | |||
// assert (sizeLast == size+HEADER_SIZE); | |||
assert (recordSize == size); | |||
return size; | |||
} | |||
@@ -381,6 +389,7 @@ public class HemfMisc { | |||
// PS_COSMETIC, this value MUST be 0x00000001. | |||
long width = leis.readUInt(); | |||
dimension.setSize(width, 0); | |||
int size = 7 * LittleEndianConsts.INT_SIZE; | |||
// A 32-bit unsigned integer that specifies a brush style for the pen from the WMF BrushStyle enumeration | |||
// | |||
@@ -389,16 +398,17 @@ public class HemfMisc { | |||
// The BS_NULL style SHOULD be used to specify a brush that has no effect | |||
brushStyle = HwmfBrushStyle.valueOf((int) leis.readUInt()); | |||
int size = 8 * LittleEndianConsts.INT_SIZE; | |||
size += LittleEndianConsts.INT_SIZE; | |||
size += colorRef.init(leis); | |||
hatchStyle = HwmfHatchStyle.valueOf(leis.readInt()); | |||
size += LittleEndianConsts.INT_SIZE; | |||
// The number of elements in the array specified in the StyleEntry | |||
// field. This value SHOULD be zero if PenStyle does not specify PS_USERSTYLE. | |||
final int numStyleEntries = (int) leis.readUInt(); | |||
size += 2 * LittleEndianConsts.INT_SIZE; | |||
size += LittleEndianConsts.INT_SIZE; | |||
assert (numStyleEntries == 0 || penStyle.getLineDash() == HwmfLineDash.USERSTYLE); | |||
@@ -463,4 +473,42 @@ public class HemfMisc { | |||
return readPointL(leis, origin); | |||
} | |||
} | |||
public static class EmfSetWorldTransform implements HemfRecord { | |||
protected final AffineTransform xForm = new AffineTransform(); | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
return HemfRecordType.setWorldTransform; | |||
} | |||
@Override | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
return readXForm(leis, xForm); | |||
} | |||
} | |||
public static class EmfModifyWorldTransform implements HemfRecord { | |||
protected final AffineTransform xForm = new AffineTransform(); | |||
protected int modifyWorldTransformMode; | |||
@Override | |||
public HemfRecordType getEmfRecordType() { | |||
return HemfRecordType.modifyWorldTransform; | |||
} | |||
@Override | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
// An XForm object that defines a two-dimensional linear transform in logical units. | |||
// This transform is used according to the ModifyWorldTransformMode to define a new value for | |||
// the world-space to page-space transform in the playback device context. | |||
int size = readXForm(leis, xForm); | |||
// A 32-bit unsigned integer that specifies how the transform specified in Xform is used. | |||
// This value MUST be in the ModifyWorldTransformMode enumeration | |||
modifyWorldTransformMode = (int)leis.readUInt(); | |||
return size + LittleEndianConsts.INT_SIZE; | |||
} | |||
} | |||
} |
@@ -21,6 +21,7 @@ package org.apache.poi.hemf.record.emf; | |||
import java.io.IOException; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hwmf.record.HwmfRecord; | |||
import org.apache.poi.util.Internal; | |||
import org.apache.poi.util.LittleEndianInputStream; | |||
@@ -42,5 +43,9 @@ public interface HemfRecord { | |||
*/ | |||
long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException; | |||
default void draw(HemfGraphics ctx) {} | |||
default void draw(HemfGraphics ctx) { | |||
if (this instanceof HwmfRecord) { | |||
((HwmfRecord) this).draw(ctx); | |||
} | |||
} | |||
} |
@@ -58,8 +58,8 @@ public enum HemfRecordType { | |||
scaleWindowExtEx(0x00000020, HemfWindowing.EmfScaleWindowExtEx::new), | |||
saveDc(0x00000021, HemfMisc.EmfSaveDc::new), | |||
restoreDc(0x00000022, HemfMisc.EmfRestoreDc::new), | |||
setworldtransform(0x00000023, UnimplementedHemfRecord::new), | |||
modifyworldtransform(0x00000024, UnimplementedHemfRecord::new), | |||
setWorldTransform(0x00000023, HemfMisc.EmfSetWorldTransform::new), | |||
modifyWorldTransform(0x00000024, HemfMisc.EmfModifyWorldTransform::new), | |||
selectObject(0x00000025, HemfDraw.EmfSelectObject::new), | |||
createPen(0x00000026, HemfMisc.EmfCreatePen::new), | |||
createBrushIndirect(0x00000027, HemfMisc.EmfCreateBrushIndirect::new), |
@@ -18,6 +18,10 @@ | |||
package org.apache.poi.hemf.usermodel; | |||
import java.awt.Graphics2D; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.util.ArrayList; | |||
@@ -26,11 +30,14 @@ import java.util.List; | |||
import java.util.Spliterator; | |||
import java.util.function.Consumer; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.record.emf.HemfHeader; | |||
import org.apache.poi.hemf.record.emf.HemfRecord; | |||
import org.apache.poi.hemf.record.emf.HemfRecordIterator; | |||
import org.apache.poi.util.Dimension2DDouble; | |||
import org.apache.poi.util.Internal; | |||
import org.apache.poi.util.LittleEndianInputStream; | |||
import org.apache.poi.util.Units; | |||
/** | |||
* Read-only EMF extractor. Lots remain | |||
@@ -74,4 +81,42 @@ public class HemfPicture implements Iterable<HemfRecord> { | |||
public void forEach(Consumer<? super HemfRecord> action) { | |||
getRecords().forEach(action); | |||
} | |||
/** | |||
* Return the image size in points | |||
* | |||
* @return the image size in points | |||
*/ | |||
public Dimension2D getSize() { | |||
HemfHeader header = (HemfHeader)getRecords().get(0); | |||
Rectangle2D dim = header.getFrameRectangle(); | |||
double coeff = (double)Units.EMU_PER_CENTIMETER/Units.EMU_PER_POINT/10.; | |||
return new Dimension2DDouble(dim.getWidth()*coeff, dim.getHeight()*coeff); | |||
} | |||
public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { | |||
HemfHeader header = (HemfHeader)getRecords().get(0); | |||
AffineTransform at = ctx.getTransform(); | |||
try { | |||
Rectangle2D emfBounds = header.getBoundsRectangle(); | |||
ctx.translate(graphicsBounds.getCenterX()-emfBounds.getCenterX(), graphicsBounds.getCenterY()-emfBounds.getCenterY()); | |||
// scale output bounds to image bounds | |||
ctx.translate(emfBounds.getCenterX(), emfBounds.getCenterY()); | |||
ctx.scale(graphicsBounds.getWidth()/emfBounds.getWidth(), graphicsBounds.getHeight()/emfBounds.getHeight()); | |||
ctx.translate(-emfBounds.getCenterX(), -emfBounds.getCenterY()); | |||
HemfGraphics g = new HemfGraphics(ctx, emfBounds); | |||
for (HemfRecord r : getRecords()) { | |||
g.draw(r); | |||
} | |||
} finally { | |||
ctx.setTransform(at); | |||
} | |||
} | |||
} |
@@ -20,6 +20,7 @@ package org.apache.poi.hwmf.draw; | |||
import java.awt.Color; | |||
import java.awt.Shape; | |||
import java.awt.geom.Area; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
@@ -88,7 +89,7 @@ public class HwmfDrawProperties { | |||
} | |||
public HwmfDrawProperties(HwmfDrawProperties other) { | |||
this.window = (Rectangle2D)other.window.clone(); | |||
this.window = (other.window == null) ? null : (Rectangle2D)other.window.clone(); | |||
this.viewport = (other.viewport == null) ? null : (Rectangle2D)other.viewport.clone(); | |||
this.location = (Point2D)other.location.clone(); | |||
this.mapMode = other.mapMode; | |||
@@ -367,4 +368,11 @@ public class HwmfDrawProperties { | |||
public void setTextVAlignAsian(HwmfTextVerticalAlignment textVAlignAsian) { | |||
this.textVAlignAsian = textVAlignAsian; | |||
} | |||
/** | |||
* @return the current active winding rule ({@link Path2D#WIND_EVEN_ODD} or {@link Path2D#WIND_NON_ZERO}) | |||
*/ | |||
public int getWindingRule() { | |||
return getPolyfillMode().awtFlag; | |||
} | |||
} |
@@ -54,9 +54,9 @@ public class HwmfGraphics { | |||
protected final List<HwmfDrawProperties> propStack = new LinkedList<>(); | |||
protected HwmfDrawProperties prop; | |||
protected final Graphics2D graphicsCtx; | |||
private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; | |||
private final Graphics2D graphicsCtx; | |||
private final List<HwmfObjectTableEntry> objectTable = new ArrayList<>(); | |||
/** Bounding box from the placeable header */ | |||
private final Rectangle2D bbox; | |||
@@ -72,7 +72,6 @@ public class HwmfGraphics { | |||
this.graphicsCtx = graphicsCtx; | |||
this.bbox = (Rectangle2D)bbox.clone(); | |||
this.initialAT = graphicsCtx.getTransform(); | |||
DrawFactory.getInstance(graphicsCtx).fixFonts(graphicsCtx); | |||
} | |||
public HwmfDrawProperties getProperties() { | |||
@@ -178,6 +177,7 @@ public class HwmfGraphics { | |||
if (h == HwmfHatchStyle.HS_BDIAGONAL || h == HwmfHatchStyle.HS_DIAGCROSS) { | |||
g.drawLine(0, dim, dim, 0); | |||
} | |||
// TODO: handle new HS_* enumeration values | |||
g.dispose(); | |||
return new TexturePaint(bi, new Rectangle(0,0,dim,dim)); | |||
} | |||
@@ -248,9 +248,10 @@ public class HwmfGraphics { | |||
* Saves the current properties to the stack | |||
*/ | |||
public void saveProperties() { | |||
assert(prop != null); | |||
propStack.add(prop); | |||
prop = new HwmfDrawProperties(prop); | |||
final HwmfDrawProperties p = getProperties(); | |||
assert(p != null); | |||
propStack.add(p); | |||
prop = new HwmfDrawProperties(p); | |||
} | |||
/** |
@@ -31,20 +31,25 @@ import java.io.InputStream; | |||
import org.apache.poi.hwmf.usermodel.HwmfPicture; | |||
import org.apache.poi.sl.draw.DrawPictureShape; | |||
import org.apache.poi.sl.draw.ImageRenderer; | |||
import org.apache.poi.sl.usermodel.PictureData; | |||
import org.apache.poi.sl.usermodel.PictureData.PictureType; | |||
import org.apache.poi.util.Units; | |||
/** | |||
* Helper class which is instantiated by {@link DrawPictureShape} | |||
* via reflection | |||
*/ | |||
public class HwmfSLImageRenderer implements ImageRenderer { | |||
public class HwmfImageRenderer implements ImageRenderer { | |||
HwmfPicture image; | |||
double alpha; | |||
@Override | |||
public boolean canRender(String contentType) { | |||
return PictureType.WMF.contentType.equalsIgnoreCase(contentType); | |||
} | |||
@Override | |||
public void loadImage(InputStream data, String contentType) throws IOException { | |||
if (!PictureData.PictureType.WMF.contentType.equals(contentType)) { | |||
if (!PictureType.WMF.contentType.equals(contentType)) { | |||
throw new IOException("Invalid picture type"); | |||
} | |||
image = new HwmfPicture(data); | |||
@@ -52,7 +57,7 @@ public class HwmfSLImageRenderer implements ImageRenderer { | |||
@Override | |||
public void loadImage(byte[] data, String contentType) throws IOException { | |||
if (!PictureData.PictureType.WMF.contentType.equals(contentType)) { | |||
if (!PictureType.WMF.contentType.equals(contentType)) { | |||
throw new IOException("Invalid picture type"); | |||
} | |||
image = new HwmfPicture(new ByteArrayInputStream(data)); |
@@ -124,9 +124,9 @@ public class HwmfDraw { | |||
@Override | |||
public void draw(HwmfGraphics ctx) { | |||
Path2D p = getShape(ctx); | |||
Path2D p = (Path2D)poly.clone(); | |||
// don't close the path | |||
p.setWindingRule(getWindingRule(ctx)); | |||
p.setWindingRule(ctx.getProperties().getWindingRule()); | |||
if (isFill()) { | |||
ctx.fill(p); | |||
} else { | |||
@@ -134,10 +134,6 @@ public class HwmfDraw { | |||
} | |||
} | |||
protected Path2D getShape(HwmfGraphics ctx) { | |||
return (Path2D)poly.clone(); | |||
} | |||
/** | |||
* @return true, if the shape should be filled | |||
*/ | |||
@@ -303,11 +299,20 @@ public class HwmfDraw { | |||
@Override | |||
public void draw(HwmfGraphics ctx) { | |||
if (polyList.isEmpty()) { | |||
Area area = getShape(ctx); | |||
if (area == null) { | |||
return; | |||
} | |||
int windingRule = getWindingRule(ctx); | |||
if (isFill()) { | |||
ctx.fill(area); | |||
} else { | |||
ctx.draw(area); | |||
} | |||
} | |||
protected Area getShape(HwmfGraphics ctx) { | |||
int windingRule = ctx.getProperties().getWindingRule(); | |||
Area area = null; | |||
for (Path2D poly : polyList) { | |||
Path2D p = (Path2D)poly.clone(); | |||
@@ -320,14 +325,9 @@ public class HwmfDraw { | |||
} | |||
} | |||
if (isFill()) { | |||
ctx.fill(area); | |||
} else { | |||
ctx.draw(area); | |||
} | |||
return area; | |||
} | |||
/** | |||
* @return true, if the shape should be filled | |||
*/ | |||
@@ -459,6 +459,20 @@ public class HwmfDraw { | |||
@Override | |||
public void draw(HwmfGraphics ctx) { | |||
Shape s = getShape(); | |||
switch (getWmfRecordType()) { | |||
default: | |||
case arc: | |||
ctx.draw(s); | |||
break; | |||
case chord: | |||
case pie: | |||
ctx.fill(s); | |||
break; | |||
} | |||
} | |||
protected Arc2D getShape() { | |||
double startAngle = Math.toDegrees(Math.atan2(-(startPoint.getY() - bounds.getCenterY()), startPoint.getX() - bounds.getCenterX())); | |||
double endAngle = Math.toDegrees(Math.atan2(-(endPoint.getY() - bounds.getCenterY()), endPoint.getX() - bounds.getCenterX())); | |||
double arcAngle = (endAngle - startAngle) + (endAngle - startAngle > 0 ? 0 : 360); | |||
@@ -472,24 +486,16 @@ public class HwmfDraw { | |||
default: | |||
case arc: | |||
arcClosure = Arc2D.OPEN; | |||
fillShape = false; | |||
break; | |||
case chord: | |||
arcClosure = Arc2D.CHORD; | |||
fillShape = true; | |||
break; | |||
case pie: | |||
arcClosure = Arc2D.PIE; | |||
fillShape = true; | |||
break; | |||
} | |||
Shape s = new Arc2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), startAngle, arcAngle, arcClosure); | |||
if (fillShape) { | |||
ctx.fill(s); | |||
} else { | |||
ctx.draw(s); | |||
} | |||
return new Arc2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), startAngle, arcAngle, arcClosure); | |||
} | |||
} | |||
@@ -552,10 +558,6 @@ public class HwmfDraw { | |||
} | |||
} | |||
private static int getWindingRule(HwmfGraphics ctx) { | |||
return ctx.getProperties().getPolyfillMode().awtFlag; | |||
} | |||
static int readBounds(LittleEndianInputStream leis, Rectangle2D bounds) { | |||
/** | |||
* The 16-bit signed integers that defines the corners of the bounding rectangle. |
@@ -32,7 +32,20 @@ public enum HwmfHatchStyle { | |||
/** +++++ - A horizontal and vertical cross-hatch. */ | |||
HS_CROSS(0x0004), | |||
/** xxxxx - A 45-degree crosshatch. */ | |||
HS_DIAGCROSS(0x0005); | |||
HS_DIAGCROSS(0x0005), | |||
/** The hatch is not a pattern, but is a solid color. */ | |||
HS_SOLIDCLR(0x0006), | |||
/** The hatch is not a pattern, but is a dithered color. */ | |||
HS_DITHEREDCLR(0x0007), | |||
/** The hatch is not a pattern, but is a solid color, defined by the current text (foreground) color. */ | |||
HS_SOLIDTEXTCLR(0x0008), | |||
/** The hatch is not a pattern, but is a dithered color, defined by the current text (foreground) color. */ | |||
HS_DITHEREDTEXTCLR(0x0009), | |||
/** The hatch is not a pattern, but is a solid color, defined by the current background color. */ | |||
HS_SOLIDBKCLR(0x000A), | |||
/** The hatch is not a pattern, but is a dithered color, defined by the current background color. */ | |||
HS_DITHEREDBKCLR(0x000B) | |||
; | |||
int flag; | |||
HwmfHatchStyle(int flag) { |
@@ -22,14 +22,24 @@ import static org.apache.poi.POITestCase.assertContains; | |||
import static org.junit.Assert.assertEquals; | |||
import static org.junit.Assert.assertTrue; | |||
import java.awt.Graphics2D; | |||
import java.awt.RenderingHints; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.File; | |||
import java.io.FileInputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Set; | |||
import javax.imageio.ImageIO; | |||
import org.apache.poi.POIDataSamples; | |||
import org.apache.poi.hemf.record.emf.HemfComment; | |||
import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; | |||
@@ -41,22 +51,60 @@ import org.apache.poi.hemf.record.emf.HemfRecordType; | |||
import org.apache.poi.hemf.record.emf.HemfText; | |||
import org.apache.poi.util.IOUtils; | |||
import org.apache.poi.util.RecordFormatException; | |||
import org.apache.poi.util.Units; | |||
import org.junit.Ignore; | |||
import org.junit.Test; | |||
public class HemfPictureTest { | |||
private POIDataSamples samples = POIDataSamples.getSpreadSheetInstance(); | |||
private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance(); | |||
private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance(); | |||
@Test | |||
@Ignore("Only for manual tests") | |||
public void paint() throws IOException { | |||
File f = sl_samples.getFile("wrench.emf"); | |||
try (FileInputStream fis = new FileInputStream(f)) { | |||
HemfPicture emf = new HemfPicture(fis); | |||
Dimension2D dim = emf.getSize(); | |||
int width = Units.pointsToPixel(dim.getWidth()); | |||
// keep aspect ratio for height | |||
int height = Units.pointsToPixel(dim.getHeight()); | |||
double max = Math.max(width, height); | |||
if (max > 1500) { | |||
width *= 1500 / max; | |||
height *= 1500 / max; | |||
} | |||
BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D g = bufImg.createGraphics(); | |||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); | |||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); | |||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); | |||
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); | |||
emf.draw(g, new Rectangle2D.Double(0,0,width,height)); | |||
g.dispose(); | |||
ImageIO.write(bufImg, "PNG", new File("bla.png")); | |||
} | |||
} | |||
@Test | |||
public void testBasicWindows() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("SimpleEMF_windows.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) { | |||
HemfPicture pic = new HemfPicture(is); | |||
HemfHeader header = pic.getHeader(); | |||
assertEquals(27864, header.getBytes()); | |||
assertEquals(31, header.getRecords()); | |||
assertEquals(3, header.getHandles()); | |||
assertEquals(346000, header.getMicrometersX()); | |||
assertEquals(194000, header.getMicrometersY()); | |||
assertEquals(346000, header.getMicroDimension().getWidth()); | |||
assertEquals(194000, header.getMicroDimension().getHeight()); | |||
List<HemfRecord> records = pic.getRecords(); | |||
@@ -66,7 +114,7 @@ public class HemfPictureTest { | |||
@Test | |||
public void testBasicMac() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("SimpleEMF_mac.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_mac.emf")) { | |||
HemfPicture pic = new HemfPicture(is); | |||
HemfHeader header = pic.getHeader(); | |||
@@ -102,7 +150,7 @@ public class HemfPictureTest { | |||
@Test | |||
public void testMacText() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("SimpleEMF_mac.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_mac.emf")) { | |||
HemfPicture pic = new HemfPicture(is); | |||
double lastY = -1; | |||
@@ -134,7 +182,7 @@ public class HemfPictureTest { | |||
@Test | |||
public void testWindowsText() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("SimpleEMF_windows.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) { | |||
HemfPicture pic = new HemfPicture(is); | |||
double lastY = -1; | |||
double lastX = -1; | |||
@@ -173,7 +221,7 @@ public class HemfPictureTest { | |||
@Test(expected = RecordFormatException.class) | |||
public void testInfiniteLoopOnFile() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("61294.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("61294.emf")) { | |||
HemfPicture pic = new HemfPicture(is); | |||
for (HemfRecord record : pic) { | |||
@@ -183,7 +231,7 @@ public class HemfPictureTest { | |||
@Test(expected = RecordFormatException.class) | |||
public void testInfiniteLoopOnByteArray() throws Exception { | |||
try (InputStream is = samples.openResourceAsStream("61294.emf")) { | |||
try (InputStream is = ss_samples.openResourceAsStream("61294.emf")) { | |||
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |||
IOUtils.copy(is, bos); | |||
is.close(); |
@@ -210,7 +210,6 @@ public final class TestPicture { | |||
} else { | |||
BufferedImage img = new BufferedImage(pg.width, pg.height, BufferedImage.TYPE_INT_ARGB); | |||
Graphics2D graphics = img.createGraphics(); | |||
DrawFactory.getInstance(graphics).fixFonts(graphics); | |||
slide.draw(graphics); | |||
graphics.setColor(Color.BLACK); | |||
graphics.setStroke(new BasicStroke(1)); |