From f6388c2fff1893fd09c8ca9ab75a0ce823035b04 Mon Sep 17 00:00:00 2001 From: Tim Allison Date: Thu, 19 Jan 2017 16:22:29 +0000 Subject: [PATCH] Bug 60570 - Add rudimentary EMF read-only capability git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1779493 13f79535-47bb-0310-9956-ffa450edef68 --- src/java/org/apache/poi/util/IOUtils.java | 23 ++ .../poi/hemf/extractor/HemfExtractor.java | 115 ++++++++ .../hemf/hemfplus/record/HemfPlusHeader.java | 82 ++++++ .../hemf/hemfplus/record/HemfPlusRecord.java | 45 +++ .../hemfplus/record/HemfPlusRecordType.java | 97 +++++++ .../record/UnimplementedHemfPlusRecord.java | 53 ++++ .../poi/hemf/record/AbstractHemfComment.java | 39 +++ .../apache/poi/hemf/record/HemfComment.java | 31 +++ .../poi/hemf/record/HemfCommentEMFPlus.java | 107 +++++++ .../poi/hemf/record/HemfCommentEMFSpool.java | 31 +++ .../poi/hemf/record/HemfCommentPublic.java | 176 ++++++++++++ .../poi/hemf/record/HemfCommentRecord.java | 139 ++++++++++ .../apache/poi/hemf/record/HemfHeader.java | 198 +++++++++++++ .../apache/poi/hemf/record/HemfRecord.java | 41 +++ .../poi/hemf/record/HemfRecordType.java | 159 +++++++++++ .../org/apache/poi/hemf/record/HemfText.java | 262 ++++++++++++++++++ .../hemf/record/UnimplementedHemfRecord.java | 49 ++++ .../poi/hemf/extractor/HemfExtractorTest.java | 167 +++++++++++ .../extractor/HemfPlusExtractorTest.java | 96 +++++++ test-data/document/testException2.doc-2.wmf | Bin 0 -> 4130 bytes test-data/spreadsheet/SimpleEMF_mac.emf | Bin 0 -> 133320 bytes test-data/spreadsheet/SimpleEMF_windows.emf | Bin 0 -> 27864 bytes 22 files changed, 1910 insertions(+) create mode 100644 src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java create mode 100644 test-data/document/testException2.doc-2.wmf create mode 100644 test-data/spreadsheet/SimpleEMF_mac.emf create mode 100644 test-data/spreadsheet/SimpleEMF_windows.emf diff --git a/src/java/org/apache/poi/util/IOUtils.java b/src/java/org/apache/poi/util/IOUtils.java index 929ade06c..a15c3b2b2 100644 --- a/src/java/org/apache/poi/util/IOUtils.java +++ b/src/java/org/apache/poi/util/IOUtils.java @@ -251,4 +251,27 @@ public final class IOUtils { exc ); } } + + /** + * Skips bytes from a stream. Returns -1L if EOF was hit before + * the end of the stream. + * + * @param in inputstream + * @param len length to skip + * @return number of bytes skipped + * @throws IOException on IOException + */ + public static long skipFully(InputStream in, long len) throws IOException { + int total = 0; + while (true) { + long got = in.skip(len-total); + if (got < 0) { + return -1L; + } + total += got; + if (total == len) { + return total; + } + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java b/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java new file mode 100644 index 000000000..05379e552 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java @@ -0,0 +1,115 @@ +/* ==================================================================== + 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.extractor; + + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +import org.apache.poi.hemf.record.HemfHeader; +import org.apache.poi.hemf.record.HemfRecord; +import org.apache.poi.hemf.record.HemfRecordType; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RecordFormatException; + +/** + * Read-only EMF extractor. Lots remain + */ +@Internal +public class HemfExtractor implements Iterable { + + private HemfHeader header; + private final LittleEndianInputStream stream; + + public HemfExtractor(InputStream is) throws IOException { + stream = new LittleEndianInputStream(is); + header = new HemfHeader(); + long recordId = stream.readUInt(); + long recordSize = stream.readUInt(); + + header = new HemfHeader(); + header.init(stream, recordId, recordSize-8); + } + + @Override + public Iterator iterator() { + return new HemfRecordIterator(); + } + + public HemfHeader getHeader() { + return header; + } + + private class HemfRecordIterator implements Iterator { + + private HemfRecord currentRecord = null; + + HemfRecordIterator() { + //queue the first non-header record + currentRecord = _next(); + } + + @Override + public boolean hasNext() { + return currentRecord != null; + } + + @Override + public HemfRecord next() { + HemfRecord toReturn = currentRecord; + currentRecord = _next(); + return toReturn; + } + + private HemfRecord _next() { + if (currentRecord != null && currentRecord.getRecordType().equals(HemfRecordType.eof)) { + return null; + } + long recordId = stream.readUInt(); + long recordSize = stream.readUInt(); + + HemfRecord record = null; + HemfRecordType type = HemfRecordType.getById(recordId); + if (type == null) { + throw new RuntimeException("Undefined record of type:"+recordId); + } + try { + record = type.clazz.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + try { + record.init(stream, recordId, recordSize-8); + } catch (IOException e) { + throw new RecordFormatException(e); + } + + return record; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java new file mode 100644 index 000000000..25947937b --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java @@ -0,0 +1,82 @@ +/* ==================================================================== + 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.hemfplus.record; + + +import java.io.IOException; + +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; + +@Internal +public class HemfPlusHeader implements HemfPlusRecord { + + private int flags; + private long version; //hack for now; replace with EmfPlusGraphicsVersion object + private long emfPlusFlags; + private long logicalDpiX; + private long logicalDpiY; + + @Override + public HemfPlusRecordType getRecordType() { + return HemfPlusRecordType.header; + } + + public int getFlags() { + return flags; + } + + @Override + public void init(byte[] dataBytes, int recordId, int flags) throws IOException { + //assert record id == header + this.flags = flags; + int offset = 0; + this.version = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; + this.emfPlusFlags = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; + this.logicalDpiX = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; + this.logicalDpiY = LittleEndian.getUInt(dataBytes, offset); + + } + + public long getVersion() { + return version; + } + + public long getEmfPlusFlags() { + return emfPlusFlags; + } + + public long getLogicalDpiX() { + return logicalDpiX; + } + + public long getLogicalDpiY() { + return logicalDpiY; + } + + @Override + public String toString() { + return "HemfPlusHeader{" + + "flags=" + flags + + ", version=" + version + + ", emfPlusFlags=" + emfPlusFlags + + ", logicalDpiX=" + logicalDpiX + + ", logicalDpiY=" + logicalDpiY + + '}'; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java new file mode 100644 index 000000000..6186d9a69 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java @@ -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.hemfplus.record; + + +import java.io.IOException; + +import org.apache.poi.util.Internal; + +@Internal +public interface HemfPlusRecord { + + HemfPlusRecordType getRecordType(); + + int getFlags(); + + /** + * + * @param dataBytes these are the bytes that start after the id, flags, record size + * and go to the end of the record; they do not include any required padding + * at the end. + * @param recordId record type id + * @param flags flags + * @return + * @throws IOException, RecordFormatException + */ + void init(byte[] dataBytes, int recordId, int flags) throws IOException; + + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java new file mode 100644 index 000000000..70837628e --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java @@ -0,0 +1,97 @@ +/* ==================================================================== + 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.hemfplus.record; + +import org.apache.poi.util.Internal; + +@Internal +public enum HemfPlusRecordType { + header(0x4001, HemfPlusHeader.class), + endOfFile(0x4002, UnimplementedHemfPlusRecord.class), + comment(0x4003, UnimplementedHemfPlusRecord.class), + getDC(0x4004, UnimplementedHemfPlusRecord.class), + multiFormatStart(0x4005, UnimplementedHemfPlusRecord.class), + multiFormatSection(0x4006, UnimplementedHemfPlusRecord.class), + multiFormatEnd(0x4007, UnimplementedHemfPlusRecord.class), + object(0x4008, UnimplementedHemfPlusRecord.class), + clear(0x4009, UnimplementedHemfPlusRecord.class), + fillRects(0x400A, UnimplementedHemfPlusRecord.class), + drawRects(0x400B, UnimplementedHemfPlusRecord.class), + fillPolygon(0x400C, UnimplementedHemfPlusRecord.class), + drawLines(0x400D, UnimplementedHemfPlusRecord.class), + fillEllipse(0x400E, UnimplementedHemfPlusRecord.class), + drawEllipse(0x400F, UnimplementedHemfPlusRecord.class), + fillPie(0x4010, UnimplementedHemfPlusRecord.class), + drawPie(0x4011, UnimplementedHemfPlusRecord.class), + drawArc(0x4012, UnimplementedHemfPlusRecord.class), + fillRegion(0x4013, UnimplementedHemfPlusRecord.class), + fillPath(0x4014, UnimplementedHemfPlusRecord.class), + drawPath(0x4015, UnimplementedHemfPlusRecord.class), + fillClosedCurve(0x4016, UnimplementedHemfPlusRecord.class), + drawClosedCurve(0x4017, UnimplementedHemfPlusRecord.class), + drawCurve(0x4018, UnimplementedHemfPlusRecord.class), + drawBeziers(0x4019, UnimplementedHemfPlusRecord.class), + drawImage(0x401A, UnimplementedHemfPlusRecord.class), + drawImagePoints(0x401B, UnimplementedHemfPlusRecord.class), + drawString(0x401C, UnimplementedHemfPlusRecord.class), + setRenderingOrigin(0x401D, UnimplementedHemfPlusRecord.class), + setAntiAliasMode(0x401E, UnimplementedHemfPlusRecord.class), + setTextRenderingHint(0x401F, UnimplementedHemfPlusRecord.class), + setTextContrast(0x4020, UnimplementedHemfPlusRecord.class), + setInterpolationMode(0x4021, UnimplementedHemfPlusRecord.class), + setPixelOffsetMode(0x4022, UnimplementedHemfPlusRecord.class), + setComositingMode(0x4023, UnimplementedHemfPlusRecord.class), + setCompositingQuality(0x4024, UnimplementedHemfPlusRecord.class), + save(0x4025, UnimplementedHemfPlusRecord.class), + restore(0x4026, UnimplementedHemfPlusRecord.class), + beginContainer(0x4027, UnimplementedHemfPlusRecord.class), + beginContainerNoParams(0x428, UnimplementedHemfPlusRecord.class), + endContainer(0x4029, UnimplementedHemfPlusRecord.class), + setWorldTransform(0x402A, UnimplementedHemfPlusRecord.class), + resetWorldTransform(0x402B, UnimplementedHemfPlusRecord.class), + multiplyWorldTransform(0x402C, UnimplementedHemfPlusRecord.class), + translateWorldTransform(0x402D, UnimplementedHemfPlusRecord.class), + scaleWorldTransform(0x402E, UnimplementedHemfPlusRecord.class), + rotateWorldTransform(0x402F, UnimplementedHemfPlusRecord.class), + setPageTransform(0x4030, UnimplementedHemfPlusRecord.class), + resetClip(0x4031, UnimplementedHemfPlusRecord.class), + setClipRect(0x4032, UnimplementedHemfPlusRecord.class), + setClipRegion(0x4033, UnimplementedHemfPlusRecord.class), + setClipPath(0x4034, UnimplementedHemfPlusRecord.class), + offsetClip(0x4035, UnimplementedHemfPlusRecord.class), + drawDriverstring(0x4036, UnimplementedHemfPlusRecord.class), + strokeFillPath(0x4037, UnimplementedHemfPlusRecord.class), + serializableObject(0x4038, UnimplementedHemfPlusRecord.class), + setTSGraphics(0x4039, UnimplementedHemfPlusRecord.class), + setTSClip(0x403A, UnimplementedHemfPlusRecord.class); + + public final long id; + public final Class clazz; + + HemfPlusRecordType(long id, Class clazz) { + this.id = id; + this.clazz = clazz; + } + + public static HemfPlusRecordType getById(long id) { + for (HemfPlusRecordType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java new file mode 100644 index 000000000..7e3cbcff4 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java @@ -0,0 +1,53 @@ +/* ==================================================================== + 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.hemfplus.record; + + +import java.io.IOException; + +import org.apache.poi.util.Internal; + +@Internal +public class UnimplementedHemfPlusRecord implements HemfPlusRecord { + + private int recordId; + private int flags; + private byte[] recordBytes; + + @Override + public HemfPlusRecordType getRecordType() { + return HemfPlusRecordType.getById(recordId); + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public void init(byte[] recordBytes, int recordId, int flags) throws IOException { + this.recordId = recordId; + this.flags = flags; + this.recordBytes = recordBytes; + } + + public byte[] getRecordBytes() { + //should probably defensively return a copy. + return recordBytes; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java new file mode 100644 index 000000000..7ffff6b01 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java @@ -0,0 +1,39 @@ +/* ==================================================================== + 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; + +import org.apache.poi.util.Internal; + +/** + * Syntactic utility to allow for four different + * comment classes + */ +@Internal +public abstract class AbstractHemfComment { + + private final byte[] rawBytes; + + public AbstractHemfComment(byte[] rawBytes) { + this.rawBytes = rawBytes; + } + + public byte[] getRawBytes() { + return rawBytes; + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java new file mode 100644 index 000000000..5d45927bb --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java @@ -0,0 +1,31 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hemf.record; + +import org.apache.poi.util.Internal; + +/** + * Contains arbitrary data + */ +@Internal +public class HemfComment extends AbstractHemfComment { + + public HemfComment(byte[] rawBytes) { + super(rawBytes); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java new file mode 100644 index 000000000..b32bf5481 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java @@ -0,0 +1,107 @@ +/* ==================================================================== + 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.hemf.hemfplus.record.HemfPlusRecord; +import org.apache.poi.hemf.hemfplus.record.HemfPlusRecordType; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.RecordFormatException; + +/** + * An HemfCommentEMFPlus may contain one or more EMFPlus records + */ +@Internal +public class HemfCommentEMFPlus extends AbstractHemfComment { + + long dataSize; + public HemfCommentEMFPlus(byte[] rawBytes) { + //these rawBytes contain only the EMFPlusRecord(s?) + //the EmfComment type, size, datasize and comment identifier have all been stripped. + //The EmfPlus type, flags, size, data size should start at rawBytes[0] + super(rawBytes); + + } + + public List getRecords() { + return HemfPlusParser.parse(getRawBytes()); + } + + private static class HemfPlusParser { + + public static List parse(byte[] bytes) { + List records = new ArrayList(); + int offset = 0; + while (offset < bytes.length) { + if (offset + 12 > bytes.length) { + //if header will go beyond bytes, stop now + //TODO: log or throw + break; + } + int type = LittleEndian.getUShort(bytes, offset); offset += LittleEndian.SHORT_SIZE; + int flags = LittleEndian.getUShort(bytes, offset); offset += LittleEndian.SHORT_SIZE; + long sizeLong = LittleEndian.getUInt(bytes, offset); offset += LittleEndian.INT_SIZE; + if (sizeLong >= Integer.MAX_VALUE) { + throw new RecordFormatException("size of emf record >= Integer.MAX_VALUE"); + } + int size = (int)sizeLong; + long dataSizeLong = LittleEndian.getUInt(bytes, offset); offset += LittleEndian.INT_SIZE; + if (dataSizeLong >= Integer.MAX_VALUE) { + throw new RuntimeException("data size of emfplus record cannot be >= Integer.MAX_VALUE"); + } + int dataSize = (int)dataSizeLong; + if (dataSize + offset > bytes.length) { + //TODO: log or throw? + break; + } + HemfPlusRecord record = buildRecord(type, flags, dataSize, offset, bytes); + records.add(record); + offset += dataSize; + } + return records; + } + + private static HemfPlusRecord buildRecord(int recordId, int flags, int size, int offset, byte[] bytes) { + HemfPlusRecord record = null; + HemfPlusRecordType type = HemfPlusRecordType.getById(recordId); + if (type == null) { + throw new RuntimeException("Undefined record of type:"+recordId); + } + try { + record = type.clazz.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + byte[] dataBytes = new byte[size]; + System.arraycopy(bytes, offset, dataBytes, 0, size); + try { + record.init(dataBytes, recordId, flags); + } catch (IOException e) { + throw new RuntimeException(e); + } + return record; + + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java new file mode 100644 index 000000000..009974d10 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java @@ -0,0 +1,31 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hemf.record; + +import org.apache.poi.util.Internal; + +/** + * Not yet implemented + */ +@Internal +public class HemfCommentEMFSpool extends AbstractHemfComment { + + public HemfCommentEMFSpool(byte[] rawBytes) { + super(rawBytes); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java new file mode 100644 index 000000000..cb0447619 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java @@ -0,0 +1,176 @@ +/* ==================================================================== + 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; + + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.RecordFormatException; + +/** + * Container class for four subtypes of HemfCommentPublic: BeginGroup, EndGroup, MultiFormats + * and Windows Metafile. + */ +@Internal +public class HemfCommentPublic { + + /** + * Stub, to be implemented + */ + public static class BeginGroup extends AbstractHemfComment { + + public BeginGroup(byte[] rawBytes) { + super(rawBytes); + } + + } + + /** + * Stub, to be implemented + */ + public static class EndGroup extends AbstractHemfComment { + + public EndGroup(byte[] rawBytes) { + super(rawBytes); + } + } + + public static class MultiFormats extends AbstractHemfComment { + + public MultiFormats(byte[] rawBytes) { + super(rawBytes); + } + + /** + * + * @return a list of HemfMultFormatsData + */ + public List getData() { + + byte[] rawBytes = getRawBytes(); + //note that raw bytes includes the public comment identifier + int currentOffset = 4 + 16;//4 public comment identifier, 16 for outputrect + long countFormats = LittleEndian.getUInt(rawBytes, currentOffset); + currentOffset += LittleEndian.INT_SIZE; + List emrFormatList = new ArrayList(); + for (long i = 0; i < countFormats; i++) { + emrFormatList.add(new EmrFormat(rawBytes, currentOffset)); + currentOffset += 4 * LittleEndian.INT_SIZE; + } + List list = new ArrayList(); + for (EmrFormat emrFormat : emrFormatList) { + byte[] data = new byte[emrFormat.size]; + System.arraycopy(rawBytes, emrFormat.offset-4, data, 0, emrFormat.size); + list.add(new HemfMultiFormatsData(emrFormat.signature, emrFormat.version, data)); + } + return list; + } + + private class EmrFormat { + long signature; + long version; + int size; + int offset; + + public EmrFormat(byte[] rawBytes, int currentOffset) { + signature = LittleEndian.getUInt(rawBytes, currentOffset); currentOffset += LittleEndian.INT_SIZE; + version = LittleEndian.getUInt(rawBytes, currentOffset); currentOffset += LittleEndian.INT_SIZE; + //spec says this must be a 32bit "aligned" typo for "signed"? + //realistically, this has to be an int... + size = LittleEndian.getInt(rawBytes, currentOffset); currentOffset += LittleEndian.INT_SIZE; + //y, can be long, but realistically? + offset = LittleEndian.getInt(rawBytes, currentOffset); currentOffset += LittleEndian.INT_SIZE; + if (size < 0) { + throw new RecordFormatException("size for emrformat must be > 0"); + } + if (offset < 0) { + throw new RecordFormatException("offset for emrformat must be > 0"); + } + } + } + } + + /** + * Stub, to be implemented + */ + public static class WindowsMetafile extends AbstractHemfComment { + + private final byte[] wmfBytes; + public WindowsMetafile(byte[] rawBytes) { + super(rawBytes); + int offset = LittleEndian.INT_SIZE;//public comment identifier + int version = LittleEndian.getUShort(rawBytes, offset); offset += LittleEndian.SHORT_SIZE; + int reserved = LittleEndian.getUShort(rawBytes, offset); offset += LittleEndian.SHORT_SIZE; + offset += LittleEndian.INT_SIZE; //checksum + offset += LittleEndian.INT_SIZE; //flags + long winMetafileSizeLong = LittleEndian.getUInt(rawBytes, offset); offset += LittleEndian.INT_SIZE; + if (winMetafileSizeLong == 0L) { + wmfBytes = new byte[0]; + return; + } + if (winMetafileSizeLong > Integer.MAX_VALUE) { + throw new RecordFormatException("Metafile record length can't be > Integer.MAX_VALUE"); + } + int winMetafileSize = (int)winMetafileSizeLong; + wmfBytes = new byte[winMetafileSize]; + System.arraycopy(rawBytes, offset, wmfBytes, 0, winMetafileSize); + } + + /** + * + * @return an InputStream for the embedded WMF file + */ + public InputStream getWmfInputStream() { + return new ByteArrayInputStream(wmfBytes); + } + } + + /** + * This encapulates a single record stored within + * a HemfCommentPublic.MultiFormats record. + */ + public static class HemfMultiFormatsData { + + long signature; + long version; + byte[] data; + + public HemfMultiFormatsData(long signature, long version, byte[] data) { + this.signature = signature; + this.version = version; + this.data = data; + } + + public long getSignature() { + return signature; + } + + public long getVersion() { + return version; + } + + public byte[] getData() { + return data; + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java new file mode 100644 index 000000000..51efb2f2b --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java @@ -0,0 +1,139 @@ +/* ==================================================================== + 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; + + +import java.io.IOException; + +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RecordFormatException; + +/** + * This is the outer comment record that is recognized + * by the initial parse by {@link HemfRecordType#comment}. + * However, there are four types of comment: EMR_COMMENT, + * EMR_COMMENT_EMFPLUS, EMR_COMMENT_EMFSPOOL, and EMF_COMMENT_PUBLIC. + * To get the underlying comment, call {@link #getComment()}. + * + */ +@Internal +public class HemfCommentRecord implements HemfRecord { + + public final static long COMMENT_EMFSPOOL = 0x00000000; + public final static long COMMENT_EMFPLUS = 0x2B464D45; + public final static long COMMENT_PUBLIC = 0x43494447; + + + private AbstractHemfComment comment; + @Override + public HemfRecordType getRecordType() { + return HemfRecordType.comment; + } + + @Override + public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { + long dataSize = leis.readUInt(); recordSize -= LittleEndian.INT_SIZE; + + byte[] optionalCommentIndentifierBuffer = new byte[4]; + leis.readFully(optionalCommentIndentifierBuffer); + dataSize = dataSize-LittleEndian.INT_SIZE; //size minus the first int which could be a comment identifier + recordSize -= LittleEndian.INT_SIZE; + long optionalCommentIdentifier = LittleEndian.getInt(optionalCommentIndentifierBuffer) & 0x00FFFFFFFFL; + if (optionalCommentIdentifier == COMMENT_EMFSPOOL) { + comment = new HemfCommentEMFSpool(readToByteArray(leis, dataSize, recordSize)); + } else if (optionalCommentIdentifier == COMMENT_EMFPLUS) { + comment = new HemfCommentEMFPlus(readToByteArray(leis, dataSize, recordSize)); + } else if (optionalCommentIdentifier == COMMENT_PUBLIC) { + comment = CommentPublicParser.parse(readToByteArray(leis, dataSize, recordSize)); + } else { + comment = new HemfComment(readToByteArray(optionalCommentIndentifierBuffer, leis, dataSize, recordSize)); + } + + return recordSize; + } + + //this prepends the initial "int" which turned out NOT to be + //a signifier of emfplus, spool, public. + private byte[] readToByteArray(byte[] initialBytes, LittleEndianInputStream leis, + long remainingDataSize, long remainingRecordSize) throws IOException { + if (remainingDataSize > Integer.MAX_VALUE) { + throw new RecordFormatException("Data size can't be > Integer.MAX_VALUE"); + } + + if (remainingRecordSize > Integer.MAX_VALUE) { + throw new RecordFormatException("Record size can't be > Integer.MAX_VALUE"); + } + if (remainingRecordSize == 0) { + return new byte[0]; + } + + int dataSize = (int)remainingDataSize; + int recordSize = (int)remainingRecordSize; + byte[] arr = new byte[dataSize+initialBytes.length]; + System.arraycopy(initialBytes,0,arr, 0, initialBytes.length); + IOUtils.readFully(leis, arr, initialBytes.length, dataSize); + IOUtils.skipFully(leis, recordSize-dataSize); + + return arr; + } + + private byte[] readToByteArray(LittleEndianInputStream leis, long dataSize, long recordSize) throws IOException { + assert dataSize < Integer.MAX_VALUE; + + if (recordSize == 0) { + return new byte[0]; + } + + byte[] arr = new byte[(int)dataSize]; + IOUtils.readFully(leis, arr); + IOUtils.skipFully(leis, recordSize-dataSize); + return arr; + } + + public AbstractHemfComment getComment() { + return comment; + } + + private static class CommentPublicParser { + private static final long WINDOWS_METAFILE = 0x80000001L; //wmf + private static final long BEGINGROUP = 0x00000002; //beginning of a group of drawing records + private static final long ENDGROUP = 0x00000003; //end of a group of drawing records + private static final long MULTIFORMATS = 0x40000004; //allows multiple definitions of an image, including encapsulated postscript + private static final long UNICODE_STRING = 0x00000040; //reserved. must not be used + private static final long UNICODE_END = 0x00000080; //reserved, must not be used + + private static AbstractHemfComment parse(byte[] bytes) { + long publicCommentIdentifier = LittleEndian.getUInt(bytes, 0); + if (publicCommentIdentifier == WINDOWS_METAFILE) { + return new HemfCommentPublic.WindowsMetafile(bytes); + } else if (publicCommentIdentifier == BEGINGROUP) { + return new HemfCommentPublic.BeginGroup(bytes); + } else if (publicCommentIdentifier == ENDGROUP) { + return new HemfCommentPublic.EndGroup(bytes); + } else if (publicCommentIdentifier == MULTIFORMATS) { + return new HemfCommentPublic.MultiFormats(bytes); + } else if (publicCommentIdentifier == UNICODE_STRING || publicCommentIdentifier == UNICODE_END) { + throw new RuntimeException("UNICODE_STRING/UNICODE_END values are reserved in CommentPublic records"); + } + throw new RuntimeException("Unrecognized public comment type:" +publicCommentIdentifier + " ; " + WINDOWS_METAFILE); + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java new file mode 100644 index 000000000..a23f4fdae --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java @@ -0,0 +1,198 @@ +/* ==================================================================== + 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; + +import java.awt.Rectangle; +import java.io.IOException; + +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianInputStream; + +/** + * Extracts the full header from EMF files. + * @see org.apache.poi.sl.image.ImageHeaderEMF + */ +@Internal +public class HemfHeader implements HemfRecord { + + private Rectangle boundsRectangle; + private Rectangle frameRectangle; + private long bytes; + private long records; + private int handles; + private long nDescription; + private long offDescription; + private long nPalEntries; + private boolean hasExtension1; + private long cbPixelFormat; + private long offPixelFormat; + private long bOpenGL; + private boolean hasExtension2; + private long micrometersX; + private long micrometersY; + + public Rectangle getBoundsRectangle() { + return boundsRectangle; + } + + public Rectangle getFrameRectangle() { + return frameRectangle; + } + + public long getBytes() { + return bytes; + } + + public long getRecords() { + return records; + } + + public int getHandles() { + return handles; + } + + public long getnDescription() { + return nDescription; + } + + public long getOffDescription() { + return offDescription; + } + + public long getnPalEntries() { + return nPalEntries; + } + + public boolean isHasExtension1() { + return hasExtension1; + } + + public long getCbPixelFormat() { + return cbPixelFormat; + } + + public long getOffPixelFormat() { + return offPixelFormat; + } + + public long getbOpenGL() { + return bOpenGL; + } + + public boolean isHasExtension2() { + return hasExtension2; + } + + public long getMicrometersX() { + return micrometersX; + } + + public long getMicrometersY() { + return micrometersY; + } + + @Override + public String toString() { + return "HemfHeader{" + + "boundsRectangle=" + boundsRectangle + + ", frameRectangle=" + frameRectangle + + ", bytes=" + bytes + + ", records=" + records + + ", handles=" + handles + + ", nDescription=" + nDescription + + ", offDescription=" + offDescription + + ", nPalEntries=" + nPalEntries + + ", hasExtension1=" + hasExtension1 + + ", cbPixelFormat=" + cbPixelFormat + + ", offPixelFormat=" + offPixelFormat + + ", bOpenGL=" + bOpenGL + + ", hasExtension2=" + hasExtension2 + + ", micrometersX=" + micrometersX + + ", micrometersY=" + micrometersY + + '}'; + } + + @Override + public HemfRecordType getRecordType() { + return HemfRecordType.header; + } + + @Override + public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { + if (recordId != 1L) { + throw new IOException("Not a valid EMF header. Record type:"+recordId); + } + //read the record--id and size (2 bytes) have already been read + byte[] data = new byte[(int)recordSize]; + IOUtils.readFully(leis, data); + + int offset = 0; + + //bounds + int boundsLeft = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int boundsTop = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int boundsRight = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int boundsBottom = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + boundsRectangle = new Rectangle(boundsLeft, boundsTop, + boundsRight - boundsLeft, boundsBottom - boundsTop); + + int frameLeft = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int frameTop = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int frameRight = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + int frameBottom = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + frameRectangle = new Rectangle(frameLeft, frameTop, + frameRight - frameLeft, frameBottom - frameTop); + + long recordSignature = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + if (recordSignature != 0x464D4520) { + throw new IOException("bad record signature: " + recordSignature); + } + + long version = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; + //According to the spec, MSOffice doesn't pay attention to this value. + //It _should_ be 0x00010000 + bytes = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + records = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + handles = LittleEndian.getUShort(data, offset);offset += LittleEndian.SHORT_SIZE; + offset += LittleEndian.SHORT_SIZE;//reserved + nDescription = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + offDescription = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + nPalEntries = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + + //should be skips + offset += 8;//device + offset += 8;//millimeters + + + if (recordSize+8 >= 100) { + hasExtension1 = true; + cbPixelFormat = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + offPixelFormat = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + bOpenGL= LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + } + + if (recordSize+8 >= 108) { + hasExtension2 = true; + micrometersX = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + micrometersY = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; + } + return recordSize; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java new file mode 100644 index 000000000..de1271e69 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java @@ -0,0 +1,41 @@ +/* ==================================================================== + 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; + + +import java.io.IOException; + +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; + +@Internal +public interface HemfRecord { + + HemfRecordType getRecordType(); + + /** + * Init record from stream + * + * @param leis the little endian input stream + * @return count of processed bytes + * @throws IOException + */ + long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException; + + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java new file mode 100644 index 000000000..b1c585711 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java @@ -0,0 +1,159 @@ +/* ==================================================================== + 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; + +import org.apache.poi.util.Internal; + +@Internal +public enum HemfRecordType { + + header(0x00000001, UnimplementedHemfRecord.class), + polybeizer(0x00000002, UnimplementedHemfRecord.class), + polygon(0x00000003, UnimplementedHemfRecord.class), + polyline(0x00000004, UnimplementedHemfRecord.class), + polybezierto(0x00000005, UnimplementedHemfRecord.class), + polylineto(0x00000006, UnimplementedHemfRecord.class), + polypolyline(0x00000007, UnimplementedHemfRecord.class), + polypolygon(0x00000008, UnimplementedHemfRecord.class), + setwindowextex(0x00000009, UnimplementedHemfRecord.class), + setwindoworgex(0x0000000A, UnimplementedHemfRecord.class), + setviewportextex(0x0000000B, UnimplementedHemfRecord.class), + setviewportorgex(0x0000000C, UnimplementedHemfRecord.class), + setbrushorgex(0x0000000D, UnimplementedHemfRecord.class), + eof(0x0000000E, UnimplementedHemfRecord.class), + setpixelv(0x0000000F, UnimplementedHemfRecord.class), + setmapperflags(0x00000010, UnimplementedHemfRecord.class), + setmapmode(0x00000011, UnimplementedHemfRecord.class), + setbkmode(0x00000012, UnimplementedHemfRecord.class), + setpolyfillmode(0x00000013, UnimplementedHemfRecord.class), + setrop2(0x00000014, UnimplementedHemfRecord.class), + setstretchbltmode(0x00000015, UnimplementedHemfRecord.class), + settextalign(0x00000016, HemfText.SetTextAlign.class), + setcoloradjustment(0x00000017, UnimplementedHemfRecord.class), + settextcolor(0x00000018, HemfText.SetTextColor.class), + setbkcolor(0x00000019, UnimplementedHemfRecord.class), + setoffsetcliprgn(0x0000001A, UnimplementedHemfRecord.class), + setmovetoex(0x0000001B, UnimplementedHemfRecord.class), + setmetargn(0x0000001C, UnimplementedHemfRecord.class), + setexcludecliprect(0x0000001D, UnimplementedHemfRecord.class), + setintersectcliprect(0x0000001E, UnimplementedHemfRecord.class), + scaleviewportextex(0x0000001F, UnimplementedHemfRecord.class), + scalewindowextex(0x00000020, UnimplementedHemfRecord.class), + savedc(0x00000021, UnimplementedHemfRecord.class), + restoredc(0x00000022, UnimplementedHemfRecord.class), + setworldtransform(0x00000023, UnimplementedHemfRecord.class), + modifyworldtransform(0x00000024, UnimplementedHemfRecord.class), + selectobject(0x00000025, UnimplementedHemfRecord.class), + createpen(0x00000026, UnimplementedHemfRecord.class), + createbrushindirect(0x00000027, UnimplementedHemfRecord.class), + deleteobject(0x00000028, UnimplementedHemfRecord.class), + anglearc(0x00000029, UnimplementedHemfRecord.class), + ellipse(0x0000002A, UnimplementedHemfRecord.class), + rectangle(0x0000002B, UnimplementedHemfRecord.class), + roundirect(0x0000002C, UnimplementedHemfRecord.class), + arc(0x0000002D, UnimplementedHemfRecord.class), + chord(0x0000002E, UnimplementedHemfRecord.class), + pie(0x0000002F, UnimplementedHemfRecord.class), + selectpalette(0x00000030, UnimplementedHemfRecord.class), + createpalette(0x00000031, UnimplementedHemfRecord.class), + setpaletteentries(0x00000032, UnimplementedHemfRecord.class), + resizepalette(0x00000033, UnimplementedHemfRecord.class), + realizepalette(0x0000034, UnimplementedHemfRecord.class), + extfloodfill(0x00000035, UnimplementedHemfRecord.class), + lineto(0x00000036, UnimplementedHemfRecord.class), + arcto(0x00000037, UnimplementedHemfRecord.class), + polydraw(0x00000038, UnimplementedHemfRecord.class), + setarcdirection(0x00000039, UnimplementedHemfRecord.class), + setmiterlimit(0x0000003A, UnimplementedHemfRecord.class), + beginpath(0x0000003B, UnimplementedHemfRecord.class), + endpath(0x0000003C, UnimplementedHemfRecord.class), + closefigure(0x0000003D, UnimplementedHemfRecord.class), + fillpath(0x0000003E, UnimplementedHemfRecord.class), + strokeandfillpath(0x0000003F, UnimplementedHemfRecord.class), + strokepath(0x00000040, UnimplementedHemfRecord.class), + flattenpath(0x00000041, UnimplementedHemfRecord.class), + widenpath(0x00000042, UnimplementedHemfRecord.class), + selectclippath(0x00000043, UnimplementedHemfRecord.class), + abortpath(0x00000044, UnimplementedHemfRecord.class), //no 45?! + comment(0x00000046, HemfCommentRecord.class), + fillrgn(0x00000047, UnimplementedHemfRecord.class), + framergn(0x00000048, UnimplementedHemfRecord.class), + invertrgn(0x00000049, UnimplementedHemfRecord.class), + paintrgn(0x0000004A, UnimplementedHemfRecord.class), + extselectciprrgn(0x0000004B, UnimplementedHemfRecord.class), + bitblt(0x0000004C, UnimplementedHemfRecord.class), + stretchblt(0x0000004D, UnimplementedHemfRecord.class), + maskblt(0x0000004E, UnimplementedHemfRecord.class), + plgblt(0x0000004F, UnimplementedHemfRecord.class), + setbitstodevice(0x00000050, UnimplementedHemfRecord.class), + stretchdibits(0x00000051, UnimplementedHemfRecord.class), + extcreatefontindirectw(0x00000052, HemfText.ExtCreateFontIndirectW.class), + exttextouta(0x00000053, HemfText.ExtTextOutA.class), + exttextoutw(0x00000054, HemfText.ExtTextOutW.class), + polybezier16(0x00000055, UnimplementedHemfRecord.class), + polygon16(0x00000056, UnimplementedHemfRecord.class), + polyline16(0x00000057, UnimplementedHemfRecord.class), + polybezierto16(0x00000058, UnimplementedHemfRecord.class), + polylineto16(0x00000059, UnimplementedHemfRecord.class), + polypolyline16(0x0000005A, UnimplementedHemfRecord.class), + polypolygon16(0x0000005B, UnimplementedHemfRecord.class), + polydraw16(0x0000005C, UnimplementedHemfRecord.class), + createmonobrush16(0x0000005D, UnimplementedHemfRecord.class), + createdibpatternbrushpt(0x0000005E, UnimplementedHemfRecord.class), + extcreatepen(0x0000005F, UnimplementedHemfRecord.class), + polytextouta(0x00000060, HemfText.PolyTextOutA.class), + polytextoutw(0x00000061, HemfText.PolyTextOutW.class), + seticmmode(0x00000062, UnimplementedHemfRecord.class), + createcolorspace(0x00000063, UnimplementedHemfRecord.class), + setcolorspace(0x00000064, UnimplementedHemfRecord.class), + deletecolorspace(0x00000065, UnimplementedHemfRecord.class), + glsrecord(0x00000066, UnimplementedHemfRecord.class), + glsboundedrecord(0x00000067, UnimplementedHemfRecord.class), + pixelformat(0x00000068, UnimplementedHemfRecord.class), + drawescape(0x00000069, UnimplementedHemfRecord.class), + extescape(0x0000006A, UnimplementedHemfRecord.class),//no 6b?! + smalltextout(0x0000006C, UnimplementedHemfRecord.class), + forceufimapping(0x0000006D, UnimplementedHemfRecord.class), + namedescape(0x0000006E, UnimplementedHemfRecord.class), + colorcorrectpalette(0x0000006F, UnimplementedHemfRecord.class), + seticmprofilea(0x00000070, UnimplementedHemfRecord.class), + seticmprofilew(0x00000071, UnimplementedHemfRecord.class), + alphablend(0x00000072, UnimplementedHemfRecord.class), + setlayout(0x00000073, UnimplementedHemfRecord.class), + transparentblt(0x00000074, UnimplementedHemfRecord.class), + gradientfill(0x00000076, UnimplementedHemfRecord.class), //no 75?! + setlinkdufis(0x00000077, UnimplementedHemfRecord.class), + settextjustification(0x00000078, HemfText.SetTextJustification.class), + colormatchtargetw(0x00000079, UnimplementedHemfRecord.class), + createcolorspacew(0x0000007A, UnimplementedHemfRecord.class); + + public final long id; + public final Class clazz; + + HemfRecordType(long id, Class clazz) { + this.id = id; + this.clazz = clazz; + } + + public static HemfRecordType getById(long id) { + for (HemfRecordType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java new file mode 100644 index 000000000..d46814b92 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java @@ -0,0 +1,262 @@ +/* ==================================================================== + 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; + + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; +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 +public class HemfText { + + private final static Charset UTF16LE = Charset.forName("UTF-16LE"); + + public static class ExtCreateFontIndirectW extends UnimplementedHemfRecord { + } + + public static class ExtTextOutA implements HemfRecord { + + private long left,top,right,bottom; + + //TODO: translate this to a graphicsmode enum + private long graphicsMode; + + private long exScale; + private long eyScale; + EmrTextObject textObject; + + @Override + public HemfRecordType getRecordType() { + return HemfRecordType.exttextouta; + } + + @Override + public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { + //note that the first 2 uInts have been read off and the recordsize has + //been decreased by 8 + left = leis.readInt(); + top = leis.readInt(); + right = leis.readInt(); + bottom = leis.readInt(); + graphicsMode = leis.readUInt(); + exScale = leis.readUInt(); + eyScale = leis.readUInt(); + + int recordSizeInt = -1; + if (recordSize < Integer.MAX_VALUE) { + recordSizeInt = (int)recordSize; + } else { + throw new RecordFormatException("can't have text length > Integer.MAX_VALUE"); + } + //guarantee to read the rest of the EMRTextObjectRecord + //emrtextbytes start after 7*4 bytes read above + byte[] emrTextBytes = new byte[recordSizeInt-(7*LittleEndian.INT_SIZE)]; + IOUtils.readFully(leis, emrTextBytes); + textObject = new EmrTextObject(emrTextBytes, getEncodingHint(), 20);//should be 28, but recordSizeInt has already subtracted 8 + return recordSize; + } + + protected Charset getEncodingHint() { + return null; + } + + /** + * + * To be implemented! We need to get the current character set + * from the current font for {@link ExtTextOutA}, + * which has to be tracked in the playback device. + * + * For {@link ExtTextOutW}, the charset is "UTF-16LE" + * + * @param charset the charset to be used to decode the character bytes + * @return + * @throws IOException + */ + public String getText(Charset charset) throws IOException { + return textObject.getText(charset); + } + + /** + * + * @return the x offset for the EmrTextObject + */ + public long getX() { + return textObject.x; + } + + /** + * + * @return the y offset for the EmrTextObject + */ + public long getY() { + return textObject.y; + } + + public long getLeft() { + return left; + } + + public long getTop() { + return top; + } + + public long getRight() { + return right; + } + + public long getBottom() { + return bottom; + } + + public long getGraphicsMode() { + return graphicsMode; + } + + public long getExScale() { + return exScale; + } + + public long getEyScale() { + return eyScale; + } + + } + + public static class ExtTextOutW extends ExtTextOutA { + + @Override + public HemfRecordType getRecordType() { + return HemfRecordType.exttextoutw; + } + + @Override + protected Charset getEncodingHint() { + return UTF16LE; + } + + public String getText() throws IOException { + return getText(UTF16LE); + } + } + + /** + * 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 { + + } + + public static class SetTextAlign extends UnimplementedHemfRecord { + } + + public static class SetTextColor extends UnimplementedHemfRecord { + } + + + public static class SetTextJustification extends UnimplementedHemfRecord { + + } + + private static class EmrTextObject { + long x; + long y; + int numChars; + byte[] rawTextBytes;//this stores _all_ of the bytes to the end of the EMRTextObject record. + //Because of potential variable length encodings, must + //carefully read only the numChars from this byte array. + + EmrTextObject(byte[] emrTextObjBytes, Charset charsetHint, int readSoFar) throws IOException { + + int offset = 0; + x = LittleEndian.getUInt(emrTextObjBytes, offset); offset+= LittleEndian.INT_SIZE; + y = LittleEndian.getUInt(emrTextObjBytes, offset); offset+= LittleEndian.INT_SIZE; + long numCharsLong = LittleEndian.getUInt(emrTextObjBytes, offset); offset += LittleEndian.INT_SIZE; + long offString = LittleEndian.getUInt(emrTextObjBytes, offset); offset += LittleEndian.INT_SIZE; + int start = (int)offString-offset-readSoFar; + + if (numCharsLong == 0) { + rawTextBytes = new byte[0]; + numChars = 0; + return; + } + if (numCharsLong > Integer.MAX_VALUE) { + throw new RecordFormatException("Number of characters can't be > Integer.MAX_VALUE"); + } + numChars = (int)numCharsLong; + rawTextBytes = new byte[emrTextObjBytes.length-start]; + System.arraycopy(emrTextObjBytes, start, rawTextBytes, 0, emrTextObjBytes.length-start); + } + + String getText(Charset charset) throws IOException { + StringBuilder sb = new StringBuilder(); + Reader r = null; + try { + r = new InputStreamReader(new ByteArrayInputStream(rawTextBytes), charset); + for (int i = 0; i < numChars; i++) { + sb.appendCodePoint(readCodePoint(r)); + } + } finally { + IOUtils.closeQuietly(r); + } + return sb.toString(); + } + + //TODO: move this to IOUtils? + private int readCodePoint(Reader r) throws IOException { + int c1 = r.read(); + if (c1 == -1) { + throw new EOFException("Tried to read beyond byte array"); + } + if (!Character.isHighSurrogate((char)c1)) { + return c1; + } + int c2 = r.read(); + if (c2 == -1) { + throw new EOFException("Tried to read beyond byte array"); + } + if (!Character.isLowSurrogate((char)c2)) { + throw new RecordFormatException("Expected low surrogate after high surrogate"); + } + return Character.toCodePoint((char)c1, (char)c2); + } + } + + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java new file mode 100644 index 000000000..a951e0ec5 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java @@ -0,0 +1,49 @@ +/* ==================================================================== + 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; + + +import java.io.IOException; + +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; + +@Internal +public class UnimplementedHemfRecord implements HemfRecord { + + private long recordId; + public UnimplementedHemfRecord() { + + } + + @Override + public HemfRecordType getRecordType() { + return HemfRecordType.getById(recordId); + } + + @Override + public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { + this.recordId = recordId; + long skipped = IOUtils.skipFully(leis, recordSize); + if (skipped < 0) { + throw new IOException("End of stream reached before record read"); + } + return skipped; + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java new file mode 100644 index 000000000..0849e23ab --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java @@ -0,0 +1,167 @@ +/* ==================================================================== + 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.extractor; + +import static org.apache.poi.POITestCase.assertContains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.hemf.record.AbstractHemfComment; +import org.apache.poi.hemf.record.HemfCommentPublic; +import org.apache.poi.hemf.record.HemfCommentRecord; +import org.apache.poi.hemf.record.HemfHeader; +import org.apache.poi.hemf.record.HemfRecord; +import org.apache.poi.hemf.record.HemfRecordType; +import org.apache.poi.hemf.record.HemfText; +import org.junit.Test; + +public class HemfExtractorTest { + + @Test + public void testBasicWindows() throws Exception { + InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_windows.emf"); + HemfExtractor ex = new HemfExtractor(is); + HemfHeader header = ex.getHeader(); + assertEquals(27864, header.getBytes()); + assertEquals(31, header.getRecords()); + assertEquals(3, header.getHandles()); + assertEquals(346000, header.getMicrometersX()); + assertEquals(194000, header.getMicrometersY()); + + int records = 0; + for (HemfRecord record : ex) { + records++; + } + + assertEquals(header.getRecords() - 1, records); + } + + @Test + public void testBasicMac() throws Exception { + InputStream is = + POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_mac.emf"); + HemfExtractor ex = new HemfExtractor(is); + HemfHeader header = ex.getHeader(); + + int records = 0; + boolean extractedData = false; + for (HemfRecord record : ex) { + if (record.getRecordType() == HemfRecordType.comment) { + AbstractHemfComment comment = ((HemfCommentRecord) record).getComment(); + if (comment instanceof HemfCommentPublic.MultiFormats) { + for (HemfCommentPublic.HemfMultiFormatsData d : ((HemfCommentPublic.MultiFormats) comment).getData()) { + byte[] data = d.getData(); + //make sure header starts at 0 + assertEquals('%', data[0]); + assertEquals('P', data[1]); + assertEquals('D', data[2]); + assertEquals('F', data[3]); + + //make sure byte array ends at EOF\n + assertEquals('E', data[data.length - 4]); + assertEquals('O', data[data.length - 3]); + assertEquals('F', data[data.length - 2]); + assertEquals('\n', data[data.length - 1]); + extractedData = true; + } + } + } + records++; + } + assertTrue(extractedData); + assertEquals(header.getRecords() - 1, records); + } + + @Test + public void testMacText() throws Exception { + InputStream is = + POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_mac.emf"); + HemfExtractor ex = new HemfExtractor(is); + + long lastY = -1; + long lastX = -1; + long fudgeFactorX = 1000;//derive this from the font information! + StringBuilder sb = new StringBuilder(); + for (HemfRecord record : ex) { + if (record.getRecordType().equals(HemfRecordType.exttextoutw)) { + HemfText.ExtTextOutW extTextOutW = (HemfText.ExtTextOutW) record; + if (lastY > -1 && lastY != extTextOutW.getY()) { + sb.append("\n"); + lastX = -1; + } + if (lastX > -1 && extTextOutW.getX() - lastX > fudgeFactorX) { + sb.append(" "); + } + sb.append(extTextOutW.getText()); + lastY = extTextOutW.getY(); + lastX = extTextOutW.getX(); + } + } + String txt = sb.toString(); + assertContains(txt, "Tika http://incubator.apache.org"); + assertContains(txt, "Latest News\n"); + } + + @Test + public void testWindowsText() throws Exception { + InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_windows.emf"); + HemfExtractor ex = new HemfExtractor(is); + long lastY = -1; + long lastX = -1; + long fudgeFactorX = 1000;//derive this from the font or frame/bounds information + StringBuilder sb = new StringBuilder(); + Set expectedParts = new HashSet(); + expectedParts.add("C:\\Users\\tallison\\"); + expectedParts.add("testPDF.pdf"); + int foundExpected = 0; + for (HemfRecord record : ex) { + if (record.getRecordType().equals(HemfRecordType.exttextoutw)) { + HemfText.ExtTextOutW extTextOutW = (HemfText.ExtTextOutW) record; + if (lastY > -1 && lastY != extTextOutW.getY()) { + sb.append("\n"); + lastX = -1; + } + if (lastX > -1 && extTextOutW.getX() - lastX > fudgeFactorX) { + sb.append(" "); + } + String txt = extTextOutW.getText(); + if (expectedParts.contains(txt)) { + foundExpected++; + } + sb.append(txt); + lastY = extTextOutW.getY(); + lastX = extTextOutW.getX(); + } + } + String txt = sb.toString(); + assertContains(txt, "C:\\Users\\tallison\\\n"); + assertContains(txt, "asf2-git-1.x\\tika-\n"); + assertEquals(expectedParts.size(), foundExpected); + } + + /* + govdocs1 064213.doc-0.emf contains an example of extextouta + */ + +} \ No newline at end of file diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java new file mode 100644 index 000000000..c42233ab7 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java @@ -0,0 +1,96 @@ +/* ==================================================================== + 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.hemfplus.extractor; + + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.hemf.extractor.HemfExtractor; +import org.apache.poi.hemf.hemfplus.record.HemfPlusHeader; +import org.apache.poi.hemf.hemfplus.record.HemfPlusRecord; +import org.apache.poi.hemf.hemfplus.record.HemfPlusRecordType; +import org.apache.poi.hemf.record.HemfCommentEMFPlus; +import org.apache.poi.hemf.record.HemfCommentRecord; +import org.apache.poi.hemf.record.HemfRecord; +import org.junit.Test; + +public class HemfPlusExtractorTest { + + @Test + public void testBasic() throws Exception { + //test header + HemfCommentEMFPlus emfPlus = getCommentRecord("SimpleEMF_windows.emf", 0); + List records = emfPlus.getRecords(); + assertEquals(1, records.size()); + assertEquals(HemfPlusRecordType.header, records.get(0).getRecordType()); + + HemfPlusHeader header = (HemfPlusHeader)records.get(0); + assertEquals(240, header.getLogicalDpiX()); + assertEquals(240, header.getLogicalDpiY()); + assertEquals(1, header.getFlags()); + assertEquals(1, header.getEmfPlusFlags()); + + + + //test that the HemfCommentEMFPlus record at offset 1 + //contains 6 HemfCommentEMFPlus records within it + List expected = new ArrayList(); + expected.add(HemfPlusRecordType.setPixelOffsetMode); + expected.add(HemfPlusRecordType.setAntiAliasMode); + expected.add(HemfPlusRecordType.setCompositingQuality); + expected.add(HemfPlusRecordType.setPageTransform); + expected.add(HemfPlusRecordType.setInterpolationMode); + expected.add(HemfPlusRecordType.getDC); + + emfPlus = getCommentRecord("SimpleEMF_windows.emf", 1); + records = emfPlus.getRecords(); + assertEquals(expected.size(), records.size()); + + for (int i = 0; i < expected.size(); i++) { + assertEquals(expected.get(i), records.get(i).getRecordType()); + } + } + + + private HemfCommentEMFPlus getCommentRecord(String testFileName, int recordIndex) throws Exception { + InputStream is = null; + HemfCommentEMFPlus returnRecord = null; + + try { + is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream(testFileName); + HemfExtractor ex = new HemfExtractor(is); + int i = 0; + for (HemfRecord record : ex) { + if (i == recordIndex) { + HemfCommentRecord commentRecord = ((HemfCommentRecord) record); + returnRecord = (HemfCommentEMFPlus) commentRecord.getComment(); + break; + } + i++; + } + } finally { + is.close(); + } + return returnRecord; + } +} diff --git a/test-data/document/testException2.doc-2.wmf b/test-data/document/testException2.doc-2.wmf new file mode 100644 index 0000000000000000000000000000000000000000..914563bf2ba1a95a900db524773a9fed181241bb GIT binary patch literal 4130 zcmc&%&u?2r5T1FlojQ)2v`*?4r|?o*wU<o;7TQ=B5^<^QtC<-{{RwyK##3dam@h^{D{mqvv2)kVyB17=1E_&^JaGT z+nL$fXQn7o+rB+SduV{<2?H@F&d(|`>U18^BbHH!u{^O}l z*G+{!;JcU^ud)zccD2xsRi|l~!z{?~Gzo^;Fmrrc>-Q>0U^nf*wH~+gzt0mS*ud(7 z`wcuj;CG=j3{#<>_!aE-fX{ve((8~UD?^%r6BtSC1nlqG#kk%hd;I~5aiGzEv%Bz) zNVL-!#}rw_T`*R|e-muXTpN)|?u1SI|G+gy}nD9V*QwNy2%mW&vEYb=LCL&cia8!&gVqc&rANO$2r6y z-}fQD)WV!d#AJu|_PE3eE+~$Hhak~(HbK5DiwsttQhbm5L~yL>EP`xX7R2{>L9vbt zLvYWpQ*-}Gg<0Dq-QvTFPdG_}Til1({_XrO*Cw7X<@%L!zMQ$aFtzAW=?w@!$2%UU zGk=uE0dqf}@~ha?dViSdG<^zxcBKn+ncq@m`2y}2k>!3D%R!oiY%P;K!YRcyPO@O6 zYI%;XL$;P3d>b7`@C7=c;C9vW41EUKT25frq~cYcU_r8Kd6tesZgxKjYBh_T%qYs7 zWx;yWS8w-wOTZ$GRl5{rbW_24O|RZWd~EbQdWTmP!|dS;N#y#yI?W~MJiqgl`A_|x zOgLrqO*3QXH&~LA{EOlU>P3*OSA(b(a^KE-tNG&GbPmF^Xh)%b4F)GOsTFpFF0L#s zU!Y-|-*`Jgubua2K6E=?=K* z+?1Os<_b4lUWHcTB)UP-4ds^e?s#FY=<>(V?NePZoe|wgZnlswx^FHo=NG2Dn=XF^ ziypP;qqAahWhHlGY2_m??+)d23st^%VYgrH4$?WXt0k}N{Ro?8S8>oeVmy!f5v*`b zF<(;?(q&A>?U&Ok&gndCo6pg#8+@BbaP||58GOYHCZEG~mV00mX#rPz#)OxRX+k;9 zyuj)u)XHu!5wcyuW-5qS1sytt&XhtFD(KE4n>prpk#vXJ?7oNZ!Yu4spWP7d*gNePp`-4s$!k1640>5LyK;+uI*<9OGndSpx OqgKbcQjy>AME?Tyf0!iz literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/SimpleEMF_mac.emf b/test-data/spreadsheet/SimpleEMF_mac.emf new file mode 100644 index 0000000000000000000000000000000000000000..f7b0ecc8077b8fe741f9693f3155a833886188b9 GIT binary patch literal 133320 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 literal 0 HcmV?d00001