| /* ==================================================================== |
| 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 java.nio.charset.StandardCharsets.UTF_16LE; |
| import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionFloat; |
| 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.Dimension2D; |
| import java.awt.geom.Rectangle2D; |
| import java.io.IOException; |
| import java.nio.charset.Charset; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| |
| import org.apache.poi.hemf.draw.HemfGraphics; |
| import org.apache.poi.hwmf.draw.HwmfGraphics; |
| import org.apache.poi.hwmf.record.HwmfText; |
| import org.apache.poi.hwmf.record.HwmfText.WmfSetTextAlign; |
| import org.apache.poi.util.Dimension2DDouble; |
| import org.apache.poi.util.GenericRecordJsonWriter; |
| import org.apache.poi.util.GenericRecordUtil; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.Internal; |
| import org.apache.poi.util.LittleEndianConsts; |
| import org.apache.poi.util.LittleEndianInputStream; |
| import org.apache.poi.util.RecordFormatException; |
| |
| /** |
| * Container class to gather all text-related commands |
| * This is starting out as read only, and very little is actually |
| * implemented at this point! |
| */ |
| @Internal |
| @SuppressWarnings("WeakerAccess") |
| public class HemfText { |
| |
| private static final int MAX_RECORD_LENGTH = 1_000_000; |
| |
| public enum EmfGraphicsMode { |
| GM_COMPATIBLE, GM_ADVANCED |
| } |
| |
| public static class EmfExtTextOutA extends HwmfText.WmfExtTextOut implements HemfRecord { |
| |
| protected Rectangle2D boundsIgnored = new Rectangle2D.Double(); |
| protected EmfGraphicsMode graphicsMode; |
| |
| /** |
| * The scale factor to apply along the X/Y axis to convert from page space units to .01mm units. |
| * This SHOULD be used only if the graphics mode specified by iGraphicsMode is GM_COMPATIBLE. |
| */ |
| protected final Dimension2D scale = new Dimension2DDouble(); |
| |
| public EmfExtTextOutA() { |
| super(new EmfExtTextOutOptions()); |
| } |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.extTextOutA; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| if (recordSize < 0 || Integer.MAX_VALUE <= recordSize) { |
| throw new RecordFormatException("recordSize must be a positive integer (0-0x7FFFFFFF)"); |
| } |
| |
| // A WMF RectL object. It is not used and MUST be ignored on receipt. |
| long size = readRectL(leis, boundsIgnored); |
| |
| // A 32-bit unsigned integer that specifies the graphics mode from the GraphicsMode enumeration |
| graphicsMode = EmfGraphicsMode.values()[leis.readInt()-1]; |
| size += LittleEndianConsts.INT_SIZE; |
| |
| size += readDimensionFloat(leis, scale); |
| |
| // A WMF PointL object that specifies the coordinates of the reference point used to position the string. |
| // The reference point is defined by the last EMR_SETTEXTALIGN record. |
| // If no such record has been set, the defaulint alignment is TA_LEFT,TA_TOP. |
| size += readPointL(leis, reference); |
| // A 32-bit unsigned integer that specifies the number of characters in the string. |
| stringLength = (int)leis.readUInt(); |
| // A 32-bit unsigned integer that specifies the offset to the output string, in bytes, |
| // from the start of the record in which this object is contained. |
| // This value MUST be 8- or 16-bit aligned, according to the character format. |
| int offString = (int)leis.readUInt(); |
| size += 2*LittleEndianConsts.INT_SIZE; |
| |
| size += options.init(leis); |
| // An optional WMF RectL object that defines a clipping and/or opaquing rectangle in logical units. |
| // This rectangle is applied to the text output performed by the containing record. |
| if (options.isClipped() || options.isOpaque()) { |
| size += readRectL(leis, bounds); |
| } |
| |
| // A 32-bit unsigned integer that specifies the offset to an intercharacter spacing array, in bytes, |
| // from the start of the record in which this object is contained. This value MUST be 32-bit aligned. |
| int offDx = (int)leis.readUInt(); |
| size += LittleEndianConsts.INT_SIZE; |
| |
| // handle dx before string and other way round |
| final String order = (offDx < offString) ? "ds" : "sd"; |
| // the next byte index after the string ends |
| int strEnd = (int)((offDx <= HEADER_SIZE) ? recordSize : offDx-HEADER_SIZE); |
| for (char op : order.toCharArray()) { |
| switch (op) { |
| case 'd': { |
| dx.clear(); |
| int undefinedSpace2 = (int) (offDx - (size + HEADER_SIZE)); |
| if (offDx > 0 && undefinedSpace2 >= 0 && offDx-HEADER_SIZE < recordSize) { |
| leis.skipFully(undefinedSpace2); |
| size += undefinedSpace2; |
| |
| // An array of 32-bit unsigned integers that specify the output spacing between the origins of adjacent |
| // character cells in logical units. The location of this field is specified by the value of offDx |
| // in bytes from the start of this record. If spacing is defined, this field contains the same number |
| // of values as characters in the output string. |
| // |
| // If the Options field of the EmrText object contains the ETO_PDY flag, then this buffer |
| // contains twice as many values as there are characters in the output string, one |
| // horizontal and one vertical offset for each, in that order. |
| // |
| // If ETO_RTLREADING is specified, characters are laid right to left instead of left to right. |
| // No other options affect the interpretation of this field. |
| final int maxSize = (int)Math.min((offDx < offString) ? (offString-HEADER_SIZE) : recordSize, recordSize); |
| while (size <= maxSize-LittleEndianConsts.INT_SIZE) { |
| dx.add((int) leis.readUInt()); |
| size += LittleEndianConsts.INT_SIZE; |
| } |
| } |
| if (dx.size() < stringLength) { |
| // invalid dx array |
| dx.clear(); |
| } |
| strEnd = (int)recordSize; |
| break; |
| } |
| default: |
| case 's': { |
| int undefinedSpace1 = (int)(offString - (size + HEADER_SIZE)); |
| if (offString > 0 && undefinedSpace1 >= 0 && offString-HEADER_SIZE < recordSize) { |
| leis.skipFully(undefinedSpace1); |
| size += undefinedSpace1; |
| |
| // read all available bytes and not just "stringLength * 1(ansi)/2(unicode)" |
| // in case we need to deal with surrogate pairs |
| final int maxSize = (int)(Math.min(recordSize, strEnd)-size); |
| rawTextBytes = IOUtils.safelyAllocate(maxSize, MAX_RECORD_LENGTH); |
| leis.readFully(rawTextBytes); |
| size += maxSize; |
| break; |
| } |
| } |
| } |
| } |
| |
| return size; |
| } |
| |
| /** |
| * |
| * To be implemented! We need to get the current character set |
| * from the current font for {@link EmfExtTextOutA}, |
| * which has to be tracked in the playback device. |
| * |
| * For {@link EmfExtTextOutW}, the charset is "UTF-16LE" |
| * |
| * @param charset the charset to be used to decode the character bytes |
| * @return text from this text element |
| * @throws IOException if the charset is not compatible to the underlying bytes |
| */ |
| public String getText(Charset charset) throws IOException { |
| return super.getText(charset); |
| } |
| |
| public EmfGraphicsMode getGraphicsMode() { |
| return graphicsMode; |
| } |
| |
| public Dimension2D getScale() { |
| return scale; |
| } |
| |
| @Override |
| public void draw(HwmfGraphics ctx) { |
| // A 32-bit floating-point value that specifies the scale factor to apply along |
| // the axis to convert from page space units to .01mm units. |
| // This SHOULD be used only if the graphics mode specified by iGraphicsMode is GM_COMPATIBLE. |
| Dimension2D scl = graphicsMode == EmfGraphicsMode.GM_COMPATIBLE ? scale : null; |
| ctx.setCharsetProvider(charsetProvider); |
| ctx.drawString(rawTextBytes, stringLength, reference, scl, bounds, options, dx, isUnicode()); |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "boundsIgnored", () -> boundsIgnored, |
| "graphicsMode", this::getGraphicsMode, |
| "scale", this::getScale |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| public static class EmfExtTextOutW extends EmfExtTextOutA { |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.extTextOutW; |
| } |
| |
| public String getText() throws IOException { |
| return getText(UTF_16LE); |
| } |
| |
| protected boolean isUnicode() { |
| return true; |
| } |
| } |
| |
| /** |
| * The EMR_SETTEXTALIGN record specifies text alignment. |
| */ |
| public static class EmfSetTextAlign extends WmfSetTextAlign implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.setTextAlign; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| // A 32-bit unsigned integer that specifies text alignment by using a mask of text alignment flags. |
| // These are either WMF TextAlignmentMode Flags for text with a horizontal baseline, |
| // or WMF VerticalTextAlignmentMode Flags for text with a vertical baseline. |
| // Only one value can be chosen from those that affect horizontal and vertical alignment. |
| textAlignmentMode = (int)leis.readUInt(); |
| return LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| /** |
| * The EMR_SETTEXTCOLOR record defines the current text color. |
| */ |
| public static class EmfSetTextColor extends HwmfText.WmfSetTextColor implements HemfRecord { |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.setTextColor; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| return colorRef.init(leis); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| |
| |
| public static class EmfExtCreateFontIndirectW extends HwmfText.WmfCreateFontIndirect |
| implements HemfRecord { |
| int fontIdx; |
| |
| public EmfExtCreateFontIndirectW() { |
| super(new HemfFont()); |
| } |
| |
| @Override |
| public HemfRecordType getEmfRecordType() { |
| return HemfRecordType.extCreateFontIndirectW; |
| } |
| |
| @Override |
| public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { |
| // A 32-bit unsigned integer that specifies the index of the logical font object |
| // in the EMF Object Table |
| fontIdx = (int)leis.readUInt(); |
| long size = font.init(leis, (int)(recordSize-LittleEndianConsts.INT_SIZE)); |
| return size+LittleEndianConsts.INT_SIZE; |
| } |
| |
| @Override |
| public void draw(HemfGraphics ctx) { |
| ctx.addObjectTableEntry(this, fontIdx); |
| } |
| |
| @Override |
| public String toString() { |
| return GenericRecordJsonWriter.marshal(this); |
| } |
| |
| public int getFontIdx() { |
| return fontIdx; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return GenericRecordUtil.getGenericProperties( |
| "base", super::getGenericProperties, |
| "fontIdx", this::getFontIdx |
| ); |
| } |
| |
| @Override |
| public HemfRecordType getGenericRecordType() { |
| return getEmfRecordType(); |
| } |
| } |
| |
| public static class EmfExtTextOutOptions extends HwmfText.WmfExtTextOutOptions { |
| @Override |
| public int init(LittleEndianInputStream leis) { |
| // A 32-bit unsigned integer that specifies how to use the rectangle specified in the Rectangle field. |
| // This field can be a combination of more than one ExtTextOutOptions enumeration |
| flags = (int)leis.readUInt(); |
| return LittleEndianConsts.INT_SIZE; |
| } |
| } |
| |
| public static class SetTextJustification extends UnimplementedHemfRecord { |
| |
| } |
| |
| /** |
| * Needs to be implemented. Couldn't find example. |
| */ |
| public static class PolyTextOutA extends UnimplementedHemfRecord { |
| |
| } |
| |
| /** |
| * Needs to be implemented. Couldn't find example. |
| */ |
| public static class PolyTextOutW extends UnimplementedHemfRecord { |
| |
| } |
| |
| } |