| /* ==================================================================== |
| 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.emfplus; |
| |
| import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; |
| |
| import java.awt.Color; |
| 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.IOException; |
| import java.math.BigDecimal; |
| import java.math.RoundingMode; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.PrimitiveIterator.OfInt; |
| import java.util.function.BiFunction; |
| import java.util.function.Supplier; |
| |
| import org.apache.commons.math3.linear.LUDecomposition; |
| import org.apache.commons.math3.linear.MatrixUtils; |
| import org.apache.commons.math3.linear.RealMatrix; |
| import org.apache.poi.hemf.draw.HemfDrawProperties; |
| import org.apache.poi.hemf.draw.HemfGraphics; |
| import org.apache.poi.hemf.record.emf.HemfFill; |
| import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; |
| import org.apache.poi.hwmf.record.HwmfBrushStyle; |
| import org.apache.poi.hwmf.record.HwmfColorRef; |
| import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; |
| import org.apache.poi.hwmf.record.HwmfPenStyle; |
| import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; |
| import org.apache.poi.hwmf.record.HwmfText; |
| import org.apache.poi.sl.draw.ImageRenderer; |
| import org.apache.poi.util.BitField; |
| import org.apache.poi.util.BitFieldFactory; |
| import org.apache.poi.util.GenericRecordJsonWriter; |
| import org.apache.poi.util.GenericRecordUtil; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.LittleEndianConsts; |
| import org.apache.poi.util.LittleEndianInputStream; |
| import org.apache.poi.util.StringUtil; |
| |
| @SuppressWarnings("WeakerAccess") |
| public final class HemfPlusDraw { |
| private static final int MAX_OBJECT_SIZE = 1_000_000; |
| |
| private HemfPlusDraw() {} |
| |
| public enum EmfPlusUnitType { |
| /** Specifies a unit of logical distance within the world space. */ |
| World(0x00), |
| /** Specifies a unit of distance based on the characteristics of the physical display. */ |
| Display(0x01), |
| /** Specifies a unit of 1 pixel. */ |
| Pixel(0x02), |
| /** Specifies a unit of 1 printer's point, or 1/72 inch. */ |
| Point(0x03), |
| /** Specifies a unit of 1 inch. */ |
| Inch(0x04), |
| /** Specifies a unit of 1/300 inch. */ |
| Document(0x05), |
| /** Specifies a unit of 1 millimeter. */ |
| Millimeter(0x06) |
| ; |
| |
| public final int id; |
| |
| EmfPlusUnitType(int id) { |
| this.id = id; |
| } |
| |
| public static EmfPlusUnitType valueOf(int id) { |
| for (EmfPlusUnitType wrt : values()) { |
| if (wrt.id == id) return wrt; |
| } |
| return null; |
| } |
| |
| } |
| |
| public interface EmfPlusCompressed { |
| /** |
| * This bit indicates whether the data in the RectData field is compressed. |
| * If set, RectData contains an EmfPlusRect object. |
| * If clear, RectData contains an EmfPlusRectF object object. |
| */ |
| BitField COMPRESSED = BitFieldFactory.getInstance(0x4000); |
| |
| int getFlags(); |
| |
| /** |
| * The index in the EMF+ Object Table to associate with the object |
| * created by this record. The value MUST be zero to 63, inclusive. |
| */ |
| default boolean isCompressed() { |
| return COMPRESSED.isSet(getFlags()); |
| } |
| |
| default BiFunction<LittleEndianInputStream, Rectangle2D, Integer> getReadRect() { |
| return isCompressed() ? HemfPlusDraw::readRectS : HemfPlusDraw::readRectF; |
| } |
| } |
| |
| public interface EmfPlusRelativePosition { |
| /** |
| * This bit indicates whether the PointData field specifies relative or absolute locations. |
| * If set, each element in PointData specifies a location in the coordinate space that is relative to the |
| * location specified by the previous element in the array. In the case of the first element in PointData, |
| * a previous location at coordinates (0,0) is assumed. |
| * If clear, PointData specifies absolute locations according to the {@link EmfPlusCompressed#isCompressed()} flag. |
| * |
| * Note If this flag is set, the {@link EmfPlusCompressed#isCompressed()} flag (above) is undefined and MUST be ignored. |
| */ |
| BitField POSITION = BitFieldFactory.getInstance(0x0800); |
| |
| int getFlags(); |
| |
| default boolean isRelativePosition() { |
| return POSITION.isSet(getFlags()); |
| } |
| } |
| |
| public interface EmfPlusSolidColor { |
| /** |
| * If set, brushId specifies a color as an EmfPlusARGB object. |
| * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. |
| */ |
| BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000); |
| |
| int getFlags(); |
| |
| int getBrushIdValue(); |
| |
| default boolean isSolidColor() { |
| return SOLID_COLOR.isSet(getFlags()); |
| } |
| |
| default int getBrushId() { |
| return (isSolidColor()) ? -1 : getBrushIdValue(); |
| } |
| |
| default Color getSolidColor() { |
| return (isSolidColor()) ? readARGB(getBrushIdValue()) : null; |
| } |
| |
| default void applyColor(HemfGraphics ctx) { |
| HemfDrawProperties prop = ctx.getProperties(); |
| if (isSolidColor()) { |
| prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); |
| prop.setBrushColor(new HwmfColorRef(getSolidColor())); |
| } else { |
| ctx.applyPlusObjectTableEntry(getBrushId()); |
| } |
| } |
| } |
| |
| |
| /** |
| * The EmfPlusDrawPath record specifies drawing a graphics path |
| */ |
| public static class EmfPlusDrawPath implements HemfPlusRecord, EmfPlusObjectId { |
| private int flags; |
| private int penId; |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.drawPath; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| public int getPenId() { |
| return penId; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that specifies an index in the EMF+ Object Table for an EmfPlusPen object |
| // to use for drawing the EmfPlusPath. The value MUST be zero to 63, inclusive. |
| penId = leis.readInt(); |
| assert (0 <= penId && penId <= 63); |
| |
| assert (dataSize == LittleEndianConsts.INT_SIZE); |
| |
| return LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.applyPlusObjectTableEntry(penId); |
| ctx.applyPlusObjectTableEntry(getObjectId()); |
| |
| HemfDrawProperties prop = ctx.getProperties(); |
| final Path2D path = prop.getPath(); |
| if (path != null) { |
| ctx.draw(path); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public HemfPlusRecordType getGenericRecordType() { |
| return getEmfPlusRecordType(); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "penId", this::getPenId |
| ); |
| } |
| } |
| |
| /** |
| * The EmfPlusFillRects record specifies filling the interiors of a series of rectangles. |
| */ |
| public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed, EmfPlusSolidColor { |
| private int flags; |
| private int brushId; |
| private final ArrayList<Rectangle2D> rectData = new ArrayList<>(); |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.fillRects; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that defines the brush, the content of which is |
| // determined by the S bit in the Flags field. |
| brushId = leis.readInt(); |
| |
| // A 32-bit unsigned integer that specifies the number of rectangles in the RectData field. |
| int count = leis.readInt(); |
| |
| BiFunction<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect(); |
| |
| rectData.ensureCapacity(count); |
| |
| int size = 2 * LittleEndianConsts.INT_SIZE; |
| for (int i = 0; i<count; i++) { |
| Rectangle2D rect = new Rectangle2D.Double(); |
| size += readRect.apply(leis, rect); |
| rectData.add(rect); |
| } |
| |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| HemfDrawProperties prop = ctx.getProperties(); |
| applyColor(ctx); |
| |
| Area area = new Area(); |
| rectData.stream().map(Area::new).forEach(area::add); |
| HwmfPenStyle ps = prop.getPenStyle(); |
| try { |
| prop.setPenStyle(null); |
| ctx.fill(area); |
| } finally { |
| prop.setPenStyle(ps); |
| } |
| } |
| |
| @Override |
| public int getBrushIdValue() { |
| return brushId; |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public HemfPlusRecordType getGenericRecordType() { |
| return getEmfPlusRecordType(); |
| } |
| |
| public List<Rectangle2D> getRectData() { |
| return rectData; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "brushId", this::getBrushId, |
| "brushColor", this::getSolidColor, |
| "rectData", this::getRectData |
| ); |
| } |
| } |
| |
| /** The EmfPlusDrawImagePoints record specifies drawing a scaled image inside a parallelogram. */ |
| @SuppressWarnings("unused") |
| public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition { |
| /** |
| * This bit indicates that the rendering of the image includes applying an effect. |
| * If set, an object of the Effect class MUST have been specified in an earlier EmfPlusSerializableObject record. |
| */ |
| private static final BitField EFFECT = BitFieldFactory.getInstance(0x2000); |
| |
| private int flags; |
| private int imageAttributesID; |
| private EmfPlusUnitType srcUnit; |
| private final Rectangle2D srcRect = new Rectangle2D.Double(); |
| private final Point2D upperLeft = new Point2D.Double(); |
| private final Point2D lowerRight = new Point2D.Double(); |
| private final Point2D lowerLeft = new Point2D.Double(); |
| private final AffineTransform trans = new AffineTransform(); |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.drawImagePoints; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that contains the index of the |
| // optional EmfPlusImageAttributes object in the EMF+ Object Table. |
| imageAttributesID = leis.readInt(); |
| |
| // A 32-bit signed integer that defines the units of the SrcRect field. |
| // It MUST be the UnitPixel value of the UnitType enumeration |
| srcUnit = EmfPlusUnitType.valueOf(leis.readInt()); |
| assert(srcUnit == EmfPlusUnitType.Pixel); |
| |
| int size = 2 * LittleEndianConsts.INT_SIZE; |
| |
| // An EmfPlusRectF object that defines a portion of the image to be rendered. |
| size += readRectF(leis, srcRect); |
| |
| // A 32-bit unsigned integer that specifies the number of points in the PointData array. |
| // Exactly 3 points MUST be specified. |
| int count = leis.readInt(); |
| assert(count == 3); |
| size += LittleEndianConsts.INT_SIZE; |
| |
| BiFunction<LittleEndianInputStream, Point2D, Integer> readPoint; |
| |
| if (isRelativePosition()) { |
| // If the POSITION flag is set in the Flags, the points specify relative locations. |
| readPoint = HemfPlusDraw::readPointR; |
| } else if (isCompressed()) { |
| // If the POSITION bit is clear and the COMPRESSED bit is set in the Flags field, the points |
| // specify absolute locations with integer values. |
| readPoint = HemfPlusDraw::readPointS; |
| } else { |
| // If the POSITION bit is clear and the COMPRESSED bit is clear in the Flags field, the points |
| // specify absolute locations with floating-point values. |
| readPoint = HemfPlusDraw::readPointF; |
| } |
| |
| // TODO: handle relative coordinates |
| |
| // An array of Count points that specify three points of a parallelogram. |
| // The three points represent the upper-left, upper-right, and lower-left corners of the parallelogram. |
| // The fourth point of the parallelogram is extrapolated from the first three. |
| // The portion of the image specified by the SrcRect field SHOULD have scaling and shearing transforms |
| // applied if necessary to fit inside the parallelogram. |
| |
| |
| // size += readPoint.apply(leis, upperLeft); |
| // size += readPoint.apply(leis, upperRight); |
| // size += readPoint.apply(leis, lowerLeft); |
| |
| size += readPoint.apply(leis, lowerLeft); |
| size += readPoint.apply(leis, lowerRight); |
| size += readPoint.apply(leis, upperLeft); |
| |
| // https://math.stackexchange.com/questions/2772737/how-to-transform-arbitrary-rectangle-into-specific-parallelogram |
| |
| RealMatrix para2normal = MatrixUtils.createRealMatrix(new double[][] { |
| { lowerLeft.getX(), lowerRight.getX(), upperLeft.getX() }, |
| { lowerLeft.getY(), lowerRight.getY(), upperLeft.getY() }, |
| { 1, 1, 1 } |
| }); |
| |
| RealMatrix rect2normal = MatrixUtils.createRealMatrix(new double[][]{ |
| { srcRect.getMinX(), srcRect.getMaxX(), srcRect.getMinX() }, |
| { srcRect.getMinY(), srcRect.getMinY(), srcRect.getMaxY() }, |
| { 1, 1, 1 } |
| }); |
| |
| RealMatrix normal2rect = new LUDecomposition(rect2normal).getSolver().getInverse(); |
| double[][] m = para2normal.multiply(normal2rect).getData(); |
| trans.setTransform(round10(m[0][0]), round10(m[1][0]), round10(m[0][1]), round10(m[1][1]), round10(m[0][2]), round10(m[1][2])); |
| |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| HemfDrawProperties prop = ctx.getProperties(); |
| |
| ctx.applyPlusObjectTableEntry(imageAttributesID); |
| ctx.applyPlusObjectTableEntry(getObjectId()); |
| |
| final ImageRenderer ir = prop.getEmfPlusImage(); |
| if (ir == null) { |
| return; |
| } |
| |
| AffineTransform txSaved = ctx.getTransform(); |
| AffineTransform tx = (AffineTransform)txSaved.clone(); |
| HwmfTernaryRasterOp oldOp = prop.getRasterOp3(); |
| HwmfBkMode oldBk = prop.getBkMode(); |
| try { |
| tx.concatenate(trans); |
| ctx.setTransform(tx); |
| |
| prop.setRasterOp3(HwmfTernaryRasterOp.SRCCOPY); |
| prop.setBkMode(HwmfBkMode.TRANSPARENT); |
| |
| // transformation from srcRect to destRect was already applied, |
| // therefore use srcRect as third parameter |
| ctx.drawImage(ir, srcRect, srcRect); |
| } finally { |
| prop.setBkMode(oldBk); |
| prop.setRasterOp3(oldOp); |
| ctx.setTransform(txSaved); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| final Map<String,Supplier<?>> m = new LinkedHashMap<>(); |
| m.put("flags", this::getFlags); |
| m.put("imageAttributesID", () -> imageAttributesID); |
| m.put("srcUnit", () -> srcUnit); |
| m.put("srcRect", () -> srcRect); |
| m.put("upperLeft", () -> upperLeft); |
| m.put("lowerLeft", () -> lowerLeft); |
| m.put("lowerRight", () -> lowerRight); |
| m.put("transform", () -> trans); |
| return Collections.unmodifiableMap(m); |
| } |
| } |
| |
| /** The EmfPlusDrawImage record specifies drawing a scaled image. */ |
| public static class EmfPlusDrawImage implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed { |
| private int flags; |
| private int imageAttributesID; |
| private EmfPlusUnitType srcUnit; |
| private final Rectangle2D srcRect = new Rectangle2D.Double(); |
| private final Rectangle2D rectData = new Rectangle2D.Double(); |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.drawImage; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that contains the index of the |
| // optional EmfPlusImageAttributes object in the EMF+ Object Table. |
| imageAttributesID = leis.readInt(); |
| |
| // A 32-bit signed integer that defines the units of the SrcRect field. |
| // It MUST be the UnitPixel value of the UnitType enumeration |
| srcUnit = EmfPlusUnitType.valueOf(leis.readInt()); |
| assert(srcUnit == EmfPlusUnitType.Pixel); |
| |
| int size = 2 * LittleEndianConsts.INT_SIZE; |
| |
| // An EmfPlusRectF object that specifies a portion of the image to be rendered. The portion of the image |
| // specified by this rectangle is scaled to fit the destination rectangle specified by the RectData field. |
| size += readRectF(leis, srcRect); |
| |
| // Either an EmfPlusRect or EmfPlusRectF object that defines the bounding box of the image. The portion of |
| // the image specified by the SrcRect field is scaled to fit this rectangle. |
| size += getReadRect().apply(leis, rectData); |
| |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.applyPlusObjectTableEntry(imageAttributesID); |
| ctx.applyPlusObjectTableEntry(getObjectId()); |
| |
| HemfDrawProperties prop = ctx.getProperties(); |
| prop.setRasterOp3(HwmfTernaryRasterOp.SRCCOPY); |
| prop.setBkMode(HwmfBkMode.TRANSPARENT); |
| |
| ctx.drawImage(prop.getEmfPlusImage(), srcRect, rectData); |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "imageAttributesID", () -> imageAttributesID, |
| "srcUnit", () -> srcUnit, |
| "srcRect", () -> srcRect, |
| "rectData", () -> rectData |
| ); |
| } |
| } |
| |
| /** The EmfPlusFillRegion record specifies filling the interior of a graphics region. */ |
| public static class EmfPlusFillRegion implements HemfPlusRecord, EmfPlusSolidColor, EmfPlusObjectId { |
| private int flags; |
| private int brushId; |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.fillRegion; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public int getBrushIdValue() { |
| return brushId; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that defines the brush, the content of which is determined by |
| // the SOLID_COLOR bit in the Flags field. |
| // If SOLID_COLOR is set, BrushId specifies a color as an EmfPlusARGB object. |
| // If clear, BrushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. |
| brushId = leis.readInt(); |
| |
| return LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| applyColor(ctx); |
| ctx.applyPlusObjectTableEntry(getObjectId()); |
| HemfDrawProperties prop = ctx.getProperties(); |
| ctx.fill(prop.getPath()); |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "brushId", () -> brushId |
| ); |
| } |
| } |
| |
| /** The EmfPlusFillPath record specifies filling the interior of a graphics path. */ |
| public static class EmfPlusFillPath extends EmfPlusFillRegion { |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.fillPath; |
| } |
| |
| } |
| |
| /** The EmfPlusDrawDriverString record specifies text output with character positions. */ |
| @SuppressWarnings("unused") |
| public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId, EmfPlusSolidColor { |
| /** |
| * If set, the positions of character glyphs SHOULD be specified in a character map lookup table. |
| * If clear, the glyph positions SHOULD be obtained from an array of coordinates. |
| */ |
| private static final BitField CMAP_LOOKUP = BitFieldFactory.getInstance(0x0001); |
| |
| /** |
| * If set, the string SHOULD be rendered vertically. |
| * If clear, the string SHOULD be rendered horizontally. |
| */ |
| private static final BitField VERTICAL = BitFieldFactory.getInstance(0x0002); |
| |
| /** |
| * If set, character glyph positions SHOULD be calculated relative to the position of the first glyph. |
| * If clear, the glyph positions SHOULD be obtained from an array of coordinates. |
| */ |
| private static final BitField REALIZED_ADVANCE = BitFieldFactory.getInstance(0x0004); |
| |
| /** |
| * If set, less memory SHOULD be used to cache anti-aliased glyphs, which produces lower quality text rendering. |
| * If clear, more memory SHOULD be used, which produces higher quality text rendering. |
| */ |
| private static final BitField LIMIT_SUBPIXEL = BitFieldFactory.getInstance(0x0008); |
| |
| private static final int[] OPTIONS_MASK = { 0x0001, 0x0002, 0x0004, 0x0008 }; |
| |
| private static final String[] OPTIONS_NAMES = { |
| "CMAP_LOOKUP", "VERTICAL", "REALIZED_ADVANCE", "LIMIT_SUBPIXEL" |
| }; |
| |
| private int flags; |
| private int brushId; |
| private int optionsFlags; |
| private String glyphs; |
| private final List<Point2D> glyphPos = new ArrayList<>(); |
| private final AffineTransform transformMatrix = new AffineTransform(); |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.drawDriverString; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public int getBrushIdValue() { |
| return brushId; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that specifies either the foreground color of the text or a graphics brush, |
| // depending on the value of the SOLID_COLOR flag in the Flags. |
| brushId = leis.readInt(); |
| |
| // A 32-bit unsigned integer that specifies the spacing, orientation, and quality of rendering for the |
| // string. This value MUST be composed of DriverStringOptions flags |
| optionsFlags = leis.readInt(); |
| |
| // A 32-bit unsigned integer that specifies whether a transform matrix is present in the |
| // TransformMatrix field. |
| int matrixPresent = leis.readInt(); |
| |
| // A 32-bit unsigned integer that specifies number of glyphs in the string. |
| int glyphCount = leis.readInt(); |
| |
| int size = 4 * LittleEndianConsts.INT_SIZE; |
| |
| // TOOD: implement Non-Cmap-Lookup correctly |
| |
| // If the CMAP_LOOKUP flag in the optionsFlags field is set, each value in this array specifies a |
| // Unicode character. Otherwise, each value specifies an index to a character glyph in the EmfPlusFont |
| // object specified by the ObjectId value in Flags field. |
| byte[] glyphBuf = IOUtils.toByteArray(leis, glyphCount*2, MAX_OBJECT_SIZE); |
| glyphs = StringUtil.getFromUnicodeLE(glyphBuf); |
| |
| size += glyphBuf.length; |
| |
| // An array of EmfPlusPointF objects that specify the output position of each character glyph. |
| // There MUST be GlyphCount elements, which have a one-to-one correspondence with the elements |
| // in the Glyphs array. |
| // |
| // Glyph positions are calculated from the position of the first glyph if the REALIZED_ADVANCE flag in |
| // Options flags is set. In this case, GlyphPos specifies the position of the first glyph only. |
| int glyphPosCnt = REALIZED_ADVANCE.isSet(optionsFlags) ? 1 : glyphCount; |
| for (int i=0; i<glyphCount; i++) { |
| Point2D p = new Point2D.Double(); |
| size += readPointF(leis, p); |
| glyphPos.add(p); |
| } |
| |
| if (matrixPresent != 0) { |
| size += HemfFill.readXForm(leis, transformMatrix); |
| } |
| |
| |
| return size; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| HemfDrawProperties prop = ctx.getProperties(); |
| prop.setTextAlignLatin(HwmfText.HwmfTextAlignment.LEFT); |
| prop.setTextVAlignLatin(HwmfText.HwmfTextVerticalAlignment.BASELINE); |
| |
| ctx.applyPlusObjectTableEntry(getObjectId()); |
| if (isSolidColor()) { |
| prop.setTextColor(new HwmfColorRef(getSolidColor())); |
| } else { |
| ctx.applyPlusObjectTableEntry(getBrushId()); |
| } |
| |
| if (REALIZED_ADVANCE.isSet(optionsFlags)) { |
| byte[] buf = glyphs.getBytes(StandardCharsets.UTF_16LE); |
| ctx.drawString(buf, buf.length, glyphPos.get(0), null, null, null, null, true); |
| } else { |
| final OfInt glyphIter = glyphs.codePoints().iterator(); |
| glyphPos.forEach(p -> { |
| byte[] buf = new String(new int[]{glyphIter.next()}, 0, 1).getBytes(StandardCharsets.UTF_16LE); |
| ctx.drawString(buf, buf.length, p, null, null, null, null, true); |
| }); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "brushId", this::getBrushId, |
| "optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES), |
| "glyphs", () -> glyphs, |
| "glyphPos", () -> glyphPos, |
| "transform", () -> transformMatrix |
| ); |
| } |
| } |
| |
| /** The EmfPlusDrawRects record specifies drawing a series of rectangles. */ |
| public static class EmfPlusDrawRects implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed { |
| private int flags; |
| private final List<Rectangle2D> rectData = new ArrayList<>(); |
| |
| @Override |
| public HemfPlusRecordType getEmfPlusRecordType() { |
| return HemfPlusRecordType.drawRects; |
| } |
| |
| @Override |
| public int getFlags() { |
| return flags; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { |
| this.flags = flags; |
| |
| // A 32-bit unsigned integer that specifies the number of rectangles in the RectData member. |
| int count = leis.readInt(); |
| int size = LittleEndianConsts.INT_SIZE; |
| |
| BiFunction<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect(); |
| |
| for (int i=0; i<count; i++) { |
| Rectangle2D rect = new Rectangle2D.Double(); |
| size += readRect.apply(leis, rect); |
| } |
| |
| return size; |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "flags", this::getFlags, |
| "rectData", () -> rectData |
| ); |
| } |
| } |
| |
| @SuppressWarnings("squid:S2111") |
| static double round10(double d) { |
| return new BigDecimal(d).setScale(10, RoundingMode.HALF_UP).doubleValue(); |
| } |
| |
| static int readRectS(LittleEndianInputStream leis, Rectangle2D bounds) { |
| // A 16-bit signed integer that defines the ... coordinate |
| final int x = leis.readShort(); |
| final int y = leis.readShort(); |
| final int width = leis.readShort(); |
| final int height = leis.readShort(); |
| bounds.setRect(x, y, width, height); |
| |
| return 4 * LittleEndianConsts.SHORT_SIZE; |
| } |
| |
| static int readRectF(LittleEndianInputStream leis, Rectangle2D bounds) { |
| // A 32-bit floating-point that defines the ... coordinate |
| final double x = leis.readFloat(); |
| final double y = leis.readFloat(); |
| final double width = leis.readFloat(); |
| final double height = leis.readFloat(); |
| bounds.setRect(x, y, width, height); |
| |
| return 4 * LittleEndianConsts.INT_SIZE; |
| } |
| |
| /** |
| * The EmfPlusPoint object specifies an ordered pair of integer (X,Y) values that define an absolute |
| * location in a coordinate space. |
| */ |
| static int readPointS(LittleEndianInputStream leis, Point2D point) { |
| double x = leis.readShort(); |
| double y = leis.readShort(); |
| point.setLocation(x,y); |
| return 2*LittleEndianConsts.SHORT_SIZE; |
| } |
| |
| /** |
| * The EmfPlusPointF object specifies an ordered pair of floating-point (X,Y) values that define an |
| * absolute location in a coordinate space. |
| */ |
| static int readPointF(LittleEndianInputStream leis, Point2D point) { |
| double x = leis.readFloat(); |
| double y = leis.readFloat(); |
| point.setLocation(x,y); |
| return 2*LittleEndianConsts.INT_SIZE; |
| } |
| |
| /** |
| * The EmfPlusPointR object specifies an ordered pair of integer (X,Y) values that define a relative |
| * location in a coordinate space. |
| */ |
| static int readPointR(LittleEndianInputStream leis, Point2D point) { |
| int[] p = { 0 }; |
| int size = readEmfPlusInteger(leis, p); |
| double x = p[0]; |
| size += readEmfPlusInteger(leis, p); |
| double y = p[0]; |
| point.setLocation(x,y); |
| return size; |
| } |
| |
| private static int readEmfPlusInteger(LittleEndianInputStream leis, int[] value) { |
| value[0] = leis.readByte(); |
| // check for EmfPlusInteger7 value |
| if ((value[0] & 0x80) == 0) { |
| return LittleEndianConsts.BYTE_SIZE; |
| } |
| // ok we've read a EmfPlusInteger15 |
| value[0] = ((value[0] << 8) | (leis.readByte() & 0xFF)) & 0x7FFF; |
| return LittleEndianConsts.SHORT_SIZE; |
| } |
| |
| static Color readARGB(int argb) { |
| return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF); |
| } |
| |
| } |