Source code for ged2doc.dumbemf

'''Python module for generating EMF.

Only the most trivial features are implemented, stuff that is required by
ged2doc package.
'''

__all__ = ['EMF', 'BackgroundMode']

import abc
import contextlib
import logging
import math
import struct

from .size import Size


_LOG = logging.getLogger(__name__)


# Record types, only few selected that we need
EMR_HEADER = 0x00000001
EMR_POLYLINE = 0x00000004
EMR_SETWINDOWEXTEX = 0x00000009
EMR_SETWINDOWORGEX = 0x0000000A
EMR_SETVIEWPORTEXTEX = 0x0000000B
EMR_SETVIEWPORTORGEX = 0x0000000C
EMR_EOF = 0x0000000E
EMR_SETMAPMODE = 0x00000011
EMR_SETBKMODE = 0x00000012
EMR_SETPOLYFILLMODE = 0x00000013
EMR_SETROP2 = 0x00000014
EMR_SETTEXTALIGN = 0x00000016
EMR_SETTEXTCOLOR = 0x00000018
EMR_MOVETOEX = 0x0000001B
EMR_SETWORLDTRANSFORM = 0x00000023
EMR_MODIFYWORLDTRANSFORM = 0x00000024
EMR_SELECTOBJECT = 0x00000025
EMR_CREATEPEN = 0x00000026
EMR_DELETEOBJECT = 0x00000028
EMR_RECTANGLE = 0x0000002B
EMR_GDICOMMENT = 0x00000046
EMR_LINETO = 0x00000036
EMR_ARCTO = 0x00000037
EMR_POLYDRAW = 0x00000038
EMR_SETMITERLIMIT = 0x0000003A
EMR_BEGINPATH = 0x0000003B
EMR_ENDPATH = 0x0000003C
EMR_CLOSEFIGURE = 0x0000003D
EMR_STROKEPATH = 0x00000040
EMR_EXTCREATEFONTINDIRECTW = 0x00000052
EMR_EXTTEXTOUTW = 0x00000054
EMR_EXTCREATEPEN = 0x0000005F
EMR_SETTEXTJUSTIFICATION = 0x00000078

# text alignment
TA_LEFT = 0x0000
TA_TOP = 0x0000
TA_NOUPDATECP = 0x0000
TA_UPDATECP = 0x0001
TA_RIGHT = 0x0002
TA_CENTER = 0x0006
TA_BOTTOM = 0x0008
TA_BASELINE = 0x0018
TA_RTLREADING = 0x0100

GM_COMPATIBLE = 0x00000001
GM_ADVANCED = 0x00000002

# ExtTextOutOptions
ETO_OPAQUE = 0x00000002
ETO_CLIPPED = 0x00000004
ETO_GLYPH_INDEX = 0x00000010
ETO_RTLREADING = 0x00000080
ETO_NO_RECT = 0x00000100
ETO_SMALL_CHARS = 0x00000200
ETO_NUMERICSLOCAL = 0x00000400
ETO_NUMERICSLATIN = 0x00000800
ETO_IGNORELANGUAGE = 0x00001000
ETO_PDY = 0x00002000
ETO_REVERSE_INDEX_MAP = 0x00010000


[docs]class BackgroundMode: TRANSPARENT = 0x0001 OPAQUE = 0x000
class MapMode: MM_TEXT = 0x01 MM_LOMETRIC = 0x02 MM_HIMETRIC = 0x03 MM_LOENGLISH = 0x04 MM_HIENGLISH = 0x05 MM_TWIPS = 0x06 MM_ISOTROPIC = 0x07 MM_ANISOTROPIC = 0x08 class StockObjects: NULL_BRUSH = 0x80000005 NULL_PEN = 0x80000008 DEVICE_DEFAULT_FONT = 0x8000000E class PenStyle: PS_COSMETIC = 0x00000000 PS_ENDCAP_ROUND = 0x00000000 PS_JOIN_ROUND = 0x00000000 PS_SOLID = 0x00000000 PS_DASH = 0x00000001 PS_DOT = 0x00000002 PS_DASHDOT = 0x00000003 PS_DASHDOTDOT = 0x00000004 PS_NULL = 0x00000005 PS_INSIDEFRAME = 0x00000006 PS_USERSTYLE = 0x00000007 PS_ALTERNATE = 0x00000008 PS_ENDCAP_SQUARE = 0x00000100 PS_ENDCAP_FLAT = 0x00000200 PS_JOIN_BEVEL = 0x00001000 PS_JOIN_MITER = 0x00002000 PS_GEOMETRIC = 0x00010000 _pen_styles = { "solid": PenStyle.PS_SOLID | PenStyle.PS_GEOMETRIC, "dash": PenStyle.PS_DASH | PenStyle.PS_GEOMETRIC, "dot": PenStyle.PS_DASH | PenStyle.PS_GEOMETRIC, "dashdot": PenStyle.PS_DASHDOT | PenStyle.PS_GEOMETRIC, "dashdotdot": PenStyle.PS_DASHDOTDOT | PenStyle.PS_GEOMETRIC, } def _pack(*args): """Helper method to simplify struct.pack call. Accepts a list of tuples, each tuple has a characted format code as first element and packed data values as remaining elements. Example: _pack(("I", 1, 2, 3), ("H", 4, 5)) is equivalent to: struct.pack("IIIHH", 1, 2, 3, 4, 5) Parameters ---------- *args : `tuple` Tuple where first item is a string format for `struct.pack` call and remaining items are values to to be packed. """ fmt = "<" values = () for tup in args: tval = tup[1:] fmt += tup[0] * len(tval) values += tval _LOG.debug("_pack: fmt=%s", fmt) return struct.pack(fmt, *values) def _strencode(str, size): encoded = str[:size//2].encode("utf_16_le", "strict") encoded += b"\0" * (size - len(encoded)) return encoded
[docs]class EMF: """Class for EMF, top-level structure. Parameters ---------- width, height : `ged2doc.size.Size` Document width and height, accepts anything convertible to `ged2doc.size.Size`. """ def __init__(self, width, height): self._width = Size(width) self._height = Size(height) self._records = [] # List of all records added so far _LOG.debug("EMF: size = %s x %s (dpi %s x %s)", self._width, self._height, self._width.dpi, self._height.dpi) # self._handles = {} # self._records += [ # GeneralRecord(EMR_SETMAPMODE, ("I", MapMode.MM_TEXT)), # GeneralRecord(EMR_MODIFYWORLDTRANSFORM, ("f", 1., 0., 0., 1., 0., 0.), ("I", 2)), # GeneralRecord(EMR_SETBKMODE, ("I", BackgroundMode.TRANSPARENT)), # GeneralRecord(EMR_SETPOLYFILLMODE, ("I", 2)), # GeneralRecord(EMR_SETTEXTALIGN, ("I", TA_CENTER | TA_BASELINE)), # GeneralRecord(EMR_SETTEXTCOLOR, ("I", 0)), # GeneralRecord(EMR_SETROP2, ("I", 0x000D)), # ] # def _handle_for(self, what): # handle = self._handles.get(what) # if handle is None: # handle = len(self._handles) + 1 # self._handles[what] = handle # return handle
[docs] def data(self): """Produce complete EMF structure. Returns ------- data : `bytes` Byte-string with EMF data. """ records = self._records + [_EOFRecord()] n_rec = len(records) + 1 rec_size = sum(rec.size() for rec in records) n_handles = 2 header = _HeaderRecord(self._width, self._height, n_rec, rec_size, n_handles) records.insert(0, header) return b"".join(rec.data() for rec in records)
[docs] @contextlib.contextmanager def use_pen(self, style, width, color): """Context manager which sets pen parameters. Parameters ---------- style : `str` Pen style. width : `ged2doc.size.Size` Pen width. color : `int` Pen color. """ pen_handle = 1 # self._handle_for("pen") style = _pen_styles.get(style, style) width = int(math.ceil(width.pxf)) # math.ceil returns float in Python2 # rec = GeneralRecord(EMR_CREATEPEN, ("I", pen_handle, style, width, width, color)) rec = GeneralRecord(EMR_EXTCREATEPEN, ("I", pen_handle, 0, 0, 0, 0, style, width, 0, color, 6, 0, 0)) self._records.append(rec) _LOG.debug("EMF: create_pen: id=%s style=%s width=%s color=%s", pen_handle, style, width, color) rec = GeneralRecord(EMR_SELECTOBJECT, ("I", pen_handle)) self._records.append(rec) yield pen_handle rec = GeneralRecord(EMR_SELECTOBJECT, ("I", StockObjects.NULL_PEN)) self._records.append(rec) rec = GeneralRecord(EMR_DELETEOBJECT, ("I", pen_handle)) self._records.append(rec)
[docs] @contextlib.contextmanager def use_font(self, size, fontname="Times New Roman"): """Context manager which sets font parameters. Parameters ---------- size : `ged2doc.size.Size` Font size. fontname : `str` Font family name. """ font_handle = 1 # self._handle_for("font") height = - size.px # negative to enable matching width = 0 weight = 400 # normal facename = _strencode(fontname, 64) # fullname = _strencode("", 128) # style = _strencode("", 64) _LOG.debug("EMF: create_font: facename=%r", facename) rec = GeneralRecord( EMR_EXTCREATEFONTINDIRECTW, ("I", font_handle), # LogFont ("i", height, width), ("i", 0, 0, weight), ("B", 0, 0, 0, 1), # ital/underl/strike/charset ("B", 0, 0, 0, 0), # OutPrec/ClipPrec/Qual/Pitch ("64s", facename), # ("128s", fullname), # ("64s", style), # ("I", 0, 0, 0, 0, 0, 0), # version/stylesize/match/resv/vendor/culture # ("B", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), # panose # ("H", 0), # padding ) self._records.append(rec) _LOG.debug("EMF: create_font: rec.size=%s", rec.size()) rec = GeneralRecord(EMR_SELECTOBJECT, ("I", font_handle)) self._records.append(rec) yield font_handle rec = GeneralRecord(EMR_SELECTOBJECT, ("I", StockObjects.DEVICE_DEFAULT_FONT)) self._records.append(rec) rec = GeneralRecord(EMR_DELETEOBJECT, ("I", font_handle)) self._records.append(rec)
[docs] def set_bkmode(self, mode): """Set background mode. Parameters ---------- mode : `int` Mode, one of `BackgroundMode` constants. """ rec = GeneralRecord(EMR_SETBKMODE, ("I", mode)) self._records.append(rec)
[docs] def polyline(self, points): """Draw polyline. Parameters ---------- points : `list` [ `tuple` ] List of 2-tuples with (x, y) coordinates, each coordinate is `ged2doc.size.Size`. """ points = [(x.px, y.px) for x, y in points] left = min(x for x, y in points) right = max(x for x, y in points) top = min(y for x, y in points) bottom = max(y for x, y in points) coords = [] for x, y in points: coords += [x, y] rec = GeneralRecord( EMR_POLYLINE, ("i", left, top, right, bottom), ("I", len(points)), ("i",) + tuple(coords) ) self._records.append(rec)
[docs] def rectangle(self, left, top, right, bottom): """Draw rectangle. Parameters ---------- left, top, right, bottom : `ged2doc.size.Size` Rectangle coordinates. """ left, top, right, bottom = [pos.px for pos in (left, top, right, bottom)] _LOG.debug("EMF: rect: left=%s top=%s right=%s bottom=%s", left, top, right, bottom) # rec = GeneralRecord(EMR_SELECTOBJECT, ("I", StockObjects.NULL_BRUSH)) # self._records.append(rec) # rec = GeneralRecord(EMR_RECTANGLE, ("i",) + rect) # self._records.append(rec) self._records.append(GeneralRecord(EMR_BEGINPATH)) self._records.append(GeneralRecord(EMR_MOVETOEX, ("I", left, top))) self._records.append(GeneralRecord(EMR_LINETO, ("I", right, top))) self._records.append(GeneralRecord(EMR_LINETO, ("I", right, bottom))) self._records.append(GeneralRecord(EMR_LINETO, ("I", left, bottom))) self._records.append(GeneralRecord(EMR_CLOSEFIGURE)) self._records.append(GeneralRecord(EMR_ENDPATH)) self._records.append(GeneralRecord(EMR_STROKEPATH, ("i", 0, 0, -1, -1)))
[docs] def text_align(self, align_mode="c"): """Set text alignment for next text drawing operation Parameters ---------- align_mode : `str`, optional One of "l", "c", "r". """ if align_mode == "l": align_mode = TA_LEFT elif align_mode == "r": align_mode = TA_RIGHT elif align_mode == "c": align_mode = TA_CENTER align_mode |= TA_BASELINE _LOG.debug("EMF: text_align: align=%x", align_mode) rec = GeneralRecord(EMR_SETTEXTALIGN, ("I", align_mode)) self._records.append(rec)
[docs] def text_color(self, color): """Set text color for next text drawing operation Parameters ---------- color : `int` """ _LOG.debug("EMF: text_color: color=%o", color) rec = GeneralRecord(EMR_SETTEXTCOLOR, ("I", color)) self._records.append(rec)
[docs] def text(self, x, y, text): """Draw text. Parameters ---------- x, y : `ged2doc.size.Size` Text coordinates. text : `str` Text to draw. """ pos = tuple(pos.px for pos in (x, y)) iGraphicsMode = GM_COMPATIBLE exScale, eyScale = 1., 1. nChars = len(text) # number of characters, not bytes # encode as UTF16-LE and pad to 4-byte txt_bytes = text.encode("utf_16_le", "replace") if len(txt_bytes) % 4 != 0: txt_bytes += b"\0\0" _LOG.debug("EMF: text: pos=%s, txt_bytes=%r", pos, txt_bytes) offString = 76 offDx = 0 options = 0 rec = GeneralRecord( EMR_EXTTEXTOUTW, ("i", 0, 0, -1, -1), # bounds ("I", iGraphicsMode), ("f", exScale, eyScale), ("i", pos[0], pos[1]), # x, y ("I", nChars), ("I", offString), ("I", options), ("i", 0, 0, -1, -1), # Rectangle ("I", offDx), ("{}s".format(len(txt_bytes)), txt_bytes), ) self._records.append(rec)
class Record(metaclass=abc.ABCMeta): """Base class for all EMF records. """ @abc.abstractmethod def size(self): """Return size of this record in bytes. Returns ------- size : `int` Record size, always multiple of 4. """ raise NotImplementedError() @abc.abstractmethod def data(self): """Produce record contents as byte string. Returns ------- data : `bytes` Byte-string with record data. """ raise NotImplementedError() class GeneralRecord(Record): """Base class for all EMF records. Parameters ---------- type : `int` Records type, one of EMR_* constants. *pack_args : `tuple` Data to pack into record, same format as for `_pack` method. """ def __init__(self, type, *pack_args): if pack_args: rec = _pack(*pack_args) self._size = len(rec) + 8 self._rec = struct.pack("II", type, self._size) + rec else: self._size = 8 self._rec = struct.pack("II", type, self._size) def size(self): # docstring inherited from base class return self._size def data(self): # docstring inherited from base class return self._rec class _HeaderRecord(Record): """EMF header record. Clients don't need to add it explicitly, it is for internal use. Parameters ---------- width, height : `ged2doc.size.Size` Size of the image. n_rec : `int` Number of records in file, not including header. rec_size : `int` Size of all of records in file, not including header. """ def __init__(self, width, height, n_rec, rec_size, n_handles): self._type = EMR_HEADER self._width = width self._height = height self._n_rec = n_rec self._rec_size = rec_size self._n_handles = n_handles def data(self): # docstring inherited from base class boundsX = int(math.ceil(self._width.pxf)) boundsY = int(math.ceil(self._height.pxf)) sizeXmm = int(math.ceil(self._width.mm)) sizeYmm = int(math.ceil(self._height.mm)) frameX = sizeXmm * 100 frameY = sizeYmm * 100 version = 0x00010000 emf_size = self._rec_size + self.size() nDescription, offDescription = 7, 108 nPalEntries = 0 cbPixelFormat, offPixelFormat = 0, 0 bOpenGL = 0 MicrometersX = sizeXmm * 1000 MicrometersY = sizeYmm * 1000 _LOG.debug("EMF: header: bounds = %s x %s; frame = %s x %s; size_mm = %s x %s", boundsX, boundsY, frameX, frameY, sizeXmm, sizeYmm) return _pack( ("I", self._type, self.size()), ("I", 0, 0, boundsX, boundsY), ("I", 0, 0, frameX, frameY), ("c", b" ", b"E", b"M", b"F"), ("I", version, emf_size, self._n_rec), ("H", self._n_handles, 0), ("I", nDescription, offDescription, nPalEntries), ("I", boundsX, boundsY), ("I", sizeXmm, sizeYmm), ("I", cbPixelFormat, offPixelFormat, bOpenGL), ("I", MicrometersX, MicrometersY), ("16s", b"\0d\0u\0m\0b\0e\0m\0f\0\0"), ) def size(self): # docstring inherited from base class size = 108 + 16 return size class _EOFRecord(Record): """EMF record for EOF. Clients don't need to add it explicitly, it is for internal use. """ def __init__(self): self._type = EMR_EOF def data(self): # docstring inherited from base class size = self.size() return _pack( ("I", self._type, size, 0, 16, size) ) def size(self): # docstring inherited from base class return 20 def _parse(): """Simple command line utility to parse/dump EMF. .. note:: This method is for testing only, not a part of regular interface. """ import argparse parser = argparse.ArgumentParser() parser.add_argument("file", type=argparse.FileType("rb")) parser.add_argument("-v", dest="verbose", action="store_true", default=False, help="verbose output") args = parser.parse_args() data = args.file.read(8) while data: rectype, size = struct.unpack("II", data) for name, value in globals().items(): if name.startswith("EMR_") and value == rectype: rectype = name print("{} size={}".format(rectype, size)) # read remaining data data += args.file.read(size - 8) if args.verbose: offset = 0 while data: line, data = data[:16], data[16:] fline = " {:03d}:".format(offset) bline = list(line) bline += [None] * (16 - len(bline)) for i, b in enumerate(bline): if i % 4 == 0: fline += " " if b is not None: fline += " {:02X}".format(b) else: fline += " " for i, b in enumerate(bline): if i % 4 == 0: fline += " " if b is None: fline += " " elif 32 <= b < 127: fline += " {}".format(chr(b)) else: fline += " ." for i in (0, 4, 8, 12): if i < len(line): v, = struct.unpack("I", line[i:i+4]) fline += " {:010d}".format(v) print(fline) offset += 16 # next record, if any data = args.file.read(8) if __name__ == "__main__": _parse()