| /* ==================================================================== |
| 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 static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; |
| import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString; |
| |
| import java.awt.Shape; |
| import java.awt.geom.Arc2D; |
| import java.awt.geom.Dimension2D; |
| import java.awt.geom.Path2D; |
| import java.awt.geom.PathIterator; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.io.IOException; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| import java.util.stream.IntStream; |
| |
| import org.apache.poi.hemf.draw.HemfDrawProperties; |
| import org.apache.poi.hemf.draw.HemfGraphics; |
| import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle; |
| import org.apache.poi.hwmf.record.HwmfDraw; |
| import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject; |
| import org.apache.poi.util.GenericRecordJsonWriter; |
| import org.apache.poi.util.GenericRecordUtil; |
| import org.apache.poi.util.LittleEndianConsts; |
| import org.apache.poi.util.LittleEndianInputStream; |
| |
| public final class HemfDraw { |
| private HemfDraw() {} |
| |
| /** |
| * The EMR_SELECTOBJECT record adds a graphics object to the current metafile playback device |
| * context. The object is specified either by its index in the EMF Object Table or by its |
| * value from the StockObject enumeration. |
| */ |
| public static class EmfSelectObject extends WmfSelectObject implements HemfRecord { |
| |
| private static final int[] IDX_MASKS = IntStream.rangeClosed(0x80000000,0x80000013).toArray(); |
| |
| private static final String[] IDX_NAMES = { |
| "WHITE_BRUSH", |
| "LTGRAY_BRUSH", |
| "GRAY_BRUSH", |
| "DKGRAY_BRUSH", |
| "BLACK_BRUSH", |
| "NULL_BRUSH", |
| "WHITE_PEN", |
| "BLACK_PEN", |
| "NULL_PEN", |
| // 0x80000009 is not a valid stock object |
| "INVALID", |
| "OEM_FIXED_FONT", |
| "ANSI_FIXED_FONT", |
| "ANSI_VAR_FONT", |
| "SYSTEM_FONT", |
| "DEVICE_DEFAULT_FONT", |
| "DEFAULT_PALETTE", |
| "SYSTEM_FIXED_FONT", |
| "DEFAULT_GUI_FONT", |
| "DC_BRUSH", |
| "DC_PEN" |
| }; |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.selectObject; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| // A 32-bit unsigned integer that specifies either the index of a graphics object in the |
| // EMF Object Table or the index of a stock object from the StockObject enumeration. |
| objectIndex = leis.readInt(); |
| return LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "objectIndex", getEnumBitsAsString(this::getObjectIndex, IDX_MASKS, IDX_NAMES) |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| |
| /** The EMR_POLYBEZIER record specifies one or more Bezier curves. */ |
| public static class EmfPolyBezier extends HwmfDraw.WmfPolygon implements HemfRecord { |
| private final Rectangle2D bounds = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyBezier; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointL(leis, point); |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| |
| /* A 32-bit unsigned integer that specifies the number of points in the points |
| * array. This value MUST be one more than three times the number of curves to |
| * be drawn, because each Bezier curve requires two control points and an |
| * endpoint, and the initial curve requires an additional starting point. |
| * |
| * Line width | Device supports wideline | Maximum points allowed |
| * 1 | n/a | 16K |
| * > 1 | yes | 16K |
| * > 1 | no | 1360 |
| * |
| * Any extra points MUST be ignored. |
| */ |
| final int count = (int)leis.readUInt(); |
| final int points = Math.min(count, 16384); |
| size += LittleEndianConsts.INT_SIZE; |
| |
| poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, points+2); |
| |
| /* Cubic Bezier curves are defined using the endpoints and control points |
| * specified by the points field. The first curve is drawn from the first |
| * point to the fourth point, using the second and third points as control |
| * points. Each subsequent curve in the sequence needs exactly three more points: |
| * the ending point of the previous curve is used as the starting point, |
| * the next two points in the sequence are control points, |
| * and the third is the ending point. |
| * The cubic Bezier curves SHOULD be drawn using the current pen. |
| */ |
| |
| Point2D[] pnt = {new Point2D.Double(), new Point2D.Double(), new Point2D.Double()}; |
| |
| int i=0; |
| if (hasStartPoint()) { |
| if (i < points) { |
| size += readPoint(leis, pnt[0]); |
| poly.moveTo(pnt[0].getX(), pnt[0].getY()); |
| i++; |
| } |
| } else { |
| poly.moveTo(0, 0); |
| } |
| |
| for (; i+2<points; i+=3) { |
| size += readPoint(leis, pnt[0]); |
| size += readPoint(leis, pnt[1]); |
| size += readPoint(leis, pnt[2]); |
| |
| poly.curveTo( |
| pnt[0].getX(),pnt[0].getY(), |
| pnt[1].getX(),pnt[1].getY(), |
| pnt[2].getX(),pnt[2].getY() |
| ); |
| } |
| |
| return size; |
| } |
| |
| /** |
| * @return true, if start point is in the list of points. false, if start point is taken from the context |
| */ |
| protected boolean hasStartPoint() { |
| return true; |
| } |
| |
| @Override |
| protected FillDrawStyle getFillDrawStyle() { |
| // The cubic Bezier curves SHOULD be drawn using the current pen. |
| return FillDrawStyle.DRAW; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(poly, !hasStartPoint()), getFillDrawStyle()); |
| } |
| |
| public Rectangle2D getBounds() { |
| return bounds; |
| } |
| |
| @Override |
| protected boolean addClose() { |
| return false; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "bounds", this::getBounds |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_POLYBEZIER16 record specifies one or more Bezier curves. |
| * The curves are drawn using the current pen. |
| */ |
| public static class EmfPolyBezier16 extends EmfPolyBezier { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyBezier16; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| |
| /** |
| * 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 { |
| private final Rectangle2D bounds = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polygon; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointL(leis, point); |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| |
| // see PolyBezier about limits |
| final int count = (int)leis.readUInt(); |
| final int points = Math.min(count, 16384); |
| size += LittleEndianConsts.INT_SIZE; |
| |
| poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, points); |
| |
| Point2D pnt = new Point2D.Double(); |
| for (int i=0; i<points; i++) { |
| size += readPoint(leis, pnt); |
| if (i==0) { |
| if (hasStartPoint()) { |
| 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); |
| poly.lineTo(pnt.getX(), pnt.getY()); |
| } |
| } else { |
| poly.lineTo(pnt.getX(), pnt.getY()); |
| } |
| } |
| |
| return size; |
| } |
| |
| /** |
| * @return true, if start point is in the list of points. false, if start point is taken from the context |
| */ |
| protected boolean hasStartPoint() { |
| return true; |
| } |
| |
| @Override |
| protected FillDrawStyle getFillDrawStyle() { |
| // The polygon SHOULD be outlined using the current pen and filled using the current brush and |
| // polygon fill mode. The polygon SHOULD be closed automatically by drawing a line from the last |
| // vertex to the first. |
| return FillDrawStyle.FILL_DRAW; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(poly, false), getFillDrawStyle()); |
| } |
| |
| public Rectangle2D getBounds() { |
| return bounds; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "bounds", this::getBounds |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_POLYGON16 record specifies a polygon consisting of two or more vertexes connected by straight lines. |
| * The polygon is outlined by using the current pen and filled by using the current brush and polygon fill mode. |
| * The polygon is closed automatically by drawing a line from the last vertex to the first |
| */ |
| public static class EmfPolygon16 extends EmfPolygon { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polygon16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * The EMR_POLYLINE record specifies a series of line segments by connecting the points in the |
| * specified array. |
| */ |
| public static class EmfPolyline extends EmfPolygon { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyline; |
| } |
| |
| @Override |
| protected FillDrawStyle getFillDrawStyle() { |
| // The line segments SHOULD be drawn using the current pen. |
| return FillDrawStyle.DRAW; |
| } |
| |
| @Override |
| protected boolean addClose() { |
| return false; |
| } |
| } |
| |
| /** |
| * The EMR_POLYLINE16 record specifies a series of line segments by connecting the points in the |
| * specified array. |
| */ |
| public static class EmfPolyline16 extends EmfPolyline { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyline16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * The EMR_POLYBEZIERTO record specifies one or more Bezier curves based upon the current |
| * position. |
| */ |
| public static class EmfPolyBezierTo extends EmfPolyBezier { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyBezierTo; |
| } |
| |
| @Override |
| protected boolean hasStartPoint() { |
| return false; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| polyTo(ctx, poly, getFillDrawStyle()); |
| } |
| } |
| |
| /** |
| * The EMR_POLYBEZIERTO16 record specifies one or more Bezier curves based on the current |
| * position. |
| */ |
| public static class EmfPolyBezierTo16 extends EmfPolyBezierTo { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyBezierTo16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** The EMR_POLYLINETO record specifies one or more straight lines based upon the current position. */ |
| public static class EmfPolylineTo extends EmfPolyline { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polylineTo; |
| } |
| |
| @Override |
| protected boolean hasStartPoint() { |
| return false; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| polyTo(ctx, poly, getFillDrawStyle()); |
| } |
| } |
| |
| /** |
| * The EMR_POLYLINETO16 record specifies one or more straight lines based upon the current position. |
| * A line is drawn from the current position to the first point specified by the points field by using the |
| * current pen. For each additional line, drawing is performed from the ending point of the previous |
| * line to the next point specified by points. |
| */ |
| public static class EmfPolylineTo16 extends EmfPolylineTo { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polylineTo16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * The EMR_POLYPOLYGON record specifies a series of closed polygons. |
| */ |
| public static class EmfPolyPolygon extends HwmfDraw.WmfPolyPolygon implements HemfRecord { |
| private final Rectangle2D bounds = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyPolygon; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointL(leis, point); |
| } |
| |
| @SuppressWarnings("unused") |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| |
| // A 32-bit unsigned integer that specifies the number of polygons. |
| long numberOfPolygons = leis.readUInt(); |
| // A 32-bit unsigned integer that specifies the total number of points in all polygons. |
| long count = Math.min(16384, leis.readUInt()); |
| |
| size += 2 * LittleEndianConsts.INT_SIZE; |
| |
| // An array of 32-bit unsigned integers that specifies the point count for each polygon. |
| long[] polygonPointCount = new long[(int)numberOfPolygons]; |
| |
| size += numberOfPolygons * LittleEndianConsts.INT_SIZE; |
| |
| for (int i=0; i<numberOfPolygons; i++) { |
| polygonPointCount[i] = leis.readUInt(); |
| } |
| |
| Point2D pnt = new Point2D.Double(); |
| for (long nPoints : polygonPointCount) { |
| // An array of WMF PointL objects that specifies the points for all polygons in logical units. |
| // The number of points is specified by the Count field value. |
| Path2D poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, (int)nPoints); |
| for (int i=0; i<nPoints; i++) { |
| size += readPoint(leis, pnt); |
| if (i == 0) { |
| poly.moveTo(pnt.getX(), pnt.getY()); |
| } else { |
| poly.lineTo(pnt.getX(), pnt.getY()); |
| } |
| } |
| if (isClosed()) { |
| poly.closePath(); |
| } |
| polyList.add(poly); |
| } |
| return size; |
| } |
| |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| Shape shape = getShape(ctx); |
| if (shape == null) { |
| return; |
| } |
| |
| ctx.draw(path -> path.append(shape, false), getFillDrawStyle()); |
| } |
| |
| public Rectangle2D getBounds() { |
| return bounds; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "bounds", this::getBounds |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_POLYPOLYGON16 record specifies a series of closed polygons. Each polygon is outlined |
| * using the current pen, and filled using the current brush and polygon fill mode. |
| * The polygons drawn by this record can overlap. |
| */ |
| public static class EmfPolyPolygon16 extends EmfPolyPolygon { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyPolygon16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * The EMR_POLYPOLYLINE record specifies multiple series of connected line segments. |
| */ |
| public static class EmfPolyPolyline extends EmfPolyPolygon { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyPolyline; |
| } |
| |
| @Override |
| protected boolean isClosed() { |
| return false; |
| } |
| |
| @Override |
| protected FillDrawStyle getFillDrawStyle() { |
| return FillDrawStyle.DRAW; |
| } |
| } |
| |
| /** The EMR_POLYPOLYLINE16 record specifies multiple series of connected line segments. */ |
| public static class EmfPolyPolyline16 extends EmfPolyPolyline { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyPolyline16; |
| } |
| |
| @Override |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * The EMR_SETPIXELV record defines the color of the pixel at the specified logical coordinates. |
| */ |
| public static class EmfSetPixelV extends HwmfDraw.WmfSetPixel implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.setPixelV; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readPointL(leis, point); |
| size += colorRef.init(leis); |
| return size; |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_MOVETOEX record specifies coordinates of the new current position, in logical units. |
| */ |
| public static class EmfSetMoveToEx extends HwmfDraw.WmfMoveTo implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.setMoveToEx; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return readPointL(leis, point); |
| } |
| |
| @Override |
| public void draw(final HemfGraphics ctx) { |
| ctx.draw((path) -> path.moveTo(point.getX(), point.getY()), FillDrawStyle.NONE); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_ARC record specifies an elliptical arc. |
| * It resets the current position to the end point of the arc. |
| */ |
| public static class EmfArc extends HwmfDraw.WmfArc implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.arc; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| size += readPointL(leis, startPoint); |
| size += readPointL(leis, endPoint); |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_CHORD record specifies a chord, which is a region bounded by the intersection of an |
| * ellipse and a line segment, called a secant. The chord is outlined by using the current pen |
| * and filled by using the current brush. |
| */ |
| public static class EmfChord extends HwmfDraw.WmfChord implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.chord; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| size += readPointL(leis, startPoint); |
| size += readPointL(leis, endPoint); |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_PIE record specifies a pie-shaped wedge bounded by the intersection of an ellipse and two |
| * radials. The pie is outlined by using the current pen and filled by using the current brush. |
| */ |
| public static class EmfPie extends HwmfDraw.WmfPie implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.pie; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| size += readPointL(leis, startPoint); |
| size += readPointL(leis, endPoint); |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_ELLIPSE record specifies an ellipse. The center of the ellipse is the center of the specified |
| * bounding rectangle. The ellipse is outlined by using the current pen and is filled by using the current |
| * brush. |
| */ |
| public static class EmfEllipse extends HwmfDraw.WmfEllipse implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.ellipse; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return readRectL(leis, bounds); |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(getShape(), false), FillDrawStyle.FILL_DRAW); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_RECTANGLE record draws a rectangle. The rectangle is outlined by using the current pen |
| * and filled by using the current brush. |
| */ |
| public static class EmfRectangle extends HwmfDraw.WmfRectangle implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.rectangle; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return readRectL(leis, bounds); |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(normalizeBounds(bounds), false), FillDrawStyle.FILL_DRAW); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_ROUNDRECT record specifies a rectangle with rounded corners. The rectangle is outlined |
| * by using the current pen and filled by using the current brush. |
| */ |
| public static class EmfRoundRect extends HwmfDraw.WmfRoundRect implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.roundRect; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| |
| // A 32-bit unsigned integer that defines the x-coordinate of the point. |
| int width = (int)leis.readUInt(); |
| int height = (int)leis.readUInt(); |
| corners.setSize(width, height); |
| |
| return size + 2*LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(getShape(), false), FillDrawStyle.FILL_DRAW); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_LINETO record specifies a line from the current position up to, but not including, the |
| * specified point. It resets the current position to the specified point. |
| */ |
| public static class EmfLineTo extends HwmfDraw.WmfLineTo implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.lineTo; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return readPointL(leis, point); |
| } |
| |
| @Override |
| public void draw(final HemfGraphics ctx) { |
| ctx.draw((path) -> path.lineTo(point.getX(), point.getY()), FillDrawStyle.DRAW); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_ARCTO record specifies an elliptical arc. |
| * It resets the current position to the end point of the arc. |
| */ |
| public static class EmfArcTo extends HwmfDraw.WmfArc implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.arcTo; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| size += readPointL(leis, startPoint); |
| size += readPointL(leis, endPoint); |
| return size; |
| } |
| |
| @Override |
| public void draw(final HemfGraphics ctx) { |
| final Arc2D arc = getShape(); |
| ctx.draw((path) -> path.append(arc, true), getFillDrawStyle()); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** The EMR_POLYDRAW record specifies a set of line segments and Bezier curves. */ |
| public static class EmfPolyDraw extends HwmfDraw.WmfPolygon implements HemfRecord { |
| private final Rectangle2D bounds = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyDraw; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointL(leis, point); |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| long size = readRectL(leis, bounds); |
| int count = (int)leis.readUInt(); |
| size += LittleEndianConsts.INT_SIZE; |
| Point2D[] points = new Point2D[count]; |
| for (int i=0; i<count; i++) { |
| points[i] = new Point2D.Double(); |
| size += readPoint(leis, points[i]); |
| } |
| |
| poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, count); |
| |
| for (int i=0; i<count; i++) { |
| int mode = leis.readUByte(); |
| switch (mode & 0x06) { |
| // PT_LINETO |
| // Specifies that a line is to be drawn from the current position to this point, which |
| // then becomes the new current position. |
| case 0x02: |
| poly.lineTo(points[i].getX(), points[i].getY()); |
| break; |
| // PT_BEZIERTO |
| // Specifies that this point is a control point or ending point for a Bezier curve. |
| // PT_BEZIERTO types always occur in sets of three. |
| // The current position defines the starting point for the Bezier curve. |
| // The first two PT_BEZIERTO points are the control points, |
| // and the third PT_BEZIERTO point is the ending point. |
| // The ending point becomes the new current position. |
| // If there are not three consecutive PT_BEZIERTO points, an error results. |
| case 0x04: |
| int mode2 = leis.readUByte(); |
| int mode3 = leis.readUByte(); |
| assert(mode2 == 0x04 && (mode3 == 0x04 || mode3 == 0x05)); |
| if ((i + 2) >= points.length) { |
| throw new IllegalStateException("Points index causes index out of bounds"); |
| } |
| poly.curveTo( |
| points[i].getX(), points[i].getY(), |
| points[i+1].getX(), points[i+1].getY(), |
| points[i+2].getX(), points[i+2].getY() |
| ); |
| // update mode for closePath handling below |
| mode = mode3; |
| i+=2; |
| break; |
| // PT_MOVETO |
| // Specifies that this point starts a disjoint figure. This point becomes the new current position. |
| case 0x06: |
| poly.moveTo(points[i].getX(), points[i].getY()); |
| break; |
| default: |
| // TODO: log error |
| break; |
| } |
| |
| // PT_CLOSEFIGURE |
| // A PT_LINETO or PT_BEZIERTO type can be combined with this value by using the bitwise operator OR |
| // to indicate that the corresponding point is the last point in a figure and the figure is closed. |
| // The current position is set to the ending point of the closing line. |
| if ((mode & 0x01) == 0x01) { |
| this.poly.closePath(); |
| } |
| } |
| size += count; |
| return size; |
| } |
| |
| @Override |
| protected FillDrawStyle getFillDrawStyle() { |
| // Draws a set of line segments and Bezier curves. |
| return FillDrawStyle.DRAW; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.draw(path -> path.append(poly, false), getFillDrawStyle()); |
| } |
| |
| public Rectangle2D getBounds() { |
| return bounds; |
| } |
| |
| @Override |
| protected boolean addClose() { |
| return false; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "bounds", this::getBounds |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| public static class EmfPolyDraw16 extends EmfPolyDraw { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.polyDraw16; |
| } |
| |
| protected long readPoint(LittleEndianInputStream leis, Point2D point) { |
| return readPointS(leis, point); |
| } |
| } |
| |
| /** |
| * 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 HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.beginPath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return 0; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| final HemfDrawProperties prop = ctx.getProperties(); |
| prop.setPath(new Path2D.Double()); |
| prop.setUsePathBracket(true); |
| } |
| |
| @Override |
| public String toString() { |
| return "{}"; |
| } |
| } |
| |
| /** |
| * This record closes a path bracket and selects the path defined by the bracket into |
| * the playback device context. |
| */ |
| public static class EmfEndPath implements HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.endPath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return 0; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| final HemfDrawProperties prop = ctx.getProperties(); |
| prop.setUsePathBracket(false); |
| } |
| |
| @Override |
| public String toString() { |
| return "{}"; |
| } |
| } |
| |
| /** |
| * This record aborts a path bracket or discards the path from a closed path bracket. |
| */ |
| public static class EmfAbortPath implements HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.abortPath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return 0; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| final HemfDrawProperties prop = ctx.getProperties(); |
| prop.setPath(null); |
| prop.setUsePathBracket(false); |
| } |
| |
| @Override |
| public String toString() { |
| return "{}"; |
| } |
| } |
| |
| /** |
| * 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 HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.closeFigure; |
| } |
| |
| @Override |
| 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(); |
| if (path != null && path.getCurrentPoint() != null) { |
| path.closePath(); |
| prop.setLocation(path.getCurrentPoint()); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "{}"; |
| } |
| } |
| |
| /** |
| * 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 HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.flattenPath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return 0; |
| } |
| } |
| |
| /** |
| * 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 HemfRecordWithoutProperties { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.widenPath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return 0; |
| } |
| |
| @Override |
| public String toString() { |
| return "{}"; |
| } |
| } |
| |
| /** |
| * The EMR_STROKEPATH record renders the specified path by using the current pen. |
| */ |
| public static class EmfStrokePath implements HemfRecord { |
| protected final Rectangle2D bounds = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.strokePath; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| // A 128-bit WMF RectL object, which specifies bounding rectangle, in device units |
| return (recordSize == 0) ? 0 : readRectL(leis, bounds); |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| HemfDrawProperties props = ctx.getProperties(); |
| Path2D path = props.getPath(); |
| path.setWindingRule(ctx.getProperties().getWindingRule()); |
| ctx.draw(path); |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| public Rectangle2D getBounds() { |
| return bounds; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties("bounds", this::getBounds); |
| } |
| } |
| |
| |
| /** |
| * 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 extends EmfStrokePath { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.fillPath; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| final HemfDrawProperties prop = ctx.getProperties(); |
| final Path2D origPath = prop.getPath(); |
| if (origPath.getCurrentPoint() == null) { |
| return; |
| } |
| final Path2D path = (Path2D)origPath.clone(); |
| path.closePath(); |
| path.setWindingRule(ctx.getProperties().getWindingRule()); |
| ctx.fill(path); |
| } |
| } |
| |
| /** |
| * The EMR_STROKEANDFILLPATH record closes any open figures in a path, strokes the outline of the |
| * path by using the current pen, and fills its interior by using the current brush. |
| */ |
| public static class EmfStrokeAndFillPath extends EmfStrokePath { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.strokeAndFillPath; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| HemfDrawProperties props = ctx.getProperties(); |
| Path2D path = props.getPath(); |
| path.closePath(); |
| path.setWindingRule(ctx.getProperties().getWindingRule()); |
| ctx.fill(path); |
| ctx.draw(path); |
| } |
| } |
| |
| static long readRectL(LittleEndianInputStream leis, Rectangle2D bounds) { |
| /* A 32-bit signed integer that defines the x coordinate, in logical coordinates, |
| * of the ... corner of the rectangle. |
| */ |
| final double left = leis.readInt(); |
| final double top = leis.readInt(); |
| final double right = leis.readInt(); |
| final double bottom = leis.readInt(); |
| bounds.setRect(left, top, right-left, bottom-top); |
| |
| return 4L * LittleEndianConsts.INT_SIZE; |
| } |
| |
| static long readPointS(LittleEndianInputStream leis, Point2D point) { |
| // x (2 bytes): A 16-bit signed integer that defines the horizontal (x) coordinate of the point. |
| final int x = leis.readShort(); |
| // y (2 bytes): A 16-bit signed integer that defines the vertical (y) coordinate of the point. |
| final int y = leis.readShort(); |
| point.setLocation(x, y); |
| |
| return 2L*LittleEndianConsts.SHORT_SIZE; |
| |
| } |
| static long readPointL(LittleEndianInputStream leis, Point2D point) { |
| // x (4 bytes): A 32-bit signed integer that defines the horizontal (x) coordinate of the point. |
| final int x = leis.readInt(); |
| // y (4 bytes): A 32-bit signed integer that defines the vertical (y) coordinate of the point. |
| final int y = leis.readInt(); |
| point.setLocation(x, y); |
| |
| return 2L*LittleEndianConsts.INT_SIZE; |
| |
| } |
| |
| static long readDimensionFloat(LittleEndianInputStream leis, Dimension2D dimension) { |
| final double width = leis.readFloat(); |
| final double height = leis.readFloat(); |
| dimension.setSize(width, height); |
| return 2L*LittleEndianConsts.INT_SIZE; |
| } |
| |
| static long readDimensionInt(LittleEndianInputStream leis, Dimension2D dimension) { |
| // although the spec says "use unsigned ints", there are examples out there using signed ints |
| final double width = leis.readInt(); |
| final double height = leis.readInt(); |
| dimension.setSize(width, height); |
| return 2L*LittleEndianConsts.INT_SIZE; |
| } |
| |
| private static void polyTo(final HemfGraphics ctx, final Path2D poly, FillDrawStyle fillDrawStyle) { |
| if (poly.getCurrentPoint() == null) { |
| return; |
| } |
| |
| final PathIterator pi = poly.getPathIterator(null); |
| // ignore empty polys and dummy start point (moveTo) |
| pi.next(); |
| if (pi.isDone()) { |
| return; |
| } |
| |
| ctx.draw((path) -> path.append(pi, true), fillDrawStyle); |
| } |
| } |