From ca10d560496dc9f6edde8b422fe591c96c175a6d Mon Sep 17 00:00:00 2001 From: Keshav Sharma Date: Sun, 03 Jul 2011 11:22:14 +0000 Subject: added activity folder --- diff --git a/ImageProcess.py b/ImageProcess.py new file mode 100644 index 0000000..2e5cb54 --- /dev/null +++ b/ImageProcess.py @@ -0,0 +1,523 @@ +# Copyright (C) 2008, One Laptop per Child +# Author: Keshav Sharma & Vaibhav Sharma +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from __future__ import division +import pygtk +import gtk +from gtk import gdk +import gobject +import sys +import Image,ImageEnhance,ImageFont,ImageFilter,ImageOps +import ImageDraw +import StringIO +import logging +import random +from sugar import mime +import gst, pygame, sys, time +from random import * + +class ImageProcessor(gtk.DrawingArea): + __gsignals__ = { + 'expose-event' : ('override'), + 'zoom-changed' : (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, []), + 'angle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, []), + } + + __gproperties__ = { + 'zoom': (gobject.TYPE_FLOAT, 'Zoom Factor', 'Factor of zoom',0, 4, 1, gobject.PARAM_READWRITE), + 'angle': (gobject.TYPE_INT, 'Angle', 'Angle of rotation',0, 360, 0, gobject.PARAM_READWRITE), + 'file_location':( gobject.TYPE_STRING, 'File Location', 'Location of the image file', + '', gobject.PARAM_READWRITE), + } + + def __init__(self): + gtk.DrawingArea.__init__(self) + self.set_app_paintable(True) + self.pixbuf = None + self.zoom = None + self.input_text = "text to edit" + self._tempfile = None + self.file_location = None + self._temp_pixbuf = None + self.edit_text = None + self.im = None + self._image_changed_flag = True + self._optimal_zoom_flag = True + self.angle = 0 + + def do_get_property(self, pspec): + if pspec.name == 'zoom' : return self.zoom + elif pspec.name == 'angle' : return self.angle + elif pspec.name == 'file_location': return self.file_location + else: raise AttributeError('unknown property %s' % pspec.name) + + def do_set_property(self, pspec, value): + if pspec.name == 'zoom': self.set_zoom(value) + elif pspec.name == 'angle': self.set_angle(value) + elif pspec.name == 'file_location' : self.set_file_location(value) + else: raise AttributeError('unknown property %s' % pspec.name) + + def calculate_optimal_zoom(self, width=None, height=None, pixbuf=None): + # This tries to figure out a best fit model + # If the image can fit in, we show it in 1:1, + # in any other case we show it in a fit to screen way + + if pixbuf == None: + pixbuf = self.pixbuf + + if width == None or height == None: + rect = self.parent.get_allocation() + width = rect.width + height = rect.height + + if width < pixbuf.get_width() or height < pixbuf.get_height(): + # Image is larger than allocated size + zoom = min(width / pixbuf.get_width(), + height / pixbuf.get_height()) + else: zoom = 1 + + self._optimal_zoom_flag = True + + return zoom - 0.018 #XXX: Hack + + def set_pixbuf( self , pixbuf ): + self.pixbuf = pixbuf + self.zoom = None + self._image_changed_flag = True + if self.window: + alloc = self.get_allocation() + rect = gdk.Rectangle( alloc.x , alloc.y , alloc.width ,alloc.height ) + self.window.invalidate_rect( rect , True ) + self.window.process_updates( True ) + + #def do_size_request(self, requisition): + # requisition.width = self.pixbuf.get_width() + # requisition.height = self.pixbuf.get_height() + + def do_expose_event(self, event): + ctx = self.window.cairo_create() + ctx.rectangle(event.area.x, event.area.y, + event.area.width, event.area.height) + ctx.clip() + self.draw(ctx) + + def draw(self, ctx): + if not self.pixbuf: + return + if self.zoom == None: + self.zoom = self.calculate_optimal_zoom() + + if self._temp_pixbuf == None or self._image_changed_flag == True: + width, height = self.rotate() + self._temp_pixbuf = self._temp_pixbuf.scale_simple(width, height, gtk.gdk.INTERP_TILES) + self._image_changed_flag = False + + rect = self.get_allocation() + x = rect.x + y = rect.y + + width = self._temp_pixbuf.get_width() + height = self._temp_pixbuf.get_height() + + if self.parent: + rect = self.parent.get_allocation() + if rect.width > width: + x = int(((rect.width - x) - width) / 2) + + if rect.height > height: + y = int(((rect.height - y) - height) / 2) + + self.set_size_request(self._temp_pixbuf.get_width(), + self._temp_pixbuf.get_height()) + + ctx.set_source_pixbuf(self._temp_pixbuf, x, y) + + ctx.paint() + + def set_zoom(self, zoom): + self._image_changed_flag = True + self._optimal_zoom_flag = False + self.zoom = zoom + + if self.window: + alloc = self.get_allocation() + rect = gdk.Rectangle(alloc.x, alloc.y, + alloc.width, alloc.height) + self.window.invalidate_rect(rect, True) + self.window.process_updates(True) + + self.emit('zoom-changed') + + def set_angle(self, angle): + self._image_changed_flag = True + self._optimal_zoom_flag = True + + self.angle = angle + + if self.window: + alloc = self.get_allocation() + rect = gdk.Rectangle(alloc.x, alloc.y, + alloc.width, alloc.height) + self.window.invalidate_rect(rect, True) + self.window.process_updates(True) + + self.emit('angle-changed') + + def rotate(self): + if self.angle == 0: + rotate = gtk.gdk.PIXBUF_ROTATE_NONE + elif self.angle == 90: + rotate = gtk.gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE + elif self.angle == 180: + rotate = gtk.gdk.PIXBUF_ROTATE_UPSIDEDOWN + elif self.angle == 270: + rotate = gtk.gdk.PIXBUF_ROTATE_CLOCKWISE + elif self.angle == 360: + self.angle = 0 + rotate = gtk.gdk.PIXBUF_ROTATE_NONE + else: + logging.warning('Got unsupported rotate angle') + + self._temp_pixbuf = self.pixbuf.rotate_simple(rotate) + width = int(self._temp_pixbuf.get_width() * self.zoom) + height = int(self._temp_pixbuf.get_height() * self.zoom) + + return (width, height) + + def zoom_in(self): + self.set_zoom(self.zoom + 0.2) + if self.zoom > (4): return False + else: return True + + def zoom_out(self): + self.set_zoom(self.zoom - 0.2) + if self.zoom <= 0.2: return False + else: return True + + def grey(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im.convert("RGB") + r, g, b = im.split() + im = Image.merge("RGB", (g,g,g)) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + def image_copy(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + self.im = im.copy() + + def image_paste(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.im + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Blur(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.filter(ImageFilter.BLUR) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Transpose(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.transpose(Image.FLIP_LEFT_RIGHT) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Offset(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + w,h=im.size + im = im.offset(w/2,h/2) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Contour(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.filter(ImageFilter.CONTOUR) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Finedges(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.filter(ImageFilter.FIND_EDGES) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Solarize(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = ImageOps.solarize(im, threshold=128) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_left_top(self,edit_text): + self.edit_text="left_top" + self.image_edit_text(self.edit_text) + + def image_right_top(self,edit_text): + self.edit_text='right_top' + self.image_edit_text(self.edit_text) + + def image_left_bottom(self,edit_text): + self.edit_text='left_bottom' + self.image_edit_text(self.edit_text) + + def image_right_bottom(self,edit_text): + self.edit_text='right_bottom' + self.image_edit_text(self.edit_text) + + def image_edit_text(self,edit_text): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + self.edit_text = edit_text + if self.window: + im = self.pixbuftoImage(pixbuf) + out=self.Imprint(im, self.input_text,self.edit_text) + pix=self.imagetopixbuf(out) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + def input_text_cb(self,text): + self.input_text=str(text) + + def Imprint(self,im, inputtext,edit_text, font=None, color=None, opacity=.6, margin=(30,30)): + """ + imprints a PIL image with the indicated text in lower-right corner + """ + if im.mode != "RGBA": + im = im.convert("RGBA") + w,h=im.size + textlayer = Image.new("RGBA", im.size, (0,0,0,0)) + textdraw = ImageDraw.Draw(textlayer) + textsize = textdraw.textsize(inputtext, font=font) + if edit_text=="left_top": + textpos = [margin[i] for i in [0,1]] + elif edit_text=="right_top": + textpos = im.size[0]-textsize[0]-margin[0],margin[1] + elif edit_text=="left_bottom": + textpos = margin[0],im.size[1]-textsize[1]-margin[1] + else: + textpos = [im.size[i]-textsize[i]-margin[i] for i in [0,1]] + textdraw.text(textpos, inputtext, font=font, fill="red") + if opacity != 1: + textlayer = self.reduce_opacity(textlayer,opacity) + return Image.composite(textlayer, im, textlayer) + + def image_Sharpen(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.filter(ImageFilter.SHARPEN) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Ambross(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = im.filter(ImageFilter.EMBOSS) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def image_Invert(self,value): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + if self.window: + im = self.pixbuftoImage(pixbuf) + im = ImageOps.invert(im) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + + def image_Watermark(self,mark,pos): + pixbuf = self.pixbuf + self._image_changed_flag = True + self._optimal_zoom_flag = True + + if self.window: + im1=Image.open(mark) + im = self.pixbuftoImage(pixbuf) + if pos=="tile": + im = self.watermark(im, im1,"tile",0.5) + elif pos=="scale": + im = self.watermark(im, im1,"scale",0.5) + else :#if pos is top_left + im = self.watermark(im, im1,(0,0),0.5) + #(raw_input('type of watermark(tile/scale,(xsize,ysize))')), 0.5) + pix=self.imagetopixbuf(im) + self.set_pixbuf( pix ) + self.window.process_updates( True ) + + + def pixbuftoImage(self,pb): + width,height = pb.get_width(),pb.get_height() + return Image.fromstring("RGB",(width,height),pb.get_pixels() ) + + def imagetopixbuf(self,im): + file1 = StringIO.StringIO() + im.save(file1, "ppm") + contents = file1.getvalue() + file1.close() + loader = gtk.gdk.PixbufLoader("pnm") + loader.write(contents, len(contents)) + pixbuf = loader.get_pixbuf() + loader.close() + return pixbuf + + def reduce_opacity(self,im, opacity): + """Returns an image with reduced opacity.""" + assert opacity >= 0 and opacity <= 1 + if im.mode != 'RGBA': im = im.convert('RGBA') + else : im = im.copy() + alpha = im.split()[3] + alpha = ImageEnhance.Brightness(alpha).enhance(opacity) + im.putalpha(alpha) + return im + #watermark to an image + def watermark(self,im, mark, position, opacity=1): + """Adds a watermark to an image.""" + if opacity < 1 : mark = self.reduce_opacity(mark, opacity) + if im.mode != 'RGBA': im = im.convert('RGBA') + # create a transparent layer the size of the image and draw the + # watermark in that layer. + layer = Image.new('RGBA', im.size, (0,0,0,0)) + if position == 'tile': + w,h=im.size + mark = mark.resize((w/2, h/2)) + for y in range(0, im.size[1], mark.size[1]): + for x in range(0, im.size[0], mark.size[0]): layer.paste(mark,(x, y)) + elif position == 'scale': + # scale, but preserve the aspect ratio + ratio = min(float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1]) + w = int(mark.size[0] * ratio) + h = int(mark.size[1] * ratio) + mark = mark.resize((w, h)) + layer.paste(mark, ((im.size[0] - w) / 2, (im.size[1] - h) / 2)) + else: + w,h=im.size + mark = mark.resize((w/2, h/2)) + layer.paste(mark, position) + # composite the watermark with the layer + return Image.composite(layer, im, layer) + + def original_cb(self,value): + self.set_file_location(self.original) + + def set_file_location(self, file_location): + self.original=file_location + self.pixbuf = gtk.gdk.pixbuf_new_from_file(file_location) + self.file_location = file_location + self.zoom = None + self._image_changed_flag = True + if self.window: + alloc = self.get_allocation() + rect = gdk.Rectangle(alloc.x, alloc.y, alloc.width, alloc.height) + self.window.invalidate_rect(rect, True) + self.window.process_updates(True) + +#check working of functions +def update(view_object): + view.grey("g") + return True + + +if __name__ == '__main__': + window = gtk.Window() + + vadj = gtk.Adjustment() + hadj = gtk.Adjustment() + sw = gtk.ScrolledWindow(hadj, vadj) + + view = ImageProcessor() + + view.set_file_location(sys.argv[1]) + + + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + + + sw.add_with_viewport(view) + window.add(sw) + + window.set_size_request(800, 600) + + window.show_all() + + gobject.timeout_add(1000, update, view) + + gtk.main() diff --git a/ImageProcessorActivity.py b/ImageProcessorActivity.py new file mode 100644 index 0000000..24fb3e3 --- /dev/null +++ b/ImageProcessorActivity.py @@ -0,0 +1,564 @@ +# Copyright (C) 2008, One Laptop per Child +# Author: Keshav Sharma & Vaibhav Sharma +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# The sharing bits have been taken from ReadEtexts + +from __future__ import division + +from sugar.activity import activity +import logging + +from gettext import gettext as _ + +import time +import os +import gtk +import gobject + +from sugar.graphics.alert import NotifyAlert +from sugar.graphics.objectchooser import ObjectChooser +from sugar import mime +from sugar.graphics.toolbarbox import ToolbarButton +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolbarbox import ToolbarBox +from sugar.activity.widgets import ActivityToolbarButton +from sugar.activity.widgets import StopButton +from toolbar import ViewToolbar, EditToolbar + +from sugar import network +from sugar.datastore import datastore +import telepathy +import dbus +import pic +import ImageProcess +import ProgressDialog + +_logger = logging.getLogger('imageprocessor-activity') + + +class ImageProcessorHTTPRequestHandler(network.ChunkedGlibHTTPRequestHandler): + """HTTP Request Handler for transferring document while collaborating. + + RequestHandler class that integrates with Glib mainloop. It writes + the specified file to the client in chunks, returning control to the + mainloop between chunks. + + """ + + def translate_path(self, path): + """Return the filepath to the shared document.""" + return self.server.filepath + + +class ImageProcessorHTTPServer(network.GlibTCPServer): + """HTTP Server for transferring document while collaborating.""" + + def __init__(self, server_address, filepath): + """Set up the GlibTCPServer with the ImageProcessorHTTPRequestHandler. + + filepath -- path to shared document to be served. + """ + self.filepath = filepath + network.GlibTCPServer.__init__(self, server_address, + ImageProcessorHTTPRequestHandler) + + +class ImageProcessorURLDownloader(network.GlibURLDownloader): + """URLDownloader that provides content-length and content-type.""" + + def get_content_length(self): + """Return the content-length of the download.""" + if self._info is not None: + return int(self._info.headers.get('Content-Length')) + + def get_content_type(self): + """Return the content-type of the download.""" + if self._info is not None: + return self._info.headers.get('Content-type') + return None + +IMAGEVIEWER_STREAM_SERVICE = 'imageprocessor-activity-http' + + +class ImageProcessorActivity(activity.Activity): + + def __init__(self, handle): + activity.Activity.__init__(self, handle) + + self.zoom = None + self._object_id = handle.object_id + self._old_zoom = None + self._fileserver = None + self._fileserver_tube_id = None + self.view = ImageProcess.ImageProcessor() + self.progressdialog = None + self.im=None + toolbar_box = ToolbarBox() + self._add_toolbar_buttons(toolbar_box) + self.set_toolbar_box(toolbar_box) + toolbar_box.show() + + vadj = gtk.Adjustment() + hadj = gtk.Adjustment() + self.sw = gtk.ScrolledWindow(hadj, vadj) + + self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.sw.add_with_viewport(self.view) + self.set_canvas(self.sw) + self.sw.show_all() + + self.unused_download_tubes = set() + self._want_document = True + self._download_content_length = 0 + self._download_content_type = None + # Status of temp file used for write_file: + self._tempfile = None + self._close_requested = False + self.connect("shared", self._shared_cb) + h = hash(self._activity_id) + self.port = 1024 + (h % 64511) + + self.is_received_document = False + + if self._shared_activity and handle.object_id == None: + # We're joining, and we don't already have the document. + if self.get_shared(): + # Already joined for some reason, just get the document + self._joined_cb(self) + else: + # Wait for a successful join before trying to get the document + self.connect("joined", self._joined_cb) + elif self._object_id is None: + self._show_object_picker = gobject.timeout_add(1000, self._show_picker_cb) + + def handle_view_source(self): + pass + raise NotImplementedError + + def enter_callback(self, widget, entry): + self.view.input_text_cb(entry.get_text()) + + def fullscreen(self): + self._old_zoom = self.view.get_property('zoom') #XXX: Hack + # Zoom to fit screen if possible + screen = self.get_screen() + zoom = self.view.calculate_optimal_zoom( + screen.get_width(), screen.get_height()) + self.view.set_zoom(zoom) + activity.Activity.fullscreen(self) + + def unfullscreen(self): + self.view.set_zoom(self._old_zoom) + activity.Activity.unfullscreen(self) + + def _add_toolbar_buttons(self, toolbar_box): + activity_button = ActivityToolbarButton(self) + toolbar_box.toolbar.insert(activity_button, 0) + activity_button.show() + + + self._view_toolbar = ViewToolbar() + + self._view_toolbar.connect('zoom_in', self.__zoom_in_cb) + self._view_toolbar.connect('zoom_out', self.__zoom_out_cb) + self._view_toolbar.connect('zoom_to_fit', self.__zoom_tofit_cb) + self._view_toolbar.connect('zoom_original', self.__zoom_original_cb) + self._view_toolbar.connect('rotate_clockwise', self.__rotate_anticlockwise_cb) + self._view_toolbar.connect('rotate_anticlockwise', self.__rotate_clockwise_cb) + self._view_toolbar.connect('copy', self.view.image_copy) + self._view_toolbar.connect('paste', self.view.image_paste) + view_toolbar_button = ToolbarButton(page=self._view_toolbar, icon_name='toolbar-view') + self._view_toolbar.show() + toolbar_box.toolbar.insert(view_toolbar_button, -1) + view_toolbar_button.show() + + self.entry = gtk.Entry() + self.entry.set_max_length(15) + self.entry.connect("activate", self.enter_callback,self.entry) + self.entry.set_text("text") + self.entry.insert_text(" to edit", len(self.entry.get_text())) + self.entry.select_region(0, len(self.entry.get_text())) + self.entry.show() + tool_item = gtk.ToolItem() + tool_item.set_expand(True) + tool_item.add(self.entry) + toolbar_box.toolbar.insert(tool_item, -1) + tool_item.show() + + + self._edit_toolbar = EditToolbar() + self._edit_toolbar.connect('grey', self.view.grey) + self._edit_toolbar.connect('blur', self.view.image_Blur) + self._edit_toolbar.connect('transpose', self.view.image_Transpose) + self._edit_toolbar.connect('offset', self.view.image_Offset) + self._edit_toolbar.connect('contour', self.view.image_Contour) + self._edit_toolbar.connect('finedges', self.view.image_Finedges) + self._edit_toolbar.connect('solarize', self.view.image_Solarize) + self._edit_toolbar.connect('invert', self.view.image_Invert) + self._edit_toolbar.connect('watermark_tl', self.watermrk_cb,"tl") + self._edit_toolbar.connect('watermark_tile', self.watermrk_cb,"tile") + self._edit_toolbar.connect('watermark_scale', self.watermrk_cb,"scale") + self._edit_toolbar.connect('ambross', self.view.image_Ambross) + self._edit_toolbar.connect('left_top', self.view.image_left_top) + self._edit_toolbar.connect('right_top', self.view.image_right_top) + self._edit_toolbar.connect('left_bottom', self.view.image_left_bottom) + self._edit_toolbar.connect('right_bottom', self.view.image_right_bottom) + self._edit_toolbar.connect('sharpen', self.view.image_Sharpen) + + edit_toolbar_button = ToolbarButton(page=self._edit_toolbar, icon_name='toolbar-edit') + self._edit_toolbar.show() + toolbar_box.toolbar.insert(edit_toolbar_button, -1) + edit_toolbar_button.set_expanded(True) + edit_toolbar_button.show() + + spacer = gtk.SeparatorToolItem() + spacer.props.draw = False + toolbar_box.toolbar.insert(spacer, -1) + spacer.show() + + original_button = ToolButton('original') + original_button.set_tooltip(_('Undo original pic')) + original_button.connect('clicked', self.view.original_cb) + toolbar_box.toolbar.insert(original_button, -1) + original_button.show() + + fullscreen_button = ToolButton('view-fullscreen') + fullscreen_button.set_tooltip(_('Fullscreen')) + fullscreen_button.connect('clicked', self.__fullscreen_cb) + toolbar_box.toolbar.insert(fullscreen_button, -1) + fullscreen_button.show() + + + + separator = gtk.SeparatorToolItem() + separator.props.draw = False + separator.set_expand(True) + toolbar_box.toolbar.insert(separator, -1) + separator.show() + + stop_button = StopButton(self) + toolbar_box.toolbar.insert(stop_button, -1) + stop_button.show() + + + def watermrk_cb(self, button,pos): + self.w="kuch" + self.do_load_an_image_cb(button) + self.view.image_Watermark(self.im,pos) + + def __zoom_in_cb(self, button): + self._view_toolbar._zoom_in_button.set_sensitive(self.view.zoom_in()) + self._view_toolbar._zoom_out_button.set_sensitive(True) + + def __zoom_out_cb(self, button): + self._view_toolbar._zoom_out_button.set_sensitive(self.view.zoom_out()) + self._view_toolbar._zoom_in_button.set_sensitive(True) + + def __zoom_tofit_cb(self, button): + zoom = self.view.calculate_optimal_zoom() + self.view.set_zoom(zoom) + + def __zoom_original_cb(self, button): + self.view.set_zoom(1) + + def __rotate_anticlockwise_cb(self, button): + angle = self.view.get_property('angle') + self.view.set_angle(angle + 90) + + def __rotate_clockwise_cb(self, button): + angle = self.view.get_property('angle') + if angle == 0: + angle = 360 + + self.view.set_angle(angle - 90) + + def __fullscreen_cb(self, button): + self._old_zoom = self.view.get_property('zoom') #XXX: Hack + # Zoom to fit screen if possible + screen = self.get_screen() + zoom = self.view.calculate_optimal_zoom(screen.get_width(), screen.get_height()) + self.view.set_zoom(zoom) + + self.fullscreen() + + def _show_picker_cb(self): + if not self._want_document: + return + + chooser = ObjectChooser(_('Choose document'), self, + gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, \ + what_filter=mime.GENERIC_TYPE_IMAGE) + + try: + result = chooser.run() + if result == gtk.RESPONSE_ACCEPT: + jobject = chooser.get_selected_object() + if jobject and jobject.file_path: + self.read_file(jobject.file_path) + finally: + chooser.destroy() + del chooser + + def do_load_an_image_cb(self, button): + """ Load an image from the Journal """ + chooser = ObjectChooser(_('Choose document'), self, + gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, \ + what_filter=mime.GENERIC_TYPE_IMAGE) + try: + result = chooser.run() + if result == gtk.RESPONSE_ACCEPT: + dsobject = chooser.get_selected_object() + try: + _logger.debug("opening %s " % dsobject.file_path) + tempfile = os.path.join(self.get_activity_root(), 'instance','tmp%i' % time.time()) + os.link(dsobject.file_path, tempfile) + self.im=tempfile + except: + _logger.debug("couldn't open %s" % dsobject.file_path) + dsobject.destroy() + finally: + chooser.destroy() + del chooser + return + + def read_file(self, file_path): + self._want_document = False + + tempfile = os.path.join(self.get_activity_root(), 'instance', \ + 'tmp%i' % time.time()) + + os.link(file_path, tempfile) + self._tempfile = tempfile + gobject.idle_add(self.__set_file_idle_cb, tempfile) + + def __set_file_idle_cb(self, file_path): + self.view.set_file_location(file_path) + + try: + self.zoom = int(self.metadata.get('zoom', '0')) + if self.zoom > 0: + self.view.set_zoom(self.zoom) + except Exception: + pass + + return False + + def write_file(self, file_path): + if self._tempfile: + self.metadata['activity'] = self.get_bundle_id() + self.metadata['zoom'] = str(self.zoom) + if self._close_requested: + os.link(self._tempfile, file_path) + os.unlink(self._tempfile) + self._tempfile = None + else: + raise NotImplementedError + + def can_close(self): + self._close_requested = True + return True + + def _download_result_cb(self, getter, tempfile, suggested_name, tube_id): + if self._download_content_type == 'text/html': + # got an error page instead + self._download_error_cb(getter, 'HTTP Error', tube_id) + return + + del self.unused_download_tubes + + self._tempfile = tempfile + file_path = os.path.join(self.get_activity_root(), 'instance', + '%i' % time.time()) + _logger.debug("Saving file %s to datastore...", file_path) + os.link(tempfile, file_path) + self._jobject.file_path = file_path + datastore.write(self._jobject, transfer_ownership=True) + + + _logger.debug("Got document %s (%s) from tube %u", + tempfile, suggested_name, tube_id) + + self.progressdialog.destroy() + + gobject.idle_add(self.__set_file_idle_cb, tempfile) + self.save() + + def _download_progress_cb(self, getter, bytes_downloaded, tube_id): + if self._download_content_length > 0: + _logger.debug("Downloaded %u of %u bytes from tube %u...", + bytes_downloaded, self._download_content_length, + tube_id) + else: + _logger.debug("Downloaded %u bytes from tube %u...", + bytes_downloaded, tube_id) + total = self._download_content_length + + fraction = bytes_downloaded / total + self.progressdialog.set_fraction(fraction) + + #gtk.main_iteration() + + def _download_error_cb(self, getter, err, tube_id): + _logger.debug("Error getting document from tube %u: %s", + tube_id, err) + self._alert('Failure', 'Error getting document from tube') + self._want_document = True + self._download_content_length = 0 + self._download_content_type = None + gobject.idle_add(self._get_document) + + def _download_document(self, tube_id, path): + # FIXME: should ideally have the CM listen on a Unix socket + # instead of IPv4 (might be more compatible with Rainbow) + chan = self._shared_activity.telepathy_tubes_chan + iface = chan[telepathy.CHANNEL_TYPE_TUBES] + addr = iface.AcceptStreamTube(tube_id, + telepathy.SOCKET_ADDRESS_TYPE_IPV4, + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0, + utf8_strings=True) + _logger.debug('Accepted stream tube: listening address is %r', addr) + # SOCKET_ADDRESS_TYPE_IPV4 is defined to have addresses of type '(sq)' + assert isinstance(addr, dbus.Struct) + assert len(addr) == 2 + assert isinstance(addr[0], str) + assert isinstance(addr[1], (int, long)) + assert addr[1] > 0 and addr[1] < 65536 + port = int(addr[1]) + + getter = ImageProcessorURLDownloader("http://%s:%d/document" + % (addr[0], port)) + getter.connect("finished", self._download_result_cb, tube_id) + getter.connect("progress", self._download_progress_cb, tube_id) + getter.connect("error", self._download_error_cb, tube_id) + _logger.debug("Starting download to %s...", path) + getter.start(path) + self._download_content_length = getter.get_content_length() + self._download_content_type = getter.get_content_type() + + return False + + def _get_document(self): + if not self._want_document: + return False + + # Assign a file path to download if one doesn't exist yet + if not self._jobject.file_path: + path = os.path.join(self.get_activity_root(), 'instance', + 'tmp%i' % time.time()) + else: + path = self._jobject.file_path + + # Pick an arbitrary tube we can try to download the document from + try: + tube_id = self.unused_download_tubes.pop() + except (ValueError, KeyError), e: + _logger.debug('No tubes to get the document from right now: %s', + e) + return False + + # Avoid trying to download the document multiple times at once + self._want_document = False + gobject.idle_add(self._download_document, tube_id, path) + return False + + def _joined_cb(self, also_self): + """Callback for when a shared activity is joined. + + Get the shared document from another participant. + """ + self.watch_for_tubes() + + self.progressdialog = ProgressDialog.ProgressDialog(self) + self.progressdialog.show_all() + + gobject.idle_add(self._get_document) + + def _share_document(self): + """Share the document.""" + # FIXME: should ideally have the fileserver listen on a Unix socket + # instead of IPv4 (might be more compatible with Rainbow) + + _logger.debug('Starting HTTP server on port %d', self.port) + self._fileserver = ImageProcessorHTTPServer(("", self.port), + self._tempfile) + + # Make a tube for it + chan = self._shared_activity.telepathy_tubes_chan + iface = chan[telepathy.CHANNEL_TYPE_TUBES] + self._fileserver_tube_id = \ + iface.OfferStreamTube(IMAGEVIEWER_STREAM_SERVICE, + {}, + telepathy.SOCKET_ADDRESS_TYPE_IPV4, + ('127.0.0.1', dbus.UInt16(self.port)), + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0) + + def watch_for_tubes(self): + """Watch for new tubes.""" + tubes_chan = self._shared_activity.telepathy_tubes_chan + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', + self._new_tube_cb) + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + + def _new_tube_cb(self, tube_id, initiator, tube_type, service, params, + state): + """Callback when a new tube becomes available.""" + _logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' + 'params=%r state=%d', tube_id, initiator, tube_type, + service, params, state) + if service == IMAGEVIEWER_STREAM_SERVICE: + _logger.debug('I could download from that tube') + self.unused_download_tubes.add(tube_id) + # if no download is in progress, let's fetch the document + if self._want_document: + gobject.idle_add(self._get_document) + + def _list_tubes_reply_cb(self, tubes): + """Callback when new tubes are available.""" + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + """Handle ListTubes error by logging.""" + _logger.error('ListTubes() failed: %s', e) + + def _shared_cb(self, activityid): + """Callback when activity shared. + + Set up to share the document. + + """ + # We initiated this activity and have now shared it, so by + # definition we have the file. + _logger.debug('Activity became shared') + self.watch_for_tubes() + self._share_document() + + def _alert(self, title, text=None): + alert = NotifyAlert(timeout=5) + alert.props.title = title + alert.props.msg = text + self.add_alert(alert) + alert.connect('response', self._alert_cancel_cb) + alert.show() + + def _alert_cancel_cb(self, alert, response_id): + self.remove_alert(alert) diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..5d698c0 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,116 @@ +ImageProcessor.activity/ +ImageProcessor.activity/activity +ImageProcessor.activity/activity/activity-imageprocessor.svg +ImageProcessor.activity/activity/activity.info +ImageProcessor.activity/icons +ImageProcessor.activity/icons/blur.svg +ImageProcessor.activity/icons/cam.svg +ImageProcessor.activity/icons/contour.svg +ImageProcessor.activity/icons/copy.svg +ImageProcessor.activity/icons/embross.svg +ImageProcessor.activity/icons/finedges.svg +ImageProcessor.activity/icons/grey.svg +ImageProcessor.activity/icons/invert.svg +ImageProcessor.activity/icons/left_bottom.svg +ImageProcessor.activity/icons/left_top.svg +ImageProcessor.activity/icons/mirror.svg +ImageProcessor.activity/icons/offset.svg +ImageProcessor.activity/icons/original.svg +ImageProcessor.activity/icons/paste.svg +ImageProcessor.activity/icons/right_bottom.svg +ImageProcessor.activity/icons/right_top.svg +ImageProcessor.activity/icons/rotate_anticlockwise.svg +ImageProcessor.activity/icons/rotate_clockwise.svg +ImageProcessor.activity/icons/sharpen.svg +ImageProcessor.activity/icons/solarize.svg +ImageProcessor.activity/icons/watermark_scale.svg +ImageProcessor.activity/icons/watermark_tile.svg +ImageProcessor.activity/icons/watermark_tl.svg +ImageProcessor.activity/ImageProcess.py +ImageProcessor.activity/ImageProcess.py~ +ImageProcessor.activity/ImageProcessor.xo +ImageProcessor.activity/ImageProcessorActivity.py +ImageProcessor.activity/MANIFEST +ImageProcessor.activity/pic.py +ImageProcessor.activity/PIL +ImageProcessor.activity/PIL/ImageGL.py +ImageProcessor.activity/PIL/PixarImagePlugin.py +ImageProcessor.activity/PIL/ArgImagePlugin.py +ImageProcessor.activity/PIL/BdfFontFile.py +ImageProcessor.activity/PIL/BmpImagePlugin.py +ImageProcessor.activity/PIL/BufrStubImagePlugin.py +ImageProcessor.activity/PIL/ContainerIO.py +ImageProcessor.activity/PIL/CurImagePlugin.py +ImageProcessor.activity/PIL/DcxImagePlugin.py +ImageProcessor.activity/PIL/EpsImagePlugin.py +ImageProcessor.activity/PIL/ExifTags.py +ImageProcessor.activity/PIL/FitsStubImagePlugin.py +ImageProcessor.activity/PIL/FliImagePlugin.py +ImageProcessor.activity/PIL/FontFile.py +ImageProcessor.activity/PIL/FpxImagePlugin.py +ImageProcessor.activity/PIL/GbrImagePlugin.py +ImageProcessor.activity/PIL/GdImageFile.py +ImageProcessor.activity/PIL/GifImagePlugin.py +ImageProcessor.activity/PIL/GimpGradientFile.py +ImageProcessor.activity/PIL/GimpPaletteFile.py +ImageProcessor.activity/PIL/GribStubImagePlugin.py +ImageProcessor.activity/PIL/Hdf5StubImagePlugin.py +ImageProcessor.activity/PIL/IcnsImagePlugin.py +ImageProcessor.activity/PIL/IcoImagePlugin.py +ImageProcessor.activity/PIL/Image.py +ImageProcessor.activity/PIL/ImageChops.py +ImageProcessor.activity/PIL/ImageColor.py +ImageProcessor.activity/PIL/ImageDraw.py +ImageProcessor.activity/PIL/ImageDraw2.py +ImageProcessor.activity/PIL/ImageEnhance.py +ImageProcessor.activity/PIL/ImageFile.py +ImageProcessor.activity/PIL/ImageFileIO.py +ImageProcessor.activity/PIL/ImageFilter.py +ImageProcessor.activity/PIL/ImageFont.py +ImageProcessor.activity/PIL/PngImagePlugin.py +ImageProcessor.activity/PIL/PpmImagePlugin.py +ImageProcessor.activity/PIL/PsdImagePlugin.py +ImageProcessor.activity/PIL/PSDraw.py +ImageProcessor.activity/PIL/SgiImagePlugin.py +ImageProcessor.activity/PIL/SpiderImagePlugin.py +ImageProcessor.activity/PIL/SunImagePlugin.py +ImageProcessor.activity/PIL/TarIO.py +ImageProcessor.activity/PIL/TgaImagePlugin.py +ImageProcessor.activity/PIL/TiffImagePlugin.py +ImageProcessor.activity/PIL/TiffTags.py +ImageProcessor.activity/PIL/WalImageFile.py +ImageProcessor.activity/PIL/WmfImagePlugin.py +ImageProcessor.activity/PIL/XbmImagePlugin.py +ImageProcessor.activity/PIL/XpmImagePlugin.py +ImageProcessor.activity/PIL/XVThumbImagePlugin.py +ImageProcessor.activity/PIL/__init__.py +ImageProcessor.activity/PIL/ImageGrab.py +ImageProcessor.activity/PIL/ImageMath.py +ImageProcessor.activity/PIL/ImageMode.py +ImageProcessor.activity/PIL/ImageOps.py +ImageProcessor.activity/PIL/ImagePalette.py +ImageProcessor.activity/PIL/ImagePath.py +ImageProcessor.activity/PIL/ImageQt.py +ImageProcessor.activity/PIL/ImageSequence.py +ImageProcessor.activity/PIL/ImageStat.py +ImageProcessor.activity/PIL/ImageTk.py +ImageProcessor.activity/PIL/ImageTransform.py +ImageProcessor.activity/PIL/ImageWin.py +ImageProcessor.activity/PIL/ImImagePlugin.py +ImageProcessor.activity/PIL/ImtImagePlugin.py +ImageProcessor.activity/PIL/IptcImagePlugin.py +ImageProcessor.activity/PIL/JpegImagePlugin.py +ImageProcessor.activity/PIL/McIdasImagePlugin.py +ImageProcessor.activity/PIL/MicImagePlugin.py +ImageProcessor.activity/PIL/MpegImagePlugin.py +ImageProcessor.activity/PIL/MspImagePlugin.py +ImageProcessor.activity/PIL/OleFileIO.py +ImageProcessor.activity/PIL/PaletteFile.py +ImageProcessor.activity/PIL/PalmImagePlugin.py +ImageProcessor.activity/PIL/PcdImagePlugin.py +ImageProcessor.activity/PIL/PcfFontFile.py +ImageProcessor.activity/PIL/PcxImagePlugin.py +ImageProcessor.activity/PIL/PdfImagePlugin.py +ImageProcessor.activity/ProgressDialog.py +ImageProcessor.activity/setup.py +ImageProcessor.activity/toolbar.py diff --git a/PIL/ArgImagePlugin.py b/PIL/ArgImagePlugin.py new file mode 100644 index 0000000..48582d9 --- /dev/null +++ b/PIL/ArgImagePlugin.py @@ -0,0 +1,498 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library. +# $Id: ArgImagePlugin.py 2309 2005-03-02 15:06:34Z fredrik $ +# +# ARG animation support code +# +# history: +# 1996-12-30 fl Created +# 1996-01-06 fl Added safe scripting environment +# 1996-01-10 fl Added JHDR, UHDR and sYNC support +# 2005-03-02 fl Removed AAPP and ARUN support +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996-97. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.4" + +import marshal, string + +import Image, ImageFile, ImagePalette + +from PngImagePlugin import i16, i32, ChunkStream, _MODES + +MAGIC = "\212ARG\r\n\032\n" + +# -------------------------------------------------------------------- +# ARG parser + +class ArgStream(ChunkStream): + "Parser callbacks for ARG data" + + def __init__(self, fp): + + ChunkStream.__init__(self, fp) + + self.eof = 0 + + self.im = None + self.palette = None + + self.__reset() + + def __reset(self): + + # reset decoder state (called on init and sync) + + self.count = 0 + self.id = None + self.action = ("NONE",) + + self.images = {} + self.names = {} + + + def chunk_AHDR(self, offset, bytes): + "AHDR -- animation header" + + # assertions + if self.count != 0: + raise SyntaxError, "misplaced AHDR chunk" + + s = self.fp.read(bytes) + self.size = i32(s), i32(s[4:]) + try: + self.mode, self.rawmode = _MODES[(ord(s[8]), ord(s[9]))] + except: + raise SyntaxError, "unknown ARG mode" + + if Image.DEBUG: + print "AHDR size", self.size + print "AHDR mode", self.mode, self.rawmode + + return s + + def chunk_AFRM(self, offset, bytes): + "AFRM -- next frame follows" + + # assertions + if self.count != 0: + raise SyntaxError, "misplaced AFRM chunk" + + self.show = 1 + self.id = 0 + self.count = 1 + self.repair = None + + s = self.fp.read(bytes) + if len(s) >= 2: + self.id = i16(s) + if len(s) >= 4: + self.count = i16(s[2:4]) + if len(s) >= 6: + self.repair = i16(s[4:6]) + else: + self.repair = None + + if Image.DEBUG: + print "AFRM", self.id, self.count + + return s + + def chunk_ADEF(self, offset, bytes): + "ADEF -- store image" + + # assertions + if self.count != 0: + raise SyntaxError, "misplaced ADEF chunk" + + self.show = 0 + self.id = 0 + self.count = 1 + self.repair = None + + s = self.fp.read(bytes) + if len(s) >= 2: + self.id = i16(s) + if len(s) >= 4: + self.count = i16(s[2:4]) + + if Image.DEBUG: + print "ADEF", self.id, self.count + + return s + + def chunk_NAME(self, offset, bytes): + "NAME -- name the current image" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced NAME chunk" + + name = self.fp.read(bytes) + self.names[self.id] = name + + return name + + def chunk_AEND(self, offset, bytes): + "AEND -- end of animation" + + if Image.DEBUG: + print "AEND" + + self.eof = 1 + + raise EOFError, "end of ARG file" + + def __getmodesize(self, s, full=1): + + size = i32(s), i32(s[4:]) + + try: + mode, rawmode = _MODES[(ord(s[8]), ord(s[9]))] + except: + raise SyntaxError, "unknown image mode" + + if full: + if ord(s[12]): + pass # interlace not yet supported + if ord(s[11]): + raise SyntaxError, "unknown filter category" + + return size, mode, rawmode + + def chunk_PAST(self, offset, bytes): + "PAST -- paste one image into another" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced PAST chunk" + + if self.repair is not None: + # we must repair the target image before we + # start pasting + + # brute force; a better solution would be to + # update only the dirty rectangles in images[id]. + # note that if images[id] doesn't exist, it must + # be created + + self.images[self.id] = self.images[self.repair].copy() + self.repair = None + + s = self.fp.read(bytes) + im = self.images[i16(s)] + x, y = i32(s[2:6]), i32(s[6:10]) + bbox = x, y, im.size[0]+x, im.size[1]+y + + if im.mode in ["RGBA"]: + # paste with transparency + # FIXME: should handle P+transparency as well + self.images[self.id].paste(im, bbox, im) + else: + # paste without transparency + self.images[self.id].paste(im, bbox) + + self.action = ("PAST",) + self.__store() + + return s + + def chunk_BLNK(self, offset, bytes): + "BLNK -- create blank image" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced BLNK chunk" + + s = self.fp.read(bytes) + size, mode, rawmode = self.__getmodesize(s, 0) + + # store image (FIXME: handle colour) + self.action = ("BLNK",) + self.im = Image.core.fill(mode, size, 0) + self.__store() + + return s + + def chunk_IHDR(self, offset, bytes): + "IHDR -- full image follows" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced IHDR chunk" + + # image header + s = self.fp.read(bytes) + size, mode, rawmode = self.__getmodesize(s) + + # decode and store image + self.action = ("IHDR",) + self.im = Image.core.new(mode, size) + self.decoder = Image.core.zip_decoder(rawmode) + self.decoder.setimage(self.im, (0,0) + size) + self.data = "" + + return s + + def chunk_DHDR(self, offset, bytes): + "DHDR -- delta image follows" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced DHDR chunk" + + s = self.fp.read(bytes) + + size, mode, rawmode = self.__getmodesize(s) + + # delta header + diff = ord(s[13]) + offs = i32(s[14:18]), i32(s[18:22]) + + bbox = offs + (offs[0]+size[0], offs[1]+size[1]) + + if Image.DEBUG: + print "DHDR", diff, bbox + + # FIXME: decode and apply image + self.action = ("DHDR", diff, bbox) + + # setup decoder + self.im = Image.core.new(mode, size) + + self.decoder = Image.core.zip_decoder(rawmode) + self.decoder.setimage(self.im, (0,0) + size) + + self.data = "" + + return s + + def chunk_JHDR(self, offset, bytes): + "JHDR -- JPEG image follows" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced JHDR chunk" + + # image header + s = self.fp.read(bytes) + size, mode, rawmode = self.__getmodesize(s, 0) + + # decode and store image + self.action = ("JHDR",) + self.im = Image.core.new(mode, size) + self.decoder = Image.core.jpeg_decoder(rawmode) + self.decoder.setimage(self.im, (0,0) + size) + self.data = "" + + return s + + def chunk_UHDR(self, offset, bytes): + "UHDR -- uncompressed image data follows (EXPERIMENTAL)" + + # assertions + if self.count == 0: + raise SyntaxError, "misplaced UHDR chunk" + + # image header + s = self.fp.read(bytes) + size, mode, rawmode = self.__getmodesize(s, 0) + + # decode and store image + self.action = ("UHDR",) + self.im = Image.core.new(mode, size) + self.decoder = Image.core.raw_decoder(rawmode) + self.decoder.setimage(self.im, (0,0) + size) + self.data = "" + + return s + + def chunk_IDAT(self, offset, bytes): + "IDAT -- image data block" + + # pass compressed chunks through the decoder + s = self.fp.read(bytes) + self.data = self.data + s + n, e = self.decoder.decode(self.data) + if n < 0: + # end of image + if e < 0: + raise IOError, "decoder error %d" % e + else: + self.data = self.data[n:] + + return s + + def chunk_DEND(self, offset, bytes): + return self.chunk_IEND(offset, bytes) + + def chunk_JEND(self, offset, bytes): + return self.chunk_IEND(offset, bytes) + + def chunk_UEND(self, offset, bytes): + return self.chunk_IEND(offset, bytes) + + def chunk_IEND(self, offset, bytes): + "IEND -- end of image" + + # we now have a new image. carry out the operation + # defined by the image header. + + # won't need these anymore + del self.decoder + del self.data + + self.__store() + + return self.fp.read(bytes) + + def __store(self): + + # apply operation + cid = self.action[0] + + if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]: + # store + self.images[self.id] = self.im + + elif cid == "DHDR": + # paste + cid, mode, bbox = self.action + im0 = self.images[self.id] + im1 = self.im + if mode == 0: + im1 = im1.chop_add_modulo(im0.crop(bbox)) + im0.paste(im1, bbox) + + self.count = self.count - 1 + + if self.count == 0 and self.show: + self.im = self.images[self.id] + raise EOFError # end of this frame + + def chunk_PLTE(self, offset, bytes): + "PLTE -- palette data" + + s = self.fp.read(bytes) + if self.mode == "P": + self.palette = ImagePalette.raw("RGB", s) + return s + + def chunk_sYNC(self, offset, bytes): + "SYNC -- reset decoder" + + if self.count != 0: + raise SyntaxError, "misplaced sYNC chunk" + + s = self.fp.read(bytes) + self.__reset() + return s + + +# -------------------------------------------------------------------- +# ARG reader + +def _accept(prefix): + return prefix[:8] == MAGIC + +## +# Image plugin for the experimental Animated Raster Graphics format. + +class ArgImageFile(ImageFile.ImageFile): + + format = "ARG" + format_description = "Animated raster graphics" + + def _open(self): + + if self.fp.read(8) != MAGIC: + raise SyntaxError, "not an ARG file" + + self.arg = ArgStream(self.fp) + + # read and process the first chunk (AHDR) + + cid, offset, bytes = self.arg.read() + + if cid != "AHDR": + raise SyntaxError, "expected an AHDR chunk" + + s = self.arg.call(cid, offset, bytes) + + self.arg.crc(cid, s) + + # image characteristics + self.mode = self.arg.mode + self.size = self.arg.size + + def load(self): + + if self.arg.im is None: + self.seek(0) + + # image data + self.im = self.arg.im + self.palette = self.arg.palette + + # set things up for further processing + Image.Image.load(self) + + def seek(self, frame): + + if self.arg.eof: + raise EOFError, "end of animation" + + self.fp = self.arg.fp + + while 1: + + # + # process chunks + + cid, offset, bytes = self.arg.read() + + if self.arg.eof: + raise EOFError, "end of animation" + + try: + s = self.arg.call(cid, offset, bytes) + except EOFError: + break + + except "glurk": # AttributeError + if Image.DEBUG: + print cid, bytes, "(unknown)" + s = self.fp.read(bytes) + + self.arg.crc(cid, s) + + self.fp.read(4) # ship extra CRC + + def tell(self): + return 0 + + def verify(self): + "Verify ARG file" + + # back up to first chunk + self.fp.seek(8) + + self.arg.verify(self) + self.arg.close() + + self.fp = None + +# +# -------------------------------------------------------------------- + +Image.register_open("ARG", ArgImageFile, _accept) + +Image.register_extension("ARG", ".arg") + +Image.register_mime("ARG", "video/x-arg") diff --git a/PIL/BdfFontFile.py b/PIL/BdfFontFile.py new file mode 100644 index 0000000..9297a18 --- /dev/null +++ b/PIL/BdfFontFile.py @@ -0,0 +1,133 @@ +# +# The Python Imaging Library +# $Id: BdfFontFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# bitmap distribution font (bdf) file parser +# +# history: +# 1996-05-16 fl created (as bdf2pil) +# 1997-08-25 fl converted to FontFile driver +# 2001-05-25 fl removed bogus __init__ call +# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev) +# 2003-04-22 fl more robustification (from Graham Dumpleton) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +import Image +import FontFile + +import string + +# -------------------------------------------------------------------- +# parse X Bitmap Distribution Format (BDF) +# -------------------------------------------------------------------- + +bdf_slant = { + "R": "Roman", + "I": "Italic", + "O": "Oblique", + "RI": "Reverse Italic", + "RO": "Reverse Oblique", + "OT": "Other" +} + +bdf_spacing = { + "P": "Proportional", + "M": "Monospaced", + "C": "Cell" +} + +def bdf_char(f): + + # skip to STARTCHAR + while 1: + s = f.readline() + if not s: + return None + if s[:9] == "STARTCHAR": + break + id = string.strip(s[9:]) + + # load symbol properties + props = {} + while 1: + s = f.readline() + if not s or s[:6] == "BITMAP": + break + i = string.find(s, " ") + props[s[:i]] = s[i+1:-1] + + # load bitmap + bitmap = [] + while 1: + s = f.readline() + if not s or s[:7] == "ENDCHAR": + break + bitmap.append(s[:-1]) + bitmap = string.join(bitmap, "") + + [x, y, l, d] = map(int, string.split(props["BBX"])) + [dx, dy] = map(int, string.split(props["DWIDTH"])) + + bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y) + + try: + im = Image.fromstring("1", (x, y), bitmap, "hex", "1") + except ValueError: + # deal with zero-width characters + im = Image.new("1", (x, y)) + + return id, int(props["ENCODING"]), bbox, im + +## +# Font file plugin for the X11 BDF format. + +class BdfFontFile(FontFile.FontFile): + + def __init__(self, fp): + + FontFile.FontFile.__init__(self) + + s = fp.readline() + if s[:13] != "STARTFONT 2.1": + raise SyntaxError, "not a valid BDF file" + + props = {} + comments = [] + + while 1: + s = fp.readline() + if not s or s[:13] == "ENDPROPERTIES": + break + i = string.find(s, " ") + props[s[:i]] = s[i+1:-1] + if s[:i] in ["COMMENT", "COPYRIGHT"]: + if string.find(s, "LogicalFontDescription") < 0: + comments.append(s[i+1:-1]) + + font = string.split(props["FONT"], "-") + + font[4] = bdf_slant[string.upper(font[4])] + font[11] = bdf_spacing[string.upper(font[11])] + + ascent = int(props["FONT_ASCENT"]) + descent = int(props["FONT_DESCENT"]) + + fontname = string.join(font[1:], ";") + + # print "#", fontname + # for i in comments: + # print "#", i + + font = [] + while 1: + c = bdf_char(fp) + if not c: + break + id, ch, (xy, dst, src), im = c + if ch >= 0 and ch < len(self.glyph): + self.glyph[ch] = xy, dst, src, im diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py new file mode 100644 index 0000000..a939d9b --- /dev/null +++ b/PIL/BmpImagePlugin.py @@ -0,0 +1,245 @@ +# +# The Python Imaging Library. +# $Id: BmpImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# BMP file handler +# +# Windows (and OS/2) native bitmap storage format. +# +# history: +# 1995-09-01 fl Created +# 1996-04-30 fl Added save +# 1997-08-27 fl Fixed save of 1-bit images +# 1998-03-06 fl Load P images as L where possible +# 1998-07-03 fl Load P images as 1 where possible +# 1998-12-29 fl Handle small palettes +# 2002-12-30 fl Fixed load of 1-bit palette images +# 2003-04-21 fl Fixed load of 1-bit monochrome images +# 2003-04-23 fl Added limited support for BI_BITFIELDS compression +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1995-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.7" + + +import string +import Image, ImageFile, ImagePalette + + +# +# -------------------------------------------------------------------- +# Read BMP file + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + + +BIT2MODE = { + # bits => mode, rawmode + 1: ("P", "P;1"), + 4: ("P", "P;4"), + 8: ("P", "P"), + 16: ("RGB", "BGR;16"), + 24: ("RGB", "BGR"), + 32: ("RGB", "BGRX") +} + +def _accept(prefix): + return prefix[:2] == "BM" + +## +# Image plugin for the Windows BMP format. + +class BmpImageFile(ImageFile.ImageFile): + + format = "BMP" + format_description = "Windows Bitmap" + + def _bitmap(self, header = 0, offset = 0): + + if header: + self.fp.seek(header) + + read = self.fp.read + + # CORE/INFO + s = read(4) + s = s + ImageFile._safe_read(self.fp, i32(s)-4) + + if len(s) == 12: + + # OS/2 1.0 CORE + bits = i16(s[10:]) + self.size = i16(s[4:]), i16(s[6:]) + compression = 0 + lutsize = 3 + colors = 0 + + elif len(s) in [40, 64]: + + # WIN 3.1 or OS/2 2.0 INFO + bits = i16(s[14:]) + self.size = i32(s[4:]), i32(s[8:]) + compression = i32(s[16:]) + lutsize = 4 + colors = i32(s[32:]) + + else: + raise IOError("Unsupported BMP header type (%d)" % len(s)) + + if not colors: + colors = 1 << bits + + # MODE + try: + self.mode, rawmode = BIT2MODE[bits] + except KeyError: + raise IOError("Unsupported BMP pixel depth (%d)" % bits) + + if compression == 3: + # BI_BITFIELDS compression + mask = i32(read(4)), i32(read(4)), i32(read(4)) + if bits == 32 and mask == (0xff0000, 0x00ff00, 0x0000ff): + rawmode = "BGRX" + elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f): + rawmode = "BGR;16" + elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f): + rawmode = "BGR;15" + else: + # print bits, map(hex, mask) + raise IOError("Unsupported BMP bitfields layout") + elif compression != 0: + raise IOError("Unsupported BMP compression (%d)" % compression) + + # LUT + if self.mode == "P": + palette = [] + greyscale = 1 + if colors == 2: + indices = (0, 255) + else: + indices = range(colors) + for i in indices: + rgb = read(lutsize)[:3] + if rgb != chr(i)*3: + greyscale = 0 + palette.append(rgb) + if greyscale: + if colors == 2: + self.mode = rawmode = "1" + else: + self.mode = rawmode = "L" + else: + self.mode = "P" + self.palette = ImagePalette.raw( + "BGR", string.join(palette, "") + ) + + if not offset: + offset = self.fp.tell() + + self.tile = [("raw", + (0, 0) + self.size, + offset, + (rawmode, ((self.size[0]*bits+31)>>3)&(~3), -1))] + + self.info["compression"] = compression + + def _open(self): + + # HEAD + s = self.fp.read(14) + if s[:2] != "BM": + raise SyntaxError("Not a BMP file") + offset = i32(s[10:]) + + self._bitmap(offset=offset) + + +class DibImageFile(BmpImageFile): + + format = "DIB" + format_description = "Windows Bitmap" + + def _open(self): + self._bitmap() + +# +# -------------------------------------------------------------------- +# Write BMP file + +def o16(i): + return chr(i&255) + chr(i>>8&255) + +def o32(i): + return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255) + +SAVE = { + "1": ("1", 1, 2), + "L": ("L", 8, 256), + "P": ("P", 8, 256), + "RGB": ("BGR", 24, 0), +} + +def _save(im, fp, filename, check=0): + + try: + rawmode, bits, colors = SAVE[im.mode] + except KeyError: + raise IOError("cannot write mode %s as BMP" % im.mode) + + if check: + return check + + stride = ((im.size[0]*bits+7)/8+3)&(~3) + header = 40 # or 64 for OS/2 version 2 + offset = 14 + header + colors * 4 + image = stride * im.size[1] + + # bitmap header + fp.write("BM" + # file type (magic) + o32(offset+image) + # file size + o32(0) + # reserved + o32(offset)) # image data offset + + # bitmap info header + fp.write(o32(header) + # info header size + o32(im.size[0]) + # width + o32(im.size[1]) + # height + o16(1) + # planes + o16(bits) + # depth + o32(0) + # compression (0=uncompressed) + o32(image) + # size of bitmap + o32(1) + o32(1) + # resolution + o32(colors) + # colors used + o32(colors)) # colors important + + fp.write("\000" * (header - 40)) # padding (for OS/2 format) + + if im.mode == "1": + for i in (0, 255): + fp.write(chr(i) * 4) + elif im.mode == "L": + for i in range(256): + fp.write(chr(i) * 4) + elif im.mode == "P": + fp.write(im.im.getpalette("RGB", "BGRX")) + + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, stride, -1))]) + +# +# -------------------------------------------------------------------- +# Registry + +Image.register_open(BmpImageFile.format, BmpImageFile, _accept) +Image.register_save(BmpImageFile.format, _save) + +Image.register_extension(BmpImageFile.format, ".bmp") diff --git a/PIL/BufrStubImagePlugin.py b/PIL/BufrStubImagePlugin.py new file mode 100644 index 0000000..ee02659 --- /dev/null +++ b/PIL/BufrStubImagePlugin.py @@ -0,0 +1,68 @@ +# +# The Python Imaging Library +# $Id: BufrStubImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# BUFR stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile + +_handler = None + +## +# Install application-specific BUFR image handler. +# +# @param handler Handler object. + +def register_handler(handler): + global _handler + _handler = handler + +# -------------------------------------------------------------------- +# Image adapter + +def _accept(prefix): + return prefix[:4] == "BUFR" or prefix[:4] == "ZCZC" + +class BufrStubImageFile(ImageFile.StubImageFile): + + format = "BUFR" + format_description = "BUFR" + + def _open(self): + + offset = self.fp.tell() + + if not _accept(self.fp.read(8)): + raise SyntaxError("Not a BUFR file") + + self.fp.seek(offset) + + # make something up + self.mode = "F" + self.size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self): + return _handler + +def _save(im, fp, filename): + if _handler is None or not hasattr("_handler", "save"): + raise IOError("BUFR save handler not installed") + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept) +Image.register_save(BufrStubImageFile.format, _save) + +Image.register_extension(BufrStubImageFile.format, ".bufr") diff --git a/PIL/ContainerIO.py b/PIL/ContainerIO.py new file mode 100644 index 0000000..7b7283f --- /dev/null +++ b/PIL/ContainerIO.py @@ -0,0 +1,116 @@ +# +# The Python Imaging Library. +# $Id: ContainerIO.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# a class to read from a container file +# +# History: +# 1995-06-18 fl Created +# 1995-09-07 fl Added readline(), readlines() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +## +# A file object that provides read access to a part of an existing +# file (for example a TAR file). + +class ContainerIO: + + ## + # Create file object. + # + # @param file Existing file. + # @param offset Start of region, in bytes. + # @param length Size of region, in bytes. + + def __init__(self, file, offset, length): + self.fh = file + self.pos = 0 + self.offset = offset + self.length = length + self.fh.seek(offset) + + ## + # Always false. + + def isatty(self): + return 0 + + ## + # Move file pointer. + # + # @param offset Offset in bytes. + # @param mode Starting position. Use 0 for beginning of region, 1 + # for current offset, and 2 for end of region. You cannot move + # the pointer outside the defined region. + + def seek(self, offset, mode = 0): + if mode == 1: + self.pos = self.pos + offset + elif mode == 2: + self.pos = self.length + offset + else: + self.pos = offset + # clamp + self.pos = max(0, min(self.pos, self.length)) + self.fh.seek(self.offset + self.pos) + + ## + # Get current file pointer. + # + # @return Offset from start of region, in bytes. + + def tell(self): + return self.pos + + ## + # Read data. + # + # @def read(bytes=0) + # @param bytes Number of bytes to read. If omitted or zero, + # read until end of region. + # @return An 8-bit string. + + def read(self, n = 0): + if n: + n = min(n, self.length - self.pos) + else: + n = self.length - self.pos + if not n: # EOF + return "" + self.pos = self.pos + n + return self.fh.read(n) + + ## + # Read a line of text. + # + # @return An 8-bit string. + + def readline(self): + s = "" + while 1: + c = self.read(1) + if not c: + break + s = s + c + if c == "\n": + break + return s + + ## + # Read multiple lines of text. + # + # @return A list of 8-bit strings. + + def readlines(self): + l = [] + while 1: + s = self.readline() + if not s: + break + l.append(s) + return l diff --git a/PIL/CurImagePlugin.py b/PIL/CurImagePlugin.py new file mode 100644 index 0000000..b879da3 --- /dev/null +++ b/PIL/CurImagePlugin.py @@ -0,0 +1,90 @@ +# +# The Python Imaging Library. +# $Id: CurImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Windows Cursor support for PIL +# +# notes: +# uses BmpImagePlugin.py to read the bitmap data. +# +# history: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + +import string + +import Image, BmpImagePlugin + + +# +# -------------------------------------------------------------------- + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + + +def _accept(prefix): + return prefix[:4] == "\0\0\2\0" + +## +# Image plugin for Windows Cursor files. + +class CurImageFile(BmpImagePlugin.BmpImageFile): + + format = "CUR" + format_description = "Windows Cursor" + + def _open(self): + + offset = self.fp.tell() + + # check magic + s = self.fp.read(6) + if not _accept(s): + raise SyntaxError, "not an CUR file" + + # pick the largest cursor in the file + m = "" + for i in range(i16(s[4:])): + s = self.fp.read(16) + if not m: + m = s + elif ord(s[0]) > ord(m[0]) and ord(s[1]) > ord(m[1]): + m = s + #print "width", ord(s[0]) + #print "height", ord(s[1]) + #print "colors", ord(s[2]) + #print "reserved", ord(s[3]) + #print "hotspot x", i16(s[4:]) + #print "hotspot y", i16(s[6:]) + #print "bytes", i32(s[8:]) + #print "offset", i32(s[12:]) + + # load as bitmap + self._bitmap(i32(m[12:]) + offset) + + # patch up the bitmap height + self.size = self.size[0], self.size[1]/2 + d, e, o, a = self.tile[0] + self.tile[0] = d, (0,0)+self.size, o, a + + return + + +# +# -------------------------------------------------------------------- + +Image.register_open("CUR", CurImageFile, _accept) + +Image.register_extension("CUR", ".cur") diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py new file mode 100644 index 0000000..3a0df01 --- /dev/null +++ b/PIL/DcxImagePlugin.py @@ -0,0 +1,78 @@ +# +# The Python Imaging Library. +# $Id: DcxImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# DCX file handling +# +# DCX is a container file format defined by Intel, commonly used +# for fax applications. Each DCX file consists of a directory +# (a list of file offsets) followed by a set of (usually 1-bit) +# PCX files. +# +# History: +# 1995-09-09 fl Created +# 1996-03-20 fl Properly derived from PcxImageFile. +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2002-07-30 fl Fixed file handling +# +# Copyright (c) 1997-98 by Secret Labs AB. +# Copyright (c) 1995-96 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.2" + +import Image, ImageFile + +from PcxImagePlugin import PcxImageFile + +MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + +def _accept(prefix): + return i32(prefix) == MAGIC + +## +# Image plugin for the Intel DCX format. + +class DcxImageFile(PcxImageFile): + + format = "DCX" + format_description = "Intel DCX" + + def _open(self): + + # Header + s = self.fp.read(4) + if i32(s) != MAGIC: + raise SyntaxError, "not a DCX file" + + # Component directory + self._offset = [] + for i in range(1024): + offset = i32(self.fp.read(4)) + if not offset: + break + self._offset.append(offset) + + self.__fp = self.fp + self.seek(0) + + def seek(self, frame): + if frame >= len(self._offset): + raise EOFError("attempt to seek outside DCX directory") + self.frame = frame + self.fp = self.__fp + self.fp.seek(self._offset[frame]) + PcxImageFile._open(self) + + def tell(self): + return self.frame + + +Image.register_open("DCX", DcxImageFile, _accept) + +Image.register_extension("DCX", ".dcx") diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py new file mode 100644 index 0000000..e0a608e --- /dev/null +++ b/PIL/EpsImagePlugin.py @@ -0,0 +1,349 @@ +# +# The Python Imaging Library. +# $Id: EpsImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# EPS file handling +# +# History: +# 1995-09-01 fl Created (0.1) +# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2) +# 1996-08-22 fl Don't choke on floating point BoundingBox values +# 1996-08-23 fl Handle files from Macintosh (0.3) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.5" + +import re, string +import Image, ImageFile + +# +# -------------------------------------------------------------------- + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + +def o32(i): + return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255) + +split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") +field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") + +def Ghostscript(tile, size, fp): + """Render an image using Ghostscript (Unix only)""" + + # Unpack decoder tile + decoder, tile, offset, data = tile[0] + length, bbox = data + + import tempfile, os + + file = tempfile.mktemp() + + # Build ghostscript command + command = ["gs", + "-q", # quite mode + "-g%dx%d" % size, # set output geometry (pixels) + "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode + "-sDEVICE=ppmraw", # ppm driver + "-sOutputFile=%s" % file,# output file + "- >/dev/null 2>/dev/null"] + + command = string.join(command) + + # push data through ghostscript + try: + gs = os.popen(command, "w") + # adjust for image origin + if bbox[0] != 0 or bbox[1] != 0: + gs.write("%d %d translate\n" % (-bbox[0], -bbox[1])) + fp.seek(offset) + while length > 0: + s = fp.read(8192) + if not s: + break + length = length - len(s) + gs.write(s) + status = gs.close() + if status: + raise IOError("gs failed (status %d)" % status) + im = Image.core.open_ppm(file) + finally: + try: os.unlink(file) + except: pass + + return im + + +class PSFile: + """Wrapper that treats either CR or LF as end of line.""" + def __init__(self, fp): + self.fp = fp + self.char = None + def __getattr__(self, id): + v = getattr(self.fp, id) + setattr(self, id, v) + return v + def seek(self, offset, whence=0): + self.char = None + self.fp.seek(offset, whence) + def tell(self): + pos = self.fp.tell() + if self.char: + pos = pos - 1 + return pos + def readline(self): + s = "" + if self.char: + c = self.char + self.char = None + else: + c = self.fp.read(1) + while c not in "\r\n": + s = s + c + c = self.fp.read(1) + if c == "\r": + self.char = self.fp.read(1) + if self.char == "\n": + self.char = None + return s + "\n" + + +def _accept(prefix): + return prefix[:4] == "%!PS" or i32(prefix) == 0xC6D3D0C5L + +## +# Image plugin for Encapsulated Postscript. This plugin supports only +# a few variants of this format. + +class EpsImageFile(ImageFile.ImageFile): + """EPS File Parser for the Python Imaging Library""" + + format = "EPS" + format_description = "Encapsulated Postscript" + + def _open(self): + + # FIXME: should check the first 512 bytes to see if this + # really is necessary (platform-dependent, though...) + + fp = PSFile(self.fp) + + # HEAD + s = fp.read(512) + if s[:4] == "%!PS": + offset = 0 + fp.seek(0, 2) + length = fp.tell() + elif i32(s) == 0xC6D3D0C5L: + offset = i32(s[4:]) + length = i32(s[8:]) + fp.seek(offset) + else: + raise SyntaxError, "not an EPS file" + + fp.seek(offset) + + box = None + + self.mode = "RGB" + self.size = 1, 1 # FIXME: huh? + + # + # Load EPS header + + s = fp.readline() + + while s: + + if len(s) > 255: + raise SyntaxError, "not an EPS file" + + if s[-2:] == '\r\n': + s = s[:-2] + elif s[-1:] == '\n': + s = s[:-1] + + try: + m = split.match(s) + except re.error, v: + raise SyntaxError, "not an EPS file" + + if m: + k, v = m.group(1, 2) + self.info[k] = v + if k == "BoundingBox": + try: + # Note: The DSC spec says that BoundingBox + # fields should be integers, but some drivers + # put floating point values there anyway. + box = map(int, map(float, string.split(v))) + self.size = box[2] - box[0], box[3] - box[1] + self.tile = [("eps", (0,0) + self.size, offset, + (length, box))] + except: + pass + + else: + + m = field.match(s) + + if m: + k = m.group(1) + if k == "EndComments": + break + if k[:8] == "PS-Adobe": + self.info[k[:8]] = k[9:] + else: + self.info[k] = "" + else: + raise IOError, "bad EPS header" + + s = fp.readline() + + if s[:1] != "%": + break + + + # + # Scan for an "ImageData" descriptor + + while s[0] == "%": + + if len(s) > 255: + raise SyntaxError, "not an EPS file" + + if s[-2:] == '\r\n': + s = s[:-2] + elif s[-1:] == '\n': + s = s[:-1] + + if s[:11] == "%ImageData:": + + [x, y, bi, mo, z3, z4, en, id] =\ + string.split(s[11:], maxsplit=7) + + x = int(x); y = int(y) + + bi = int(bi) + mo = int(mo) + + en = int(en) + + if en == 1: + decoder = "eps_binary" + elif en == 2: + decoder = "eps_hex" + else: + break + if bi != 8: + break + if mo == 1: + self.mode = "L" + elif mo == 2: + self.mode = "LAB" + elif mo == 3: + self.mode = "RGB" + else: + break + + if id[:1] == id[-1:] == '"': + id = id[1:-1] + + # Scan forward to the actual image data + while 1: + s = fp.readline() + if not s: + break + if s[:len(id)] == id: + self.size = x, y + self.tile2 = [(decoder, + (0, 0, x, y), + fp.tell(), + 0)] + return + + s = fp.readline() + if not s: + break + + if not box: + raise IOError, "cannot determine EPS bounding box" + + def load(self): + # Load EPS via Ghostscript + if not self.tile: + return + self.im = Ghostscript(self.tile, self.size, self.fp) + self.mode = self.im.mode + self.size = self.im.size + self.tile = [] + +# +# -------------------------------------------------------------------- + +def _save(im, fp, filename, eps=1): + """EPS Writer for the Python Imaging Library.""" + + # + # make sure image data is available + im.load() + + # + # determine postscript image mode + if im.mode == "L": + operator = (8, 1, "image") + elif im.mode == "RGB": + operator = (8, 3, "false 3 colorimage") + elif im.mode == "CMYK": + operator = (8, 4, "false 4 colorimage") + else: + raise ValueError, "image mode is not supported" + + if eps: + # + # write EPS header + fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") + fp.write("%%Creator: PIL 0.1 EpsEncode\n") + #fp.write("%%CreationDate: %s"...) + fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) + fp.write("%%Pages: 1\n") + fp.write("%%EndComments\n") + fp.write("%%Page: 1 1\n") + fp.write("%%ImageData: %d %d " % im.size) + fp.write("%d %d 0 1 1 \"%s\"\n" % operator) + + # + # image header + fp.write("gsave\n") + fp.write("10 dict begin\n") + fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) + fp.write("%d %d scale\n" % im.size) + fp.write("%d %d 8\n" % im.size) # <= bits + fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) + fp.write("{ currentfile buf readhexstring pop } bind\n") + fp.write("%s\n" % operator[2]) + + ImageFile._save(im, fp, [("eps", (0,0)+im.size, 0, None)]) + + fp.write("\n%%%%EndBinary\n") + fp.write("grestore end\n") + fp.flush() + +# +# -------------------------------------------------------------------- + +Image.register_open(EpsImageFile.format, EpsImageFile, _accept) + +Image.register_save(EpsImageFile.format, _save) + +Image.register_extension(EpsImageFile.format, ".ps") +Image.register_extension(EpsImageFile.format, ".eps") + +Image.register_mime(EpsImageFile.format, "application/postscript") diff --git a/PIL/ExifTags.py b/PIL/ExifTags.py new file mode 100644 index 0000000..01a6b74 --- /dev/null +++ b/PIL/ExifTags.py @@ -0,0 +1,157 @@ +# +# The Python Imaging Library. +# $Id: ExifTags.py 2631 2006-02-12 23:41:52Z fredrik $ +# +# EXIF tags +# +# Copyright (c) 2003 by Secret Labs AB +# +# See the README file for information on usage and redistribution. +# + +## +# This module provides constants and clear-text names for various +# well-known EXIF tags. +## + +## +# Maps EXIF tags to tag names. + +TAGS = { + + # possibly incomplete + 0x0100: "ImageWidth", + 0x0101: "ImageLength", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x010e: "ImageDescription", + 0x010f: "Make", + 0x0110: "Model", + 0x0111: "StripOffsets", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteConunts", + 0x011a: "XResolution", + 0x011a: "XResolution", + 0x011b: "YResolution", + 0x011b: "YResolution", + 0x011c: "PlanarConfiguration", + 0x0128: "ResolutionUnit", + 0x0128: "ResolutionUnit", + 0x012d: "TransferFunction", + 0x0131: "Software", + 0x0132: "DateTime", + 0x013b: "Artist", + 0x013e: "WhitePoint", + 0x013f: "PrimaryChromaticities", + 0x0201: "JpegIFOffset", + 0x0202: "JpegIFByteCount", + 0x0211: "YCbCrCoefficients", + 0x0211: "YCbCrCoefficients", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x0213: "YCbCrPositioning", + 0x0214: "ReferenceBlackWhite", + 0x0214: "ReferenceBlackWhite", + 0x1000: "RelatedImageFileFormat", + 0x1001: "RelatedImageLength", + 0x1001: "RelatedImageWidth", + 0x828d: "CFARepeatPatternDim", + 0x828e: "CFAPattern", + 0x828f: "BatteryLevel", + 0x8298: "Copyright", + 0x829a: "ExposureTime", + 0x829d: "FNumber", + 0x8769: "ExifOffset", + 0x8773: "InterColorProfile", + 0x8822: "ExposureProgram", + 0x8824: "SpectralSensitivity", + 0x8825: "GPSInfo", + 0x8827: "ISOSpeedRatings", + 0x8828: "OECF", + 0x8829: "Interlace", + 0x882a: "TimeZoneOffset", + 0x882b: "SelfTimerMode", + 0x9000: "ExifVersion", + 0x9003: "DateTimeOriginal", + 0x9004: "DateTimeDigitized", + 0x9101: "ComponentsConfiguration", + 0x9102: "CompressedBitsPerPixel", + 0x9201: "ShutterSpeedValue", + 0x9202: "ApertureValue", + 0x9203: "BrightnessValue", + 0x9204: "ExposureBiasValue", + 0x9205: "MaxApertureValue", + 0x9206: "SubjectDistance", + 0x9207: "MeteringMode", + 0x9208: "LightSource", + 0x9209: "Flash", + 0x920a: "FocalLength", + 0x920b: "FlashEnergy", + 0x920c: "SpatialFrequencyResponse", + 0x920d: "Noise", + 0x9211: "ImageNumber", + 0x9212: "SecurityClassification", + 0x9213: "ImageHistory", + 0x9214: "SubjectLocation", + 0x9215: "ExposureIndex", + 0x9216: "TIFF/EPStandardID", + 0x927c: "MakerNote", + 0x9286: "UserComment", + 0x9290: "SubsecTime", + 0x9291: "SubsecTimeOriginal", + 0x9292: "SubsecTimeDigitized", + 0xa000: "FlashPixVersion", + 0xa001: "ColorSpace", + 0xa002: "ExifImageWidth", + 0xa003: "ExifImageHeight", + 0xa004: "RelatedSoundFile", + 0xa005: "ExifInteroperabilityOffset", + 0xa20b: "FlashEnergy", + 0xa20c: "SpatialFrequencyResponse", + 0xa20e: "FocalPlaneXResolution", + 0xa20f: "FocalPlaneYResolution", + 0xa210: "FocalPlaneResolutionUnit", + 0xa214: "SubjectLocation", + 0xa215: "ExposureIndex", + 0xa217: "SensingMethod", + 0xa300: "FileSource", + 0xa301: "SceneType", + 0xa302: "CFAPattern", + +} + +## +# Maps EXIF GSP tags to tag names. + +GPSTAGS = { + 0: "GPSVersionID", + 1: "GPSLatitudeRef", + 2: "GPSLatitude", + 3: "GPSLongitudeRef", + 4: "GPSLongitude", + 5: "GPSAltitudeRef", + 6: "GPSAltitude", + 7: "GPSTimeStamp", + 8: "GPSSatellites", + 9: "GPSStatus", + 10: "GPSMeasureMode", + 11: "GPSDOP", + 12: "GPSSpeedRef", + 13: "GPSSpeed", + 14: "GPSTrackRef", + 15: "GPSTrack", + 16: "GPSImgDirectionRef", + 17: "GPSImgDirection", + 18: "GPSMapDatum", + 19: "GPSDestLatitudeRef", + 20: "GPSDestLatitude", + 21: "GPSDestLongitudeRef", + 22: "GPSDestLongitude", + 23: "GPSDestBearingRef", + 24: "GPSDestBearing", + 25: "GPSDestDistanceRef", + 26: "GPSDestDistance" +} diff --git a/PIL/FitsStubImagePlugin.py b/PIL/FitsStubImagePlugin.py new file mode 100644 index 0000000..05f5d46 --- /dev/null +++ b/PIL/FitsStubImagePlugin.py @@ -0,0 +1,73 @@ +# +# The Python Imaging Library +# $Id: FitsStubImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# FITS stub adapter +# +# Copyright (c) 1998-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile + +_handler = None + +## +# Install application-specific FITS image handler. +# +# @param handler Handler object. + +def register_handler(handler): + global _handler + _handler = handler + +# -------------------------------------------------------------------- +# Image adapter + +def _accept(prefix): + return prefix[:6] == "SIMPLE" + +class FITSStubImageFile(ImageFile.StubImageFile): + + format = "FITS" + format_description = "FITS" + + def _open(self): + + offset = self.fp.tell() + + if not _accept(self.fp.read(6)): + raise SyntaxError("Not a FITS file") + + # FIXME: add more sanity checks here; mandatory header items + # include SIMPLE, BITPIX, NAXIS, etc. + + self.fp.seek(offset) + + # make something up + self.mode = "F" + self.size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self): + return _handler + + +def _save(im, fp, filename): + if _handler is None or not hasattr("_handler", "save"): + raise IOError("FITS save handler not installed") + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(FITSStubImageFile.format, FITSStubImageFile, _accept) +Image.register_save(FITSStubImageFile.format, _save) + +Image.register_extension(FITSStubImageFile.format, ".fit") +Image.register_extension(FITSStubImageFile.format, ".fits") diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py new file mode 100644 index 0000000..5b1bf8e --- /dev/null +++ b/PIL/FliImagePlugin.py @@ -0,0 +1,142 @@ +# +# The Python Imaging Library. +# $Id: FliImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# FLI/FLC file handling. +# +# History: +# 95-09-01 fl Created +# 97-01-03 fl Fixed parser, setup decoder tile +# 98-07-15 fl Renamed offset attribute to avoid name clash +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.2" + +import Image, ImageFile, ImagePalette +import string + + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + +# +# decoder + +def _accept(prefix): + return i16(prefix[4:6]) in [0xAF11, 0xAF12] + +## +# Image plugin for the FLI/FLC animation format. Use the seek +# method to load individual frames. + +class FliImageFile(ImageFile.ImageFile): + + format = "FLI" + format_description = "Autodesk FLI/FLC Animation" + + def _open(self): + + # HEAD + s = self.fp.read(128) + magic = i16(s[4:6]) + if magic not in [0xAF11, 0xAF12]: + raise SyntaxError, "not an FLI/FLC file" + + # image characteristics + self.mode = "P" + self.size = i16(s[8:10]), i16(s[10:12]) + + # animation speed + duration = i32(s[16:20]) + if magic == 0xAF11: + duration = (duration * 1000) / 70 + self.info["duration"] = duration + + # look for palette + palette = map(lambda a: (a,a,a), range(256)) + + s = self.fp.read(16) + + self.__offset = 128 + + if i16(s[4:6]) == 0xF100: + # prefix chunk; ignore it + self.__offset = self.__offset + i32(s) + s = self.fp.read(16) + + if i16(s[4:6]) == 0xF1FA: + # look for palette chunk + s = self.fp.read(6) + if i16(s[4:6]) == 11: + self._palette(palette, 2) + elif i16(s[4:6]) == 4: + self._palette(palette, 0) + + palette = map(lambda (r,g,b): chr(r)+chr(g)+chr(b), palette) + self.palette = ImagePalette.raw("RGB", string.join(palette, "")) + + # set things up to decode first frame + self.frame = -1 + self.__fp = self.fp + + self.seek(0) + + def _palette(self, palette, shift): + # load palette + + i = 0 + for e in range(i16(self.fp.read(2))): + s = self.fp.read(2) + i = i + ord(s[0]) + n = ord(s[1]) + if n == 0: + n = 256 + s = self.fp.read(n * 3) + for n in range(0, len(s), 3): + r = ord(s[n]) << shift + g = ord(s[n+1]) << shift + b = ord(s[n+2]) << shift + palette[i] = (r, g, b) + i = i + 1 + + def seek(self, frame): + + if frame != self.frame + 1: + raise ValueError, "cannot seek to frame %d" % frame + self.frame = frame + + # move to next frame + self.fp = self.__fp + self.fp.seek(self.__offset) + + s = self.fp.read(4) + if not s: + raise EOFError + + framesize = i32(s) + + self.decodermaxblock = framesize + self.tile = [("fli", (0,0)+self.size, self.__offset, None)] + + self.__offset = self.__offset + framesize + + def tell(self): + + return self.frame + +# +# registry + +Image.register_open("FLI", FliImageFile, _accept) + +Image.register_extension("FLI", ".fli") +Image.register_extension("FLI", ".flc") diff --git a/PIL/FontFile.py b/PIL/FontFile.py new file mode 100644 index 0000000..bf4b905 --- /dev/null +++ b/PIL/FontFile.py @@ -0,0 +1,146 @@ +# +# The Python Imaging Library +# $Id: FontFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# base class for raster font file parsers +# +# history: +# 1997-06-05 fl created +# 1997-08-19 fl restrict image width +# +# Copyright (c) 1997-1998 by Secret Labs AB +# Copyright (c) 1997-1998 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import os +import Image + +import marshal + +try: + import zlib +except ImportError: + zlib = None + +WIDTH = 800 + +def puti16(fp, values): + # write network order (big-endian) 16-bit sequence + for v in values: + if v < 0: + v = v + 65536 + fp.write(chr(v>>8&255) + chr(v&255)) + +## +# Base class for raster font file handlers. + +class FontFile: + + bitmap = None + + def __init__(self): + + self.info = {} + self.glyph = [None] * 256 + + def __getitem__(self, ix): + return self.glyph[ix] + + def compile(self): + "Create metrics and bitmap" + + if self.bitmap: + return + + # create bitmap large enough to hold all data + h = w = maxwidth = 0 + lines = 1 + for glyph in self: + if glyph: + d, dst, src, im = glyph + h = max(h, src[3] - src[1]) + w = w + (src[2] - src[0]) + if w > WIDTH: + lines = lines + 1 + w = (src[2] - src[0]) + maxwidth = max(maxwidth, w) + + xsize = maxwidth + ysize = lines * h + + if xsize == 0 and ysize == 0: + return "" + + self.ysize = h + + # paste glyphs into bitmap + self.bitmap = Image.new("1", (xsize, ysize)) + self.metrics = [None] * 256 + x = y = 0 + for i in range(256): + glyph = self[i] + if glyph: + d, dst, src, im = glyph + xx, yy = src[2] - src[0], src[3] - src[1] + x0, y0 = x, y + x = x + xx + if x > WIDTH: + x, y = 0, y + h + x0, y0 = x, y + x = xx + s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 + self.bitmap.paste(im.crop(src), s) + # print chr(i), dst, s + self.metrics[i] = d, dst, s + + + def save1(self, filename): + "Save font in version 1 format" + + self.compile() + + # font data + self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") + + # font metrics + fp = open(os.path.splitext(filename)[0] + ".pil", "wb") + fp.write("PILfont\n") + fp.write(";;;;;;%d;\n" % self.ysize) # HACK!!! + fp.write("DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, [0] * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) + fp.close() + + + def save2(self, filename): + "Save font in version 2 format" + + # THIS IS WORK IN PROGRESS + + self.compile() + + data = marshal.dumps((self.metrics, self.info)) + + if zlib: + data = "z" + zlib.compress(data, 9) + else: + data = "u" + data + + fp = open(os.path.splitext(filename)[0] + ".pil", "wb") + + fp.write("PILfont2\n" + self.name + "\n" + "DATA\n") + + fp.write(data) + + self.bitmap.save(fp, "PNG") + + fp.close() + + + save = save1 # for now diff --git a/PIL/FpxImagePlugin.py b/PIL/FpxImagePlugin.py new file mode 100644 index 0000000..d4a1aef --- /dev/null +++ b/PIL/FpxImagePlugin.py @@ -0,0 +1,226 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library. +# $Id: FpxImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# FlashPix support for PIL +# +# History: +# 97-01-25 fl Created (reads uncompressed RGB images only) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + + +import string + +import Image, ImageFile +from OleFileIO import * + + +# we map from colour field tuples to (mode, rawmode) descriptors +MODES = { + # opacity + (0x00007ffe): ("A", "L"), + # monochrome + (0x00010000,): ("L", "L"), + (0x00018000, 0x00017ffe): ("RGBA", "LA"), + # photo YCC + (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), + (0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"), + # standard RGB (NIFRGB) + (0x00030000, 0x00030001, 0x00030002): ("RGB","RGB"), + (0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA","RGBA"), +} + +# +# -------------------------------------------------------------------- + +def _accept(prefix): + return prefix[:8] == MAGIC + +## +# Image plugin for the FlashPix images. + +class FpxImageFile(ImageFile.ImageFile): + + format = "FPX" + format_description = "FlashPix" + + def _open(self): + # + # read the OLE directory and see if this is a likely + # to be a FlashPix file + + try: + self.ole = OleFileIO(self.fp) + except IOError: + raise SyntaxError, "not an FPX file; invalid OLE file" + + if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B": + raise SyntaxError, "not an FPX file; bad root CLSID" + + self._open_index(1) + + def _open_index(self, index = 1): + # + # get the Image Contents Property Set + + prop = self.ole.getproperties([ + "Data Object Store %06d" % index, + "\005Image Contents" + ]) + + # size (highest resolution) + + self.size = prop[0x1000002], prop[0x1000003] + + size = max(self.size) + i = 1 + while size > 64: + size = size / 2 + i = i + 1 + self.maxid = i - 1 + + # mode. instead of using a single field for this, flashpix + # requires you to specify the mode for each channel in each + # resolution subimage, and leaves it to the decoder to make + # sure that they all match. for now, we'll cheat and assume + # that this is always the case. + + id = self.maxid << 16 + + s = prop[0x2000002|id] + + colors = [] + for i in range(i32(s, 4)): + # note: for now, we ignore the "uncalibrated" flag + colors.append(i32(s, 8+i*4) & 0x7fffffff) + + self.mode, self.rawmode = MODES[tuple(colors)] + + # load JPEG tables, if any + self.jpeg = {} + for i in range(256): + id = 0x3000001|(i << 16) + if prop.has_key(id): + self.jpeg[i] = prop[id] + + # print len(self.jpeg), "tables loaded" + + self._open_subimage(1, self.maxid) + + def _open_subimage(self, index = 1, subimage = 0): + # + # setup tile descriptors for a given subimage + + stream = [ + "Data Object Store %06d" % index, + "Resolution %04d" % subimage, + "Subimage 0000 Header" + ] + + fp = self.ole.openstream(stream) + + # skip prefix + p = fp.read(28) + + # header stream + s = fp.read(36) + + size = i32(s, 4), i32(s, 8) + tilecount = i32(s, 12) + tilesize = i32(s, 16), i32(s, 20) + channels = i32(s, 24) + offset = i32(s, 28) + length = i32(s, 32) + + # print size, self.mode, self.rawmode + + if size != self.size: + raise IOError, "subimage mismatch" + + # get tile descriptors + fp.seek(28 + offset) + s = fp.read(i32(s, 12) * length) + + x = y = 0 + xsize, ysize = size + xtile, ytile = tilesize + self.tile = [] + + for i in range(0, len(s), length): + + compression = i32(s, i+8) + + if compression == 0: + self.tile.append(("raw", (x,y,x+xtile,y+ytile), + i32(s, i) + 28, (self.rawmode))) + + elif compression == 1: + + # FIXME: the fill decoder is not implemented + self.tile.append(("fill", (x,y,x+xtile,y+ytile), + i32(s, i) + 28, (self.rawmode, s[12:16]))) + + elif compression == 2: + + internal_color_conversion = ord(s[14]) + jpeg_tables = ord(s[15]) + rawmode = self.rawmode + + if internal_color_conversion: + # The image is stored as usual (usually YCbCr). + if rawmode == "RGBA": + # For "RGBA", data is stored as YCbCrA based on + # negative RGB. The following trick works around + # this problem : + jpegmode, rawmode = "YCbCrK", "CMYK" + else: + jpegmode = None # let the decoder decide + + else: + # The image is stored as defined by rawmode + jpegmode = rawmode + + self.tile.append(("jpeg", (x,y,x+xtile,y+ytile), + i32(s, i) + 28, (rawmode, jpegmode))) + + # FIXME: jpeg tables are tile dependent; the prefix + # data must be placed in the tile descriptor itself! + + if jpeg_tables: + self.tile_prefix = self.jpeg[jpeg_tables] + + else: + raise IOError, "unknown/invalid compression" + + x = x + xtile + if x >= xsize: + x, y = 0, y + ytile + if y >= ysize: + break # isn't really required + + self.stream = stream + self.fp = None + + def load(self): + + if not self.fp: + self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) + + ImageFile.ImageFile.load(self) + +# +# -------------------------------------------------------------------- + +Image.register_open("FPX", FpxImageFile, _accept) + +Image.register_extension("FPX", ".fpx") diff --git a/PIL/GbrImagePlugin.py b/PIL/GbrImagePlugin.py new file mode 100644 index 0000000..273e1f8 --- /dev/null +++ b/PIL/GbrImagePlugin.py @@ -0,0 +1,70 @@ +# +# The Python Imaging Library +# $Id: GbrImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# load a GIMP brush file +# +# History: +# 96-03-14 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile + +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24L) + +def _accept(prefix): + return i32(prefix) >= 20 and i32(prefix[4:8]) == 1 + +## +# Image plugin for the GIMP brush format. + +class GbrImageFile(ImageFile.ImageFile): + + format = "GBR" + format_description = "GIMP brush file" + + def _open(self): + + header_size = i32(self.fp.read(4)) + version = i32(self.fp.read(4)) + if header_size < 20 or version != 1: + raise SyntaxError, "not a GIMP brush" + + width = i32(self.fp.read(4)) + height = i32(self.fp.read(4)) + bytes = i32(self.fp.read(4)) + if width <= 0 or height <= 0 or bytes != 1: + raise SyntaxError, "not a GIMP brush" + + comment = self.fp.read(header_size - 20)[:-1] + + self.mode = "L" + self.size = width, height + + self.info["comment"] = comment + + # Since the brush is so small, we read the data immediately + self.data = self.fp.read(width * height) + + def load(self): + + if not self.data: + return + + # create an image out of the brush data block + self.im = Image.core.new(self.mode, self.size) + self.im.fromstring(self.data) + self.data = "" + +# +# registry + +Image.register_open("GBR", GbrImageFile, _accept) + +Image.register_extension("GBR", ".gbr") diff --git a/PIL/GdImageFile.py b/PIL/GdImageFile.py new file mode 100644 index 0000000..e6ddfa7 --- /dev/null +++ b/PIL/GdImageFile.py @@ -0,0 +1,86 @@ +# +# The Python Imaging Library. +# $Id: GdImageFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# GD file handling +# +# History: +# 1996-04-12 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + + +# NOTE: This format cannot be automatically recognized, so the +# class is not registered for use with Image.open(). To open a +# gd file, use the GdImageFile.open() function instead. + +# THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This +# implementation is provided for convenience and demonstrational +# purposes only. + + +__version__ = "0.1" + +import string +import Image, ImageFile, ImagePalette + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) + +## +# Image plugin for the GD uncompressed format. Note that this format +# is not supported by the standard Image.open function. To use +# this plugin, you have to import the GdImageFile module and +# use the GdImageFile.open function. + +class GdImageFile(ImageFile.ImageFile): + + format = "GD" + format_description = "GD uncompressed images" + + def _open(self): + + # Header + s = self.fp.read(775) + + self.mode = "L" # FIXME: "P" + self.size = i16(s[0:2]), i16(s[2:4]) + + # transparency index + tindex = i16(s[5:7]) + if tindex < 256: + self.info["transparent"] = tindex + + self.palette = ImagePalette.raw("RGB", s[7:]) + + self.tile = [("raw", (0,0)+self.size, 775, ("L", 0, -1))] + +## +# Load texture from a GD image file. +# +# @param filename GD file name, or an opened file handle. +# @param mode Optional mode. In this version, if the mode argument +# is given, it must be "r". +# @return An image instance. +# @exception IOError If the image could not be read. + +def open(fp, mode = "r"): + + if mode != "r": + raise ValueError("bad mode") + + if type(fp) == type(""): + import __builtin__ + filename = fp + fp = __builtin__.open(fp, "rb") + else: + filename = "" + + try: + return GdImageFile(fp, filename) + except SyntaxError: + raise IOError("cannot identify this image file") diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py new file mode 100644 index 0000000..cce0fe2 --- /dev/null +++ b/PIL/GifImagePlugin.py @@ -0,0 +1,405 @@ +# +# The Python Imaging Library. +# $Id: GifImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# GIF file handling +# +# History: +# 1995-09-01 fl Created +# 1996-12-14 fl Added interlace support +# 1996-12-30 fl Added animation support +# 1997-01-05 fl Added write support, fixed local colour map bug +# 1997-02-23 fl Make sure to load raster data in getdata() +# 1997-07-05 fl Support external decoder (0.4) +# 1998-07-09 fl Handle all modes when saving (0.5) +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 2001-04-16 fl Added rewind support (seek to frame 0) (0.6) +# 2001-04-17 fl Added palette optimization (0.7) +# 2002-06-06 fl Added transparency support for save (0.8) +# 2004-02-24 fl Disable interlacing for small images +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.9" + + +import Image, ImageFile, ImagePalette + + +# -------------------------------------------------------------------- +# Helpers + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def o16(i): + return chr(i&255) + chr(i>>8&255) + + +# -------------------------------------------------------------------- +# Identify/read GIF files + +def _accept(prefix): + return prefix[:6] in ["GIF87a", "GIF89a"] + +## +# Image plugin for GIF images. This plugin supports both GIF87 and +# GIF89 images. + +class GifImageFile(ImageFile.ImageFile): + + format = "GIF" + format_description = "Compuserve GIF" + + global_palette = None + + def data(self): + s = self.fp.read(1) + if s and ord(s): + return self.fp.read(ord(s)) + return None + + def _open(self): + + # Screen + s = self.fp.read(13) + if s[:6] not in ["GIF87a", "GIF89a"]: + raise SyntaxError, "not a GIF file" + + self.info["version"] = s[:6] + + self.size = i16(s[6:]), i16(s[8:]) + + self.tile = [] + + flags = ord(s[10]) + + bits = (flags & 7) + 1 + + if flags & 128: + # get global palette + self.info["background"] = ord(s[11]) + # check if palette contains colour indices + p = self.fp.read(3<%s" % (file, filename)) + else: + os.system("ppmquant 256 %s | ppmtogif >%s" % (file, filename)) + try: os.unlink(file) + except: pass + + +# -------------------------------------------------------------------- +# GIF utilities + +def getheader(im, info=None): + """Return a list of strings representing a GIF header""" + + optimize = info and info.get("optimize", 0) + + s = [ + "GIF87a" + # magic + o16(im.size[0]) + # size + o16(im.size[1]) + + chr(7 + 128) + # flags: bits + palette + chr(0) + # background + chr(0) # reserved/aspect + ] + + if optimize: + # minimize color palette + i = 0 + maxcolor = 0 + for count in im.histogram(): + if count: + maxcolor = i + i = i + 1 + else: + maxcolor = 256 + + # global palette + if im.mode == "P": + # colour palette + s.append(im.im.getpalette("RGB")[:maxcolor*3]) + else: + # greyscale + for i in range(maxcolor): + s.append(chr(i) * 3) + + return s + +def getdata(im, offset = (0, 0), **params): + """Return a list of strings representing this image. + The first string is a local image header, the rest contains + encoded image data.""" + + class collector: + data = [] + def write(self, data): + self.data.append(data) + + im.load() # make sure raster data is available + + fp = collector() + + try: + im.encoderinfo = params + + # local image header + fp.write("," + + o16(offset[0]) + # offset + o16(offset[1]) + + o16(im.size[0]) + # size + o16(im.size[1]) + + chr(0) + # flags + chr(8)) # bits + + ImageFile._save(im, fp, [("gif", (0,0)+im.size, 0, RAWMODE[im.mode])]) + + fp.write("\0") # end of image data + + finally: + del im.encoderinfo + + return fp.data + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GifImageFile.format, GifImageFile, _accept) +Image.register_save(GifImageFile.format, _save) +Image.register_extension(GifImageFile.format, ".gif") +Image.register_mime(GifImageFile.format, "image/gif") + +# +# Uncomment the following line if you wish to use NETPBM/PBMPLUS +# instead of the built-in "uncompressed" GIF encoder + +# Image.register_save(GifImageFile.format, _save_netpbm) diff --git a/PIL/GimpGradientFile.py b/PIL/GimpGradientFile.py new file mode 100644 index 0000000..e246d6d --- /dev/null +++ b/PIL/GimpGradientFile.py @@ -0,0 +1,124 @@ +# +# Python Imaging Library +# $Id: GimpGradientFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# stuff to read (and render) GIMP gradient files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +from math import pi, log, sin, sqrt +import string + +# -------------------------------------------------------------------- +# Stuff to translate curve segments to palette values (derived from +# the corresponding code in GIMP, written by Federico Mena Quintero. +# See the GIMP distribution for more information.) +# + +EPSILON = 1e-10 + +def linear(middle, pos): + if pos <= middle: + if middle < EPSILON: + return 0.0 + else: + return 0.5 * pos / middle + else: + pos = pos - middle + middle = 1.0 - middle + if middle < EPSILON: + return 1.0 + else: + return 0.5 + 0.5 * pos / middle + +def curved(middle, pos): + return pos ** (log(0.5) / log(max(middle, EPSILON))) + +def sine(middle, pos): + return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 + +def sphere_increasing(middle, pos): + return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) + +def sphere_decreasing(middle, pos): + return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) + +SEGMENTS = [ linear, curved, sine, sphere_increasing, sphere_decreasing ] + +class GradientFile: + + gradient = None + + def getpalette(self, entries = 256): + + palette = [] + + ix = 0 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + for i in range(entries): + + x = i / float(entries-1) + + while x1 < x: + ix = ix + 1 + x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] + + w = x1 - x0 + + if w < EPSILON: + scale = segment(0.5, 0.5) + else: + scale = segment((xm - x0) / w, (x - x0) / w) + + # expand to RGBA + r = chr(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5)) + g = chr(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5)) + b = chr(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5)) + a = chr(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5)) + + # add to palette + palette.append(r + g + b + a) + + return string.join(palette, ""), "RGBA" + +## +# File handler for GIMP's gradient format. + +class GimpGradientFile(GradientFile): + + def __init__(self, fp): + + if fp.readline()[:13] != "GIMP Gradient": + raise SyntaxError, "not a GIMP gradient file" + + count = int(fp.readline()) + + gradient = [] + + for i in range(count): + + s = string.split(fp.readline()) + w = map(float, s[:11]) + + x0, x1 = w[0], w[2] + xm = w[1] + rgb0 = w[3:7] + rgb1 = w[7:11] + + segment = SEGMENTS[int(s[11])] + cspace = int(s[12]) + + if cspace != 0: + raise IOError, "cannot handle HSV colour space" + + gradient.append((x0, x1, xm, rgb0, rgb1, segment)) + + self.gradient = gradient diff --git a/PIL/GimpPaletteFile.py b/PIL/GimpPaletteFile.py new file mode 100644 index 0000000..3939147 --- /dev/null +++ b/PIL/GimpPaletteFile.py @@ -0,0 +1,61 @@ +# +# Python Imaging Library +# $Id: GimpPaletteFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# stuff to read GIMP palette files +# +# History: +# 1997-08-23 fl Created +# 2004-09-07 fl Support GIMP 2.0 palette files. +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1997-2004. +# +# See the README file for information on usage and redistribution. +# + +import re, string + +## +# File handler for GIMP's palette format. + +class GimpPaletteFile: + + rawmode = "RGB" + + def __init__(self, fp): + + self.palette = map(lambda i: chr(i)*3, range(256)) + + if fp.readline()[:12] != "GIMP Palette": + raise SyntaxError, "not a GIMP palette file" + + i = 0 + + while i <= 255: + + s = fp.readline() + + if not s: + break + # skip fields and comment lines + if re.match("\w+:|#", s): + continue + if len(s) > 100: + raise SyntaxError, "bad palette file" + + v = tuple(map(int, string.split(s)[:3])) + if len(v) != 3: + raise ValueError, "bad palette entry" + + if 0 <= i <= 255: + self.palette[i] = chr(v[0]) + chr(v[1]) + chr(v[2]) + + i = i + 1 + + self.palette = string.join(self.palette, "") + + + def getpalette(self): + + return self.palette, self.rawmode diff --git a/PIL/GribStubImagePlugin.py b/PIL/GribStubImagePlugin.py new file mode 100644 index 0000000..73d5404 --- /dev/null +++ b/PIL/GribStubImagePlugin.py @@ -0,0 +1,68 @@ +# +# The Python Imaging Library +# $Id: GribStubImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# GRIB stub adapter +# +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile + +_handler = None + +## +# Install application-specific GRIB image handler. +# +# @param handler Handler object. + +def register_handler(handler): + global _handler + _handler = handler + +# -------------------------------------------------------------------- +# Image adapter + +def _accept(prefix): + return prefix[0:4] == "GRIB" and prefix[7] == chr(1) + +class GribStubImageFile(ImageFile.StubImageFile): + + format = "GRIB" + format_description = "GRIB" + + def _open(self): + + offset = self.fp.tell() + + if not _accept(self.fp.read(8)): + raise SyntaxError("Not a GRIB file") + + self.fp.seek(offset) + + # make something up + self.mode = "F" + self.size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self): + return _handler + +def _save(im, fp, filename): + if _handler is None or not hasattr("_handler", "save"): + raise IOError("GRIB save handler not installed") + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept) +Image.register_save(GribStubImageFile.format, _save) + +Image.register_extension(GribStubImageFile.format, ".grib") diff --git a/PIL/Hdf5StubImagePlugin.py b/PIL/Hdf5StubImagePlugin.py new file mode 100644 index 0000000..31d4f6e --- /dev/null +++ b/PIL/Hdf5StubImagePlugin.py @@ -0,0 +1,70 @@ +# +# The Python Imaging Library +# $Id: Hdf5StubImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# HDF5 stub adapter +# +# Copyright (c) 2000-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile + +_handler = None + +## +# Install application-specific HDF5 image handler. +# +# @param handler Handler object. + +def register_handler(handler): + global _handler + _handler = handler + +# -------------------------------------------------------------------- +# Image adapter + +def _accept(prefix): + return prefix[:8] == "\x89HDF\r\n\x1a\n" + +class HDF5StubImageFile(ImageFile.StubImageFile): + + format = "HDF5" + format_description = "HDF5" + + def _open(self): + + offset = self.fp.tell() + + if not _accept(self.fp.read(8)): + raise SyntaxError("Not an HDF file") + + self.fp.seek(offset) + + # make something up + self.mode = "F" + self.size = 1, 1 + + loader = self._load() + if loader: + loader.open(self) + + def _load(self): + return _handler + + +def _save(im, fp, filename): + if _handler is None or not hasattr("_handler", "save"): + raise IOError("HDF5 save handler not installed") + _handler.save(im, fp, filename) + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept) +Image.register_save(HDF5StubImageFile.format, _save) + +Image.register_extension(HDF5StubImageFile.format, ".h5") +Image.register_extension(HDF5StubImageFile.format, ".hdf") diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py new file mode 100644 index 0000000..a85695d --- /dev/null +++ b/PIL/IcnsImagePlugin.py @@ -0,0 +1,211 @@ +# +# The Python Imaging Library. +# $Id: ImImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Mac OS X icns file decoder, based on icns.py by Bob Ippolito. +# +# history: +# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies. +# +# Copyright (c) 2004 by Bob Ippolito. +# Copyright (c) 2004 by Secret Labs. +# Copyright (c) 2004 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFile +import string, struct + +HEADERSIZE = 8 + +def nextheader(fobj): + return struct.unpack('>4sI', fobj.read(HEADERSIZE)) + +def read_32t(fobj, (start, length), (width, height)): + # The 128x128 icon seems to have an extra header for some reason. + fobj.seek(start) + sig = fobj.read(4) + if sig != '\x00\x00\x00\x00': + raise SyntaxError, 'Unknown signature, expecting 0x00000000' + return read_32(fobj, (start + 4, length - 4), (width, height)) + +def read_32(fobj, (start, length), size): + """ + Read a 32bit RGB icon resource. Seems to be either uncompressed or + an RLE packbits-like scheme. + """ + fobj.seek(start) + sizesq = size[0] * size[1] + if length == sizesq * 3: + # uncompressed ("RGBRGBGB") + indata = fobj.read(length) + im = Image.frombuffer("RGB", size, indata, "raw", "RGB", 0, 1) + else: + # decode image + im = Image.new("RGB", size, None) + for band_ix in range(3): + data = [] + bytesleft = sizesq + while bytesleft > 0: + byte = fobj.read(1) + if not byte: + break + byte = ord(byte) + if byte & 0x80: + blocksize = byte - 125 + byte = fobj.read(1) + for i in range(blocksize): + data.append(byte) + else: + blocksize = byte + 1 + data.append(fobj.read(blocksize)) + bytesleft = bytesleft - blocksize + if bytesleft <= 0: + break + if bytesleft != 0: + raise SyntaxError( + "Error reading %r channel [%r]" % (channel, bytesleft) + ) + band = Image.frombuffer( + "L", size, string.join(data, ""), "raw", "L", 0, 1 + ) + im.im.putband(band.im, band_ix) + return {"RGB": im} + +def read_mk(fobj, (start, length), size): + # Alpha masks seem to be uncompressed + fobj.seek(start) + band = Image.frombuffer( + "L", size, fobj.read(size[0]*size[1]), "raw", "L", 0, 1 + ) + return {"A": band} + +class IcnsFile: + + SIZES = { + (128, 128): [ + ('it32', read_32t), + ('t8mk', read_mk), + ], + (48, 48): [ + ('ih32', read_32), + ('h8mk', read_mk), + ], + (32, 32): [ + ('il32', read_32), + ('l8mk', read_mk), + ], + (16, 16): [ + ('is32', read_32), + ('s8mk', read_mk), + ], + } + + def __init__(self, fobj): + """ + fobj is a file-like object as an icns resource + """ + # signature : (start, length) + self.dct = dct = {} + self.fobj = fobj + sig, filesize = nextheader(fobj) + if sig != 'icns': + raise SyntaxError, 'not an icns file' + i = HEADERSIZE + while i < filesize: + sig, blocksize = nextheader(fobj) + i = i + HEADERSIZE + blocksize = blocksize - HEADERSIZE + dct[sig] = (i, blocksize) + fobj.seek(blocksize, 1) + i = i + blocksize + + def itersizes(self): + sizes = [] + for size, fmts in self.SIZES.items(): + for (fmt, reader) in fmts: + if self.dct.has_key(fmt): + sizes.append(size) + break + return sizes + + def bestsize(self): + sizes = self.itersizes() + if not sizes: + raise SyntaxError, "No 32bit icon resources found" + return max(sizes) + + def dataforsize(self, size): + """ + Get an icon resource as {channel: array}. Note that + the arrays are bottom-up like windows bitmaps and will likely + need to be flipped or transposed in some way. + """ + dct = {} + for code, reader in self.SIZES[size]: + desc = self.dct.get(code) + if desc is not None: + dct.update(reader(self.fobj, desc, size)) + return dct + + def getimage(self, size=None): + if size is None: + size = self.bestsize() + channels = self.dataforsize(size) + im = channels.get("RGB").copy() + try: + im.putalpha(channels["A"]) + except KeyError: + pass + return im + +## +# Image plugin for Mac OS icons. + +class IcnsImageFile(ImageFile.ImageFile): + """ + PIL read-only image support for Mac OS .icns files. + Chooses the best resolution, but will possibly load + a different size image if you mutate the size attribute + before calling 'load'. + + The info dictionary has a key 'sizes' that is a list + of sizes that the icns file has. + """ + + format = "ICNS" + format_description = "Mac OS icns resource" + + def _open(self): + self.icns = IcnsFile(self.fp) + self.mode = 'RGBA' + self.size = self.icns.bestsize() + self.info['sizes'] = self.icns.itersizes() + # Just use this to see if it's loaded or not yet. + self.tile = ('',) + + def load(self): + Image.Image.load(self) + if not self.tile: + return + self.load_prepare() + # This is likely NOT the best way to do it, but whatever. + im = self.icns.getimage(self.size) + self.im = im.im + self.mode = im.mode + self.size = im.size + self.fp = None + self.icns = None + self.tile = () + self.load_end() + + +Image.register_open("ICNS", IcnsImageFile, lambda x: x[:4] == 'icns') +Image.register_extension("ICNS", '.icns') + +if __name__ == '__main__': + import os, sys + im = Image.open(open(sys.argv[1], "rb")) + im.save("out.png") + os.startfile("out.png") diff --git a/PIL/IcoImagePlugin.py b/PIL/IcoImagePlugin.py new file mode 100644 index 0000000..5cdd284 --- /dev/null +++ b/PIL/IcoImagePlugin.py @@ -0,0 +1,88 @@ +# +# The Python Imaging Library. +# $Id: IcoImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Windows Icon support for PIL +# +# Notes: +# uses BmpImagePlugin.py to read the bitmap data. +# +# History: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + +import string + +import Image, BmpImagePlugin + + +# +# -------------------------------------------------------------------- + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + + +def _accept(prefix): + return prefix[:4] == "\0\0\1\0" + +## +# Image plugin for Windows Icon files. + +class IcoImageFile(BmpImagePlugin.BmpImageFile): + + format = "ICO" + format_description = "Windows Icon" + + def _open(self): + + # check magic + s = self.fp.read(6) + if not _accept(s): + raise SyntaxError, "not an ICO file" + + # pick the largest icon in the file + m = "" + for i in range(i16(s[4:])): + s = self.fp.read(16) + if not m: + m = s + elif ord(s[0]) > ord(m[0]) and ord(s[1]) > ord(m[1]): + m = s + #print "width", ord(s[0]) + #print "height", ord(s[1]) + #print "colors", ord(s[2]) + #print "reserved", ord(s[3]) + #print "planes", i16(s[4:]) + #print "bitcount", i16(s[6:]) + #print "bytes", i32(s[8:]) + #print "offset", i32(s[12:]) + + # load as bitmap + self._bitmap(i32(m[12:])) + + # patch up the bitmap height + self.size = self.size[0], self.size[1]/2 + d, e, o, a = self.tile[0] + self.tile[0] = d, (0,0)+self.size, o, a + + return + + +# +# -------------------------------------------------------------------- + +Image.register_open("ICO", IcoImageFile, _accept) + +Image.register_extension("ICO", ".ico") diff --git a/PIL/ImImagePlugin.py b/PIL/ImImagePlugin.py new file mode 100644 index 0000000..17b8c0e --- /dev/null +++ b/PIL/ImImagePlugin.py @@ -0,0 +1,335 @@ +# +# The Python Imaging Library. +# $Id: ImImagePlugin.py 2285 2005-02-07 23:52:14Z fredrik $ +# +# IFUNC IM file handling for PIL +# +# history: +# 1995-09-01 fl Created. +# 1997-01-03 fl Save palette images +# 1997-01-08 fl Added sequence support +# 1997-01-23 fl Added P and RGB save support +# 1997-05-31 fl Read floating point images +# 1997-06-22 fl Save floating point images +# 1997-08-27 fl Read and save 1-bit images +# 1998-06-25 fl Added support for RGB+LUT images +# 1998-07-02 fl Added support for YCC images +# 1998-07-15 fl Renamed offset attribute to avoid name clash +# 1998-12-29 fl Added I;16 support +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# 2003-09-26 fl Added LA/PA support +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.7" + +import re, string +import Image, ImageFile, ImagePalette + + +# -------------------------------------------------------------------- +# Standard tags + +COMMENT = "Comment" +DATE = "Date" +EQUIPMENT = "Digitalization equipment" +FRAMES = "File size (no of images)" +LUT = "Lut" +NAME = "Name" +SCALE = "Scale (x,y)" +SIZE = "Image size (x*y)" +MODE = "Image type" + +TAGS = { COMMENT:0, DATE:0, EQUIPMENT:0, FRAMES:0, LUT:0, NAME:0, + SCALE:0, SIZE:0, MODE:0 } + +OPEN = { + # ifunc93/p3cfunc formats + "0 1 image": ("1", "1"), + "L 1 image": ("1", "1"), + "Greyscale image": ("L", "L"), + "Grayscale image": ("L", "L"), + "RGB image": ("RGB", "RGB;L"), + "RLB image": ("RGB", "RLB"), + "RYB image": ("RGB", "RLB"), + "B1 image": ("1", "1"), + "B2 image": ("P", "P;2"), + "B4 image": ("P", "P;4"), + "X 24 image": ("RGB", "RGB"), + "L 32 S image": ("I", "I;32"), + "L 32 F image": ("F", "F;32"), + # old p3cfunc formats + "RGB3 image": ("RGB", "RGB;T"), + "RYB3 image": ("RGB", "RYB;T"), + # extensions + "LA image": ("LA", "LA;L"), + "RGBA image": ("RGBA", "RGBA;L"), + "RGBX image": ("RGBX", "RGBX;L"), + "CMYK image": ("CMYK", "CMYK;L"), + "YCC image": ("YCbCr", "YCbCr;L"), +} + +# ifunc95 extensions +for i in ["8", "8S", "16", "16S", "32", "32F"]: + OPEN["L %s image" % i] = ("F", "F;%s" % i) + OPEN["L*%s image" % i] = ("F", "F;%s" % i) +for i in ["16", "16B"]: + OPEN["L %s image" % i] = ("I;%s" % i, "I;%s" % i) + OPEN["L*%s image" % i] = ("I;%s" % i, "I;%s" % i) +for i in ["32S"]: + OPEN["L %s image" % i] = ("I", "I;%s" % i) + OPEN["L*%s image" % i] = ("I", "I;%s" % i) +for i in range(2, 33): + OPEN["L*%s image" % i] = ("F", "F;%s" % i) + + +# -------------------------------------------------------------------- +# Read IM directory + +split = re.compile(r"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") + +def number(s): + try: + return int(s) + except ValueError: + return float(s) + +## +# Image plugin for the IFUNC IM file format. + +class ImImageFile(ImageFile.ImageFile): + + format = "IM" + format_description = "IFUNC Image Memory" + + def _open(self): + + # Quick rejection: if there's not an LF among the first + # 100 bytes, this is (probably) not a text header. + + if not "\n" in self.fp.read(100): + raise SyntaxError, "not an IM file" + self.fp.seek(0) + + n = 0 + + # Default values + self.info[MODE] = "L" + self.info[SIZE] = (512, 512) + self.info[FRAMES] = 1 + + self.rawmode = "L" + + while 1: + + s = self.fp.read(1) + + # Some versions of IFUNC uses \n\r instead of \r\n... + if s == "\r": + continue + + if not s or s[0] == chr(0) or s[0] == chr(26): + break + + # FIXME: this may read whole file if not a text file + s = s + self.fp.readline() + + if len(s) > 100: + raise SyntaxError, "not an IM file" + + if s[-2:] == '\r\n': + s = s[:-2] + elif s[-1:] == '\n': + s = s[:-1] + + try: + m = split.match(s) + except re.error, v: + raise SyntaxError, "not an IM file" + + if m: + + k, v = m.group(1,2) + + # Convert value as appropriate + if k in [FRAMES, SCALE, SIZE]: + v = string.replace(v, "*", ",") + v = tuple(map(number, string.split(v, ","))) + if len(v) == 1: + v = v[0] + elif k == MODE and OPEN.has_key(v): + v, self.rawmode = OPEN[v] + + # Add to dictionary. Note that COMMENT tags are + # combined into a list of strings. + if k == COMMENT: + if self.info.has_key(k): + self.info[k].append(v) + else: + self.info[k] = [v] + else: + self.info[k] = v + + if TAGS.has_key(k): + n = n + 1 + + else: + + raise SyntaxError, "Syntax error in IM header: " + s + + if not n: + raise SyntaxError, "Not an IM file" + + # Basic attributes + self.size = self.info[SIZE] + self.mode = self.info[MODE] + + # Skip forward to start of image data + while s and s[0] != chr(26): + s = self.fp.read(1) + if not s: + raise SyntaxError, "File truncated" + + if self.info.has_key(LUT): + # convert lookup table to palette or lut attribute + palette = self.fp.read(768) + greyscale = 1 # greyscale palette + linear = 1 # linear greyscale palette + for i in range(256): + if palette[i] == palette[i+256] == palette[i+512]: + if palette[i] != chr(i): + linear = 0 + else: + greyscale = 0 + if self.mode == "L" or self.mode == "LA": + if greyscale: + if not linear: + self.lut = map(ord, palette[:256]) + else: + if self.mode == "L": + self.mode = self.rawmode = "P" + elif self.mode == "LA": + self.mode = self.rawmode = "PA" + self.palette = ImagePalette.raw("RGB;L", palette) + elif self.mode == "RGB": + if not greyscale or not linear: + self.lut = map(ord, palette) + + self.frame = 0 + + self.__offset = offs = self.fp.tell() + + self.__fp = self.fp # FIXME: hack + + if self.rawmode[:2] == "F;": + + # ifunc95 formats + try: + # use bit decoder (if necessary) + bits = int(self.rawmode[2:]) + if bits not in [8, 16, 32]: + self.tile = [("bit", (0,0)+self.size, offs, + (bits, 8, 3, 0, -1))] + return + except ValueError: + pass + + if self.rawmode in ["RGB;T", "RYB;T"]: + # Old LabEye/3PC files. Would be very surprised if anyone + # ever stumbled upon such a file ;-) + size = self.size[0] * self.size[1] + self.tile = [("raw", (0,0)+self.size, offs, ("G", 0, -1)), + ("raw", (0,0)+self.size, offs+size, ("R", 0, -1)), + ("raw", (0,0)+self.size, offs+2*size, ("B", 0, -1))] + else: + # LabEye/IFUNC files + self.tile = [("raw", (0,0)+self.size, offs, (self.rawmode, 0, -1))] + + def seek(self, frame): + + if frame < 0 or frame >= self.info[FRAMES]: + raise EOFError, "seek outside sequence" + + if self.frame == frame: + return + + self.frame = frame + + if self.mode == "1": + bits = 1 + else: + bits = 8 * len(self.mode) + + size = ((self.size[0] * bits + 7) / 8) * self.size[1] + offs = self.__offset + frame * size + + self.fp = self.__fp + + self.tile = [("raw", (0,0)+self.size, offs, (self.rawmode, 0, -1))] + + def tell(self): + + return self.frame + +# +# -------------------------------------------------------------------- +# Save IM files + +SAVE = { + # mode: (im type, raw mode) + "1": ("0 1", "1"), + "L": ("Greyscale", "L"), + "LA": ("LA", "LA;L"), + "P": ("Greyscale", "P"), + "PA": ("LA", "PA;L"), + "I": ("L 32S", "I;32S"), + "I;16": ("L 16", "I;16"), + "I;16B": ("L 16B", "I;16B"), + "F": ("L 32F", "F;32F"), + "RGB": ("RGB", "RGB;L"), + "RGBA": ("RGBA", "RGBA;L"), + "RGBX": ("RGBX", "RGBX;L"), + "CMYK": ("CMYK", "CMYK;L"), + "YCbCr": ("YCC", "YCbCr;L") +} + +def _save(im, fp, filename, check=0): + + try: + type, rawmode = SAVE[im.mode] + except KeyError: + raise ValueError, "Cannot save %s images as IM" % im.mode + + try: + frames = im.encoderinfo["frames"] + except KeyError: + frames = 1 + + if check: + return check + + fp.write("Image type: %s image\r\n" % type) + if filename: + fp.write("Name: %s\r\n" % filename) + fp.write("Image size (x*y): %d*%d\r\n" % im.size) + fp.write("File size (no of images): %d\r\n" % frames) + if im.mode == "P": + fp.write("Lut: 1\r\n") + fp.write("\000" * (511-fp.tell()) + "\032") + if im.mode == "P": + fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, -1))]) + +# +# -------------------------------------------------------------------- +# Registry + +Image.register_open("IM", ImImageFile) +Image.register_save("IM", _save) + +Image.register_extension("IM", ".im") diff --git a/PIL/Image.py b/PIL/Image.py new file mode 100644 index 0000000..04ed316 --- /dev/null +++ b/PIL/Image.py @@ -0,0 +1,2093 @@ +# +# The Python Imaging Library. +# $Id: Image.py 2933 2006-12-03 12:08:22Z fredrik $ +# +# the Image class wrapper +# +# partial release history: +# 1995-09-09 fl Created +# 1996-03-11 fl PIL release 0.0 (proof of concept) +# 1996-04-30 fl PIL release 0.1b1 +# 1999-07-28 fl PIL release 1.0 final +# 2000-06-07 fl PIL release 1.1 +# 2000-10-20 fl PIL release 1.1.1 +# 2001-05-07 fl PIL release 1.1.2 +# 2002-03-15 fl PIL release 1.1.3 +# 2003-05-10 fl PIL release 1.1.4 +# 2005-03-28 fl PIL release 1.1.5 +# 2006-12-02 fl PIL release 1.1.6 +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-2006 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +VERSION = "1.1.6" + +try: + import warnings +except ImportError: + warnings = None + +class _imaging_not_installed: + # module placeholder + def __getattr__(self, id): + raise ImportError("The _imaging C module is not installed") + +try: + # give Tk a chance to set up the environment, in case we're + # using an _imaging module linked against libtcl/libtk (use + # __import__ to hide this from naive packagers; we don't really + # depend on Tk unless ImageTk is used, and that module already + # imports Tkinter) + __import__("FixTk") +except ImportError: + pass + +try: + # If the _imaging C module is not present, you can still use + # the "open" function to identify files, but you cannot load + # them. Note that other modules should not refer to _imaging + # directly; import Image and use the Image.core variable instead. + import _imaging + core = _imaging + del _imaging +except ImportError, v: + core = _imaging_not_installed() + if str(v)[:20] == "Module use of python" and warnings: + # The _imaging C module is present, but not compiled for + # the right version (windows only). Print a warning, if + # possible. + warnings.warn( + "The _imaging extension was built for another version " + "of Python; most PIL functions will be disabled", + RuntimeWarning + ) + +import ImageMode +import ImagePalette + +import os, string, sys + +# type stuff +from types import IntType, StringType, TupleType + +try: + UnicodeStringType = type(unicode("")) + ## + # (Internal) Checks if an object is a string. If the current + # Python version supports Unicode, this checks for both 8-bit + # and Unicode strings. + def isStringType(t): + return isinstance(t, StringType) or isinstance(t, UnicodeStringType) +except NameError: + def isStringType(t): + return isinstance(t, StringType) + +## +# (Internal) Checks if an object is a tuple. + +def isTupleType(t): + return isinstance(t, TupleType) + +## +# (Internal) Checks if an object is an image object. + +def isImageType(t): + return hasattr(t, "im") + +## +# (Internal) Checks if an object is a string, and that it points to a +# directory. + +def isDirectory(f): + return isStringType(f) and os.path.isdir(f) + +from operator import isNumberType, isSequenceType + +# +# Debug level + +DEBUG = 0 + +# +# Constants (also defined in _imagingmodule.c!) + +NONE = 0 + +# transpose +FLIP_LEFT_RIGHT = 0 +FLIP_TOP_BOTTOM = 1 +ROTATE_90 = 2 +ROTATE_180 = 3 +ROTATE_270 = 4 + +# transforms +AFFINE = 0 +EXTENT = 1 +PERSPECTIVE = 2 +QUAD = 3 +MESH = 4 + +# resampling filters +NONE = 0 +NEAREST = 0 +ANTIALIAS = 1 # 3-lobed lanczos +LINEAR = BILINEAR = 2 +CUBIC = BICUBIC = 3 + +# dithers +NONE = 0 +NEAREST = 0 +ORDERED = 1 # Not yet implemented +RASTERIZE = 2 # Not yet implemented +FLOYDSTEINBERG = 3 # default + +# palettes/quantizers +WEB = 0 +ADAPTIVE = 1 + +# categories +NORMAL = 0 +SEQUENCE = 1 +CONTAINER = 2 + +# -------------------------------------------------------------------- +# Registries + +ID = [] +OPEN = {} +MIME = {} +SAVE = {} +EXTENSION = {} + +# -------------------------------------------------------------------- +# Modes supported by this version + +_MODEINFO = { + # NOTE: this table will be removed in future versions. use + # getmode* functions or ImageMode descriptors instead. + + # official modes + "1": ("L", "L", ("1",)), + "L": ("L", "L", ("L",)), + "I": ("L", "I", ("I",)), + "F": ("L", "F", ("F",)), + "P": ("RGB", "L", ("P",)), + "RGB": ("RGB", "L", ("R", "G", "B")), + "RGBX": ("RGB", "L", ("R", "G", "B", "X")), + "RGBA": ("RGB", "L", ("R", "G", "B", "A")), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), + + # Experimental modes include I;16, I;16B, RGBa, BGR;15, + # and BGR;24. Use these modes only if you know exactly + # what you're doing... + +} + +if sys.byteorder == 'little': + _ENDIAN = '<' +else: + _ENDIAN = '>' + +_MODE_CONV = { + # official modes + "1": ('|b1', None), + "L": ('|u1', None), + "I": ('%si4' % _ENDIAN, None), # FIXME: is this correct? + "F": ('%sf4' % _ENDIAN, None), # FIXME: is this correct? + "P": ('|u1', None), + "RGB": ('|u1', 3), + "RGBX": ('|u1', 4), + "RGBA": ('|u1', 4), + "CMYK": ('|u1', 4), + "YCbCr": ('|u1', 4), +} + +def _conv_type_shape(im): + shape = im.size[::-1] + typ, extra = _MODE_CONV[im.mode] + if extra is None: + return shape, typ + else: + return shape+(extra,), typ + + +MODES = _MODEINFO.keys() +MODES.sort() + +# raw modes that may be memory mapped. NOTE: if you change this, you +# may have to modify the stride calculation in map.c too! +_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16B") + +## +# Gets the "base" mode for given mode. This function returns "L" for +# images that contain grayscale data, and "RGB" for images that +# contain color data. +# +# @param mode Input mode. +# @return "L" or "RGB". +# @exception KeyError If the input mode was not a standard mode. + +def getmodebase(mode): + return ImageMode.getmode(mode).basemode + +## +# Gets the storage type mode. Given a mode, this function returns a +# single-layer mode suitable for storing individual bands. +# +# @param mode Input mode. +# @return "L", "I", or "F". +# @exception KeyError If the input mode was not a standard mode. + +def getmodetype(mode): + return ImageMode.getmode(mode).basetype + +## +# Gets a list of individual band names. Given a mode, this function +# returns a tuple containing the names of individual bands (use +# {@link #getmodetype} to get the mode used to store each individual +# band. +# +# @param mode Input mode. +# @return A tuple containing band names. The length of the tuple +# gives the number of bands in an image of the given mode. +# @exception KeyError If the input mode was not a standard mode. + +def getmodebandnames(mode): + return ImageMode.getmode(mode).bands + +## +# Gets the number of individual bands for this mode. +# +# @param mode Input mode. +# @return The number of bands in this mode. +# @exception KeyError If the input mode was not a standard mode. + +def getmodebands(mode): + return len(ImageMode.getmode(mode).bands) + +# -------------------------------------------------------------------- +# Helpers + +_initialized = 0 + +## +# Explicitly loads standard file format drivers. + +def preinit(): + "Load standard file format drivers." + + global _initialized + if _initialized >= 1: + return + + try: + import BmpImagePlugin + except ImportError: + pass + try: + import GifImagePlugin + except ImportError: + pass + try: + import JpegImagePlugin + except ImportError: + pass + try: + import PpmImagePlugin + except ImportError: + pass + try: + import PngImagePlugin + except ImportError: + pass +# try: +# import TiffImagePlugin +# except ImportError: +# pass + + _initialized = 1 + +## +# Explicitly initializes the Python Imaging Library. This function +# loads all available file format drivers. + +def init(): + "Load all file format drivers." + + global _initialized + if _initialized >= 2: + return + + visited = {} + + directories = sys.path + + try: + directories = directories + [os.path.dirname(__file__)] + except NameError: + pass + + # only check directories (including current, if present in the path) + for directory in filter(isDirectory, directories): + fullpath = os.path.abspath(directory) + if visited.has_key(fullpath): + continue + for file in os.listdir(directory): + if file[-14:] == "ImagePlugin.py": + f, e = os.path.splitext(file) + try: + sys.path.insert(0, directory) + try: + __import__(f, globals(), locals(), []) + finally: + del sys.path[0] + except ImportError: + if DEBUG: + print "Image: failed to import", + print f, ":", sys.exc_value + visited[fullpath] = None + + if OPEN or SAVE: + _initialized = 2 + + +# -------------------------------------------------------------------- +# Codec factories (used by tostring/fromstring and ImageFile.load) + +def _getdecoder(mode, decoder_name, args, extra=()): + + # tweak arguments + if args is None: + args = () + elif not isTupleType(args): + args = (args,) + + try: + # get decoder + decoder = getattr(core, decoder_name + "_decoder") + # print decoder, (mode,) + args + extra + return apply(decoder, (mode,) + args + extra) + except AttributeError: + raise IOError("decoder %s not available" % decoder_name) + +def _getencoder(mode, encoder_name, args, extra=()): + + # tweak arguments + if args is None: + args = () + elif not isTupleType(args): + args = (args,) + + try: + # get encoder + encoder = getattr(core, encoder_name + "_encoder") + # print encoder, (mode,) + args + extra + return apply(encoder, (mode,) + args + extra) + except AttributeError: + raise IOError("encoder %s not available" % encoder_name) + + +# -------------------------------------------------------------------- +# Simple expression analyzer + +class _E: + def __init__(self, data): self.data = data + def __coerce__(self, other): return self, _E(other) + def __add__(self, other): return _E((self.data, "__add__", other.data)) + def __mul__(self, other): return _E((self.data, "__mul__", other.data)) + +def _getscaleoffset(expr): + stub = ["stub"] + data = expr(_E(stub)).data + try: + (a, b, c) = data # simplified syntax + if (a is stub and b == "__mul__" and isNumberType(c)): + return c, 0.0 + if (a is stub and b == "__add__" and isNumberType(c)): + return 1.0, c + except TypeError: pass + try: + ((a, b, c), d, e) = data # full syntax + if (a is stub and b == "__mul__" and isNumberType(c) and + d == "__add__" and isNumberType(e)): + return c, e + except TypeError: pass + raise ValueError("illegal expression") + + +# -------------------------------------------------------------------- +# Implementation wrapper + +## +# This class represents an image object. To create Image objects, use +# the appropriate factory functions. There's hardly ever any reason +# to call the Image constructor directly. +# +# @see #open +# @see #new +# @see #fromstring + +class Image: + + format = None + format_description = None + + def __init__(self): + self.im = None + self.mode = "" + self.size = (0, 0) + self.palette = None + self.info = {} + self.category = NORMAL + self.readonly = 0 + + def _new(self, im): + new = Image() + new.im = im + new.mode = im.mode + new.size = im.size + new.palette = self.palette + if im.mode == "P": + new.palette = ImagePalette.ImagePalette() + try: + new.info = self.info.copy() + except AttributeError: + # fallback (pre-1.5.2) + new.info = {} + for k, v in self.info: + new.info[k] = v + return new + + _makeself = _new # compatibility + + def _copy(self): + self.load() + self.im = self.im.copy() + self.readonly = 0 + + def _dump(self, file=None, format=None): + import tempfile + if not file: + file = tempfile.mktemp() + self.load() + if not format or format == "PPM": + self.im.save_ppm(file) + else: + file = file + "." + format + self.save(file, format) + return file + + def __getattr__(self, name): + if name == "__array_interface__": + # numpy array interface support + new = {} + shape, typestr = _conv_type_shape(self) + new['shape'] = shape + new['typestr'] = typestr + new['data'] = self.tostring() + return new + raise AttributeError(name) + + ## + # Returns a string containing pixel data. + # + # @param encoder_name What encoder to use. The default is to + # use the standard "raw" encoder. + # @param *args Extra arguments to the encoder. + # @return An 8-bit string. + + def tostring(self, encoder_name="raw", *args): + "Return image as a binary string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if encoder_name == "raw" and args == (): + args = self.mode + + self.load() + + # unpack data + e = _getencoder(self.mode, encoder_name, args) + e.setimage(self.im) + + bufsize = max(65536, self.size[0] * 4) # see RawEncode.c + + data = [] + while 1: + l, s, d = e.encode(bufsize) + data.append(d) + if s: + break + if s < 0: + raise RuntimeError("encoder error %d in tostring" % s) + + return string.join(data, "") + + ## + # Returns the image converted to an X11 bitmap. This method + # only works for mode "1" images. + # + # @param name The name prefix to use for the bitmap variables. + # @return A string containing an X11 bitmap. + # @exception ValueError If the mode is not "1" + + def tobitmap(self, name="image"): + "Return image as an XBM bitmap" + + self.load() + if self.mode != "1": + raise ValueError("not a bitmap") + data = self.tostring("xbm") + return string.join(["#define %s_width %d\n" % (name, self.size[0]), + "#define %s_height %d\n"% (name, self.size[1]), + "static char %s_bits[] = {\n" % name, data, "};"], "") + + ## + # Loads this image with pixel data from a string. + #

+ # This method is similar to the {@link #fromstring} function, but + # loads data into this image instead of creating a new image + # object. + + def fromstring(self, data, decoder_name="raw", *args): + "Load data to image from binary string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + # default format + if decoder_name == "raw" and args == (): + args = self.mode + + # unpack data + d = _getdecoder(self.mode, decoder_name, args) + d.setimage(self.im) + s = d.decode(data) + + if s[0] >= 0: + raise ValueError("not enough image data") + if s[1] != 0: + raise ValueError("cannot decode image data") + + ## + # Allocates storage for the image and loads the pixel data. In + # normal cases, you don't need to call this method, since the + # Image class automatically loads an opened image when it is + # accessed for the first time. + # + # @return An image access object. + + def load(self): + "Explicitly load pixel data." + if self.im and self.palette and self.palette.dirty: + # realize palette + apply(self.im.putpalette, self.palette.getdata()) + self.palette.dirty = 0 + self.palette.mode = "RGB" + self.palette.rawmode = None + if self.info.has_key("transparency"): + self.im.putpalettealpha(self.info["transparency"], 0) + self.palette.mode = "RGBA" + if self.im: + return self.im.pixel_access(self.readonly) + + ## + # Verifies the contents of a file. For data read from a file, this + # method attempts to determine if the file is broken, without + # actually decoding the image data. If this method finds any + # problems, it raises suitable exceptions. If you need to load + # the image after using this method, you must reopen the image + # file. + + def verify(self): + "Verify file contents." + pass + + + ## + # Returns a converted copy of this image. For the "P" mode, this + # method translates pixels through the palette. If mode is + # omitted, a mode is chosen so that all information in the image + # and the palette can be represented without a palette. + #

+ # The current version supports all possible conversions between + # "L", "RGB" and "CMYK." + #

+ # When translating a colour image to black and white (mode "L"), + # the library uses the ITU-R 601-2 luma transform: + #

+ # L = R * 299/1000 + G * 587/1000 + B * 114/1000 + #

+ # When translating a greyscale image into a bilevel image (mode + # "1"), all non-zero values are set to 255 (white). To use other + # thresholds, use the {@link #Image.point} method. + # + # @def convert(mode, matrix=None) + # @param mode The requested mode. + # @param matrix An optional conversion matrix. If given, this + # should be 4- or 16-tuple containing floating point values. + # @return An Image object. + + def convert(self, mode=None, data=None, dither=None, + palette=WEB, colors=256): + "Convert to other pixel format" + + if not mode: + # determine default mode + if self.mode == "P": + self.load() + if self.palette: + mode = self.palette.mode + else: + mode = "RGB" + else: + return self.copy() + + self.load() + + if data: + # matrix conversion + if mode not in ("L", "RGB"): + raise ValueError("illegal conversion") + im = self.im.convert_matrix(mode, data) + return self._new(im) + + if mode == "P" and palette == ADAPTIVE: + im = self.im.quantize(colors) + return self._new(im) + + # colourspace conversion + if dither is None: + dither = FLOYDSTEINBERG + + try: + im = self.im.convert(mode, dither) + except ValueError: + try: + # normalize source image and try again + im = self.im.convert(getmodebase(self.mode)) + im = im.convert(mode, dither) + except KeyError: + raise ValueError("illegal conversion") + + return self._new(im) + + def quantize(self, colors=256, method=0, kmeans=0, palette=None): + + # methods: + # 0 = median cut + # 1 = maximum coverage + + # NOTE: this functionality will be moved to the extended + # quantizer interface in a later version of PIL. + + self.load() + + if palette: + # use palette from reference image + palette.load() + if palette.mode != "P": + raise ValueError("bad mode for palette image") + if self.mode != "RGB" and self.mode != "L": + raise ValueError( + "only RGB or L mode images can be quantized to a palette" + ) + im = self.im.convert("P", 1, palette.im) + return self._makeself(im) + + im = self.im.quantize(colors, method, kmeans) + return self._new(im) + + ## + # Copies this image. Use this method if you wish to paste things + # into an image, but still retain the original. + # + # @return An Image object. + + def copy(self): + "Copy raster data" + + self.load() + im = self.im.copy() + return self._new(im) + + ## + # Returns a rectangular region from this image. The box is a + # 4-tuple defining the left, upper, right, and lower pixel + # coordinate. + #

+ # This is a lazy operation. Changes to the source image may or + # may not be reflected in the cropped image. To break the + # connection, call the {@link #Image.load} method on the cropped + # copy. + # + # @param The crop rectangle, as a (left, upper, right, lower)-tuple. + # @return An Image object. + + def crop(self, box=None): + "Crop region from image" + + self.load() + if box is None: + return self.copy() + + # lazy operation + return _ImageCrop(self, box) + + ## + # Configures the image file loader so it returns a version of the + # image that as closely as possible matches the given mode and + # size. For example, you can use this method to convert a colour + # JPEG to greyscale while loading it, or to extract a 128x192 + # version from a PCD file. + #

+ # Note that this method modifies the Image object in place. If + # the image has already been loaded, this method has no effect. + # + # @param mode The requested mode. + # @param size The requested size. + + def draft(self, mode, size): + "Configure image decoder" + + pass + + def _expand(self, xmargin, ymargin=None): + if ymargin is None: + ymargin = xmargin + self.load() + return self._new(self.im.expand(xmargin, ymargin, 0)) + + ## + # Filters this image using the given filter. For a list of + # available filters, see the ImageFilter module. + # + # @param filter Filter kernel. + # @return An Image object. + # @see ImageFilter + + def filter(self, filter): + "Apply environment filter to image" + + self.load() + + from ImageFilter import Filter + if not isinstance(filter, Filter): + filter = filter() + + if self.im.bands == 1: + return self._new(filter.filter(self.im)) + # fix to handle multiband images since _imaging doesn't + ims = [] + for c in range(self.im.bands): + ims.append(self._new(filter.filter(self.im.getband(c)))) + return merge(self.mode, ims) + + ## + # Returns a tuple containing the name of each band in this image. + # For example, getbands on an RGB image returns ("R", "G", "B"). + # + # @return A tuple containing band names. + + def getbands(self): + "Get band names" + + return ImageMode.getmode(self.mode).bands + + ## + # Calculates the bounding box of the non-zero regions in the + # image. + # + # @return The bounding box is returned as a 4-tuple defining the + # left, upper, right, and lower pixel coordinate. If the image + # is completely empty, this method returns None. + + def getbbox(self): + "Get bounding box of actual data (non-zero pixels) in image" + + self.load() + return self.im.getbbox() + + ## + # Returns a list of colors used in this image. + # + # @param maxcolors Maximum number of colors. If this number is + # exceeded, this method returns None. The default limit is + # 256 colors. + # @return An unsorted list of (count, pixel) values. + + def getcolors(self, maxcolors=256): + "Get colors from image, up to given limit" + + self.load() + if self.mode in ("1", "L", "P"): + h = self.im.histogram() + out = [] + for i in range(256): + if h[i]: + out.append((h[i], i)) + if len(out) > maxcolors: + return None + return out + return self.im.getcolors(maxcolors) + + ## + # Returns the contents of this image as a sequence object + # containing pixel values. The sequence object is flattened, so + # that values for line one follow directly after the values of + # line zero, and so on. + #

+ # Note that the sequence object returned by this method is an + # internal PIL data type, which only supports certain sequence + # operations. To convert it to an ordinary sequence (e.g. for + # printing), use list(im.getdata()). + # + # @param band What band to return. The default is to return + # all bands. To return a single band, pass in the index + # value (e.g. 0 to get the "R" band from an "RGB" image). + # @return A sequence-like object. + + def getdata(self, band = None): + "Get image data as sequence object." + + self.load() + if band is not None: + return self.im.getband(band) + return self.im # could be abused + + ## + # Gets the the minimum and maximum pixel values for each band in + # the image. + # + # @return For a single-band image, a 2-tuple containing the + # minimum and maximum pixel value. For a multi-band image, + # a tuple containing one 2-tuple for each band. + + def getextrema(self): + "Get min/max value" + + self.load() + if self.im.bands > 1: + extrema = [] + for i in range(self.im.bands): + extrema.append(self.im.getband(i).getextrema()) + return tuple(extrema) + return self.im.getextrema() + + ## + # Returns a PyCObject that points to the internal image memory. + # + # @return A PyCObject object. + + def getim(self): + "Get PyCObject pointer to internal image memory" + + self.load() + return self.im.ptr + + + ## + # Returns the image palette as a list. + # + # @return A list of color values [r, g, b, ...], or None if the + # image has no palette. + + def getpalette(self): + "Get palette contents." + + self.load() + try: + return map(ord, self.im.getpalette()) + except ValueError: + return None # no palette + + + ## + # Returns the pixel value at a given position. + # + # @param xy The coordinate, given as (x, y). + # @return The pixel value. If the image is a multi-layer image, + # this method returns a tuple. + + def getpixel(self, xy): + "Get pixel value" + + self.load() + return self.im.getpixel(xy) + + ## + # Returns the horizontal and vertical projection. + # + # @return Two sequences, indicating where there are non-zero + # pixels along the X-axis and the Y-axis, respectively. + + def getprojection(self): + "Get projection to x and y axes" + + self.load() + x, y = self.im.getprojection() + return map(ord, x), map(ord, y) + + ## + # Returns a histogram for the image. The histogram is returned as + # a list of pixel counts, one for each pixel value in the source + # image. If the image has more than one band, the histograms for + # all bands are concatenated (for example, the histogram for an + # "RGB" image contains 768 values). + #

+ # A bilevel image (mode "1") is treated as a greyscale ("L") image + # by this method. + #

+ # If a mask is provided, the method returns a histogram for those + # parts of the image where the mask image is non-zero. The mask + # image must have the same size as the image, and be either a + # bi-level image (mode "1") or a greyscale image ("L"). + # + # @def histogram(mask=None) + # @param mask An optional mask. + # @return A list containing pixel counts. + + def histogram(self, mask=None, extrema=None): + "Take histogram of image" + + self.load() + if mask: + mask.load() + return self.im.histogram((0, 0), mask.im) + if self.mode in ("I", "F"): + if extrema is None: + extrema = self.getextrema() + return self.im.histogram(extrema) + return self.im.histogram() + + ## + # (Deprecated) Returns a copy of the image where the data has been + # offset by the given distances. Data wraps around the edges. If + # yoffset is omitted, it is assumed to be equal to xoffset. + #

+ # This method is deprecated. New code should use the offset + # function in the ImageChops module. + # + # @param xoffset The horizontal distance. + # @param yoffset The vertical distance. If omitted, both + # distances are set to the same value. + # @return An Image object. + + def offset(self, xoffset, yoffset=None): + "(deprecated) Offset image in horizontal and/or vertical direction" + if warnings: + warnings.warn( + "'offset' is deprecated; use 'ImageChops.offset' instead", + DeprecationWarning, stacklevel=2 + ) + import ImageChops + return ImageChops.offset(self, xoffset, yoffset) + + ## + # Pastes another image into this image. The box argument is either + # a 2-tuple giving the upper left corner, a 4-tuple defining the + # left, upper, right, and lower pixel coordinate, or None (same as + # (0, 0)). If a 4-tuple is given, the size of the pasted image + # must match the size of the region. + #

+ # If the modes don't match, the pasted image is converted to the + # mode of this image (see the {@link #Image.convert} method for + # details). + #

+ # Instead of an image, the source can be a integer or tuple + # containing pixel values. The method then fills the region + # with the given colour. When creating RGB images, you can + # also use colour strings as supported by the ImageColor module. + #

+ # If a mask is given, this method updates only the regions + # indicated by the mask. You can use either "1", "L" or "RGBA" + # images (in the latter case, the alpha band is used as mask). + # Where the mask is 255, the given image is copied as is. Where + # the mask is 0, the current value is preserved. Intermediate + # values can be used for transparency effects. + #

+ # Note that if you paste an "RGBA" image, the alpha band is + # ignored. You can work around this by using the same image as + # both source image and mask. + # + # @param im Source image or pixel value (integer or tuple). + # @param box An optional 4-tuple giving the region to paste into. + # If a 2-tuple is used instead, it's treated as the upper left + # corner. If omitted or None, the source is pasted into the + # upper left corner. + #

+ # If an image is given as the second argument and there is no + # third, the box defaults to (0, 0), and the second argument + # is interpreted as a mask image. + # @param mask An optional mask image. + # @return An Image object. + + def paste(self, im, box=None, mask=None): + "Paste other image into region" + + if isImageType(box) and mask is None: + # abbreviated paste(im, mask) syntax + mask = box; box = None + + if box is None: + # cover all of self + box = (0, 0) + self.size + + if len(box) == 2: + # lower left corner given; get size from image or mask + if isImageType(im): + size = im.size + elif isImageType(mask): + size = mask.size + else: + # FIXME: use self.size here? + raise ValueError( + "cannot determine region size; use 4-item box" + ) + box = box + (box[0]+size[0], box[1]+size[1]) + + if isStringType(im): + import ImageColor + im = ImageColor.getcolor(im, self.mode) + + elif isImageType(im): + im.load() + if self.mode != im.mode: + if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): + # should use an adapter for this! + im = im.convert(self.mode) + im = im.im + + self.load() + if self.readonly: + self._copy() + + if mask: + mask.load() + self.im.paste(im, box, mask.im) + else: + self.im.paste(im, box) + + ## + # Maps this image through a lookup table or function. + # + # @param lut A lookup table, containing 256 values per band in the + # image. A function can be used instead, it should take a single + # argument. The function is called once for each possible pixel + # value, and the resulting table is applied to all bands of the + # image. + # @param mode Output mode (default is same as input). In the + # current version, this can only be used if the source image + # has mode "L" or "P", and the output has mode "1". + # @return An Image object. + + def point(self, lut, mode=None): + "Map image through lookup table" + + self.load() + + if not isSequenceType(lut): + # if it isn't a list, it should be a function + if self.mode in ("I", "I;16", "F"): + # check if the function can be used with point_transform + scale, offset = _getscaleoffset(lut) + return self._new(self.im.point_transform(scale, offset)) + # for other modes, convert the function to a table + lut = map(lut, range(256)) * self.im.bands + + if self.mode == "F": + # FIXME: _imaging returns a confusing error message for this case + raise ValueError("point operation not supported for this mode") + + return self._new(self.im.point(lut, mode)) + + ## + # Adds or replaces the alpha layer in this image. If the image + # does not have an alpha layer, it's converted to "LA" or "RGBA". + # The new layer must be either "L" or "1". + # + # @param im The new alpha layer. This can either be an "L" or "1" + # image having the same size as this image, or an integer or + # other color value. + + def putalpha(self, alpha): + "Set alpha layer" + + self.load() + if self.readonly: + self._copy() + + if self.mode not in ("LA", "RGBA"): + # attempt to promote self to a matching alpha mode + try: + mode = getmodebase(self.mode) + "A" + try: + self.im.setmode(mode) + except (AttributeError, ValueError): + # do things the hard way + im = self.im.convert(mode) + if im.mode not in ("LA", "RGBA"): + raise ValueError # sanity check + self.im = im + self.mode = self.im.mode + except (KeyError, ValueError): + raise ValueError("illegal image mode") + + if self.mode == "LA": + band = 1 + else: + band = 3 + + if isImageType(alpha): + # alpha layer + if alpha.mode not in ("1", "L"): + raise ValueError("illegal image mode") + alpha.load() + if alpha.mode == "1": + alpha = alpha.convert("L") + else: + # constant alpha + try: + self.im.fillband(band, alpha) + except (AttributeError, ValueError): + # do things the hard way + alpha = new("L", self.size, alpha) + else: + return + + self.im.putband(alpha.im, band) + + ## + # Copies pixel data to this image. This method copies data from a + # sequence object into the image, starting at the upper left + # corner (0, 0), and continuing until either the image or the + # sequence ends. The scale and offset values are used to adjust + # the sequence values: pixel = value*scale + offset. + # + # @param data A sequence object. + # @param scale An optional scale value. The default is 1.0. + # @param offset An optional offset value. The default is 0.0. + + def putdata(self, data, scale=1.0, offset=0.0): + "Put data from a sequence object into an image." + + self.load() + if self.readonly: + self._copy() + + self.im.putdata(data, scale, offset) + + ## + # Attaches a palette to this image. The image must be a "P" or + # "L" image, and the palette sequence must contain 768 integer + # values, where each group of three values represent the red, + # green, and blue values for the corresponding pixel + # index. Instead of an integer sequence, you can use an 8-bit + # string. + # + # @def putpalette(data) + # @param data A palette sequence (either a list or a string). + + def putpalette(self, data, rawmode="RGB"): + "Put palette data into an image." + + self.load() + if self.mode not in ("L", "P"): + raise ValueError("illegal image mode") + if not isStringType(data): + data = string.join(map(chr, data), "") + self.mode = "P" + self.palette = ImagePalette.raw(rawmode, data) + self.palette.mode = "RGB" + self.load() # install new palette + + ## + # Modifies the pixel at the given position. The colour is given as + # a single numerical value for single-band images, and a tuple for + # multi-band images. + #

+ # Note that this method is relatively slow. For more extensive + # changes, use {@link #Image.paste} or the ImageDraw module + # instead. + # + # @param xy The pixel coordinate, given as (x, y). + # @param value The pixel value. + # @see #Image.paste + # @see #Image.putdata + # @see ImageDraw + + def putpixel(self, xy, value): + "Set pixel value" + + self.load() + if self.readonly: + self._copy() + + return self.im.putpixel(xy, value) + + ## + # Returns a resized copy of this image. + # + # @def resize(size, filter=NEAREST) + # @param size The requested size in pixels, as a 2-tuple: + # (width, height). + # @param filter An optional resampling filter. This can be + # one of NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), BICUBIC + # (cubic spline interpolation in a 4x4 environment), or + # ANTIALIAS (a high-quality downsampling filter). + # If omitted, or if the image has mode "1" or "P", it is + # set NEAREST. + # @return An Image object. + + def resize(self, size, resample=NEAREST): + "Resize image" + + if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): + raise ValueError("unknown resampling filter") + + self.load() + + if self.mode in ("1", "P"): + resample = NEAREST + + if resample == ANTIALIAS: + # requires stretch support (imToolkit & PIL 1.1.3) + try: + im = self.im.stretch(size, resample) + except AttributeError: + raise ValueError("unsupported resampling filter") + else: + im = self.im.resize(size, resample) + + return self._new(im) + + ## + # Returns a rotated copy of this image. This method returns a + # copy of this image, rotated the given number of degrees counter + # clockwise around its centre. + # + # @def rotate(angle, filter=NEAREST) + # @param angle In degrees counter clockwise. + # @param filter An optional resampling filter. This can be + # one of NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), or BICUBIC + # (cubic spline interpolation in a 4x4 environment). + # If omitted, or if the image has mode "1" or "P", it is + # set NEAREST. + # @param expand Optional expansion flag. If true, expands the output + # image to make it large enough to hold the entire rotated image. + # If false or omitted, make the output image the same size as the + # input image. + # @return An Image object. + + def rotate(self, angle, resample=NEAREST, expand=0): + "Rotate image. Angle given as degrees counter-clockwise." + + if expand: + import math + angle = -angle * math.pi / 180 + matrix = [ + math.cos(angle), math.sin(angle), 0.0, + -math.sin(angle), math.cos(angle), 0.0 + ] + def transform(x, y, (a, b, c, d, e, f)=matrix): + return a*x + b*y + c, d*x + e*y + f + + # calculate output size + w, h = self.size + xx = [] + yy = [] + for x, y in ((0, 0), (w, 0), (w, h), (0, h)): + x, y = transform(x, y) + xx.append(x) + yy.append(y) + w = int(math.ceil(max(xx)) - math.floor(min(xx))) + h = int(math.ceil(max(yy)) - math.floor(min(yy))) + + # adjust center + x, y = transform(w / 2.0, h / 2.0) + matrix[2] = self.size[0] / 2.0 - x + matrix[5] = self.size[1] / 2.0 - y + + return self.transform((w, h), AFFINE, matrix) + + if resample not in (NEAREST, BILINEAR, BICUBIC): + raise ValueError("unknown resampling filter") + + self.load() + + if self.mode in ("1", "P"): + resample = NEAREST + + return self._new(self.im.rotate(angle, resample)) + + ## + # Saves this image under the given filename. If no format is + # specified, the format to use is determined from the filename + # extension, if possible. + #

+ # Keyword options can be used to provide additional instructions + # to the writer. If a writer doesn't recognise an option, it is + # silently ignored. The available options are described later in + # this handbook. + #

+ # You can use a file object instead of a filename. In this case, + # you must always specify the format. The file object must + # implement the seek, tell, and write + # methods, and be opened in binary mode. + # + # @def save(file, format=None, **options) + # @param file File name or file object. + # @param format Optional format override. If omitted, the + # format to use is determined from the filename extension. + # If a file object was used instead of a filename, this + # parameter should always be used. + # @param **options Extra parameters to the image writer. + # @return None + # @exception KeyError If the output format could not be determined + # from the file name. Use the format option to solve this. + # @exception IOError If the file could not be written. The file + # may have been created, and may contain partial data. + + def save(self, fp, format=None, **params): + "Save image to file or stream" + + if isStringType(fp): + filename = fp + else: + if hasattr(fp, "name") and isStringType(fp.name): + filename = fp.name + else: + filename = "" + + # may mutate self! + self.load() + + self.encoderinfo = params + self.encoderconfig = () + + preinit() + + ext = string.lower(os.path.splitext(filename)[1]) + + if not format: + try: + format = EXTENSION[ext] + except KeyError: + init() + try: + format = EXTENSION[ext] + except KeyError: + raise KeyError(ext) # unknown extension + + try: + save_handler = SAVE[string.upper(format)] + except KeyError: + init() + save_handler = SAVE[string.upper(format)] # unknown format + + if isStringType(fp): + import __builtin__ + fp = __builtin__.open(fp, "wb") + close = 1 + else: + close = 0 + + try: + save_handler(self, fp, filename) + finally: + # do what we can to clean up + if close: + fp.close() + + ## + # Seeks to the given frame in this sequence file. If you seek + # beyond the end of the sequence, the method raises an + # EOFError exception. When a sequence file is opened, the + # library automatically seeks to frame 0. + #

+ # Note that in the current version of the library, most sequence + # formats only allows you to seek to the next frame. + # + # @param frame Frame number, starting at 0. + # @exception EOFError If the call attempts to seek beyond the end + # of the sequence. + # @see #Image.tell + + def seek(self, frame): + "Seek to given frame in sequence file" + + # overridden by file handlers + if frame != 0: + raise EOFError + + ## + # Displays this image. This method is mainly intended for + # debugging purposes. + #

+ # On Unix platforms, this method saves the image to a temporary + # PPM file, and calls the xv utility. + #

+ # On Windows, it saves the image to a temporary BMP file, and uses + # the standard BMP display utility to show it (usually Paint). + # + # @def show(title=None) + # @param title Optional title to use for the image window, + # where possible. + + def show(self, title=None, command=None): + "Display image (for debug purposes only)" + + _showxv(self, title, command) + + ## + # Split this image into individual bands. This method returns a + # tuple of individual image bands from an image. For example, + # splitting an "RGB" image creates three new images each + # containing a copy of one of the original bands (red, green, + # blue). + # + # @return A tuple containing bands. + + def split(self): + "Split image into bands" + + ims = [] + self.load() + for i in range(self.im.bands): + ims.append(self._new(self.im.getband(i))) + return tuple(ims) + + ## + # Returns the current frame number. + # + # @return Frame number, starting with 0. + # @see #Image.seek + + def tell(self): + "Return current frame number" + + return 0 + + ## + # Make this image into a thumbnail. This method modifies the + # image to contain a thumbnail version of itself, no larger than + # the given size. This method calculates an appropriate thumbnail + # size to preserve the aspect of the image, calls the {@link + # #Image.draft} method to configure the file reader (where + # applicable), and finally resizes the image. + #

+ # Note that the bilinear and bicubic filters in the current + # version of PIL are not well-suited for thumbnail generation. + # You should use ANTIALIAS unless speed is much more + # important than quality. + #

+ # Also note that this function modifies the Image object in place. + # If you need to use the full resolution image as well, apply this + # method to a {@link #Image.copy} of the original image. + # + # @param size Requested size. + # @param resample Optional resampling filter. This can be one + # of NEAREST, BILINEAR, BICUBIC, or + # ANTIALIAS (best quality). If omitted, it defaults + # to NEAREST (this will be changed to ANTIALIAS in a + # future version). + # @return None + + def thumbnail(self, size, resample=NEAREST): + "Create thumbnail representation (modifies image in place)" + + # FIXME: the default resampling filter will be changed + # to ANTIALIAS in future versions + + # preserve aspect ratio + x, y = self.size + if x > size[0]: y = max(y * size[0] / x, 1); x = size[0] + if y > size[1]: x = max(x * size[1] / y, 1); y = size[1] + size = x, y + + if size == self.size: + return + + self.draft(None, size) + + self.load() + + try: + im = self.resize(size, resample) + except ValueError: + if resample != ANTIALIAS: + raise + im = self.resize(size, NEAREST) # fallback + + self.im = im.im + self.mode = im.mode + self.size = size + + self.readonly = 0 + + # FIXME: the different tranform methods need further explanation + # instead of bloating the method docs, add a separate chapter. + + ## + # Transforms this image. This method creates a new image with the + # given size, and the same mode as the original, and copies data + # to the new image using the given transform. + #

+ # @def transform(size, method, data, resample=NEAREST) + # @param size The output size. + # @param method The transformation method. This is one of + # EXTENT (cut out a rectangular subregion), AFFINE + # (affine transform), PERSPECTIVE (perspective + # transform), QUAD (map a quadrilateral to a + # rectangle), or MESH (map a number of source quadrilaterals + # in one operation). + # @param data Extra data to the transformation method. + # @param resample Optional resampling filter. It can be one of + # NEAREST (use nearest neighbour), BILINEAR + # (linear interpolation in a 2x2 environment), or + # BICUBIC (cubic spline interpolation in a 4x4 + # environment). If omitted, or if the image has mode + # "1" or "P", it is set to NEAREST. + # @return An Image object. + + def transform(self, size, method, data=None, resample=NEAREST, fill=1): + "Transform image" + + import ImageTransform + if isinstance(method, ImageTransform.Transform): + method, data = method.getdata() + if data is None: + raise ValueError("missing method data") + im = new(self.mode, size, None) + if method == MESH: + # list of quads + for box, quad in data: + im.__transformer(box, self, QUAD, quad, resample, fill) + else: + im.__transformer((0, 0)+size, self, method, data, resample, fill) + + return im + + def __transformer(self, box, image, method, data, + resample=NEAREST, fill=1): + + # FIXME: this should be turned into a lazy operation (?) + + w = box[2]-box[0] + h = box[3]-box[1] + + if method == AFFINE: + # change argument order to match implementation + data = (data[2], data[0], data[1], + data[5], data[3], data[4]) + elif method == EXTENT: + # convert extent to an affine transform + x0, y0, x1, y1 = data + xs = float(x1 - x0) / w + ys = float(y1 - y0) / h + method = AFFINE + data = (x0 + xs/2, xs, 0, y0 + ys/2, 0, ys) + elif method == PERSPECTIVE: + # change argument order to match implementation + data = (data[2], data[0], data[1], + data[5], data[3], data[4], + data[6], data[7]) + elif method == QUAD: + # quadrilateral warp. data specifies the four corners + # given as NW, SW, SE, and NE. + nw = data[0:2]; sw = data[2:4]; se = data[4:6]; ne = data[6:8] + x0, y0 = nw; As = 1.0 / w; At = 1.0 / h + data = (x0, (ne[0]-x0)*As, (sw[0]-x0)*At, + (se[0]-sw[0]-ne[0]+x0)*As*At, + y0, (ne[1]-y0)*As, (sw[1]-y0)*At, + (se[1]-sw[1]-ne[1]+y0)*As*At) + else: + raise ValueError("unknown transformation method") + + if resample not in (NEAREST, BILINEAR, BICUBIC): + raise ValueError("unknown resampling filter") + + image.load() + + self.load() + + if image.mode in ("1", "P"): + resample = NEAREST + + self.im.transform2(box, image.im, method, data, resample, fill) + + ## + # Returns a flipped or rotated copy of this image. + # + # @param method One of FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, + # ROTATE_90, ROTATE_180, or ROTATE_270. + + def transpose(self, method): + "Transpose image (flip or rotate in 90 degree steps)" + + self.load() + im = self.im.transpose(method) + return self._new(im) + +# -------------------------------------------------------------------- +# Lazy operations + +class _ImageCrop(Image): + + def __init__(self, im, box): + + Image.__init__(self) + + x0, y0, x1, y1 = box + if x1 < x0: + x1 = x0 + if y1 < y0: + y1 = y0 + + self.mode = im.mode + self.size = x1-x0, y1-y0 + + self.__crop = x0, y0, x1, y1 + + self.im = im.im + + def load(self): + + # lazy evaluation! + if self.__crop: + self.im = self.im.crop(self.__crop) + self.__crop = None + + # FIXME: future versions should optimize crop/paste + # sequences! + +# -------------------------------------------------------------------- +# Factories + +# +# Debugging + +def _wedge(): + "Create greyscale wedge (for debugging only)" + + return Image()._new(core.wedge("L")) + +## +# Creates a new image with the given mode and size. +# +# @param mode The mode to use for the new image. +# @param size A 2-tuple, containing (width, height) in pixels. +# @param color What colour to use for the image. Default is black. +# If given, this should be a single integer or floating point value +# for single-band modes, and a tuple for multi-band modes (one value +# per band). When creating RGB images, you can also use colour +# strings as supported by the ImageColor module. If the colour is +# None, the image is not initialised. +# @return An Image object. + +def new(mode, size, color=0): + "Create a new image" + + if color is None: + # don't initialize + return Image()._new(core.new(mode, size)) + + if isStringType(color): + # css3-style specifier + + import ImageColor + color = ImageColor.getcolor(color, mode) + + return Image()._new(core.fill(mode, size, color)) + +## +# Creates an image memory from pixel data in a string. +#

+# In its simplest form, this function takes three arguments +# (mode, size, and unpacked pixel data). +#

+# You can also use any pixel decoder supported by PIL. For more +# information on available decoders, see the section Writing Your Own File Decoder. +#

+# Note that this function decodes pixel data only, not entire images. +# If you have an entire image in a string, wrap it in a +# StringIO object, and use {@link #open} to load it. +# +# @param mode The image mode. +# @param size The image size. +# @param data An 8-bit string containing raw data for the given mode. +# @param decoder_name What decoder to use. +# @param *args Additional parameters for the given decoder. +# @return An Image object. + +def fromstring(mode, size, data, decoder_name="raw", *args): + "Load image from string" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if decoder_name == "raw" and args == (): + args = mode + + im = new(mode, size) + im.fromstring(data, decoder_name, args) + return im + +## +# (New in 1.1.4) Creates an image memory from pixel data in a string +# or byte buffer. +#

+# This function is similar to {@link #fromstring}, but uses data in +# the byte buffer, where possible. This means that changes to the +# original buffer object are reflected in this image). Not all modes +# can share memory; supported modes include "L", "RGBX", "RGBA", and +# "CMYK". +#

+# Note that this function decodes pixel data only, not entire images. +# If you have an entire image file in a string, wrap it in a +# StringIO object, and use {@link #open} to load it. +#

+# In the current version, the default parameters used for the "raw" +# decoder differs from that used for {@link fromstring}. This is a +# bug, and will probably be fixed in a future release. The current +# release issues a warning if you do this; to disable the warning, +# you should provide the full set of parameters. See below for +# details. +# +# @param mode The image mode. +# @param size The image size. +# @param data An 8-bit string or other buffer object containing raw +# data for the given mode. +# @param decoder_name What decoder to use. +# @param *args Additional parameters for the given decoder. For the +# default encoder ("raw"), it's recommended that you provide the +# full set of parameters: +# frombuffer(mode, size, data, "raw", mode, 0, 1). +# @return An Image object. +# @since 1.1.4 + +def frombuffer(mode, size, data, decoder_name="raw", *args): + "Load image from string or buffer" + + # may pass tuple instead of argument list + if len(args) == 1 and isTupleType(args[0]): + args = args[0] + + if decoder_name == "raw": + if args == (): + if warnings: + warnings.warn( + "the frombuffer defaults may change in a future release; " + "for portability, change the call to read:\n" + " frombuffer(mode, size, data, 'raw', mode, 0, 1)", + RuntimeWarning, stacklevel=2 + ) + args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + if args[0] in _MAPMODES: + im = new(mode, (1,1)) + im = im._new( + core.map_buffer(data, size, decoder_name, None, 0, args) + ) + im.readonly = 1 + return im + + return apply(fromstring, (mode, size, data, decoder_name, args)) + + +## +# (New in 1.1.6) Create an image memory from an object exporting +# the array interface (using the buffer protocol). +# +# If obj is not contiguous, then the tostring method is called +# and {@link frombuffer} is used. +# +# @param obj Object with array interface +# @param mode Mode to use (will be determined from type if None) +# @return An image memory. + +def fromarray(obj, mode=None): + arr = obj.__array_interface__ + shape = arr['shape'] + ndim = len(shape) + try: + strides = arr['strides'] + except KeyError: + strides = None + if mode is None: + typestr = arr['typestr'] + if not (typestr[0] == '|' or typestr[0] == _ENDIAN or + typestr[1:] not in ['u1', 'b1', 'i4', 'f4']): + raise TypeError("cannot handle data-type") + typestr = typestr[:2] + if typestr == 'i4': + mode = 'I' + elif typestr == 'f4': + mode = 'F' + elif typestr == 'b1': + mode = '1' + elif ndim == 2: + mode = 'L' + elif ndim == 3: + mode = 'RGB' + elif ndim == 4: + mode = 'RGBA' + else: + raise TypeError("Do not understand data.") + ndmax = 4 + bad_dims=0 + if mode in ['1','L','I','P','F']: + ndmax = 2 + elif mode == 'RGB': + ndmax = 3 + if ndim > ndmax: + raise ValueError("Too many dimensions.") + + size = shape[:2][::-1] + if strides is not None: + obj = obj.tostring() + + return frombuffer(mode, size, obj, "raw", mode, 0, 1) + +## +# Opens and identifies the given image file. +#

+# This is a lazy operation; this function identifies the file, but the +# actual image data is not read from the file until you try to process +# the data (or call the {@link #Image.load} method). +# +# @def open(file, mode="r") +# @param file A filename (string) or a file object. The file object +# must implement read, seek, and tell methods, +# and be opened in binary mode. +# @param mode The mode. If given, this argument must be "r". +# @return An Image object. +# @exception IOError If the file cannot be found, or the image cannot be +# opened and identified. +# @see #new + +def open(fp, mode="r"): + "Open an image file, without loading the raster data" + + if mode != "r": + raise ValueError("bad mode") + + if isStringType(fp): + import __builtin__ + filename = fp + fp = __builtin__.open(fp, "rb") + else: + filename = "" + + prefix = fp.read(16) + + preinit() + + for i in ID: + try: + factory, accept = OPEN[i] + if not accept or accept(prefix): + fp.seek(0) + return factory(fp, filename) + except (SyntaxError, IndexError, TypeError): + pass + + init() + + for i in ID: + try: + factory, accept = OPEN[i] + if not accept or accept(prefix): + fp.seek(0) + return factory(fp, filename) + except (SyntaxError, IndexError, TypeError): + pass + + raise IOError("cannot identify image file") + +# +# Image processing. + +## +# Creates a new image by interpolating between two input images, using +# a constant alpha. +# +#

+#    out = image1 * (1.0 - alpha) + image2 * alpha
+# 
+# +# @param im1 The first image. +# @param im2 The second image. Must have the same mode and size as +# the first image. +# @param alpha The interpolation alpha factor. If alpha is 0.0, a +# copy of the first image is returned. If alpha is 1.0, a copy of +# the second image is returned. There are no restrictions on the +# alpha value. If necessary, the result is clipped to fit into +# the allowed output range. +# @return An Image object. + +def blend(im1, im2, alpha): + "Interpolate between images." + + im1.load() + im2.load() + return im1._new(core.blend(im1.im, im2.im, alpha)) + +## +# Creates a new image by interpolating between two input images, +# using the mask as alpha. +# +# @param image1 The first image. +# @param image2 The second image. Must have the same mode and +# size as the first image. +# @param mask A mask image. This image can can have mode +# "1", "L", or "RGBA", and must have the same size as the +# other two images. + +def composite(image1, image2, mask): + "Create composite image by blending images using a transparency mask" + + image = image2.copy() + image.paste(image1, None, mask) + return image + +## +# Applies the function (which should take one argument) to each pixel +# in the given image. If the image has more than one band, the same +# function is applied to each band. Note that the function is +# evaluated once for each possible pixel value, so you cannot use +# random components or other generators. +# +# @def eval(image, function) +# @param image The input image. +# @param function A function object, taking one integer argument. +# @return An Image object. + +def eval(image, *args): + "Evaluate image expression" + + return image.point(args[0]) + +## +# Creates a new image from a number of single-band images. +# +# @param mode The mode to use for the output image. +# @param bands A sequence containing one single-band image for +# each band in the output image. All bands must have the +# same size. +# @return An Image object. + +def merge(mode, bands): + "Merge a set of single band images into a new multiband image." + + if getmodebands(mode) != len(bands) or "*" in mode: + raise ValueError("wrong number of bands") + for im in bands[1:]: + if im.mode != getmodetype(mode): + raise ValueError("mode mismatch") + if im.size != bands[0].size: + raise ValueError("size mismatch") + im = core.new(mode, bands[0].size) + for i in range(getmodebands(mode)): + bands[i].load() + im.putband(bands[i].im, i) + return bands[0]._new(im) + +# -------------------------------------------------------------------- +# Plugin registry + +## +# Register an image file plugin. This function should not be used +# in application code. +# +# @param id An image format identifier. +# @param factory An image file factory method. +# @param accept An optional function that can be used to quickly +# reject images having another format. + +def register_open(id, factory, accept=None): + id = string.upper(id) + ID.append(id) + OPEN[id] = factory, accept + +## +# Registers an image MIME type. This function should not be used +# in application code. +# +# @param id An image format identifier. +# @param mimetype The image MIME type for this format. + +def register_mime(id, mimetype): + MIME[string.upper(id)] = mimetype + +## +# Registers an image save function. This function should not be +# used in application code. +# +# @param id An image format identifier. +# @param driver A function to save images in this format. + +def register_save(id, driver): + SAVE[string.upper(id)] = driver + +## +# Registers an image extension. This function should not be +# used in application code. +# +# @param id An image format identifier. +# @param extension An extension used for this format. + +def register_extension(id, extension): + EXTENSION[string.lower(extension)] = string.upper(id) + + +# -------------------------------------------------------------------- +# Simple display support + +def _showxv(image, title=None, command=None): + + if os.name == "nt": + format = "BMP" + elif sys.platform == "darwin": + format = "JPEG" + if not command: + command = "open -a /Applications/Preview.app" + else: + format = None + if not command: + command = "xv" + if title: + command = command + " -name \"%s\"" % title + + if image.mode == "I;16": + # @PIL88 @PIL101 + # "I;16" isn't an 'official' mode, but we still want to + # provide a simple way to show 16-bit images. + base = "L" + else: + base = getmodebase(image.mode) + if base != image.mode and image.mode != "1": + file = image.convert(base)._dump(format=format) + else: + file = image._dump(format=format) + + if os.name == "nt": + command = "start /wait %s && del /f %s" % (file, file) + elif sys.platform == "darwin": + # on darwin open returns immediately resulting in the temp + # file removal while app is opening + command = "(%s %s; sleep 20; rm -f %s)&" % (command, file, file) + else: + command = "(%s %s; rm -f %s)&" % (command, file, file) + + os.system(command) diff --git a/PIL/ImageChops.py b/PIL/ImageChops.py new file mode 100644 index 0000000..6db0710 --- /dev/null +++ b/PIL/ImageChops.py @@ -0,0 +1,302 @@ +# +# The Python Imaging Library. +# $Id: ImageChops.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# standard channel operations +# +# History: +# 1996-03-24 fl Created +# 1996-08-13 fl Added logical operations (for "1" images) +# 2000-10-12 fl Added offset method (from Image.py) +# +# Copyright (c) 1997-2000 by Secret Labs AB +# Copyright (c) 1996-2000 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image + +## +# The ImageChops module contains a number of arithmetical image +# operations, called channel operations ("chops"). These can be +# used for various purposes, including special effects, image +# compositions, algorithmic painting, and more. +#

+# At this time, channel operations are only implemented for 8-bit +# images (e.g. "L" and "RGB"). +#

+# Most channel operations take one or two image arguments and returns +# a new image. Unless otherwise noted, the result of a channel +# operation is always clipped to the range 0 to MAX (which is 255 for +# all modes supported by the operations in this module). +## + +## +# Return an image with the same size as the given image, but filled +# with the given pixel value. +# +# @param image Reference image. +# @param value Pixel value. +# @return An image object. + +def constant(image, value): + "Fill a channel with a given grey level" + + return Image.new("L", image.size, value) + +## +# Copy image. +# +# @param image Source image. +# @return A copy of the source image. + +def duplicate(image): + "Create a copy of a channel" + + return image.copy() + +## +# Inverts an image +# (MAX - image). +# +# @param image Source image. +# @return An image object. + +def invert(image): + "Invert a channel" + + image.load() + return image._new(image.im.chop_invert()) + +## +# Compare images, and return lighter pixel value +# (max(image1, image2)). +#

+# Compares the two images, pixel by pixel, and returns a new image +# containing the lighter values. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def lighter(image1, image2): + "Select the lighter pixels from each image" + + image1.load() + image2.load() + return image1._new(image1.im.chop_lighter(image2.im)) + +## +# Compare images, and return darker pixel value +# (min(image1, image2)). +#

+# Compares the two images, pixel by pixel, and returns a new image +# containing the darker values. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def darker(image1, image2): + "Select the darker pixels from each image" + + image1.load() + image2.load() + return image1._new(image1.im.chop_darker(image2.im)) + +## +# Calculate absolute difference +# (abs(image1 - image2)). +#

+# Returns the absolute value of the difference between the two images. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def difference(image1, image2): + "Subtract one image from another" + + image1.load() + image2.load() + return image1._new(image1.im.chop_difference(image2.im)) + +## +# Superimpose positive images +# (image1 * image2 / MAX). +#

+# Superimposes two images on top of each other. If you multiply an +# image with a solid black image, the result is black. If you multiply +# with a solid white image, the image is unaffected. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def multiply(image1, image2): + "Superimpose two positive images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_multiply(image2.im)) + +## +# Superimpose negative images +# (MAX - ((MAX - image1) * (MAX - image2) / MAX)). +#

+# Superimposes two inverted images on top of each other. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def screen(image1, image2): + "Superimpose two negative images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_screen(image2.im)) + +## +# Add images +# ((image1 + image2) / scale + offset). +#

+# Adds two images, dividing the result by scale and adding the +# offset. If omitted, scale defaults to 1.0, and offset to 0.0. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def add(image1, image2, scale=1.0, offset=0): + "Add two images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_add(image2.im, scale, offset)) + +## +# Subtract images +# ((image1 - image2) / scale + offset). +#

+# Subtracts two images, dividing the result by scale and adding the +# offset. If omitted, scale defaults to 1.0, and offset to 0.0. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def subtract(image1, image2, scale=1.0, offset=0): + "Subtract two images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract(image2.im, scale, offset)) + +## +# Add images without clipping +# ((image1 + image2) % MAX). +#

+# Adds two images, without clipping the result. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def add_modulo(image1, image2): + "Add two images without clipping" + + image1.load() + image2.load() + return image1._new(image1.im.chop_add_modulo(image2.im)) + +## +# Subtract images without clipping +# ((image1 - image2) % MAX). +#

+# Subtracts two images, without clipping the result. +# +# @param image1 First image. +# @param image1 Second image. +# @return An image object. + +def subtract_modulo(image1, image2): + "Subtract two images without clipping" + + image1.load() + image2.load() + return image1._new(image1.im.chop_subtract_modulo(image2.im)) + +## +# Logical AND +# (image1 and image2). + +def logical_and(image1, image2): + "Logical and between two images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_and(image2.im)) + +## +# Logical OR +# (image1 or image2). + +def logical_or(image1, image2): + "Logical or between two images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_or(image2.im)) + +## +# Logical XOR +# (image1 xor image2). + +def logical_xor(image1, image2): + "Logical xor between two images" + + image1.load() + image2.load() + return image1._new(image1.im.chop_xor(image2.im)) + +## +# Blend images using constant transparency weight. +#

+# Same as the blend function in the Image module. + +def blend(image1, image2, alpha): + "Blend two images using a constant transparency weight" + + return Image.blend(image1, image2, alpha) + +## +# Create composite using transparency mask. +#

+# Same as the composite function in the Image module. + +def composite(image1, image2, mask): + "Create composite image by blending images using a transparency mask" + + return Image.composite(image1, image2, mask) + +## +# Offset image data. +#

+# Returns a copy of the image where data has been offset by the given +# distances. Data wraps around the edges. If yoffset is omitted, it +# is assumed to be equal to xoffset. +# +# @param image Source image. +# @param xoffset The horizontal distance. +# @param yoffset The vertical distance. If omitted, both +# distances are set to the same value. +# @return An Image object. + +def offset(image, xoffset, yoffset=None): + "Offset image in horizontal and/or vertical direction" + if yoffset is None: + yoffset = xoffset + image.load() + return image._new(image.im.offset(xoffset, yoffset)) diff --git a/PIL/ImageColor.py b/PIL/ImageColor.py new file mode 100644 index 0000000..d97c0c4 --- /dev/null +++ b/PIL/ImageColor.py @@ -0,0 +1,262 @@ +# +# The Python Imaging Library +# $Id: ImageColor.py 2813 2006-10-07 10:11:35Z fredrik $ +# +# map CSS3-style colour description strings to RGB +# +# History: +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-15 fl Added RGBA support +# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2 +# 2004-07-19 fl Fixed gray/grey spelling issues +# +# Copyright (c) 2002-2004 by Secret Labs AB +# Copyright (c) 2002-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import re, string + +try: + x = int("a", 16) +except TypeError: + # python 1.5.2 doesn't support int(x,b) + str2int = string.atoi +else: + str2int = int + +## +# Convert color string to RGB tuple. +# +# @param color A CSS3-style colour string. +# @return An RGB-tuple. +# @exception ValueError If the color string could not be interpreted +# as an RGB value. + +def getrgb(color): + # FIXME: add RGBA support + try: + rgb = colormap[color] + except KeyError: + try: + # fall back on case-insensitive lookup + rgb = colormap[string.lower(color)] + except KeyError: + rgb = None + # found color in cache + if rgb: + if isinstance(rgb, type(())): + return rgb + colormap[color] = rgb = getrgb(rgb) + return rgb + # check for known string formats + m = re.match("#\w\w\w$", color) + if m: + return ( + str2int(color[1]*2, 16), + str2int(color[2]*2, 16), + str2int(color[3]*2, 16) + ) + m = re.match("#\w\w\w\w\w\w$", color) + if m: + return ( + str2int(color[1:3], 16), + str2int(color[3:5], 16), + str2int(color[5:7], 16) + ) + m = re.match("rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + if m: + return ( + str2int(m.group(1)), + str2int(m.group(2)), + str2int(m.group(3)) + ) + m = re.match("rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + if m: + return ( + int((str2int(m.group(1)) * 255) / 100.0 + 0.5), + int((str2int(m.group(2)) * 255) / 100.0 + 0.5), + int((str2int(m.group(3)) * 255) / 100.0 + 0.5) + ) + m = re.match("hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + if m: + from colorsys import hls_to_rgb + rgb = hls_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(3)) / 100.0, + float(m.group(2)) / 100.0, + ) + return ( + int(rgb[0] * 255 + 0.5), + int(rgb[1] * 255 + 0.5), + int(rgb[2] * 255 + 0.5) + ) + raise ValueError("unknown color specifier: %r" % color) + +def getcolor(color, mode): + # same as getrgb, but converts the result to the given mode + color = getrgb(color) + if mode == "RGB": + return color + if mode == "RGBA": + r, g, b = color + return r, g, b, 255 + if Image.getmodebase(mode) == "L": + r, g, b = color + return r*299/1000 + g*587/1000 + b*114/1000 + return color + +colormap = { + # X11 colour table (from "CSS3 module: Color working draft"), with + # gray/grey spelling issues fixed. This is a superset of HTML 4.0 + # colour names used in CSS 1. + "aliceblue": "#f0f8ff", + "antiquewhite": "#faebd7", + "aqua": "#00ffff", + "aquamarine": "#7fffd4", + "azure": "#f0ffff", + "beige": "#f5f5dc", + "bisque": "#ffe4c4", + "black": "#000000", + "blanchedalmond": "#ffebcd", + "blue": "#0000ff", + "blueviolet": "#8a2be2", + "brown": "#a52a2a", + "burlywood": "#deb887", + "cadetblue": "#5f9ea0", + "chartreuse": "#7fff00", + "chocolate": "#d2691e", + "coral": "#ff7f50", + "cornflowerblue": "#6495ed", + "cornsilk": "#fff8dc", + "crimson": "#dc143c", + "cyan": "#00ffff", + "darkblue": "#00008b", + "darkcyan": "#008b8b", + "darkgoldenrod": "#b8860b", + "darkgray": "#a9a9a9", + "darkgrey": "#a9a9a9", + "darkgreen": "#006400", + "darkkhaki": "#bdb76b", + "darkmagenta": "#8b008b", + "darkolivegreen": "#556b2f", + "darkorange": "#ff8c00", + "darkorchid": "#9932cc", + "darkred": "#8b0000", + "darksalmon": "#e9967a", + "darkseagreen": "#8fbc8f", + "darkslateblue": "#483d8b", + "darkslategray": "#2f4f4f", + "darkslategrey": "#2f4f4f", + "darkturquoise": "#00ced1", + "darkviolet": "#9400d3", + "deeppink": "#ff1493", + "deepskyblue": "#00bfff", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1e90ff", + "firebrick": "#b22222", + "floralwhite": "#fffaf0", + "forestgreen": "#228b22", + "fuchsia": "#ff00ff", + "gainsboro": "#dcdcdc", + "ghostwhite": "#f8f8ff", + "gold": "#ffd700", + "goldenrod": "#daa520", + "gray": "#808080", + "grey": "#808080", + "green": "#008000", + "greenyellow": "#adff2f", + "honeydew": "#f0fff0", + "hotpink": "#ff69b4", + "indianred": "#cd5c5c", + "indigo": "#4b0082", + "ivory": "#fffff0", + "khaki": "#f0e68c", + "lavender": "#e6e6fa", + "lavenderblush": "#fff0f5", + "lawngreen": "#7cfc00", + "lemonchiffon": "#fffacd", + "lightblue": "#add8e6", + "lightcoral": "#f08080", + "lightcyan": "#e0ffff", + "lightgoldenrodyellow": "#fafad2", + "lightgreen": "#90ee90", + "lightgray": "#d3d3d3", + "lightgrey": "#d3d3d3", + "lightpink": "#ffb6c1", + "lightsalmon": "#ffa07a", + "lightseagreen": "#20b2aa", + "lightskyblue": "#87cefa", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#b0c4de", + "lightyellow": "#ffffe0", + "lime": "#00ff00", + "limegreen": "#32cd32", + "linen": "#faf0e6", + "magenta": "#ff00ff", + "maroon": "#800000", + "mediumaquamarine": "#66cdaa", + "mediumblue": "#0000cd", + "mediumorchid": "#ba55d3", + "mediumpurple": "#9370db", + "mediumseagreen": "#3cb371", + "mediumslateblue": "#7b68ee", + "mediumspringgreen": "#00fa9a", + "mediumturquoise": "#48d1cc", + "mediumvioletred": "#c71585", + "midnightblue": "#191970", + "mintcream": "#f5fffa", + "mistyrose": "#ffe4e1", + "moccasin": "#ffe4b5", + "navajowhite": "#ffdead", + "navy": "#000080", + "oldlace": "#fdf5e6", + "olive": "#808000", + "olivedrab": "#6b8e23", + "orange": "#ffa500", + "orangered": "#ff4500", + "orchid": "#da70d6", + "palegoldenrod": "#eee8aa", + "palegreen": "#98fb98", + "paleturquoise": "#afeeee", + "palevioletred": "#db7093", + "papayawhip": "#ffefd5", + "peachpuff": "#ffdab9", + "peru": "#cd853f", + "pink": "#ffc0cb", + "plum": "#dda0dd", + "powderblue": "#b0e0e6", + "purple": "#800080", + "red": "#ff0000", + "rosybrown": "#bc8f8f", + "royalblue": "#4169e1", + "saddlebrown": "#8b4513", + "salmon": "#fa8072", + "sandybrown": "#f4a460", + "seagreen": "#2e8b57", + "seashell": "#fff5ee", + "sienna": "#a0522d", + "silver": "#c0c0c0", + "skyblue": "#87ceeb", + "slateblue": "#6a5acd", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#fffafa", + "springgreen": "#00ff7f", + "steelblue": "#4682b4", + "tan": "#d2b48c", + "teal": "#008080", + "thistle": "#d8bfd8", + "tomato": "#ff6347", + "turquoise": "#40e0d0", + "violet": "#ee82ee", + "wheat": "#f5deb3", + "white": "#ffffff", + "whitesmoke": "#f5f5f5", + "yellow": "#ffff00", + "yellowgreen": "#9acd32", +} diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py new file mode 100644 index 0000000..7f9107b --- /dev/null +++ b/PIL/ImageDraw.py @@ -0,0 +1,378 @@ +# +# The Python Imaging Library +# $Id: ImageDraw.py 2817 2006-10-07 15:34:03Z fredrik $ +# +# drawing interface operations +# +# History: +# 1996-04-13 fl Created (experimental) +# 1996-08-07 fl Filled polygons, ellipses. +# 1996-08-13 fl Added text support +# 1998-06-28 fl Handle I and F images +# 1998-12-29 fl Added arc; use arc primitive to draw ellipses +# 1999-01-10 fl Added shape stuff (experimental) +# 1999-02-06 fl Added bitmap support +# 1999-02-11 fl Changed all primitives to take options +# 1999-02-20 fl Fixed backwards compatibility +# 2000-10-12 fl Copy on write, when necessary +# 2001-02-18 fl Use default ink for bitmap/text also in fill mode +# 2002-10-24 fl Added support for CSS-style color strings +# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing +# 2002-12-11 fl Refactored low-level drawing API (work in progress) +# 2004-08-26 fl Made Draw() a factory function, added getdraw() support +# 2004-09-04 fl Added width support to line primitive +# 2004-09-10 fl Added font mode handling +# 2006-06-19 fl Added font bearing support (getmask2) +# +# Copyright (c) 1997-2006 by Secret Labs AB +# Copyright (c) 1996-2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageColor + +try: + import warnings +except ImportError: + warnings = None + +## +# A simple 2D drawing interface for PIL images. +#

+# Application code should use the Draw factory, instead of +# directly. + +class ImageDraw: + + ## + # Create a drawing instance. + # + # @param im The image to draw in. + # @param mode Optional mode to use for color values. For RGB + # images, this argument can be RGB or RGBA (to blend the + # drawing into the image). For all other modes, this argument + # must be the same as the image mode. If omitted, the mode + # defaults to the mode of the image. + + def __init__(self, im, mode=None): + im.load() + if im.readonly: + im._copy() # make it writable + blend = 0 + if mode is None: + mode = im.mode + if mode != im.mode: + if mode == "RGBA" and im.mode == "RGB": + blend = 1 + else: + raise ValueError("mode mismatch") + if mode == "P": + self.palette = im.palette + else: + self.palette = None + self.im = im.im + self.draw = Image.core.draw(self.im, blend) + self.mode = mode + if mode in ("I", "F"): + self.ink = self.draw.draw_ink(1, mode) + else: + self.ink = self.draw.draw_ink(-1, mode) + if mode in ("1", "P", "I", "F"): + # FIXME: fix Fill2 to properly support matte for I+F images + self.fontmode = "1" + else: + self.fontmode = "L" # aliasing is okay for other modes + self.fill = 0 + self.font = None + + ## + # Set the default pen color. + + def setink(self, ink): + # compatibility + if warnings: + warnings.warn( + "'setink' is deprecated; use keyword arguments instead", + DeprecationWarning, stacklevel=2 + ) + if Image.isStringType(ink): + ink = ImageColor.getcolor(ink, self.mode) + if self.palette and not Image.isNumberType(ink): + ink = self.palette.getcolor(ink) + self.ink = self.draw.draw_ink(ink, self.mode) + + ## + # Set the default background color. + + def setfill(self, onoff): + # compatibility + if warnings: + warnings.warn( + "'setfill' is deprecated; use keyword arguments instead", + DeprecationWarning, stacklevel=2 + ) + self.fill = onoff + + ## + # Set the default font. + + def setfont(self, font): + # compatibility + self.font = font + + ## + # Get the current default font. + + def getfont(self): + if not self.font: + # FIXME: should add a font repository + import ImageFont + self.font = ImageFont.load_default() + return self.font + + def _getink(self, ink, fill=None): + if ink is None and fill is None: + if self.fill: + fill = self.ink + else: + ink = self.ink + else: + if ink is not None: + if Image.isStringType(ink): + ink = ImageColor.getcolor(ink, self.mode) + if self.palette and not Image.isNumberType(ink): + ink = self.palette.getcolor(ink) + ink = self.draw.draw_ink(ink, self.mode) + if fill is not None: + if Image.isStringType(fill): + fill = ImageColor.getcolor(fill, self.mode) + if self.palette and not Image.isNumberType(fill): + fill = self.palette.getcolor(fill) + fill = self.draw.draw_ink(fill, self.mode) + return ink, fill + + ## + # Draw an arc. + + def arc(self, xy, start, end, fill=None): + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_arc(xy, start, end, ink) + + ## + # Draw a bitmap. + + def bitmap(self, xy, bitmap, fill=None): + bitmap.load() + ink, fill = self._getink(fill) + if ink is None: + ink = fill + if ink is not None: + self.draw.draw_bitmap(xy, bitmap.im, ink) + + ## + # Draw a chord. + + def chord(self, xy, start, end, fill=None, outline=None): + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_chord(xy, start, end, fill, 1) + if ink is not None: + self.draw.draw_chord(xy, start, end, ink, 0) + + ## + # Draw an ellipse. + + def ellipse(self, xy, fill=None, outline=None): + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_ellipse(xy, fill, 1) + if ink is not None: + self.draw.draw_ellipse(xy, ink, 0) + + ## + # Draw a line, or a connected sequence of line segments. + + def line(self, xy, fill=None, width=0): + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_lines(xy, ink, width) + + ## + # (Experimental) Draw a shape. + + def shape(self, shape, fill=None, outline=None): + # experimental + shape.close() + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_outline(shape, fill, 1) + if ink is not None: + self.draw.draw_outline(shape, ink, 0) + + ## + # Draw a pieslice. + + def pieslice(self, xy, start, end, fill=None, outline=None): + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_pieslice(xy, start, end, fill, 1) + if ink is not None: + self.draw.draw_pieslice(xy, start, end, ink, 0) + + ## + # Draw one or more individual pixels. + + def point(self, xy, fill=None): + ink, fill = self._getink(fill) + if ink is not None: + self.draw.draw_points(xy, ink) + + ## + # Draw a polygon. + + def polygon(self, xy, fill=None, outline=None): + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_polygon(xy, fill, 1) + if ink is not None: + self.draw.draw_polygon(xy, ink, 0) + + ## + # Draw a rectangle. + + def rectangle(self, xy, fill=None, outline=None): + ink, fill = self._getink(outline, fill) + if fill is not None: + self.draw.draw_rectangle(xy, fill, 1) + if ink is not None: + self.draw.draw_rectangle(xy, ink, 0) + + ## + # Draw text. + + def text(self, xy, text, fill=None, font=None, anchor=None): + ink, fill = self._getink(fill) + if font is None: + font = self.getfont() + if ink is None: + ink = fill + if ink is not None: + try: + mask, offset = font.getmask2(text, self.fontmode) + xy = xy[0] + offset[0], xy[1] + offset[1] + except AttributeError: + try: + mask = font.getmask(text, self.fontmode) + except TypeError: + mask = font.getmask(text) + self.draw.draw_bitmap(xy, mask, ink) + + ## + # Get the size of a given string, in pixels. + + def textsize(self, text, font=None): + if font is None: + font = self.getfont() + return font.getsize(text) + +## +# A simple 2D drawing interface for PIL images. +# +# @param im The image to draw in. +# @param mode Optional mode to use for color values. For RGB +# images, this argument can be RGB or RGBA (to blend the +# drawing into the image). For all other modes, this argument +# must be the same as the image mode. If omitted, the mode +# defaults to the mode of the image. + +def Draw(im, mode=None): + try: + return im.getdraw(mode) + except AttributeError: + return ImageDraw(im, mode) + +# experimental access to the outline API +try: + Outline = Image.core.outline +except: + Outline = None + +## +# (Experimental) A more advanced 2D drawing interface for PIL images, +# based on the WCK interface. +# +# @param im The image to draw in. +# @param hints An optional list of hints. +# @return A (drawing context, drawing resource factory) tuple. + +def getdraw(im=None, hints=None): + # FIXME: this needs more work! + # FIXME: come up with a better 'hints' scheme. + handler = None + if not hints or "nicest" in hints: + try: + import _imagingagg + handler = _imagingagg + except ImportError: + pass + if handler is None: + import ImageDraw2 + handler = ImageDraw2 + if im: + im = handler.Draw(im) + return im, handler + +## +# (experimental) Fills a bounded region with a given color. +# +# @param image Target image. +# @param xy Seed position (a 2-item coordinate tuple). +# @param value Fill color. +# @param border Optional border value. If given, the region consists of +# pixels with a color different from the border color. If not given, +# the region consists of pixels having the same color as the seed +# pixel. + +def floodfill(image, xy, value, border=None): + "Fill bounded region." + # based on an implementation by Eric S. Raymond + pixel = image.load() + x, y = xy + try: + background = pixel[x, y] + if background == value: + return # seed point already has fill color + pixel[x, y] = value + except IndexError: + return # seed point outside image + edge = [(x, y)] + if border is None: + while edge: + newedge = [] + for (x, y) in edge: + for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): + try: + p = pixel[s, t] + except IndexError: + pass + else: + if p == background: + pixel[s, t] = value + newedge.append((s, t)) + edge = newedge + else: + while edge: + newedge = [] + for (x, y) in edge: + for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): + try: + p = pixel[s, t] + except IndexError: + pass + else: + if p != value and p != border: + pixel[s, t] = value + newedge.append((s, t)) + edge = newedge diff --git a/PIL/ImageDraw2.py b/PIL/ImageDraw2.py new file mode 100644 index 0000000..65c8cbb --- /dev/null +++ b/PIL/ImageDraw2.py @@ -0,0 +1,108 @@ +# +# The Python Imaging Library +# $Id: ImageDraw.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# WCK-style drawing interface operations +# +# History: +# 2003-12-07 fl created +# 2005-05-15 fl updated; added to PIL as ImageDraw2 +# 2005-05-15 fl added text support +# 2005-05-20 fl added arc/chord/pieslice support +# +# Copyright (c) 2003-2005 by Secret Labs AB +# Copyright (c) 2003-2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageColor, ImageDraw, ImageFont, ImagePath + +class Pen: + def __init__(self, color, width=1, opacity=255): + self.color = ImageColor.getrgb(color) + self.width = width + +class Brush: + def __init__(self, color, opacity=255): + self.color = ImageColor.getrgb(color) + +class Font: + def __init__(self, color, file, size=12): + # FIXME: add support for bitmap fonts + self.color = ImageColor.getrgb(color) + self.font = ImageFont.truetype(file, size) + +class Draw: + + def __init__(self, image, size=None, color=None): + if not hasattr(image, "im"): + image = Image.new(image, size, color) + self.draw = ImageDraw.Draw(image) + self.image = image + self.transform = None + + def flush(self): + return self.image + + def render(self, op, xy, pen, brush=None): + # handle color arguments + outline = fill = None; width = 1 + if isinstance(pen, Pen): + outline = pen.color + width = pen.width + elif isinstance(brush, Pen): + outline = brush.color + width = brush.width + if isinstance(brush, Brush): + fill = brush.color + elif isinstance(pen, Brush): + fill = pen.color + # handle transformation + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + # render the item + if op == "line": + self.draw.line(xy, fill=outline, width=width) + else: + getattr(self.draw, op)(xy, fill=fill, outline=outline) + + def settransform(self, (xoffset, yoffset)): + self.transform = (1, 0, xoffset, 0, 1, yoffset) + + def arc(self, xy, start, end, *options): + self.render("arc", xy, start, end, *options) + + def chord(self, xy, start, end, *options): + self.render("chord", xy, start, end, *options) + + def ellipse(self, xy, *options): + self.render("ellipse", xy, *options) + + def pieslice(self, xy, start, end, *options): + self.render("pieslice", xy, start, end, *options) + + def line(self, xy, *options): + self.render("line", xy, *options) + + def rectangle(self, xy, *options): + self.render("rectangle", xy, *options) + + def ellipse(self, xy, *options): + self.render("ellipse", xy, *options) + + def polygon(self, xy, *options): + self.render("polygon", xy, *options) + + def symbol(self, xy, symbol, *options): + raise NotImplementedError("not in this version") + + def text(self, xy, text, font): + if self.transform: + xy = ImagePath.Path(xy) + xy.transform(self.transform) + self.draw.text(xy, text, font=font.font, fill=font.color) + + def textsize(self, text, font): + return self.draw.textsize(text, font=font.font) diff --git a/PIL/ImageEnhance.py b/PIL/ImageEnhance.py new file mode 100644 index 0000000..cddd084 --- /dev/null +++ b/PIL/ImageEnhance.py @@ -0,0 +1,89 @@ +# +# The Python Imaging Library. +# $Id: ImageEnhance.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# image enhancement classes +# +# For a background, see "Image Processing By Interpolation and +# Extrapolation", Paul Haeberli and Douglas Voorhies. Available +# at http://www.sgi.com/grafica/interp/index.html +# +# History: +# 96-03-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +import Image, ImageFilter + +class _Enhance: + + ## + # Returns an enhanced image. The enhancement factor is a floating + # point value controlling the enhancement. Factor 1.0 always + # returns a copy of the original image, lower factors mean less + # colour (brightness, contrast, etc), and higher values more. + # There are no restrictions on this value. + # + # @param factor Enhancement factor. + # @return An enhanced image. + + def enhance(self, factor): + return Image.blend(self.degenerate, self.image, factor) + +## +# Color enhancement object. +#

+# This class can be used to adjust the colour balance of an image, in +# a manner similar to the controls on a colour TV set. An enhancement +# factor of 0.0 gives a black and white image, a factor of 1.0 gives +# the original image. + +class Color(_Enhance): + "Adjust image colour balance" + def __init__(self, image): + self.image = image + self.degenerate = image.convert("L").convert(image.mode) + +## +# Contrast enhancement object. +#

+# This class can be used to control the contrast of an image, similar +# to the contrast control on a TV set. An enhancement factor of 0.0 +# gives an solid grey image, factor 1.0 gives the original image. + +class Contrast(_Enhance): + "Adjust image contrast" + def __init__(self, image): + self.image = image + mean = reduce(lambda a,b: a+b, image.convert("L").histogram())/256.0 + self.degenerate = Image.new("L", image.size, mean).convert(image.mode) + +## +# Brightness enhancement object. +#

+# This class can be used to control the brighntess of an image. An +# enhancement factor of 0.0 gives a black image, factor 1.0 gives the +# original image. + +class Brightness(_Enhance): + "Adjust image brightness" + def __init__(self, image): + self.image = image + self.degenerate = Image.new(image.mode, image.size, 0) + +## +# Sharpness enhancement object. +#

+# This class can be used to adjust the sharpness of an image. The +# enhancement factor 0.0 gives a blurred image, 1.0 gives the original +# image, and a factor of 2.0 gives a sharpened image. + +class Sharpness(_Enhance): + "Adjust image sharpness" + def __init__(self, image): + self.image = image + self.degenerate = image.filter(ImageFilter.SMOOTH) diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py new file mode 100644 index 0000000..d51aff3 --- /dev/null +++ b/PIL/ImageFile.py @@ -0,0 +1,520 @@ +# +# The Python Imaging Library. +# $Id: ImageFile.py 2930 2006-12-02 13:50:40Z fredrik $ +# +# base class for image file handlers +# +# history: +# 1995-09-09 fl Created +# 1996-03-11 fl Fixed load mechanism. +# 1996-04-15 fl Added pcx/xbm decoders. +# 1996-04-30 fl Added encoders. +# 1996-12-14 fl Added load helpers +# 1997-01-11 fl Use encode_to_file where possible +# 1997-08-27 fl Flush output in _save +# 1998-03-05 fl Use memory mapping for some modes +# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B" +# 1999-05-31 fl Added image parser +# 2000-10-12 fl Set readonly flag on memory-mapped images +# 2002-03-20 fl Use better messages for common decoder errors +# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available +# 2003-10-30 fl Added StubImageFile class +# 2004-02-25 fl Made incremental parser more robust +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1995-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import traceback, sys, string, os + +MAXBLOCK = 65536 + +SAFEBLOCK = 1024*1024 + +ERRORS = { + -1: "image buffer overrun error", + -2: "decoding error", + -3: "unknown error", + -8: "bad configuration", + -9: "out of memory error" +} + +# +# -------------------------------------------------------------------- +# Helpers + +def _tilesort(t1, t2): + # sort on offset + return cmp(t1[2], t2[2]) + +# +# -------------------------------------------------------------------- +# ImageFile base class + +## +# Base class for image file handlers. + +class ImageFile(Image.Image): + "Base class for image file format handlers." + + def __init__(self, fp=None, filename=None): + Image.Image.__init__(self) + + self.tile = None + self.readonly = 1 # until we know better + + self.decoderconfig = () + self.decodermaxblock = MAXBLOCK + + if Image.isStringType(fp): + # filename + self.fp = open(fp, "rb") + self.filename = fp + else: + # stream + self.fp = fp + self.filename = filename + + try: + self._open() + except IndexError, v: # end of data + if Image.DEBUG > 1: + traceback.print_exc() + raise SyntaxError, v + except TypeError, v: # end of data (ord) + if Image.DEBUG > 1: + traceback.print_exc() + raise SyntaxError, v + except KeyError, v: # unsupported mode + if Image.DEBUG > 1: + traceback.print_exc() + raise SyntaxError, v + except EOFError, v: # got header but not the first frame + if Image.DEBUG > 1: + traceback.print_exc() + raise SyntaxError, v + + if not self.mode or self.size[0] <= 0: + raise SyntaxError, "not identified by this driver" + + def draft(self, mode, size): + "Set draft mode" + + pass + + def verify(self): + "Check file integrity" + + # raise exception if something's wrong. must be called + # directly after open, and closes file when finished. + self.fp = None + + def load(self): + "Load image data based on tile list" + + pixel = Image.Image.load(self) + + if self.tile is None: + raise IOError("cannot load this image") + if not self.tile: + return pixel + + self.map = None + + readonly = 0 + + if self.filename and len(self.tile) == 1: + # try memory mapping + d, e, o, a = self.tile[0] + if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES: + try: + if hasattr(Image.core, "map"): + # use built-in mapper + self.map = Image.core.map(self.filename) + self.map.seek(o) + self.im = self.map.readimage( + self.mode, self.size, a[1], a[2] + ) + else: + # use mmap, if possible + import mmap + file = open(self.filename, "r+") + size = os.path.getsize(self.filename) + # FIXME: on Unix, use PROT_READ etc + self.map = mmap.mmap(file.fileno(), size) + self.im = Image.core.map_buffer( + self.map, self.size, d, e, o, a + ) + readonly = 1 + except (AttributeError, EnvironmentError, ImportError): + self.map = None + + self.load_prepare() + + # look for read/seek overrides + try: + read = self.load_read + except AttributeError: + read = self.fp.read + + try: + seek = self.load_seek + except AttributeError: + seek = self.fp.seek + + if not self.map: + + # sort tiles in file order + self.tile.sort(_tilesort) + + try: + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = self.tile_prefix + except AttributeError: + prefix = "" + + for d, e, o, a in self.tile: + d = Image._getdecoder(self.mode, d, a, self.decoderconfig) + seek(o) + try: + d.setimage(self.im, e) + except ValueError: + continue + b = prefix + t = len(b) + while 1: + s = read(self.decodermaxblock) + if not s: + self.tile = [] + raise IOError("image file is truncated (%d bytes not processed)" % len(b)) + b = b + s + n, e = d.decode(b) + if n < 0: + break + b = b[n:] + t = t + n + + self.tile = [] + self.readonly = readonly + + self.fp = None # might be shared + + if not self.map and e < 0: + error = ERRORS.get(e, "decoder error %d" % e) + raise IOError(error + " when reading image file") + + # post processing + if hasattr(self, "tile_post_rotate"): + # FIXME: This is a hack to handle rotated PCD's + self.im = self.im.rotate(self.tile_post_rotate) + self.size = self.im.size + + self.load_end() + + return Image.Image.load(self) + + def load_prepare(self): + # create image memory if necessary + if not self.im or\ + self.im.mode != self.mode or self.im.size != self.size: + self.im = Image.core.new(self.mode, self.size) + # create palette (optional) + if self.mode == "P": + Image.Image.load(self) + + def load_end(self): + # may be overridden + pass + + # may be defined for contained formats + # def load_seek(self, pos): + # pass + + # may be defined for blocked formats (e.g. PNG) + # def load_read(self, bytes): + # pass + +## +# Base class for stub image loaders. +#

+# A stub loader is an image loader that can identify files of a +# certain format, but relies on external code to load the file. + +class StubImageFile(ImageFile): + "Base class for stub image loaders." + + def _open(self): + raise NotImplementedError( + "StubImageFile subclass must implement _open" + ) + + def load(self): + loader = self._load() + if loader is None: + raise IOError("cannot find loader for this %s file" % self.format) + image = loader.load(self) + assert image is not None + # become the other object (!) + self.__class__ = image.__class__ + self.__dict__ = image.__dict__ + + ## + # (Hook) Find actual image loader. + + def _load(self): + raise NotImplementedError( + "StubImageFile subclass must implement _load" + ) + +## +# (Internal) Support class for the Parser file. + +class _ParserFile: + # parser support class. + + def __init__(self, data): + self.data = data + self.offset = 0 + + def close(self): + self.data = self.offset = None + + def tell(self): + return self.offset + + def seek(self, offset, whence=0): + if whence == 0: + self.offset = offset + elif whence == 1: + self.offset = self.offset + offset + else: + # force error in Image.open + raise IOError("illegal argument to seek") + + def read(self, bytes=0): + pos = self.offset + if bytes: + data = self.data[pos:pos+bytes] + else: + data = self.data[pos:] + self.offset = pos + len(data) + return data + + def readline(self): + # FIXME: this is slow! + s = "" + while 1: + c = self.read(1) + if not c: + break + s = s + c + if c == "\n": + break + return s + +## +# Incremental image parser. This class implements the standard +# feed/close consumer interface. + +class Parser: + + incremental = None + image = None + data = None + decoder = None + finished = 0 + + ## + # (Consumer) Reset the parser. Note that you can only call this + # method immediately after you've created a parser; parser + # instances cannot be reused. + + def reset(self): + assert self.data is None, "cannot reuse parsers" + + ## + # (Consumer) Feed data to the parser. + # + # @param data A string buffer. + # @exception IOError If the parser failed to parse the image file. + + def feed(self, data): + # collect data + + if self.finished: + return + + if self.data is None: + self.data = data + else: + self.data = self.data + data + + # parse what we have + if self.decoder: + + if self.offset > 0: + # skip header + skip = min(len(self.data), self.offset) + self.data = self.data[skip:] + self.offset = self.offset - skip + if self.offset > 0 or not self.data: + return + + n, e = self.decoder.decode(self.data) + + if n < 0: + # end of stream + self.data = None + self.finished = 1 + if e < 0: + # decoding error + self.image = None + error = ERRORS.get(e, "decoder error %d" % e) + raise IOError(error + " when reading image file") + else: + # end of image + return + self.data = self.data[n:] + + elif self.image: + + # if we end up here with no decoder, this file cannot + # be incrementally parsed. wait until we've gotten all + # available data + pass + + else: + + # attempt to open this file + try: + try: + fp = _ParserFile(self.data) + im = Image.open(fp) + finally: + fp.close() # explicitly close the virtual file + except IOError: + pass # not enough data + else: + flag = hasattr(im, "load_seek") or hasattr(im, "load_read") + if flag or len(im.tile) != 1: + # custom load code, or multiple tiles + self.decode = None + else: + # initialize decoder + im.load_prepare() + d, e, o, a = im.tile[0] + im.tile = [] + self.decoder = Image._getdecoder( + im.mode, d, a, im.decoderconfig + ) + self.decoder.setimage(im.im, e) + + # calculate decoder offset + self.offset = o + if self.offset <= len(self.data): + self.data = self.data[self.offset:] + self.offset = 0 + + self.image = im + + ## + # (Consumer) Close the stream. + # + # @return An image object. + # @exception IOError If the parser failed to parse the image file. + + def close(self): + # finish decoding + if self.decoder: + # get rid of what's left in the buffers + self.feed("") + self.data = self.decoder = None + if not self.finished: + raise IOError("image was incomplete") + if not self.image: + raise IOError("cannot parse this image") + if self.data: + # incremental parsing not possible; reopen the file + # not that we have all data + try: + fp = _ParserFile(self.data) + self.image = Image.open(fp) + finally: + fp.close() # explicitly close the virtual file + return self.image + +# -------------------------------------------------------------------- + +## +# (Helper) Save image body to file. +# +# @param im Image object. +# @param fp File object. +# @param tile Tile list. + +def _save(im, fp, tile): + "Helper to save image based on tile list" + + im.load() + if not hasattr(im, "encoderconfig"): + im.encoderconfig = () + tile.sort(_tilesort) + # FIXME: make MAXBLOCK a configuration parameter + bufsize = max(MAXBLOCK, im.size[0] * 4) # see RawEncode.c + try: + fh = fp.fileno() + fp.flush() + except AttributeError: + # compress to Python file-compatible object + for e, b, o, a in tile: + e = Image._getencoder(im.mode, e, a, im.encoderconfig) + if o > 0: + fp.seek(o, 0) + e.setimage(im.im, b) + while 1: + l, s, d = e.encode(bufsize) + fp.write(d) + if s: + break + if s < 0: + raise IOError("encoder error %d when writing image file" % s) + else: + # slight speedup: compress to real file object + for e, b, o, a in tile: + e = Image._getencoder(im.mode, e, a, im.encoderconfig) + if o > 0: + fp.seek(o, 0) + e.setimage(im.im, b) + s = e.encode_to_file(fh, bufsize) + if s < 0: + raise IOError("encoder error %d when writing image file" % s) + try: + fp.flush() + except: pass + + +## +# Reads large blocks in a safe way. Unlike fp.read(n), this function +# doesn't trust the user. If the requested size is larger than +# SAFEBLOCK, the file is read block by block. +# +# @param fp File handle. Must implement a read method. +# @param size Number of bytes to read. +# @return A string containing up to size bytes of data. + +def _safe_read(fp, size): + if size <= 0: + return "" + if size <= SAFEBLOCK: + return fp.read(size) + data = [] + while size > 0: + block = fp.read(min(size, SAFEBLOCK)) + if not block: + break + data.append(block) + size = size - len(block) + return string.join(data, "") diff --git a/PIL/ImageFileIO.py b/PIL/ImageFileIO.py new file mode 100644 index 0000000..7c6a5c1 --- /dev/null +++ b/PIL/ImageFileIO.py @@ -0,0 +1,47 @@ +# +# The Python Imaging Library. +# $Id: ImageFileIO.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# kludge to get basic ImageFileIO functionality +# +# History: +# 1998-08-06 fl Recreated +# +# Copyright (c) Secret Labs AB 1998-2002. +# +# See the README file for information on usage and redistribution. +# + +from StringIO import StringIO + +## +# The ImageFileIO module can be used to read an image from a +# socket, or any other stream device. +#

+# This module is deprecated. New code should use the Parser +# class in the ImageFile module instead. +# +# @see ImageFile#Parser + +class ImageFileIO(StringIO): + + ## + # Adds buffering to a stream file object, in order to + # provide seek and tell methods required + # by the Image.open method. The stream object must + # implement read and close methods. + # + # @param fp Stream file handle. + # @see Image#open + + def __init__(self, fp): + data = fp.read() + StringIO.__init__(self, data) + +if __name__ == "__main__": + + import Image + fp = open("/images/clenna.im", "rb") + im = Image.open(ImageFileIO(fp)) + im.load() # make sure we can read the raster data + print im.mode, im.size diff --git a/PIL/ImageFilter.py b/PIL/ImageFilter.py new file mode 100644 index 0000000..5857686 --- /dev/null +++ b/PIL/ImageFilter.py @@ -0,0 +1,263 @@ +# +# The Python Imaging Library. +# $Id: ImageFilter.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# standard filters +# +# History: +# 1995-11-27 fl Created +# 2002-06-08 fl Added rank and mode filters +# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2002 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +class Filter: + pass + +## +# Convolution filter kernel. + +class Kernel(Filter): + + ## + # Create a convolution kernel. The current version only + # supports 3x3 and 5x5 integer and floating point kernels. + #

+ # In the current version, kernels can only be applied to + # "L" and "RGB" images. + # + # @def __init__(size, kernel, **options) + # @param size Kernel size, given as (width, height). In + # the current version, this must be (3,3) or (5,5). + # @param kernel A sequence containing kernel weights. + # @param **options Optional keyword arguments. + # @keyparam scale Scale factor. If given, the result for each + # pixel is divided by this value. The default is the sum + # of the kernel weights. + # @keyparam offset Offset. If given, this value is added to the + # result, after it has been divided by the scale factor. + + def __init__(self, size, kernel, scale=None, offset=0): + if scale is None: + # default scale is sum of kernel + scale = reduce(lambda a,b: a+b, kernel) + if size[0] * size[1] != len(kernel): + raise ValueError("not enough coefficients in kernel") + self.filterargs = size, scale, offset, kernel + + def filter(self, image): + if image.mode == "P": + raise ValueError("cannot filter palette images") + return apply(image.filter, self.filterargs) + +class BuiltinFilter(Kernel): + def __init__(self): + pass + +## +# Rank filter. + +class RankFilter(Filter): + name = "Rank" + + ## + # Create a rank filter. The rank filter sorts all pixels in + # a window of the given size, and returns the rank'th value. + # + # @param size The kernel size, in pixels. + # @param rank What pixel value to pick. Use 0 for a min filter, + # size*size/2 for a median filter, size*size-1 for a max filter, + # etc. + + def __init__(self, size, rank): + self.size = size + self.rank = rank + + def filter(self, image): + image = image.expand(self.size/2, self.size/2) + return image.rankfilter(self.size, self.rank) + +## +# Median filter. Picks the median pixel value in a window with the +# given size. + +class MedianFilter(RankFilter): + name = "Median" + + ## + # Create a median filter. + # + # @param size The kernel size, in pixels. + + def __init__(self, size=3): + self.size = size + self.rank = size*size/2 + +## +# Min filter. Picks the lowest pixel value in a window with the given +# size. + +class MinFilter(RankFilter): + name = "Min" + + ## + # Create a min filter. + # + # @param size The kernel size, in pixels. + + def __init__(self, size=3): + self.size = size + self.rank = 0 + +## +# Max filter. Picks the largest pixel value in a window with the +# given size. + +class MaxFilter(RankFilter): + name = "Max" + + ## + # Create a max filter. + # + # @param size The kernel size, in pixels. + + def __init__(self, size=3): + self.size = size + self.rank = size*size-1 + +## +# Mode filter. Picks the most frequent pixel value in a box with the +# given size. Pixel values that occur only once or twice are ignored; +# if no pixel value occurs more than twice, the original pixel value +# is preserved. + +class ModeFilter(Filter): + name = "Mode" + + ## + # Create a mode filter. + # + # @param size The kernel size, in pixels. + + def __init__(self, size=3): + self.size = size + def filter(self, image): + return image.modefilter(self.size) + +## +# Blur filter. + +class BLUR(BuiltinFilter): + name = "Blur" + filterargs = (5, 5), 16, 0, ( + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ) + +## +# Contour filter. + +class CONTOUR(BuiltinFilter): + name = "Contour" + filterargs = (3, 3), 1, 255, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1 + ) + +## +# Detail filter. + +class DETAIL(BuiltinFilter): + name = "Detail" + filterargs = (3, 3), 6, 0, ( + 0, -1, 0, + -1, 10, -1, + 0, -1, 0 + ) + +## +# Edge enhancement filter. + +class EDGE_ENHANCE(BuiltinFilter): + name = "Edge-enhance" + filterargs = (3, 3), 2, 0, ( + -1, -1, -1, + -1, 10, -1, + -1, -1, -1 + ) + +## +# Stronger edge enhancement filter. + +class EDGE_ENHANCE_MORE(BuiltinFilter): + name = "Edge-enhance More" + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 9, -1, + -1, -1, -1 + ) + +## +# Embossing filter. + +class EMBOSS(BuiltinFilter): + name = "Emboss" + filterargs = (3, 3), 1, 128, ( + -1, 0, 0, + 0, 1, 0, + 0, 0, 0 + ) + +## +# Edge-finding filter. + +class FIND_EDGES(BuiltinFilter): + name = "Find Edges" + filterargs = (3, 3), 1, 0, ( + -1, -1, -1, + -1, 8, -1, + -1, -1, -1 + ) + +## +# Smoothing filter. + +class SMOOTH(BuiltinFilter): + name = "Smooth" + filterargs = (3, 3), 13, 0, ( + 1, 1, 1, + 1, 5, 1, + 1, 1, 1 + ) + +## +# Stronger smoothing filter. + +class SMOOTH_MORE(BuiltinFilter): + name = "Smooth More" + filterargs = (5, 5), 100, 0, ( + 1, 1, 1, 1, 1, + 1, 5, 5, 5, 1, + 1, 5, 44, 5, 1, + 1, 5, 5, 5, 1, + 1, 1, 1, 1, 1 + ) + +## +# Sharpening filter. + +class SHARPEN(BuiltinFilter): + name = "Sharpen" + filterargs = (3, 3), 16, 0, ( + -2, -2, -2, + -2, 32, -2, + -2, -2, -2 + ) diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py new file mode 100644 index 0000000..b987a33 --- /dev/null +++ b/PIL/ImageFont.py @@ -0,0 +1,377 @@ +# +# The Python Imaging Library. +# $Id: ImageFont.py 2813 2006-10-07 10:11:35Z fredrik $ +# +# PIL raster font management +# +# History: +# 1996-08-07 fl created (experimental) +# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3 +# 1999-02-06 fl rewrote most font management stuff in C +# 1999-03-17 fl take pth files into account in load_path (from Richard Jones) +# 2001-02-17 fl added freetype support +# 2001-05-09 fl added TransposedFont wrapper class +# 2002-03-04 fl make sure we have a "L" or "1" font +# 2002-12-04 fl skip non-directory entries in the system path +# 2003-04-29 fl add embedded default font +# 2003-09-27 fl added support for truetype charmap encodings +# +# Todo: +# Adapt to PILFONT2 format (16-bit fonts, compressed, single file) +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1996-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import os, string, sys + +# FIXME: add support for pilfont2 format (see FontFile.py) + +# -------------------------------------------------------------------- +# Font metrics format: +# "PILfont" LF +# fontdescriptor LF +# (optional) key=value... LF +# "DATA" LF +# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox) +# +# To place a character, cut out srcbox and paste at dstbox, +# relative to the character position. Then move the character +# position according to dx, dy. +# -------------------------------------------------------------------- + +## +# The ImageFont module defines a class with the same name. +# Instances of this class store bitmap fonts, and are used with the +# text method of the ImageDraw class. +#

+# PIL uses it's own font file format to store bitmap fonts. You can +# use the pilfont utility to convert BDF and PCF font +# descriptors (X window font formats) to this format. +#

+# Starting with version 1.1.4, PIL can be configured to support +# TrueType and OpenType fonts. For earlier version, TrueType +# support is only available as part of the imToolkit package +# +# @see ImageDraw#ImageDraw.text +# @see pilfont + +class ImageFont: + "PIL font wrapper" + + def _load_pilfont(self, filename): + + file = open(filename, "rb") + + for ext in (".png", ".gif", ".pbm"): + try: + fullname = os.path.splitext(filename)[0] + ext + image = Image.open(fullname) + except: + pass + else: + if image and image.mode in ("1", "L"): + break + else: + raise IOError("cannot find glyph data file") + + self.file = fullname + + return self._load_pilfont_data(file, image) + + def _load_pilfont_data(self, file, image): + + # read PILfont header + if file.readline() != "PILfont\n": + raise SyntaxError("Not a PILfont file") + d = string.split(file.readline(), ";") + self.info = [] # FIXME: should be a dictionary + s = file.readline() + while s and s != "DATA\n": + self.info.append(s) + + # read PILfont metrics + data = file.read(256*20) + + # check image + if image.mode not in ("1", "L"): + raise TypeError("invalid font image mode") + + image.load() + + self.font = Image.core.font(image.im, data) + + # delegate critical operations to internal type + self.getsize = self.font.getsize + self.getmask = self.font.getmask + +## +# Wrapper for FreeType fonts. Application code should use the +# truetype factory function to create font objects. + +class FreeTypeFont: + "FreeType font wrapper (requires _imagingft service)" + + def __init__(self, file, size, index=0, encoding=""): + # FIXME: use service provider instead + import _imagingft + self.font = _imagingft.getfont(file, size, index, encoding) + + def getname(self): + return self.font.family, self.font.style + + def getmetrics(self): + return self.font.ascent, self.font.descent + + def getsize(self, text): + return self.font.getsize(text)[0] + + def getmask(self, text, mode=""): + return self.getmask2(text, mode)[0] + + def getmask2(self, text, mode="", fill=Image.core.fill): + size, offset = self.font.getsize(text) + im = fill("L", size, 0) + self.font.render(text, im.id, mode=="1") + return im, offset + +## +# Wrapper that creates a transposed font from any existing font +# object. +# +# @param font A font object. +# @param orientation An optional orientation. If given, this should +# be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, +# Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. + +class TransposedFont: + "Wrapper for writing rotated or mirrored text" + + def __init__(self, font, orientation=None): + self.font = font + self.orientation = orientation # any 'transpose' argument, or None + + def getsize(self, text): + w, h = self.font.getsize(text) + if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): + return h, w + return w, h + + def getmask(self, text, mode=""): + im = self.font.getmask(text, mode) + if self.orientation is not None: + return im.transpose(self.orientation) + return im + +## +# Load font file. This function loads a font object from the given +# bitmap font file, and returns the corresponding font object. +# +# @param filename Name of font file. +# @return A font object. +# @exception IOError If the file could not be read. + +def load(filename): + "Load a font file." + f = ImageFont() + f._load_pilfont(filename) + return f + +## +# Load a TrueType or OpenType font file, and create a font object. +# This function loads a font object from the given file, and creates +# a font object for a font of the given size. +#

+# This function requires the _imagingft service. +# +# @param filename A truetype font file. Under Windows, if the file +# is not found in this filename, the loader also looks in Windows +# fonts directory +# @param size The requested size, in points. +# @param index Which font face to load (default is first available face). +# @param encoding Which font encoding to use (default is Unicode). Common +# encodings are "unic" (Unicode), "symb" (Microsoft Symbol), "ADOB" +# (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple Roman). +# See the FreeType documentation for more information. +# @return A font object. +# @exception IOError If the file could not be read. + +def truetype(filename, size, index=0, encoding=""): + "Load a truetype font file." + try: + return FreeTypeFont(filename, size, index, encoding) + except IOError: + if sys.platform == "win32": + # check the windows font repository + # NOTE: must use uppercase WINDIR, to work around bugs in + # 1.5.2's os.environ.get() + windir = os.environ.get("WINDIR") + if windir: + filename = os.path.join(windir, "fonts", filename) + return FreeTypeFont(filename, size, index, encoding) + raise + +## +# Load font file. Same as load, but searches for a bitmap font along +# the Python path. +# +# @param filename Name of font file. +# @return A font object. +# @exception IOError If the file could not be read. +# @see #load + +def load_path(filename): + "Load a font file, searching along the Python path." + for dir in sys.path: + if Image.isDirectory(dir): + try: + return load(os.path.join(dir, filename)) + except IOError: + pass + raise IOError("cannot find font file") + +## +# Load a (probably rather ugly) default font. +# +# @return A font object. + +def load_default(): + "Load a default font." + from StringIO import StringIO + import base64 + f = ImageFont() + f._load_pilfont_data( + # courB08 + StringIO(base64.decodestring(''' +UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA +BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL +AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA +AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB +ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A +BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB +//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA +AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH +AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA +ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv +AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/ +/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5 +AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA +AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG +AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA +BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA +AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA +2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF +AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA//// ++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA +////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA +BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv +AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA +AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA +AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA +BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP// +//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA +AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF +AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB +mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn +AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA +AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7 +AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA +Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB +//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA +AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ +AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC +DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ +AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/ ++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5 +AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/ +///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG +AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA +BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA +Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC +eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG +AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA//// ++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA +////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA +BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT +AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A +AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA +Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA +Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP// +//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA +AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ +AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA +LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5 +AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA +AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5 +AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA +AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG +AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA +EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK +AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA +pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG +AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA//// ++QAGAAIAzgAKANUAEw== +''')), Image.open(StringIO(base64.decodestring(''' +iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u +Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9 +M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g +LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F +IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA +Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791 +NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx +in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9 +SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY +AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt +y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG +ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY +lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H +/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3 +AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47 +c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/ +/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw +pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv +oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR +evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA +AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v// +Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR +w7IkEbzhVQAAAABJRU5ErkJggg== +''')))) + return f + +if __name__ == "__main__": + # create font data chunk for embedding + import base64, os, sys + font = "../Images/courB08" + print " f._load_pilfont_data(" + print " # %s" % os.path.basename(font) + print " StringIO(base64.decodestring('''" + base64.encode(open(font + ".pil", "rb"), sys.stdout) + print "''')), Image.open(StringIO(base64.decodestring('''" + base64.encode(open(font + ".pbm", "rb"), sys.stdout) + print "'''))))" diff --git a/PIL/ImageGL.py b/PIL/ImageGL.py new file mode 100644 index 0000000..482f42d --- /dev/null +++ b/PIL/ImageGL.py @@ -0,0 +1,28 @@ +# +# The Python Imaging Library. +# $Id: ImageGL.py 2438 2005-05-25 21:09:48Z Fredrik $ +# +# OpenGL pixmap/texture interface (requires imToolkit OpenGL extensions) +# +# History: +# 2003-09-13 fl Added +# +# Copyright (c) Secret Labs AB 2003. +# +# See the README file for information on usage and redistribution. +# + +## +# OpenGL pixmap/texture interface (requires imToolkit OpenGL +# extensions.) +## + +import _imaginggl + +## +# Texture factory. + +class TextureFactory: + pass # overwritten by the _imaginggl module + +from _imaginggl import * diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py new file mode 100644 index 0000000..ecbfbac --- /dev/null +++ b/PIL/ImageGrab.py @@ -0,0 +1,71 @@ +# +# The Python Imaging Library +# $Id: ImageGrab.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# screen grabber (windows only) +# +# History: +# 2001-04-26 fl created +# 2001-09-17 fl use builtin driver, if present +# 2002-11-19 fl added grabclipboard support +# +# Copyright (c) 2001-2002 by Secret Labs AB +# Copyright (c) 2001-2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image + +## +# (New in 1.1.3) The ImageGrab module can be used to copy +# the contents of the screen to a PIL image memory. +#

+# The current version works on Windows only.

+# +# @since 1.1.3 +## + +try: + # built-in driver (1.1.3 and later) + grabber = Image.core.grabscreen +except AttributeError: + # stand-alone driver (pil plus) + import _grabscreen + grabber = _grabscreen.grab + +## +# (New in 1.1.3) Take a snapshot of the screen. The pixels inside the +# bounding box are returned as an "RGB" image. If the bounding box is +# omitted, the entire screen is copied. +# +# @param bbox What region to copy. Default is the entire screen. +# @return An image +# @since 1.1.3 + +def grab(bbox=None): + size, data = grabber() + im = Image.fromstring( + "RGB", size, data, + # RGB, 32-bit line padding, origo in lower left corner + "raw", "BGR", (size[0]*3 + 3) & -4, -1 + ) + if bbox: + im = im.crop(bbox) + return im + +## +# (New in 1.1.4) Take a snapshot of the clipboard image, if any. +# +# @return An image, a list of filenames, or None if the clipboard does +# not contain image data or filenames. Note that if a list is +# returned, the filenames may not represent image files. +# @since 1.1.4 + +def grabclipboard(): + debug = 0 # temporary interface + data = Image.core.grabclipboard(debug) + if Image.isStringType(data): + import BmpImagePlugin, StringIO + return BmpImagePlugin.DibImageFile(StringIO.StringIO(data)) + return data diff --git a/PIL/ImageMath.py b/PIL/ImageMath.py new file mode 100644 index 0000000..117a5ae --- /dev/null +++ b/PIL/ImageMath.py @@ -0,0 +1,207 @@ +# +# The Python Imaging Library +# $Id: ImageMath.py 2508 2005-09-12 19:01:03Z fredrik $ +# +# a simple math add-on for the Python Imaging Library +# +# History: +# 1999-02-15 fl Original PIL Plus release +# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6 +# 2005-09-12 fl Fixed int() and float() for Python 2.4.1 +# +# Copyright (c) 1999-2005 by Secret Labs AB +# Copyright (c) 2005 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import _imagingmath + +VERBOSE = 0 + +def _isconstant(v): + return isinstance(v, type(0)) or isinstance(v, type(0.0)) + +class _Operand: + # wraps an image operand, providing standard operators + + def __init__(self, im): + self.im = im + + def __fixup(self, im1): + # convert image to suitable mode + if isinstance(im1, _Operand): + # argument was an image. + if im1.im.mode in ("1", "L"): + return im1.im.convert("I") + elif im1.im.mode in ("I", "F"): + return im1.im + else: + raise ValueError, "unsupported mode: %s" % im1.im.mode + else: + # argument was a constant + if _isconstant(im1) and self.im.mode in ("1", "L", "I"): + return Image.new("I", self.im.size, im1) + else: + return Image.new("F", self.im.size, im1) + + def apply(self, op, im1, im2=None, mode=None): + im1 = self.__fixup(im1) + if im2 is None: + # unary operation + out = Image.new(mode or im1.mode, im1.size, None) + im1.load() + try: + op = getattr(_imagingmath, op+"_"+im1.mode) + except AttributeError: + raise TypeError, "bad operand type for '%s'" % op + _imagingmath.unop(op, out.im.id, im1.im.id) + else: + # binary operation + im2 = self.__fixup(im2) + if im1.mode != im2.mode: + # convert both arguments to floating point + if im1.mode != "F": im1 = im1.convert("F") + if im2.mode != "F": im2 = im2.convert("F") + if im1.mode != im2.mode: + raise ValueError, "mode mismatch" + if im1.size != im2.size: + # crop both arguments to a common size + size = (min(im1.size[0], im2.size[0]), + min(im1.size[1], im2.size[1])) + if im1.size != size: im1 = im1.crop((0, 0) + size) + if im2.size != size: im2 = im2.crop((0, 0) + size) + out = Image.new(mode or im1.mode, size, None) + else: + out = Image.new(mode or im1.mode, im1.size, None) + im1.load(); im2.load() + try: + op = getattr(_imagingmath, op+"_"+im1.mode) + except AttributeError: + raise TypeError, "bad operand type for '%s'" % op + _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) + return _Operand(out) + + # unary operators + def __nonzero__(self): + # an image is "true" if it contains at least one non-zero pixel + return self.im.getbbox() is not None + def __abs__(self): + return self.apply("abs", self) + def __pos__(self): + return self + def __neg__(self): + return self.apply("neg", self) + + # binary operators + def __add__(self, other): + return self.apply("add", self, other) + def __radd__(self, other): + return self.apply("add", other, self) + def __sub__(self, other): + return self.apply("sub", self, other) + def __rsub__(self, other): + return self.apply("sub", other, self) + def __mul__(self, other): + return self.apply("mul", self, other) + def __rmul__(self, other): + return self.apply("mul", other, self) + def __div__(self, other): + return self.apply("div", self, other) + def __rdiv__(self, other): + return self.apply("div", other, self) + def __mod__(self, other): + return self.apply("mod", self, other) + def __rmod__(self, other): + return self.apply("mod", other, self) + def __pow__(self, other): + return self.apply("pow", self, other) + def __rpow__(self, other): + return self.apply("pow", other, self) + + # bitwise + def __invert__(self): + return self.apply("invert", self) + def __and__(self, other): + return self.apply("and", self, other) + def __rand__(self, other): + return self.apply("and", other, self) + def __or__(self, other): + return self.apply("or", self, other) + def __ror__(self, other): + return self.apply("or", other, self) + def __xor__(self, other): + return self.apply("xor", self, other) + def __rxor__(self, other): + return self.apply("xor", other, self) + def __lshift__(self, other): + return self.apply("lshift", self, other) + def __rshift__(self, other): + return self.apply("rshift", self, other) + + # logical + def __eq__(self, other): + return self.apply("eq", self, other) + def __ne__(self, other): + return self.apply("ne", self, other) + def __lt__(self, other): + return self.apply("lt", self, other) + def __le__(self, other): + return self.apply("le", self, other) + def __gt__(self, other): + return self.apply("gt", self, other) + def __ge__(self, other): + return self.apply("ge", self, other) + +# conversions +def imagemath_int(self): + return _Operand(self.im.convert("I")) +def imagemath_float(self): + return _Operand(self.im.convert("F")) + +# logical +def imagemath_equal(self, other): + return self.apply("eq", self, other, mode="I") +def imagemath_notequal(self, other): + return self.apply("ne", self, other, mode="I") + +def imagemath_min(self, other): + return self.apply("min", self, other) +def imagemath_max(self, other): + return self.apply("max", self, other) + +def imagemath_convert(self, mode): + return _Operand(self.im.convert(mode)) + +ops = {} +for k, v in globals().items(): + if k[:10] == "imagemath_": + ops[k[10:]] = v + +## +# Evaluates an image expression. +# +# @param expression A string containing a Python-style expression. +# @keyparam options Values to add to the evaluation context. You +# can either use a dictionary, or one or more keyword arguments. +# @return The evaluated expression. This is usually an image object, +# but can also be an integer, a floating point value, or a pixel +# tuple, depending on the expression. + +def eval(expression, _dict={}, **kw): + + # build execution namespace + args = ops.copy() + args.update(_dict) + args.update(kw) + for k, v in args.items(): + if hasattr(v, "im"): + args[k] = _Operand(v) + + import __builtin__ + out =__builtin__.eval(expression, args) + try: + return out.im + except AttributeError: + return out diff --git a/PIL/ImageMode.py b/PIL/ImageMode.py new file mode 100644 index 0000000..87a0e5a --- /dev/null +++ b/PIL/ImageMode.py @@ -0,0 +1,46 @@ +# +# The Python Imaging Library. +# $Id: /work/modules/pil/PIL/ImageFilter.py 486 2004-10-06T08:55:20.930352Z fredrik $ +# +# standard mode descriptors +# +# History: +# 2006-03-20 fl Added +# +# Copyright (c) 2006 by Secret Labs AB. +# Copyright (c) 2006 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +# mode descriptor cache +_modes = {} + +## +# Wrapper for mode strings. + +class ModeDescriptor: + + def __init__(self, mode, bands, basemode, basetype): + self.mode = mode + self.bands = bands + self.basemode = basemode + self.basetype = basetype + + def __str__(self): + return self.mode + +## +# Gets a mode descriptor for the given mode. + +def getmode(mode): + if not _modes: + # initialize mode cache + import Image + # core modes + for m, (basemode, basetype, bands) in Image._MODEINFO.items(): + _modes[m] = ModeDescriptor(m, bands, basemode, basetype) + # extra experimental modes + _modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") + _modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") + return _modes[mode] diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py new file mode 100644 index 0000000..89b5e72 --- /dev/null +++ b/PIL/ImageOps.py @@ -0,0 +1,408 @@ +# +# The Python Imaging Library. +# $Id: ImageOps.py 2760 2006-06-19 13:31:40Z fredrik $ +# +# standard image operations +# +# History: +# 2001-10-20 fl Created +# 2001-10-23 fl Added autocontrast operator +# 2001-12-18 fl Added Kevin's fit operator +# 2004-03-14 fl Fixed potential division by zero in equalize +# 2005-05-05 fl Fixed equalize for low number of values +# +# Copyright (c) 2001-2004 by Secret Labs AB +# Copyright (c) 2001-2004 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image +import operator + +## +# (New in 1.1.3) The ImageOps module contains a number of +# 'ready-made' image processing operations. This module is somewhat +# experimental, and most operators only work on L and RGB images. +# +# @since 1.1.3 +## + +# +# helpers + +def _border(border): + if type(border) is type(()): + if len(border) == 2: + left, top = right, bottom = border + elif len(border) == 4: + left, top, right, bottom = border + else: + left = top = right = bottom = border + return left, top, right, bottom + +def _color(color, mode): + if Image.isStringType(color): + import ImageColor + color = ImageColor.getcolor(color, mode) + return color + +def _lut(image, lut): + if image.mode == "P": + # FIXME: apply to lookup table, not image data + raise NotImplementedError("mode P support coming soon") + elif image.mode in ("L", "RGB"): + if image.mode == "RGB" and len(lut) == 256: + lut = lut + lut + lut + return image.point(lut) + else: + raise IOError, "not supported for this image mode" + +# +# actions + +## +# Maximize (normalize) image contrast. This function calculates a +# histogram of the input image, removes cutoff percent of the +# lightest and darkest pixels from the histogram, and remaps the image +# so that the darkest pixel becomes black (0), and the lightest +# becomes white (255). +# +# @param image The image to process. +# @param cutoff How many percent to cut off from the histogram. +# @param ignore The background pixel value (use None for no background). +# @return An image. + +def autocontrast(image, cutoff=0, ignore=None): + "Maximize image contrast, based on histogram" + histogram = image.histogram() + lut = [] + for layer in range(0, len(histogram), 256): + h = histogram[layer:layer+256] + if ignore is not None: + # get rid of outliers + try: + h[ignore] = 0 + except TypeError: + # assume sequence + for ix in ignore: + h[ix] = 0 + if cutoff: + # cut off pixels from both ends of the histogram + # get number of pixels + n = 0 + for ix in range(256): + n = n + h[ix] + # remove cutoff% pixels from the low end + cut = n * cutoff / 100 + for lo in range(256): + if cut > h[lo]: + cut = cut - h[lo] + h[lo] = 0 + else: + h[lo] = h[lo] - cut + cut = 0 + if cut <= 0: + break + # remove cutoff% samples from the hi end + cut = n * cutoff / 100 + for hi in range(255, -1, -1): + if cut > h[hi]: + cut = cut - h[hi] + h[hi] = 0 + else: + h[hi] = h[hi] - cut + cut = 0 + if cut <= 0: + break + # find lowest/highest samples after preprocessing + for lo in range(256): + if h[lo]: + break + for hi in range(255, -1, -1): + if h[hi]: + break + if hi <= lo: + # don't bother + lut.extend(range(256)) + else: + scale = 255.0 / (hi - lo) + offset = -lo * scale + for ix in range(256): + ix = int(ix * scale + offset) + if ix < 0: + ix = 0 + elif ix > 255: + ix = 255 + lut.append(ix) + return _lut(image, lut) + +## +# Colorize grayscale image. The black and white +# arguments should be RGB tuples; this function calculates a colour +# wedge mapping all black pixels in the source image to the first +# colour, and all white pixels to the second colour. +# +# @param image The image to colourize. +# @param black The colour to use for black input pixels. +# @param white The colour to use for white input pixels. +# @return An image. + +def colorize(image, black, white): + "Colorize a grayscale image" + assert image.mode == "L" + black = _color(black, "RGB") + white = _color(white, "RGB") + red = []; green = []; blue = [] + for i in range(256): + red.append(black[0]+i*(white[0]-black[0])/255) + green.append(black[1]+i*(white[1]-black[1])/255) + blue.append(black[2]+i*(white[2]-black[2])/255) + image = image.convert("RGB") + return _lut(image, red + green + blue) + +## +# Remove border from image. The same amount of pixels are removed +# from all four sides. This function works on all image modes. +# +# @param image The image to crop. +# @param border The number of pixels to remove. +# @return An image. +# @see Image#Image.crop + +def crop(image, border=0): + "Crop border off image" + left, top, right, bottom = _border(border) + return image.crop( + (left, top, image.size[0]-right, image.size[1]-bottom) + ) + +## +# Deform the image. +# +# @param image The image to deform. +# @param deformer A deformer object. Any object that implements a +# getmesh method can be used. +# @param resample What resampling filter to use. +# @return An image. + +def deform(image, deformer, resample=Image.BILINEAR): + "Deform image using the given deformer" + return image.transform( + image.size, Image.MESH, deformer.getmesh(image), resample + ) + +## +# Equalize the image histogram. This function applies a non-linear +# mapping to the input image, in order to create a uniform +# distribution of grayscale values in the output image. +# +# @param image The image to equalize. +# @param mask An optional mask. If given, only the pixels selected by +# the mask are included in the analysis. +# @return An image. + +def equalize(image, mask=None): + "Equalize image histogram" + if image.mode == "P": + image = image.convert("RGB") + h = image.histogram(mask) + lut = [] + for b in range(0, len(h), 256): + histo = filter(None, h[b:b+256]) + if len(histo) <= 1: + lut.extend(range(256)) + else: + step = (reduce(operator.add, histo) - histo[-1]) / 255 + if not step: + lut.extend(range(256)) + else: + n = step / 2 + for i in range(256): + lut.append(n / step) + n = n + h[i+b] + return _lut(image, lut) + +## +# Add border to the image +# +# @param image The image to expand. +# @param border Border width, in pixels. +# @param fill Pixel fill value (a colour value). Default is 0 (black). +# @return An image. + +def expand(image, border=0, fill=0): + "Add border to image" + left, top, right, bottom = _border(border) + width = left + image.size[0] + right + height = top + image.size[1] + bottom + out = Image.new(image.mode, (width, height), _color(fill, image.mode)) + out.paste(image, (left, top)) + return out + +## +# Returns a sized and cropped version of the image, cropped to the +# requested aspect ratio and size. +#

+# The fit function was contributed by Kevin Cazabon. +# +# @param size The requested output size in pixels, given as a +# (width, height) tuple. +# @param method What resampling method to use. Default is Image.NEAREST. +# @param bleed Remove a border around the outside of the image (from all +# four edges. The value is a decimal percentage (use 0.01 for one +# percent). The default value is 0 (no border). +# @param centering Control the cropping position. Use (0.5, 0.5) for +# center cropping (e.g. if cropping the width, take 50% off of the +# left side, and therefore 50% off the right side). (0.0, 0.0) +# will crop from the top left corner (i.e. if cropping the width, +# take all of the crop off of the right side, and if cropping the +# height, take all of it off the bottom). (1.0, 0.0) will crop +# from the bottom left corner, etc. (i.e. if cropping the width, +# take all of the crop off the left side, and if cropping the height +# take none from the top, and therefore all off the bottom). +# @return An image. + +def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): + """ + This method returns a sized and cropped version of the image, + cropped to the aspect ratio and size that you request. + """ + + # by Kevin Cazabon, Feb 17/2000 + # kevin@cazabon.com + # http://www.cazabon.com + + # ensure inputs are valid + if type(centering) != type([]): + centering = [centering[0], centering[1]] + + if centering[0] > 1.0 or centering[0] < 0.0: + centering [0] = 0.50 + if centering[1] > 1.0 or centering[1] < 0.0: + centering[1] = 0.50 + + if bleed > 0.49999 or bleed < 0.0: + bleed = 0.0 + + # calculate the area to use for resizing and cropping, subtracting + # the 'bleed' around the edges + + # number of pixels to trim off on Top and Bottom, Left and Right + bleedPixels = ( + int((float(bleed) * float(image.size[0])) + 0.5), + int((float(bleed) * float(image.size[1])) + 0.5) + ) + + liveArea = ( + bleedPixels[0], bleedPixels[1], image.size[0] - bleedPixels[0] - 1, + image.size[1] - bleedPixels[1] - 1 + ) + + liveSize = (liveArea[2] - liveArea[0], liveArea[3] - liveArea[1]) + + # calculate the aspect ratio of the liveArea + liveAreaAspectRatio = float(liveSize[0])/float(liveSize[1]) + + # calculate the aspect ratio of the output image + aspectRatio = float(size[0]) / float(size[1]) + + # figure out if the sides or top/bottom will be cropped off + if liveAreaAspectRatio >= aspectRatio: + # liveArea is wider than what's needed, crop the sides + cropWidth = int((aspectRatio * float(liveSize[1])) + 0.5) + cropHeight = liveSize[1] + else: + # liveArea is taller than what's needed, crop the top and bottom + cropWidth = liveSize[0] + cropHeight = int((float(liveSize[0])/aspectRatio) + 0.5) + + # make the crop + leftSide = int(liveArea[0] + (float(liveSize[0]-cropWidth) * centering[0])) + if leftSide < 0: + leftSide = 0 + topSide = int(liveArea[1] + (float(liveSize[1]-cropHeight) * centering[1])) + if topSide < 0: + topSide = 0 + + out = image.crop( + (leftSide, topSide, leftSide + cropWidth, topSide + cropHeight) + ) + + # resize the image and return it + return out.resize(size, method) + +## +# Flip the image vertically (top to bottom). +# +# @param image The image to flip. +# @return An image. + +def flip(image): + "Flip image vertically" + return image.transpose(Image.FLIP_TOP_BOTTOM) + +## +# Convert the image to grayscale. +# +# @param image The image to convert. +# @return An image. + +def grayscale(image): + "Convert to grayscale" + return image.convert("L") + +## +# Invert (negate) the image. +# +# @param image The image to invert. +# @return An image. + +def invert(image): + "Invert image (negate)" + lut = [] + for i in range(256): + lut.append(255-i) + return _lut(image, lut) + +## +# Flip image horizontally (left to right). +# +# @param image The image to mirror. +# @return An image. + +def mirror(image): + "Flip image horizontally" + return image.transpose(Image.FLIP_LEFT_RIGHT) + +## +# Reduce the number of bits for each colour channel. +# +# @param image The image to posterize. +# @param bits The number of bits to keep for each channel (1-8). +# @return An image. + +def posterize(image, bits): + "Reduce the number of bits per color channel" + lut = [] + mask = ~(2**(8-bits)-1) + for i in range(256): + lut.append(i & mask) + return _lut(image, lut) + +## +# Invert all pixel values above a threshold. +# +# @param image The image to posterize. +# @param threshold All pixels above this greyscale level are inverted. +# @return An image. + +def solarize(image, threshold=128): + "Invert all values above threshold" + lut = [] + for i in range(256): + if i < threshold: + lut.append(i) + else: + lut.append(255-i) + return _lut(image, lut) diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py new file mode 100644 index 0000000..0e9a93a --- /dev/null +++ b/PIL/ImagePalette.py @@ -0,0 +1,161 @@ +# +# The Python Imaging Library. +# $Id: ImagePalette.py 2339 2005-03-25 08:02:17Z fredrik $ +# +# image palette object +# +# History: +# 1996-03-11 fl Rewritten. +# 1997-01-03 fl Up and running. +# 1997-08-23 fl Added load hack +# 2001-04-16 fl Fixed randint shadow bug in random() +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import array +import Image + +## +# Colour palette wrapper for palette mapped images. + +class ImagePalette: + "Colour palette for palette mapped images" + + def __init__(self, mode = "RGB", palette = None): + self.mode = mode + self.rawmode = None # if set, palette contains raw data + self.palette = palette or range(256)*len(self.mode) + self.colors = {} + self.dirty = None + if len(self.mode)*256 != len(self.palette): + raise ValueError, "wrong palette size" + + def getdata(self): + # experimental: get palette contains in format suitable + # for the low-level im.putpalette primitive + if self.rawmode: + return self.rawmode, self.palette + return self.mode + ";L", self.tostring() + + def tostring(self): + # experimental: convert palette to string + if self.rawmode: + raise ValueError("palette contains raw palette data") + if Image.isStringType(self.palette): + return self.palette + return array.array("B", self.palette).tostring() + + def getcolor(self, color): + # experimental: given an rgb tuple, allocate palette entry + if self.rawmode: + raise ValueError("palette contains raw palette data") + if Image.isTupleType(color): + try: + return self.colors[color] + except KeyError: + # allocate new color slot + if Image.isStringType(self.palette): + self.palette = map(int, self.palette) + index = len(self.colors) + if index >= 256: + raise ValueError("cannot allocate more than 256 colors") + self.colors[color] = index + self.palette[index] = color[0] + self.palette[index+256] = color[1] + self.palette[index+512] = color[2] + self.dirty = 1 + return index + else: + raise ValueError("unknown color specifier: %r" % color) + + def save(self, fp): + # (experimental) save palette to text file + if self.rawmode: + raise ValueError("palette contains raw palette data") + if type(fp) == type(""): + fp = open(fp, "w") + fp.write("# Palette\n") + fp.write("# Mode: %s\n" % self.mode) + for i in range(256): + fp.write("%d" % i) + for j in range(i, len(self.palette), 256): + fp.write(" %d" % self.palette[j]) + fp.write("\n") + fp.close() + +# -------------------------------------------------------------------- +# Internal + +def raw(rawmode, data): + palette = ImagePalette() + palette.rawmode = rawmode + palette.palette = data + palette.dirty = 1 + return palette + +# -------------------------------------------------------------------- +# Factories + +def new(mode, data): + return Image.core.new_palette(mode, data) + +def negative(mode = "RGB"): + palette = range(256) + palette.reverse() + return ImagePalette(mode, palette * len(mode)) + +def random(mode = "RGB"): + from random import randint + palette = map(lambda a, randint=randint: + randint(0, 255), [0]*256*len(mode)) + return ImagePalette(mode, palette) + +def wedge(mode = "RGB"): + return ImagePalette(mode, range(256) * len(mode)) + +def load(filename): + + # FIXME: supports GIMP gradients only + + fp = open(filename, "rb") + + lut = None + + if not lut: + try: + import GimpPaletteFile + fp.seek(0) + p = GimpPaletteFile.GimpPaletteFile(fp) + lut = p.getpalette() + except (SyntaxError, ValueError): + pass + + if not lut: + try: + import GimpGradientFile + fp.seek(0) + p = GimpGradientFile.GimpGradientFile(fp) + lut = p.getpalette() + except (SyntaxError, ValueError): + pass + + if not lut: + try: + import PaletteFile + fp.seek(0) + p = PaletteFile.PaletteFile(fp) + lut = p.getpalette() + except (SyntaxError, ValueError): + pass + + if not lut: + raise IOError, "cannot load palette" + + return lut # data, rawmode + + +# add some psuedocolour palettes as well diff --git a/PIL/ImagePath.py b/PIL/ImagePath.py new file mode 100644 index 0000000..120a607 --- /dev/null +++ b/PIL/ImagePath.py @@ -0,0 +1,71 @@ +# +# The Python Imaging Library +# $Id: ImagePath.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# path interface +# +# History: +# 1996-11-04 fl Created +# 2002-04-14 fl Added documentation stub class +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +import Image + +## +# Path wrapper. + +class Path: + + ## + # Creates a path object. + # + # @param xy Sequence. The sequence can contain 2-tuples [(x, y), ...] + # or a flat list of numbers [x, y, ...]. + + def __init__(self, xy): + pass + + ## + # Compacts the path, by removing points that are close to each + # other. This method modifies the path in place. + + def compact(self, distance=2): + pass + + ## + # Gets the bounding box. + + def getbbox(self): + pass + + ## + # Maps the path through a function. + + def map(self, function): + pass + + ## + # Converts the path to Python list. + # + # @param flat By default, this function returns a list of 2-tuples + # [(x, y), ...]. If this argument is true, it returns a flat + # list [x, y, ...] instead. + # @return A list of coordinates. + + def tolist(self, flat=0): + pass + + ## + # Transforms the path. + + def transform(self, matrix): + pass + + +# override with C implementation +Path = Image.core.path diff --git a/PIL/ImageQt.py b/PIL/ImageQt.py new file mode 100644 index 0000000..2e9f476 --- /dev/null +++ b/PIL/ImageQt.py @@ -0,0 +1,84 @@ +# +# The Python Imaging Library. +# $Id: ImageQt.py 2741 2006-06-18 16:17:20Z fredrik $ +# +# a simple Qt image interface. +# +# history: +# 2006-06-03 fl: created +# 2006-06-04 fl: inherit from QImage instead of wrapping it +# 2006-06-05 fl: removed toimage helper; move string support to ImageQt +# +# Copyright (c) 2006 by Secret Labs AB +# Copyright (c) 2006 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image + +from PyQt4.QtGui import QImage, qRgb + +## +# (Internal) Turns an RGB color into a Qt compatible color integer. + +def rgb(r, g, b): + # use qRgb to pack the colors, and then turn the resulting long + # into a negative integer with the same bitpattern. + return (qRgb(r, g, b) & 0xffffff) - 0x1000000 + +## +# An PIL image wrapper for Qt. This is a subclass of PyQt4's QImage +# class. +# +# @param im A PIL Image object, or a file name (given either as Python +# string or a PyQt string object). + +class ImageQt(QImage): + + def __init__(self, im): + + data = None + colortable = None + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + im = unicode(im.toUtf8(), "utf-8") + if Image.isStringType(im): + im = Image.open(im) + + if im.mode == "1": + format = QImage.Format_Mono + elif im.mode == "L": + format = QImage.Format_Indexed8 + colortable = [] + for i in range(256): + colortable.append(rgb(i, i, i)) + elif im.mode == "P": + format = QImage.Format_Indexed8 + colortable = [] + palette = im.getpalette() + for i in range(0, len(palette), 3): + colortable.append(rgb(*palette[i:i+3])) + elif im.mode == "RGB": + data = im.tostring("raw", "BGRX") + format = QImage.Format_RGB32 + elif im.mode == "RGBA": + try: + data = im.tostring("raw", "BGRA") + except SystemError: + # workaround for earlier versions + r, g, b, a = im.split() + im = Image.merge("RGBA", (b, g, r, a)) + format = QImage.Format_ARGB32 + else: + raise ValueError("unsupported image mode %r" % im.mode) + + # must keep a reference, or Qt will crash! + self.__data = data or im.tostring() + + QImage.__init__(self, self.__data, im.size[0], im.size[1], format) + + if colortable: + self.setColorTable(colortable) diff --git a/PIL/ImageSequence.py b/PIL/ImageSequence.py new file mode 100644 index 0000000..452b096 --- /dev/null +++ b/PIL/ImageSequence.py @@ -0,0 +1,38 @@ +# +# The Python Imaging Library. +# $Id: ImageSequence.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# sequence support classes +# +# history: +# 1997-02-20 fl Created +# +# Copyright (c) 1997 by Secret Labs AB. +# Copyright (c) 1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +# This class implements an iterator object that can be used to loop +# over an image sequence. + +class Iterator: + + ## + # Create an iterator. + # + # @param im An image object. + + def __init__(self, im): + if not hasattr(im, "seek"): + raise AttributeError("im must have seek method") + self.im = im + + def __getitem__(self, ix): + try: + if ix: + self.im.seek(ix) + return self.im + except EOFError: + raise IndexError # end of sequence diff --git a/PIL/ImageStat.py b/PIL/ImageStat.py new file mode 100644 index 0000000..d53e645 --- /dev/null +++ b/PIL/ImageStat.py @@ -0,0 +1,179 @@ +# +# The Python Imaging Library. +# $Id: ImageStat.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# global image statistics +# +# History: +# 1996-04-05 fl Created +# 1997-05-21 fl Added mask; added rms, var, stddev attributes +# 1997-08-05 fl Added median +# 1998-07-05 hk Fixed integer overflow error +# +# Notes: +# This class shows how to implement delayed evaluation of attributes. +# To get a certain value, simply access the corresponding attribute. +# The __getattr__ dispatcher takes care of the rest. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996-97. +# +# See the README file for information on usage and redistribution. +# + +import Image +import operator, math + +## +# The ImageStat module calculates global statistics for an +# image, or a region of an image. +## + +## +# Calculate statistics for the given image. If a mask is included, +# only the regions covered by that mask are included in the +# statistics. + +class Stat: + "Get image or feature statistics" + + ## + # Create a statistics object. + # + # @def __init__(image, mask=None) + # @param image A PIL image, or a precalculate histogram. + # @param mask An optional mask. + + def __init__(self, image_or_list, mask = None): + try: + if mask: + self.h = image_or_list.histogram(mask) + else: + self.h = image_or_list.histogram() + except AttributeError: + self.h = image_or_list # assume it to be a histogram list + if type(self.h) != type([]): + raise TypeError, "first argument must be image or list" + self.bands = range(len(self.h) / 256) + + def __getattr__(self, id): + "Calculate missing attribute" + if id[:4] == "_get": + raise AttributeError, id + # calculate missing attribute + v = getattr(self, "_get" + id)() + setattr(self, id, v) + return v + + def _getextrema(self): + "Get min/max values for each band in the image" + + def minmax(histogram): + n = 255 + x = 0 + for i in range(256): + if histogram[i]: + n = min(n, i) + x = max(x, i) + return n, x # returns (255, 0) if there's no data in the histogram + + v = [] + for i in range(0, len(self.h), 256): + v.append(minmax(self.h[i:])) + return v + + def _getcount(self): + "Get total number of pixels in each layer" + + v = [] + for i in range(0, len(self.h), 256): + v.append(reduce(operator.add, self.h[i:i+256])) + return v + + def _getsum(self): + "Get sum of all pixels in each layer" + + v = [] + for i in range(0, len(self.h), 256): + sum = 0.0 + for j in range(256): + sum = sum + j * self.h[i+j] + v.append(sum) + return v + + def _getsum2(self): + "Get squared sum of all pixels in each layer" + + v = [] + for i in range(0, len(self.h), 256): + sum2 = 0.0 + for j in range(256): + sum2 = sum2 + (j ** 2) * float(self.h[i+j]) + v.append(sum2) + return v + + def _getmean(self): + "Get average pixel level for each layer" + + v = [] + for i in self.bands: + v.append(self.sum[i] / self.count[i]) + return v + + def _getmedian(self): + "Get median pixel level for each layer" + + v = [] + for i in self.bands: + s = 0 + l = self.count[i]/2 + b = i * 256 + for j in range(256): + s = s + self.h[b+j] + if s > l: + break + v.append(j) + return v + + def _getrms(self): + "Get RMS for each layer" + + v = [] + for i in self.bands: + v.append(math.sqrt(self.sum2[i] / self.count[i])) + return v + + + def _getvar(self): + "Get variance for each layer" + + v = [] + for i in self.bands: + n = self.count[i] + v.append((self.sum2[i]-(self.sum[i]**2.0)/n)/n) + return v + + def _getstddev(self): + "Get standard deviation for each layer" + + v = [] + for i in self.bands: + v.append(math.sqrt(self.var[i])) + return v + +Global = Stat # compatibility + +if __name__ == "__main__": + + im = Image.open("Images/lena.ppm") + + st = Stat(im) + + print "extrema", st.extrema + print "sum ", st.sum + print "mean ", st.mean + print "median ", st.median + print "rms ", st.rms + print "sum2 ", st.sum2 + print "var ", st.var + print "stddev ", st.stddev diff --git a/PIL/ImageTk.py b/PIL/ImageTk.py new file mode 100644 index 0000000..30f7c0e --- /dev/null +++ b/PIL/ImageTk.py @@ -0,0 +1,296 @@ +# +# The Python Imaging Library. +# $Id: ImageTk.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# a Tk display interface +# +# History: +# 96-04-08 fl Created +# 96-09-06 fl Added getimage method +# 96-11-01 fl Rewritten, removed image attribute and crop method +# 97-05-09 fl Use PyImagingPaste method instead of image type +# 97-05-12 fl Minor tweaks to match the IFUNC95 interface +# 97-05-17 fl Support the "pilbitmap" booster patch +# 97-06-05 fl Added file= and data= argument to image constructors +# 98-03-09 fl Added width and height methods to Image classes +# 98-07-02 fl Use default mode for "P" images without palette attribute +# 98-07-02 fl Explicitly destroy Tkinter image objects +# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch) +# 99-07-26 fl Automatically hook into Tkinter (if possible) +# 99-08-15 fl Hook uses _imagingtk instead of _imaging +# +# Copyright (c) 1997-1999 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Tkinter, Image + +## +# The ImageTk module contains support to create and modify +# Tkinter BitmapImage and PhotoImage objects. +#

+# For examples, see the demo programs in the Scripts +# directory. +## + +# -------------------------------------------------------------------- +# Check for Tkinter interface hooks + +_pilbitmap_ok = None + +def _pilbitmap_check(): + global _pilbitmap_ok + if _pilbitmap_ok is None: + try: + im = Image.new("1", (1,1)) + Tkinter.BitmapImage(data="PIL:%d" % im.im.id) + _pilbitmap_ok = 1 + except Tkinter.TclError: + _pilbitmap_ok = 0 + return _pilbitmap_ok + +# -------------------------------------------------------------------- +# PhotoImage + +## +# Creates a Tkinter-compatible photo image. This can be used +# everywhere Tkinter expects an image object. If the image is an RGBA +# image, pixels having alpha 0 are treated as transparent. + +class PhotoImage: + + ## + # Create a photo image object. The constructor takes either + # a PIL image, or a mode and a size. Alternatively, you can + # use the file or data options to initialize + # the photo image object. + #

+ # @def __init__(image=None, size=None, **options) + # @param image Either a PIL image, or a mode string. If a + # mode string is used, a size must also be given. + # @param size If the first argument is a mode string, this + # defines the size of the image. + # @keyparam file A filename to load the image from (using + # Image.open(file)). + # @keyparam data An 8-bit string containing image data (as + # loaded from an image file). + + def __init__(self, image=None, size=None, **kw): + + # Tk compatibility: file or data + if image is None: + if kw.has_key("file"): + image = Image.open(kw["file"]) + del kw["file"] + elif kw.has_key("data"): + from StringIO import StringIO + image = Image.open(StringIO(kw["data"])) + del kw["data"] + + if hasattr(image, "mode") and hasattr(image, "size"): + # got an image instead of a mode + mode = image.mode + if mode == "P": + # palette mapped data + image.load() + try: + mode = image.palette.mode + except AttributeError: + mode = "RGB" # default + size = image.size + kw["width"], kw["height"] = size + else: + mode = image + image = None + + if mode not in ["1", "L", "RGB", "RGBA"]: + mode = Image.getmodebase(mode) + + self.__mode = mode + self.__size = size + self.__photo = apply(Tkinter.PhotoImage, (), kw) + self.tk = self.__photo.tk + if image: + self.paste(image) + + def __del__(self): + name = self.__photo.name + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except: + pass # ignore internal errors + + ## + # Get the Tkinter photo image identifier. This method is + # automatically called by Tkinter whenever a PhotoImage object is + # passed to a Tkinter method. + # + # @return A Tkinter photo image identifier (a string). + + def __str__(self): + return str(self.__photo) + + ## + # Get the width of the image. + # + # @return The width, in pixels. + + def width(self): + return self.__size[0] + + ## + # Get the height of the image. + # + # @return The height, in pixels. + + def height(self): + return self.__size[1] + + ## + # Paste a PIL image into the photo image. Note that this can + # be very slow if the photo image is displayed. + # + # @param im A PIL image. The size must match the target region. + # If the mode does not match, the image is converted to the + # mode of the bitmap image. + # @param box A 4-tuple defining the left, upper, right, and + # lower pixel coordinate. If None is given instead of a + # tuple, all of the image is assumed. + + def paste(self, im, box=None): + + # convert to blittable + im.load() + image = im.im + if image.isblock() and im.mode == self.__mode: + block = image + else: + block = image.new_block(self.__mode, im.size) + image.convert2(block, image) # convert directly between buffers + + tk = self.__photo.tk + + try: + tk.call("PyImagingPhoto", self.__photo, block.id) + except Tkinter.TclError, v: + # activate Tkinter hook + try: + import _imagingtk + try: + _imagingtk.tkinit(tk.interpaddr(), 1) + except AttributeError: + _imagingtk.tkinit(id(tk), 0) + tk.call("PyImagingPhoto", self.__photo, block.id) + except (ImportError, AttributeError, Tkinter.TclError): + raise # configuration problem; cannot attach to Tkinter + +# -------------------------------------------------------------------- +# BitmapImage + +## +# Create a Tkinter-compatible bitmap image. This can be used +# everywhere Tkinter expects an image object. + +class BitmapImage: + + ## + # Create a Tkinter-compatible bitmap image. + #

+ # The given image must have mode "1". Pixels having value 0 are + # treated as transparent. Options, if any, are passed on to + # Tkinter. The most commonly used option is foreground, + # which is used to specify the colour for the non-transparent + # parts. See the Tkinter documentation for information on how to + # specify colours. + # + # @def __init__(image=None, **options) + # @param image A PIL image. + + def __init__(self, image=None, **kw): + + # Tk compatibility: file or data + if image is None: + if kw.has_key("file"): + image = Image.open(kw["file"]) + del kw["file"] + elif kw.has_key("data"): + from StringIO import StringIO + image = Image.open(StringIO(kw["data"])) + del kw["data"] + + self.__mode = image.mode + self.__size = image.size + + if _pilbitmap_check(): + # fast way (requires the pilbitmap booster patch) + image.load() + kw["data"] = "PIL:%d" % image.im.id + self.__im = image # must keep a reference + else: + # slow but safe way + kw["data"] = image.tobitmap() + self.__photo = apply(Tkinter.BitmapImage, (), kw) + + def __del__(self): + name = self.__photo.name + self.__photo.name = None + try: + self.__photo.tk.call("image", "delete", name) + except: + pass # ignore internal errors + + ## + # Get the width of the image. + # + # @return The width, in pixels. + + def width(self): + return self.__size[0] + + ## + # Get the height of the image. + # + # @return The height, in pixels. + + def height(self): + return self.__size[1] + + ## + # Get the Tkinter bitmap image identifier. This method is + # automatically called by Tkinter whenever a BitmapImage object + # is passed to a Tkinter method. + # + # @return A Tkinter bitmap image identifier (a string). + + def __str__(self): + return str(self.__photo) + +## +# Copies the contents of a PhotoImage to a PIL image memory. + +def getimage(photo): + photo.tk.call("PyImagingPhotoGet", photo) + +# -------------------------------------------------------------------- +# Helper for the Image.show method. + +def _show(image, title): + + class UI(Tkinter.Label): + def __init__(self, master, im): + if im.mode == "1": + self.image = BitmapImage(im, foreground="white", master=master) + else: + self.image = PhotoImage(im, master=master) + Tkinter.Label.__init__(self, master, image=self.image, + bg="black", bd=0) + + if not Tkinter._default_root: + raise IOError, "tkinter not initialized" + top = Tkinter.Toplevel() + if title: + top.title(title) + UI(top, image).pack() diff --git a/PIL/ImageTransform.py b/PIL/ImageTransform.py new file mode 100644 index 0000000..2944cd3 --- /dev/null +++ b/PIL/ImageTransform.py @@ -0,0 +1,91 @@ +# +# The Python Imaging Library. +# $Id: ImageTransform.py 2813 2006-10-07 10:11:35Z fredrik $ +# +# transform wrappers +# +# History: +# 2002-04-08 fl Created +# +# Copyright (c) 2002 by Secret Labs AB +# Copyright (c) 2002 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +import Image + +class Transform: + def __init__(self, data): + self.data = data + def getdata(self): + return self.method, self.data + +## +# Define an affine image transform. +#

+# This function takes a 6-tuple (a, b, c, d, e, f) which +# contain the first two rows from an affine transform matrix. For +# each pixel (x, y) in the output image, the new value is +# taken from a position (a x + b y + c, +# d x + e y + f) in the input image, rounded to +# nearest pixel. +#

+# This function can be used to scale, translate, rotate, and shear the +# original image. +# +# @def AffineTransform(matrix) +# @param matrix A 6-tuple (a, b, c, d, e, f) containing +# the first two rows from an affine transform matrix. +# @see Image#Image.transform + +class AffineTransform(Transform): + method = Image.AFFINE + +## +# Define a transform to extract a subregion from an image. +#

+# Maps a rectangle (defined by two corners) from the image to a +# rectangle of the given size. The resulting image will contain +# data sampled from between the corners, such that (x0, y0) +# in the input image will end up at (0,0) in the output image, +# and (x1, y1) at size. +#

+# This method can be used to crop, stretch, shrink, or mirror an +# arbitrary rectangle in the current image. It is slightly slower than +# crop, but about as fast as a corresponding resize +# operation. +# +# @def ExtentTransform(bbox) +# @param bbox A 4-tuple (x0, y0, x1, y1) which specifies +# two points in the input image's coordinate system. +# @see Image#Image.transform + +class ExtentTransform(Transform): + method = Image.EXTENT + +## +# Define an quad image transform. +#

+# Maps a quadrilateral (a region defined by four corners) from the +# image to a rectangle of the given size. +# +# @def QuadTransform(xy) +# @param xy An 8-tuple (x0, y0, x1, y1, x2, y2, y3, y3) which +# contain the upper left, lower left, lower right, and upper right +# corner of the source quadrilateral. +# @see Image#Image.transform + +class QuadTransform(Transform): + method = Image.QUAD + +## +# Define an mesh image transform. A mesh transform consists of one +# or more individual quad transforms. +# +# @def MeshTransform(data) +# @param data A list of (bbox, quad) tuples. +# @see Image#Image.transform + +class MeshTransform(Transform): + method = Image.MESH diff --git a/PIL/ImageWin.py b/PIL/ImageWin.py new file mode 100644 index 0000000..13c56b9 --- /dev/null +++ b/PIL/ImageWin.py @@ -0,0 +1,215 @@ +# +# The Python Imaging Library. +# $Id: ImageWin.py 2662 2006-03-21 22:41:02Z fredrik $ +# +# a Windows DIB display interface +# +# History: +# 1996-05-20 fl Created +# 1996-09-20 fl Fixed subregion exposure +# 1997-09-21 fl Added draw primitive (for tzPrint) +# 2003-05-21 fl Added experimental Window/ImageWindow classes +# 2003-09-05 fl Added fromstring/tostring methods +# +# Copyright (c) Secret Labs AB 1997-2003. +# Copyright (c) Fredrik Lundh 1996-2003. +# +# See the README file for information on usage and redistribution. +# + +import Image + +## +# The ImageWin module contains support to create and display +# images under Windows 95/98, NT, 2000 and later. + +class HDC: + def __init__(self, dc): + self.dc = dc + def __int__(self): + return self.dc + +class HWND: + def __init__(self, wnd): + self.wnd = wnd + def __int__(self): + return self.wnd + +## +# Create a Windows bitmap with the given mode and size. The mode can +# be one of "1", "L", "P", or "RGB". +# +# If the display requires a palette, this constructor creates a +# suitable palette and associates it with the image. For an "L" image, +# 128 greylevels are allocated. For an "RGB" image, a 6x6x6 colour +# cube is used, together with 20 greylevels. +# +# To make sure that palettes work properly under Windows, you must +# call the palette method upon certain events from Windows. + +class Dib: + + ## + # Create Windows bitmap. + # + # @param image Either a PIL image, or a mode string. If a + # mode string is used, a size must also be given. The + # mode can be one of "1", "L", "P", or "RGB". + # @param size If the first argument is a mode string, this + # defines the size of the image. + + def __init__(self, image, size=None): + if hasattr(image, "mode") and hasattr(image, "size"): + mode = image.mode + size = image.size + else: + mode = image + image = None + if mode not in ["1", "L", "P", "RGB"]: + mode = Image.getmodebase(mode) + self.image = Image.core.display(mode, size) + self.mode = mode + self.size = size + if image: + self.paste(image) + + ## + # Copy the bitmap contents to a device context. + # + # @param handle Device context (HDC), cast to a Python integer, + # or a HDC or HWND instance. In PythonWin, you can use the + # GetHandleAttrib method of the CDC class to get + # a suitable handle. + + def expose(self, handle): + if isinstance(handle, HWND): + dc = self.image.getdc(handle) + try: + result = self.image.expose(dc) + finally: + self.image.releasedc(handle, dc) + else: + result = self.image.expose(handle) + return result + + def draw(self, handle, dst, src=None): + if not src: + src = (0,0) + self.size + if isinstance(handle, HWND): + dc = self.image.getdc(handle) + try: + result = self.image.draw(dc, dst, src) + finally: + self.image.releasedc(handle, dc) + else: + result = self.image.draw(handle, dst, src) + return result + + ## + # Installs the palette associated with the image in the + # given device context. + #

+ # This method should be called upon QUERYNEWPALETTE + # and PALETTECHANGED events from Windows. If this + # method returns a non-zero value, one or more display + # palette entries were changed, and the image should be + # redrawn. + # + # @param handle Device context (HDC), cast to a Python integer, + # or an HDC or HWND instance. + # @return A true value if one or more entries were changed + # (this indicates that the image should be redrawn). + + def query_palette(self, handle): + if isinstance(handle, HWND): + handle = self.image.getdc(handle) + try: + result = self.image.query_palette(handle) + finally: + self.image.releasedc(handle, handle) + else: + result = self.image.query_palette(handle) + return result + + ## + # Paste a PIL image into the bitmap image. + # + # @param im A PIL image. The size must match the target region. + # If the mode does not match, the image is converted to the + # mode of the bitmap image. + # @param box A 4-tuple defining the left, upper, right, and + # lower pixel coordinate. If None is given instead of a + # tuple, all of the image is assumed. + + def paste(self, im, box=None): + im.load() + if self.mode != im.mode: + im = im.convert(self.mode) + if box: + self.image.paste(im.im, box) + else: + self.image.paste(im.im) + + ## + # Load display memory contents from string buffer. + # + # @param buffer A string buffer containing display data (usually + # data returned from tostring) + + def fromstring(self, buffer): + return self.image.fromstring(buffer) + + ## + # Copy display memory contents to string buffer. + # + # @return A string buffer containing display data. + + def tostring(self): + return self.image.tostring() + + +## +# Create a Window with the given title size. + +class Window: + + def __init__(self, title="PIL", width=None, height=None): + self.hwnd = Image.core.createwindow( + title, self.__dispatcher, width or 0, height or 0 + ) + + def __dispatcher(self, action, *args): + return apply(getattr(self, "ui_handle_" + action), args) + + def ui_handle_clear(self, dc, x0, y0, x1, y1): + pass + + def ui_handle_damage(self, x0, y0, x1, y1): + pass + + def ui_handle_destroy(self): + pass + + def ui_handle_repair(self, dc, x0, y0, x1, y1): + pass + + def ui_handle_resize(self, width, height): + pass + + def mainloop(self): + Image.core.eventloop() + +## +# Create an image window which displays the given image. + +class ImageWindow(Window): + + def __init__(self, image, title="PIL"): + if not isinstance(image, Dib): + image = Dib(image) + self.image = image + width, height = image.size + Window.__init__(self, title, width=width, height=height) + + def ui_handle_repair(self, dc, x0, y0, x1, y1): + self.image.draw(dc, (x0, y0, x1, y1)) diff --git a/PIL/ImtImagePlugin.py b/PIL/ImtImagePlugin.py new file mode 100644 index 0000000..f321ae3 --- /dev/null +++ b/PIL/ImtImagePlugin.py @@ -0,0 +1,93 @@ +# +# The Python Imaging Library. +# $Id: ImtImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# IM Tools support for PIL +# +# history: +# 1996-05-27 fl Created (read 8-bit images only) +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.2" + +import string, re + +import Image, ImageFile + +# +# -------------------------------------------------------------------- + +field = re.compile(r"([a-z]*) ([^ \r\n]*)") + +## +# Image plugin for IM Tools images. + +class ImtImageFile(ImageFile.ImageFile): + + format = "IMT" + format_description = "IM Tools" + + def _open(self): + + # Quick rejection: if there's not a LF among the first + # 100 bytes, this is (probably) not a text header. + + if not "\n" in self.fp.read(100): + raise SyntaxError, "not an IM file" + self.fp.seek(0) + + xsize = ysize = 0 + + while 1: + + s = self.fp.read(1) + if not s: + break + + if s == chr(12): + + # image data begins + self.tile = [("raw", (0,0)+self.size, + self.fp.tell(), + (self.mode, 0, 1))] + + break + + else: + + # read key/value pair + # FIXME: dangerous, may read whole file + s = s + self.fp.readline() + if len(s) == 1 or len(s) > 100: + break + if s[0] == "*": + continue # comment + + m = field.match(s) + if not m: + break + k, v = m.group(1,2) + if k == "width": + xsize = int(v) + self.size = xsize, ysize + elif k == "height": + ysize = int(v) + self.size = xsize, ysize + elif k == "pixel" and v == "n8": + self.mode = "L" + + +# +# -------------------------------------------------------------------- + +Image.register_open("IMT", ImtImageFile) + +# +# no extension registered (".im" is simply too common) diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py new file mode 100644 index 0000000..3535759 --- /dev/null +++ b/PIL/IptcImagePlugin.py @@ -0,0 +1,280 @@ +# +# The Python Imaging Library. +# $Id: IptcImagePlugin.py 2813 2006-10-07 10:11:35Z fredrik $ +# +# IPTC/NAA file handling +# +# history: +# 1995-10-01 fl Created +# 1998-03-09 fl Cleaned up and added to PIL +# 2002-06-18 fl Added getiptcinfo helper +# +# Copyright (c) Secret Labs AB 1997-2002. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.3" + + +import Image, ImageFile +import os, tempfile + + +COMPRESSION = { + 1: "raw", + 5: "jpeg" +} + +PAD = chr(0) * 4 + +# +# Helpers + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) + +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + +def i(c): + return i32((PAD + c)[-4:]) + +def dump(c): + for i in c: + print "%02x" % ord(i), + print + +## +# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields +# from TIFF and JPEG files, use the getiptcinfo function. + +class IptcImageFile(ImageFile.ImageFile): + + format = "IPTC" + format_description = "IPTC/NAA" + + def getint(self, key): + return i(self.info[key]) + + def field(self): + # + # get a IPTC field header + s = self.fp.read(5) + if not len(s): + return None, 0 + + tag = ord(s[1]), ord(s[2]) + + # syntax + if ord(s[0]) != 0x1C or tag[0] < 1 or tag[0] > 9: + raise SyntaxError, "invalid IPTC/NAA file" + + # field size + size = ord(s[3]) + if size > 132: + raise IOError, "illegal field length in IPTC/NAA file" + elif size == 128: + size = 0 + elif size > 128: + size = i(self.fp.read(size-128)) + else: + size = i16(s[3:]) + + return tag, size + + def _is_raw(self, offset, size): + # + # check if the file can be mapped + + # DISABLED: the following only slows things down... + return 0 + + self.fp.seek(offset) + t, sz = self.field() + if sz != size[0]: + return 0 + y = 1 + while 1: + self.fp.seek(sz, 1) + t, s = self.field() + if t != (8, 10): + break + if s != sz: + return 0 + y = y + 1 + return y == size[1] + + def _open(self): + + # load descriptive fields + while 1: + offset = self.fp.tell() + tag, size = self.field() + if not tag or tag == (8,10): + break + if size: + self.info[tag] = self.fp.read(size) + else: + self.info[tag] = None + # print tag, self.info[tag] + + # mode + layers = ord(self.info[(3,60)][0]) + component = ord(self.info[(3,60)][1]) + if self.info.has_key((3,65)): + id = ord(self.info[(3,65)][0])-1 + else: + id = 0 + if layers == 1 and not component: + self.mode = "L" + elif layers == 3 and component: + self.mode = "RGB"[id] + elif layers == 4 and component: + self.mode = "CMYK"[id] + + # size + self.size = self.getint((3,20)), self.getint((3,30)) + + # compression + try: + compression = COMPRESSION[self.getint((3,120))] + except KeyError: + raise IOError, "Unknown IPTC image compression" + + # tile + if tag == (8,10): + if compression == "raw" and self._is_raw(offset, self.size): + self.tile = [(compression, (offset, size + 5, -1), + (0, 0, self.size[0], self.size[1]))] + else: + self.tile = [("iptc", (compression, offset), + (0, 0, self.size[0], self.size[1]))] + + def load(self): + + if len(self.tile) != 1 or self.tile[0][0] != "iptc": + return ImageFile.ImageFile.load(self) + + type, tile, box = self.tile[0] + + encoding, offset = tile + + self.fp.seek(offset) + + # Copy image data to temporary file + outfile = tempfile.mktemp() + o = open(outfile, "wb") + if encoding == "raw": + # To simplify access to the extracted file, + # prepend a PPM header + o.write("P5\n%d %d\n255\n" % self.size) + while 1: + type, size = self.field() + if type != (8, 10): + break + while size > 0: + s = self.fp.read(min(size, 8192)) + if not s: + break + o.write(s) + size = size - len(s) + o.close() + + try: + try: + # fast + self.im = Image.core.open_ppm(outfile) + except: + # slightly slower + im = Image.open(outfile) + im.load() + self.im = im.im + finally: + try: os.unlink(outfile) + except: pass + + +Image.register_open("IPTC", IptcImageFile) + +Image.register_extension("IPTC", ".iim") + +## +# Get IPTC information from TIFF, JPEG, or IPTC file. +# +# @param im An image containing IPTC data. +# @return A dictionary containing IPTC information, or None if +# no IPTC information block was found. + +def getiptcinfo(im): + + import TiffImagePlugin, JpegImagePlugin + import StringIO + + data = None + + if isinstance(im, IptcImageFile): + # return info dictionary right away + return im.info + + elif isinstance(im, JpegImagePlugin.JpegImageFile): + # extract the IPTC/NAA resource + try: + app = im.app["APP13"] + if app[:14] == "Photoshop 3.0\x00": + app = app[14:] + # parse the image resource block + offset = 0 + while app[offset:offset+4] == "8BIM": + offset = offset + 4 + # resource code + code = JpegImagePlugin.i16(app, offset) + offset = offset + 2 + # resource name (usually empty) + name_len = ord(app[offset]) + name = app[offset+1:offset+1+name_len] + offset = 1 + offset + name_len + if offset & 1: + offset = offset + 1 + # resource data block + size = JpegImagePlugin.i32(app, offset) + offset = offset + 4 + if code == 0x0404: + # 0x0404 contains IPTC/NAA data + data = app[offset:offset+size] + break + offset = offset + size + if offset & 1: + offset = offset + 1 + except (AttributeError, KeyError): + pass + + elif isinstance(im, TiffImagePlugin.TiffImageFile): + # get raw data from the IPTC/NAA tag (PhotoShop tags the data + # as 4-byte integers, so we cannot use the get method...) + try: + type, data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + except (AttributeError, KeyError): + pass + + if data is None: + return None # no properties + + # create an IptcImagePlugin object without initializing it + class FakeImage: + pass + im = FakeImage() + im.__class__ = IptcImageFile + + # parse the IPTC information chunk + im.info = {} + im.fp = StringIO.StringIO(data) + + try: + im._open() + except (IndexError, KeyError): + pass # expected failure + + return im.info diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py new file mode 100644 index 0000000..b8ba8e4 --- /dev/null +++ b/PIL/JpegImagePlugin.py @@ -0,0 +1,430 @@ +# +# The Python Imaging Library. +# $Id: JpegImagePlugin.py 2763 2006-06-22 21:43:28Z fredrik $ +# +# JPEG (JFIF) file handling +# +# See "Digital Compression and Coding of Continous-Tone Still Images, +# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) +# +# History: +# 1995-09-09 fl Created +# 1995-09-13 fl Added full parser +# 1996-03-25 fl Added hack to use the IJG command line utilities +# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug +# 1996-05-28 fl Added draft support, JFIF version (0.1) +# 1996-12-30 fl Added encoder options, added progression property (0.2) +# 1997-08-27 fl Save mode 1 images as BW (0.3) +# 1998-07-12 fl Added YCbCr to draft and save methods (0.4) +# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1) +# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2) +# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3) +# 2003-04-25 fl Added experimental EXIF decoder (0.5) +# 2003-06-06 fl Added experimental EXIF GPSinfo decoder +# 2003-09-13 fl Extract COM markers +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.5" + +import array, string +import Image, ImageFile + +def i16(c,o=0): + return ord(c[o+1]) + (ord(c[o])<<8) + +def i32(c,o=0): + return ord(c[o+3]) + (ord(c[o+2])<<8) + (ord(c[o+1])<<16) + (ord(c[o])<<24) + +# +# Parser + +def Skip(self, marker): + n = i16(self.fp.read(2))-2 + ImageFile._safe_read(self.fp, n) + +def APP(self, marker): + # + # Application marker. Store these in the APP dictionary. + # Also look for well-known application markers. + + n = i16(self.fp.read(2))-2 + s = ImageFile._safe_read(self.fp, n) + + app = "APP%d" % (marker&15) + + self.app[app] = s # compatibility + self.applist.append((app, s)) + + if marker == 0xFFE0 and s[:4] == "JFIF": + # extract JFIF information + self.info["jfif"] = version = i16(s, 5) # version + self.info["jfif_version"] = divmod(version, 256) + # extract JFIF properties + try: + jfif_unit = ord(s[7]) + jfif_density = i16(s, 8), i16(s, 10) + except: + pass + else: + if jfif_unit == 1: + self.info["dpi"] = jfif_density + self.info["jfif_unit"] = jfif_unit + self.info["jfif_density"] = jfif_density + elif marker == 0xFFE1 and s[:5] == "Exif\0": + # extract Exif information (incomplete) + self.info["exif"] = s # FIXME: value will change + elif marker == 0xFFE2 and s[:5] == "FPXR\0": + # extract FlashPix information (incomplete) + self.info["flashpix"] = s # FIXME: value will change + elif marker == 0xFFEE and s[:5] == "Adobe": + self.info["adobe"] = i16(s, 5) + # extract Adobe custom properties + try: + adobe_transform = ord(s[1]) + except: + pass + else: + self.info["adobe_transform"] = adobe_transform + +def COM(self, marker): + # + # Comment marker. Store these in the APP dictionary. + + n = i16(self.fp.read(2))-2 + s = ImageFile._safe_read(self.fp, n) + + self.app["COM"] = s # compatibility + self.applist.append(("COM", s)) + +def SOF(self, marker): + # + # Start of frame marker. Defines the size and mode of the + # image. JPEG is colour blind, so we use some simple + # heuristics to map the number of layers to an appropriate + # mode. Note that this could be made a bit brighter, by + # looking for JFIF and Adobe APP markers. + + n = i16(self.fp.read(2))-2 + s = ImageFile._safe_read(self.fp, n) + self.size = i16(s[3:]), i16(s[1:]) + + self.bits = ord(s[0]) + if self.bits != 8: + raise SyntaxError("cannot handle %d-bit layers" % self.bits) + + self.layers = ord(s[5]) + if self.layers == 1: + self.mode = "L" + elif self.layers == 3: + self.mode = "RGB" + elif self.layers == 4: + self.mode = "CMYK" + else: + raise SyntaxError("cannot handle %d-layer images" % self.layers) + + if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]: + self.info["progression"] = 1 + + for i in range(6, len(s), 3): + t = s[i:i+3] + # 4-tuples: id, vsamp, hsamp, qtable + self.layer.append((t[0], ord(t[1])/16, ord(t[1])&15, ord(t[2]))) + +def DQT(self, marker): + # + # Define quantization table. Support baseline 8-bit tables + # only. Note that there might be more than one table in + # each marker. + + # FIXME: The quantization tables can be used to estimate the + # compression quality. + + n = i16(self.fp.read(2))-2 + s = ImageFile._safe_read(self.fp, n) + while len(s): + if len(s) < 65: + raise SyntaxError("bad quantization table marker") + v = ord(s[0]) + if v/16 == 0: + self.quantization[v&15] = array.array("b", s[1:65]) + s = s[65:] + else: + return # FIXME: add code to read 16-bit tables! + # raise SyntaxError, "bad quantization table element size" + + +# +# JPEG marker table + +MARKER = { + 0xFFC0: ("SOF0", "Baseline DCT", SOF), + 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF), + 0xFFC2: ("SOF2", "Progressive DCT", SOF), + 0xFFC3: ("SOF3", "Spatial lossless", SOF), + 0xFFC4: ("DHT", "Define Huffman table", Skip), + 0xFFC5: ("SOF5", "Differential sequential DCT", SOF), + 0xFFC6: ("SOF6", "Differential progressive DCT", SOF), + 0xFFC7: ("SOF7", "Differential spatial", SOF), + 0xFFC8: ("JPG", "Extension", None), + 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF), + 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF), + 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF), + 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip), + 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF), + 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF), + 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF), + 0xFFD0: ("RST0", "Restart 0", None), + 0xFFD1: ("RST1", "Restart 1", None), + 0xFFD2: ("RST2", "Restart 2", None), + 0xFFD3: ("RST3", "Restart 3", None), + 0xFFD4: ("RST4", "Restart 4", None), + 0xFFD5: ("RST5", "Restart 5", None), + 0xFFD6: ("RST6", "Restart 6", None), + 0xFFD7: ("RST7", "Restart 7", None), + 0xFFD8: ("SOI", "Start of image", None), + 0xFFD9: ("EOI", "End of image", None), + 0xFFDA: ("SOS", "Start of scan", Skip), + 0xFFDB: ("DQT", "Define quantization table", DQT), + 0xFFDC: ("DNL", "Define number of lines", Skip), + 0xFFDD: ("DRI", "Define restart interval", Skip), + 0xFFDE: ("DHP", "Define hierarchical progression", SOF), + 0xFFDF: ("EXP", "Expand reference component", Skip), + 0xFFE0: ("APP0", "Application segment 0", APP), + 0xFFE1: ("APP1", "Application segment 1", APP), + 0xFFE2: ("APP2", "Application segment 2", APP), + 0xFFE3: ("APP3", "Application segment 3", APP), + 0xFFE4: ("APP4", "Application segment 4", APP), + 0xFFE5: ("APP5", "Application segment 5", APP), + 0xFFE6: ("APP6", "Application segment 6", APP), + 0xFFE7: ("APP7", "Application segment 7", APP), + 0xFFE8: ("APP8", "Application segment 8", APP), + 0xFFE9: ("APP9", "Application segment 9", APP), + 0xFFEA: ("APP10", "Application segment 10", APP), + 0xFFEB: ("APP11", "Application segment 11", APP), + 0xFFEC: ("APP12", "Application segment 12", APP), + 0xFFED: ("APP13", "Application segment 13", APP), + 0xFFEE: ("APP14", "Application segment 14", APP), + 0xFFEF: ("APP15", "Application segment 15", APP), + 0xFFF0: ("JPG0", "Extension 0", None), + 0xFFF1: ("JPG1", "Extension 1", None), + 0xFFF2: ("JPG2", "Extension 2", None), + 0xFFF3: ("JPG3", "Extension 3", None), + 0xFFF4: ("JPG4", "Extension 4", None), + 0xFFF5: ("JPG5", "Extension 5", None), + 0xFFF6: ("JPG6", "Extension 6", None), + 0xFFF7: ("JPG7", "Extension 7", None), + 0xFFF8: ("JPG8", "Extension 8", None), + 0xFFF9: ("JPG9", "Extension 9", None), + 0xFFFA: ("JPG10", "Extension 10", None), + 0xFFFB: ("JPG11", "Extension 11", None), + 0xFFFC: ("JPG12", "Extension 12", None), + 0xFFFD: ("JPG13", "Extension 13", None), + 0xFFFE: ("COM", "Comment", COM) +} + + +def _accept(prefix): + return prefix[0] == "\377" + +## +# Image plugin for JPEG and JFIF images. + +class JpegImageFile(ImageFile.ImageFile): + + format = "JPEG" + format_description = "JPEG (ISO 10918)" + + def _open(self): + + s = self.fp.read(1) + + if ord(s[0]) != 255: + raise SyntaxError("not a JPEG file") + + # Create attributes + self.bits = self.layers = 0 + + # JPEG specifics (internal) + self.layer = [] + self.huffman_dc = {} + self.huffman_ac = {} + self.quantization = {} + self.app = {} # compatibility + self.applist = [] + + while 1: + + s = s + self.fp.read(1) + + i = i16(s) + + if MARKER.has_key(i): + name, description, handler = MARKER[i] + # print hex(i), name, description + if handler is not None: + handler(self, i) + if i == 0xFFDA: # start of scan + rawmode = self.mode + if self.mode == "CMYK": + rawmode = "CMYK;I" + self.tile = [("jpeg", (0,0) + self.size, 0, (rawmode, ""))] + # self.__offset = self.fp.tell() + break + s = self.fp.read(1) + elif i == 0 or i == 65535: + # padded marker or junk; move on + s = "\xff" + else: + raise SyntaxError("no marker found") + + def draft(self, mode, size): + + if len(self.tile) != 1: + return + + d, e, o, a = self.tile[0] + scale = 0 + + if a[0] == "RGB" and mode in ["L", "YCbCr"]: + self.mode = mode + a = mode, "" + + if size: + scale = max(self.size[0] / size[0], self.size[1] / size[1]) + for s in [8, 4, 2, 1]: + if scale >= s: + break + e = e[0], e[1], (e[2]-e[0]+s-1)/s+e[0], (e[3]-e[1]+s-1)/s+e[1] + self.size = ((self.size[0]+s-1)/s, (self.size[1]+s-1)/s) + scale = s + + self.tile = [(d, e, o, a)] + self.decoderconfig = (scale, 1) + + return self + + def load_djpeg(self): + + # ALTERNATIVE: handle JPEGs via the IJG command line utilities + + import tempfile, os + file = tempfile.mktemp() + os.system("djpeg %s >%s" % (self.filename, file)) + + try: + self.im = Image.core.open_ppm(file) + finally: + try: os.unlink(file) + except: pass + + self.mode = self.im.mode + self.size = self.im.size + + self.tile = [] + + def _getexif(self): + # Extract EXIF information. This method is highly experimental, + # and is likely to be replaced with something better in a future + # version. + import TiffImagePlugin, StringIO + def fixup(value): + if len(value) == 1: + return value[0] + return value + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + try: + data = self.info["exif"] + except KeyError: + return None + file = StringIO.StringIO(data[6:]) + head = file.read(8) + exif = {} + # process dictionary + info = TiffImagePlugin.ImageFileDirectory(head) + info.load(file) + for key, value in info.items(): + exif[key] = fixup(value) + # get exif extension + file.seek(exif[0x8769]) + info = TiffImagePlugin.ImageFileDirectory(head) + info.load(file) + for key, value in info.items(): + exif[key] = fixup(value) + # get gpsinfo extension + try: + file.seek(exif[0x8825]) + except KeyError: + pass + else: + info = TiffImagePlugin.ImageFileDirectory(head) + info.load(file) + exif[0x8825] = gps = {} + for key, value in info.items(): + gps[key] = fixup(value) + return exif + +# -------------------------------------------------------------------- +# stuff to save JPEG files + +RAWMODE = { + "1": "L", + "L": "L", + "RGB": "RGB", + "RGBA": "RGB", + "RGBX": "RGB", + "CMYK": "CMYK;I", + "YCbCr": "YCbCr", +} + +def _save(im, fp, filename): + + try: + rawmode = RAWMODE[im.mode] + except KeyError: + raise IOError("cannot write mode %s as JPEG" % im.mode) + + info = im.encoderinfo + + dpi = info.get("dpi", (0, 0)) + + # get keyword arguments + im.encoderconfig = ( + info.get("quality", 0), + # "progressive" is the official name, but older documentation + # says "progression" + # FIXME: issue a warning if the wrong form is used (post-1.1.5) + info.has_key("progressive") or info.has_key("progression"), + info.get("smooth", 0), + info.has_key("optimize"), + info.get("streamtype", 0), + dpi[0], dpi[1] + ) + + ImageFile._save(im, fp, [("jpeg", (0,0)+im.size, 0, rawmode)]) + +def _save_cjpeg(im, fp, filename): + # ALTERNATIVE: handle JPEGs via the IJG command line utilities. + import os + file = im._dump() + os.system("cjpeg %s >%s" % (file, filename)) + try: os.unlink(file) + except: pass + +# -------------------------------------------------------------------q- +# Registry stuff + +Image.register_open("JPEG", JpegImageFile, _accept) +Image.register_save("JPEG", _save) + +Image.register_extension("JPEG", ".jfif") +Image.register_extension("JPEG", ".jpe") +Image.register_extension("JPEG", ".jpg") +Image.register_extension("JPEG", ".jpeg") + +Image.register_mime("JPEG", "image/jpeg") diff --git a/PIL/McIdasImagePlugin.py b/PIL/McIdasImagePlugin.py new file mode 100644 index 0000000..b3b48ec --- /dev/null +++ b/PIL/McIdasImagePlugin.py @@ -0,0 +1,74 @@ +# +# The Python Imaging Library. +# $Id: McIdasImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Basic McIdas support for PIL +# +# History: +# 97-05-05 fl Created (8-bit images only) +# +# Thanks to Richard Jones for specs +# and samples. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.1" + +import string + +import Image, ImageFile + +def i16(c,i=0): + return ord(c[1+i])+(ord(c[i])<<8) + +def i32(c,i=0): + return ord(c[3+i])+(ord(c[2+i])<<8)+(ord(c[1+i])<<16)+(ord(c[i])<<24) + +def _accept(s): + return i32(s) == 0 and i32(s, 4) == 4 + +## +# Image plugin for McIdas area images. + +class McIdasImageFile(ImageFile.ImageFile): + + format = "MCIDAS" + format_description = "McIdas area file" + + def _open(self): + + # parse area file directory + s = self.fp.read(256) + if not _accept(s): + raise SyntaxError, "not an McIdas area file" + + # get mode + if i32(s, 40) != 1 or i32(s, 52) != 1: + raise SyntaxError, "unsupported McIdas format" + + self.mode = "L" + + # get size + self.size = i32(s, 36), i32(s, 32) + + # setup image descriptor + prefix = i32(s, 56) + offset = i32(s, 132) + + self.tile = [("raw", (0, 0) + self.size, offset, + ("L", prefix + self.size[0], 1))] + + # FIXME: should store the navigation and calibration blocks + # somewhere (or perhaps extract some basic information from + # them...) + +# -------------------------------------------------------------------- +# registry + +Image.register_open("MCIDAS", McIdasImageFile, _accept) + +# no default extension diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py new file mode 100644 index 0000000..b7b108e --- /dev/null +++ b/PIL/MicImagePlugin.py @@ -0,0 +1,96 @@ +# +# The Python Imaging Library. +# $Id: MicImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Microsoft Image Composer support for PIL +# +# Notes: +# uses TiffImagePlugin.py to read the actual image streams +# +# History: +# 97-01-20 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + +import string + +import Image, TiffImagePlugin +from OleFileIO import * + + +# +# -------------------------------------------------------------------- + + +def _accept(prefix): + return prefix[:8] == MAGIC + +## +# Image plugin for Microsoft's Image Composer file format. + +class MicImageFile(TiffImagePlugin.TiffImageFile): + + format = "MIC" + format_description = "Microsoft Image Composer" + + def _open(self): + + # read the OLE directory and see if this is a likely + # to be a Microsoft Image Composer file + + try: + self.ole = OleFileIO(self.fp) + except IOError: + raise SyntaxError, "not an MIC file; invalid OLE file" + + # find ACI subfiles with Image members (maybe not the + # best way to identify MIC files, but what the... ;-) + + self.images = [] + for file in self.ole.listdir(): + if file[1:] and file[0][-4:] == ".ACI" and file[1] == "Image": + self.images.append(file) + + # if we didn't find any images, this is probably not + # an MIC file. + if not self.images: + raise SyntaxError, "not an MIC file; no image entries" + + self.__fp = self.fp + self.frame = 0 + + if len(self.images) > 1: + self.category = Image.CONTAINER + + self.seek(0) + + def seek(self, frame): + + try: + filename = self.images[frame] + except IndexError: + raise EOFError, "no such frame" + + self.fp = self.ole.openstream(filename) + + TiffImagePlugin.TiffImageFile._open(self) + + self.frame = frame + + def tell(self): + + return self.frame + +# +# -------------------------------------------------------------------- + +Image.register_open("MIC", MicImageFile, _accept) + +Image.register_extension("MIC", ".mic") diff --git a/PIL/MpegImagePlugin.py b/PIL/MpegImagePlugin.py new file mode 100644 index 0000000..9623c7a --- /dev/null +++ b/PIL/MpegImagePlugin.py @@ -0,0 +1,83 @@ +# +# The Python Imaging Library. +# $Id: MpegImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# MPEG file handling +# +# History: +# 95-09-09 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.1" + +import array, string +import Image, ImageFile + +# +# Bitstream parser + +class BitStream: + + def __init__(self, fp): + self.fp = fp + self.bits = 0 + self.bitbuffer = 0 + + def next(self): + return ord(self.fp.read(1)) + + def peek(self, bits): + while self.bits < bits: + c = self.next() + if c < 0: + self.bits = 0 + continue + self.bitbuffer = (self.bitbuffer << 8) + c + self.bits = self.bits + 8 + return self.bitbuffer >> (self.bits - bits) & (1L << bits) - 1 + + def skip(self, bits): + while self.bits < bits: + self.bitbuffer = (self.bitbuffer << 8) + ord(self.fp.read(1)) + self.bits = self.bits + 8 + self.bits = self.bits - bits + + def read(self, bits): + v = self.peek(bits) + self.bits = self.bits - bits + return v + +## +# Image plugin for MPEG streams. This plugin can identify a stream, +# but it cannot read it. + +class MpegImageFile(ImageFile.ImageFile): + + format = "MPEG" + format_description = "MPEG" + + def _open(self): + + s = BitStream(self.fp) + + if s.read(32) != 0x1B3: + raise SyntaxError, "not an MPEG file" + + self.mode = "RGB" + self.size = s.read(12), s.read(12) + + +# -------------------------------------------------------------------- +# Registry stuff + +Image.register_open("MPEG", MpegImageFile) + +Image.register_extension("MPEG", ".mpg") +Image.register_extension("MPEG", ".mpeg") + +Image.register_mime("MPEG", "video/mpeg") diff --git a/PIL/MspImagePlugin.py b/PIL/MspImagePlugin.py new file mode 100644 index 0000000..40aac19 --- /dev/null +++ b/PIL/MspImagePlugin.py @@ -0,0 +1,103 @@ +# +# The Python Imaging Library. +# $Id: MspImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# MSP file handling +# +# This is the format used by the Paint program in Windows 1 and 2. +# +# History: +# 95-09-05 fl Created +# 97-01-03 fl Read/write MSP images +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + +import Image, ImageFile + + +# +# read MSP files + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def _accept(prefix): + return prefix[:4] in ["DanM", "LinS"] + +## +# Image plugin for Windows MSP images. This plugin supports both +# uncompressed (Windows 1.0). + +class MspImageFile(ImageFile.ImageFile): + + format = "MSP" + format_description = "Windows Paint" + + def _open(self): + + # Header + s = self.fp.read(32) + if s[:4] not in ["DanM", "LinS"]: + raise SyntaxError, "not an MSP file" + + # Header checksum + sum = 0 + for i in range(0, 32, 2): + sum = sum ^ i16(s[i:i+2]) + if sum != 0: + raise SyntaxError, "bad MSP checksum" + + self.mode = "1" + self.size = i16(s[4:]), i16(s[6:]) + + if s[:4] == "DanM": + self.tile = [("raw", (0,0)+self.size, 32, ("1", 0, 1))] + else: + self.tile = [("msp", (0,0)+self.size, 32+2*self.size[1], None)] + +# +# write MSP files (uncompressed only) + +def o16(i): + return chr(i&255) + chr(i>>8&255) + +def _save(im, fp, filename): + + if im.mode != "1": + raise IOError, "cannot write mode %s as MSP" % im.mode + + # create MSP header + header = [0] * 16 + + header[0], header[1] = i16("Da"), i16("nM") # version 1 + header[2], header[3] = im.size + header[4], header[5] = 1, 1 + header[6], header[7] = 1, 1 + header[8], header[9] = im.size + + sum = 0 + for h in header: + sum = sum ^ h + header[12] = sum # FIXME: is this the right field? + + # header + for h in header: + fp.write(o16(h)) + + # image body + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 32, ("1", 0, 1))]) + +# +# registry + +Image.register_open("MSP", MspImageFile, _accept) +Image.register_save("MSP", _save) + +Image.register_extension("MSP", ".msp") diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py new file mode 100644 index 0000000..9383eca --- /dev/null +++ b/PIL/OleFileIO.py @@ -0,0 +1,528 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library +# $Id: OleFileIO.py 2339 2005-03-25 08:02:17Z fredrik $ +# +# stuff to deal with OLE2 Structured Storage files. this module is +# used by PIL to read Image Composer and FlashPix files, but can also +# be used to read other files of this type. +# +# History: +# 1997-01-20 fl Created +# 1997-01-22 fl Fixed 64-bit portability quirk +# 2003-09-09 fl Fixed typo in OleFileIO.loadfat (noted by Daniel Haertle) +# 2004-02-29 fl Changed long hex constants to signed integers +# +# Notes: +# FIXME: sort out sign problem (eliminate long hex constants) +# FIXME: change filename to use "a/b/c" instead of ["a", "b", "c"] +# FIXME: provide a glob mechanism function (using fnmatchcase) +# +# Literature: +# +# "FlashPix Format Specification, Appendix A", Kodak and Microsoft, +# September 1996. +# +# Quotes: +# +# "If this document and functionality of the Software conflict, +# the actual functionality of the Software represents the correct +# functionality" -- Microsoft, in the OLE format specification +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +import string, StringIO + + +def i16(c, o = 0): + return ord(c[o])+(ord(c[o+1])<<8) + +def i32(c, o = 0): + return ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24) + + +MAGIC = '\320\317\021\340\241\261\032\341' + +# +# -------------------------------------------------------------------- +# property types + +VT_EMPTY=0; VT_NULL=1; VT_I2=2; VT_I4=3; VT_R4=4; VT_R8=5; VT_CY=6; +VT_DATE=7; VT_BSTR=8; VT_DISPATCH=9; VT_ERROR=10; VT_BOOL=11; +VT_VARIANT=12; VT_UNKNOWN=13; VT_DECIMAL=14; VT_I1=16; VT_UI1=17; +VT_UI2=18; VT_UI4=19; VT_I8=20; VT_UI8=21; VT_INT=22; VT_UINT=23; +VT_VOID=24; VT_HRESULT=25; VT_PTR=26; VT_SAFEARRAY=27; VT_CARRAY=28; +VT_USERDEFINED=29; VT_LPSTR=30; VT_LPWSTR=31; VT_FILETIME=64; +VT_BLOB=65; VT_STREAM=66; VT_STORAGE=67; VT_STREAMED_OBJECT=68; +VT_STORED_OBJECT=69; VT_BLOB_OBJECT=70; VT_CF=71; VT_CLSID=72; +VT_VECTOR=0x1000; + +# map property id to name (for debugging purposes) + +VT = {} +for k, v in vars().items(): + if k[:3] == "VT_": + VT[v] = k + +# +# -------------------------------------------------------------------- +# Some common document types (root.clsid fields) + +WORD_CLSID = "00020900-0000-0000-C000-000000000046" + + +# +# -------------------------------------------------------------------- + +class _OleStream(StringIO.StringIO): + + """OLE2 Stream + + Returns a read-only file object which can be used to read + the contents of a OLE stream. To open a stream, use the + openstream method in the OleFile class. + + This function can be used with either ordinary streams, + or ministreams, depending on the offset, sectorsize, and + fat table arguments. + """ + + # FIXME: should store the list of sects obtained by following + # the fat chain, and load new sectors on demand instead of + # loading it all in one go. + + def __init__(self, fp, sect, size, offset, sectorsize, fat): + + data = [] + + while sect != -2: # 0xFFFFFFFEL: + fp.seek(offset + sectorsize * sect) + data.append(fp.read(sectorsize)) + sect = fat[sect] + + data = string.join(data, "") + + # print len(data), size + + StringIO.StringIO.__init__(self, data[:size]) + +# +# -------------------------------------------------------------------- + +# FIXME: should add a counter in here to avoid looping forever +# if the tree is broken. + +class _OleDirectoryEntry: + + """OLE2 Directory Entry + + Encapsulates a stream directory entry. Note that the + constructor builds a tree of all subentries, so we only + have to call it with the root object. + """ + + def __init__(self, sidlist, sid): + + # store directory parameters. the caller provides + # a complete list of directory entries, as read from + # the directory stream. + + name, type, sect, size, sids, clsid = sidlist[sid] + + self.sid = sid + self.name = name + self.type = type # 1=storage 2=stream + self.sect = sect + self.size = size + self.clsid = clsid + + # process child nodes, if any + + self.kids = [] + + sid = sidlist[sid][4][2] + + if sid != -1: + + # the directory entries are organized as a red-black tree. + # the following piece of code does an ordered traversal of + # such a tree (at least that's what I hope ;-) + + stack = [self.sid] + + # start at leftmost position + + left, right, child = sidlist[sid][4] + + while left != -1: # 0xFFFFFFFFL: + stack.append(sid) + sid = left + left, right, child = sidlist[sid][4] + + while sid != self.sid: + + self.kids.append(_OleDirectoryEntry(sidlist, sid)) + + # try to move right + left, right, child = sidlist[sid][4] + if right != -1: # 0xFFFFFFFFL: + # and then back to the left + sid = right + while 1: + left, right, child = sidlist[sid][4] + if left == -1: # 0xFFFFFFFFL: + break + stack.append(sid) + sid = left + else: + # couldn't move right; move up instead + while 1: + ptr = stack[-1] + del stack[-1] + left, right, child = sidlist[ptr][4] + if right != sid: + break + sid = right + left, right, child = sidlist[sid][4] + if right != ptr: + sid = ptr + + # in the OLE file, entries are sorted on (length, name). + # for convenience, we sort them on name instead. + + self.kids.sort() + + def __cmp__(self, other): + "Compare entries by name" + + return cmp(self.name, other.name) + + def dump(self, tab = 0): + "Dump this entry, and all its subentries (for debug purposes only)" + + TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)", + "(property)", "(root)"] + + print " "*tab + repr(self.name), TYPES[self.type], + if self.type in (2, 5): + print self.size, "bytes", + print + if self.type in (1, 5) and self.clsid: + print " "*tab + "{%s}" % self.clsid + + for kid in self.kids: + kid.dump(tab + 2) + +# +# -------------------------------------------------------------------- + +## +# This class encapsulates the interface to an OLE 2 structured +# storage file. Use the {@link listdir} and {@link openstream} +# methods to access the contents of this file. + +class OleFileIO: + """OLE container object + + This class encapsulates the interface to an OLE 2 structured + storage file. Use the listdir and openstream methods to access + the contents of this file. + + Object names are given as a list of strings, one for each subentry + level. The root entry should be omitted. For example, the following + code extracts all image streams from a Microsoft Image Composer file: + + ole = OleFileIO("fan.mic") + + for entry in ole.listdir(): + if entry[1:2] == "Image": + fin = ole.openstream(entry) + fout = open(entry[0:1], "wb") + while 1: + s = fin.read(8192) + if not s: + break + fout.write(s) + + You can use the viewer application provided with the Python Imaging + Library to view the resulting files (which happens to be standard + TIFF files). + """ + + def __init__(self, filename = None): + + if filename: + self.open(filename) + + ## + # Open an OLE2 file. + + def open(self, filename): + """Open an OLE2 file""" + + if type(filename) == type(""): + self.fp = open(filename, "rb") + else: + self.fp = filename + + header = self.fp.read(512) + + if len(header) != 512 or header[:8] != MAGIC: + raise IOError, "not an OLE2 structured storage file" + + # file clsid (probably never used, so we don't store it) + clsid = self._clsid(header[8:24]) + + # FIXME: could check version and byte order fields + + self.sectorsize = 1 << i16(header, 30) + self.minisectorsize = 1 << i16(header, 32) + + self.minisectorcutoff = i32(header, 56) + + # Load file allocation tables + self.loadfat(header) + + # Load direcory. This sets both the sidlist (ordered by id) + # and the root (ordered by hierarchy) members. + self.loaddirectory(i32(header, 48)) + + self.ministream = None + self.minifatsect = i32(header, 60) + + def loadfat(self, header): + # Load the FAT table. The header contains a sector numbers + # for the first 109 FAT sectors. Additional sectors are + # described by DIF blocks (FIXME: not yet implemented) + + sect = header[76:512] + fat = [] + for i in range(0, len(sect), 4): + ix = i32(sect, i) + if ix == -2 or ix == -1: # ix == 0xFFFFFFFEL or ix == 0xFFFFFFFFL: + break + s = self.getsect(ix) + fat = fat + map(lambda i, s=s: i32(s, i), range(0, len(s), 4)) + self.fat = fat + + def loadminifat(self): + # Load the MINIFAT table. This is stored in a standard sub- + # stream, pointed to by a header field. + + s = self._open(self.minifatsect).read() + + self.minifat = map(lambda i, s=s: i32(s, i), range(0, len(s), 4)) + + def getsect(self, sect): + # Read given sector + + self.fp.seek(512 + self.sectorsize * sect) + return self.fp.read(self.sectorsize) + + def _unicode(self, s): + # Map unicode string to Latin 1 + + # FIXME: some day, Python will provide an official way to handle + # Unicode strings, but until then, this will have to do... + return filter(ord, s) + + def loaddirectory(self, sect): + # Load the directory. The directory is stored in a standard + # substream, independent of its size. + + # read directory stream + fp = self._open(sect) + + # create list of sid entries + self.sidlist = [] + while 1: + entry = fp.read(128) + if not entry: + break + type = ord(entry[66]) + name = self._unicode(entry[0:0+i16(entry, 64)]) + ptrs = i32(entry, 68), i32(entry, 72), i32(entry, 76) + sect, size = i32(entry, 116), i32(entry, 120) + clsid = self._clsid(entry[80:96]) + self.sidlist.append((name, type, sect, size, ptrs, clsid)) + + # create hierarchical list of directory entries + self.root = _OleDirectoryEntry(self.sidlist, 0) + + def dumpdirectory(self): + # Dump directory (for debugging only) + + self.root.dump() + + def _clsid(self, clsid): + if clsid == "\0" * len(clsid): + return "" + return (("%08X-%04X-%04X-%02X%02X-" + "%02X" * 6) % + ((i32(clsid, 0), i16(clsid, 4), i16(clsid, 6)) + + tuple(map(ord, clsid[8:16])))) + + def _list(self, files, prefix, node): + # listdir helper + + prefix = prefix + [node.name] + for entry in node.kids: + if entry.kids: + self._list(files, prefix, entry) + else: + files.append(prefix[1:] + [entry.name]) + + def _find(self, filename): + # openstream helper + + node = self.root + for name in filename: + for kid in node.kids: + if kid.name == name: + break + else: + raise IOError, "file not found" + node = kid + return node.sid + + def _open(self, start, size = 0x7FFFFFFF): + # openstream helper. + + if size < self.minisectorcutoff: + # ministream object + if not self.ministream: + self.loadminifat() + self.ministream = self._open(self.sidlist[0][2]) + return _OleStream(self.ministream, start, size, 0, + self.minisectorsize, self.minifat) + + # standard stream + return _OleStream(self.fp, start, size, 512, + self.sectorsize, self.fat) + + ## + # Returns a list of streams stored in this file. + + def listdir(self): + """Return a list of streams stored in this file""" + + files = [] + self._list(files, [], self.root) + return files + + ## + # Opens a stream as a read-only file object. + + def openstream(self, filename): + """Open a stream as a read-only file object""" + + slot = self._find(filename) + name, type, sect, size, sids, clsid = self.sidlist[slot] + if type != 2: + raise IOError, "this file is not a stream" + return self._open(sect, size) + + ## + # Gets a list of properties described in substream. + + def getproperties(self, filename): + """Return properties described in substream""" + + fp = self.openstream(filename) + + data = {} + + # header + s = fp.read(28) + clsid = self._clsid(s[8:24]) + + # format id + s = fp.read(20) + fmtid = self._clsid(s[:16]) + fp.seek(i32(s, 16)) + + # get section + s = "****" + fp.read(i32(fp.read(4))-4) + + for i in range(i32(s, 4)): + + id = i32(s, 8+i*8) + offset = i32(s, 12+i*8) + type = i32(s, offset) + + # test for common types first (should perhaps use + # a dictionary instead?) + + if type == VT_I2: + value = i16(s, offset+4) + if value >= 32768: + value = value - 65536 + elif type == VT_UI2: + value = i16(s, offset+4) + elif type in (VT_I4, VT_ERROR): + value = i32(s, offset+4) + elif type == VT_UI4: + value = i32(s, offset+4) # FIXME + elif type in (VT_BSTR, VT_LPSTR): + count = i32(s, offset+4) + value = s[offset+8:offset+8+count-1] + elif type == VT_BLOB: + count = i32(s, offset+4) + value = s[offset+8:offset+8+count] + elif type == VT_LPWSTR: + count = i32(s, offset+4) + value = self._unicode(s[offset+8:offset+8+count*2]) + elif type == VT_FILETIME: + value = long(i32(s, offset+4)) + (long(i32(s, offset+8))<<32) + # FIXME: this is a 64-bit int: "number of 100ns periods + # since Jan 1,1601". Should map this to Python time + value = value / 10000000L # seconds + elif type == VT_UI1: + value = ord(s[offset+4]) + elif type == VT_CLSID: + value = self._clsid(s[offset+4:offset+20]) + elif type == VT_CF: + count = i32(s, offset+4) + value = s[offset+8:offset+8+count] + else: + value = None # everything else yields "None" + + # FIXME: add support for VT_VECTOR + + #print "%08x" % id, repr(value), + #print "(%s)" % VT[i32(s, offset) & 0xFFF] + + data[id] = value + + return data + +# +# -------------------------------------------------------------------- +# This script can be used to dump the directory of any OLE2 structured +# storage file. + +if __name__ == "__main__": + + import sys + + for file in sys.argv[1:]: + try: + ole = OleFileIO(file) + print "-" * 68 + print file + print "-" * 68 + ole.dumpdirectory() + for file in ole.listdir(): + if file[-1][0] == "\005": + print file + props = ole.getproperties(file) + props = props.items() + props.sort() + for k, v in props: + print " ", k, v + except IOError, v: + print "***", "cannot read", file, "-", v diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py new file mode 100644 index 0000000..7e88f55 --- /dev/null +++ b/PIL/PSDraw.py @@ -0,0 +1,199 @@ +# +# The Python Imaging Library +# $Id: PSDraw.py 2813 2006-10-07 10:11:35Z fredrik $ +# +# simple postscript graphics interface +# +# History: +# 1996-04-20 fl Created +# 1999-01-10 fl Added gsave/grestore to image method +# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge) +# +# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +import EpsImagePlugin +import string + +## +# Simple Postscript graphics interface. + +class PSDraw: + + def __init__(self, fp=None): + if not fp: + import sys + fp = sys.stdout + self.fp = fp + + def begin_document(self, id = None): + "Write Postscript DSC header" + # FIXME: incomplete + self.fp.write("%!PS-Adobe-3.0\n" + "save\n" + "/showpage { } def\n" + "%%EndComments\n" + "%%BeginDocument\n") + #self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write("%%EndProlog\n") + self.isofont = {} + + def end_document(self): + "Write Postscript DSC footer" + self.fp.write("%%EndDocument\n" + "restore showpage\n" + "%%End\n") + if hasattr(self.fp, "flush"): + self.fp.flush() + + def setfont(self, font, size): + if not self.isofont.has_key(font): + # reencode font + self.fp.write("/PSDraw-%s ISOLatin1Encoding /%s E\n" %\ + (font, font)) + self.isofont[font] = 1 + # rough + self.fp.write("/F0 %d /PSDraw-%s F\n" % (size, font)) + + def setink(self, ink): + print "*** NOT YET IMPLEMENTED ***" + + def line(self, xy0, xy1): + xy = xy0 + xy1 + self.fp.write("%d %d %d %d Vl\n" % xy) + + def rectangle(self, box): + self.fp.write("%d %d M %d %d 0 Vr\n" % box) + + def text(self, xy, text): + text = string.joinfields(string.splitfields(text, "("), "\\(") + text = string.joinfields(string.splitfields(text, ")"), "\\)") + xy = xy + (text,) + self.fp.write("%d %d M (%s) S\n" % xy) + + def image(self, box, im, dpi = None): + "Write an PIL image" + # default resolution depends on mode + if not dpi: + if im.mode == "1": + dpi = 200 # fax + else: + dpi = 100 # greyscale + # image size (on paper) + x = float(im.size[0] * 72) / dpi + y = float(im.size[1] * 72) / dpi + # max allowed size + xmax = float(box[2] - box[0]) + ymax = float(box[3] - box[1]) + if x > xmax: + y = y * xmax / x; x = xmax + if y > ymax: + x = x * ymax / y; y = ymax + dx = (xmax - x) / 2 + box[0] + dy = (ymax - y) / 2 + box[1] + self.fp.write("gsave\n%f %f translate\n" % (dx, dy)) + if (x, y) != im.size: + # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) + sx = x / im.size[0] + sy = y / im.size[1] + self.fp.write("%f %f scale\n" % (sx, sy)) + EpsImagePlugin._save(im, self.fp, None, 0) + self.fp.write("\ngrestore\n") + +# -------------------------------------------------------------------- +# Postscript driver + +# +# EDROFF.PS -- Postscript driver for Edroff 2 +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + +EDROFF_PS = """\ +/S { show } bind def +/P { moveto show } bind def +/M { moveto } bind def +/X { 0 rmoveto } bind def +/Y { 0 exch rmoveto } bind def +/E { findfont + dup maxlength dict begin + { + 1 index /FID ne { def } { pop pop } ifelse + } forall + /Encoding exch def + dup /FontName exch def + currentdict end definefont pop +} bind def +/F { findfont exch scalefont dup setfont + [ exch /setfont cvx ] cvx bind def +} bind def +""" + +# +# VDI.PS -- Postscript driver for VDI meta commands +# +# History: +# 94-01-25 fl: created (edroff 2.04) +# +# Copyright (c) Fredrik Lundh 1994. +# + +VDI_PS = """\ +/Vm { moveto } bind def +/Va { newpath arcn stroke } bind def +/Vl { moveto lineto stroke } bind def +/Vc { newpath 0 360 arc closepath } bind def +/Vr { exch dup 0 rlineto + exch dup neg 0 exch rlineto + exch neg 0 rlineto + 0 exch rlineto + 100 div setgray fill 0 setgray } bind def +/Tm matrix def +/Ve { Tm currentmatrix pop + translate scale newpath 0 0 .5 0 360 arc closepath + Tm setmatrix +} bind def +/Vf { currentgray exch setgray fill setgray } bind def +""" + +# +# ERROR.PS -- Error handler +# +# History: +# 89-11-21 fl: created (pslist 1.10) +# + +ERROR_PS = """\ +/landscape false def +/errorBUF 200 string def +/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def +errordict begin /handleerror { + initmatrix /Courier findfont 10 scalefont setfont + newpath 72 720 moveto $error begin /newerror false def + (PostScript Error) show errorNL errorNL + (Error: ) show + /errorname load errorBUF cvs show errorNL errorNL + (Command: ) show + /command load dup type /stringtype ne { errorBUF cvs } if show + errorNL errorNL + (VMstatus: ) show + vmstatus errorBUF cvs show ( bytes available, ) show + errorBUF cvs show ( bytes used at level ) show + errorBUF cvs show errorNL errorNL + (Operand stargck: ) show errorNL /ostargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall errorNL + (Execution stargck: ) show errorNL /estargck load { + dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL + } forall + end showpage +} def end +""" diff --git a/PIL/PaletteFile.py b/PIL/PaletteFile.py new file mode 100644 index 0000000..bbbebfa --- /dev/null +++ b/PIL/PaletteFile.py @@ -0,0 +1,53 @@ +# +# Python Imaging Library +# $Id: PaletteFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# stuff to read simple, teragon-style palette files +# +# History: +# 97-08-23 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +import string + +## +# File handler for Teragon-style palette files. + +class PaletteFile: + + rawmode = "RGB" + + def __init__(self, fp): + + self.palette = map(lambda i: (i, i, i), range(256)) + + while 1: + + s = fp.readline() + + if not s: + break + if len(s) > 100: + raise SyntaxError, "bad palette file" + + v = map(int, string.split(s)) + try: + [i, r, g, b] = v + except ValueError: + [i, r] = v + g = b = r + + if 0 <= i <= 255: + self.palette[i] = chr(r) + chr(g) + chr(b) + + self.palette = string.join(self.palette, "") + + + def getpalette(self): + + return self.palette, self.rawmode diff --git a/PIL/PalmImagePlugin.py b/PIL/PalmImagePlugin.py new file mode 100644 index 0000000..19f0c2f --- /dev/null +++ b/PIL/PalmImagePlugin.py @@ -0,0 +1,226 @@ +# +# The Python Imaging Library. +# $Id: PalmImagePlugin.py 2438 2005-05-25 21:09:48Z Fredrik $ +# + +## +# Image plugin for Palm pixmap images (output only). +## + +__version__ = "1.0" + +import Image, ImageFile +import StringIO + +_Palm8BitColormapValues = ( + ( 255, 255, 255 ), ( 255, 204, 255 ), ( 255, 153, 255 ), ( 255, 102, 255 ), + ( 255, 51, 255 ), ( 255, 0, 255 ), ( 255, 255, 204 ), ( 255, 204, 204 ), + ( 255, 153, 204 ), ( 255, 102, 204 ), ( 255, 51, 204 ), ( 255, 0, 204 ), + ( 255, 255, 153 ), ( 255, 204, 153 ), ( 255, 153, 153 ), ( 255, 102, 153 ), + ( 255, 51, 153 ), ( 255, 0, 153 ), ( 204, 255, 255 ), ( 204, 204, 255 ), + ( 204, 153, 255 ), ( 204, 102, 255 ), ( 204, 51, 255 ), ( 204, 0, 255 ), + ( 204, 255, 204 ), ( 204, 204, 204 ), ( 204, 153, 204 ), ( 204, 102, 204 ), + ( 204, 51, 204 ), ( 204, 0, 204 ), ( 204, 255, 153 ), ( 204, 204, 153 ), + ( 204, 153, 153 ), ( 204, 102, 153 ), ( 204, 51, 153 ), ( 204, 0, 153 ), + ( 153, 255, 255 ), ( 153, 204, 255 ), ( 153, 153, 255 ), ( 153, 102, 255 ), + ( 153, 51, 255 ), ( 153, 0, 255 ), ( 153, 255, 204 ), ( 153, 204, 204 ), + ( 153, 153, 204 ), ( 153, 102, 204 ), ( 153, 51, 204 ), ( 153, 0, 204 ), + ( 153, 255, 153 ), ( 153, 204, 153 ), ( 153, 153, 153 ), ( 153, 102, 153 ), + ( 153, 51, 153 ), ( 153, 0, 153 ), ( 102, 255, 255 ), ( 102, 204, 255 ), + ( 102, 153, 255 ), ( 102, 102, 255 ), ( 102, 51, 255 ), ( 102, 0, 255 ), + ( 102, 255, 204 ), ( 102, 204, 204 ), ( 102, 153, 204 ), ( 102, 102, 204 ), + ( 102, 51, 204 ), ( 102, 0, 204 ), ( 102, 255, 153 ), ( 102, 204, 153 ), + ( 102, 153, 153 ), ( 102, 102, 153 ), ( 102, 51, 153 ), ( 102, 0, 153 ), + ( 51, 255, 255 ), ( 51, 204, 255 ), ( 51, 153, 255 ), ( 51, 102, 255 ), + ( 51, 51, 255 ), ( 51, 0, 255 ), ( 51, 255, 204 ), ( 51, 204, 204 ), + ( 51, 153, 204 ), ( 51, 102, 204 ), ( 51, 51, 204 ), ( 51, 0, 204 ), + ( 51, 255, 153 ), ( 51, 204, 153 ), ( 51, 153, 153 ), ( 51, 102, 153 ), + ( 51, 51, 153 ), ( 51, 0, 153 ), ( 0, 255, 255 ), ( 0, 204, 255 ), + ( 0, 153, 255 ), ( 0, 102, 255 ), ( 0, 51, 255 ), ( 0, 0, 255 ), + ( 0, 255, 204 ), ( 0, 204, 204 ), ( 0, 153, 204 ), ( 0, 102, 204 ), + ( 0, 51, 204 ), ( 0, 0, 204 ), ( 0, 255, 153 ), ( 0, 204, 153 ), + ( 0, 153, 153 ), ( 0, 102, 153 ), ( 0, 51, 153 ), ( 0, 0, 153 ), + ( 255, 255, 102 ), ( 255, 204, 102 ), ( 255, 153, 102 ), ( 255, 102, 102 ), + ( 255, 51, 102 ), ( 255, 0, 102 ), ( 255, 255, 51 ), ( 255, 204, 51 ), + ( 255, 153, 51 ), ( 255, 102, 51 ), ( 255, 51, 51 ), ( 255, 0, 51 ), + ( 255, 255, 0 ), ( 255, 204, 0 ), ( 255, 153, 0 ), ( 255, 102, 0 ), + ( 255, 51, 0 ), ( 255, 0, 0 ), ( 204, 255, 102 ), ( 204, 204, 102 ), + ( 204, 153, 102 ), ( 204, 102, 102 ), ( 204, 51, 102 ), ( 204, 0, 102 ), + ( 204, 255, 51 ), ( 204, 204, 51 ), ( 204, 153, 51 ), ( 204, 102, 51 ), + ( 204, 51, 51 ), ( 204, 0, 51 ), ( 204, 255, 0 ), ( 204, 204, 0 ), + ( 204, 153, 0 ), ( 204, 102, 0 ), ( 204, 51, 0 ), ( 204, 0, 0 ), + ( 153, 255, 102 ), ( 153, 204, 102 ), ( 153, 153, 102 ), ( 153, 102, 102 ), + ( 153, 51, 102 ), ( 153, 0, 102 ), ( 153, 255, 51 ), ( 153, 204, 51 ), + ( 153, 153, 51 ), ( 153, 102, 51 ), ( 153, 51, 51 ), ( 153, 0, 51 ), + ( 153, 255, 0 ), ( 153, 204, 0 ), ( 153, 153, 0 ), ( 153, 102, 0 ), + ( 153, 51, 0 ), ( 153, 0, 0 ), ( 102, 255, 102 ), ( 102, 204, 102 ), + ( 102, 153, 102 ), ( 102, 102, 102 ), ( 102, 51, 102 ), ( 102, 0, 102 ), + ( 102, 255, 51 ), ( 102, 204, 51 ), ( 102, 153, 51 ), ( 102, 102, 51 ), + ( 102, 51, 51 ), ( 102, 0, 51 ), ( 102, 255, 0 ), ( 102, 204, 0 ), + ( 102, 153, 0 ), ( 102, 102, 0 ), ( 102, 51, 0 ), ( 102, 0, 0 ), + ( 51, 255, 102 ), ( 51, 204, 102 ), ( 51, 153, 102 ), ( 51, 102, 102 ), + ( 51, 51, 102 ), ( 51, 0, 102 ), ( 51, 255, 51 ), ( 51, 204, 51 ), + ( 51, 153, 51 ), ( 51, 102, 51 ), ( 51, 51, 51 ), ( 51, 0, 51 ), + ( 51, 255, 0 ), ( 51, 204, 0 ), ( 51, 153, 0 ), ( 51, 102, 0 ), + ( 51, 51, 0 ), ( 51, 0, 0 ), ( 0, 255, 102 ), ( 0, 204, 102 ), + ( 0, 153, 102 ), ( 0, 102, 102 ), ( 0, 51, 102 ), ( 0, 0, 102 ), + ( 0, 255, 51 ), ( 0, 204, 51 ), ( 0, 153, 51 ), ( 0, 102, 51 ), + ( 0, 51, 51 ), ( 0, 0, 51 ), ( 0, 255, 0 ), ( 0, 204, 0 ), + ( 0, 153, 0 ), ( 0, 102, 0 ), ( 0, 51, 0 ), ( 17, 17, 17 ), + ( 34, 34, 34 ), ( 68, 68, 68 ), ( 85, 85, 85 ), ( 119, 119, 119 ), + ( 136, 136, 136 ), ( 170, 170, 170 ), ( 187, 187, 187 ), ( 221, 221, 221 ), + ( 238, 238, 238 ), ( 192, 192, 192 ), ( 128, 0, 0 ), ( 128, 0, 128 ), + ( 0, 128, 0 ), ( 0, 128, 128 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), + ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 )) + +# so build a prototype image to be used for palette resampling +def build_prototype_image(): + image = Image.new("L", (1,len(_Palm8BitColormapValues),)) + image.putdata(range(len(_Palm8BitColormapValues))) + palettedata = () + for i in range(len(_Palm8BitColormapValues)): + palettedata = palettedata + _Palm8BitColormapValues[i] + for i in range(256 - len(_Palm8BitColormapValues)): + palettedata = palettedata + (0, 0, 0) + image.putpalette(palettedata) + return image + +Palm8BitColormapImage = build_prototype_image() + +# OK, we now have in Palm8BitColormapImage, a "P"-mode image with the right palette +# +# -------------------------------------------------------------------- + +_FLAGS = { + "custom-colormap": 0x4000, + "is-compressed": 0x8000, + "has-transparent": 0x2000, + } + +_COMPRESSION_TYPES = { + "none": 0xFF, + "rle": 0x01, + "scanline": 0x00, + } + +def o16b(i): + return chr(i>>8&255) + chr(i&255) + +# +# -------------------------------------------------------------------- + +## +# (Internal) Image save plugin for the Palm format. + +def _save(im, fp, filename, check=0): + + if im.mode == "P": + + # we assume this is a color Palm image with the standard colormap, + # unless the "info" dict has a "custom-colormap" field + + rawmode = "P" + bpp = 8 + version = 1 + + elif im.mode == "L" and im.encoderinfo.has_key("bpp") and im.encoderinfo["bpp"] in (1, 2, 4): + + # this is 8-bit grayscale, so we shift it to get the high-order bits, and invert it because + # Palm does greyscale from white (0) to black (1) + bpp = im.encoderinfo["bpp"] + im = im.point(lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift)) + # we ignore the palette here + im.mode = "P" + rawmode = "P;" + str(bpp) + version = 1 + + elif im.mode == "L" and im.info.has_key("bpp") and im.info["bpp"] in (1, 2, 4): + + # here we assume that even though the inherent mode is 8-bit grayscale, only + # the lower bpp bits are significant. We invert them to match the Palm. + bpp = im.info["bpp"] + im = im.point(lambda x, maxval=(1 << bpp)-1: maxval - (x & maxval)) + # we ignore the palette here + im.mode = "P" + rawmode = "P;" + str(bpp) + version = 1 + + elif im.mode == "1": + + # monochrome -- write it inverted, as is the Palm standard + rawmode = "1;I" + bpp = 1 + version = 0 + + else: + + raise IOError, "cannot write mode %s as Palm" % im.mode + + if check: + return check + + # + # make sure image data is available + im.load() + + # write header + + cols = im.size[0] + rows = im.size[1] + + rowbytes = ((cols + (16/bpp - 1)) / (16 / bpp)) * 2; + transparent_index = 0 + compression_type = _COMPRESSION_TYPES["none"] + + flags = 0; + if im.mode == "P" and im.info.has_key("custom-colormap"): + flags = flags & _FLAGS["custom-colormap"] + colormapsize = 4 * 256 + 2; + colormapmode = im.palette.mode + colormap = im.getdata().getpalette() + else: + colormapsize = 0 + + if im.info.has_key("offset"): + offset = (rowbytes * rows + 16 + 3 + colormapsize) / 4; + else: + offset = 0 + + fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags)) + fp.write(chr(bpp)) + fp.write(chr(version)) + fp.write(o16b(offset)) + fp.write(chr(transparent_index)) + fp.write(chr(compression_type)) + fp.write(o16b(0)) # reserved by Palm + + # now write colormap if necessary + + if colormapsize > 0: + fp.write(o16b(256)) + for i in range(256): + fp.write(chr(i)) + if colormapmode == 'RGB': + fp.write(chr(colormap[3 * i]) + chr(colormap[3 * i + 1]) + chr(colormap[3 * i + 2])) + elif colormapmode == 'RGBA': + fp.write(chr(colormap[4 * i]) + chr(colormap[4 * i + 1]) + chr(colormap[4 * i + 2])) + + # now convert data to raw form + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, rowbytes, 1))]) + + fp.flush() + + +# +# -------------------------------------------------------------------- + +Image.register_save("Palm", _save) + +Image.register_extension("Palm", ".palm") + +Image.register_mime("Palm", "image/palm") diff --git a/PIL/PcdImagePlugin.py b/PIL/PcdImagePlugin.py new file mode 100644 index 0000000..957da24 --- /dev/null +++ b/PIL/PcdImagePlugin.py @@ -0,0 +1,76 @@ +# +# The Python Imaging Library. +# $Id: PcdImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# PCD file handling +# +# History: +# 96-05-10 fl Created +# 96-05-27 fl Added draft mode (128x192, 256x384) +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + + +import Image, ImageFile + +## +# Image plugin for PhotoCD images. This plugin only reads the 768x512 +# image from the file; higher resolutions are encoded in a proprietary +# encoding. + +class PcdImageFile(ImageFile.ImageFile): + + format = "PCD" + format_description = "Kodak PhotoCD" + + def _open(self): + + # rough + self.fp.seek(2048) + s = self.fp.read(2048) + + if s[:4] != "PCD_": + raise SyntaxError, "not a PCD file" + + orientation = ord(s[1538]) & 3 + if orientation == 1: + self.tile_post_rotate = 90 # hack + elif orientation == 3: + self.tile_post_rotate = -90 + + self.mode = "RGB" + self.size = 768, 512 # FIXME: not correct for rotated images! + self.tile = [("pcd", (0,0)+self.size, 96*2048, None)] + + def draft(self, mode, size): + + if len(self.tile) != 1: + return + + d, e, o, a = self.tile[0] + + if size: + scale = max(self.size[0] / size[0], self.size[1] / size[1]) + for s, o in [(4,0*2048), (2,0*2048), (1,96*2048)]: + if scale >= s: + break + # e = e[0], e[1], (e[2]-e[0]+s-1)/s+e[0], (e[3]-e[1]+s-1)/s+e[1] + # self.size = ((self.size[0]+s-1)/s, (self.size[1]+s-1)/s) + + self.tile = [(d, e, o, a)] + + return self + +# +# registry + +Image.register_open("PCD", PcdImageFile) + +Image.register_extension("PCD", ".pcd") diff --git a/PIL/PcfFontFile.py b/PIL/PcfFontFile.py new file mode 100644 index 0000000..9a689b2 --- /dev/null +++ b/PIL/PcfFontFile.py @@ -0,0 +1,256 @@ +# +# THIS IS WORK IN PROGRESS +# +# The Python Imaging Library +# $Id: PcfFontFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# portable compiled font file parser +# +# history: +# 1997-08-19 fl created +# 2003-09-13 fl fixed loading of unicode fonts +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1997-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +import Image +import FontFile + +import string + +# -------------------------------------------------------------------- +# declarations + +PCF_MAGIC = 0x70636601 # "\x01fcp" + +PCF_PROPERTIES = (1<<0) +PCF_ACCELERATORS = (1<<1) +PCF_METRICS = (1<<2) +PCF_BITMAPS = (1<<3) +PCF_INK_METRICS = (1<<4) +PCF_BDF_ENCODINGS = (1<<5) +PCF_SWIDTHS = (1<<6) +PCF_GLYPH_NAMES = (1<<7) +PCF_BDF_ACCELERATORS = (1<<8) + +BYTES_PER_ROW = [ + lambda bits: ((bits+7) >> 3), + lambda bits: ((bits+15) >> 3) & ~1, + lambda bits: ((bits+31) >> 3) & ~3, + lambda bits: ((bits+63) >> 3) & ~7, +] + + +def l16(c): + return ord(c[0]) + (ord(c[1])<<8) +def l32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + +def b16(c): + return ord(c[1]) + (ord(c[0])<<8) +def b32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + +def sz(s, o): + return s[o:string.index(s, "\0", o)] + +## +# Font file plugin for the X11 PCF format. + +class PcfFontFile(FontFile.FontFile): + + name = "name" + + def __init__(self, fp): + + magic = l32(fp.read(4)) + if magic != PCF_MAGIC: + raise SyntaxError, "not a PCF file" + + FontFile.FontFile.__init__(self) + + count = l32(fp.read(4)) + self.toc = {} + for i in range(count): + type = l32(fp.read(4)) + self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4)) + + self.fp = fp + + self.info = self._load_properties() + + metrics = self._load_metrics() + bitmaps = self._load_bitmaps(metrics) + encoding = self._load_encoding() + + # + # create glyph structure + + for ch in range(256): + ix = encoding[ch] + if ix is not None: + x, y, l, r, w, a, d, f = metrics[ix] + glyph = (w, 0), (l, d-y, x+l, d), (0, 0, x, y), bitmaps[ix] + self.glyph[ch] = glyph + + def _getformat(self, tag): + + format, size, offset = self.toc[tag] + + fp = self.fp + fp.seek(offset) + + format = l32(fp.read(4)) + + if format & 4: + i16, i32 = b16, b32 + else: + i16, i32 = l16, l32 + + return fp, format, i16, i32 + + def _load_properties(self): + + # + # font properties + + properties = {} + + fp, format, i16, i32 = self._getformat(PCF_PROPERTIES) + + nprops = i32(fp.read(4)) + + # read property description + p = [] + for i in range(nprops): + p.append((i32(fp.read(4)), ord(fp.read(1)), i32(fp.read(4)))) + if nprops & 3: + fp.seek(4 - (nprops & 3), 1) # pad + + data = fp.read(i32(fp.read(4))) + + for k, s, v in p: + k = sz(data, k) + if s: + v = sz(data, v) + properties[k] = v + + return properties + + def _load_metrics(self): + + # + # font metrics + + metrics = [] + + fp, format, i16, i32 = self._getformat(PCF_METRICS) + + append = metrics.append + + if (format & 0xff00) == 0x100: + + # "compressed" metrics + for i in range(i16(fp.read(2))): + left = ord(fp.read(1)) - 128 + right = ord(fp.read(1)) - 128 + width = ord(fp.read(1)) - 128 + ascent = ord(fp.read(1)) - 128 + descent = ord(fp.read(1)) - 128 + xsize = right - left + ysize = ascent + descent + append( + (xsize, ysize, left, right, width, + ascent, descent, 0) + ) + + else: + + # "jumbo" metrics + for i in range(i32(fp.read(4))): + left = i16(fp.read(2)) + right = i16(fp.read(2)) + width = i16(fp.read(2)) + ascent = i16(fp.read(2)) + descent = i16(fp.read(2)) + attributes = i16(fp.read(2)) + xsize = right - left + ysize = ascent + descent + append( + (xsize, ysize, left, right, width, + ascent, descent, attributes) + ) + + return metrics + + def _load_bitmaps(self, metrics): + + # + # bitmap data + + bitmaps = [] + + fp, format, i16, i32 = self._getformat(PCF_BITMAPS) + + nbitmaps = i32(fp.read(4)) + + if nbitmaps != len(metrics): + raise IOError, "Wrong number of bitmaps" + + offsets = [] + for i in range(nbitmaps): + offsets.append(i32(fp.read(4))) + + bitmapSizes = [] + for i in range(4): + bitmapSizes.append(i32(fp.read(4))) + + byteorder = format & 4 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB + padindex = format & 3 + + bitmapsize = bitmapSizes[padindex] + offsets.append(bitmapsize) + + data = fp.read(bitmapsize) + + pad = BYTES_PER_ROW[padindex] + mode = "1;R" + if bitorder: + mode = "1" + + for i in range(nbitmaps): + x, y, l, r, w, a, d, f = metrics[i] + b, e = offsets[i], offsets[i+1] + bitmaps.append( + Image.fromstring("1", (x, y), data[b:e], "raw", mode, pad(x)) + ) + + return bitmaps + + def _load_encoding(self): + + # map character code to bitmap index + encoding = [None] * 256 + + fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) + + firstCol, lastCol = i16(fp.read(2)), i16(fp.read(2)) + firstRow, lastRow = i16(fp.read(2)), i16(fp.read(2)) + + default = i16(fp.read(2)) + + nencoding = (lastCol - firstCol + 1) * (lastRow - firstRow + 1) + + for i in range(nencoding): + encodingOffset = i16(fp.read(2)) + if encodingOffset != 0xFFFF: + try: + encoding[i+firstCol] = encodingOffset + except IndexError: + break # only load ISO-8859-1 glyphs + + return encoding diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py new file mode 100644 index 0000000..8c0cd11 --- /dev/null +++ b/PIL/PcxImagePlugin.py @@ -0,0 +1,167 @@ +# +# The Python Imaging Library. +# $Id: PcxImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# PCX file handling +# +# This format was originally used by ZSoft's popular PaintBrush +# program for the IBM PC. It is also supported by many MS-DOS and +# Windows applications, including the Windows PaintBrush program in +# Windows 3. +# +# history: +# 1995-09-01 fl Created +# 1996-05-20 fl Fixed RGB support +# 1997-01-03 fl Fixed 2-bit and 4-bit support +# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1) +# 1999-02-07 fl Added write support +# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust +# 2002-07-30 fl Seek from to current position, not beginning of file +# 2003-06-03 fl Extract DPI settings (info["dpi"]) +# +# Copyright (c) 1997-2003 by Secret Labs AB. +# Copyright (c) 1995-2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.6" + +import Image, ImageFile, ImagePalette + +def i16(c,o): + return ord(c[o]) + (ord(c[o+1])<<8) + +def _accept(prefix): + return ord(prefix[0]) == 10 and ord(prefix[1]) in [0, 2, 3, 5] + +## +# Image plugin for Paintbrush images. + +class PcxImageFile(ImageFile.ImageFile): + + format = "PCX" + format_description = "Paintbrush" + + def _open(self): + + # header + s = self.fp.read(128) + if not _accept(s): + raise SyntaxError, "not a PCX file" + + # image + bbox = i16(s,4), i16(s,6), i16(s,8)+1, i16(s,10)+1 + if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: + raise SyntaxError, "bad PCX image size" + + # format + version = ord(s[1]) + bits = ord(s[3]) + planes = ord(s[65]) + stride = i16(s,66) + + self.info["dpi"] = i16(s,12), i16(s,14) + + if bits == 1 and planes == 1: + mode = rawmode = "1" + + elif bits == 1 and planes in (2, 4): + mode = "P" + rawmode = "P;%dL" % planes + self.palette = ImagePalette.raw("RGB", s[16:64]) + + elif version == 5 and bits == 8 and planes == 1: + mode = rawmode = "L" + # FIXME: hey, this doesn't work with the incremental loader !!! + self.fp.seek(-769, 2) + s = self.fp.read(769) + if len(s) == 769 and ord(s[0]) == 12: + # check if the palette is linear greyscale + for i in range(256): + if s[i*3+1:i*3+4] != chr(i)*3: + mode = rawmode = "P" + break + if mode == "P": + self.palette = ImagePalette.raw("RGB", s[1:]) + self.fp.seek(128) + + elif version == 5 and bits == 8 and planes == 3: + mode = "RGB" + rawmode = "RGB;L" + + else: + raise IOError, "unknown PCX mode" + + self.mode = mode + self.size = bbox[2]-bbox[0], bbox[3]-bbox[1] + + bbox = (0, 0) + self.size + + self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] + +# -------------------------------------------------------------------- +# save PCX files + +SAVE = { + # mode: (version, bits, planes, raw mode) + "1": (2, 1, 1, "1"), + "L": (5, 8, 1, "L"), + "P": (5, 8, 1, "P"), + "RGB": (5, 8, 3, "RGB;L"), +} + +def o16(i): + return chr(i&255) + chr(i>>8&255) + +def _save(im, fp, filename, check=0): + + try: + version, bits, planes, rawmode = SAVE[im.mode] + except KeyError: + raise ValueError, "Cannot save %s images as PCX" % im.mode + + if check: + return check + + # bytes per plane + stride = (im.size[0] * bits + 7) / 8 + + # under windows, we could determine the current screen size with + # "Image.core.display_mode()[1]", but I think that's overkill... + + screen = im.size + + dpi = 100, 100 + + # PCX header + fp.write( + chr(10) + chr(version) + chr(1) + chr(bits) + o16(0) + + o16(0) + o16(im.size[0]-1) + o16(im.size[1]-1) + o16(dpi[0]) + + o16(dpi[1]) + chr(0)*24 + chr(255)*24 + chr(0) + chr(planes) + + o16(stride) + o16(1) + o16(screen[0]) + o16(screen[1]) + + chr(0)*54 + ) + + assert fp.tell() == 128 + + ImageFile._save(im, fp, [("pcx", (0,0)+im.size, 0, + (rawmode, bits*planes))]) + + if im.mode == "P": + # colour palette + fp.write(chr(12)) + fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes + elif im.mode == "L": + # greyscale palette + fp.write(chr(12)) + for i in range(256): + fp.write(chr(i)*3) + +# -------------------------------------------------------------------- +# registry + +Image.register_open("PCX", PcxImageFile, _accept) +Image.register_save("PCX", _save) + +Image.register_extension("PCX", ".pcx") diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py new file mode 100644 index 0000000..86567d6 --- /dev/null +++ b/PIL/PdfImagePlugin.py @@ -0,0 +1,210 @@ +# +# The Python Imaging Library. +# $Id: PdfImagePlugin.py 2438 2005-05-25 21:09:48Z Fredrik $ +# +# PDF (Acrobat) file handling +# +# History: +# 1996-07-16 fl Created +# 1997-01-18 fl Fixed header +# 2004-02-21 fl Fixes for 1/L/CMYK images, etc. +# 2004-02-24 fl Fixes for 1 and P images. +# +# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved. +# Copyright (c) 1996-1997 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +## +# Image plugin for PDF images (output only). +## + +__version__ = "0.4" + +import Image, ImageFile +import StringIO + + +# +# -------------------------------------------------------------------- + +# object ids: +# 1. catalogue +# 2. pages +# 3. image +# 4. page +# 5. page contents + +def _obj(fp, obj, **dict): + fp.write("%d 0 obj\n" % obj) + if dict: + fp.write("<<\n") + for k, v in dict.items(): + if v is not None: + fp.write("/%s %s\n" % (k, v)) + fp.write(">>\n") + +def _endobj(fp): + fp.write("endobj\n") + +## +# (Internal) Image save plugin for the PDF format. + +def _save(im, fp, filename): + + # + # make sure image data is available + im.load() + + xref = [0]*(5+1) # placeholders + + fp.write("%PDF-1.2\n") + fp.write("% created by PIL PDF driver " + __version__ + "\n") + + # + # Get image characteristics + + width, height = im.size + + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) + # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports + # Flatedecode (zip compression). + + bits = 8 + params = None + + if im.mode == "1": + filter = "/ASCIIHexDecode" + colorspace = "/DeviceGray" + procset = "/ImageB" # grayscale + bits = 1 + elif im.mode == "L": + filter = "/DCTDecode" + # params = "<< /Predictor 15 /Columns %d >>" % (width-2) + colorspace = "/DeviceGray" + procset = "/ImageB" # grayscale + elif im.mode == "P": + filter = "/ASCIIHexDecode" + colorspace = "[ /Indexed /DeviceRGB 255 <" + palette = im.im.getpalette("RGB") + for i in range(256): + r = ord(palette[i*3]) + g = ord(palette[i*3+1]) + b = ord(palette[i*3+2]) + colorspace = colorspace + "%02x%02x%02x " % (r, g, b) + colorspace = colorspace + "> ]" + procset = "/ImageI" # indexed color + elif im.mode == "RGB": + filter = "/DCTDecode" + colorspace = "/DeviceRGB" + procset = "/ImageC" # color images + elif im.mode == "CMYK": + filter = "/DCTDecode" + colorspace = "/DeviceCMYK" + procset = "/ImageC" # color images + else: + raise ValueError("cannot save mode %s" % im.mode) + + # + # catalogue + + xref[1] = fp.tell() + _obj(fp, 1, Type = "/Catalog", + Pages = "2 0 R") + _endobj(fp) + + # + # pages + + xref[2] = fp.tell() + _obj(fp, 2, Type = "/Pages", + Count = 1, + Kids = "[4 0 R]") + _endobj(fp) + + # + # image + + op = StringIO.StringIO() + + if filter == "/ASCIIHexDecode": + if bits == 1: + # FIXME: the hex encoder doesn't support packed 1-bit + # images; do things the hard way... + data = im.tostring("raw", "1") + im = Image.new("L", (len(data), 1), None) + im.putdata(data) + ImageFile._save(im, op, [("hex", (0,0)+im.size, 0, im.mode)]) + elif filter == "/DCTDecode": + ImageFile._save(im, op, [("jpeg", (0,0)+im.size, 0, im.mode)]) + elif filter == "/FlateDecode": + ImageFile._save(im, op, [("zip", (0,0)+im.size, 0, im.mode)]) + elif filter == "/RunLengthDecode": + ImageFile._save(im, op, [("packbits", (0,0)+im.size, 0, im.mode)]) + else: + raise ValueError("unsupported PDF filter (%s)" % filter) + + xref[3] = fp.tell() + _obj(fp, 3, Type = "/XObject", + Subtype = "/Image", + Width = width, + Height = height, + Length = len(op.getvalue()), + Filter = filter, + BitsPerComponent = bits, + DecodeParams = params, + ColorSpace = colorspace) + + fp.write("stream\n") + fp.write(op.getvalue()) + fp.write("\nendstream\n") + + _endobj(fp) + + # + # page + + xref[4] = fp.tell() + _obj(fp, 4) + fp.write("<<\n/Type /Page\n/Parent 2 0 R\n"\ + "/Resources <<\n/ProcSet [ /PDF %s ]\n"\ + "/XObject << /image 3 0 R >>\n>>\n"\ + "/MediaBox [ 0 0 %d %d ]\n/Contents 5 0 R\n>>\n" %\ + (procset, width, height)) + _endobj(fp) + + # + # page contents + + op = StringIO.StringIO() + + op.write("q %d 0 0 %d 0 0 cm /image Do Q\n" % (width, height)) + + xref[5] = fp.tell() + _obj(fp, 5, Length = len(op.getvalue())) + + fp.write("stream\n") + fp.write(op.getvalue()) + fp.write("\nendstream\n") + + _endobj(fp) + + # + # trailer + startxref = fp.tell() + fp.write("xref\n0 %d\n0000000000 65535 f \n" % len(xref)) + for x in xref[1:]: + fp.write("%010d 00000 n \n" % x) + fp.write("trailer\n<<\n/Size %d\n/Root 1 0 R\n>>\n" % len(xref)) + fp.write("startxref\n%d\n%%%%EOF\n" % startxref) + fp.flush() + +# +# -------------------------------------------------------------------- + +Image.register_save("PDF", _save) + +Image.register_extension("PDF", ".pdf") + +Image.register_mime("PDF", "application/pdf") diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py new file mode 100644 index 0000000..d4b136f --- /dev/null +++ b/PIL/PixarImagePlugin.py @@ -0,0 +1,71 @@ +# +# The Python Imaging Library. +# $Id: PixarImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# PIXAR raster support for PIL +# +# history: +# 97-01-29 fl Created +# +# notes: +# This is incomplete; it is based on a few samples created with +# Photoshop 2.5 and 3.0, and a summary description provided by +# Greg Coats . Hopefully, "L" and +# "RGBA" support will be added in future versions. +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1997. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.1" + +import Image, ImageFile + +# +# helpers + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + +## +# Image plugin for PIXAR raster images. + +class PixarImageFile(ImageFile.ImageFile): + + format = "PIXAR" + format_description = "PIXAR raster image" + + def _open(self): + + # assuming a 4-byte magic label (FIXME: add "_accept" hook) + s = self.fp.read(4) + if s != "\200\350\000\000": + raise SyntaxError, "not a PIXAR file" + + # read rest of header + s = s + self.fp.read(508) + + self.size = i16(s[418:420]), i16(s[416:418]) + + # get channel/depth descriptions + mode = i16(s[424:426]), i16(s[426:428]) + + if mode == (14, 2): + self.mode = "RGB" + # FIXME: to be continued... + + # create tile descriptor (assuming "dumped") + self.tile = [("raw", (0,0)+self.size, 1024, (self.mode, 0, 1))] + +# +# -------------------------------------------------------------------- + +Image.register_open("PIXAR", PixarImageFile) + +# +# FIXME: what's the standard extension? diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py new file mode 100644 index 0000000..0e8ce9f --- /dev/null +++ b/PIL/PngImagePlugin.py @@ -0,0 +1,558 @@ +# +# The Python Imaging Library. +# $Id: PngImagePlugin.py 2203 2004-12-19 14:32:32Z fredrik $ +# +# PNG support code +# +# See "PNG (Portable Network Graphics) Specification, version 1.0; +# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.). +# +# history: +# 1996-05-06 fl Created (couldn't resist it) +# 1996-12-14 fl Upgraded, added read and verify support (0.2) +# 1996-12-15 fl Separate PNG stream parser +# 1996-12-29 fl Added write support, added getchunks +# 1996-12-30 fl Eliminated circular references in decoder (0.3) +# 1998-07-12 fl Read/write 16-bit images as mode I (0.4) +# 2001-02-08 fl Added transparency support (from Zircon) (0.5) +# 2001-04-16 fl Don't close data source in "open" method (0.6) +# 2004-02-24 fl Don't even pretend to support interlaced files (0.7) +# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8) +# 2004-09-20 fl Added PngInfo chunk container +# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev) +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.8.2" + +import re, string + +import Image, ImageFile, ImagePalette + + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + +is_cid = re.compile("\w\w\w\w").match + + +_MAGIC = "\211PNG\r\n\032\n" + + +_MODES = { + # supported bits/color combinations, and corresponding modes/rawmodes + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), + (16,0): ("I", "I;16B"), + (8, 2): ("RGB", "RGB"), + (16,2): ("RGB", "RGB;16B"), + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + (8, 4): ("LA", "LA"), + (16,4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available + (8, 6): ("RGBA", "RGBA"), + (16,6): ("RGBA", "RGBA;16B"), +} + + +# -------------------------------------------------------------------- +# Support classes. Suitable for PNG and related formats like MNG etc. + +class ChunkStream: + + def __init__(self, fp): + + self.fp = fp + self.queue = [] + + if not hasattr(Image.core, "crc32"): + self.crc = self.crc_skip + + def read(self): + "Fetch a new chunk. Returns header information." + + if self.queue: + cid, pos, len = self.queue[-1] + del self.queue[-1] + self.fp.seek(pos) + else: + s = self.fp.read(8) + cid = s[4:] + pos = self.fp.tell() + len = i32(s) + + if not is_cid(cid): + raise SyntaxError, "broken PNG file (chunk %s)" % repr(cid) + + return cid, pos, len + + def close(self): + self.queue = self.crc = self.fp = None + + def push(self, cid, pos, len): + + self.queue.append((cid, pos, len)) + + def call(self, cid, pos, len): + "Call the appropriate chunk handler" + + if Image.DEBUG: + print "STREAM", cid, pos, len + return getattr(self, "chunk_" + cid)(pos, len) + + def crc(self, cid, data): + "Read and verify checksum" + + crc1 = Image.core.crc32(data, Image.core.crc32(cid)) + crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) + if crc1 != crc2: + raise SyntaxError, "broken PNG file"\ + "(bad header checksum in %s)" % cid + + def crc_skip(self, cid, data): + "Read checksum. Used if the C module is not present" + + self.fp.read(4) + + def verify(self, endchunk = "IEND"): + + # Simple approach; just calculate checksum for all remaining + # blocks. Must be called directly after open. + + cids = [] + + while 1: + cid, pos, len = self.read() + if cid == endchunk: + break + self.crc(cid, ImageFile._safe_read(self.fp, len)) + cids.append(cid) + + return cids + + +# -------------------------------------------------------------------- +# PNG chunk container (for use with save(pnginfo=)) + +class PngInfo: + + def __init__(self): + self.chunks = [] + + def add(self, cid, data): + self.chunks.append((cid, data)) + + def add_text(self, key, value, zip=0): + if zip: + import zlib + self.add("zTXt", key + "\0\0" + zlib.compress(value)) + else: + self.add("tEXt", key + "\0" + value) + +# -------------------------------------------------------------------- +# PNG image stream (IHDR/IEND) + +class PngStream(ChunkStream): + + def __init__(self, fp): + + ChunkStream.__init__(self, fp) + + # local copies of Image attributes + self.im_info = {} + self.im_size = (0,0) + self.im_mode = None + self.im_tile = None + self.im_palette = None + + def chunk_IHDR(self, pos, len): + + # image header + s = ImageFile._safe_read(self.fp, len) + self.im_size = i32(s), i32(s[4:]) + try: + self.im_mode, self.im_rawmode = _MODES[(ord(s[8]), ord(s[9]))] + except: + pass + if ord(s[12]): + self.im_info["interlace"] = 1 + if ord(s[11]): + raise SyntaxError, "unknown filter category" + return s + + def chunk_IDAT(self, pos, len): + + # image data + self.im_tile = [("zip", (0,0)+self.im_size, pos, self.im_rawmode)] + self.im_idat = len + raise EOFError + + def chunk_IEND(self, pos, len): + + # end of PNG image + raise EOFError + + def chunk_PLTE(self, pos, len): + + # palette + s = ImageFile._safe_read(self.fp, len) + if self.im_mode == "P": + self.im_palette = "RGB", s + return s + + def chunk_tRNS(self, pos, len): + + # transparency + s = ImageFile._safe_read(self.fp, len) + if self.im_mode == "P": + i = string.find(s, chr(0)) + if i >= 0: + self.im_info["transparency"] = i + elif self.im_mode == "L": + self.im_info["transparency"] = i16(s) + return s + + def chunk_gAMA(self, pos, len): + + # gamma setting + s = ImageFile._safe_read(self.fp, len) + self.im_info["gamma"] = i32(s) / 100000.0 + return s + + def chunk_pHYs(self, pos, len): + + # pixels per unit + s = ImageFile._safe_read(self.fp, len) + px, py = i32(s), i32(s[4:]) + unit = ord(s[8]) + if unit == 1: # meter + dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) + self.im_info["dpi"] = dpi + elif unit == 0: + self.im_info["aspect"] = px, py + return s + + def chunk_tEXt(self, pos, len): + + # text + s = ImageFile._safe_read(self.fp, len) + try: + k, v = string.split(s, "\0", 1) + except ValueError: + k = s; v = "" # fallback for broken tEXt tags + if k: + self.im_info[k] = v + return s + + +# -------------------------------------------------------------------- +# PNG reader + +def _accept(prefix): + return prefix[:8] == _MAGIC + +## +# Image plugin for PNG images. + +class PngImageFile(ImageFile.ImageFile): + + format = "PNG" + format_description = "Portable network graphics" + + def _open(self): + + if self.fp.read(8) != _MAGIC: + raise SyntaxError, "not a PNG file" + + # + # Parse headers up to the first IDAT chunk + + self.png = PngStream(self.fp) + + while 1: + + # + # get next chunk + + cid, pos, len = self.png.read() + + try: + s = self.png.call(cid, pos, len) + except EOFError: + break + except AttributeError: + if Image.DEBUG: + print cid, pos, len, "(unknown)" + s = ImageFile._safe_read(self.fp, len) + + self.png.crc(cid, s) + + # + # Copy relevant attributes from the PngStream. An alternative + # would be to let the PngStream class modify these attributes + # directly, but that introduces circular references which are + # difficult to break if things go wrong in the decoder... + # (believe me, I've tried ;-) + + self.mode = self.png.im_mode + self.size = self.png.im_size + self.info = self.png.im_info + self.tile = self.png.im_tile + + if self.png.im_palette: + rawmode, data = self.png.im_palette + self.palette = ImagePalette.raw(rawmode, data) + + self.__idat = len # used by load_read() + + + def verify(self): + "Verify PNG file" + + if self.fp is None: + raise RuntimeError("verify must be called directly after open") + + # back up to beginning of IDAT block + self.fp.seek(self.tile[0][2] - 8) + + self.png.verify() + self.png.close() + + self.fp = None + + def load_prepare(self): + "internal: prepare to read PNG file" + + if self.info.get("interlace"): + raise IOError("cannot read interlaced PNG files") + + ImageFile.ImageFile.load_prepare(self) + + def load_read(self, bytes): + "internal: read more image data" + + while self.__idat == 0: + # end of chunk, skip forward to next one + + self.fp.read(4) # CRC + + cid, pos, len = self.png.read() + + if cid not in ["IDAT", "DDAT"]: + self.png.push(cid, pos, len) + return "" + + self.__idat = len # empty chunks are allowed + + # read more data from this chunk + if bytes <= 0: + bytes = self.__idat + else: + bytes = min(bytes, self.__idat) + + self.__idat = self.__idat - bytes + + return self.fp.read(bytes) + + + def load_end(self): + "internal: finished reading image data" + + self.png.close() + self.png = None + + +# -------------------------------------------------------------------- +# PNG writer + +def o16(i): + return chr(i>>8&255) + chr(i&255) + +def o32(i): + return chr(i>>24&255) + chr(i>>16&255) + chr(i>>8&255) + chr(i&255) + +_OUTMODES = { + # supported PIL modes, and corresponding rawmodes/bits/color combinations + "1": ("1", chr(1)+chr(0)), + "L;1": ("L;1", chr(1)+chr(0)), + "L;2": ("L;2", chr(2)+chr(0)), + "L;4": ("L;4", chr(4)+chr(0)), + "L": ("L", chr(8)+chr(0)), + "LA": ("LA", chr(8)+chr(4)), + "I": ("I;16B", chr(16)+chr(0)), + "P;1": ("P;1", chr(1)+chr(3)), + "P;2": ("P;2", chr(2)+chr(3)), + "P;4": ("P;4", chr(4)+chr(3)), + "P": ("P", chr(8)+chr(3)), + "RGB": ("RGB", chr(8)+chr(2)), + "RGBA":("RGBA", chr(8)+chr(6)), +} + +def putchunk(fp, cid, *data): + "Write a PNG chunk (including CRC field)" + + data = string.join(data, "") + + fp.write(o32(len(data)) + cid) + fp.write(data) + hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) + fp.write(o16(hi) + o16(lo)) + +class _idat: + # wrap output from the encoder in IDAT chunks + + def __init__(self, fp, chunk): + self.fp = fp + self.chunk = chunk + def write(self, data): + self.chunk(self.fp, "IDAT", data) + +def _save(im, fp, filename, chunk=putchunk, check=0): + # save an image to disk (called by the save method) + + mode = im.mode + + if mode == "P": + + # + # attempt to minimize storage requirements for palette images + + if im.encoderinfo.has_key("bits"): + + # number of bits specified by user + n = 1 << im.encoderinfo["bits"] + + else: + + # check palette contents + n = 256 # FIXME + + if n <= 2: + bits = 1 + elif n <= 4: + bits = 2 + elif n <= 16: + bits = 4 + else: + bits = 8 + + if bits != 8: + mode = "%s;%d" % (mode, bits) + + # encoder options + if im.encoderinfo.has_key("dictionary"): + dictionary = im.encoderinfo["dictionary"] + else: + dictionary = "" + + im.encoderconfig = (im.encoderinfo.has_key("optimize"), dictionary) + + # get the corresponding PNG mode + try: + rawmode, mode = _OUTMODES[mode] + except KeyError: + raise IOError, "cannot write mode %s as PNG" % mode + + if check: + return check + + # + # write minimal PNG file + + fp.write(_MAGIC) + + chunk(fp, "IHDR", + o32(im.size[0]), o32(im.size[1]), # 0: size + mode, # 8: depth/type + chr(0), # 10: compression + chr(0), # 11: filter category + chr(0)) # 12: interlace flag + + if im.mode == "P": + chunk(fp, "PLTE", im.im.getpalette("RGB")) + + if im.encoderinfo.has_key("transparency"): + if im.mode == "P": + transparency = max(0, min(255, im.encoderinfo["transparency"])) + chunk(fp, "tRNS", chr(255) * transparency + chr(0)) + elif im.mode == "L": + transparency = max(0, min(65535, im.encoderinfo["transparency"])) + chunk(fp, "tRNS", o16(transparency)) + else: + raise IOError, "cannot use transparency for this mode" + + if 0: + # FIXME: to be supported some day + chunk(fp, "gAMA", o32(int(gamma * 100000.0))) + + dpi = im.encoderinfo.get("dpi") + if dpi: + chunk(fp, "pHYs", + o32(int(dpi[0] / 0.0254 + 0.5)), + o32(int(dpi[1] / 0.0254 + 0.5)), + chr(1)) + + info = im.encoderinfo.get("pnginfo") + if info: + for cid, data in info.chunks: + chunk(fp, cid, data) + + ImageFile._save(im, _idat(fp, chunk), [("zip", (0,0)+im.size, 0, rawmode)]) + + chunk(fp, "IEND", "") + + try: + fp.flush() + except: + pass + + +# -------------------------------------------------------------------- +# PNG chunk converter + +def getchunks(im, **params): + """Return a list of PNG chunks representing this image.""" + + class collector: + data = [] + def write(self, data): + pass + def append(self, chunk): + self.data.append(chunk) + + def append(fp, cid, *data): + data = string.join(data, "") + hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) + crc = o16(hi) + o16(lo) + fp.append((cid, data, crc)) + + fp = collector() + + try: + im.encoderinfo = params + _save(im, fp, None, append) + finally: + del im.encoderinfo + + return fp.data + + +# -------------------------------------------------------------------- +# Registry + +Image.register_open("PNG", PngImageFile, _accept) +Image.register_save("PNG", _save) + +Image.register_extension("PNG", ".png") + +Image.register_mime("PNG", "image/png") diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py new file mode 100644 index 0000000..b9b9f81 --- /dev/null +++ b/PIL/PpmImagePlugin.py @@ -0,0 +1,131 @@ +# +# The Python Imaging Library. +# $Id: PpmImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# PPM support for PIL +# +# History: +# 96-03-24 fl Created +# 98-03-06 fl Write RGBA images (as RGB, that is) +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.2" + +import string + +import Image, ImageFile + +# +# -------------------------------------------------------------------- + +MODES = { + # standard + "P4": "1", + "P5": "L", + "P6": "RGB", + # extensions + "P0CMYK": "CMYK", + # PIL extensions (for test purposes only) + "PyP": "P", + "PyRGBA": "RGBA", + "PyCMYK": "CMYK" +} + +def _accept(prefix): + return prefix[0] == "P" and prefix[1] in "0456y" + +## +# Image plugin for PBM, PGM, and PPM images. + +class PpmImageFile(ImageFile.ImageFile): + + format = "PPM" + format_description = "Pbmplus image" + + def _token(self, s = ""): + while 1: # read until next whitespace + c = self.fp.read(1) + if not c or c in string.whitespace: + break + s = s + c + return s + + def _open(self): + + # check magic + s = self.fp.read(1) + if s != "P": + raise SyntaxError, "not a PPM file" + mode = MODES[self._token(s)] + + if mode == "1": + self.mode = "1" + rawmode = "1;I" + else: + self.mode = rawmode = mode + + for ix in range(3): + while 1: + while 1: + s = self.fp.read(1) + if s not in string.whitespace: + break + if s != "#": + break + s = self.fp.readline() + s = int(self._token(s)) + if ix == 0: + xsize = s + elif ix == 1: + ysize = s + if mode == "1": + break + + self.size = xsize, ysize + self.tile = [("raw", + (0, 0, xsize, ysize), + self.fp.tell(), + (rawmode, 0, 1))] + + # ALTERNATIVE: load via builtin debug function + # self.im = Image.core.open_ppm(self.filename) + # self.mode = self.im.mode + # self.size = self.im.size + +# +# -------------------------------------------------------------------- + +def _save(im, fp, filename): + if im.mode == "1": + rawmode, head = "1;I", "P4" + elif im.mode == "L": + rawmode, head = "L", "P5" + elif im.mode == "RGB": + rawmode, head = "RGB", "P6" + elif im.mode == "RGBA": + rawmode, head = "RGB", "P6" + else: + raise IOError, "cannot write mode %s as PPM" % im.mode + fp.write(head + "\n%d %d\n" % im.size) + if head != "P4": + fp.write("255\n") + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, 1))]) + + # ALTERNATIVE: save via builtin debug function + # im._dump(filename) + +# +# -------------------------------------------------------------------- + +Image.register_open("PPM", PpmImageFile, _accept) +Image.register_save("PPM", _save) + +Image.register_extension("PPM", ".pbm") +Image.register_extension("PPM", ".pgm") +Image.register_extension("PPM", ".ppm") diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py new file mode 100644 index 0000000..1e68c0d --- /dev/null +++ b/PIL/PsdImagePlugin.py @@ -0,0 +1,271 @@ +# +# The Python Imaging Library +# $Id: PsdImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Adobe PSD 2.5/3.0 file handling +# +# History: +# 1995-09-01 fl Created +# 1997-01-03 fl Read most PSD images +# 1997-01-18 fl Fixed P and CMYK support +# 2001-10-21 fl Added seek/tell support (for layers) +# +# Copyright (c) 1997-2001 by Secret Labs AB. +# Copyright (c) 1995-2001 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.4" + +import string +import Image, ImageFile, ImagePalette + +MODES = { + # (photoshop mode, bits) -> (pil mode, required channels) + (0, 1): ("1", 1), + (0, 8): ("L", 1), + (1, 8): ("L", 1), + (2, 8): ("P", 1), + (3, 8): ("RGB", 3), + (4, 8): ("CMYK", 4), + (7, 8): ("L", 1), # FIXME: multilayer + (8, 8): ("L", 1), # duotone + (9, 8): ("LAB", 3) +} + +# +# helpers + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) + +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + +# --------------------------------------------------------------------. +# read PSD images + +def _accept(prefix): + return prefix[:4] == "8BPS" + +## +# Image plugin for Photoshop images. + +class PsdImageFile(ImageFile.ImageFile): + + format = "PSD" + format_description = "Adobe Photoshop" + + def _open(self): + + read = self.fp.read + + # + # header + + s = read(26) + if s[:4] != "8BPS" or i16(s[4:]) != 1: + raise SyntaxError, "not a PSD file" + + psd_bits = i16(s[22:]) + psd_channels = i16(s[12:]) + psd_mode = i16(s[24:]) + + mode, channels = MODES[(psd_mode, psd_bits)] + + if channels > psd_channels: + raise IOError, "not enough channels" + + self.mode = mode + self.size = i32(s[18:]), i32(s[14:]) + + # + # color mode data + + size = i32(read(4)) + if size: + data = read(size) + if mode == "P" and size == 768: + self.palette = ImagePalette.raw("RGB;L", data) + + # + # image resources + + self.resources = [] + + size = i32(read(4)) + if size: + # load resources + end = self.fp.tell() + size + while self.fp.tell() < end: + signature = read(4) + id = i16(read(2)) + name = read(ord(read(1))) + if not (len(name) & 1): + read(1) # padding + data = read(i32(read(4))) + if (len(data) & 1): + read(1) # padding + self.resources.append((id, name, data)) + + # + # layer and mask information + + self.layers = [] + + size = i32(read(4)) + if size: + end = self.fp.tell() + size + size = i32(read(4)) + if size: + self.layers = _layerinfo(self.fp) + self.fp.seek(end) + + # + # image descriptor + + self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) + + # keep the file open + self._fp = self.fp + self.frame = 0 + + def seek(self, layer): + # seek to given layer (1..max) + if layer == self.frame: + return + try: + if layer <= 0: + raise IndexError + name, mode, bbox, tile = self.layers[layer-1] + self.mode = mode + self.tile = tile + self.frame = layer + self.fp = self._fp + return name, bbox + except IndexError: + raise EOFError, "no such layer" + + def tell(self): + # return layer number (0=image, 1..max=layers) + return self.frame + + def load_prepare(self): + # create image memory if necessary + if not self.im or\ + self.im.mode != self.mode or self.im.size != self.size: + self.im = Image.core.fill(self.mode, self.size, 0) + # create palette (optional) + if self.mode == "P": + Image.Image.load(self) + +def _layerinfo(file): + # read layerinfo block + layers = [] + read = file.read + + for i in range(abs(i16(read(2)))): + + # bounding box + y0 = i32(read(4)); x0 = i32(read(4)) + y1 = i32(read(4)); x1 = i32(read(4)) + + # image info + info = [] + mode = [] + for i in range(i16(read(2))): + type = i16(read(2)) + if type == 65535: + m = "A" + else: + m = "RGB"[type] + mode.append(m) + size = i32(read(4)) + info.append((m, size)) + + # figure out the image mode + mode.sort() + if mode == ["R"]: + mode = "L" + elif mode == ["B", "G", "R"]: + mode = "RGB" + elif mode == ["A", "B", "G", "R"]: + mode = "RGBA" + else: + mode = None # unknown + + # skip over blend flags and extra information + filler = read(12) + name = None # FIXME + file.seek(i32(read(4)), 1) + + layers.append((name, mode, (x0, y0, x1, y1))) + + # get tiles + i = 0 + for name, mode, bbox in layers: + tile = [] + for m in mode: + t = _maketile(file, m, bbox, 1) + if t: + tile.extend(t) + layers[i] = name, mode, bbox, tile + i = i + 1 + + return layers + +def _maketile(file, mode, bbox, channels): + + tile = None + read = file.read + + compression = i16(read(2)) + + xsize = bbox[2] - bbox[0] + ysize = bbox[3] - bbox[1] + + offset = file.tell() + + if compression == 0: + # + # raw compression + tile = [] + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer = layer + ";I" + tile.append(("raw", bbox, offset, layer)) + offset = offset + xsize*ysize + + elif compression == 1: + # + # packbits compression + i = 0 + tile = [] + bytecount = read(channels * ysize * 2) + offset = file.tell() + for channel in range(channels): + layer = mode[channel] + if mode == "CMYK": + layer = layer + ";I" + tile.append( + ("packbits", bbox, offset, layer) + ) + for y in range(ysize): + offset = offset + i16(bytecount[i:i+2]) + i = i + 2 + + file.seek(offset) + + if offset & 1: + read(1) # padding + + return tile + +# -------------------------------------------------------------------- +# registry + +Image.register_open("PSD", PsdImageFile, _accept) + +Image.register_extension("PSD", ".psd") diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py new file mode 100644 index 0000000..0ab741e --- /dev/null +++ b/PIL/SgiImagePlugin.py @@ -0,0 +1,91 @@ +# +# The Python Imaging Library. +# $Id: SgiImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# SGI image file handling +# +# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. +# +# +# History: +# 1995-09-10 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.1" + + +import string +import Image, ImageFile + + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) + +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + + +def _accept(prefix): + return i16(prefix) == 474 + +## +# Image plugin for SGI images. + +class SgiImageFile(ImageFile.ImageFile): + + format = "SGI" + format_description = "SGI Image File Format" + + def _open(self): + + # HEAD + s = self.fp.read(512) + if i16(s) != 474: + raise SyntaxError, "not an SGI image file" + + # relevant header entries + compression = ord(s[2]) + + # bytes, dimension, zsize + layout = ord(s[3]), i16(s[4:]), i16(s[10:]) + + # determine mode from bytes/zsize + if layout == (1, 2, 1): + self.mode = "L" + elif layout == (1, 3, 3): + self.mode = "RGB" + else: + raise SyntaxError, "unsupported SGI image mode" + + # size + self.size = i16(s[6:]), i16(s[8:]) + + # decoder info + if compression == 0: + if self.mode == "RGB": + # RGB images are band interleaved + size = self.size[0]*self.size[1] + self.tile = [("raw", (0,0)+self.size, 512, ("R",0,1)), + ("raw", (0,0)+self.size, 512+size, ("G",0,1)), + ("raw", (0,0)+self.size, 512+2*size, ("B",0,1))] + else: + self.tile = [("raw", (0,0)+self.size, 512, (self.mode, 0, 1))] + if compression == 1: + self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, 1))] + +# +# registry + +Image.register_open("SGI", SgiImageFile, _accept) + +Image.register_extension("SGI", ".bw") +Image.register_extension("SGI", ".rgb") +Image.register_extension("SGI", ".rgba") + +Image.register_extension("SGI", ".sgi") # really? diff --git a/PIL/SpiderImagePlugin.py b/PIL/SpiderImagePlugin.py new file mode 100644 index 0000000..e0fd045 --- /dev/null +++ b/PIL/SpiderImagePlugin.py @@ -0,0 +1,294 @@ +# +# The Python Imaging Library. +# +# SPIDER image file handling +# +# History: +# 2004-08-02 Created BB +# 2006-03-02 added save method +# 2006-03-13 added support for stack images +# +# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144. +# Copyright (c) 2004 by William Baxter. +# Copyright (c) 2004 by Secret Labs AB. +# Copyright (c) 2004 by Fredrik Lundh. +# + +## +# Image plugin for the Spider image format. This format is is used +# by the SPIDER software, in processing image data from electron +# microscopy and tomography. +## + +# +# SpiderImagePlugin.py +# +# The Spider image format is used by SPIDER software, in processing +# image data from electron microscopy and tomography. +# +# Spider home page: +# http://www.wadsworth.org/spider_doc/spider/docs/spider.html +# +# Details about the Spider image format: +# http://www.wadsworth.org/spider_doc/spider/docs/image_doc.html +# + +import Image, ImageFile +import os, string, struct, sys + +def isInt(f): + try: + i = int(f) + if f-i == 0: return 1 + else: return 0 + except: + return 0 + +iforms = [1,3,-11,-12,-21,-22] + +# There is no magic number to identify Spider files, so just check a +# series of header locations to see if they have reasonable values. +# Returns no.of bytes in the header, if it is a valid Spider header, +# otherwise returns 0 + +def isSpiderHeader(t): + h = (99,) + t # add 1 value so can use spider header index start=1 + # header values 1,2,5,12,13,22,23 should be integers + for i in [1,2,5,12,13,22,23]: + if not isInt(h[i]): return 0 + # check iform + iform = int(h[5]) + if not iform in iforms: return 0 + # check other header values + labrec = int(h[13]) # no. records in file header + labbyt = int(h[22]) # total no. of bytes in header + lenbyt = int(h[23]) # record length in bytes + #print "labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt) + if labbyt != (labrec * lenbyt): return 0 + # looks like a valid header + return labbyt + +def isSpiderImage(filename): + fp = open(filename,'rb') + f = fp.read(92) # read 23 * 4 bytes + fp.close() + bigendian = 1 + t = struct.unpack('>23f',f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + bigendian = 0 + t = struct.unpack('<23f',f) # little-endian + hdrlen = isSpiderHeader(t) + return hdrlen + + +class SpiderImageFile(ImageFile.ImageFile): + + format = "SPIDER" + format_description = "Spider 2D image" + + def _open(self): + # check header + n = 27 * 4 # read 27 float values + f = self.fp.read(n) + + try: + self.bigendian = 1 + t = struct.unpack('>27f',f) # try big-endian first + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + self.bigendian = 0 + t = struct.unpack('<27f',f) # little-endian + hdrlen = isSpiderHeader(t) + if hdrlen == 0: + raise SyntaxError, "not a valid Spider file" + except struct.error: + raise SyntaxError, "not a valid Spider file" + + h = (99,) + t # add 1 value : spider header index starts at 1 + iform = int(h[5]) + if iform != 1: + raise SyntaxError, "not a Spider 2D image" + + self.size = int(h[12]), int(h[2]) # size in pixels (width, height) + self.istack = int(h[24]) + self.imgnumber = int(h[27]) + + if self.istack == 0 and self.imgnumber == 0: + # stk=0, img=0: a regular 2D image + offset = hdrlen + self.nimages = 1 + elif self.istack > 0 and self.imgnumber == 0: + # stk>0, img=0: Opening the stack for the first time + self.imgbytes = int(h[12]) * int(h[2]) * 4 + self.hdrlen = hdrlen + self.nimages = int(h[26]) + # Point to the first image in the stack + offset = hdrlen * 2 + self.imgnumber = 1 + elif self.istack == 0 and self.imgnumber > 0: + # stk=0, img>0: an image within the stack + offset = hdrlen + self.stkoffset + self.istack = 2 # So Image knows it's still a stack + else: + raise SyntaxError, "inconsistent stack header values" + + if self.bigendian: + self.rawmode = "F;32BF" + else: + self.rawmode = "F;32F" + self.mode = "F" + + self.tile = [("raw", (0, 0) + self.size, offset, + (self.rawmode, 0, 1))] + self.__fp = self.fp # FIXME: hack + + # 1st image index is zero (although SPIDER imgnumber starts at 1) + def tell(self): + if self.imgnumber < 1: + return 0 + else: + return self.imgnumber - 1 + + def seek(self, frame): + if self.istack == 0: + return + if frame >= self.nimages: + raise EOFError, "attempt to seek past end of file" + self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) + self.fp = self.__fp + self.fp.seek(self.stkoffset) + self._open() + + # returns a byte image after rescaling to 0..255 + def convert2byte(self, depth=255): + (min, max) = self.getextrema() + m = 1 + if max != min: + m = depth / (max-min) + b = -m * min + return self.point(lambda i, m=m, b=b: i * m + b).convert("L") + + # returns a ImageTk.PhotoImage object, after rescaling to 0..255 + def tkPhotoImage(self): + import ImageTk + return ImageTk.PhotoImage(self.convert2byte(), palette=256) + +# -------------------------------------------------------------------- +# Image series + +# given a list of filenames, return a list of images +def loadImageSeries(filelist=None): + " create a list of Image.images for use in montage " + if filelist == None or len(filelist) < 1: + return + + imglist = [] + for img in filelist: + if not os.path.exists(img): + print "unable to find %s" % img + continue + try: + im = Image.open(img).convert2byte() + except: + if not isSpiderImage(img): + print img + " is not a Spider image file" + continue + im.info['filename'] = img + imglist.append(im) + return imglist + +# -------------------------------------------------------------------- +# For saving images in Spider format + +def makeSpiderHeader(im): + nsam,nrow = im.size + lenbyt = nsam * 4 # There are labrec records in the header + labrec = 1024 / lenbyt + if 1024%lenbyt != 0: labrec += 1 + labbyt = labrec * lenbyt + hdr = [] + nvalues = labbyt / 4 + for i in range(nvalues): + hdr.append(0.0) + + if len(hdr) < 23: + return [] + + # NB these are Fortran indices + hdr[1] = 1.0 # nslice (=1 for an image) + hdr[2] = float(nrow) # number of rows per slice + hdr[5] = 1.0 # iform for 2D image + hdr[12] = float(nsam) # number of pixels per line + hdr[13] = float(labrec) # number of records in file header + hdr[22] = float(labbyt) # total number of bytes in header + hdr[23] = float(lenbyt) # record length in bytes + + # adjust for Fortran indexing + hdr = hdr[1:] + hdr.append(0.0) + # pack binary data into a string + hdrstr = [] + for v in hdr: + hdrstr.append(struct.pack('f',v)) + return hdrstr + +def _save(im, fp, filename): + if im.mode[0] != "F": + im = im.convert('F') + + hdr = makeSpiderHeader(im) + if len(hdr) < 256: + raise IOError, "Error creating Spider header" + + # write the SPIDER header + try: + fp = open(filename, 'wb') + except: + raise IOError, "Unable to open %s for writing" % filename + fp.writelines(hdr) + + rawmode = "F;32NF" #32-bit native floating point + ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode,0,1))]) + + fp.close() + +def _save_spider(im, fp, filename): + # get the filename extension and register it with Image + fn, ext = os.path.splitext(filename) + Image.register_extension("SPIDER", ext) + _save(im, fp, filename) + +# -------------------------------------------------------------------- + +Image.register_open("SPIDER", SpiderImageFile) +Image.register_save("SPIDER", _save_spider) + +if __name__ == "__main__": + + if not sys.argv[1:]: + print "Syntax: python SpiderImagePlugin.py Spiderimage [outfile]" + sys.exit() + + filename = sys.argv[1] + if not isSpiderImage(filename): + print "input image must be in Spider format" + sys.exit() + + outfile = "" + if len(sys.argv[1:]) > 1: + outfile = sys.argv[2] + + im = Image.open(filename) + print "image: " + str(im) + print "format: " + str(im.format) + print "size: " + str(im.size) + print "mode: " + str(im.mode) + print "max, min: ", + print im.getextrema() + + if outfile != "": + # perform some image operation + im = im.transpose(Image.FLIP_LEFT_RIGHT) + print "saving a flipped version of %s as %s " % (os.path.basename(filename), outfile) + im.save(outfile, "SPIDER") diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py new file mode 100644 index 0000000..2e7fe52 --- /dev/null +++ b/PIL/SunImagePlugin.py @@ -0,0 +1,87 @@ +# +# The Python Imaging Library. +# $Id: SunImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# Sun image file handling +# +# History: +# 1995-09-10 fl Created +# 1996-05-28 fl Fixed 32-bit alignment +# 1998-12-29 fl Import ImagePalette module +# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault) +# +# Copyright (c) 1997-2001 by Secret Labs AB +# Copyright (c) 1995-1996 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.3" + + +import string +import Image, ImageFile, ImagePalette + + +def i16(c): + return ord(c[1]) + (ord(c[0])<<8) + +def i32(c): + return ord(c[3]) + (ord(c[2])<<8) + (ord(c[1])<<16) + (ord(c[0])<<24) + + +def _accept(prefix): + return i32(prefix) == 0x59a66a95 + +## +# Image plugin for Sun raster files. + +class SunImageFile(ImageFile.ImageFile): + + format = "SUN" + format_description = "Sun Raster File" + + def _open(self): + + # HEAD + s = self.fp.read(32) + if i32(s) != 0x59a66a95: + raise SyntaxError, "not an SUN raster file" + + offset = 32 + + self.size = i32(s[4:8]), i32(s[8:12]) + + depth = i32(s[12:16]) + if depth == 1: + self.mode, rawmode = "1", "1;I" + elif depth == 8: + self.mode = rawmode = "L" + elif depth == 24: + self.mode, rawmode = "RGB", "BGR" + else: + raise SyntaxError, "unsupported mode" + + compression = i32(s[20:24]) + + if i32(s[24:28]) != 0: + length = i32(s[28:32]) + offset = offset + length + self.palette = ImagePalette.raw("RGB;L", self.fp.read(length)) + if self.mode == "L": + self.mode = rawmode = "P" + + stride = (((self.size[0] * depth + 7) / 8) + 3) & (~3) + + if compression == 1: + self.tile = [("raw", (0,0)+self.size, offset, (rawmode, stride))] + elif compression == 2: + self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)] + +# +# registry + +Image.register_open("SUN", SunImageFile, _accept) + +Image.register_extension("SUN", ".ras") diff --git a/PIL/TarIO.py b/PIL/TarIO.py new file mode 100644 index 0000000..fbab8ca --- /dev/null +++ b/PIL/TarIO.py @@ -0,0 +1,57 @@ +# +# The Python Imaging Library. +# $Id: TarIO.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# read files from within a tar file +# +# History: +# 95-06-18 fl Created +# 96-05-28 fl Open files in binary mode +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1995-96. +# +# See the README file for information on usage and redistribution. +# + +import ContainerIO +import string + +## +# A file object that provides read access to a given member of a TAR +# file. + +class TarIO(ContainerIO.ContainerIO): + + ## + # Create file object. + # + # @param tarfile Name of TAR file. + # @param file Name of member file. + + def __init__(self, tarfile, file): + + fh = open(tarfile, "rb") + + while 1: + + s = fh.read(512) + if len(s) != 512: + raise IOError, "unexpected end of tar file" + + name = s[:100] + i = string.find(name, chr(0)) + if i == 0: + raise IOError, "cannot find subfile" + if i > 0: + name = name[:i] + + size = string.atoi(s[124:136], 8) + + if file == name: + break + + fh.seek((size + 511) & (~511), 1) + + # Open region + ContainerIO.ContainerIO.__init__(self, fh, fh.tell(), size) diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py new file mode 100644 index 0000000..e709de8 --- /dev/null +++ b/PIL/TgaImagePlugin.py @@ -0,0 +1,133 @@ +# +# The Python Imaging Library. +# $Id: TgaImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# TGA file handling +# +# History: +# 95-09-01 fl created (reads 24-bit files only) +# 97-01-04 fl support more TGA versions, including compressed images +# 98-07-04 fl fixed orientation and alpha layer bugs +# 98-09-11 fl fixed orientation for runlength decoder +# +# Copyright (c) Secret Labs AB 1997-98. +# Copyright (c) Fredrik Lundh 1995-97. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.3" + +import Image, ImageFile, ImagePalette + + +def i16(c): + return ord(c[0]) + (ord(c[1])<<8) + +def i32(c): + return ord(c[0]) + (ord(c[1])<<8) + (ord(c[2])<<16) + (ord(c[3])<<24) + + +MODES = { + # map imagetype/depth to rawmode + (1, 8): "P", + (3, 1): "1", + (3, 8): "L", + (2, 16): "BGR;5", + (2, 24): "BGR", + (2, 32): "BGRA", +} + + +def _accept(prefix): + return prefix[0] == "\0" + +## +# Image plugin for Targa files. + +class TgaImageFile(ImageFile.ImageFile): + + format = "TGA" + format_description = "Targa" + + def _open(self): + + # process header + s = self.fp.read(18) + + id = ord(s[0]) + + colormaptype = ord(s[1]) + imagetype = ord(s[2]) + + depth = ord(s[16]) + + flags = ord(s[17]) + + self.size = i16(s[12:]), i16(s[14:]) + + # validate header fields + if id != 0 or colormaptype not in (0, 1) or\ + self.size[0] <= 0 or self.size[1] <= 0 or\ + depth not in (8, 16, 24, 32): + raise SyntaxError, "not a TGA file" + + # image mode + if imagetype in (3, 11): + self.mode = "L" + if depth == 1: + self.mode = "1" # ??? + elif imagetype in (1, 9): + self.mode = "P" + elif imagetype in (2, 10): + self.mode = "RGB" + if depth == 32: + self.mode = "RGBA" + else: + raise SyntaxError, "unknown TGA mode" + + # orientation + orientation = flags & 0x30 + if orientation == 0x20: + orientation = 1 + elif not orientation: + orientation = -1 + else: + raise SyntaxError, "unknown TGA orientation" + + if imagetype & 8: + self.info["compression"] = "tga_rle" + + if colormaptype: + # read palette + start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:]) + if mapdepth == 16: + self.palette = ImagePalette.raw("BGR;16", + "\0"*2*start + self.fp.read(2*size)) + elif mapdepth == 24: + self.palette = ImagePalette.raw("BGR", + "\0"*3*start + self.fp.read(3*size)) + elif mapdepth == 32: + self.palette = ImagePalette.raw("BGRA", + "\0"*4*start + self.fp.read(4*size)) + + # setup tile descriptor + try: + rawmode = MODES[(imagetype&7, depth)] + if imagetype & 8: + # compressed + self.tile = [("tga_rle", (0, 0)+self.size, + self.fp.tell(), (rawmode, orientation, depth))] + else: + self.tile = [("raw", (0, 0)+self.size, + self.fp.tell(), (rawmode, 0, orientation))] + except KeyError: + pass # cannot decode + +# +# registry + +Image.register_open("TGA", TgaImageFile, _accept) + +Image.register_extension("TGA", ".tga") diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py new file mode 100644 index 0000000..178066c --- /dev/null +++ b/PIL/TiffImagePlugin.py @@ -0,0 +1,763 @@ +# +# The Python Imaging Library. +# $Id: TiffImagePlugin.py 2803 2006-07-31 19:18:57Z fredrik $ +# +# TIFF file handling +# +# TIFF is a flexible, if somewhat aged, image file format originally +# defined by Aldus. Although TIFF supports a wide variety of pixel +# layouts and compression methods, the name doesn't really stand for +# "thousands of incompatible file formats," it just feels that way. +# +# To read TIFF data from a stream, the stream must be seekable. For +# progressive decoding, make sure to use TIFF files where the tag +# directory is placed first in the file. +# +# History: +# 1995-09-01 fl Created +# 1996-05-04 fl Handle JPEGTABLES tag +# 1996-05-18 fl Fixed COLORMAP support +# 1997-01-05 fl Fixed PREDICTOR support +# 1997-08-27 fl Added support for rational tags (from Perry Stoll) +# 1998-01-10 fl Fixed seek/tell (from Jan Blom) +# 1998-07-15 fl Use private names for internal variables +# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) +# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) +# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) +# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) +# 2001-12-18 fl Added workaround for broken Matrox library +# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) +# 2003-05-19 fl Check FILLORDER tag +# 2003-09-26 fl Added RGBa support +# 2004-02-24 fl Added DPI support; fixed rational write support +# 2005-02-07 fl Added workaround for broken Corel Draw 10 files +# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +__version__ = "1.3.5" + +import Image, ImageFile +import ImagePalette + +import array, string, sys + +try: + if sys.byteorder == "little": + byteorder = "II" + else: + byteorder = "MM" +except AttributeError: + if ord(array.array("i",[1]).tostring()[0]): + byteorder = "II" + else: + byteorder = "MM" + +# +# -------------------------------------------------------------------- +# Read TIFF files + +def il16(c,o=0): + return ord(c[o]) + (ord(c[o+1])<<8) +def il32(c,o=0): + return ord(c[o]) + (ord(c[o+1])<<8) + (ord(c[o+2])<<16) + (ord(c[o+3])<<24) +def ol16(i): + return chr(i&255) + chr(i>>8&255) +def ol32(i): + return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255) + +def ib16(c,o=0): + return ord(c[o+1]) + (ord(c[o])<<8) +def ib32(c,o=0): + return ord(c[o+3]) + (ord(c[o+2])<<8) + (ord(c[o+1])<<16) + (ord(c[o])<<24) + +# a few tag names, just to make the code below a bit more readable +IMAGEWIDTH = 256 +IMAGELENGTH = 257 +BITSPERSAMPLE = 258 +COMPRESSION = 259 +PHOTOMETRIC_INTERPRETATION = 262 +FILLORDER = 266 +IMAGEDESCRIPTION = 270 +STRIPOFFSETS = 273 +SAMPLESPERPIXEL = 277 +ROWSPERSTRIP = 278 +STRIPBYTECOUNTS = 279 +X_RESOLUTION = 282 +Y_RESOLUTION = 283 +PLANAR_CONFIGURATION = 284 +RESOLUTION_UNIT = 296 +SOFTWARE = 305 +DATE_TIME = 306 +ARTIST = 315 +PREDICTOR = 317 +COLORMAP = 320 +EXTRASAMPLES = 338 +SAMPLEFORMAT = 339 +JPEGTABLES = 347 +COPYRIGHT = 33432 +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties + +COMPRESSION_INFO = { + # Compression => pil compression name + 1: "raw", + 2: "tiff_ccitt", + 3: "group3", + 4: "group4", + 5: "tiff_lzw", + 6: "tiff_jpeg", # obsolete + 7: "jpeg", + 32771: "tiff_raw_16", # 16-bit padding + 32773: "packbits" +} + +OPEN_INFO = { + # (PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, + # ExtraSamples) => mode, rawmode + (0, 1, 1, (1,), ()): ("1", "1;I"), + (0, 1, 2, (1,), ()): ("1", "1;IR"), + (0, 1, 1, (8,), ()): ("L", "L;I"), + (0, 1, 2, (8,), ()): ("L", "L;IR"), + (1, 1, 1, (1,), ()): ("1", "1"), + (1, 1, 2, (1,), ()): ("1", "1;R"), + (1, 1, 1, (8,), ()): ("L", "L"), + (1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (1, 1, 2, (8,), ()): ("L", "L;R"), + (1, 1, 1, (16,), ()): ("I;16", "I;16"), + (1, 2, 1, (16,), ()): ("I;16S", "I;16S"), + (1, 2, 1, (32,), ()): ("I", "I;32S"), + (1, 3, 1, (32,), ()): ("F", "F;32F"), + (2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), + (2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), + (2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), + (2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), + (2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), + (2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (3, 1, 1, (1,), ()): ("P", "P;1"), + (3, 1, 2, (1,), ()): ("P", "P;1R"), + (3, 1, 1, (2,), ()): ("P", "P;2"), + (3, 1, 2, (2,), ()): ("P", "P;2R"), + (3, 1, 1, (4,), ()): ("P", "P;4"), + (3, 1, 2, (4,), ()): ("P", "P;4R"), + (3, 1, 1, (8,), ()): ("P", "P"), + (3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (3, 1, 2, (8,), ()): ("P", "P;R"), + (5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), + (6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), + (8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), +} + +PREFIXES = ["MM\000\052", "II\052\000", "II\xBC\000"] + +def _accept(prefix): + return prefix[:4] in PREFIXES + +## +# Wrapper for TIFF IFDs. + +class ImageFileDirectory: + + # represents a TIFF tag directory. to speed things up, + # we don't decode tags unless they're asked for. + + def __init__(self, prefix="II"): + self.prefix = prefix[:2] + if self.prefix == "MM": + self.i16, self.i32 = ib16, ib32 + # FIXME: save doesn't yet support big-endian mode... + elif self.prefix == "II": + self.i16, self.i32 = il16, il32 + self.o16, self.o32 = ol16, ol32 + else: + raise SyntaxError("not a TIFF IFD") + self.reset() + + def reset(self): + self.tags = {} + self.tagdata = {} + self.next = None + + # dictionary API (sort of) + + def keys(self): + return self.tagdata.keys() + self.tags.keys() + + def items(self): + items = self.tags.items() + for tag in self.tagdata.keys(): + items.append((tag, self[tag])) + return items + + def __len__(self): + return len(self.tagdata) + len(self.tags) + + def __getitem__(self, tag): + try: + return self.tags[tag] + except KeyError: + type, data = self.tagdata[tag] # unpack on the fly + size, handler = self.load_dispatch[type] + self.tags[tag] = data = handler(self, data) + del self.tagdata[tag] + return data + + def get(self, tag, default=None): + try: + return self[tag] + except KeyError: + return default + + def getscalar(self, tag, default=None): + try: + value = self[tag] + if len(value) != 1: + if tag == SAMPLEFORMAT: + # work around broken (?) matrox library + # (from Ted Wright, via Bob Klimek) + raise KeyError # use default + raise ValueError, "not a scalar" + return value[0] + except KeyError: + if default is None: + raise + return default + + def has_key(self, tag): + return self.tags.has_key(tag) or self.tagdata.has_key(tag) + + def __setitem__(self, tag, value): + if type(value) is not type(()): + value = (value,) + self.tags[tag] = value + + # load primitives + + load_dispatch = {} + + def load_byte(self, data): + l = [] + for i in range(len(data)): + l.append(ord(data[i])) + return tuple(l) + load_dispatch[1] = (1, load_byte) + + def load_string(self, data): + if data[-1:] == '\0': + data = data[:-1] + return data + load_dispatch[2] = (1, load_string) + + def load_short(self, data): + l = [] + for i in range(0, len(data), 2): + l.append(self.i16(data, i)) + return tuple(l) + load_dispatch[3] = (2, load_short) + + def load_long(self, data): + l = [] + for i in range(0, len(data), 4): + l.append(self.i32(data, i)) + return tuple(l) + load_dispatch[4] = (4, load_long) + + def load_rational(self, data): + l = [] + for i in range(0, len(data), 8): + l.append((self.i32(data, i), self.i32(data, i+4))) + return tuple(l) + load_dispatch[5] = (8, load_rational) + + def load_float(self, data): + a = array.array("f", data) + if self.prefix != byteorder: + a.byteswap() + return tuple(a) + load_dispatch[11] = (4, load_float) + + def load_double(self, data): + a = array.array("d", data) + if self.prefix != byteorder: + a.byteswap() + return tuple(a) + load_dispatch[12] = (8, load_double) + + def load_undefined(self, data): + # Untyped data + return data + load_dispatch[7] = (1, load_undefined) + + def load(self, fp): + # load tag dictionary + + self.reset() + + i16 = self.i16 + i32 = self.i32 + + for i in range(i16(fp.read(2))): + + ifd = fp.read(12) + + tag, typ = i16(ifd), i16(ifd, 2) + + if Image.DEBUG: + import TiffTags + tagname = TiffTags.TAGS.get(tag, "unknown") + typname = TiffTags.TYPES.get(typ, "unknown") + print "tag: %s (%d)" % (tagname, tag), + print "- type: %s (%d)" % (typname, typ), + + try: + dispatch = self.load_dispatch[typ] + except KeyError: + if Image.DEBUG: + print "- unsupported type", typ + continue # ignore unsupported type + + size, handler = dispatch + + size = size * i32(ifd, 4) + + # Get and expand tag value + if size > 4: + here = fp.tell() + fp.seek(i32(ifd, 8)) + data = ImageFile._safe_read(fp, size) + fp.seek(here) + else: + data = ifd[8:8+size] + + if len(data) != size: + raise IOError, "not enough data" + + self.tagdata[tag] = typ, data + + if Image.DEBUG: + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK): + print "- value: " % size + else: + print "- value:", self[tag] + + self.next = i32(fp.read(4)) + + # save primitives + + def save(self, fp): + + o16 = self.o16 + o32 = self.o32 + + fp.write(o16(len(self.tags))) + + # always write in ascending tag order + tags = self.tags.items() + tags.sort() + + directory = [] + append = directory.append + + offset = fp.tell() + len(self.tags) * 12 + 4 + + stripoffsets = None + + # pass 1: convert tags to binary format + for tag, value in tags: + + if Image.DEBUG: + import TiffTags + tagname = TiffTags.TAGS.get(tag, "unknown") + print "save: %s (%d)" % (tagname, tag), + print "- value:", value + + if type(value[0]) is type(""): + # string data + typ = 2 + data = value = string.join(value, "\0") + "\0" + + else: + # integer data + if tag == STRIPOFFSETS: + stripoffsets = len(directory) + typ = 4 # to avoid catch-22 + elif tag in (X_RESOLUTION, Y_RESOLUTION): + # identify rational data fields + typ = 5 + else: + typ = 3 + for v in value: + if v >= 65536: + typ = 4 + if typ == 3: + data = string.join(map(o16, value), "") + else: + data = string.join(map(o32, value), "") + + # figure out if data fits into the directory + if len(data) == 4: + append((tag, typ, len(value), data, "")) + elif len(data) < 4: + append((tag, typ, len(value), data + (4-len(data))*"\0", "")) + else: + count = len(value) + if typ == 5: + count = count / 2 # adjust for rational data field + append((tag, typ, count, o32(offset), data)) + offset = offset + len(data) + if offset & 1: + offset = offset + 1 # word padding + + # update strip offset data to point beyond auxiliary data + if stripoffsets is not None: + tag, typ, count, value, data = directory[stripoffsets] + assert not data, "multistrip support not yet implemented" + value = o32(self.i32(value) + offset) + directory[stripoffsets] = tag, typ, count, value, data + + # pass 2: write directory to file + for tag, typ, count, value, data in directory: + if Image.DEBUG > 1: + print tag, typ, count, repr(value), repr(data) + fp.write(o16(tag) + o16(typ) + o32(count) + value) + fp.write("\0\0\0\0") # end of directory + + # pass 3: write auxiliary data to file + for tag, typ, count, value, data in directory: + fp.write(data) + if len(data) & 1: + fp.write("\0") + + return offset + +## +# Image plugin for TIFF files. + +class TiffImageFile(ImageFile.ImageFile): + + format = "TIFF" + format_description = "Adobe TIFF" + + def _open(self): + "Open the first image in a TIFF file" + + # Header + ifh = self.fp.read(8) + + if ifh[:4] not in PREFIXES: + raise SyntaxError, "not a TIFF file" + + # image file directory (tag dictionary) + self.tag = self.ifd = ImageFileDirectory(ifh[:2]) + + # setup frame pointers + self.__first = self.__next = self.ifd.i32(ifh, 4) + self.__frame = -1 + self.__fp = self.fp + + # and load the first frame + self._seek(0) + + def seek(self, frame): + "Select a given frame as current image" + + if frame < 0: + frame = 0 + self._seek(frame) + + def tell(self): + "Return the current frame number" + + return self._tell() + + def _seek(self, frame): + + self.fp = self.__fp + if frame < self.__frame: + # rewind file + self.__frame = -1 + self.__next = self.__first + while self.__frame < frame: + if not self.__next: + raise EOFError, "no more images in TIFF file" + self.fp.seek(self.__next) + self.tag.load(self.fp) + self.__next = self.tag.next + self.__frame = self.__frame + 1 + self._setup() + + def _tell(self): + + return self.__frame + + def _decoder(self, rawmode, layer): + "Setup decoder contexts" + + args = None + if rawmode == "RGB" and self._planar_configuration == 2: + rawmode = rawmode[layer] + compression = self._compression + if compression == "raw": + args = (rawmode, 0, 1) + elif compression == "jpeg": + args = rawmode, "" + if self.tag.has_key(JPEGTABLES): + # Hack to handle abbreviated JPEG headers + self.tile_prefix = self.tag[JPEGTABLES] + elif compression == "packbits": + args = rawmode + elif compression == "tiff_lzw": + args = rawmode + if self.tag.has_key(317): + # Section 14: Differencing Predictor + self.decoderconfig = (self.tag[PREDICTOR][0],) + + return args + + def _setup(self): + "Setup this image object based on current tags" + + if self.tag.has_key(0xBC01): + raise IOError, "Windows Media Photo files not yet supported" + + getscalar = self.tag.getscalar + + # extract relevant tags + self._compression = COMPRESSION_INFO[getscalar(COMPRESSION, 1)] + self._planar_configuration = getscalar(PLANAR_CONFIGURATION, 1) + + # photometric is a required tag, but not everyone is reading + # the specification + photo = getscalar(PHOTOMETRIC_INTERPRETATION, 0) + + fillorder = getscalar(FILLORDER, 1) + + if Image.DEBUG: + print "*** Summary ***" + print "- compression:", self._compression + print "- photometric_interpretation:", photo + print "- planar_configuration:", self._planar_configuration + print "- fill_order:", fillorder + + # size + xsize = getscalar(IMAGEWIDTH) + ysize = getscalar(IMAGELENGTH) + self.size = xsize, ysize + + if Image.DEBUG: + print "- size:", self.size + + format = getscalar(SAMPLEFORMAT, 1) + + # mode: check photometric interpretation and bits per pixel + key = ( + photo, format, fillorder, + self.tag.get(BITSPERSAMPLE, (1,)), + self.tag.get(EXTRASAMPLES, ()) + ) + if Image.DEBUG: + print "format key:", key + try: + self.mode, rawmode = OPEN_INFO[key] + except KeyError: + if Image.DEBUG: + print "- unsupported format" + raise SyntaxError, "unknown pixel mode" + + if Image.DEBUG: + print "- raw mode:", rawmode + print "- pil mode:", self.mode + + self.info["compression"] = self._compression + + xdpi = getscalar(X_RESOLUTION, (1, 1)) + ydpi = getscalar(Y_RESOLUTION, (1, 1)) + + if xdpi and ydpi and getscalar(RESOLUTION_UNIT, 1) == 1: + xdpi = xdpi[0] / (xdpi[1] or 1) + ydpi = ydpi[0] / (ydpi[1] or 1) + self.info["dpi"] = xdpi, ydpi + + # build tile descriptors + x = y = l = 0 + self.tile = [] + if self.tag.has_key(STRIPOFFSETS): + # striped image + h = getscalar(ROWSPERSTRIP, ysize) + w = self.size[0] + a = None + for o in self.tag[STRIPOFFSETS]: + if not a: + a = self._decoder(rawmode, l) + self.tile.append( + (self._compression, + (0, min(y, ysize), w, min(y+h, ysize)), + o, a)) + y = y + h + if y >= self.size[1]: + x = y = 0 + l = l + 1 + a = None + elif self.tag.has_key(324): + # tiled image + w = getscalar(322) + h = getscalar(323) + a = None + for o in self.tag[324]: + if not a: + a = self._decoder(rawmode, l) + # FIXME: this doesn't work if the image size + # is not a multiple of the tile size... + self.tile.append( + (self._compression, + (x, y, x+w, y+h), + o, a)) + x = x + w + if x >= self.size[0]: + x, y = 0, y + h + if y >= self.size[1]: + x = y = 0 + l = l + 1 + a = None + else: + if Image.DEBUG: + print "- unsupported data organization" + raise SyntaxError("unknown data organization") + + # fixup palette descriptor + if self.mode == "P": + palette = map(lambda a: chr(a / 256), self.tag[COLORMAP]) + self.palette = ImagePalette.raw("RGB;L", string.join(palette, "")) + +# +# -------------------------------------------------------------------- +# Write TIFF files + +# little endian is default + +SAVE_INFO = { + # mode => rawmode, photometrics, sampleformat, bitspersample, extra + "1": ("1", 1, 1, (1,), None), + "L": ("L", 1, 1, (8,), None), + "LA": ("LA", 1, 1, (8,8), 2), + "P": ("P", 3, 1, (8,), None), + "PA": ("PA", 3, 1, (8,8), 2), + "I": ("I;32S", 1, 2, (32,), None), + "I;16": ("I;16", 1, 1, (16,), None), + "I;16S": ("I;16S", 1, 2, (16,), None), + "F": ("F;32F", 1, 3, (32,), None), + "RGB": ("RGB", 2, 1, (8,8,8), None), + "RGBX": ("RGBX", 2, 1, (8,8,8,8), 0), + "RGBA": ("RGBA", 2, 1, (8,8,8,8), 2), + "CMYK": ("CMYK", 5, 1, (8,8,8,8), None), + "YCbCr": ("YCbCr", 6, 1, (8,8,8), None), + "LAB": ("LAB", 8, 1, (8,8,8), None), +} + +def _cvt_res(value): + # convert value to TIFF rational number -- (numerator, denominator) + if type(value) in (type([]), type(())): + assert(len(value) % 2 == 0) + return value + if type(value) == type(1): + return (value, 1) + value = float(value) + return (int(value * 65536), 65536) + +def _save(im, fp, filename): + + try: + rawmode, photo, format, bits, extra = SAVE_INFO[im.mode] + except KeyError: + raise IOError, "cannot write mode %s as TIFF" % im.mode + + ifd = ImageFileDirectory() + + # tiff header (write via IFD to get everything right) + fp.write(ifd.prefix + ifd.o16(42) + ifd.o32(8)) + + ifd[IMAGEWIDTH] = im.size[0] + ifd[IMAGELENGTH] = im.size[1] + + # additions written by Greg Couch, gregc@cgl.ucsf.edu + # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com + if hasattr(im, 'tag'): + # preserve tags from original TIFF image file + for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION): + if im.tag.tagdata.has_key(key): + ifd[key] = im.tag.tagdata.get(key) + if im.encoderinfo.has_key("description"): + ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"] + if im.encoderinfo.has_key("resolution"): + ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \ + = _cvt_res(im.encoderinfo["resolution"]) + if im.encoderinfo.has_key("x resolution"): + ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"]) + if im.encoderinfo.has_key("y resolution"): + ifd[Y_RESOLUTION] = _cvt_res(im.encoderinfo["y resolution"]) + if im.encoderinfo.has_key("resolution unit"): + unit = im.encoderinfo["resolution unit"] + if unit == "inch": + ifd[RESOLUTION_UNIT] = 2 + elif unit == "cm" or unit == "centimeter": + ifd[RESOLUTION_UNIT] = 3 + else: + ifd[RESOLUTION_UNIT] = 1 + if im.encoderinfo.has_key("software"): + ifd[SOFTWARE] = im.encoderinfo["software"] + if im.encoderinfo.has_key("date time"): + ifd[DATE_TIME] = im.encoderinfo["date time"] + if im.encoderinfo.has_key("artist"): + ifd[ARTIST] = im.encoderinfo["artist"] + if im.encoderinfo.has_key("copyright"): + ifd[COPYRIGHT] = im.encoderinfo["copyright"] + + dpi = im.encoderinfo.get("dpi") + if dpi: + ifd[RESOLUTION_UNIT] = 1 + ifd[X_RESOLUTION] = _cvt_res(dpi[0]) + ifd[Y_RESOLUTION] = _cvt_res(dpi[1]) + + if bits != (1,): + ifd[BITSPERSAMPLE] = bits + if len(bits) != 1: + ifd[SAMPLESPERPIXEL] = len(bits) + if extra is not None: + ifd[EXTRASAMPLES] = extra + if format != 1: + ifd[SAMPLEFORMAT] = format + + ifd[PHOTOMETRIC_INTERPRETATION] = photo + + if im.mode == "P": + lut = im.im.getpalette("RGB", "RGB;L") + ifd[COLORMAP] = tuple(map(lambda v: ord(v) * 256, lut)) + + # data orientation + stride = len(bits) * ((im.size[0]*bits[0]+7)/8) + ifd[ROWSPERSTRIP] = im.size[1] + ifd[STRIPBYTECOUNTS] = stride * im.size[1] + ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + ifd[COMPRESSION] = 1 # no compression + + offset = ifd.save(fp) + + ImageFile._save(im, fp, [ + ("raw", (0,0)+im.size, offset, (rawmode, stride, 1)) + ]) + +# +# -------------------------------------------------------------------- +# Register + +Image.register_open("TIFF", TiffImageFile, _accept) +Image.register_save("TIFF", _save) + +Image.register_extension("TIFF", ".tif") +Image.register_extension("TIFF", ".tiff") + +Image.register_mime("TIFF", "image/tiff") diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py new file mode 100644 index 0000000..2795fc3 --- /dev/null +++ b/PIL/TiffTags.py @@ -0,0 +1,200 @@ +# +# The Python Imaging Library. +# $Id: TiffTags.py 2340 2005-03-25 08:05:37Z fredrik $ +# +# TIFF tags +# +# This module provides clear-text names for various well-known +# TIFF tags. the TIFF codec works just fine without it. +# +# Copyright (c) Secret Labs AB 1999. +# +# See the README file for information on usage and redistribution. +# + +## +# This module provides constants and clear-text names for various +# well-known TIFF tags. +## + +## +# Map tag numbers (or tag number, tag value tuples) to tag names. + +TAGS = { + + 254: "NewSubfileType", + 255: "SubfileType", + 256: "ImageWidth", + 257: "ImageLength", + 258: "BitsPerSample", + + 259: "Compression", + (259, 1): "Uncompressed", + (259, 2): "CCITT 1d", + (259, 3): "Group 3 Fax", + (259, 4): "Group 4 Fax", + (259, 5): "LZW", + (259, 6): "JPEG", + (259, 32773): "PackBits", + + 262: "PhotometricInterpretation", + (262, 0): "WhiteIsZero", + (262, 1): "BlackIsZero", + (262, 2): "RGB", + (262, 3): "RGB Palette", + (262, 4): "Transparency Mask", + (262, 5): "CMYK", + (262, 6): "YCbCr", + (262, 8): "CieLAB", + (262, 32803): "CFA", # TIFF/EP, Adobe DNG + (262, 32892): "LinearRaw", # Adobe DNG + + 263: "Thresholding", + 264: "CellWidth", + 265: "CellHeight", + 266: "FillOrder", + 269: "DocumentName", + + 270: "ImageDescription", + 271: "Make", + 272: "Model", + 273: "StripOffsets", + 274: "Orientation", + 277: "SamplesPerPixel", + 278: "RowsPerStrip", + 279: "StripByteCounts", + + 280: "MinSampleValue", + 281: "MaxSampleValue", + 282: "XResolution", + 283: "YResolution", + 284: "PlanarConfiguration", + (284, 1): "Contigous", + (284, 2): "Separate", + + 285: "PageName", + 286: "XPosition", + 287: "YPosition", + 288: "FreeOffsets", + 289: "FreeByteCounts", + + 290: "GrayResponseUnit", + 291: "GrayResponseCurve", + 292: "T4Options", + 293: "T6Options", + 296: "ResolutionUnit", + 297: "PageNumber", + + 301: "TransferFunction", + 305: "Software", + 306: "DateTime", + + 315: "Artist", + 316: "HostComputer", + 317: "Predictor", + 318: "WhitePoint", + 319: "PrimaryChromaticies", + + 320: "ColorMap", + 321: "HalftoneHints", + 322: "TileWidth", + 323: "TileLength", + 324: "TileOffsets", + 325: "TileByteCounts", + + 332: "InkSet", + 333: "InkNames", + 334: "NumberOfInks", + 336: "DotRange", + 337: "TargetPrinter", + 338: "ExtraSamples", + 339: "SampleFormat", + + 340: "SMinSampleValue", + 341: "SMaxSampleValue", + 342: "TransferRange", + + 347: "JPEGTables", + + # obsolete JPEG tags + 512: "JPEGProc", + 513: "JPEGInterchangeFormat", + 514: "JPEGInterchangeFormatLength", + 515: "JPEGRestartInterval", + 517: "JPEGLosslessPredictors", + 518: "JPEGPointTransforms", + 519: "JPEGQTables", + 520: "JPEGDCTables", + 521: "JPEGACTables", + + 529: "YCbCrCoefficients", + 530: "YCbCrSubSampling", + 531: "YCbCrPositioning", + 532: "ReferenceBlackWhite", + + 33432: "Copyright", + + # various extensions (should check specs for "official" names) + 33723: "IptcNaaInfo", + 34377: "PhotoshopInfo", + + # Adobe DNG + 50706: "DNGVersion", + 50707: "DNGBackwardVersion", + 50708: "UniqueCameraModel", + 50709: "LocalizedCameraModel", + 50710: "CFAPlaneColor", + 50711: "CFALayout", + 50712: "LinearizationTable", + 50713: "BlackLevelRepeatDim", + 50714: "BlackLevel", + 50715: "BlackLevelDeltaH", + 50716: "BlackLevelDeltaV", + 50717: "WhiteLevel", + 50718: "DefaultScale", + 50741: "BestQualityScale", + 50719: "DefaultCropOrigin", + 50720: "DefaultCropSize", + 50778: "CalibrationIlluminant1", + 50779: "CalibrationIlluminant2", + 50721: "ColorMatrix1", + 50722: "ColorMatrix2", + 50723: "CameraCalibration1", + 50724: "CameraCalibration2", + 50725: "ReductionMatrix1", + 50726: "ReductionMatrix2", + 50727: "AnalogBalance", + 50728: "AsShotNeutral", + 50729: "AsShotWhiteXY", + 50730: "BaselineExposure", + 50731: "BaselineNoise", + 50732: "BaselineSharpness", + 50733: "BayerGreenSplit", + 50734: "LinearResponseLimit", + 50735: "CameraSerialNumber", + 50736: "LensInfo", + 50737: "ChromaBlurRadius", + 50738: "AntiAliasStrength", + 50740: "DNGPrivateData", + 50741: "MakerNoteSafety", +} + +## +# Map type numbers to type names. + +TYPES = { + + 1: "byte", + 2: "ascii", + 3: "short", + 4: "long", + 5: "rational", + 6: "signed byte", + 7: "undefined", + 8: "signed short", + 9: "signed long", + 10: "signed rational", + 11: "float", + 12: "double", + +} diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py new file mode 100644 index 0000000..34e7fc2 --- /dev/null +++ b/PIL/WalImageFile.py @@ -0,0 +1,125 @@ +# +# The Python Imaging Library. +# $Id: WalImageFile.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# WAL file handling +# +# History: +# 2003-04-23 fl created +# +# Copyright (c) 2003 by Fredrik Lundh. +# +# See the README file for information on usage and redistribution. +# + +# NOTE: This format cannot be automatically recognized, so the reader +# is not registered for use with Image.open(). To open a WEL file, use +# the WalImageFile.open() function instead. + +# This reader is based on the specification available from: +# http://www.flipcode.com/tutorials/tut_q2levels.shtml +# and has been tested with a few sample files found using google. + +import Image + +def i32(c, o=0): + return ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24) + +## +# Load texture from a Quake2 WAL texture file. +#

+# By default, a Quake2 standard palette is attached to the texture. +# To override the palette, use the putpalette method. +# +# @param filename WAL file name, or an opened file handle. +# @return An image instance. + +def open(filename): + # FIXME: modify to return a WalImageFile instance instead of + # plain Image object ? + + if hasattr(filename, "read"): + fp = filename + else: + import __builtin__ + fp = __builtin__.open(filename, "rb") + + # read header fields + header = fp.read(32+24+32+12) + size = i32(header, 32), i32(header, 36) + offset = i32(header, 40) + + # load pixel data + fp.seek(offset) + + im = Image.fromstring("P", size, fp.read(size[0] * size[1])) + im.putpalette(quake2palette) + + im.format = "WAL" + im.format_description = "Quake2 Texture" + + # strings are null-terminated + im.info["name"] = header[:32].split("\0", 1)[0] + next_name = header[56:56+32].split("\0", 1)[0] + if next_name: + im.info["next_name"] = next_name + + return im + + +quake2palette = ( + # default palette taken from piffo 0.93 by Hans Häggström + "\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" + "\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" + "\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" + "\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b" + "\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10" + "\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07" + "\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f" + "\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16" + "\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d" + "\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31" + "\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28" + "\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07" + "\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27" + "\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b" + "\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01" + "\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21" + "\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14" + "\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07" + "\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14" + "\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f" + "\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34" + "\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d" + "\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14" + "\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01" + "\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24" + "\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10" + "\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01" + "\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27" + "\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c" + "\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a" + "\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26" + "\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d" + "\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01" + "\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20" + "\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17" + "\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07" + "\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25" + "\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c" + "\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01" + "\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23" + "\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f" + "\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b" + "\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37" + "\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b" + "\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01" + "\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10" + "\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" + "\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" +) + +if __name__ == "__main__": + im = open("../hacks/sample.wal") + print im.info, im.mode, im.size + im.save("../out.png") diff --git a/PIL/WmfImagePlugin.py b/PIL/WmfImagePlugin.py new file mode 100644 index 0000000..2191160 --- /dev/null +++ b/PIL/WmfImagePlugin.py @@ -0,0 +1,148 @@ +# +# The Python Imaging Library +# $Id: WmfImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# WMF stub codec +# +# history: +# 1996-12-14 fl Created +# 2004-02-22 fl Turned into a stub driver +# 2004-02-23 fl Added EMF support +# +# Copyright (c) Secret Labs AB 1997-2004. All rights reserved. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.2" + +import Image, ImageFile + +_handler = None + +## +# Install application-specific WMF image handler. +# +# @param handler Handler object. + +def register_handler(handler): + global _handler + _handler = handler + +# -------------------------------------------------------------------- + +def word(c, o=0): + return ord(c[o]) + (ord(c[o+1])<<8) + +def short(c, o=0): + v = ord(c[o]) + (ord(c[o+1])<<8) + if v >= 32768: + v = v - 65536 + return v + +def dword(c, o=0): + return ord(c[o]) + (ord(c[o+1])<<8) + (ord(c[o+2])<<16) + (ord(c[o+3])<<24) + +def long(c, o=0): + return dword(c, o) + +# +# -------------------------------------------------------------------- +# Read WMF file + +def _accept(prefix): + return ( + prefix[:6] == "\xd7\xcd\xc6\x9a\x00\x00" or + prefix[:4] == "\x01\x00\x00\x00" + ) + +## +# Image plugin for Windows metafiles. + +class WmfStubImageFile(ImageFile.StubImageFile): + + format = "WMF" + format_description = "Windows Metafile" + + def _open(self): + + # check placable header + s = self.fp.read(80) + + if s[:6] == "\xd7\xcd\xc6\x9a\x00\x00": + + # placeable windows metafile + + # get units per inch + inch = word(s, 14) + + # get bounding box + x0 = short(s, 6); y0 = short(s, 8) + x1 = short(s, 10); y1 = short(s, 12) + + # normalize size to 72 dots per inch + size = (x1 - x0) * 72 / inch, (y1 - y0) * 72 / inch + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + self.info["dpi"] = 72 + + # print self.mode, self.size, self.info + + # sanity check (standard metafile header) + if s[22:26] != "\x01\x00\t\x00": + raise SyntaxError("Unsupported WMF file format") + + elif long(s) == 1 and s[40:44] == " EMF": + # enhanced metafile + + # get bounding box + x0 = long(s, 8); y0 = long(s, 12) + x1 = long(s, 16); y1 = long(s, 20) + + # get frame (in 0.01 millimeter units) + frame = long(s, 24), long(s, 28), long(s, 32), long(s, 36) + + # normalize size to 72 dots per inch + size = x1 - x0, y1 - y0 + + # calculate dots per inch from bbox and frame + xdpi = 2540 * (x1 - y0) / (frame[2] - frame[0]) + ydpi = 2540 * (y1 - y0) / (frame[3] - frame[1]) + + self.info["wmf_bbox"] = x0, y0, x1, y1 + + if xdpi == ydpi: + self.info["dpi"] = xdpi + else: + self.info["dpi"] = xdpi, ydpi + + else: + raise SyntaxError("Unsupported file format") + + self.mode = "RGB" + self.size = size + + loader = self._load() + if loader: + loader.open(self) + + def _load(self): + return _handler + + +def _save(im, fp, filename): + if _handler is None or not hasattr("_handler", "save"): + raise IOError("WMF save handler not installed") + _handler.save(im, fp, filename) + +# +# -------------------------------------------------------------------- +# Registry stuff + +Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept) +Image.register_save(WmfStubImageFile.format, _save) + +Image.register_extension(WmfStubImageFile.format, ".wmf") +Image.register_extension(WmfStubImageFile.format, ".emf") diff --git a/PIL/XVThumbImagePlugin.py b/PIL/XVThumbImagePlugin.py new file mode 100644 index 0000000..4a9c0d6 --- /dev/null +++ b/PIL/XVThumbImagePlugin.py @@ -0,0 +1,68 @@ +# +# The Python Imaging Library. +# $Id: XVThumbImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# XV Thumbnail file handler by Charles E. "Gene" Cash +# (gcash@magicnet.net) +# +# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV, +# available from ftp://ftp.cis.upenn.edu/pub/xv/ +# +# history: +# 98-08-15 cec created (b/w only) +# 98-12-09 cec added color palette +# 98-12-28 fl added to PIL (with only a few very minor modifications) +# +# To do: +# FIXME: make save work (this requires quantization support) +# + +__version__ = "0.1" + +import string +import Image, ImageFile, ImagePalette + +# standard color palette for thumbnails (RGB332) +PALETTE = "" +for r in range(8): + for g in range(8): + for b in range(4): + PALETTE = PALETTE + (chr((r*255)/7)+chr((g*255)/7)+chr((b*255)/3)) + +## +# Image plugin for XV thumbnail images. + +class XVThumbImageFile(ImageFile.ImageFile): + + format = "XVThumb" + format_description = "XV thumbnail image" + + def _open(self): + + # check magic + s = self.fp.read(6) + if s != "P7 332": + raise SyntaxError, "not an XV thumbnail file" + + # skip info comments + while 1: + s = string.strip(self.fp.readline()) + if s == "#END_OF_COMMENTS": + break + + # read header line + s = string.split(self.fp.readline()) + + self.mode = "P" + self.size = int(s[0]), int(s[1]) + + self.palette = ImagePalette.raw("RGB", PALETTE) + + self.tile = [ + ("raw", (0, 0)+self.size, + self.fp.tell(), (self.mode, 0, 1) + )] + +# -------------------------------------------------------------------- + +Image.register_open("XVThumb", XVThumbImageFile) diff --git a/PIL/XbmImagePlugin.py b/PIL/XbmImagePlugin.py new file mode 100644 index 0000000..a1dccea --- /dev/null +++ b/PIL/XbmImagePlugin.py @@ -0,0 +1,94 @@ +# +# The Python Imaging Library. +# $Id: XbmImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# XBM File handling +# +# History: +# 1995-09-08 fl Created +# 1996-11-01 fl Added save support +# 1997-07-07 fl Made header parser more tolerant +# 1997-07-22 fl Fixed yet another parser bug +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) +# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog) +# 2004-02-24 fl Allow some whitespace before first #define +# +# Copyright (c) 1997-2004 by Secret Labs AB +# Copyright (c) 1996-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +__version__ = "0.6" + +import re, string +import Image, ImageFile + +# XBM header +xbm_head = re.compile( + "\s*#define[ \t]+[^_]*_width[ \t]+(?P[0-9]+)[\r\n]+" + "#define[ \t]+[^_]*_height[ \t]+(?P[0-9]+)[\r\n]+" + "(?P" + "#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" + "#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" + ")?" + "[\\000-\\377]*_bits\\[\\]" +) + +def _accept(prefix): + return string.lstrip(prefix)[:7] == "#define" + +## +# Image plugin for X11 bitmaps. + +class XbmImageFile(ImageFile.ImageFile): + + format = "XBM" + format_description = "X11 Bitmap" + + def _open(self): + + m = xbm_head.match(self.fp.read(512)) + + if m: + + xsize = int(m.group("width")) + ysize = int(m.group("height")) + + if m.group("hotspot"): + self.info["hotspot"] = ( + int(m.group("xhot")), int(m.group("yhot")) + ) + + self.mode = "1" + self.size = xsize, ysize + + self.tile = [("xbm", (0, 0)+self.size, m.end(), None)] + + +def _save(im, fp, filename): + + if im.mode != "1": + raise IOError, "cannot write mode %s as XBM" % im.mode + + fp.write("#define im_width %d\n" % im.size[0]) + fp.write("#define im_height %d\n" % im.size[1]) + + hotspot = im.encoderinfo.get("hotspot") + if hotspot: + fp.write("#define im_x_hot %d\n" % hotspot[0]) + fp.write("#define im_y_hot %d\n" % hotspot[1]) + + fp.write("static char im_bits[] = {\n") + + ImageFile._save(im, fp, [("xbm", (0,0)+im.size, 0, None)]) + + fp.write("};\n") + + +Image.register_open("XBM", XbmImageFile, _accept) +Image.register_save("XBM", _save) + +Image.register_extension("XBM", ".xbm") + +Image.register_mime("XBM", "image/xbm") diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py new file mode 100644 index 0000000..b8ac01f --- /dev/null +++ b/PIL/XpmImagePlugin.py @@ -0,0 +1,129 @@ +# +# The Python Imaging Library. +# $Id: XpmImagePlugin.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# XPM File handling +# +# History: +# 1996-12-29 fl Created +# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7) +# +# Copyright (c) Secret Labs AB 1997-2001. +# Copyright (c) Fredrik Lundh 1996-2001. +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.2" + + +import re, string +import Image, ImageFile, ImagePalette + +# XPM header +xpm_head = re.compile("\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)") + + +def _accept(prefix): + return prefix[:9] == "/* XPM */" + +## +# Image plugin for X11 pixel maps. + +class XpmImageFile(ImageFile.ImageFile): + + format = "XPM" + format_description = "X11 Pixel Map" + + def _open(self): + + if not _accept(self.fp.read(9)): + raise SyntaxError, "not an XPM file" + + # skip forward to next string + while 1: + s = self.fp.readline() + if not s: + raise SyntaxError, "broken XPM file" + m = xpm_head.match(s) + if m: + break + + self.size = int(m.group(1)), int(m.group(2)) + + pal = int(m.group(3)) + bpp = int(m.group(4)) + + if pal > 256 or bpp != 1: + raise ValueError, "cannot read this XPM file" + + # + # load palette description + + palette = ["\0\0\0"] * 256 + + for i in range(pal): + + s = self.fp.readline() + if s[-2:] == '\r\n': + s = s[:-2] + elif s[-1:] in '\r\n': + s = s[:-1] + + c = ord(s[1]) + s = string.split(s[2:-2]) + + for i in range(0, len(s), 2): + + if s[i] == "c": + + # process colour key + rgb = s[i+1] + if rgb == "None": + self.info["transparency"] = c + elif rgb[0] == "#": + # FIXME: handle colour names (see ImagePalette.py) + rgb = string.atoi(rgb[1:], 16) + palette[c] = chr((rgb >> 16) & 255) +\ + chr((rgb >> 8) & 255) +\ + chr(rgb & 255) + else: + # unknown colour + raise ValueError, "cannot read this XPM file" + break + + else: + + # missing colour key + raise ValueError, "cannot read this XPM file" + + self.mode = "P" + self.palette = ImagePalette.raw("RGB", string.join(palette, "")) + + self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), ("P", 0, 1))] + + def load_read(self, bytes): + + # + # load all image data in one chunk + + xsize, ysize = self.size + + s = [None] * ysize + + for i in range(ysize): + s[i] = string.ljust(self.fp.readline()[1:xsize+1], xsize) + + self.fp = None + + return string.join(s, "") + +# +# Registry + +Image.register_open("XPM", XpmImageFile, _accept) + +Image.register_extension("XPM", ".xpm") + +Image.register_mime("XPM", "image/xpm") diff --git a/PIL/__init__.py b/PIL/__init__.py new file mode 100644 index 0000000..3290883 --- /dev/null +++ b/PIL/__init__.py @@ -0,0 +1,12 @@ +# +# The Python Imaging Library. +# $Id: __init__.py 2134 2004-10-06 08:55:20Z fredrik $ +# +# package placeholder +# +# Copyright (c) 1999 by Secret Labs AB. +# +# See the README file for information on usage and redistribution. +# + +# ;-) diff --git a/ProgressDialog.py b/ProgressDialog.py new file mode 100644 index 0000000..6d58ef5 --- /dev/null +++ b/ProgressDialog.py @@ -0,0 +1,19 @@ +import gtk +from gettext import gettext as _ + + +class ProgressDialog(gtk.Dialog): + + def __init__(self, parent): + gtk.Dialog.__init__(self, _('Downloading...'), parent, \ + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, \ + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)) + self._activity = parent + self.connect('response', self._response_cb) + self._pb = gtk.ProgressBar() + self._pb.set_text(_('Retrieving shared image, please wait...')) + self.vbox.add(self._pb) + def _response_cb(self, dialog, response_id): + if response_id == gtk.RESPONSE_REJECT : self._activity.close() + else: pass + def set_fraction(self, fraction) : self._pb.set_fraction(fraction) diff --git a/activity/activity-imageprocessor.svg b/activity/activity-imageprocessor.svg new file mode 100644 index 0000000..7a011e3 --- /dev/null +++ b/activity/activity-imageprocessor.svg @@ -0,0 +1,296 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..274972e --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,19 @@ +[Activity] +sweet = imageprocessor +name = Image Processor +summary = The Image Processor activity is a simple and fast image processor tool +description = It has features one would expect of a standard image processor, + like filtering, merging, watermarkin text and images over images, etc and also image viewer functions. +homepage = http://wiki.sugarlabs.org/go/Activities/Image_Processor +license = GPLv2+ + +icon = activity-imageprocessor +exec = sugar-activity ImageProcessorActivity.ImageProcessorActivity +mime_types = image/bmp;image/gif;image/jpeg;image/png;image/tiff;image/svg+xml + +version = 1 +stability = testing + +# support original activity.info fields +activity_version = 1 +bundle_id = org.laptop.ImageProcessorActivity diff --git a/icons/blur.svg b/icons/blur.svg new file mode 100644 index 0000000..de2a505 --- /dev/null +++ b/icons/blur.svg @@ -0,0 +1,91 @@ + + + + + + + + + + image/svg+xml + + + + + + + B + + + + diff --git a/icons/cam.svg b/icons/cam.svg new file mode 100644 index 0000000..857f037 --- /dev/null +++ b/icons/cam.svg @@ -0,0 +1,105 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/icons/contour.svg b/icons/contour.svg new file mode 100644 index 0000000..3ca6972 --- /dev/null +++ b/icons/contour.svg @@ -0,0 +1,81 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/icons/copy.svg b/icons/copy.svg new file mode 100644 index 0000000..87eed71 --- /dev/null +++ b/icons/copy.svg @@ -0,0 +1,90 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/icons/embross.svg b/icons/embross.svg new file mode 100644 index 0000000..2e15a5b --- /dev/null +++ b/icons/embross.svg @@ -0,0 +1,109 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + E + + + + diff --git a/icons/finedges.svg b/icons/finedges.svg new file mode 100644 index 0000000..3b16d4e --- /dev/null +++ b/icons/finedges.svg @@ -0,0 +1,100 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + f + + + diff --git a/icons/grey.svg b/icons/grey.svg new file mode 100644 index 0000000..4eb8472 --- /dev/null +++ b/icons/grey.svg @@ -0,0 +1,94 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + G + + + diff --git a/icons/invert.svg b/icons/invert.svg new file mode 100644 index 0000000..f4bb71a --- /dev/null +++ b/icons/invert.svg @@ -0,0 +1,104 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + I + + + diff --git a/icons/left_bottom.svg b/icons/left_bottom.svg new file mode 100644 index 0000000..8018d15 --- /dev/null +++ b/icons/left_bottom.svg @@ -0,0 +1,274 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + L-B + + text edit + + + + + + + + + + + + + + + + + diff --git a/icons/left_top.svg b/icons/left_top.svg new file mode 100644 index 0000000..e5033f5 --- /dev/null +++ b/icons/left_top.svg @@ -0,0 +1,297 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + L-T + text edit + + + + + + + + + + + + + + + diff --git a/icons/mirror.svg b/icons/mirror.svg new file mode 100644 index 0000000..4a90bf0 --- /dev/null +++ b/icons/mirror.svg @@ -0,0 +1,116 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + M + + + + diff --git a/icons/offset.svg b/icons/offset.svg new file mode 100644 index 0000000..87f89ed --- /dev/null +++ b/icons/offset.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + o + + + diff --git a/icons/original.svg b/icons/original.svg new file mode 100644 index 0000000..78c5be8 --- /dev/null +++ b/icons/original.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + U + + diff --git a/icons/paste.svg b/icons/paste.svg new file mode 100644 index 0000000..ce04736 --- /dev/null +++ b/icons/paste.svg @@ -0,0 +1,141 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + a + + + + + + + + diff --git a/icons/right_bottom.svg b/icons/right_bottom.svg new file mode 100644 index 0000000..1131ac9 --- /dev/null +++ b/icons/right_bottom.svg @@ -0,0 +1,280 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + text edit + R-B + + + + + + + + + + + + + + + + diff --git a/icons/right_top.svg b/icons/right_top.svg new file mode 100644 index 0000000..bba9810 --- /dev/null +++ b/icons/right_top.svg @@ -0,0 +1,314 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + text edit + + R-T + + + + + + + + + + + + + + diff --git a/icons/rotate_anticlockwise.svg b/icons/rotate_anticlockwise.svg new file mode 100644 index 0000000..55a9c61 --- /dev/null +++ b/icons/rotate_anticlockwise.svg @@ -0,0 +1,71 @@ + + +image/svg+xml + + + + + + + + \ No newline at end of file diff --git a/icons/rotate_clockwise.svg b/icons/rotate_clockwise.svg new file mode 100644 index 0000000..474f6f0 --- /dev/null +++ b/icons/rotate_clockwise.svg @@ -0,0 +1,71 @@ + + +image/svg+xml + + + + + + + + \ No newline at end of file diff --git a/icons/sharpen.svg b/icons/sharpen.svg new file mode 100644 index 0000000..c6b9524 --- /dev/null +++ b/icons/sharpen.svg @@ -0,0 +1,269 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/solarize.svg b/icons/solarize.svg new file mode 100644 index 0000000..fcd3abe --- /dev/null +++ b/icons/solarize.svg @@ -0,0 +1,109 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + S + + + + diff --git a/icons/watermark_scale.svg b/icons/watermark_scale.svg new file mode 100644 index 0000000..292bba2 --- /dev/null +++ b/icons/watermark_scale.svg @@ -0,0 +1,110 @@ + + + + + + + + + + image/svg+xml + + + + + + + + scale + + + + + + + + diff --git a/icons/watermark_tile.svg b/icons/watermark_tile.svg new file mode 100644 index 0000000..84a5547 --- /dev/null +++ b/icons/watermark_tile.svg @@ -0,0 +1,110 @@ + + + + + + + + + + image/svg+xml + + + + + + + + tile + + + + + + + + diff --git a/icons/watermark_tl.svg b/icons/watermark_tl.svg new file mode 100644 index 0000000..aacac3c --- /dev/null +++ b/icons/watermark_tl.svg @@ -0,0 +1,110 @@ + + + + + + + + + + image/svg+xml + + + + + + + + top-left + + + + + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..df4ce0d --- /dev/null +++ b/setup.py @@ -0,0 +1,6 @@ +import os +os.system("find ./ | sed 's,^./,ImageProcessor.activity/,g' > MANIFEST") +os.chdir('..') +os.system('zip -r ImageProcessor.xo ImageProcessor.activity') +os.system('mv ImageProcessor.xo ./ImageProcessor.activity') +os.chdir('ImageProcessor.activity') diff --git a/toolbar.py b/toolbar.py new file mode 100644 index 0000000..e0bf81c --- /dev/null +++ b/toolbar.py @@ -0,0 +1,355 @@ +# Copyright (C) 2006, Red Hat, Inc. +#Author: Keshav Sharma & Vaibhav Sharma +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from gettext import gettext as _ +import re + +import pango +import gobject +import gtk +import evince + +try: + import epubadapter +except: + pass + +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toggletoolbutton import ToggleToolButton +from sugar.graphics.menuitem import MenuItem +from sugar.graphics import iconentry +from sugar.activity import activity +from sugar.graphics.icon import Icon +from sugar.graphics.xocolor import XoColor + + +class ViewToolbar(gtk.Toolbar): + __gtype_name__ = 'ViewToolbar' + + __gsignals__ = { + 'zoom_in': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'zoom_out': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'zoom_to_fit': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'zoom_original': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'rotate_clockwise': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'rotate_anticlockwise': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'copy': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'paste': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self._zoom_out_button = None + self._zoom_in_button = None + + self._zoom_out_button = ToolButton('zoom-out') + self._zoom_out_button.set_tooltip(_('Zoom out')) + self._zoom_out_button.connect('clicked', self.zoom_out_cb) + self.insert(self._zoom_out_button, -1) + self._zoom_out_button.show() + + self._zoom_in_button = ToolButton('zoom-in') + self._zoom_in_button.set_tooltip(_('Zoom in')) + self._zoom_in_button.connect('clicked', self.zoom_in_cb) + self.insert(self._zoom_in_button, -1) + self._zoom_in_button.show() + + zoom_tofit_button = ToolButton('zoom-best-fit') + zoom_tofit_button.set_tooltip(_('Fit to window')) + zoom_tofit_button.connect('clicked', self.zoom_to_fit_cb) + self.insert(zoom_tofit_button, -1) + zoom_tofit_button.show() + + zoom_original_button = ToolButton('zoom-original') + zoom_original_button.set_tooltip(_('Original size')) + zoom_original_button.connect('clicked', self.zoom_original_cb) + self.insert(zoom_original_button, -1) + zoom_original_button.show() + + spacer = gtk.SeparatorToolItem() + spacer.props.draw = False + self.insert(spacer, -1) + spacer.show() + + rotate_anticlockwise_button = ToolButton('rotate_anticlockwise') + rotate_anticlockwise_button.set_tooltip(_('Rotate anticlockwise')) + rotate_anticlockwise_button.connect('clicked', + self.rotate_anticlockwise_cb) + self.insert(rotate_anticlockwise_button, -1) + rotate_anticlockwise_button.show() + + rotate_clockwise_button = ToolButton('rotate_clockwise') + rotate_clockwise_button.set_tooltip(_('Rotate clockwise')) + rotate_clockwise_button.connect('clicked', self.rotate_clockwise_cb) + self.insert(rotate_clockwise_button, -1) + rotate_clockwise_button.show() + + copy_button = ToolButton('copy') + copy_button.set_tooltip(_('copy image')) + copy_button.connect('clicked', self.copy_cb) + self.insert(copy_button, -1) + copy_button.show() + + paste_button = ToolButton('paste') + paste_button.set_tooltip(_('get back copied')) + paste_button.connect('clicked', self.paste_cb) + self.insert(paste_button, -1) + paste_button.show() + + def zoom_in_cb(self, button): + self.emit('zoom_in') + + def zoom_out_cb(self, button): + self.emit('zoom_out') + + def zoom_to_fit_cb(self, button): + self.emit('zoom_to_fit') + def zoom_original_cb(self, button): + self.emit('zoom_original') + def rotate_clockwise_cb(self, button): + self.emit('rotate_clockwise') + def rotate_anticlockwise_cb(self, button): + self.emit('rotate_anticlockwise') + def copy_cb(self, button): + self.emit('copy') + def paste_cb(self, button): + self.emit('paste') + + def set_activity(self, activity): + self.activity = activity + +class EditToolbar(gtk.Toolbar): + __gtype_name__ = 'EditToolbar' + + __gsignals__ = { + 'grey': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'blur': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'transpose': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'offset': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'contour': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'text': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'finedges': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'solarize': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'invert': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'watermark_tl': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'watermark_tile': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'watermark_scale': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'ambross': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'left_top': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'right_top': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'left_bottom': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'right_bottom': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + 'sharpen': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + + } + + def __init__(self): + gtk.Toolbar.__init__(self) + + self.grey = ToolButton('grey') + self.grey.set_tooltip(_('grey')) + self.grey.connect('clicked', self.grey_cb) + self.insert(self.grey, -1) + self.grey.show() + + self.blur = ToolButton('blur') + self.blur.set_tooltip(_('blur')) + self.blur.connect('clicked', self.blur_cb) + self.insert(self.blur, -1) + self.blur.show() + + self.left_top = ToolButton('left_top') + self.left_top.set_tooltip(_('left_top')) + self.left_top.connect('clicked', self.left_top_cb) + self.insert(self.left_top, -1) + self.left_top.show() + + self.right_top = ToolButton('right_top') + self.right_top.set_tooltip(_('right_top')) + self.right_top.connect('clicked', self.right_top_cb) + self.insert(self.right_top, -1) + self.right_top.show() + + self.left_bottom = ToolButton('left_bottom') + self.left_bottom.set_tooltip(_('left_bottom')) + self.left_bottom.connect('clicked', self.left_bottom_cb) + self.insert(self.left_bottom, -1) + self.left_bottom.show() + + self.right_bottom = ToolButton('right_bottom') + self.right_bottom.set_tooltip(_('right_bottom')) + self.right_bottom.connect('clicked', self.right_bottom_cb) + self.insert(self.right_bottom, -1) + self.right_bottom.show() + + self.watermark_tl = ToolButton('watermark_tl') + self.watermark_tl.set_tooltip(_('watermark top left')) + self.watermark_tl.connect('clicked', self.watermark_tl_cb) + self.insert(self.watermark_tl, -1) + self.watermark_tl.show() + + self.watermark_til = ToolButton('watermark_tile') + self.watermark_til.set_tooltip(_('watermark tile form')) + self.watermark_til.connect('clicked', self.watermark_tile_cb) + self.insert(self.watermark_til, -1) + self.watermark_til.show() + + self.watermark_scale = ToolButton('watermark_scale') + self.watermark_scale.set_tooltip(_('watermark scale form')) + self.watermark_scale.connect('clicked', self.watermark_scale_cb) + self.insert(self.watermark_scale, -1) + self.watermark_scale.show() + + self.transpose = ToolButton('mirror') + self.transpose.set_tooltip(_('mirror')) + self.transpose.connect('clicked', self.transpose_cb) + self.insert(self.transpose, -1) + self.transpose.show() + + self.offset = ToolButton('offset') + self.offset.set_tooltip(_('offset')) + self.offset.connect('clicked', self.offset_cb) + self.insert(self.offset, -1) + self.offset.show() + + self.contour = ToolButton('contour') + self.contour.set_tooltip(_('contour')) + self.contour.connect('clicked', self.contour_cb) + self.insert(self.contour, -1) + self.contour.show() + + self.finedges = ToolButton('finedges') + self.finedges.set_tooltip(_('findedges')) + self.finedges.connect('clicked', self.finedges_cb) + self.insert(self.finedges, -1) + self.finedges.show() + + self.solarize = ToolButton('solarize') + self.solarize.set_tooltip(_('solarize')) + self.solarize.connect('clicked', self.solarize_cb) + self.insert(self.solarize, -1) + self.solarize.show() + + self.invert = ToolButton('invert') + self.invert.set_tooltip(_('invert')) + self.invert.connect('clicked', self.invert_cb) + self.insert(self.invert, -1) + self.invert.show() + + self.ambross = ToolButton('embross') + self.ambross.set_tooltip(_('emboss')) + self.ambross.connect('clicked', self.ambross_cb) + self.insert(self.ambross, -1) + self.ambross.show() + + self.sharpen = ToolButton('sharpen') + self.sharpen.set_tooltip(_('sharpen')) + self.sharpen.connect('clicked', self.sharpen_cb) + self.insert(self.sharpen, -1) + self.sharpen.show() + + + def grey_cb(self, button): + self.emit('grey') + def blur_cb(self, button): + self.emit('blur') + def transpose_cb(self, button): + self.emit('transpose') + def offset_cb(self, button): + self.emit('offset') + def contour_cb(self, button): + self.emit('contour') + def finedges_cb(self, button): + self.emit('finedges') + def solarize_cb(self, button): + self.emit('solarize') + def invert_cb(self, button): + self.emit('invert') + def watermark_tl_cb(self, button): + self.emit('watermark_tl') + def watermark_tile_cb(self, button): + self.emit('watermark_tile') + def watermark_scale_cb(self, button): + self.emit('watermark_scale') + def ambross_cb(self, button): + self.emit('ambross') + def left_top_cb(self, button): + self.emit('left_top') + def right_top_cb(self, button): + self.emit('right_top') + def left_bottom_cb(self, button): + self.emit('left_bottom') + def right_bottom_cb(self, button): + self.emit('right_bottom') + def sharpen_cb(self, button): + self.emit('sharpen') + + -- cgit v0.9.1