Melodia/Melodia/resources/audiotools/__image__.py

539 lines
18 KiB
Python

#!/usr/bin/python
#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011 Brian Langenberger
#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 Street, Fifth Floor, Boston, MA 02110-1301 USA
from audiotools import Con
import imghdr
import cStringIO
import gettext
gettext.install("audiotools", unicode=True)
def __jpeg__(h, f):
if (h[0:3] == "FFD8FF".decode('hex')):
return 'jpeg'
else:
return None
imghdr.tests.append(__jpeg__)
def image_metrics(file_data):
"""Returns an ImageMetrics subclass from a string of file data.
Raises InvalidImage if there is an error parsing the file
or its type is unknown."""
header = imghdr.what(None, file_data)
file = cStringIO.StringIO(file_data)
try:
if (header == 'jpeg'):
return __JPEG__.parse(file)
elif (header == 'png'):
return __PNG__.parse(file)
elif (header == 'gif'):
return __GIF__.parse(file)
elif (header == 'bmp'):
return __BMP__.parse(file)
elif (header == 'tiff'):
return __TIFF__.parse(file)
else:
raise InvalidImage(_(u'Unknown image type'))
finally:
file.close()
#######################
#JPEG
#######################
class ImageMetrics:
"""A container for image data."""
def __init__(self, width, height, bits_per_pixel, color_count, mime_type):
"""Fields are as follows:
width - image width as an integer number of pixels
height - image height as an integer number of pixels
bits_per_pixel - the number of bits per pixel as an integer
color_count - for palette-based images, the total number of colors
mime_type - the image's MIME type, as a string
All of the ImageMetrics subclasses implement these fields.
In addition, they all implement a parse() classmethod
used to parse binary string data and return something
ImageMetrics compatible.
"""
self.width = width
self.height = height
self.bits_per_pixel = bits_per_pixel
self.color_count = color_count
self.mime_type = mime_type
def __repr__(self):
return "ImageMetrics(%s,%s,%s,%s,%s)" % \
(repr(self.width),
repr(self.height),
repr(self.bits_per_pixel),
repr(self.color_count),
repr(self.mime_type))
class InvalidImage(Exception):
"""Raised if an image cannot be parsed correctly."""
def __init__(self, err):
self.err = unicode(err)
def __unicode__(self):
return self.err
class InvalidJPEG(InvalidImage):
"""Raised if a JPEG cannot be parsed correctly."""
pass
class __JPEG__(ImageMetrics):
SEGMENT_HEADER = Con.Struct('segment_header',
Con.Const(Con.Byte('header'), 0xFF),
Con.Byte('type'),
Con.If(
lambda ctx: ctx['type'] not in (0xD8, 0xD9),
Con.UBInt16('length')))
APP0 = Con.Struct('JFIF_segment_marker',
Con.String('identifier', 5),
Con.Byte('major_version'),
Con.Byte('minor_version'),
Con.Byte('density_units'),
Con.UBInt16('x_density'),
Con.UBInt16('y_density'),
Con.Byte('thumbnail_width'),
Con.Byte('thumbnail_height'))
SOF = Con.Struct('start_of_frame',
Con.Byte('data_precision'),
Con.UBInt16('image_height'),
Con.UBInt16('image_width'),
Con.Byte('components'))
def __init__(self, width, height, bits_per_pixel):
ImageMetrics.__init__(self, width, height, bits_per_pixel,
0, u'image/jpeg')
@classmethod
def parse(cls, file):
try:
header = cls.SEGMENT_HEADER.parse_stream(file)
if (header.type != 0xD8):
raise InvalidJPEG(_(u'Invalid JPEG header'))
segment = cls.SEGMENT_HEADER.parse_stream(file)
while (segment.type != 0xD9):
if (segment.type == 0xDA):
break
if (segment.type in (0xC0, 0xC1, 0xC2, 0xC3,
0xC5, 0XC5, 0xC6, 0xC7,
0xC9, 0xCA, 0xCB, 0xCD,
0xCE, 0xCF)): # start of frame
segment_data = cStringIO.StringIO(
file.read(segment.length - 2))
frame0 = cls.SOF.parse_stream(segment_data)
segment_data.close()
return __JPEG__(width=frame0.image_width,
height=frame0.image_height,
bits_per_pixel=(frame0.data_precision *
frame0.components))
else:
file.seek(segment.length - 2, 1)
segment = cls.SEGMENT_HEADER.parse_stream(file)
raise InvalidJPEG(_(u'Start of frame not found'))
except Con.ConstError:
raise InvalidJPEG(_(u"Invalid JPEG segment marker at 0x%X") % \
(file.tell()))
#######################
#PNG
#######################
class InvalidPNG(InvalidImage):
"""Raised if a PNG cannot be parsed correctly."""
pass
class __PNG__(ImageMetrics):
HEADER = Con.Const(Con.String('header', 8),
'89504e470d0a1a0a'.decode('hex'))
CHUNK_HEADER = Con.Struct('chunk',
Con.UBInt32('length'),
Con.String('type', 4))
CHUNK_FOOTER = Con.Struct('crc32',
Con.UBInt32('crc'))
IHDR = Con.Struct('IHDR',
Con.UBInt32('width'),
Con.UBInt32('height'),
Con.Byte('bit_depth'),
Con.Byte('color_type'),
Con.Byte('compression_method'),
Con.Byte('filter_method'),
Con.Byte('interlace_method'))
def __init__(self, width, height, bits_per_pixel, color_count):
ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count,
u'image/png')
@classmethod
def parse(cls, file):
ihdr = None
plte = None
try:
header = cls.HEADER.parse_stream(file)
chunk_header = cls.CHUNK_HEADER.parse_stream(file)
data = file.read(chunk_header.length)
chunk_footer = cls.CHUNK_FOOTER.parse_stream(file)
while (chunk_header.type != 'IEND'):
if (chunk_header.type == 'IHDR'):
ihdr = cls.IHDR.parse(data)
elif (chunk_header.type == 'PLTE'):
plte = data
chunk_header = cls.CHUNK_HEADER.parse_stream(file)
data = file.read(chunk_header.length)
chunk_footer = cls.CHUNK_FOOTER.parse_stream(file)
if (ihdr.color_type == 0): # grayscale
bits_per_pixel = ihdr.bit_depth
color_count = 0
elif (ihdr.color_type == 2): # RGB
bits_per_pixel = ihdr.bit_depth * 3
color_count = 0
elif (ihdr.color_type == 3): # palette
bits_per_pixel = 8
if ((len(plte) % 3) != 0):
raise InvalidPNG(_(u'Invalid PLTE chunk length'))
else:
color_count = len(plte) / 3
elif (ihdr.color_type == 4): # grayscale + alpha
bits_per_pixel = ihdr.bit_depth * 2
color_count = 0
elif (ihdr.color_type == 6): # RGB + alpha
bits_per_pixel = ihdr.bit_depth * 4
color_count = 0
return __PNG__(ihdr.width, ihdr.height, bits_per_pixel,
color_count)
except Con.ConstError:
raise InvalidPNG(_(u'Invalid PNG'))
#######################
#BMP
#######################
class InvalidBMP(InvalidImage):
"""Raised if a BMP cannot be parsed correctly."""
pass
class __BMP__(ImageMetrics):
HEADER = Con.Struct('bmp_header',
Con.Const(Con.String('magic_number', 2), 'BM'),
Con.ULInt32('file_size'),
Con.ULInt16('reserved1'),
Con.ULInt16('reserved2'),
Con.ULInt32('bitmap_data_offset'))
INFORMATION = Con.Struct('bmp_information',
Con.ULInt32('header_size'),
Con.ULInt32('width'),
Con.ULInt32('height'),
Con.ULInt16('color_planes'),
Con.ULInt16('bits_per_pixel'),
Con.ULInt32('compression_method'),
Con.ULInt32('image_size'),
Con.ULInt32('horizontal_resolution'),
Con.ULInt32('vertical_resolution'),
Con.ULInt32('colors_used'),
Con.ULInt32('important_colors_used'))
def __init__(self, width, height, bits_per_pixel, color_count):
ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count,
u'image/x-ms-bmp')
@classmethod
def parse(cls, file):
try:
header = cls.HEADER.parse_stream(file)
information = cls.INFORMATION.parse_stream(file)
return __BMP__(information.width, information.height,
information.bits_per_pixel,
information.colors_used)
except Con.ConstError:
raise InvalidBMP(_(u'Invalid BMP'))
#######################
#GIF
#######################
class InvalidGIF(InvalidImage):
"""Raised if a GIF cannot be parsed correctly."""
pass
class __GIF__(ImageMetrics):
HEADER = Con.Struct('header',
Con.Const(Con.String('gif', 3), 'GIF'),
Con.String('version', 3))
SCREEN_DESCRIPTOR = Con.Struct('logical_screen_descriptor',
Con.ULInt16('width'),
Con.ULInt16('height'),
Con.Embed(
Con.BitStruct('packed_fields',
Con.Flag('global_color_table'),
Con.Bits('color_resolution', 3),
Con.Flag('sort'),
Con.Bits('global_color_table_size', 3))),
Con.Byte('background_color_index'),
Con.Byte('pixel_aspect_ratio'))
def __init__(self, width, height, color_count):
ImageMetrics.__init__(self, width, height, 8, color_count,
u'image/gif')
@classmethod
def parse(cls, file):
try:
header = cls.HEADER.parse_stream(file)
descriptor = cls.SCREEN_DESCRIPTOR.parse_stream(file)
return __GIF__(descriptor.width, descriptor.height,
2 ** (descriptor.global_color_table_size + 1))
except Con.ConstError:
raise InvalidGIF(_(u'Invalid GIF'))
#######################
#TIFF
#######################
class InvalidTIFF(InvalidImage):
"""Raised if a TIFF cannot be parsed correctly."""
pass
class __TIFF__(ImageMetrics):
HEADER = Con.Struct('header',
Con.String('byte_order', 2),
Con.Switch('order',
lambda ctx: ctx['byte_order'],
{"II": Con.Embed(
Con.Struct('little_endian',
Con.Const(Con.ULInt16('version'), 42),
Con.ULInt32('offset'))),
"MM": Con.Embed(
Con.Struct('big_endian',
Con.Const(Con.UBInt16('version'), 42),
Con.UBInt32('offset')))}))
L_IFD = Con.Struct('ifd',
Con.PrefixedArray(
length_field=Con.ULInt16('length'),
subcon=Con.Struct('tags',
Con.ULInt16('id'),
Con.ULInt16('type'),
Con.ULInt32('count'),
Con.ULInt32('offset'))),
Con.ULInt32('next'))
B_IFD = Con.Struct('ifd',
Con.PrefixedArray(
length_field=Con.UBInt16('length'),
subcon=Con.Struct('tags',
Con.UBInt16('id'),
Con.UBInt16('type'),
Con.UBInt32('count'),
Con.UBInt32('offset'))),
Con.UBInt32('next'))
def __init__(self, width, height, bits_per_pixel, color_count):
ImageMetrics.__init__(self, width, height,
bits_per_pixel, color_count,
u'image/tiff')
@classmethod
def b_tag_value(cls, file, tag):
subtype = {1: Con.Byte("data"),
2: Con.CString("data"),
3: Con.UBInt16("data"),
4: Con.UBInt32("data"),
5: Con.Struct("data",
Con.UBInt32("high"),
Con.UBInt32("low"))}[tag.type]
data = Con.StrictRepeater(tag.count,
subtype)
if ((tag.type != 2) and (data.sizeof() <= 4)):
return tag.offset
else:
file.seek(tag.offset, 0)
return data.parse_stream(file)
@classmethod
def l_tag_value(cls, file, tag):
subtype = {1: Con.Byte("data"),
2: Con.CString("data"),
3: Con.ULInt16("data"),
4: Con.ULInt32("data"),
5: Con.Struct("data",
Con.ULInt32("high"),
Con.ULInt32("low"))}[tag.type]
data = Con.StrictRepeater(tag.count,
subtype)
if ((tag.type != 2) and (data.sizeof() <= 4)):
return tag.offset
else:
file.seek(tag.offset, 0)
return data.parse_stream(file)
@classmethod
def parse(cls, file):
width = 0
height = 0
bits_per_sample = 0
color_count = 0
try:
header = cls.HEADER.parse_stream(file)
if (header.byte_order == 'II'):
IFD = cls.L_IFD
tag_value = cls.l_tag_value
elif (header.byte_order == 'MM'):
IFD = cls.B_IFD
tag_value = cls.b_tag_value
else:
raise InvalidTIFF(_(u'Invalid byte order'))
file.seek(header.offset, 0)
ifd = IFD.parse_stream(file)
while (True):
for tag in ifd.tags:
if (tag.id == 0x0100):
width = tag_value(file, tag)
elif (tag.id == 0x0101):
height = tag_value(file, tag)
elif (tag.id == 0x0102):
try:
bits_per_sample = sum(tag_value(file, tag))
except TypeError:
bits_per_sample = tag_value(file, tag)
elif (tag.id == 0x0140):
color_count = tag.count / 3
else:
pass
if (ifd.next == 0x00):
break
else:
file.seek(ifd.next, 0)
ifd = IFD.parse_stream(file)
return __TIFF__(width, height, bits_per_sample, color_count)
except Con.ConstError:
raise InvalidTIFF(_(u'Invalid TIFF'))
def can_thumbnail():
"""Returns True if we have the capability to thumbnail images."""
try:
import Image as PIL_Image
return True
except ImportError:
return False
def thumbnail_formats():
"""Returns a list of available thumbnail image formats."""
import Image as PIL_Image
import cStringIO
#performing a dummy save seeds PIL_Image.SAVE with possible save types
PIL_Image.new("RGB", (1, 1)).save(cStringIO.StringIO(), "bmp")
return PIL_Image.SAVE.keys()
def thumbnail_image(image_data, width, height, format):
"""Generates a new, smaller image from a larger one.
image_data is a binary string.
width and height are the requested maximum values.
format as a binary string, such as 'JPEG'.
"""
import cStringIO
import Image as PIL_Image
import ImageFile as PIL_ImageFile
PIL_ImageFile.MAXBLOCK = 0x100000
img = PIL_Image.open(cStringIO.StringIO(image_data)).convert('RGB')
img.thumbnail((width, height), PIL_Image.ANTIALIAS)
output = cStringIO.StringIO()
if (format.upper() == 'JPEG'):
#PIL's default JPEG save quality isn't too great
#so it's best to add a couple of optimizing parameters
#since this is a common case
img.save(output, 'JPEG', quality=90, optimize=True)
else:
img.save(output, format)
return output.getvalue()