From d25b27cca14a00c114581c491d9616c59d999f61 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Andr=C3=A9=20Wobst?= Date: Wed, 26 May 2004 10:35:26 +0000 Subject: [PATCH] bitmap module git-svn-id: https://pyx.svn.sourceforge.net/svnroot/pyx/trunk/pyx@1751 069f4177-920e-0410-937b-c2a4a81bcd90 --- CHANGES | 2 + manual/bitmap.py | 15 ++ manual/bitmap.tex | 140 +++++++++++++++++++ pyx/__init__.py | 2 +- pyx/bitmap.py | 304 +++++++++++++++++++++++++++++++++++++++++ test/functional/test_bitmap.py | 30 ++++ 6 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 manual/bitmap.py create mode 100644 manual/bitmap.tex create mode 100644 pyx/bitmap.py create mode 100644 test/functional/test_bitmap.py diff --git a/CHANGES b/CHANGES index 93f67171..a7b0dd1b 100644 --- a/CHANGES +++ b/CHANGES @@ -78,6 +78,8 @@ TODO: graph data 0.7 (xxxx/xx/xx): + - bitmap module: + - new module for inclusion of bitmap images - path module: - names of local and member variables now follow the naming convention of having a _pt suffix when containing lengths in points diff --git a/manual/bitmap.py b/manual/bitmap.py new file mode 100644 index 00000000..91a329ae --- /dev/null +++ b/manual/bitmap.py @@ -0,0 +1,15 @@ +import sys; sys.path.insert(0, "..") +from pyx import * +from pyx import bitmap + +image_bw = bitmap.image(2, 2, "L", "\0\377\377\0") +image_rgb = bitmap.image(3, 2, "RGB", "\77\77\77\177\177\177\277\277\277" + "\377\0\0\0\377\0\0\0\377") +bitmap_bw = bitmap.bitmap(0, 1, image_bw, height=0.8) +bitmap_rgb = bitmap.bitmap(0, 0, image_rgb, height=0.8) + +c = canvas.canvas() +c.insert(bitmap_bw) +c.insert(bitmap_rgb) +c.writeEPSfile("bitmap") + diff --git a/manual/bitmap.tex b/manual/bitmap.tex new file mode 100644 index 00000000..f908b305 --- /dev/null +++ b/manual/bitmap.tex @@ -0,0 +1,140 @@ +\chapter{Bitmaps} +\section{Introduction} +\PyX{} focuses on the creation of scaleable vector graphics. However, +\PyX{} also allows for the output of bitmap images. Still, the support +for creation and handling of bitmap images is quite limited. On the +other hand the interfaces are build that way, that its trivial to +combine \PyX{} with the ``Python Image Library'', also known as +``PIL''. + +The creation of a bitmap can be performed out of some unpacked binary +data by first creating image instances: +\begin{verbatim} +from pyx import * +image_bw = bitmap.image(2, 2, "L", "\0\377\377\0") +image_rgb = bitmap.image(3, 2, "RGB", "\77\77\77\177\177\177\277\277\277" + "\377\0\0\0\377\0\0\0\377") +\end{verbatim} +Now \code{image_bw} is a $2\times2$ grayscale image. The bitmap data +is provided by a string, which contains two black (\code{"\e 0" == +chr(0)}) and two white (\code{"\e 377" == chr(255)}) pixels. Currently +the values per (colour) channel is fixed to 8 bits. The coloured image +\code{image_rgb} has $3\times2$ pixels containing a row of 3 different +gray values and a row of the three colours red, green, and blue. + +The images can then be wrapped into \code{bitmap} instances by: +\begin{verbatim} +bitmap_bw = bitmap.bitmap(0, 1, image_bw, height=0.8) +bitmap_rgb = bitmap.bitmap(0, 0, image_rgb, height=0.8) +\end{verbatim} +When constructing a \code{bitmap} instance you have to specify a +certain position by the first two arguments fixing the bitmaps lower +left corner. Some optional arguments control further properties. Since +in this example there is no information about the dpi-value of the +images, we have to specify at least a \code{width} or a \code{height} +of the bitmap. + +The bitmaps are now to be inserted into a canvas: +\begin{verbatim} +c = canvas.canvas() +c.insert(bitmap_bw) +c.insert(bitmap_rgb) +c.writeEPSfile("bitmap") +\end{verbatim} +Figure~\ref{fig:bitmap} shows the resulting output. +\begin{figure}[ht] +\centerline{\includegraphics{bitmap}} +\caption{An introductory bitmap example.} +\label{fig:bitmap} +\end{figure} + +\section{Bitmap module} +\declaremodule{}{bitmap} +\modulesynopsis{Bitmap support} + +\begin{classdesc}{image}{width, height, mode, data, compressed=None} + This class is a container for image data. \var{width} and + \var{height} are the size of the image in pixel. \var{mode} is one + of \code{\textquotedbl L\textquotedbl}, \code{\textquotedbl + RGB\textquotedbl} or \code{\textquotedbl CMYK\textquotedbl} for + grayscale, rgb, or cmyk colours, respectively. \var{data} is the + bitmap data as a string, where each single character represents a + colour value with ordinal range \code{0} to \code{255}. Each pixel + is described by the appropriate number of colour components + according to \var{mode}. The pixels are listed row by row one after + the other starting at the upper left corner of the image. + + \var{compressed} might be set to \code{\textquotedbl + Flate\textquotedbl} or \code{\textquotedbl DCT\textquotedbl} to + provide already compressed data. Note that those data will be passed + to PostScript without further checks, \emph{i.e.} this option is for + experts only. +\end{classdesc} + +\begin{classdesc}{jpegimage}{file} + This class is specialized to read data from a JPEG/JFIF-file. + \var{file} is either a open file handle (it only has to provide a + \method{read()} method; the file should be opened in binary mode) or + a string. In the later case \class{jpegimage} will try to open a + file named like \var{file} for reading. + + The contents of the file is checked for some JPEG/JFIF format + markers in order to identify the size and dpi resolution of the + image for further usage. These checks will typically fail for + invalid data. The data is not uncompressed, but directly inserted + into the output stream (for invalid data the result will be invalid + PostScript). Thus there is no quality loss by recompressing the data + as it would occur when recompressing the uncompressed stream with + the lossy jpeg compression method. +\end{classdesc} + +\begin{classdesc}{bitmap}{xpos, ypos, image, width=None, height=None, + ratio=None, storedata=0, maxstrlen=4093, compressmode="Flate", + flatecompresslevel=6, dctquality=75, dctoptimize=1, + dctprogression=0} + \var{xpos} and \var{ypos} are the position of the lower left corner + of the image. This position might be modified by some additional + transformations when inserting the bitmap into a canvas. \var{image} + is an instance of \class{image} or \class{jpegimage} but it can also + be an image instance from the ``Python Image Library''. + + \var{width}, \var{height}, and \var{ratio} adjust the size of the + image. At least \var{width} or \var{height} needs to be given, when + no dpi information is available from \var{image}. + + \var{storedata} is a flag indicating, that the (still compressed) + image data should be put into the printers memory instead of writing + it as a stream into the PostScript file. While this feature consumes + memory of the PostScript interpreter, it allows for multiple usage + of the image without including the image data several times in the + PostScript file. + + \var{maxstrlen} defines a maximal string length when \var{storedata} + is enabled. Since the data must be kept in the PostScript + interpreters memory, it is stored in strings. While most + interpreters do not allow for an arbitrary string length (a common + limit is 65535 characters), a limit for the string length is set. + When more data needs to be stored, a list of strings will be used. + Note that lists are also subject to some implemenation limits. Since + a typical value is 65535 enties, in combination a huge amount of + memory can be used. + + Valid values for \var{compressmode} currently are + \code{\textquotedbl Flate\textquotedbl} (zlib compression), + \code{\textquotedbl DCT\textquotedbl} (jpeg compression), or + \code{None} (disabling the compression). The zlib compression makes + use of the zlib module as it is part of the standard Python + distribution. The jpeg compression is available for those + \var{image} instances only, which support the creation of a + jpeg-compressed stream, \emph{e.g.} images from the ``Python Image + Library'' with jpeg support installed. The compression must be + disabled when the image data is already compressed. + + \var{flatecompresslevel} is a parameter of the zlib compression. + \var{dctquality}, \var{dctoptimize}, and \var{dctprogression} are + parameters of the jpeg compression. Note, that the progression + feature of the jpeg compression should be turned off in order to + produce valid PostScript. Also the optimization feature is known to + produce errors on certain printers. +\end{classdesc} + diff --git a/pyx/__init__.py b/pyx/__init__.py index d340411e..44757d9d 100644 --- a/pyx/__init__.py +++ b/pyx/__init__.py @@ -33,7 +33,7 @@ are build out of these primitives. import version __version__ = version.version -__all__ = ["attr", "box", "canvas", "color", "connector", "deco", "deformer", "epsfile", "graph", "path", +__all__ = ["attr", "box", "bitmap", "canvas", "color", "connector", "deco", "deformer", "epsfile", "graph", "path", "style", "trafo", "text", "unit"] diff --git a/pyx/bitmap.py b/pyx/bitmap.py new file mode 100644 index 00000000..0617c703 --- /dev/null +++ b/pyx/bitmap.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python +# -*- coding: ISO-8859-1 -*- +# +# +# Copyright (C) 2004 André Wobst +# +# This file adds bitmap functionality to PyX. Its designed for usage +# with PyX 0.6.x and will, in a similar form, become part of future +# PyX releases. + + +import cStringIO, struct, sys +try: + import zlib + haszlib = 1 +except: + haszlib = 0 + +import base, bbox, prolog, trafo, unit + +def ascii85stream(file, data): + l = 0 + for i in range(len(data)): + c = data[i] + l = l*256 + ord(c) + if i%4 == 3: + if l: + l, c5 = divmod(l, 85) + l, c4 = divmod(l, 85) + l, c3 = divmod(l, 85) + c1, c2 = divmod(l, 85) + file.write(struct.pack('BBBBB', c1+33, c2+33, c3+33, c4+33, c5+33)) + l = 0 + else: + file.write("z") + if i%64 == 63: + file.write("\n") + if i%4 != 3: + for x in range(3-(i%4)): + l *= 256 + l, c5 = divmod(l, 85) + l, c4 = divmod(l, 85) + l, c3 = divmod(l, 85) + c1, c2 = divmod(l, 85) + file.write(struct.pack('BBBB', c1+33, c2+33, c3+33, c4+33)[:(i%4)+2]) + + +class image: + + def __init__(self, width, height, mode, data, compressed=None): + if width <= 0 or height <= 0: + raise ValueError("valid image size") + if mode not in ["L", "RGB", "CMYK"]: + raise ValueError("invalid mode") + if compressed is None and len(mode)*width*height != len(data): + raise ValueError("wrong size of uncompressed data") + self.size = width, height + self.mode = mode + self.data = data + self.compressed = compressed + + def tostring(self, *args): + if len(args): + raise RuntimeError("encoding not supported in this implementation") + return self.data + + def convert(self, model): + raise RuntimeError("color model conversion not supported in this implementation") + + +class jpegimage(image): + + def __init__(self, file): + try: + data = file.read() + except: + data = open(file, "rb").read() + pos = 0 + nestinglevel = 0 + try: + while 1: + if data[pos] == "\377" and data[pos+1] not in ["\0", "\377"]: + # print "marker: 0x%02x \\%03o" % (ord(data[pos+1]), ord(data[pos+1])) + if data[pos+1] == "\330": + if not nestinglevel: + begin = pos + nestinglevel += 1 + elif not nestinglevel: + raise ValueError("begin marker expected") + elif data[pos+1] == "\331": + nestinglevel -= 1 + if not nestinglevel: + end = pos + 2 + break + elif data[pos+1] in ["\300", "\301"]: + l, bits, height, width, components = struct.unpack(">HBHHB", data[pos+2:pos+10]) + if bits != 8: + raise ValueError("implementation limited to 8 bit per component only") + try: + mode = {1: "L", 3: "RGB", 4: "CMYK"}[components] + except KeyError: + raise ValueError("invalid number of components") + pos += l+1 + elif data[pos+1] == "\340": + l, id, major, minor, dpikind, xdpi, ydpi = struct.unpack(">H5sBBBHH", data[pos+2:pos+16]) + if dpikind == 1: + self.info = {"dpi": (xdpi, ydpi)} + elif dpikind == 2: + self.info = {"dpi": (xdpi*2.54, ydpi*2.45)} + # else do not provide dpi information + pos += l+1 + pos += 1 + except IndexError: + raise ValueError("end marker expected") + image.__init__(self, width, height, mode, data[begin:end], compressed="DCT") + + +class bitmap(base.PSCmd): + + def __init__(self, xpos, ypos, image, + width=None, height=None, ratio=None, + storedata=0, maxstrlen=4093, + compressmode="Flate", + flatecompresslevel=6, + dctquality=75, dctoptimize=0, dctprogression=0): + self.xpos = xpos + self.ypos = ypos + self.imagewidth, self.imageheight = image.size + self.storedata = storedata + self.maxstrlen = maxstrlen + self.imagedataid = "imagedata%d" % id(self) + self.prologs = [] + + if width is not None or height is not None: + self.width = width + self.height = height + if self.width is None: + if ratio is None: + self.width = self.height * self.imagewidth / float(self.imageheight) + else: + self.width = ratio * self.height + elif self.height is None: + if ratio is None: + self.height = self.width * self.imageheight / float(self.imagewidth) + else: + self.height = (1.0/ratio) * self.width + elif ratio is not None: + raise ValueError("can't specify a ratio when setting width and height") + else: + if ratio is not None: + raise ValueError("must specify width or height to set a ratio") + widthdpi, heightdpi = image.info["dpi"] # XXX fails when no dpi information available + self.width = unit.inch(self.imagewidth / float(widthdpi)) + self.height = unit.inch(self.imageheight / float(heightdpi)) + + self.xpos_pt = unit.topt(self.xpos) + self.ypos_pt = unit.topt(self.ypos) + self.width_pt = unit.topt(self.width) + self.height_pt = unit.topt(self.height) + + # create decode and colorspace + self.palettedata = None + if image.mode == "P": + palettemode, self.palettedata = image.palette.getdata() + self.decode = "[0 255]" + # palettedata and closing ']' is inserted in outputPS + if palettemode == "L": + self.colorspace = "[ /Indexed /DeviceGray %i" % (len(self.palettedata)/1-1) + elif palettemode == "RGB": + self.colorspace = "[ /Indexed /DeviceRGB %i" % (len(self.palettedata)/3-1) + elif palettemode == "CMYK": + self.colorspace = "[ /Indexed /DeviceCMYK %i" % (len(self.palettedata)/4-1) + else: + image = image.convert("RGB") + self.decode = "[0 1 0 1 0 1]" + self.colorspace = "/DeviceRGB" + self.palettedata = None + sys.stderr.write("*** PyX Info: image with unknown palette mode converted to rgb image\n") + elif len(image.mode) == 1: + if image.mode != "L": + image = image.convert("L") + sys.stderr.write("*** PyX Info: specific single channel image mode not natively supported, converted to regular grayscale\n") + self.decode = "[0 1]" + self.colorspace = "/DeviceGray" + elif image.mode == "CMYK": + self.decode = "[0 1 0 1 0 1 0 1]" + self.colorspace = "/DeviceCMYK" + else: + if image.mode != "RGB": + image = image.convert("RGB") + sys.stderr.write("*** PyX Info: image with unknown mode converted to rgb\n") + self.decode = "[0 1 0 1 0 1]" + self.colorspace = "/DeviceRGB" + + # create imagematrix + self.imagematrix = str(trafo.mirror(0) + .translated_pt(-self.xpos_pt, self.ypos_pt+self.height_pt) + .scaled_pt(self.imagewidth/self.width_pt, self.imageheight/self.height_pt)) + + # savely check whether imagedata is compressed or not + try: + imagecompressed = image.compressed + except: + imagecompressed = None + if compressmode != None and imagecompressed != None: + raise ValueError("compression of a compressed image not supported") + if not haszlib and compressmode == "Flate": + sys.stderr.write("*** PyX Info: zlib module not available, disable compression") + compressmode == None + + # create data + if compressmode == "Flate": + self.data = zlib.compress(image.tostring(), flatecompresslevel) + elif compressmode == "DCT": + self.data = image.tostring("jpeg", image.mode, + dctquality, dctoptimize, dctprogression) + else: + self.data = image.tostring() + self.singlestring = self.storedata and len(self.data) < self.maxstrlen + + # create datasource + if self.storedata: + if self.singlestring: + self.datasource = "/%s load" % self.imagedataid + else: + self.datasource = "/imagedataaccess load" # some printers do not allow for inline code here + self.prologs.append(prolog.definition("imagedataaccess", + "{ /imagedataindex load " # get list index + "dup 1 add /imagedataindex exch store " # store increased index + "/imagedataid load exch get }")) # select string from array + else: + self.datasource = "currentfile /ASCII85Decode filter" + if compressmode == "Flate" or imagecompressed == "Flate": + self.datasource += " /FlateDecode filter" + elif compressmode == "DCT" or imagecompressed == "DCT": + self.datasource += " /DCTDecode filter" + else: + if compressmode != None: + raise ValueError("invalid compressmode '%s'" % compressmode) + if imagecompressed != None: + raise ValueError("invalid compressed image '%s'" % imagecompressed) + + # cache prolog + if self.storedata: + # TODO resource data could be written directly on the output stream + # after proper code reorganization + buffer = cStringIO.StringIO() + if self.singlestring: + buffer.write("<~") + ascii85stream(buffer, self.data) + buffer.write("~>") + else: + buffer.write("[ ") + datalen = len(self.data) + tailpos = datalen - datalen % self.maxstrlen + for i in xrange(0, tailpos, self.maxstrlen): + buffer.write("<~") + ascii85stream(buffer, self.data[i: i+self.maxstrlen]) + buffer.write("~>\n") + if datalen != tailpos: + buffer.write("<~") + ascii85stream(buffer, self.data[tailpos:]) + buffer.write("~> ]") + else: + buffer.write("]") + self.prologs.append(prolog.definition(self.imagedataid, buffer.getvalue())) + + def bbox(self): + return bbox.bbox_pt(self.xpos_pt, self.ypos_pt, + self.xpos_pt+self.width_pt, self.ypos_pt+self.height_pt) + + def prolog(self): + return self.prologs + + def outputPS(self, file): + file.write("gsave\n" + "%s" % self.colorspace) + if self.palettedata is not None: + # insert palette data + file.write("<~") + ascii85stream(file, self.palettedata) + file.write("~> ]") + file.write(" setcolorspace\n") + + if self.storedata and not self.singlestring: + file.write("/imagedataindex 0 store\n" # not use the stack since interpreters differ in their stack usage + "/imagedataid %s store\n" % self.imagedataid) + + file.write("<<\n" + "/ImageType 1\n" + "/Width %i\n" # imagewidth + "/Height %i\n" # imageheight + "/BitsPerComponent 8\n" + "/ImageMatrix %s\n" # imagematrix + "/Decode %s\n" # decode + "/DataSource %s\n" # datasource + ">>\n" + "image\n" % (self.imagewidth, self.imageheight, + self.imagematrix, self.decode, self.datasource)) + if not self.storedata: + ascii85stream(file, self.data) + file.write("~>\n") + + file.write("grestore\n") diff --git a/test/functional/test_bitmap.py b/test/functional/test_bitmap.py new file mode 100644 index 00000000..85fc81c3 --- /dev/null +++ b/test/functional/test_bitmap.py @@ -0,0 +1,30 @@ +import sys; sys.path.insert(0, "../..") +from pyx import * + +image_bw = bitmap.image(2, 2, "L", "\0\377\377\0") +image_rgb = bitmap.image(3, 2, "RGB", "\77\77\77\177\177\177\277\277\277" + "\377\0\0\0\377\0\0\0\377") +bitmap_bw_stream = bitmap.bitmap(0, 1, image_bw, height=0.8) +bitmap_rgb_stream = bitmap.bitmap(0, 0, image_rgb, height=0.8) + +bitmap_bw_storestring = bitmap.bitmap(2, 1, image_bw, height=0.8, storedata=1) +bitmap_rgb_storestring = bitmap.bitmap(2, 0, image_rgb, height=0.8, storedata=1) + +bitmap_bw_storearray = bitmap.bitmap(4, 1, image_bw, height=0.8, storedata=1, maxstrlen=2) +bitmap_rgb_storearray = bitmap.bitmap(4, 0, image_rgb, height=0.8, storedata=1, maxstrlen=2) + +c = canvas.canvas() +c.insert(bitmap_bw_stream) +c.insert(bitmap_rgb_stream) +c.insert(bitmap_bw_stream) +c.insert(bitmap_rgb_stream) +c.insert(bitmap_bw_storestring) +c.insert(bitmap_rgb_storestring) +c.insert(bitmap_bw_storestring) +c.insert(bitmap_rgb_storestring) +c.insert(bitmap_bw_storearray) +c.insert(bitmap_rgb_storearray) +c.insert(bitmap_bw_storearray) +c.insert(bitmap_rgb_storearray) +c.writeEPSfile("test_bitmap", paperformat="a4") + -- 2.11.4.GIT