Photon-0.4.6/0000755000175000017500000000000010713565311011122 5ustar luclucPhoton-0.4.6/Photon/0000755000175000017500000000000010713565311012371 5ustar luclucPhoton-0.4.6/Photon/AVI.py0000644000175000017500000002464110342053200013354 0ustar lucluc# # Parse a AVI movie file produced by many digital camera # # import struct, array from random import randrange avi_verbose = 0 huffman_table = array.array('c', "\xFF\xC4\x01\xA2\x00\x00\x01\x05\x01\x01\x01\x01"\ "\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02"\ "\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x01\x00\x03"\ "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00"\ "\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"\ "\x0A\x0B\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05"\ "\x05\x04\x04\x00\x00\x01\x7D\x01\x02\x03\x00\x04"\ "\x11\x05\x12\x21\x31\x41\x06\x13\x51\x61\x07\x22"\ "\x71\x14\x32\x81\x91\xA1\x08\x23\x42\xB1\xC1\x15"\ "\x52\xD1\xF0\x24\x33\x62\x72\x82\x09\x0A\x16\x17"\ "\x18\x19\x1A\x25\x26\x27\x28\x29\x2A\x34\x35\x36"\ "\x37\x38\x39\x3A\x43\x44\x45\x46\x47\x48\x49\x4A"\ "\x53\x54\x55\x56\x57\x58\x59\x5A\x63\x64\x65\x66"\ "\x67\x68\x69\x6A\x73\x74\x75\x76\x77\x78\x79\x7A"\ "\x83\x84\x85\x86\x87\x88\x89\x8A\x92\x93\x94\x95"\ "\x96\x97\x98\x99\x9A\xA2\xA3\xA4\xA5\xA6\xA7\xA8"\ "\xA9\xAA\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xC2"\ "\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xD2\xD3\xD4\xD5"\ "\xD6\xD7\xD8\xD9\xDA\xE1\xE2\xE3\xE4\xE5\xE6\xE7"\ "\xE8\xE9\xEA\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9"\ "\xFA\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05"\ "\x04\x04\x00\x01\x02\x77\x00\x01\x02\x03\x11\x04"\ "\x05\x21\x31\x06\x12\x41\x51\x07\x61\x71\x13\x22"\ "\x32\x81\x08\x14\x42\x91\xA1\xB1\xC1\x09\x23\x33"\ "\x52\xF0\x15\x62\x72\xD1\x0A\x16\x24\x34\xE1\x25"\ "\xF1\x17\x18\x19\x1A\x26\x27\x28\x29\x2A\x35\x36"\ "\x37\x38\x39\x3A\x43\x44\x45\x46\x47\x48\x49\x4A"\ "\x53\x54\x55\x56\x57\x58\x59\x5A\x63\x64\x65\x66"\ "\x67\x68\x69\x6A\x73\x74\x75\x76\x77\x78\x79\x7A"\ "\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x92\x93\x94"\ "\x95\x96\x97\x98\x99\x9A\xA2\xA3\xA4\xA5\xA6\xA7"\ "\xA8\xA9\xAA\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA"\ "\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xD2\xD3\xD4"\ "\xD5\xD6\xD7\xD8\xD9\xDA\xE2\xE3\xE4\xE5\xE6\xE7"\ "\xE8\xE9\xEA\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA" ) def read_headers(filename,size=65536): h = None f = open(filename,'rb') if f: h = f.read(size) return (f, h) def read_le32(f): h = f.read(4) s = struct.unpack("1: print "Total number of frames: %d" % (info['frames']) print "Total read: %d" % n_frame_data # # Some AVI have an index to frame, at the end of the file. This can speed up a # lot, when doing fast forward/backward. But sometimes, the pos is not from the # beginning of the file, but from the start of the movie # def read_idx(f, info, size): info['frames_offsets'] = [] n = size/16 if n<1: return None while n: tag = f.read(4) flags = read_le32(f) pos = read_le32(f) len = read_le32(f) if tag == '00dc': info['frames_offsets'].append(pos, len) #print "Tag: %s; flags=%8.8x; pos=%8d; len=%8d" % (tag, flags, pos, len) n-=1 return 1 # # Is this file have an index ?, so loaded # def find_n_read_idx(f, info): f.seek(12, 0) while True: tag = f.read(4) if len(tag)<4: return None tag_size = read_le32(f) #print "Tag: %s (len=%8d)" % (tag, tag_size) if tag == 'idx1': return read_idx(f, info, tag_size) else: if tag_size&1: tag_size+=1 f.seek(tag_size, 1) return None # # Copy the Jpeg data from the stream to out # In MJPEG, many stream don't include the huffman table. # We need to parse the JPEG stream header to find if we need to add a default huffman table. # def fix_n_output_jpeg(h, out): has_huffmantable = 0 i = 2 while True: # Hum, this is not a valid chunk if h[i] != "\xff": return None i+=1 # Skip any padding ff byte (this normal) while h[i] == 0xff: i+=1 # All SOF0 to SOF15 is valid (for me) not sure #print "Found marker %2.2x at index %d"% (ord(h[i]),i) if h[i] =='\xc4': has_huffmantable = 1 if h[i] == '\xda': # Need to flush all the file if has_huffmantable: out.write(h) else: out.write(h[0:i-1]) out.write(huffman_table) out.write(h[i-1:]) return i+=1 # Skip to next marker s = struct.unpack(">H", h[i:i+2]) i += s[0] # # Extract one image from the file # def extract_jpeg_file(moviefile, info, outfile, frame): f = open(moviefile, 'rb') # Build a cache if not info.has_key('frames_offsets'): build_cache(f, info) # Seek and copy the frame data into the file try: (offset, size) = info['frames_offsets'][frame] except IndexError: print "Warning: trying to grab frame %d but this frame is not found in the movie" % frame print "Perhaps, the file is too short" (offset, size) = info['frames_offsets'][len(info['frames_offsets'])-1] f.seek(offset,0) out = open(outfile, "wb") h = f.read(size) f.close() if h[0:2] == '\xff\xd8': # To be sure that we extract a Jpeg file fix_n_output_jpeg(h, out) else: print "Warning: this is not a jpeg file (frame=%d)" % frame out.close() def __identify(filename): if struct.calcsize(">L") != 4: print "A long is not equal to 4 bytes with your python installation. Abording" return None f, h = read_headers(filename,12) if h == None: return None if h[0:4] != 'RIFF': # RIFF header return None # 4-8 Filesize if h[8:12] != 'AVI ': # Avi chunk return None info = {} stream_index = -1 # Current Stream Header currently parsing [0...n_streams] us_frame = 0 current_codec_type = None video_codec = None info['video_size'] = (0,0) info['frames'] = 0 info['format'] = None while True: tag = f.read(4) if len(tag)<4: return None tag_size = read_le32(f) if avi_verbose>0: print "Tag \"%s\" length=%d" % (tag, tag_size) if tag == 'LIST': if avi_verbose>0: print "Type LIST" stag = f.read(4) if stag == 'movi': info['movie_end'] = f.tell() + tag_size - 4 info['movie_start'] = f.tell() if avi_verbose>0: print " movie_end at %d" % info['movie_end'] break else: if avi_verbose>0: print " ignoring unknown subtag ", stag #f.seek(tag_size-4, 1) elif tag == 'avih': if avi_verbose>0: print "Type AVI header" us_frame = read_le32(f) # in microsecond bit_rate = read_le32(f) * 8 pad_gran = read_le32(f) flags = read_le32(f) info['frames'] = read_le32(f) init_frames = read_le32(f) n_streams = read_le32(f) f.seek(tag_size-7*4, 1) if avi_verbose>0: print " us_frame = %d" % us_frame print " bit_rate = %d" % bit_rate print " flags = %x" % flags print " total frames = %d" % info['frames'] print " init_frames = %d" % init_frames print " n_streams = %d" % n_streams elif tag == 'strh': if avi_verbose>0: print "Type Stream Header" stream_index+=1 stag = f.read(4) if stag == "vids": if avi_verbose>0: print " subtag 'vids'" current_codec_type = "VIDEO" codec_tag = f.read(4) flags = read_le32(f) priority_n_language = read_le32(f) inital_frame = read_le32(f) scale= read_le32(f) rate= read_le32(f) if scale and rate: frame_rate = rate frame_rate_base = scale elif us_frame: frame_rate = 1000000 frame_rate_base = us_frame else: frame_rate = 25 frame_rate_base = 1 f.seek(tag_size-7*4, 1) if avi_verbose>0: print " codec_tag = ", codec_tag print " flags = %x" % flags print " scale = %d" % scale print " rate = %d" % rate print " frame_rate = %d / frame_rate_base = %d" % (frame_rate, frame_rate_base) # Only mjpeg is supported if codec_tag != 'mjpg': return None elif stag == "auds": if avi_verbose>0: print " subtag 'auds'" current_codec_type = "AUDIO" f.seek(tag_size-4, 1) else: if avi_verbose>0: print "Unknown subtag %s for type 'strh'" % stag return None elif tag == 'strf': if current_codec_type == "VIDEO": stream_length = read_le32(f) stream_width = read_le32(f) stream_height = read_le32(f) f.seek(4, 1) video_codec = f.read(4) #f.seek(5*4, 1) #video_extra_huffmantable = f.read(tag_size - 10*4) f.seek(tag_size-5*4, 1) info['video_size'] = (stream_width, stream_height) if avi_verbose>0: print " stream_length = %d" % stream_length print " stream size = %dx%d" % info['video_size'] print " Video Codec = %s" % video_codec elif current_codec_type == "AUDIO": f.seek(tag_size, 1) else: if avi_verbose>0: print "Unknown codec_type for Stream Format" return None else: if avi_verbose>0: print "Unknow header ", tag if tag_size&1: tag_size+=1 f.seek(tag_size,1) f.close() if video_codec == "MJPG": info['format'] = "MJPG" return info print "video_codec: ", video_codec return None def identify(filename): return __identify(filename) def extract_random_picture(moviefile, pictfile): video = __identify(moviefile) if video == None: return None r = randrange(0, video['frames']) extract_jpeg_file(moviefile, video, pictfile, r) return True def extract_picture(moviefile, pictfile, frame): video = __identify(moviefile) if video == None: return None extract_jpeg_file(moviefile, video, pictfile, frame) return True if __name__ == "__main__": import sys import AVI if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=AVI.identify(filename) if info != None: print "%s (format='%s', frames='%d')" % (filename,info['format'],info['frames']) for frame in xrange(info['frames']): extract_jpeg_file(filename, info, "/tmp/photon%8.8d.jpg" % frame, frame) else: print "%s is not a AVI movie file" % filename Photon-0.4.6/Photon/EXIF.py0000644000175000017500000011223010713557067013506 0ustar lucluc# Library to extract EXIF information in digital camera image files # # Contains code from "exifdump.py" originally written by Thierry Bousch # and released into the public domain. # # Updated and turned into general-purpose library by Gene Cash # # # This copyright license is intended to be similar to the FreeBSD license. # # Copyright 2002 Gene Cash All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # This means you may do anything you want with this code, except claim you # wrote it. Also, if it breaks you get to keep both pieces. # # 21-AUG-99 TB Last update by Thierry Bousch to his code. # 17-JAN-02 CEC Discovered code on web. # Commented everything. # Made small code improvements. # Reformatted for readability. # 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs. # Added ability to extract JPEG formatted thumbnail. # Added ability to read GPS IFD (not tested). # Converted IFD data structure to dictionaries indexed by # tag name. # Factored into library returning dictionary of IFDs plus # thumbnail, if any. # 20-JAN-02 CEC Added MakerNote processing logic. # Added Olympus MakerNote. # Converted data structure to single-level dictionary, avoiding # tag name collisions by prefixing with IFD name. This makes # it much easier to use. # 23-JAN-02 CEC Trimmed nulls from end of string values. # 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote. # 26-JAN-02 CEC Added ability to extract TIFF thumbnails. # Added Nikon, Fujifilm, Casio MakerNotes. # 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an # IFD_Tag() object. # 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L. # # To do: # * Better printing of ratios def array_to_string(x): return ''.join(map(lambda c: chr(int(c)&255), x)) # field type descriptions as (length, abbreviation, full name) tuples FIELD_TYPES=( (0, 'X', 'Proprietary'), # no such type (1, 'B', 'Byte'), (1, 'A', 'ASCII'), (2, 'S', 'Short'), (4, 'L', 'Long'), (8, 'R', 'Ratio'), (1, 'SB', 'Signed Byte'), (1, 'U', 'Undefined'), (2, 'SS', 'Signed Short'), (4, 'SL', 'Signed Long'), (8, 'SR', 'Signed Ratio') ) # dictionary of main EXIF tag names # first element of tuple is tag name, optional second element is # another dictionary giving names to values EXIF_TAGS={ 0x0100: ('ImageWidth', ), 0x0101: ('ImageLength', ), 0x0102: ('BitsPerSample', ), 0x0103: ('Compression', {1: 'Uncompressed TIFF', 6: 'JPEG Compressed'}), 0x0106: ('PhotometricInterpretation', ), 0x010A: ('FillOrder', ), 0x010D: ('DocumentName', ), 0x010E: ('ImageDescription', ), 0x010F: ('Make', ), 0x0110: ('Model', ), 0x0111: ('StripOffsets', ), 0x0112: ('Orientation', ), 0x0115: ('SamplesPerPixel', ), 0x0116: ('RowsPerStrip', ), 0x0117: ('StripByteCounts', ), 0x011A: ('XResolution', ), 0x011B: ('YResolution', ), 0x011C: ('PlanarConfiguration', ), 0x0128: ('ResolutionUnit', {1: 'Not Absolute', 2: 'Pixels/Inch', 3: 'Pixels/Centimeter'}), 0x012D: ('TransferFunction', ), 0x0131: ('Software', ), 0x0132: ('DateTime', ), 0x013B: ('Artist', ), 0x013E: ('WhitePoint', ), 0x013F: ('PrimaryChromaticities', ), 0x0156: ('TransferRange', ), 0x0200: ('JPEGProc', ), 0x0201: ('JPEGInterchangeFormat', ), 0x0202: ('JPEGInterchangeFormatLength', ), 0x0211: ('YCbCrCoefficients', ), 0x0212: ('YCbCrSubSampling', ), 0x0213: ('YCbCrPositioning', ), 0x0214: ('ReferenceBlackWhite', ), 0x828D: ('CFARepeatPatternDim', ), 0x828E: ('CFAPattern', ), 0x828F: ('BatteryLevel', ), 0x8298: ('Copyright', ), 0x829A: ('ExposureTime', ), 0x829D: ('FNumber', ), 0x83BB: ('IPTC/NAA', ), 0x8769: ('ExifOffset', ), 0x8773: ('InterColorProfile', ), 0x8822: ('ExposureProgram', {0: 'Unidentified', 1: 'Manual', 2: 'Program Normal', 3: 'Aperture Priority', 4: 'Shutter Priority', 5: 'Program Creative', 6: 'Program Action', 7: 'Portrait Mode', 8: 'Landscape Mode'}), 0x8824: ('SpectralSensitivity', ), 0x8825: ('GPSInfo', ), 0x8827: ('ISOSpeedRatings', ), 0x8828: ('OECF', ), # print as string 0x9000: ('ExifVersion', array_to_string), 0x9003: ('DateTimeOriginal', ), 0x9004: ('DateTimeDigitized', ), 0x9101: ('ComponentsConfiguration', {0: '', 1: 'Y', 2: 'Cb', 3: 'Cr', 4: 'Red', 5: 'Green', 6: 'Blue'}), 0x9102: ('CompressedBitsPerPixel', ), 0x9201: ('ShutterSpeedValue', ), 0x9202: ('ApertureValue', ), 0x9203: ('BrightnessValue', ), 0x9204: ('ExposureBiasValue', ), 0x9205: ('MaxApertureValue', ), 0x9206: ('SubjectDistance', ), 0x9207: ('MeteringMode', {0: 'Unidentified', 1: 'Average', 2: 'CenterWeightedAverage', 3: 'Spot', 4: 'MultiSpot', 5: 'Matrix'}), 0x9208: ('LightSource', {0: 'Unknown', 1: 'Daylight', 2: 'Fluorescent', 3: 'Tungsten', 4: 'Flash', 9: 'Fine weather', 10: 'Flash', 11: 'Shade', 17: 'Standard Light A', 18: 'Standard Light B', 19: 'Standard Light C', 20: 'D55', 21: 'D65', 22: 'D75', 255: 'Other'}), 0x9209: ('Flash', {0: 'No', 1: 'Yes', 5: 'Yes (Strobe light not detected)', # no return sensed 7: 'Yes (Strobe light detected)', # return sensed 9: 'Yes (manual)', 13: 'Yes (manual, return light not detected)', 15: 'Yes (manual, return light detected)', 16: 'Off', 24: 'Off (auto)', 25: 'Yes (auto)', 29: 'Yes (auto, return light not detected)', 31: 'Yes (auto, return light detected)', 32: 'Not Available', 65: 'Yes (red eye reduction mode)', 69: 'Yes (red eye reduction mode return light not detected)', 71: 'Yes (red eye reduction mode return light detected)', 73: 'Yes (manual, red eye reduction mode)', 77: 'Yes (manual, red eye reduction mode, return light not detected)', 79: 'Yes (manual, red eye reduction mode, return light detected)', 89: 'Yes (auto, red eye reduction mode)', 93: 'Yes (auto, red eye reduction mode, return light not detected)', 95: 'Yes (auto, red eye reduction mode, return light detected)' }), 0x920A: ('FocalLength', ), 0x927C: ('MakerNote', ), # print as string 0x9286: ('UserComment', array_to_string), 0x9290: ('SubSecTime', ), 0x9291: ('SubSecTimeOriginal', ), 0x9292: ('SubSecTimeDigitized', ), # print as string 0xA000: ('FlashPixVersion', array_to_string), 0xA001: ('ColorSpace', { 1: 'sRGB', 2: 'Adobe RGB', 65536: 'Uncalibrated'}), 0xA002: ('ExifImageWidth', ), 0xA003: ('ExifImageLength', ), 0xA005: ('InteroperabilityOffset', ), 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - - 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - - 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - - 0xA210: ('FocalPlaneResolutionUnit', # 0x9210 - - { 1: '25.4', # inch 2: '25.4', # ??? inch 3: '10', # centimeters 4: '1', # milimeters 5: '0.001'}), # micrometers 0xA214: ('SubjectLocation', ), # 0x9214 - - 0xA215: ('ExposureIndex', ), # 0x9215 - - 0xA217: ('SensingMethod', ), # 0x9217 - - 0xA300: ('FileSource', {3: 'Digital Camera'}), 0xA301: ('SceneType', {1: 'Directly Photographed'}), 0xA401: ('CustomRendered', ), 0xA402: ('ExposureMode', ), 0xA404: ('DigitalZoomRatio', ), 0xA405: ('FocalLengthIn35mmFilm', ), 0xA406: ('SceneCaptureType', ), 0xA407: ('GainControl', ), 0xA408: ('Contrast', ), 0xA409: ('Saturation', ), 0xA40A: ('Sharpness', ), 0xA40C: ('SubjectDistanceRange', ), } # interoperability tags INTR_TAGS={ 0x0001: ('InteroperabilityIndex', ), 0x0002: ('InteroperabilityVersion', ), 0x1000: ('RelatedImageFileFormat', ), 0x1001: ('RelatedImageWidth', ), 0x1002: ('RelatedImageLength', ), } # GPS tags (not used yet, haven't seen camera with GPS) GPS_TAGS={ 0x0000: ('GPSVersionID', ), 0x0001: ('GPSLatitudeRef', ), 0x0002: ('GPSLatitude', ), 0x0003: ('GPSLongitudeRef', ), 0x0004: ('GPSLongitude', ), 0x0005: ('GPSAltitudeRef', ), 0x0006: ('GPSAltitude', ), 0x0007: ('GPSTimeStamp', ), 0x0008: ('GPSSatellites', ), 0x0009: ('GPSStatus', ), 0x000A: ('GPSMeasureMode', ), 0x000B: ('GPSDOP', ), 0x000C: ('GPSSpeedRef', ), 0x000D: ('GPSSpeed', ), 0x000E: ('GPSTrackRef', ), 0x000F: ('GPSTrack', ), 0x0010: ('GPSImgDirectionRef', ), 0x0011: ('GPSImgDirection', ), 0x0012: ('GPSMapDatum', ), 0x0013: ('GPSDestLatitudeRef', ), 0x0014: ('GPSDestLatitude', ), 0x0015: ('GPSDestLongitudeRef', ), 0x0016: ('GPSDestLongitude', ), 0x0017: ('GPSDestBearingRef', ), 0x0018: ('GPSDestBearing', ), 0x0019: ('GPSDestDistanceRef', ), 0x001A: ('GPSDestDistance', ) } # Nikon E99x MakerNote Tags # http://members.tripod.com/~tawba/990exif.htm MAKERNOTE_NIKON_NEWER_TAGS={ 0x0002: ('ISOSetting', ), 0x0003: ('ColorMode', ), 0x0004: ('Quality', ), 0x0005: ('Whitebalance', ), 0x0006: ('ImageSharpening', ), 0x0007: ('FocusMode', ), 0x0008: ('FlashSetting', ), 0x000F: ('ISOSelection', ), 0x0080: ('ImageAdjustment', ), 0x0082: ('AuxiliaryLens', ), 0x0085: ('ManualFocusDistance', ), 0x0086: ('DigitalZoomFactor', ), 0x0088: ('AFFocusPosition', {0x0000: 'Center', 0x0100: 'Top', 0x0200: 'Bottom', 0x0300: 'Left', 0x0400: 'Right'}), 0x0094: ('Saturation', {-3: 'B&W', -2: '-2', -1: '-1', 0: '0', 1: '1', 2: '2'}), 0x0095: ('NoiseReduction', ), 0x0010: ('DataDump', ) } MAKERNOTE_NIKON_OLDER_TAGS={ 0x0003: ('Quality', {1: 'VGA Basic', 2: 'VGA Normal', 3: 'VGA Fine', 4: 'SXGA Basic', 5: 'SXGA Normal', 6: 'SXGA Fine'}), 0x0004: ('ColorMode', {1: 'Color', 2: 'Monochrome'}), 0x0005: ('ImageAdjustment', {0: 'Normal', 1: 'Bright+', 2: 'Bright-', 3: 'Contrast+', 4: 'Contrast-'}), 0x0006: ('CCDSpeed', {0: 'ISO 80', 2: 'ISO 160', 4: 'ISO 320', 5: 'ISO 100'}), 0x0007: ('WhiteBalance', {0: 'Auto', 1: 'Preset', 2: 'Daylight', 3: 'Incandescent', 4: 'Fluorescent', 5: 'Cloudy', 6: 'Speed Light'}) } # decode Olympus SpecialMode tag in MakerNote def olympus_special_mode(v): a={ 0: 'Normal', 1: 'Unknown', 2: 'Fast', 3: 'Panorama'} b={ 0: 'Non-panoramic', 1: 'Left to right', 2: 'Right to left', 3: 'Bottom to top', 4: 'Top to bottom'} # FIXLUC: for broken exif jpeg save by gimp2.x if v[0]>3 or v[2]>4: return '' # FIXLUC: end return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) MAKERNOTE_OLYMPUS_TAGS={ # ah HAH! those sneeeeeaky bastids! this is how they get past the fact # that a JPEG thumbnail is not allowed in an uncompressed TIFF file 0x0100: ('JPEGThumbnail', ), 0x0200: ('SpecialMode', olympus_special_mode), 0x0201: ('JPEGQual', {1: 'SQ', 2: 'HQ', 3: 'SHQ', 6: 'RAW'}), 0x0202: ('Macro', {0: 'Normal', 1: 'Macro', 2: 'Super Macro'}), 0x0203: ('BWMode', ), 0x0204: ('DigitalZoom', ), 0x0205: ('FocalPlaneDiagonal', ), 0x0207: ('SoftwareRelease', ), 0x0208: ('PictureInfo', ), # print as string 0x0209: ('CameraID', array_to_string), 0x0F00: ('DataDump', ) } MAKERNOTE_CASIO_TAGS={ 0x0001: ('RecordingMode', {1: 'Single Shutter', 2: 'Panorama', 3: 'Night Scene', 4: 'Portrait', 5: 'Landscape'}), 0x0002: ('Quality', {1: 'Economy', 2: 'Normal', 3: 'Fine'}), 0x0003: ('FocusingMode', {2: 'Macro', 3: 'Auto Focus', 4: 'Manual Focus', 5: 'Infinity'}), 0x0004: ('FlashMode', {1: 'Auto', 2: 'On', 3: 'Off', 4: 'Red Eye Reduction'}), 0x0005: ('FlashIntensity', {11: 'Weak', 13: 'Normal', 15: 'Strong'}), 0x0006: ('Object Distance', ), 0x0007: ('WhiteBalance', {1: 'Auto', 2: 'Tungsten', 3: 'Daylight', 4: 'Fluorescent', 5: 'Shade', 129: 'Manual'}), 0x000B: ('Sharpness', {0: 'Normal', 1: 'Soft', 2: 'Hard'}), 0x000C: ('Contrast', {0: 'Normal', 1: 'Low', 2: 'High'}), 0x000D: ('Saturation', {0: 'Normal', 1: 'Low', 2: 'High'}), 0x0014: ('CCDSpeed', {64: 'Normal', 80: 'Normal', 100: 'High', 125: '+1.0', 244: '+3.0', 250: '+2.0',}) } MAKERNOTE_FUJIFILM_TAGS={ 0x0000: ('NoteVersion', array_to_string), 0x1000: ('Quality', ), 0x1001: ('Sharpness', {1: 'Soft', 2: 'Soft', 3: 'Normal', 4: 'Hard', 5: 'Hard'}), 0x1002: ('WhiteBalance', {0: 'Auto', 256: 'Daylight', 512: 'Cloudy', 768: 'DaylightColor-Fluorescent', 769: 'DaywhiteColor-Fluorescent', 770: 'White-Fluorescent', 1024: 'Incandescent', 3840: 'Custom'}), 0x1003: ('Color', {0: 'Normal', 256: 'High', 512: 'Low'}), 0x1004: ('Tone', {0: 'Normal', 256: 'High', 512: 'Low'}), 0x1010: ('FlashMode', {0: 'Auto', 1: 'On', 2: 'Off', 3: 'Red Eye Reduction'}), 0x1011: ('FlashStrength', ), 0x1020: ('Macro', {0: 'Off', 1: 'On'}), 0x1021: ('FocusMode', {0: 'Auto', 1: 'Manual'}), 0x1030: ('SlowSync', {0: 'Off', 1: 'On'}), 0x1031: ('PictureMode', {0: 'Auto', 1: 'Portrait', 2: 'Landscape', 4: 'Sports', 5: 'Night', 6: 'Program AE', 256: 'Aperture Priority AE', 512: 'Shutter Priority AE', 768: 'Manual Exposure'}), 0x1100: ('MotorOrBracket', {0: 'Off', 1: 'On'}), 0x1300: ('BlurWarning', {0: 'Off', 1: 'On'}), 0x1301: ('FocusWarning', {0: 'Off', 1: 'On'}), 0x1302: ('AEWarning', {0: 'Off', 1: 'On'}) } MAKERNOTE_CANON_TAGS={ 0x0006: ('ImageType', ), 0x0007: ('FirmwareVersion', ), 0x0008: ('ImageNumber', ), 0x0009: ('OwnerName', ) } # see http://www.burren.cx/david/canon.html by David Burren # this is in element offset, name, optional value dictionary format MAKERNOTE_CANON_TAG_0x001={ 1: ('Macromode', {1: 'Macro', 2: 'Normal'}), 2: ('SelfTimer', ), 3: ('Quality', {2: 'Normal', 3: 'Fine', 5: 'Superfine'}), 4: ('FlashMode', {0: 'Flash Not Fired', 1: 'Auto', 2: 'On', 3: 'Red-Eye Reduction', 4: 'Slow Synchro', 5: 'Auto + Red-Eye Reduction', 6: 'On + Red-Eye Reduction', 16: 'external flash'}), 5: ('ContinuousDriveMode', {0: 'Single Or Timer', 1: 'Continuous'}), 7: ('FocusMode', {0: 'One-Shot', 1: 'AI Servo', 2: 'AI Focus', 3: 'MF', 4: 'Single', 5: 'Continuous', 6: 'MF'}), 10: ('ImageSize', {0: 'Large', 1: 'Medium', 2: 'Small'}), 11: ('EasyShootingMode', {0: 'Full Auto', 1: 'Manual', 2: 'Landscape', 3: 'Fast Shutter', 4: 'Slow Shutter', 5: 'Night', 6: 'B&W', 7: 'Sepia', 8: 'Portrait', 9: 'Sports', 10: 'Macro/Close-Up', 11: 'Pan Focus'}), 12: ('DigitalZoom', {0: 'None', 1: '2x', 2: '4x'}), 13: ('Contrast', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 14: ('Saturation', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 15: ('Sharpness', {0xFFFF: 'Low', 0: 'Normal', 1: 'High'}), 16: ('ISO', {0: 'See ISOSpeedRatings Tag', 15: 'Auto', 16: '50', 17: '100', 18: '200', 19: '400'}), 17: ('MeteringMode', {3: 'Evaluative', 4: 'Partial', 5: 'Center-weighted'}), 18: ('FocusType', {0: 'Manual', 1: 'Auto', 3: 'Close-Up (Macro)', 8: 'Locked (Pan Mode)'}), 19: ('AFPointSelected', {0x3000: 'None (MF)', 0x3001: 'Auto-Selected', 0x3002: 'Right', 0x3003: 'Center', 0x3004: 'Left'}), 20: ('ExposureMode', {0: 'Easy Shooting', 1: 'Program', 2: 'Tv-priority', 3: 'Av-priority', 4: 'Manual', 5: 'A-DEP'}), 23: ('LongFocalLengthOfLensInFocalUnits', ), 24: ('ShortFocalLengthOfLensInFocalUnits', ), 25: ('FocalUnitsPerMM', ), 28: ('FlashActivity', {0: 'Did Not Fire', 1: 'Fired'}), 29: ('FlashDetails', {14: 'External E-TTL', 13: 'Internal Flash', 11: 'FP Sync Used', 7: '2nd("Rear")-Curtain Sync Used', 4: 'FP Sync Enabled'}), 32: ('FocusMode', {0: 'Single', 1: 'Continuous'}) } MAKERNOTE_CANON_TAG_0x004={ 7: ('WhiteBalance', {0: 'Auto', 1: 'Sunny', 2: 'Cloudy', 3: 'Tungsten', 4: 'Fluorescent', 5: 'Flash', 6: 'Custom'}), 9: ('SequenceNumber', ), 14: ('AFPointUsed', ), 15: ('FlashBias', {0XFFC0: '-2 EV', 0XFFCC: '-1.67 EV', 0XFFD0: '-1.50 EV', 0XFFD4: '-1.33 EV', 0XFFE0: '-1 EV', 0XFFEC: '-0.67 EV', 0XFFF0: '-0.50 EV', 0XFFF4: '-0.33 EV', 0X0000: '0 EV', 0X000C: '0.33 EV', 0X0010: '0.50 EV', 0X0014: '0.67 EV', 0X0020: '1 EV', 0X002C: '1.33 EV', 0X0030: '1.50 EV', 0X0034: '1.67 EV', 0X0040: '2 EV'}), 19: ('SubjectDistance', ) } MAKERNOTE_CANON_TAG_0x012={ 2: ('CanonImageWidth', ), 3: ('CanonImageHeight', ), 4: ('CanonImageWidthAsShot', ), 5: ('CanonImageHeightAsShot', ) } # extract multibyte integer in Motorola format (little endian) def s2n_motorola(str): x=0 for c in str: x=(x << 8) | ord(c) return x # extract multibyte integer in Intel format (big endian) def s2n_intel(str): x=0 y=0L for c in str: x=x | (ord(c) << y) y=y+8 return x # ratio object that eventually will be able to reduce itself to lowest # common denominator for printing def gcd(a, b): if b == 0: return a else: return gcd(b, a % b) class Ratio: def __init__(self, num, den): self.num=num self.den=den def __repr__(self): self.reduce() if self.den == 1: return str(self.num) return '%d/%d' % (self.num, self.den) def __int__(self): self.reduce() if self.den == 1: return self.num return int(self.num/self.den) def reduce(self): div=gcd(self.num, self.den) if div > 1: self.num=self.num/div self.den=self.den/div # for ease of dealing with tags class IFD_Tag: def __init__(self, printable, tag, field_type, values, field_offset, field_length): # printable version of data self.printable=printable # tag ID number self.tag=tag # field type as index into FIELD_TYPES self.field_type=field_type # offset of start of field in bytes from beginning of IFD self.field_offset=field_offset # length of data field in bytes self.field_length=field_length # either a string or array of data items self.values=values def __str__(self): return self.printable def __repr__(self): return '(0x%04X) %s=%s @ %d' % (self.tag, FIELD_TYPES[self.field_type][2], self.printable, self.field_offset) # class that handles an EXIF header class EXIF_header: def __init__(self, file, endian, offset, debug=0): self.file=file self.endian=endian self.offset=offset self.debug=debug self.tags={} # convert slice to integer, based on sign and endian flags def s2n(self, offset, length, signed=0): self.file.seek(self.offset+offset) slice=self.file.read(length) if self.endian == 'I': val=s2n_intel(slice) else: val=s2n_motorola(slice) # Sign extension ? if signed: #msb=1 << (8*length-1) #if val & msb: # val=val-(msb << 1) pass return val # convert offset to string def n2s(self, offset, length): s='' for i in range(length): if self.endian == 'I': s=s+chr(offset & 0xFF) else: s=chr(offset & 0xFF)+s offset=offset >> 8 return s # return first IFD def first_IFD(self): return self.s2n(4, 4) # return pointer to next IFD def next_IFD(self, ifd): entries=self.s2n(ifd, 2) return self.s2n(ifd+2+12*entries, 4) # return list of IFDs in header def list_IFDs(self): i=self.first_IFD() a=[] while i: a.append(i) i=self.next_IFD(i) return a # return list of entries in this IFD def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS): entries=self.s2n(ifd, 2) for i in range(entries): entry=ifd+2+12*i tag=self.s2n(entry, 2) field_type=self.s2n(entry+2, 2) if not 0 < field_type < len(FIELD_TYPES): # unknown field type raise ValueError, \ 'unknown type %d in tag 0x%04X' % (field_type, tag) typelen=FIELD_TYPES[field_type][0] count=self.s2n(entry+4, 4) offset=entry+8 if count*typelen > 4: # not the value, it's a pointer to the value offset=self.s2n(offset, 4) field_offset=offset if field_type == 2: # special case: null-terminated ASCII string if count != 0: self.file.seek(self.offset+offset) values=self.file.read(count).strip().replace('\x00','') else: values='' else: values=[] signed=(field_type in [6, 8, 9, 10]) for j in range(count): if field_type in (5, 10): # a ratio value_j=Ratio(self.s2n(offset, 4, signed), self.s2n(offset+4, 4, signed)) else: value_j=self.s2n(offset, typelen, signed) values.append(value_j) offset=offset+typelen # now "values" is either a string or an array if count == 1 and field_type != 2: printable=str(values[0]) else: printable=str(values) # figure out tag name tag_entry=dict.get(tag) if tag_entry: tag_name=tag_entry[0] if len(tag_entry) != 1: # optional 2nd tag element is present if callable(tag_entry[1]): # call mapping function printable=tag_entry[1](values) else: printable='' for i in values: # use LUT for this tag printable+=tag_entry[1].get(i, repr(i)) else: tag_name='Tag 0x%04X' % tag self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag, field_type, values, field_offset, count*typelen) if self.debug: print ' %s: %s' % (tag_name, repr(self.tags[ifd_name+' '+tag_name])) # extract uncompressed TIFF thumbnail (like pulling teeth) # we take advantage of the pre-existing layout in the thumbnail IFD as # much as possible def extract_TIFF_thumbnail(self, thumb_ifd): entries=self.s2n(thumb_ifd, 2) # this is header plus offset to IFD ... if self.endian == 'M': tiff='MM\x00*\x00\x00\x00\x08' else: tiff='II*\x00\x08\x00\x00\x00' # ... plus thumbnail IFD data plus a null "next IFD" pointer self.file.seek(self.offset+thumb_ifd) tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00' # fix up large value offset pointers into data area for i in range(entries): entry=thumb_ifd+2+12*i tag=self.s2n(entry, 2) field_type=self.s2n(entry+2, 2) typelen=FIELD_TYPES[field_type][0] count=self.s2n(entry+4, 4) oldoff=self.s2n(entry+8, 4) # start of the 4-byte pointer area in entry ptr=i*12+18 # remember strip offsets location if tag == 0x0111: strip_off=ptr strip_len=count*typelen # is it in the data area? if count*typelen > 4: # update offset pointer (nasty "strings are immutable" crap) # should be able to say "tiff[ptr:ptr+4]=newoff" newoff=len(tiff) tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:] # remember strip offsets location if tag == 0x0111: strip_off=newoff strip_len=4 # get original data and store it self.file.seek(self.offset+oldoff) tiff+=self.file.read(count*typelen) # add pixel strips and update strip offset info old_offsets=self.tags['Thumbnail StripOffsets'].values old_counts=self.tags['Thumbnail StripByteCounts'].values for i in range(len(old_offsets)): # update offset pointer (more nasty "strings are immutable" crap) offset=self.n2s(len(tiff), strip_len) tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:] strip_off+=strip_len # add pixel strip to end self.file.seek(self.offset+old_offsets[i]) tiff+=self.file.read(old_counts[i]) self.tags['TIFFThumbnail']=tiff # decode all the camera-specific MakerNote formats def decode_maker_note(self): note=self.tags['EXIF MakerNote'] make=self.tags['Image Make'].printable model=self.tags['Image Model'].printable # Nikon if make == 'NIKON': if note.values[0:5] == [78, 105, 107, 111, 110]: # "Nikon" # older model self.dump_IFD(note.field_offset+8, 'MakerNote', dict=MAKERNOTE_NIKON_OLDER_TAGS) else: # newer model (E99x or D1) self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_NIKON_NEWER_TAGS) return # Olympus if make[:7] == 'OLYMPUS': self.dump_IFD(note.field_offset+8, 'MakerNote', dict=MAKERNOTE_OLYMPUS_TAGS) return # Casio if make == 'Casio': self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_CASIO_TAGS) return # Fujifilm if make == 'FUJIFILM': # bug: everything else is "Motorola" endian, but the MakerNote # is "Intel" endian endian=self.endian self.endian='I' # bug: IFD offsets are from beginning of MakerNote, not # beginning of file header offset=self.offset self.offset+=note.field_offset # process note with bogus values (note is actually at offset 12) self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS) # reset to correct values self.endian=endian self.offset=offset return # Canon if make == 'Canon': self.dump_IFD(note.field_offset, 'MakerNote', dict=MAKERNOTE_CANON_TAGS) try: for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004), ('MakerNote Tag 0x0012', MAKERNOTE_CANON_TAG_0x012)): self.canon_decode_tag(self.tags[i[0]].values, i[1]) except: pass return # decode Canon MakerNote tag based on offset within tag # see http://www.burren.cx/david/canon.html by David Burren def canon_decode_tag(self, value, dict): for i in range(1, len(value)): x=dict.get(i, ('Unknown', )) if self.debug: print i, x name=x[0] if len(x) > 1: val=x[1].get(value[i], 'Unknown') else: val=value[i] # it's not a real IFD Tag but we fake one to make everybody # happy. this will have a "proprietary" type self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None, None, None) # process an image file (expects an open file object) # this is the function that has to deal with all the arbitrary nasty bits # of the EXIF standard def process_file(file, debug=0): # determine whether it's a JPEG or TIFF data=file.read(12) if data[0:4] in ['II*\x00', 'MM\x00*']: # it's a TIFF file file.seek(0) endian=file.read(1) file.read(1) offset=0 elif data[0:2] == '\xFF\xD8': # it's a JPEG file # skip JFIF style header(s) while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'): length=ord(data[4])*256+ord(data[5]) file.read(length-8) # fake an EXIF beginning of file data='\xFF\x00'+file.read(10) if data[2] == '\xFF' and data[6:10] == 'Exif': # detected EXIF header offset=file.tell() endian=file.read(1) else: # no EXIF information return {} else: # file format not recognized return {} # deal with the EXIF info we found if debug: print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format' hdr=EXIF_header(file, endian, offset, debug) ifd_list=hdr.list_IFDs() ctr=0 for i in ifd_list: if ctr == 0: IFD_name='Image' elif ctr == 1: IFD_name='Thumbnail' thumb_ifd=i else: IFD_name='IFD %d' % ctr if debug: print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i) hdr.dump_IFD(i, IFD_name) # EXIF IFD exif_off=hdr.tags.get(IFD_name+' ExifOffset') if exif_off: if debug: print ' EXIF SubIFD at offset %d:' % exif_off.values[0] hdr.dump_IFD(exif_off.values[0], 'EXIF') # Interoperability IFD contained in EXIF IFD intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset') if intr_off: if debug: print ' EXIF Interoperability SubSubIFD at offset %d:' \ % intr_off.values[0] hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability', dict=INTR_TAGS) # GPS IFD gps_off=hdr.tags.get(IFD_name+' GPSInfo') if gps_off: if debug: print ' GPS SubIFD at offset %d:' % gps_off.values[0] hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS) ctr+=1 # extract uncompressed TIFF thumbnail thumb=hdr.tags.get('Thumbnail Compression') if thumb and thumb.printable == 'Uncompressed TIFF': hdr.extract_TIFF_thumbnail(thumb_ifd) # JPEG thumbnail (thankfully the JPEG data is stored as a unit) thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat') if thumb_off: file.seek(offset+thumb_off.values[0]) size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0] hdr.tags['JPEGThumbnail']=file.read(size) # deal with MakerNote contained in EXIF IFD if hdr.tags.has_key('EXIF MakerNote'): hdr.decode_maker_note() # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote # since it's not allowed in a uncompressed TIFF IFD if not hdr.tags.has_key('JPEGThumbnail'): thumb_off=hdr.tags.get('MakerNote JPEGThumbnail') if thumb_off: file.seek(offset+thumb_off.values[0]) hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length) return hdr.tags # library test/debug function (dump given files) if __name__ == '__main__': import sys if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: try: file=open(filename, 'rb') except: print filename, 'unreadable' print continue print filename+':' # data=process_file(file, 1) # with debug info data=process_file(file) if not data: print 'No EXIF information found' continue x=data.keys() x.sort() for i in x: if i in ('JPEGThumbnail', 'TIFFThumbnail'): continue try: print ' %s (%s): %s' % \ (i, FIELD_TYPES[data[i].field_type][2], data[i].printable) except: print 'error', i, '"', data[i], '"' if data.has_key('JPEGThumbnail'): print 'File has JPEG thumbnail' print Photon-0.4.6/Photon/GIF.py0000644000175000017500000000140510331233473013345 0ustar lucluc# # This plugin identify a GIF file and returns the size # def read_headers(filename): h = None f = open(filename,'rb') if f: h = f.read(16) f.close() return h def identify(filename): h = read_headers(filename) if h == None: return None # This is the GIF header if h[:6] in ('GIF87a', 'GIF89a'): return ((ord(h[7])<<8 | ord(h[6])),(ord(h[9])<<8 | ord(h[8]))) return None if __name__ == "__main__": import sys import GIF if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=GIF.identify(filename) if info != None: print "%s (size = %d,%d)" % (filename,info[0],info[1]) else: print "%s is not a GIF file" % filename Photon-0.4.6/Photon/Image.py0000644000175000017500000000210710340265426013765 0ustar lucluc """This small library load and identify some image format """ from Photon import JPEG,GIF,PNG, PxM class Image: format = None size = (0,0) mode = None def open(filename): im = Image() size = JPEG.identify(filename) if size <> None: im.format = 'JPEG' im.size = size return im size = GIF.identify(filename) if size <> None: im.format = 'GIF' im.size = size im.mode = 'P' return im size = PNG.identify(filename) if size <> None: im.format = 'PNG' im.size = size return im (format, size) = PxM.identify(filename) if size <> None: im.format = format im.size = size return im raise IOError("Format not recognized") if __name__ == "__main__": import sys import Image if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: im=Image.open(filename) if im != None: print "%s (format=%s size <%dx%d> mode=%s)" % (filename,im.format,im.size[0],im.size[1],im.mode) else: print "%s is not recognized" % filename Photon-0.4.6/Photon/JPEG.py0000644000175000017500000000332710331233474013473 0ustar lucluc # # http://www.funducode.com/freec/Fileformats/format3/format3b.htm # def read_headers(filename,size=65536): h = None f = open(filename,'rb') if f: h = f.read(size) f.close() return h def identify(filename): # I've some photos made from a pentax that store the thumbnail at the beginning size = 65536 while True: try: h = read_headers(filename,size) if h == None: return None # This is the JPEG/JFIF header if h[0:2] == '\xff\xd8': return find_marker_SOFx(h[2:]) return None except IndexError, err: size = size * 2 print "Jpeg Resize and reload header ",size if size > 1048576: print "Can't identify this file as an JPEG file %s" % filename return None def find_marker_SOFx(h): i=0 while 1: # Hum, this is not a valid chunk if h[i] != "\xff": return None i+=1 # Skip any padding ff byte (this normal) while h[i] == 0xff: i+=1 # All SOF0 to SOF15 is valid (for me) not sure #print "Found marker %2.2x at index %d"% (ord(h[i]),i) if h[i] >= '\xc0' and h[i]<='\xcf' and h[i]!='\xc4' and h[i]!='\xcc': i+=1 return ((ord(h[i+5])<<8 | ord(h[i+6])),(ord(h[i+3])<<8 | ord(h[i+4]))) i+=1 # Skip to next marker i+= ord(h[i])<<8 | ord(h[i+1]) #print "New offset at " , i if __name__ == "__main__": # import profile # profile.run('main()') import sys import JPEG if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=JPEG.identify(filename) if info != None: print "%s (size = %d,%d)" % (filename,info[0],info[1]) else: print "%s is not a Jpeg file" % filename Photon-0.4.6/Photon/PNG.py0000644000175000017500000000153610331233474013372 0ustar lucluc# # This plugin identify a PNG file and returns the size # def read_headers(filename): h = None f = open(filename,'rb') if f: h = f.read(32) f.close() return h def identify(filename): h = read_headers(filename) if h == None: return None # This is the PNG header if h[:8] == '\x89PNG\r\n\x1a\n': w = ord(h[16])<<24 | ord(h[17])<<16 | ord(h[18])<<8 | ord(h[19]) h = ord(h[20])<<24 | ord(h[21])<<16 | ord(h[22])<<8 | ord(h[23]) return (w,h) return None if __name__ == "__main__": import sys import PNG if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=PNG.identify(filename) if info != None: print "%s (size = %d,%d)" % (filename,info[0],info[1]) else: print "%s is not a PNG file" % filename Photon-0.4.6/Photon/PxM.py0000644000175000017500000000200210340265426013441 0ustar lucluc # # PxM # Detects: # PPM (P6) # TODO: all others format :) # import re def identify_P6(h): if h[0] == '#': raise "I don't support comment in a PPM file" m = re.search("^(\d+)\s(\d+)", h) if not m: return (None, None) # The third line can be skipped return ('PPM',((int(m.group(1)), int(m.group(2))))) def identify(filename): h = None f = open(filename,'rb') if not f: return (None, None) h = f.read(1024) f.close() # PPM has an ASCII header on the first line if h[0:3] == 'P6\n': return identify_P6(h[3:]) return (None, None) if __name__ == "__main__": # import profile # profile.run('main()') import sys import PxM if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: (format, info)=PxM.identify(filename) if info != None: print "%s (format=%s; size = %d,%d)" % (filename,format, info[0],info[1]) else: print "%s is not a PPM,PGM,PBM file" % filename Photon-0.4.6/Photon/QuickTime.py0000644000175000017500000002140110331233474014632 0ustar lucluc# # Parse a QuickTime movie file produced by many digital camera # # http://developer.apple.com/documentation/QuickTime/QTFF/ # QuickTime is trademark of Apple # import struct from random import randrange quicktime_verbose = 0 def read_headers(filename,size=65536): h = None f = open(filename,'rb') if f: h = f.read(size) f.close() return h def open_atom(filename): f = open(filename,'rb') if f: return f return None def close_atom(f): f.close() def read_atom(f): h = f.read(8) if len(h) < 8: return None atom = {} s = struct.unpack(">L", h[0:4]) if s[0] < 8: return None atom['size'] = s[0] - 8 atom['type'] = h[4:8] atom['offset'] = f.tell() #atom['data'] = f.read(atom['size'] - 8) return atom def parse_PICT(f, atom): print "PICT size:%d " % atom['size'] atom['data'] = f.read(atom['size']) f.seek(-atom['size'],1) return 1 def parse_stbl(f, node, info): begin = f.tell() current = 0 while current < node['size']: atom = read_atom(f) if atom == None: break current += atom['size'] + 8 if atom['type'] == 'stsd': parse_stsd(f, atom, info) elif atom['type'] == 'stsz': parse_stsz(f, atom, info) elif atom['type'] == 'stco': parse_stco(f, atom, info) elif atom['type'] == 'stsc': parse_stsc(f, atom, info) else: f.seek(atom['size'],1) f.seek(begin,0) def parse_stsd(f, atom, info): data = f.read(atom['size']) (version, flags0, flags1, flags2, entries) = struct.unpack(">B3BL", data[0:8]) i = 8 while entries: (size, format, refindex) = struct.unpack(">LL6xH", data[i:i+16]) #print "size:%d format:%c%c%c%c refindex=%d" % (size, format>>24, (format>>16)&255, (format>>8)&255, format&255, refindex) if info.has_key('format'): print "Warning i found severall different format, is it correct. Please send your video to the author" return info['format'] = format (width, height) = struct.unpack(">HH", data[i+32:i+36]) info['width'] = width info['height'] = height i+=size entries-=1 def parse_stsc(f, atom, info): data = f.read(atom['size']) (version, entries) = struct.unpack(">B3xL", data[0:8]) if quicktime_verbose>0: print "stsc entries: %d" % (entries) i = 8 while entries: (firstchunk, sampleperchunk, sampleid) = struct.unpack(">LLL", data[i:i+12]) if quicktime_verbose>1: print "firstchunk=%d sampleperchunk=%d sampleid=%d" % (firstchunk, sampleperchunk, sampleid) firstchunk-=1 info['chunk_info'].append((firstchunk, sampleperchunk)) i+=12 entries-=1 def parse_stsz(f, atom, info): data = f.read(atom['size']) (version, defsize, entries) = struct.unpack(">B3xLL", data[0:12]) if quicktime_verbose>0: print "stsz frames: %d" % (entries) info['frames'] = entries info['sample_size'] = struct.unpack(">%dL" % entries , data[12:]) def parse_stco(f, atom, info): data = f.read(atom['size']) (version, entries) = struct.unpack(">B3xL", data[0:8]) if quicktime_verbose>0: print "stco entries: %d" % (entries) info['chunks'] = entries info['chunk_offset'] = struct.unpack(">%dL" % entries , data[8:]) # # Parse an atom an recurse if this atom contains other atoms # This is very small parser, so we didn't try to parse every track # We stop after found the first video track # def parse_moov(f, parent, info): begin = f.tell() current = 0 while current < parent['size']: atom = read_atom(f) if atom == None: break if quicktime_verbose>0: print "+ %s" % atom['type'] # Yes we have found a vmhd chunk, so this movie have a video track if atom['type'] == 'vmhd': info['video_chunk'] = parent if info['video_chunk'] != None and info['video_chunk'] == parent: if atom['type'] == 'stbl': parse_stbl(f, parent, info) if atom['type'] in ('mdia', 'mvhd', 'trak', 'minf', 'mdhd', 'hdlr', 'stbl'): parse_moov(f, atom, info) current += atom['size'] + 8 f.seek(atom['size'],1) f.seek(begin,0) def parse_atom(f, parent, prefix): begin = f.tell() current = 0 while current < parent['size']: atom = read_atom(f) if atom == None: break print "%s \"%s\" (size=%d)" % (prefix, atom['type'], atom['size']) if atom['type'] in ('mdia', 'mvhd', 'trak', 'minf', 'mdhd', 'hdlr', 'stbl'): parse_atom(f, atom, prefix + '+') current += atom['size'] + 8 f.seek(atom['size'],1) f.seek(begin,0) def extract_jpeg_files(moviefile, info, basename="/tmp/photon%8.8d.jpg"): f = open_atom(moviefile) frame = 0 for chunk in range(info['chunks']): if quicktime_verbose>1: print "chunks: %d => offset: %d" % (chunk, info['chunk_offset'][chunk]) f.seek(info['chunk_offset'][chunk],0) samplesbychunk = -1 for chunk_info in info['chunk_info']: if chunk_info[0] <= chunk: samplesbychunk = chunk_info[1] else: pass for jjj in range(samplesbychunk): size = info['sample_size'][frame] out = open(basename % frame, "wb"); h = f.read(2) if h[0:2] == '\xff\xd8': out.write(h) out.write(f.read(size-2)) else: print "Warning this is not a jpeg file (frame=%d)" % frame out.close() frame+=1 # # Extract one image from the file # def extract_jpeg_file(moviefile, info, outfile, frame): f = open_atom(moviefile) # Calculate the offset for our image chunk_group = 0 chunk_frame_start = 0 chunk_frame_end = 0 chunk_frames = 0 for chunk_info in info['chunk_info']: if chunk_frames == 0: chunk_group = chunk_info[0] chunk_frames = chunk_info[1] else: chunk_frame_end = chunk_frame_start + (chunk_info[0] - chunk_group) * chunk_frames if frame >= chunk_frame_start and frame < chunk_frame_end: break chunk_frame_start = chunk_frame_end chunk_group = chunk_info[0] chunk_frames = chunk_info[1] (chunk, offimg) = divmod(frame - chunk_frame_start, chunk_frames) chunk += chunk_group if quicktime_verbose>1: print "%4.4d | chunk=%d /// offimg=%d" % (frame, chunk, offimg) offset = info['chunk_offset'][chunk] for j in xrange(offimg): offset += info['sample_size'][frame-j-1] # Go to the beginning of the image f.seek(offset,0) size = info['sample_size'][frame] out = open(outfile, "wb") h = f.read(2) if h[0:2] == '\xff\xd8': # To be sure that we extract a Jpeg file out.write(h) out.write(f.read(size-2)) else: print "Warning: this is not a jpeg file (frame=%d)" % frame #out.write(f.read(size)) out.close() close_atom(f) def __identify(filename): if struct.calcsize(">L") != 4: print "A long is not equal to 4 bytes with your python installation. Abording" return None h = read_headers(filename,12) if h == None: return None # is this a QuickTime file ? if h[4:8] not in ('pnot', 'moov', 'mdat'): return None # Parse the file into atom (chunk), and try to find a # mdat and moov chunk f = open_atom(filename) found_mdat = found_moov = 0 info = {} info['chunk_info'] = list() while True: atom = read_atom(f) if atom == None: break #print "Found a new atom \"%s\" (size=%d)" % (atom['type'], atom['size']) if atom['type'] in ('pnot', 'PICT'): pass elif atom['type'] == 'mdat': found_mdat = 1 info['mdat_offset'] = f.tell() elif atom['type'] == 'moov': found_moov = 1 info['video_chunk'] = None parse_moov(f, atom, info) if quicktime_verbose>0: parse_atom(f, atom, "+") else: print "Abording unknow Atom type" #break f.seek(atom['size'],1) close_atom(f) if not (found_moov and found_mdat): return None if not info.has_key('format'): return None if info['format'] == 0x6a706567: # 'jpeg' #print "It's a jpeg movie" return info return None def identify(filename): return __identify(filename) def extract_random_picture(moviefile, pictfile): video = __identify(moviefile) if video == None: return None r = randrange(0, video['frames']) extract_jpeg_file(moviefile, video, pictfile, r) return True def extract_picture(moviefile, pictfile, frame): video = __identify(moviefile) if video == None: return None extract_jpeg_file(moviefile, video, pictfile, frame) return True if __name__ == "__main__": import sys import QuickTime if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=QuickTime.identify(filename) if info != None: print "%s (format='%x', frames='%d')" % (filename,info['format'],info['frames']) #extract_jpeg_files(filename, info) for frame in xrange(info['frames']): #for frame in xrange(1138,1142): extract_jpeg_file(filename, info, "/tmp/photon%8.8d.jpg" % frame, frame) else: print "%s is not a Quicktime movie file" % filename Photon-0.4.6/Photon/RAW.py0000644000175000017500000000444410713562772013412 0ustar lucluc# # This plugin identify, convert a raw image produce by a digital camera # To do the job, we use the dcraw program, exiftool, and any ppm2xxx program # import os, string, commands, popen2 class RAW: format = None def __init__(self): pass def convert(self, outputfilename): dcrawcmd = "dcraw -w -c \"%s\" > \"%s\"" % (self.filename, outputfilename) status , resultstring = commands.getstatusoutput(dcrawcmd) if status != 0: print "ERROR: dcraw can not convert the image into a PPM file (err=%d)" % status print "dcraw output:" print resultstring return None return 1 # ppmformat = None # linecounter = 0 # dcrawcmd = "dcraw -w -2 -c \"%s\"" % self.filename # handle = Popen3(dcrawcmd, False) # while handle.poll() == -1: # line = self.fromchild.readline() # if linecounter == 0: # First line is always the format # if line <> 'P6': # raise IOError("Format not recognized \"%\"" % line) # linecounter+=1 # elif linecounter == 1: # Second line is the size of the image # size=string.split(line," ") # print "Raw image size %dx%d" % size # linecounter+=1 # elif linecounter == 2: # Third line precise the max value for a color (255 or 65535) # maxvalue=int(line) # linecounter+=1 # else: pass def identify(filename): # Try to identify the image using dcraw dcrawcmd = "dcraw -i \"%s\"" % filename status , resultstring = commands.getstatusoutput(dcrawcmd) if status != 0: print "ERROR: dcraw does not recognize the image (err=%d)" % status print "dcraw output:" print resultstring return None # Get the image format idx = string.rfind(resultstring," is a ") if idx < 0: print "ERROR: Strange the string doesn't contains the magic string." print resultstring return None # All string finish by a " image." format = resultstring[idx+6:-8] raw = RAW() raw.format = format raw.filename = filename return raw if __name__ == "__main__": import sys import RAW if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: info=RAW.identify(filename) if info != None: print "%s (format = %s)" % (filename,info.format) else: print "%s is not a RAW file" % filename Photon-0.4.6/Photon/Video.py0000644000175000017500000000424110340265426014012 0ustar lucluc """This small library load and identify some image format """ from Photon import QuickTime, AVI from random import randrange class Video: format = None size = (0,0) mode = None plugin = None moviefile = None privatedata = None def __init__(self): pass def get_random_frame(self, pictfile): frame = randrange(0, self.frames) if self.plugin == 'QuickTime': return QuickTime.extract_jpeg_file(self.moviefile, self.privatedata, pictfile, frame) elif self.plugin == 'AVI': return AVI.extract_jpeg_file(self.moviefile, self.privatedata, pictfile, frame) else: return None def get_frame(self, pictfile, frame): if self.plugin == 'QuickTime': return QuickTime.extract_jpeg_file(self.moviefile, self.privatedata, pictfile, frame) elif self.plugin == 'AVI': return AVI.extract_jpeg_file(self.moviefile, self.privatedata, pictfile, frame) else: return None def identify(filename): info = AVI.identify(filename) if info <> None: video = Video() video.privatedata = info video.format = 'AVI (%s)' % info['format'] video.size = info['video_size'] video.frames = info['frames'] video.plugin = 'AVI' video.moviefile = filename return video info = QuickTime.identify(filename) if info <> None: video = Video() video.privatedata = info video.format = 'QuickTime (%c%c%c%c)' % (((info['format']>>24)&255),((info['format']>>16)&255),((info['format']>>8)&255),((info['format'])&255)) video.size = (info['width'] , info['height']) video.frames = info['frames'] video.plugin = 'QuickTime' video.moviefile = filename return video raise IOError("Format not recognized") if __name__ == "__main__": import sys import Video if len(sys.argv) < 2: print 'Usage: %s files...\n' % sys.argv[0] sys.exit(0) for filename in sys.argv[1:]: im=Video.open(filename) if im != None: print "%s (format=%s size <%dx%d> mode=%s)" % (filename,im.format,im.size[0],im.size[1]) else: print "%s is not recognized" % filename Photon-0.4.6/Photon/__init__.py0000644000175000017500000000002210713565226014501 0ustar luclucversion = '0.4.6' Photon-0.4.6/Photon/airspeed.py0000644000175000017500000006141310340265426014544 0ustar lucluc#!/usr/bin/env python import re, operator, os import StringIO # cStringIO has issues with unicode __all__ = ['Template', 'TemplateError', 'TemplateSyntaxError', 'CachingFileLoader'] ############################################################################### # Compatibility for old Pythons & Jython ############################################################################### try: True except NameError: False, True = 0, 1 try: dict except NameError: from UserDict import UserDict class dict(UserDict): def __init__(self): self.data = {} try: operator.__gt__ except AttributeError: operator.__gt__ = lambda a, b: a > b operator.__lt__ = lambda a, b: a < b operator.__ge__ = lambda a, b: a >= b operator.__le__ = lambda a, b: a <= b operator.__eq__ = lambda a, b: a == b operator.__ne__ = lambda a, b: a != b try: basestring def is_string(s): return isinstance(s, basestring) except NameError: def is_string(s): return type(s) == type('') ############################################################################### # Public interface ############################################################################### def boolean_value(variable_value): if variable_value == False: return False return not (variable_value is None) class Template: def __init__(self, content): self.content = content self.root_element = None def merge(self, namespace, loader=None): output = StringIO.StringIO() self.merge_to(namespace, output, loader) return output.getvalue() def ensure_compiled(self): if not self.root_element: self.root_element = TemplateBody(self.content) def merge_to(self, namespace, fileobj, loader=None): if loader is None: loader = NullLoader() self.ensure_compiled() self.root_element.evaluate(fileobj, namespace, loader) class TemplateError(Exception): pass class TemplateSyntaxError(TemplateError): def __init__(self, element, expected): self.element = element self.text_understood = element.full_text()[:element.end] self.line = 1 + self.text_understood.count('\n') self.column = len(self.text_understood) - self.text_understood.rfind('\n') got = element.next_text() if len(got) > 40: got = got[:36] + ' ...' Exception.__init__(self, "line %d, column %d: expected %s in %s, got: %s ..." % (self.line, self.column, expected, self.element_name(), got)) def get_position_strings(self): error_line_start = 1 + self.text_understood.rfind('\n') if '\n' in self.element.next_text(): error_line_end = self.element.next_text().find('\n') + self.element.end else: error_line_end = len(self.element.full_text()) error_line = self.element.full_text()[error_line_start:error_line_end] caret_pos = self.column return [error_line, ' ' * (caret_pos - 1) + '^'] def element_name(self): return re.sub('([A-Z])', lambda m: ' ' + m.group(1).lower(), self.element.__class__.__name__).strip() class NullLoader: def load_text(self, name): raise TemplateError("no loader available for '%s'" % name) def load_template(self, name): raise self.load_text(name) class CachingFileLoader: def __init__(self, basedir): self.basedir = basedir self.known_templates = {} # name -> (template, file_mod_time) def filename_of(self, name): return os.path.join(self.basedir, name) def load_text(self, name): f = open(os.path.join(self.basedir, name)) try: return f.read() finally: f.close() def load_template(self, name): mtime = os.path.getmtime(self.filename_of(name)) if self.known_templates.has_key(name): template, prev_mtime = self.known_templates[name] if mtime <= prev_mtime: return template template = Template(self.load_text(name)) template.ensure_compiled() self.known_templates[name] = (template, mtime) return template ############################################################################### # Internals ############################################################################### class NoMatch(Exception): pass class LocalNamespace(dict): def __init__(self, parent): dict.__init__(self) self.parent = parent def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: parent_value = self.parent[key] self[key] = parent_value return parent_value def __repr__(self): return dict.__repr__(self) + '->' + repr(self.parent) class _Element: def __init__(self, text, start=0): self._full_text = text self.start = self.end = start self.parse() def next_text(self): return self._full_text[self.end:] def my_text(self): return self._full_text[self.start:self.end] def full_text(self): return self._full_text def syntax_error(self, expected): return TemplateSyntaxError(self, expected) def identity_match(self, pattern): m = pattern.match(self._full_text, self.end) if not m: raise NoMatch() self.end = m.start(pattern.groups) return m.groups()[:-1] def next_match(self, pattern): m = pattern.match(self._full_text, self.end) if not m: return False self.end = m.start(pattern.groups) return m.groups()[:-1] def optional_match(self, pattern): m = pattern.match(self._full_text, self.end) if not m: return False self.end = m.start(pattern.groups) return True def require_match(self, pattern, expected): m = pattern.match(self._full_text, self.end) if not m: raise self.syntax_error(expected) self.end = m.start(pattern.groups) return m.groups()[:-1] def next_element(self, element_spec): if callable(element_spec): element = element_spec(self._full_text, self.end) self.end = element.end return element else: for element_class in element_spec: try: element = element_class(self._full_text, self.end) except NoMatch: pass else: self.end = element.end return element raise NoMatch() def require_next_element(self, element_spec, expected): if callable(element_spec): try: element = element_spec(self._full_text, self.end) except NoMatch: raise self.syntax_error(expected) else: self.end = element.end return element else: for element_class in element_spec: try: element = element_class(self._full_text, self.end) except NoMatch: pass else: self.end = element.end return element expected = ', '.join([cls.__name__ for cls in element_spec]) raise self.syntax_error(self, 'one of: ' + expected) class Text(_Element): PLAIN = re.compile(r'((?:[^\\\$#]+|\\[\$#])+|\$[^!\{a-z0-9_]|\$$|\\.)(.*)$', re.S + re.I) ESCAPED_CHAR = re.compile(r'\\([\\\$#])') def parse(self): text, = self.identity_match(self.PLAIN) def unescape(match): return match.group(1) self.text = self.ESCAPED_CHAR.sub(unescape, text) def evaluate(self, stream, namespace, loader): stream.write(self.text) class IntegerLiteral(_Element): INTEGER = re.compile(r'(\d+)(.*)', re.S) def parse(self): self.value, = self.identity_match(self.INTEGER) self.value = int(self.value) def calculate(self, namespace, loader): return self.value class StringLiteral(_Element): STRING = re.compile(r"'((?:\\['nrbt\\\\\\$]|[^'\n\r\\])+)'(.*)", re.S) ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])") def parse(self): value, = self.identity_match(self.STRING) def unescape(match): return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\', "'": "'"}.get(match.group(1), '\\' + match.group(1)) self.value = self.ESCAPED_CHAR.sub(unescape, value) def calculate(self, namespace, loader): return self.value class InterpolatedStringLiteral(StringLiteral): STRING = re.compile(r'"((?:\\["nrbt\\\\\\$]|[^"\n\r\\])+)"(.*)', re.S) ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])') def parse(self): StringLiteral.parse(self) self.block = Block(self.value, 0) def calculate(self, namespace, loader): output = StringIO.StringIO() self.block.evaluate(output, namespace, loader) return output.getvalue() class Range(_Element): RANGE = re.compile(r'(\-?\d+)[ \t]*\.\.[ \t]*(\-?\d+)(.*)$') def parse(self): self.value1, self.value2 = map(int, self.identity_match(self.RANGE)) def calculate(self, namespace, loader): if self.value2 < self.value1: return xrange(self.value1, self.value2 - 1, -1) return xrange(self.value1, self.value2 + 1) class ValueList(_Element): COMMA = re.compile(r'\s*,\s*(.*)$', re.S) def parse(self): self.values = [] try: value = self.next_element(Value) except NoMatch: pass else: self.values.append(value) while self.optional_match(self.COMMA): value = self.require_next_element(Value, 'value') self.values.append(value) def calculate(self, namespace, loader): return [value.calculate(namespace, loader) for value in self.values] class _EmptyValues: def calculate(self, namespace, loader): return [] class ArrayLiteral(_Element): START = re.compile(r'\[[ \t]*(.*)$', re.S) END = re.compile(r'[ \t]*\](.*)$', re.S) values = _EmptyValues() def parse(self): self.identity_match(self.START) try: self.values = self.next_element((Range, ValueList)) except NoMatch: pass self.require_match(self.END, ']') self.calculate = self.values.calculate class Value(_Element): def parse(self): self.expression = self.next_element((SimpleReference, IntegerLiteral, StringLiteral, InterpolatedStringLiteral, ArrayLiteral, Condition, UnaryOperatorValue)) def calculate(self, namespace, loader): return self.expression.calculate(namespace, loader) class NameOrCall(_Element): NAME = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)(.*)$', re.S) parameters = None def parse(self): self.name, = self.identity_match(self.NAME) try: self.parameters = self.next_element(ParameterList) except NoMatch: pass def calculate(self, current_object, loader, top_namespace): look_in_dict = True if not isinstance(current_object, LocalNamespace): try: result = getattr(current_object, self.name) look_in_dict = False except AttributeError: pass if look_in_dict: try: result = current_object[self.name] except KeyError: result = None except TypeError: result = None except AttributeError: result = None if result is None: return None ## TODO: an explicit 'not found' exception? if self.parameters is not None: result = result(*self.parameters.calculate(top_namespace, loader)) return result class SubExpression(_Element): DOT = re.compile('\.(.*)', re.S) def parse(self): self.identity_match(self.DOT) self.expression = self.next_element(VariableExpression) def calculate(self, current_object, loader, global_namespace): return self.expression.calculate(current_object, loader, global_namespace) class VariableExpression(_Element): subexpression = None def parse(self): self.part = self.next_element(NameOrCall) try: self.subexpression = self.next_element(SubExpression) except NoMatch: pass def calculate(self, namespace, loader, global_namespace=None): if global_namespace == None: global_namespace = namespace value = self.part.calculate(namespace, loader, global_namespace) if self.subexpression: value = self.subexpression.calculate(value, loader, global_namespace) return value class ParameterList(_Element): START = re.compile(r'\(\s*(.*)$', re.S) COMMA = re.compile(r'\s*,\s*(.*)$', re.S) END = re.compile(r'\s*\)(.*)$', re.S) values = _EmptyValues() def parse(self): self.identity_match(self.START) try: self.values = self.next_element(ValueList) except NoMatch: pass self.require_match(self.END, ')') def calculate(self, namespace, loader): return self.values.calculate(namespace, loader) class Placeholder(_Element): START = re.compile(r'\$(!?)(\{?)(.*)$', re.S) CLOSING_BRACE = re.compile(r'\}(.*)$', re.S) def parse(self): self.silent, self.braces = self.identity_match(self.START) self.expression = self.require_next_element(VariableExpression, 'expression') if self.braces: self.require_match(self.CLOSING_BRACE, '}') def evaluate(self, stream, namespace, loader): value = self.expression.calculate(namespace, loader) if value is None: if self.silent: value = '' else: value = self.my_text() if is_string(value): stream.write(value) else: stream.write(str(value)) class SimpleReference(_Element): LEADING_DOLLAR = re.compile('\$(.*)', re.S) def parse(self): self.identity_match(self.LEADING_DOLLAR) self.expression = self.require_next_element(VariableExpression, 'name') self.calculate = self.expression.calculate class Null: def evaluate(self, stream, namespace, loader): pass class Comment(_Element, Null): COMMENT = re.compile('#(?:#.*?(?:\n|$)|\*.*?\*#(?:[ \t]*\n)?)(.*)$', re.M + re.S) def parse(self): self.identity_match(self.COMMENT) class BinaryOperator(_Element): BINARY_OP = re.compile(r'\s*(>=|<=|<|==|!=|>|\|\||&&)\s*(.*)$', re.S) OPERATORS = {'>' : operator.__gt__, '>=': operator.__ge__, '<' : operator.__lt__, '<=': operator.__le__, '==': operator.__eq__, '!=': operator.__ne__, '||': lambda a,b : boolean_value(a) or boolean_value(b), '&&': lambda a,b : boolean_value(a) and boolean_value(b)} def parse(self): op_string, = self.identity_match(self.BINARY_OP) self.apply_to = self.OPERATORS[op_string] class UnaryOperatorValue(_Element): UNARY_OP = re.compile(r'\s*(!)\s*(.*)$', re.S) OPERATORS = {'!': operator.__not__} def parse(self): op_string, = self.identity_match(self.UNARY_OP) self.value = self.next_element(Value) self.op = self.OPERATORS[op_string] def calculate(self, namespace, loader): return self.op(self.value.calculate(namespace, loader)) class Condition(_Element): START = re.compile(r'\(\s*(.*)$', re.S) END = re.compile(r'\s*\)(.*)$', re.S) binary_operator = None value2 = None def parse(self): self.identity_match(self.START) self.value = self.next_element(Value) try: self.binary_operator = self.next_element(BinaryOperator) self.value2 = self.require_next_element(Value, 'value') except NoMatch: pass self.require_match(self.END, ') or >') def calculate(self, namespace, loader): if self.binary_operator is None: return self.value.calculate(namespace, loader) value1, value2 = self.value.calculate(namespace, loader), self.value2.calculate(namespace, loader) return self.binary_operator.apply_to(value1, value2) class End(_Element): END = re.compile(r'#end(.*)', re.I + re.S) def parse(self): self.identity_match(self.END) class ElseBlock(_Element): START = re.compile(r'#else(.*)$', re.S + re.I) def parse(self): self.identity_match(self.START) self.block = self.require_next_element(Block, 'block') self.evaluate = self.block.evaluate class ElseifBlock(_Element): START = re.compile(r'#elseif\b\s*(.*)$', re.S + re.I) def parse(self): self.identity_match(self.START) self.condition = self.require_next_element(Condition, 'condition') self.block = self.require_next_element(Block, 'block') self.calculate = self.condition.calculate self.evaluate = self.block.evaluate class IfDirective(_Element): START = re.compile(r'#if\b\s*(.*)$', re.S + re.I) else_block = Null() def parse(self): self.identity_match(self.START) self.condition = self.next_element(Condition) self.block = self.require_next_element(Block, "block") self.elseifs = [] while True: try: self.elseifs.append(self.next_element(ElseifBlock)) except NoMatch: break try: self.else_block = self.next_element(ElseBlock) except NoMatch: pass self.require_next_element(End, '#else, #elseif or #end') def evaluate(self, stream, namespace, loader): if self.condition.calculate(namespace, loader): self.block.evaluate(stream, namespace, loader) else: for elseif in self.elseifs: if elseif.calculate(namespace, loader): elseif.evaluate(stream, namespace, loader) return self.else_block.evaluate(stream, namespace, loader) class Assignment(_Element): START = re.compile(r'\s*\(\s*\$([a-z_][a-z0-9_]*)\s*=\s*(.*)$', re.S + re.I) END = re.compile(r'\s*\)(?:[ \t]*\r?\n)?(.*)$', re.S + re.M) def parse(self): self.var_name, = self.identity_match(self.START) self.value = self.require_next_element(Value, "value") self.require_match(self.END, ')') def evaluate(self, stream, namespace, loader): namespace[self.var_name] = self.value.calculate(namespace, loader) class MacroDefinition(_Element): START = re.compile(r'#macro\b(.*)', re.S + re.I) OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S) NAME = re.compile(r'\s*([a-z][a-z_0-9]*)\b(.*)', re.S + re.I) CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S) ARG_NAME = re.compile(r'[ \t]+\$([a-z][a-z_0-9]*)(.*)$', re.S + re.I) RESERVED_NAMES = ('if', 'else', 'elseif', 'set', 'macro', 'foreach', 'parse', 'include', 'stop', 'end') def parse(self): self.identity_match(self.START) self.require_match(self.OPEN_PAREN, '(') self.macro_name, = self.require_match(self.NAME, 'macro name') if self.macro_name.lower() in self.RESERVED_NAMES: raise self.syntax_error('non-reserved name') self.arg_names = [] while True: m = self.next_match(self.ARG_NAME) if not m: break self.arg_names.append(m[0]) self.require_match(self.CLOSE_PAREN, ') or arg name') self.block = self.require_next_element(Block, 'block') self.require_next_element(End, 'block') def evaluate(self, stream, namespace, loader): macro_key = '#' + self.macro_name.lower() if namespace.has_key(macro_key): raise Exception("cannot redefine macro") namespace[macro_key] = self def execute_macro(self, stream, namespace, arg_value_elements, loader): if len(arg_value_elements) != len(self.arg_names): raise Exception("expected %d arguments, got %d" % (len(self.arg_names), len(arg_value_elements))) macro_namespace = LocalNamespace(namespace) for arg_name, arg_value in zip(self.arg_names, arg_value_elements): macro_namespace[arg_name] = arg_value.calculate(namespace, loader) self.block.evaluate(stream, macro_namespace, loader) class MacroCall(_Element): START = re.compile(r'#([a-z][a-z_0-9]*)\b(.*)', re.S + re.I) OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S) CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S) SPACE = re.compile(r'[ \t]+(.*)$', re.S) def parse(self): self.macro_name, = self.identity_match(self.START) self.macro_name = self.macro_name.lower() self.args = [] if self.macro_name in MacroDefinition.RESERVED_NAMES or self.macro_name.startswith('end'): raise NoMatch() self.require_match(self.OPEN_PAREN, '(') while True: try: self.args.append(self.next_element(Value)) except NoMatch: break if not self.optional_match(self.SPACE): break self.require_match(self.CLOSE_PAREN, 'argument value or )') def evaluate(self, stream, namespace, loader): try: macro = namespace['#' + self.macro_name] except KeyError: raise Exception('no such macro: ' + self.macro_name) macro.execute_macro(stream, namespace, self.args, loader) class IncludeDirective(_Element): START = re.compile(r'#include\b(.*)', re.S + re.I) OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S) CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S) def parse(self): self.identity_match(self.START) self.require_match(self.OPEN_PAREN, '(') self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, SimpleReference), 'template name') self.require_match(self.CLOSE_PAREN, ')') def evaluate(self, stream, namespace, loader): stream.write(loader.load_text(self.name.calculate(namespace, loader))) class ParseDirective(_Element): START = re.compile(r'#parse\b(.*)', re.S + re.I) OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S) CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S) def parse(self): self.identity_match(self.START) self.require_match(self.OPEN_PAREN, '(') self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, SimpleReference), 'template name') self.require_match(self.CLOSE_PAREN, ')') def evaluate(self, stream, namespace, loader): template = loader.load_template(self.name.calculate(namespace, loader)) ## TODO: local namespace? template.merge_to(namespace, stream, loader=loader) class SetDirective(_Element): START = re.compile(r'#set\b(.*)', re.S + re.I) def parse(self): self.identity_match(self.START) self.assignment = self.require_next_element(Assignment, 'assignment') def evaluate(self, stream, namespace, loader): self.assignment.evaluate(stream, namespace, loader) class ForeachDirective(_Element): START = re.compile(r'#foreach\b(.*)$', re.S + re.I) OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S) IN = re.compile(r'[ \t]+in[ \t]+(.*)$', re.S) LOOP_VAR_NAME = re.compile(r'\$([a-z_][a-z0-9_]*)(.*)$', re.S + re.I) CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S) def parse(self): ## Could be cleaner b/c syntax error if no '(' self.identity_match(self.START) self.require_match(self.OPEN_PAREN, '(') self.loop_var_name, = self.require_match(self.LOOP_VAR_NAME, 'loop var name') self.require_match(self.IN, 'in') self.value = self.next_element(Value) self.require_match(self.CLOSE_PAREN, ')') self.block = self.next_element(Block) self.require_next_element(End, '#end') def evaluate(self, stream, namespace, loader): iterable = self.value.calculate(namespace, loader) counter = 1 try: if iterable is None: return if hasattr(iterable, 'keys'): iterable = iterable.keys() if not hasattr(iterable, '__getitem__'): raise ValueError("value for $%s is not iterable in #foreach: %s" % (self.loop_var_name, iterable)) for item in iterable: namespace = LocalNamespace(namespace) namespace['velocityCount'] = counter namespace[self.loop_var_name] = item self.block.evaluate(stream, namespace, loader) counter += 1 except TypeError: raise class TemplateBody(_Element): def parse(self): self.block = self.next_element(Block) if self.next_text(): raise self.syntax_error('block element') def evaluate(self, stream, namespace, loader): namespace = LocalNamespace(namespace) self.block.evaluate(stream, namespace, loader) class Block(_Element): def parse(self): self.children = [] while True: try: self.children.append(self.next_element((Text, Placeholder, Comment, IfDirective, SetDirective, ForeachDirective, IncludeDirective, ParseDirective, MacroDefinition, MacroCall))) except NoMatch: break def evaluate(self, stream, namespace, loader): for child in self.children: child.evaluate(stream, namespace, loader) Photon-0.4.6/debian/0000755000175000017500000000000010713565311012344 5ustar luclucPhoton-0.4.6/debian/changelog0000644000175000017500000000021410713564115014214 0ustar luclucphoton (0.4.5-0.luc0) unstable; urgency=low * New upstream release -- Luc Saillard Mon, 05 Nov 2007 10:54:12 +0100 Photon-0.4.6/debian/compat0000644000175000017500000000000210331233474013540 0ustar lucluc4 Photon-0.4.6/debian/control0000644000175000017500000000173510331233474013753 0ustar luclucSource: photon Section: graphics Priority: optional Maintainer: Luc Saillard Build-Depends: debhelper (>= 4.0.0), python2.2-dev | python2.3-dev | python2.4-dev Standards-Version: 3.6.1 Package: photon Architecture: all Depends: ${python:Depends} Recommends: gimp | python-imaging Suggests: dcraw Description: Photon is a static HTML gallery generator Photon is a photo album with a clean design. . Features: * static HTML pages (you can put all pages and images on a CD-ROM) * slideshow (use javascript optional) * can use gimp to resize picture * navigation between the image can use the keyboard (use javascript optional) * works in any browser (Mozilla, Netscape Navigator 4.x, Konqueror, Opera) * Each image can have a comment (with HTML tags) * Information about the image (if taken from a digital picture) can be draw * thumbnail image size can be choosen by the user * output images can be scalled down * control the number of thumbnail in a page. Photon-0.4.6/debian/copyright0000644000175000017500000000033610331233474014277 0ustar luclucThis package was debianized by Luc Saillard on Thu, 16 Jun 2005 14:46:26 +0200. It was downloaded from http://saillard.org/ Copyright Holder: Luc Saillard License: GPLv2 Photon-0.4.6/debian/dirs0000644000175000017500000000001010331233474013215 0ustar luclucusr/lib Photon-0.4.6/debian/docs0000644000175000017500000000001410331233474013210 0ustar luclucBUGS README Photon-0.4.6/debian/rules0000755000175000017500000000372410713564133013433 0ustar lucluc#!/usr/bin/make -f # -*- makefile -*- # Sample debian/rules that uses debhelper. # This file was originally written by Joey Hess and Craig Small. # As a special exception, when this file is copied by dh-make into a # dh-make output file, you may use that output file without restriction. # This special exception was added by Craig Small in version 0.37 of dh-make. # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 PYTHON22:=python2.2 PYTHON23:=python2.3 PYTHON24:=python2.4 PYTHON25:=python2.5 PYTHON:=python2.3 installdetect: dh_testdir dh_testroot dh_clean -k dh_installdirs # Add here commands to install the package into debian/photon. $(PYTHON) setup.py config $(PYTHON) setup.py build $(PYTHON) setup.py install --prefix $(CURDIR)/debian/photon/usr rm -f $(CURDIR)/debian/photon/usr/lib/$(PYTHON)/site-packages/Photon/*.pyc install23: dh_testdir dh_testroot dh_clean -k dh_installdirs # Add here commands to install the package into debian/photon. $(PYTHON23) setup.py config $(PYTHON23) setup.py build $(PYTHON23) setup.py install --prefix $(CURDIR)/debian/$(PYTHON23)-photon/usr rm -f $(CURDIR)/debian/$(PYTHON23)-photon/usr/lib/$(PYTHON23)/site-packages/Photon/*.pyc install24: dh_testdir dh_testroot dh_clean -k dh_installdirs # Add here commands to install the package into debian/photon. $(PYTHON24) setup.py config $(PYTHON24) setup.py build $(PYTHON24) setup.py install --prefix $(CURDIR)/debian/$(PYTHON24)-photon/usr rm -f $(CURDIR)/debian/$(PYTHON24)-photon/usr/lib/$(PYTHON24)/site-packages/Photon/*.pyc install: installdetect # Build architecture-dependent files here. binary-arch: install dh_testdir dh_testroot dh_installchangelogs ChangeLog dh_installdocs # dh_installmenu dh_installman dh_link dh_strip dh_compress dh_fixperms dh_python dh_installdeb dh_shlibdeps dh_gencontrol dh_md5sums dh_builddeb binary: binary-indep binary-arch .PHONY: build clean binary-indep binary-arch binary install configure Photon-0.4.6/images/0000755000175000017500000000000010713565311012367 5ustar luclucPhoton-0.4.6/images/blank.gif0000644000175000017500000000005210331233473014137 0ustar luclucGIF89a€ÿÿÿ!ù,@D;Photon-0.4.6/images/filesave.png0000644000175000017500000000150610331233474014673 0ustar lucluc‰PNG  IHDRóÿagAMA¯È7étEXtSoftwareAdobe ImageReadyqÉe<ØIDATxÚbüÿÿ?% €X@Ä’¥Gîqr± }ÿú‹áßß? _¾~gøđá Ă—/ß¾~ưÁđíÛw ₫ÁđưûO†wï¿0\¼xơă‡÷÷“ÿÿ¿¾ €@.X·₫ô×O?₫ưÿ÷ïßÿiË₫ÉưÿÄÅG`>:¾zïíÿâÚÿÙÙ̀ŸôÈŸ?ÿøưáóo†ï¿Nœ¿ĂđàÉ+†¿±{íơ»Ï _ÿaààäâñ́…¯_¿1|:•“ơ?ƒ§ƒ>ƒ¥6ƒ¶ªV~üøÁđäác†ß¿ưñbÀ7?1<ûơ‹ÁÆDhû_†×ï¿2<{ừ₫tÍïÿ¾|₫Êp̣ôu†OŸ>1½6 ‡sŒ Qôßÿ–T†Å˜°£!®“ß½̀Ü¡—RÏÄĘÁ=ÿ*áàrÇ5C!ÜZûŸ‚ ơÏ@C₫!;++#3#Ă ‹¾ÿûÅÀøó';Ó!^.†W@±OŸ?Á  °ß@Ñöù;ƒ’ ƒ©¶4Xèr`à₫Gụ̀(úŸaáê= ×.]@Đ@„ÄñÏ^½ü tĐf`”|iƒ øúơ' ĐY_ưøö–â‚ï?>èă'v†?AÑ O80€Â‚™ hÀׯ ?½û ºà;Ă›7Ø̃ùæÿÑ3èû÷ï LŒL@₫W`@~ê‚Ä@Acá0éˆ?ĐÀPÿô¬d ƒ 4́ÏŸŸpbÀ§/¬^¾axÉÎ̀đSEœáׯO@ ŒZ`èÿFé¯_?€¨é?#Ă‡×¯₫}ÿÔÅÈ ̉ @Œ Ààç÷¸ÇÄø_ˆñ7778‘üưûl (IƒÔÀÎׯ©đ3Åú˜™”ˆ‘̉́ `°“̃2¨  IEND®B`‚Photon-0.4.6/images/filmholes-big-left.png0000644000175000017500000000131710331233474016546 0ustar lucluc‰PNG  IHDRîJ÷›ï pHYs  œtIMEÔ/'Ø©¦nIDATxÚí›1Â0EÈ × ¤¥áĐĐG‚’&Wà ¥ …†)= ¡é §‰†)[°,Û̀ÚöîÿUä…‰å̀̀›!³„@6¼üD¿ßă¸ÑhHÿúx‡Ë墻á¢(¸•ñx,.₫¹xóÜ.%„t»]v©Ùlˆ7©ŸGƯüE·ABH¦§ÓéÉ7E ‡ĂƠj¥5RÍf³:øF«ÏÁp~(Ÿ›ú9̀çs̃Ïz½»EQÇ|£„ÍfĂ.±i3ÏsEàøöâ¹%I¢bt¿ß¯×k~¿Z.—çóY%̃¸sđ-ÿR閯׫®Ưét*å&Z™Û¿±~ß+Ï ­z’Mw\jơo¢¹̣¹₫¾,Ưêßhé́…­|ö‹{ÔØ¿‰îe¦+£‹ 3Íz20u¾5ñÍ÷<éĂùVäI'ë#”Û­û÷]¾‰r”oaÏ@(/æ›Ùˆ°ÿûd “¾¯Ï,ד¾å3ưºÏsóơëÜ E¼©‚?ÇMư›èÅxÿæ(ßđ₫Ín}†÷oToưà[ÿ¾oàøAàø¾o¾oàø¾Ax^€à¹É!ó6ç+æ£\·çæKÜå•>zưÑ JŸ+–óM:ŒH Î#r³GÆæÅóuo>Í9úfÿ'₫ ưú7ôoèß |ßÀ7đ ̣C_y¼ •ªÀb"IEND®B`‚Photon-0.4.6/images/filmholes-big-right.png0000644000175000017500000000070510331233474016731 0ustar lucluc‰PNG  IHDRîJ÷›ï pHYs  œtIMEÔ×N3³dIDATxÚíÚ1ƒ@ÆñI: K[;Oà<­ s/0g°ó‚Ö^DAS¤ »&;³‰&Æÿ×…èSăüx¢dÍœ~üĂPJiYÖâÑÓ4Ơu¦i×u‰ăxÖHÓ4÷.¼œ¶mg½DQt=EJùû_!Äù¶®ïûwÁưÎÚQJ=.u^i=<[W«¿ú†aƯơ°ơúƯäyû (¥{ơ©ë—ºø†oø†oøö†ơ@è/¾á¾áû/¾áÛÁ|Óz_̣Bß₫¹Î₫́Ă×à¾ú‹òòoøÆ₫‹oôßđ ßđmƒđ}ÉwƠÅ7|Ă7|Ă7æ ú‹oø†oø†o„₫âÛóûß—́ª.¾á¡¿øÆüÆü†ó¿øFñí¨¾UUµ+ßú¾78:ÏsÍ₫º®kP×ó<¢Y–?oă$IbÛöâÑă8–eYŵ‰; ß—|W]|Ă7æ7æ7æ·÷ä¬}Ö³Đ|²•IEND®B`‚Photon-0.4.6/images/fullscreen.png0000644000175000017500000000120110372421354015230 0ustar lucluc‰PNG  IHDRóÿabKGDÿÿÿ ½§“ pHYs.#.#x¥?vtIMEÖ4̉n RIDAT8Ë¥“¿kZQÇ?÷Ùç— BŸV ’R4#%¥@ uè² Éh–@¡k¦„.ửYÇ Ù:H—!i‹¤b)…ĐØ¥oĐç¹D«Í̉3]ç{î=ßû9âí{SVªđơ—eZ Ç™$“bqmŸ©»!¦=æ̣ƒR©öạ̈Ó;¸ÜA×ö']î Åôạ̈„üP©‚H¼6%¹<Ÿ>¿ẮÔ1ÚY¤”_ „ÀíJ 9uæ×áy¥yYæđd³SGsềúVñêáa·ÛÅ«‡™ơ­¢9ùNĂ“=—e”V£Blc£EuX]7Å«‡¹ºº¢Ùlœ‰¡©Q¬®Ơa`´³Ä6vi5*(Ç™$mă)%¦Uiâó>äÁưÇ#bÓ*!¥¤mœsœI"ä_{ơ0ž´I¯ê'c+pz€i•øQ-¡ªêµ/r‚cƒ&ÄV°OèR˜V‰ê¯26›m¤Vá–°Oèµ.n+opóö^!§Ø—6ÑÔ(₫{,Ëo0g ¶ºîá̀¦U¢WH ñG¹©Q„ !ÆÜ₫Yû·ïÇ~G1$V<{ùAÓ;hN}(®ƠÏF@ºÉ‚Ù©đèÅîL{"<}̣êåßă(;jơ³q”=”¹$¶ăư°¼uôÏeZ̃:`a~Ävœ¹ˆÿ]ç?~é’xIEND®B`‚Photon-0.4.6/images/help.png0000644000175000017500000000203010331233473014015 0ustar lucluc‰PNG  IHDRóÿa pHYs  œgAMA±|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅFIDATxÚb```c`²LgàŒ˜ÆÀ Ë ^ ~3b·-`ˆÛµÁº´ŒGV ,Ç+ÅÀ ¡ËÀÀÁæÈ³TF¿^iOO©9?>Ù₫ÿ?üøÿÿ@œ´ÿÿưuÿÿ3Lâ˜uçXe„”ÚxÀÚˆ˜ô€6ÛÔ•6¼ûÿßÿÿÿ?üÇOÿÿ/1hHùÿ ́f Œ,`íur€ë́ïÿÿG̉đöå³ÿµ+Ï₫Ÿ}è÷,Ö°ëûÉÙ@C2₫êû €ÀÈjưÿŸ ¦Üúÿÿ×' ëÛ›ÿ!“ÿ·Xñă¿ñê?ÿ7Üư 6àêưÿ­€®™4Ä(n3H/@10(úÚU¾øÿæóÿÿ]Vñœ—ÿ=gßư¸áëÿ¸#ÿÿüÿÿâGˆ«_ºñßuñ¯ÿ¦ $o¹‚ €˜&·¥œÿÿ?zëÿÿ«€6Ù»₫ÿ;ôÿÚ‰ÿÿ¯|‚úéÏçÿÙËNüØ T·üÿ¦’«ÿ8ưˆ3mÇ"-ÿÿû5C;ÈƯưÿú±ÿÿßư‚j₫úîùêƒÿc|ùŸuôÿ? ¬•·ÿ3*Güyvkÿÿ÷^ùÿèzˆAÛÿÿßôªùÛÇÿéË₫O9óưŵÿÿ3Ñê³h@ñÅÿ ̣yÄÀäXZj Ôè¹ê WüøđÓ?@Ư?ÿo;uéÄ₫·ÿkî₫ÿ_ræÿÿT ÷́AÑ™¶û? »@S–¬²æ¢ÿÿƯ¬₫ÿ?r3Đ%@TüôÂîkÿ˶ßúŸu¨N9Àp‰Ùđÿ¿r×ûÿ >½ @d pDđ¤m»` ô†/Èk€ÄÇ_@C₫Îóÿ![¾ưÏ<đÿ"ĐpÇÙÿ3ưÏ i0¤ € ‰‘C\²íåg`8ø.ûÿßañÿÿAéè…½gnư÷\ül³ûœ¿ÿùj€)Ñ*û̀v€‚ >iNa3‘²³ÿÍ̃q’çü¿ÿƯú₫÷˜̣ú¿×‚ÿÿM§|ưÏYv ¨9÷-P‡L/@Aˆ ƒ¨8y§®)<ü_®ơéÅÎwÿe4_áÉÿ ₫@Î Tì € H ¨YỀds«Qœ Ẫ "j¹ ‚JÙ Œ́n`15Wˆzë™ÀL€¢§&đr#]:IEND®B`‚Photon-0.4.6/images/info.png0000644000175000017500000000203310331233473014023 0ustar lucluc‰PNG  IHDRóÿa pHYs  œgAMA±|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF‘IDATxÚb```a`2Idà èg`Đ c9g¾&†ĐU "Ö­f°̀+cà–RËñJ20Hè20pđƒ¹D̀ †q Œn­@ 'Oơi?yíûÿ?́ÀÿÿU§ÿÿÏ8øÿ¿₫ÊÿÿÅfüÿϽüĐBae6°v€"FÍpÓ̉̉¾—ÿÿưÔtâÿÿù×ÿCÀ¿Ïÿg>ÿï»hĐr !e×ÿ3° ˜10²€µI{º.üQ_uñÿǵÿÿ—₫÷ÿäƠ;ÿ—mÛÿß{̃íÿ‰ûÿÿÚôÿ¿á" !éûÿuư@`dµø÷Ï_@Í-WÿÿwjöÙöÿêáÿÿëö?ÿ_¾ûéÿÔ#₫çüÿ?dȺÿÿ¥' 1NØ ̉ @@Û]íªî₫ÿ?ëùÿÿ@É€Íÿÿ‡́ưÿñ=ˆ‹¾}zÿ?yÇ£ÿå@ĂK@†́₫ÿßiĐ€äí Wp“GG[ ĐÏÑÛ6¯ÿÿ?fĐ í ïÿđæù£ÿñ¯₫¯»óÿơ…ÿÿ‹€¾êỵ̈Ëÿ8Eüˆ3vÅ" ÿÿû ´= è̀Øcÿÿ/€đ̣Ù“ÿ‰[¯ÿozđÿưe`ØC9äÚ[ÿ• ˆ#|Î<»ÿÿ{­₫ÿ?tËÿÿÉ  x€fÀ}ˆå@¹ø5ÿÿËV]  @ LÖ™¥¦@'yă:U‰@ÿGÓÀü;P?ù¿áúÿúÛÿÿלº(¶ôÿÁœ½ÿ˜9<ˆGRYs.0ô½ ‹°ÿÿϽ ÄWÏŸ₫^să%ĐöJ ×̣€̣“̃ÿg̣ë";@#‚'qơ[ +|^ †GĐ%‹nB øơùơÿˆ%7ÿ—ÿÿ¿‘K₫₫—.?÷ŸAÂ`H/@A›¸dÓ£ÿÎ@WS›?0<:=zô×Áăÿ#æ<₫ŸÔ tºVóÿ V9`¶ñJ3‡ ™HÉéÿ– ѹb׌÷ÿ½§½Ú ›ưư¿B 0à¬sßu(Áô ù"ª àäi‘º^¸đÈ•ÖÇÿuz̃ư×êxö_ªǜ–€I@gΪá@Ö @̉,©f²¹×€(NNQoaơ\!ålFv7°˜;D= ÈơL`&@€t("BóïIEND®B`‚Photon-0.4.6/images/notavailable.png0000644000175000017500000000453010331233474015536 0ustar lucluc‰PNG  IHDR@đƒ XPLTEÿÿÿ̀̀̀333fff™™™pwMl IDATxÚíMc£8 †„{ ɦí=L;÷¤Ù½'M÷ÿÿ• ₫”l™˜º́Œt˜”ÁØ̣Ç‹ú§×ï*ˆgơ„·Ù³9qÿíGö¤Ÿ¼ư{«?N÷̣85T>W°Q¥ç,?úÓ®äÍÑ[­â¼¸̉̉?NWPn–û;€µ~°´$j÷Â}àfho/u¥~+ơMna!t€MRÜù€ưÁ?àN‘í`”ÿ́li:ªŸJw(]Lkđæ³û‘X¡‹²•äơ°Bå–₫©p€Aƒ•¤₫uNjê ©«0‚‚8|´n2XZ¢€æ·÷ñ€Oá́,º0†'× v –úf¼u\xóư«³m×=À•¾ j« Ê`¼?X|„ÀcơØ§æ® Q€^->vXŸ`ưèâuàZm]ÔqP]eqˆô¨Éîơ0àJ]s;е­*º %Ùª.ÙÆT]ĐâÀ\ö"_Dâ#^ÉúnZ_¹Ơ¼À¶ø—l×@Ù%..:‘泉 ´ß‹h&XƠ¸z­/é(ơ<¥¹€PMo•ÇZU¼ưPqB/yĐ~:¼AJñđ`¤g£d\ôY½ZŒÔô•½%½r ÷jơæyåêJ¤á’R<t*»ƠơƠåF¢”Ú©´’ ’ÏëXKâ÷™É¤xàçTv3ªAC@ ¦ë3«.O®@ø€ÀK+Qø^“R<t*»Ù³ ‘̃— U}û‹†5ƒ°±Ñ3â½p¯mC„å^c­ÿ̀ÑëIJÙ o΋¾ØùÂVƯ_¼y¯#Ø} VÓƠÍk]µ'̃v4ø/—A¬¦«}´wi—Ñ€Ø+Zïơ‰̣„ZŒƠtYKö ¸-ˆ½¢µøœ8±®ôtÜdă€k·u°]Œl@'Ă–¤̣n‹‹¦¥ˆ:¯hKHñ$ j‹eÉ‚"'VÓx½3ơû¬^6¨ÁÇÛb_§sÔ·9â…Ơtü*ư´ca{ѽJ§ ä~Á€®#Ü„pª b¯\P]Û"ö¨møơZ«Ÿ«í³c¯óxK²w€—°GMHñ@0&±2L†n!x§7¶u—yơ{È÷ñ˜„âc€HW]Đ– 5=[ó*êw2Pc/¹GùŒêB)íÿaÅI æÓ£0rk;e®D %ùvÀË̉KL|5ø –xÂEzó'å)L#²‰̣bL¢ü,Ç̉’™-’s@CÊhj¢üô€­§+¯D0%Q~r@4ùj„s0)Q~rÀN_ç̃Œ-LJ”Ÿ ̣ë_ÙËt~@,BåA´ƒ€T¢üfî8ˆe¼e*ú€I‰̣s6™Bóà ˜”(?5 –’GR‰̣ó·Åwå—H$ÊÏ_lj̣ó×âq€™Ÿ(? 8S8Œ¶$ÁnUJ”Ÿ¼Ă´Åx7«Î§&ÊO˜û½™ÈØ*5Q~rÀ ?¦&ÊO?&ñ{Ô‘ñ}j¢ü ƒ&L‚GEƯ •(?ï¨XRΨó©‰̣llllllllllll„ ksù̉ĂŸh¹đ·\j-đ¹“BJÜ Á·ÿWTTü×ÔÔD1 €P :ưWFFÆÿ¦¦¦ÿ`/¨¨¨€\p3++ë}}ưmmmÅ99¹_EEEÿ»»»ÿ‚ PWWg••½ ̣VGGÇ---Å sUVV₫Ÿ4iÜEEÅ›ƠƠƠÿ'L˜€á€B1@IIéșӧO‡{fH|êÔ©. €₫ưƠØØøæ̀™ @ uÁ7›››Á£@X ˜1c(ºÀ bPVV¾ Ÿ6mÚ`˜ @(₫ª««û?ỵdPtÁ]2$ UUUÅ _•——ÿïëëûÔ„ Àíííư///b@aÄBAAÁÿÖÖVB°@?3Ù7AÑ P2GÖ@èüÎÎÎ₫_[[û˜*Áhhh€ ¸¯©©ù/..b@¡Găÿ̀̀LpRđfĐàÇ @âbbb(Àœv˜ß zÍÊÊzdH|/Pü0,̃ś}d=|CI‡à|ÇIEND®B`‚Photon-0.4.6/images/player_play.png0000644000175000017500000000074610331233473015422 0ustar lucluc‰PNG  IHDRóÿa pHYs  œgAMA±|ûQ“ cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF\IDATxÚbøÿÿ?2‚,Y=@1`1Dl'Ö€Â0€í?Ơí$Æ€Â0€““ó¯’’̉¨K®2 €0 `ff₫åâậÉï@,ˆË€Â0€‘‘ñ—‡‡Çÿäääÿ 0C₫±*6[ ‚]P^^₫?##ă¿®®.̀vA7 €°àêêú¿¤¤äUUƠÿüüüÿvvvÿYYYa†#« œ€4ÖƠƠư_½zơÿM›6ưOLLüÏËË R0 Y=@a5ÀÙÙùnnîÿE‹ưß½{÷ÿ¢¢¢ÿ||| ÉBt/Vlmmÿwtt€m&0`ÃØ €pzaæ̀™ÿcbb`₫6Ä̀?ÿ}}}Aœo@,/!†ÿutt@“”[Jùù±™ €°y¡›”́ `Ư¢Áʾ¥¦IEND®B`‚Photon-0.4.6/templates/0000755000175000017500000000000010713565311013120 5ustar luclucPhoton-0.4.6/templates/photonv1/0000755000175000017500000000000010713565311014676 5ustar luclucPhoton-0.4.6/templates/photonv1/common_footer.html0000644000175000017500000000562110372423710020433 0ustar lucluc
## Left img
#if ($previous_image) <-Prev
$previous_image.filename
#else #end
## Toolbar and Sizelist fullscreen #if ($exif) info #end help start slideshow
#if ($movie_filename) ## MOVIE mode Download the movie:
$movie_filename
Others preview:
#foreach ($res in $movie_sizelist) $res.frames_per_width x $res.frames_per_height
#end #else ## IMAGE mode #if ($img_is_raw) Download the raw file:
$img_raw_filename
#end Others sizes:
#if ($img_sizelist) #foreach ($res in $img_sizelist) $res.size
#end #else None #end #end
## Right img
#if ($next_image) Next ->
$next_image.filename
#else #end
Photon-0.4.6/templates/photonv1/image.html0000644000175000017500000000735110423337247016657 0ustar lucluc#* This file is a template for Photon for the imageXXXX.html pages. *# Photon: $title #if ($awstats_script_url) #end #if ($has_exif) #end #if ($javascript) #end #if ($previous_image) #end #if ($next_image) #end
#if ($comments) #end
$img_img_alt
$comments
#parse ("common_footer.html")
Generated by Photon $version
#if ($has_exif) #end #if ($javascript) #end #if ($next_image) #end Photon-0.4.6/templates/photonv1/index.html0000644000175000017500000000672710340265426016707 0ustar lucluc#* This file is a template for Photon for the index%d.html pages. *# Photon: $title #if ($previous_link) #end #if ($next_link) #end #if ($awstats_script_url) #end
#foreach($row in $row_dirs) #foreach($case in $row) #if ($case.type == 'normal') #elseif ($case.type == 'ext') #elseif ($case.type == 'blank') #end #end #end #foreach($row in $row_images) #foreach($case in $row) #if ($case.type == 'movie') #elseif ($case.type == 'img') #elseif ($case.type == 'blank') #end #end #end
$navbar
$case.name
$case.thumb_name
$case.name
$case.name

$case.name
$case.name

$case.name
#if ($page_index == 0)Start | Prev $items_per_page | #elseif ($page_index == 1) Start | Prev $items_per_page | #else Start | Prev $items_per_page | #end #if ($page_index<$max_pages) Next $items_per_page | End#else Next $items_per_page | End#end #if ($total_images == 0)   #else Images $images_processed to $max_images_displayed of $total_images #end
Generated by Photon $version
Photon-0.4.6/templates/photonv1/movie.html0000644000175000017500000000634010412515333016701 0ustar lucluc#* This file is a template for Photon for the imageXXXX.html pages. *# Photon: $title #if ($awstats_script_url) #end #if ($exif) #end #if ($javascript) #end #if ($previous_image) #end #if ($next_image) #end
#if ($comments) #end
$navbar
#foreach($frame_row in $movie_frames) #foreach($frame in $frame_row) #end #end
$frame.caption
$comments
#parse ("common_footer.html")
Generated by Photon $version
#if ($javascript) #end #if ($next_image) #end Photon-0.4.6/.hgtags0000644000175000017500000000024210713565270012402 0ustar lucluc3281a7d0c70d5dd4080ee21e84bdb81c9ddf28dd photon-0.4.4 4b0e06933221aca84eb90dab606c8718181c7392 photon-0.4.5 b7ff4af8c4dae1b7c297dfca59e4abacee3c5bc0 photon-0.4.6 Photon-0.4.6/BUGS0000644000175000017500000000046410371655732011620 0ustar lucluc Need python-dev to import distutils.sysconfig ==> need to fix them > Processing image /cdrom/04Fichiers HAUTE-Def/008.jpg JPEG 1920x2560 RGB > /home/dwitch/Photon-0.3.1/Photon/EXIF.py:640: FutureWarning: x< x=(x << 8) | ord(c) Photon-0.4.6/ChangeLog0000644000175000017500000000155310713563571012706 0ustar lucluc2007-11-05 Luc Saillard * Fix bugs * Add compat for gimp-2.4 * Release 0.4.5 2006-02-06 Luc Saillard * Template support * Mjpeg AVI * Release 0.4.4 2005-06-29 Luc Saillard * Better support of Quicktime file * Fix somes very old bugs * Release 0.4.2 2005-06-06 Luc Saillard * Raw support (using dcraw) * Fix HTML (to be w3c compliant) * Small changes for Exif (Focal Lenght) * Release 0.4.1 2005-06-01 Luc Saillard * Movie mode (some movie can now be displayed) * Can add awstats Javascript code * Better Exif Output * Support for PNG and QuickTime (only jpeg) * You can recompress a original image * Directory can now have an image * Release 0.4.0 2004-06-08 Luc Saillard * Fix missing image in web site * Release 0.3.1 Photon-0.4.6/LICENSE0000644000175000017500000000434110331233473012126 0ustar luclucPhoton is under GNU GPL Licence ----------------------------------------------------- The Noia icons for KDE. Made by Carlitus (Carles Carbonell Bernado) mail@carlitus.net http://www.carlitus.net LICENSE The Noia icons for KDE are under GNU LGPL license. Files under this license: images/help.png images/info.png images/player_pause.png images/player_play.png ----------------------------------------------------- # Library to extract EXIF information in digital camera image files # # Contains code from "exifdump.py" originally written by Thierry Bousch # and released into the public domain. # # Updated and turned into general-purpose library by Gene Cash # # # This copyright license is intended to be similar to the FreeBSD license. # # Copyright 2002 Gene Cash All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the # distribution. # # THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # This means you may do anything you want with this code, except claim you # wrote it. Also, if it breaks you get to keep both pieces. # Photon-0.4.6/Makefile0000644000175000017500000000137710331233474012570 0ustar luclucall: setup.py python setup.py build clean: setup.py python setup.py clean rm -rf build dist rm -f Photon/*.pyc rm -rf debian/python2.3-photon* rm -rf debian/python2.4-photon* rm -rf debian/photon debian/files install: setup.py python setup.py install --home=~ dist: setup.py python setup.py sdist --formats=gztar,bztar,zip rpm: setup.py python setup.py bdist_rpm deb: setup.py version=`./photon --version | cut -f2 -d " "` ; \ echo "photon ($$version-1) unstable; urgency=low" > debian/changelog echo "" >> debian/changelog echo " * New upstream release" >> debian/changelog echo "" >> debian/changelog echo " -- Luc Saillard `date -R`" >> debian/changelog dpkg-buildpackage -rfakeroot .PHONY: all clean install dist rpm Photon-0.4.6/README0000644000175000017500000000334410371656170012012 0ustar luclucPhoton ------ http://saillard.org/photon Photon is a static HTML gallery generator. Features: - static HTML pages (you can put all pages and images on a CD-ROM) - slideshow (use javascript optional) - can use gimp to resize picture - navigation between the image can use the keyboard (use javascript optional) - works in any browser (Mozilla, Netscape Navigator 4.x, Konqueror, Opera) - Each image can have a comment (with HTML tags) - Information about the image (if taken from a digital picture) can be draw - thumbnail image size can be choosen by the user - output images can be scalled down - control the number of thumbnail in a page - movie support, to show a preview - HTML template Requirement: To make the gallery - python >=2.1 (see http://www.python.org) - PIL (see http://www.pythonware.com/products/pil/) It's the library use to load image, resize. Debian users: apt-get install python2.3-imaging (if you have a debian unstable) apt-get install python2.2-imaging apt-get install python2.1-imaging - EXIF library (included in this package but optional) - Gimp (optional) - dcraw (optional) - Some photos and free space disk To show the gallery - a web browser - a web browser with javascript enabled (optional) Licence: Photon is released under the GNU General Public License v2 and can be used free of charge. How to build a gallery: The quick start: # photon -o myoutputdir /photos Use the gimp plugin # photon --resize-plugin=gimp -v -o myoutputdir /photos Change the size for all images and the thumbnail keeping the original photo (0) # photon --sizelist=0,1600x1200,800x600 --thumbsize=320x240 -o myoutputdir /photos See photon --help for a list of switches Photon-0.4.6/README.templates0000644000175000017500000000341310371656057014010 0ustar lucluc Engine: airspeed Create a new template for photon -------------------------------- o create a new folder in the template directory (~/.photon/templates or /usr/share/photon/templates). The folder need to have an unique name, else photon will use the version in your own HOME directory. EX: mkdir -p ${HOME}/.photon/templates/my_sexy_theme/ o Photon needs only three files to build pages. index.html => used to display the content on a folder image.html => used to display the page for an image movie.html => used to display the page for a movie If a file is missing, photon produce an error, and will not run. You can create your own file from scratch, but I think it's easier to copy a default template, and modify them. EX: vi ${HOME}/.photon/templates/my_sexy_theme/index.html vi ${HOME}/.photon/templates/my_sexy_theme/image.html vi ${HOME}/.photon/templates/my_sexy_theme/movie.html o Each file can include others files to reused some common part between each page, but all files need to be in the current theme folder. The current theme "photonv1" used a common part between image.html and movie.html. Look at it, to see how to used it. o Tell photon to use the template EX: photon -k my_sexy_theme ... or photon --skin my_sexy_theme ... Variables available for index.html: ----------------------------------- All options from the configuration file. exif_bordercolor exif_bgcolor exif_fgcolor body_bgcolor img_bgcolor awstats_script_url theme thumbsize_width thumbsize_height Global variables: version: the current photon's version title: Albums (for the root folder) or the current folder name Special variables: navbar: List variables: row_images row_dirs Photon-0.4.6/photon0000755000175000017500000023564710713563671012407 0ustar lucluc#!/usr/bin/python # -*- coding: iso-8859-1 -*- # vim:set ai et sts=2 sw=2: # vim600: set fdm=marker cms=#%s : # # Photon is a static HTML gallery generator. # * directory structure is kept intact # * scaled images are put in separate directories (800x600, ...) # * etc... # # Created: Luc Saillard Wed, 13 Feb 2002 20:51:12 +0100 # Last modified: Luc Saillard Tue, 15 Nov 2005 08:42:18 +0100 # Changelog: # v0.1: first release in Python. # v0.2.3: ??? # v0.2.4: speed up improvement # v0.2.5: New that indicates previous/next page # Key Shorcuts can now be use # Slideshow functions # v0.2.6: Javascript fix # Cmdline option fix # New exclude option # v0.2.7: Gimp plugin (very slow for the moment) # Bug fixes # New options: --resize-plugin # v0.2.8: Anchor is made for each row in the galery view # v0.2.9: Compat with gimp-2.0, python2.1, python2.2 # Fix a bug when thumbsize > image # Fix a bug when gimp cannot recognize the image # v0.2.10: # v0.2.11: # v0.2.12: Create html only if they are different # Fix a bug in resizing landscape images # Activate preloading the next image # v0.3.0: PIL is now optional but a few format is supported # By default, all image is now accepted # When an image is in 8bits mode (gif), convert it to 24bits # v0.3.2: Movie mode # Awstats integration # Quick save image feature # v0.4.x: template engine, accesskey # # # Copyright (C) 2002-2007 Luc Saillard # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Includes modules import getopt, os, sys, shutil, urllib, re, locale, codecs, errno, math from random import randrange, sample from fnmatch import * from stat import * from tempfile import gettempdir from commands import getstatusoutput from Photon import EXIF, Video, RAW, airspeed import md5 import Photon # ------------------------------------------------------------------------ """ Global options """ # options = {} options['verbose_level'] = 0 options['output_directory'] = './photos' options['comment_filename'] = '.comments' options['thumbsize'] = (160,120) options['sizelist'] = [(0,0), (1024,768), (800,600), (640,480)] options['forcecreate'] = 0 options['forcecompress'] = 0 options['display_columns'] = 4 options['display_lines'] = 5 options['generate_root_index_html'] = 1 options['img_bgcolor'] = "#fffaf5" options['body_bgcolor'] = "#ccccff" options['exif'] = 1 options['exif_bordercolor'] = "#008000" options['exif_bgcolor'] = "#f0fff0" options['exif_fgcolor'] = "black" options['javascript'] = 1 options['exclude'] = ["*.mov", "*.avi", "*.mpg", "*.comments", "Makefile" ] options['rawpattern'] = ["*.bay", "*.bmq", "*.cr2", "*.crw", "*.cs1", "*.dc2", "*.dcr", "*.fff", "*.k25", "*.kdc", "*.mos", "*.mrw", "*.nef", "*.orf", "*.pef", "*.raf", "*.rdc", "*.srf", "*.x3f"] options['resize_plugin'] = "gimp" options['resize_quality_low'] = 0.5 options['resize_quality_high'] = 0.85 options['gimp_program'] = "gimp" options['dcraw_program'] = "dcraw" options['rawmode_supported'] = -1 options['charset'] = 'iso-8859-15' options['fileformat_plugin'] = 'pil' options['data_path'] = [ "." ] options['movie'] = 0 options['moviepattern'] = ["*.mov", "*.avi"] options['movie_replace_mov_by_mpg'] = 1 options['awstats'] = 0 options['awstats_script_url'] = "/js/awstats_misc_tracker.js" options['theme'] = "photonv1" gimp_list = [] # This is a global list that contains jobs to be run by Gimp tempfile_list = [] # Global list to use to remove some files after processing failed_templates_loaded = [] # Global list to suppress duplicated warning message exif_summary = ( # Text String , EXIF key ('Manufacturer', 'Image Make', ), ('Model', 'Image Model',), ('Aperture Value', 'EXIF MaxApertureValue', ), ('Color Space', 'EXIF ColorSpace', ), ('Exposure Time', 'EXIF ExposureTime', ), ('Exposure Program', 'EXIF ExposureProgram', ), ('Shutter Speed', 'EXIF ShutterSpeedValue', ), ('Flash', 'EXIF Flash', ), ('Focal Length', 'EXIF FocalLength', 'mm'), ('ISO', 'EXIF ISOSpeedRatings', ), ('Metering Mode', 'EXIF MeteringMode', ), # ('Light Source', 'EXIF LightSource', ), ('Date/Time', 'Image DateTime', ), ) # ------------------------------------------------------------------------ # # Main functions # def main():# """ main procedure """ short_opts="ac:d:EfhIJk:l:mo:s:t:Vvz" long_opts=[ "comment=", "display-columns=", "no-exif", "force", "help", "no-index", "display-lines=", "output-directory=", "sizelist=", "thumbsize=", "version", "verbose", "javascript", "compress" "exif-bordercolor=", "exif-bgcolor=", "exif-fgcolor=", "body-bgcolor=", "img-bgcolor=", "exclude=", "resize-plugin=", "resize-quality-low=", "resize-quality-high=", "gimp-program=", "movie", "awstats", "awstats-url", "skin", "print-skins" ] try: opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) except getopt.GetoptError, msg: # print help information and exit: if msg: print msg usage() sys.exit(2) for o, a in opts: if o in ("-c", "--comment"): options['comment_filename'] = a elif o in ("-d", "--display-columns"): if a.isdigit(): options['display_columns'] = int(a) else: print "Bad argument:",a,"must be a number" elif o in ("-E", "--no-exif"): options['exif'] = 0 elif o in ("-ff", ): options['forcecreate'] = 2 elif o in ("-f", "--force"): options['forcecreate'] += 1 elif o in ("-h", "--help"): usage() sys.exit() elif o in ("-I", "--no-index"): options['generate_root_index_html'] = 0 elif o in ("-J", "--no-javascript"): options['javascript'] = 0 elif o in ("-l", "--display-lines"): if a.isdigit(): options['display_lines'] = int(a) else: print "Bad argument:", a, "must be a number" elif o in ("-o", "--output-directory"): options['output_directory'] = a elif o in ("-s", "--sizelist"): l=[] for s in a.split(','): if s == '0': l.append((0,0)) else: m = re.search("^(\d+)x(\d+)$", s) if m: l.append((int(m.group(1)), int(m.group(2)))) else: print "Bad size:", s, "must in the form 320x240" # End: for s in a.split(','): options['sizelist'] = l elif o in ("-t", "--thumbsize"): m = re.search("^(\d+)x(\d+)$", a) if m: options['thumbsize'] = ((int(m.group(1)),int(m.group(2)))) else: print "Bad size:", a, "must in the form 320x240" elif o in ("-V", "--version"): print "Photon", Photon.version sys.exit() elif o in ("-v", "--verbose"): options['verbose_level'] += 1 elif o in ("-z", "--compress"): options['forcecompress'] = 1 elif o == "--exif-bordercolor": if match_color_type(a): options['exif_bordercolor'] = a else: print "Bad color:", a, "must in the form #0055ff or white" elif o == "--exif-bgcolor": if match_color_type(a): options['exif_bgcolor'] = a else: print "Bad color:", a, "must in the form #0055ff or white" elif o == "--exif-fgcolor": if match_color_type(a): options['exif_fgcolor'] = a else: print "Bad color:", a, "must in the form #0055ff or white" elif o == "--body-bgcolor": if match_color_type(a): options['body_bgcolor'] = a else: print "Bad color:", a, "must in the form #0055ff or white" elif o == "--img-bgcolor": if match_color_type(a): options['img_bgcolor'] = a else: print "Bad color:", a, "must in the form #0055ff or white" elif o == "--exclude": options['exclude'].append(a) elif o == "--resize-plugin": if a in ("gimp","pil"): options['resize_plugin'] = a elif a in ("magick"): print "This method plugin ",a," is not available" else: print "This method plugin ",a," is unknow. Using default plugin: ",options['resize_plugin'] elif o == "--resize-quality-low": if a.isdigit(): options['resize_quality_low'] = int(a)/100.0 else: print "Bad argument:", a, "must be a number" elif o == "--resize-quality-high": if a.isdigit(): options['resize_quality_high'] = int(a)/100.0 else: print "Bad argument:", a, "must be a number" elif o == "--gimp-program": options['gimp_program'] = a elif o in ("-m", "--movie"): options['movie'] = 1 for pat in options['moviepattern']: try: options['exclude'].remove(pat) except ValueError: pass elif o in ("-a", "--awstats"): options['awstats'] = 1 elif o == "--awstats-url": options['awstats_script_url'] = a elif o in ("-k", "--skin"): options['theme'] = a elif o == "--print-skins": print_skins() sys.exit(0) if len(args)==0: usage() sys.exit() if options['thumbsize'] not in options['sizelist']: options['sizelist'].append(options['thumbsize']) # If PIL could not be load, check if gimp is available # and if gimp is not available, raise an error if options['fileformat_plugin'] == 'internal': options['gimp_program'] = find_gimp_program() if options['gimp_program'] is None: print 'Could not load PIL neither gimp.' print 'In order to use this script, you must have PIL installed.' print 'Get from http://www.pythonware.com/products/pil/' sys.exit(3) if options['verbose_level'] >= 1: print "Could not load PIL, switching to gimp" options['resize_plugin'] = 'gimp' # Create HTML files for path in args: process_directory(path,"") # If we are using Gimp, process the file list if options['resize_plugin'] == "gimp" and len(gimp_list)>0: options['gimp_program'] = find_gimp_program() if options['gimp_program'] is None: print "Gimp not found\nAborted\n" else: options['tempdir'] = mkdtemp("","photon") gimp_resize_files(gimp_list) os.rmdir(options['tempdir']) # Cleanup (delete all temp files) for file in tempfile_list: try: os.unlink(file) except: pass # ------------------------------------------------------------------------ def usage(): # """ Print information to use this program """ print """ Usage: photon [OPTION]... [PATH]... Options: -a --awstats Insert Javascript for Awstats --awstats-url=URL Change default value for Awstats script url -c NAME --comment Name of the comment file (default .comments) -d NUM --display-columns=NUM Number of columns in index (default 3) -E --no-exif Don't include EXIF information in HTML file -f --force Overwrite non-image generated files (html, javascript, etc) (default no) -ff --force --force Overwrite image files (default no) -h --help Print this help -I --no-index Do not generate the high level index.html -J --no-javascript Do not use javascript (no shortcuts, ... ) -k --skin=THEME Skin selector (default photonv1) --print-skins Print the list of available skins (not implemented) -l NUM --display-lines=NUM Number of lines in index (default 5) -m --movie Movie mode, include link to movie (*.mov) -o NAME --output-directory=NAME Name of the output directory -s LIST --sizelist=LIST Image sizes (default 0,1024x768,800x600,640x480) 0 is special case to specified the original resolution wxh specify the width and the height in pixel of the image -t SIZE --thumbsize=SIZE Size of thumbnails (default 160x120) -V --version Print Photon version -v --verbose Be verbose -z --compress Compress the original image if selected (default copy the image) --exif-bordercolor=COLOR Exif window border color (default #008000) --exif-bgcolor=COLOR Exif window background color (default #f0fff0) --exif-fgcolor=COLOR Exif window text color (default 'black') --body-bgcolor=COLOR Body background color (default #ccccff) --img-bgcolor=COLOR Image background color (default 'white') --exclude=PATTERN Exclude files matching PATTERN --resize-plugin=PLUG Program use to create thumbnails internal: fastest method (default) gimp: use Gimp>1.x (better quality) magick: use ImageMagick (not implemented) --resize-quality-low=Q quality for small image. 0 (bad) and 100 (good) --resize-quality-high=Q quality for big image 0 (bad) and 100 (good) --gimp-program=PROG use PROG for gimp Shortcuts while viewing an image: n/SPACE Go to the next image (with the same resolution) p/BACKSPACE Go to the previous image (with the same resolution) s Start/Stop the slideshow +/- Increase/Decrease by one second the slideshow's period h Show shortcut and help i Show Exif information z Change to the higher resolution [NOT YET IMPLEMENTED] """ # ------------------------------------------------------------------------ # # Functions that work on a directory or on a file # def process_directory(realpath, relativepath): # """ Generate thumbnails, html pages for this directory """ directories_list = [] images_list = [] destdir = os.path.join(options['output_directory'], relativepath) safe_mkdir(destdir) sourcedir = os.path.join(realpath, relativepath) # For each images build all sub-image, # for each directory, recurse into them for entry in os.listdir(sourcedir): # Don't accept directory or file beginning with a dot if entry[0] == '.': continue pathname = os.path.join(sourcedir, entry) for pattern in options['exclude']: if pattern == entry: if options['verbose_level'] >= 1: print 'Excluding %s' % pathname break if fnmatch(pathname,pattern): if options['verbose_level'] >= 1: print 'Excluding %s' % pathname break else: mode = os.stat(pathname)[ST_MODE] if S_ISDIR(mode): # Skip this directory, if we are inside of the output directory if os.path.abspath(destdir) == os.path.abspath(os.path.join(realpath,relativepath,entry)): print 'Excluding directory %s' % destdir continue process_directory(realpath, os.path.join(relativepath, entry)) directories_list.append(entry) elif S_ISREG(mode): picinfo = None if options['movie']: for pattern in options['moviepattern']: if fnmatch(pathname,pattern): picinfo = process_movie(realpath, relativepath, entry) break if not picinfo: for pattern in options['rawpattern']: if fnmatch(pathname, pattern): picinfo = process_raw_file(realpath, relativepath, entry) break else: picinfo = process_file(realpath, relativepath, entry) if picinfo: images_list.append(picinfo) else: if options['verbose_level'] >= 1: print 'Skipping %s' % pathname process_comment_file(realpath, relativepath, images_list) # Now, we have the complete list of directory, and files ... sort them images_list.sort(lambda x, y: cmp(x['filename'], y['filename'])) directories_list.sort() # ... then generate html pages make_directory_html(relativepath,directories_list, images_list) if len(images_list) > 0: make_image_html(relativepath,directories_list, images_list) # ------------------------------------------------------------------------ def process_file(sourcedir, relativepath, filename): # """ Create for this file all thumbnails and return the size of the image """ srcfile = os.path.join(sourcedir, relativepath, filename) # We need to detect, if a image can be draw at the screen. # If original_size is always smaller than any sizelist (without thumbnail) # then image is not draw to the screen. So keep a flag that can draw this # image at the original_size npicsconverted = 0 pic = {} try: im = Image.open(srcfile) if options['verbose_level'] >= 1: print 'Processing image', srcfile, im.format, "%dx%d" % im.size, im.mode except IOError, err: print "cannot create thumbnail for", srcfile ,"(", err.strerror, ")" else: # Foreach size of the image, resize them only when it's different from Original pic['filename'] = filename pic['original_size'] = im.size pic['extra_resolution'] = [] if options['exif']: file = open(srcfile, 'rb') pic['exif'] = EXIF.process_file(file) if im.size[1] > im.size[0]: # Keep aspect ratio pic['aspect'] = 34 pic['ratio'] = float(im.size[1])/im.size[0] else: pic['aspect'] = 43 pic['ratio'] = float(im.size[0])/im.size[1] for (w,h) in options['sizelist'] + [(-1,-1)]: if w == -1 and h == -1: # Small hack to process code at the end of the loop # If no image is converted (without counting thumbnail), then copy the original # original size can not be in the 'sizelist' if npicsconverted<2 and (0,0) not in options['sizelist']: (w,h) = (0,0) pic['extra_resolution'].append((0,0)) else: break if w == 0 and h == 0: # Special case when it is a original file if options['forcecompress']: (w,h) = im.size else: subdir = 'original' destdir = os.path.join(options['output_directory'], relativepath, subdir) destfile = os.path.join(destdir, filename) if file_is_newer(destfile, srcfile): safe_mkdir(destdir) shutil.copyfile(srcfile, destfile) npicsconverted+=1 if w != 0 and h != 0: # Resize the image with keeping the ratio if pic['aspect']==34: newsize = (int(w/pic['ratio']),w) is_original_smaller = h > im.size[1] else: newsize = (w,int(w/pic['ratio'])) is_original_smaller = w > im.size[0] pic['filename'] = re.sub('^(.*)\.[^.]+$',"\\1.jpg",filename) subdir='%dx%d' % (w,h) destdir = os.path.join(options['output_directory'], relativepath, subdir) destfile = os.path.join(destdir, pic['filename']) if is_original_smaller and w != options['thumbsize'][0]: # Don't generate thumbnail when original file is smaller if options['verbose_level'] >= 1: print "Skipping", srcfile, "for resolution %dx%d" % newsize continue npicsconverted+=1 if file_is_newer(destfile, srcfile): safe_mkdir(destdir) # We have 2 choices use Gimp,use Python Library, or our internal parser if options['resize_plugin'] == "gimp": if w*h<64000: gimp_file = (srcfile,destfile,newsize,options['resize_quality_low']) else: gimp_file = (srcfile,destfile,newsize,options['resize_quality_high']) gimp_list.append(gimp_file) elif options['resize_plugin'] == 'pil': try: if im.mode == 'P': # Image is in palette mode, convert it to 24bits im = im.convert('RGB') im.resize(newsize, Image.BICUBIC).save(destfile, 'JPEG', optimize=1, progressive=1) except IOError, err: print "Error while writing thumbnail, will try without optimization..." print "Perhaps you can try to increase ImageFile.MAXBLOCK in the source file" print err try: # Try to save the Jpeg file without progessive option im.resize(newsize,Image.BICUBIC).save(destfile, 'JPEG') except IOError, err: print "cannot create ", destfile, "(", err, ")" else: # Use the python internal module resize method raise("Error: resize plugin not know") # Process comment for this file only commentfile = srcfile + '.comments' try: f = open(commentfile, 'r') pic['comments'] = "".join(f.readlines()) f.close() except IOError: pass return pic # ------------------------------------------------------------------------ def process_movie(sourcedir, relativepath, filename): # """ Create for this file all thumbnails and return the size of the image """ srcfile = os.path.join(sourcedir, relativepath, filename) try: video = Video.identify(srcfile) if options['verbose_level'] >= 1: print 'Processing video', srcfile, video.format, "%dx%d" % video.size except IOError, err: print "cannot recognize movie file: ", srcfile ,"(", err.strerror, ")" return None else: movie = {} movie_to_copy = get_new_movie_filename_after_hack(srcfile) movie['realfilename'] = filename movie['filename'] = re.sub('^(.*)\.[^.]+$',"\\1.jpg", filename) movie['original_size'] = video.size movie['movie'] = 1 movie['extra_resolution'] = [] movie['exif'] = {} if video.size[1] > video.size[0]: # Keep aspect ratio movie['aspect'] = 34 movie['ratio'] = float(video.size[1])/video.size[0] else: movie['aspect'] = 43 movie['ratio'] = float(video.size[0])/video.size[1] # Copy the movie in the original directory subdir = 'original' destdir = os.path.join(options['output_directory'], relativepath, subdir) destfile = os.path.join(destdir, filename) if file_is_newer(destfile, srcfile): safe_mkdir(destdir) if movie_to_copy != srcfile: movie['movie_hack_enable'] = 1 movie['realfilename'] = re.sub('^(.*)\.[^.]+$',"\\1.mpg", filename) destfile = re.sub('^(.*)\.[^.]+$',"\\1.mpg", destfile) shutil.copyfile(movie_to_copy, destfile) # Now the big part, we will take some frames from the movie to create a # mosaic of images. The number of frames depends of the resolution of images. # But each (small) image will be the size of the preview. So preview need to # be a multiple of big image. # How image will be named ? the same of the movie, but with the frame # number in it. Example dscn00234-3000.jpg # First calculate the size of the small image, keeping the ratio of the # original size of the movie if movie['aspect'] == 34: thumbsize = (int(options['thumbsize'][0]/movie['ratio']),options['thumbsize'][0]) else: thumbsize = (options['thumbsize'][0],int(options['thumbsize'][0]/movie['ratio'])) movie['thumbsize'] = thumbsize subdir ='%dx%d' % thumbsize destdir = os.path.join(options['output_directory'], relativepath, subdir) safe_mkdir(destdir) # Get a default image to display the same in all resolutions destfile = os.path.join(destdir, movie['filename']) tempframepath = os.path.join(destdir, "..", movie['filename']) video.get_frame(tempframepath, 0) resize_image(tempframepath, destfile, thumbsize) tempfile_list.append(tempframepath) for (w,h) in options['sizelist']: if w == 0 and h == 0: continue # Find how many images we can put in this size (nimgw, restw) = divmod(w,thumbsize[0]) (nimgh, resth) = divmod(h,thumbsize[1]) if restw>0 or resth>0: if options['verbose_level'] >= 1: print "We cannot create an perfect image map for your video ", srcfile print "Size of the image: %dx%d" % (w,h) print "Size of the video: %dx%d" % video.size print "Size of the thumbnail: %dx%d" % thumbsize print "remain %d pixels in width and remain %d pixels in height" % (restw, resth) # Number of images we need to extract totalimages = nimgw * nimgh listoframes = generate_list_of_frames(video.frames, totalimages) movie['frames_%dx%d' % (w,h)] = listoframes for frame in listoframes: if options['verbose_level'] >= 2: print "Get frame N°%d from " % (frame, filename) framefilename = re.sub('^(.*)\.[^.]+$',"\\1-%6.6d.jpg"%frame,filename) destfile = os.path.join(destdir, framefilename) tempframepath = os.path.join(destdir, "..", framefilename) if file_is_newer(destfile, srcfile): video.get_frame(tempframepath, frame) # Now resize the image tempfile_list.append(tempframepath) resize_image(tempframepath, destfile, thumbsize) # end of if file_is_newer(destfile, srcfile): # end of for frame in listoframes: # for (w,h) in options['sizelist']: return movie # ------------------------------------------------------------------------ def process_raw_file(sourcedir, relativepath, filename): # """ Process a raw image produce by a digital camera. This is dcraw program, to convert image, and exiftools exif metadata into the result image. Sucess returns the size of the image """ if options['rawmode_supported'] == -1: options['rawmode_supported'] = is_raw_supported() if options['rawmode_supported'] == False: return srcfile = os.path.join(sourcedir, relativepath, filename) try: rawimg = RAW.identify(srcfile) if options['verbose_level'] >= 1: print 'Processing rawfile', srcfile, rawimg.format except IOError, err: print "This is not a raw file: ", srcfile ,"(", err.strerror, ")" return None ppmfilename = re.sub('^(.*)\.[^.]+$',"\\1.pnm",filename) # Try to convert the image, using dcraw in pnm (native format) destdir = os.path.join(options['output_directory'], relativepath) ppmpath = os.path.join(destdir, ppmfilename) tempfile_list.append(ppmpath) if rawimg.convert(ppmpath) is None: print "Error while processing %s " % filename return None # ... then process this image with process_file() picinfo = process_file(options['output_directory'], relativepath, ppmfilename) if picinfo is None: return None # in case we want the original image, copy the raw file if not options['forcecompress']: for (w,h) in options['sizelist']: if w == 0 and h == 0: # Ok we really want the original image, so we need to copy the image destfile = os.path.join(destdir, "original", filename) if file_is_newer(destfile, srcfile): safe_mkdir(destdir) shutil.copyfile(srcfile, destfile) # ... and delete the old .pnm image ppmpath = os.path.join(destdir, "original", ppmfilename) tempfile_list.append(ppmpath) break # Now returns, the image information picinfo['is_raw'] = 1 picinfo['realfilename'] = filename return picinfo # ------------------------------------------------------------------------ def generate_list_of_frames(total_frames, wanted_frames): """ Return a list (random) or non random of frames we want """ if False: listoframes = sample(xrange(total_frames), wanted_frames) listoframes.sort() return listoframes else: listoframes = [] frame = 0 while len(listoframes) < wanted_frames: listoframes.append(frame) frame += total_frames/wanted_frames return listoframes # ------------------------------------------------------------------------ def process_comment_file(realpath, relativepath, images_list): # """ Process the .comments in this directory that contains a comment for an image """ try: f = open(os.path.join(realpath, relativepath, options['comment_filename']), 'r') r = re.compile('^"([^"]+)"\s+"([^"]+)"') s = f.readline() while s: m = r.search(s) if m: # Ok we found a filename, and a comment in this line # TODO: use map function ? for i in images_list: if i['filename'] == m.group(1): i['comments'] = m.group(2) break # silently discard bad line ? else: if options['verbose_level'] >= 2: print "Bad Line: ",s pass s = f.readline() f.close() except IOError: pass # ------------------------------------------------------------------------ def make_directory_html(relativepath, directories_list, images_list): # """ Make all html page for index this directory """ if relativepath == "" and not options['generate_root_index_html']: return images_processed = 0 # Number of the images currently processed in the list page_index = 0 # Number of index.html page currently processed total_images = len(images_list) + len(directories_list) items_per_page = options['display_columns'] * options['display_lines']; if items_per_page > total_images: items_per_page = total_images output_directory = os.path.join(options['output_directory'], relativepath) # Generate extern file for the directory (images,...) copy_data_files(output_directory) # Load the template template_content = template_loader.load_text("index.html") template = airspeed.Template(template_content) # Initialize data to fill in the template namespace = options namespace['version'] = Photon.version if relativepath == "": namespace['title'] = "Albums"; else: namespace['title'] = relativepath.split(os.sep)[-1]; namespace['navbar'] = navbar_for_index_html(relativepath) if options['awstats']: namespace['awstats_script_url'] = options['awstats_script_url'] namespace['thumbsize_width'] = options['thumbsize'][0] namespace['thumbsize_height'] = options['thumbsize'][1] while images_processed < total_images: if namespace.has_key('row_images'): del namespace['row_images'] if namespace.has_key('row_dirs'): del namespace['row_dirs'] if images_processed == 0: old_index_html_filename = 'index.html' else: old_index_html_filename = 'index%d.html' % (images_processed / items_per_page) old_index_html_filename = os.path.join(output_directory, old_index_html_filename) index_html_filename = old_index_html_filename + '.new' fill_index_html_header(namespace, relativepath, None, None) if images_processed == 0: fill_index_html_directories(namespace, directories_list , images_list) total_images -= len(directories_list) # Small hack, because i want to do a do {} while(x) fill_index_html_images(namespace, images_list[images_processed : images_processed + items_per_page]) fill_index_html_footer(namespace, page_index, images_processed, total_images) f = open(index_html_filename, 'w') template.merge_to(namespace, f, template_loader) f.close() replace_if_different(old_index_html_filename, index_html_filename) images_processed += items_per_page page_index += 1 # ------------------------------------------------------------------------ def make_image_html(relativepath, directories_list, images_list): # """ Make all html page for all images """ output_directory = os.path.join(options['output_directory'], relativepath) if options['exif']: make_exif_js(os.path.join(output_directory, 'exif.js')) if options['javascript']: make_shortcut_js(os.path.join(output_directory, 'shortcuts.js')) # Load the template template_image = airspeed.Template(template_loader.load_text("image.html")) template_movie = airspeed.Template(template_loader.load_text("movie.html")) # initialize data to fill in the template namespace = options namespace['version'] = Photon.version namespace['thumbsize_width'] = options['thumbsize'][0] if options['awstats']: namespace['awstats_script_url'] = options['awstats_script_url'] if options['javascript']: namespace['on_load_script'] = "common_init()" elif options['exif']: namespace['on_load_script'] = "exif_init()" total_images = len(images_list) k = 0 while k < total_images: image = images_list[k] for resolution in options['sizelist'] + image['extra_resolution']: old_image_html_filename = get_htmlpage_for_image(image, resolution) old_image_html_filename = os.path.join(output_directory, urllib.unquote(old_image_html_filename)) image_html_filename = old_image_html_filename + '.new' # Calculate the previous and next image/link if k == 0: namespace['previous_image'] = None else: previous_image = images_list[k - 1] namespace['previous_image'] = {} namespace['previous_image']['page'] = get_htmlpage_for_image(previous_image, resolution) namespace['previous_image']['img'] = get_url_for_image(previous_image, resolution) namespace['previous_image']['thumbimg'] = get_url_for_image(previous_image, options['thumbsize']) namespace['previous_image']['filename'] = previous_image['filename'] if previous_image.has_key('comments'): namespace['previous_image']['comments'] = previous_image['comments'] if k + 1 >= len(images_list): namespace['next_image'] = None else: next_image = images_list[k + 1] namespace['next_image'] = {} namespace['next_image']['page'] = get_htmlpage_for_image(next_image, resolution) namespace['next_image']['img'] = get_url_for_image(next_image, resolution) namespace['next_image']['thumbimg'] = get_url_for_image(next_image, options['thumbsize']) namespace['next_image']['filename'] = next_image['filename'] #print "next_image of %s is %s" % (image['filename'], namespace['next_image']['page']) if next_image.has_key('comments'): namespace['next_image']['comments'] = next_image['comments'] namespace['title'] = image['filename'] namespace['comments'] = None namespace['movie_link'] = None namespace['movie_filename'] = None namespace['movie_frames'] = None namespace['img_is_raw'] = None namespace['list_of_exif_properties'] = None namespace['has_exif'] = None # Make the html page for this image and this resolution f = open(image_html_filename, 'w') if image.has_key('movie'): fill_movie_html_body(namespace, relativepath, images_list, k, resolution) template_movie.merge_to(namespace, f, template_loader) else: fill_image_html_body(namespace, relativepath, images_list, k, resolution) if options['exif']: fill_image_html_exif_window(namespace, image) template_image.merge_to(namespace, f, template_loader) f.close() replace_if_different(old_image_html_filename, image_html_filename) # End: for resolution in options['sizelist']: k += 1 # End: for image in images_list # ------------------------------------------------------------------------ # # Functions that output HTML Code # def fill_index_html_header(namespace, relativepath, previous_link, next_link): # """ Write in the file, header of an index.html page (body included) """ if previous_link: namespace['previous_link'] = previous_link if next_link: namespace['next_link'] = next_link # ------------------------------------------------------------------------ def fill_index_html_directories(namespace, directories_list, images_list): # """ Output a HTML table that contains the directories list """ if len(directories_list)==0: return column = 0 row_dirs = [] cases = [] # Create a case for each directory for d in directories_list: if column >= options['display_columns']: row_dirs.append(cases) column = 0 cases = [] case = {} case['url'] = urllib.quote(d) case['name'] = d # if the directory has a jpeg with the same name, use it folderimg = find_imagename_into_imagelist(d, images_list) if folderimg == None: case['type'] = 'normal' else: case['type'] = 'ext' if options['verbose_level'] >= 1: print "Found a small miniature for this folder \"%s\" with image \"%s\"" % (d,folderimg['filename']) folderimgurl = urllib.quote(os.path.join("%dx%d" % options['thumbsize'], folderimg['filename'])) if folderimg['aspect'] == 34: thumbsize_height = options['thumbsize'][0] thumbsize_width = thumbsize_height / folderimg['ratio'] else: thumbsize_width = options['thumbsize'][0] thumbsize_height = thumbsize_width / folderimg['ratio'] case['thumbsize_width'] = thumbsize_width case['thumbsize_height'] = thumbsize_height case['thumb_url'] = folderimgurl case['thumb_name'] = folderimg['filename'] column += 1 cases.append(case) # Fill the empty case ... if column > 0: while column < options['display_columns']: case = {} case['type'] = 'blank' column += 1 cases.append(case) row_dirs.append(cases) namespace['row_dirs'] = row_dirs # ------------------------------------------------------------------------ def fill_index_html_images(namespace, images_list): # """ Output a HTML table that contains the images list """ if len(images_list)==0: return column = 0 row = 0 row_images = [] cases = [] # Create a case for each image for pic in images_list: if column >= options['display_columns']: # Create a new row, row_images.append(cases) cases = [] row += 1 column = 0 if pic['aspect'] == 34: thumbsize_height = options['thumbsize'][0] thumbsize_width = thumbsize_height / pic['ratio'] else: thumbsize_width = options['thumbsize'][0] thumbsize_height = thumbsize_width / pic['ratio'] case = {} case['thumbsize_width'] = thumbsize_width case['thumbsize_height'] = thumbsize_height case['img_html_url'] = get_htmlpage_for_image(pic, options['sizelist'][0]) case['img_url'] = urllib.quote(os.path.join("%dx%d" % options['thumbsize'], pic['filename'])) if pic.has_key('movie'): case['type'] = 'movie' case['name'] = pic['realfilename'] else: case['type'] = 'img' case['name'] = pic['filename'] column += 1 cases.append(case) # Fill the empty case ... if column > 0: while column < options['display_columns']: # A blank image case = {} case['type'] = 'blank' column += 1 cases.append(case) row_images.append(cases) namespace['row_images'] = row_images # ------------------------------------------------------------------------ def fill_index_html_footer(namespace, page_index, images_processed, total_images): # """ Fill the footer for a index.html page """ items_per_page = options['display_columns'] * options['display_lines']; namespace['items_per_page'] = items_per_page namespace['page_index'] = page_index namespace['previous_page_index'] = page_index-1 # Output the Next link max_pages = total_images / items_per_page if (total_images % items_per_page) == 0: max_pages -= 1 namespace['max_pages'] = max_pages namespace['next_page_index'] = page_index+1 namespace['total_images'] = total_images # Output the Number of Images if total_images>0: max_images_displayed = images_processed + items_per_page if max_images_displayed > total_images: max_images_displayed = total_images namespace['max_images_displayed'] = max_images_displayed namespace['images_processed'] = images_processed+1 # ------------------------------------------------------------------------ def fill_image_html_body(namespace, relativepath, images_list, index, resolution): # """ Output in the file f, the content of the body for this image """ imageinfo = images_list[index] namespace['navbar'] = navbar_for_image_html(relativepath, images_list, index) namespace['img_img_link'] = get_url_for_image(imageinfo,resolution) namespace['img_img_alt'] = imageinfo['filename'] image_html_body_sub(namespace, imageinfo, resolution) not_available = False if imageinfo['aspect']==34: if resolution[1] > imageinfo['original_size'][1]: not_available = True else: if resolution[0] > imageinfo['original_size'][0]: not_available = True if imageinfo.has_key('is_raw') and resolution == (0,0) and not options['forcecompress']: not_available = True if not_available: namespace['img_img_link'] = 'notavailable.png' namespace['img_img_alt'] = "This image is not available in this resolution" if imageinfo.has_key('comments'): namespace['comments'] = imageinfo['comments'] # ------------------------------------------------------------------------ def fill_movie_html_body(namespace, relativepath, images_list, index, resolution): # """ Output in the file f, the content of the body for this movie """ imageinfo = images_list[index] namespace['navbar'] = navbar_for_image_html(relativepath, images_list, index) namespace['img_alt'] = imageinfo['filename'] image_html_body_sub(namespace, imageinfo, resolution) if resolution[0] > imageinfo['original_size'][0]: namespace['img_alt'] = "This image is not available in this resolution" # # Use the original size of the image is always smaller than the # greater image. So find the best resolution in the list, and # display this map. The best solution, is to display correctly # using the real screen size. # if resolution == (0,0): current_best_size = (0,0) for s in options['sizelist']: if s == (0,0) or s == options['thumbsize']: continue if s[0]*s[1] > current_best_size[0]*current_best_size[1]: current_best_size = s resolution = current_best_size if resolution == (0,0): # FIXME: find the real resolution of the movie return # Now compute the filename used to build a wall of images frames = imageinfo["frames_%dx%d" % resolution] templateimgsrc = re.sub('^(.*)\.[^.]+$',"\\1", imageinfo['realfilename']) templateimgurl = urllib.quote("%dx%d/%s" % (imageinfo["thumbsize"][0], imageinfo["thumbsize"][1], templateimgsrc)) frames_per_width = resolution[0]/imageinfo['thumbsize'][0] frames_per_height = resolution[1]/imageinfo['thumbsize'][1] # The wall of images is build using a bigtable n = 0 namespace['movie_frames'] = [] for hhh in xrange(frames_per_height): row = [] for www in xrange(frames_per_width): frame = {} frame['url'] = "%s-%6.6d.jpg" % (templateimgurl, frames[n]) frame['width'] = imageinfo['thumbsize'][0] frame['height'] = imageinfo['thumbsize'][1] n+=1 row.append(frame) namespace['movie_frames'].append(row) # ------------------------------------------------------------------------ def image_html_body_sub(namespace, imageinfo, resolution): # """ Return the HTML code to display 2 thumbnails to include in the image page """ # If the resolution is not a standard resolution, but part of extra, then # don't link next and previous image with that resolution, but the first in # the list #print imageinfo['filename'], ": resolution %dx%d" % resolution if resolution in imageinfo['extra_resolution']: resolution = options['sizelist'][0] if imageinfo.has_key('movie'): namespace['movie_link'] = get_url_for_movie(imageinfo) namespace['movie_filename'] = imageinfo['realfilename'] namespace['movie_sizelist'] = [] for r in options['sizelist']: if r in (resolution, (0,0), imageinfo['thumbsize']): continue res = {} res['frames_per_width'] = r[0]/imageinfo['thumbsize'][0] res['frames_per_height'] = r[1]/imageinfo['thumbsize'][1] res['accesskey'] = res['frames_per_width'] res['pageurl'] = get_htmlpage_for_image(imageinfo, r) namespace['movie_sizelist'].append(res) # end of for r in options['sizelist']: # This is a normal image (not a movie) elif len(options['sizelist']): # Special case for raw files if imageinfo.has_key('is_raw') and not options['forcecompress']: namespace['img_is_raw'] = 1 namespace['img_raw_link'] = urllib.quote("original/%s" % (imageinfo['realfilename'])) namespace['img_raw_filename'] = imageinfo['realfilename'] accesskeys_already_used = [] namespace['img_sizelist'] = [] for r in options['sizelist'] + imageinfo['extra_resolution']: if r == resolution or r == options['thumbsize']: continue # Don't add image that is smaller than the original file if imageinfo['aspect']==34: if r[1] > imageinfo['original_size'][1]: continue else: if r[0] > imageinfo['original_size'][0]: continue res = {} res['imgurl'] = get_url_for_image(imageinfo,r) res['pageurl'] = get_htmlpage_for_image(imageinfo,r) if r == (0,0): res['size'] = "Original" else: if imageinfo['aspect'] == 34: res['size'] = "%dx%d" % (r[1], r[0]) else: res['size'] = "%dx%d" % r # Find the first letter for accesskeys for k in xrange(len(res['size'])): if res['size'][k] not in accesskeys_already_used: accesskey = res['size'][k] res['accesskey'] = accesskey accesskeys_already_used.append(accesskey) break namespace['img_sizelist'].append(res) # End: for r in options['sizelist'] + imageinfo['extra_resolution']: # End: if len(options['sizelist']): # ------------------------------------------------------------------------ def exif_resolv_ratio(ratio_of_ifdtag): # """ Convert a string that represent a ratio (x/y) to the value of this ratio """ # Convert ratio format into a float if isinstance(ratio_of_ifdtag, EXIF.IFD_Tag): if ratio_of_ifdtag.field_type != 5: # Not ratio type return ratio_of_ifdtag ratio = ratio_of_ifdtag.printable else: ratio = ratio_of_ifdtag m = re.search("^(\d+)/(\d+)$", ratio) if m: return float(m.group(1)) / float(m.group(2)) else: return float(ratio) # ------------------------------------------------------------------------ def fill_image_html_exif_window(namespace, imageinfo): # """ Output the HTML code for the exif window page """ list_of_exif_properties = [] for exif_data in exif_summary: prop_name = exif_data[0] exif_key = exif_data[1] if len(exif_data)>2: unit = exif_data[2] else: unit = "" if imageinfo['exif'].has_key(exif_key): exif_property = {} exif_value = exif_resolv_ratio( imageinfo['exif'][exif_key] ) # Some special case if exif_key == 'EXIF FocalLength': #for a in imageinfo['exif']: # print "%s: %s" % (a, imageinfo['exif'][a]) #sys.exit(1) if imageinfo['exif'].has_key('EXIF FocalLengthIn35mmFilm'): exif_string= exif_resolv_ratio( imageinfo['exif']['EXIF FocalLengthIn35mmFilm'] ) prop_name += ' (35mm)' elif imageinfo['exif'].has_key('MakerNote FocalPlaneDiagonal'): # diagonal needs to be in mm diag= exif_resolv_ratio( imageinfo['exif']['MakerNote FocalPlaneDiagonal'] ) exif_string = "%.2f" % (exif_value * math.hypot(36,24) / diag) prop_name += ' (35mm)' elif imageinfo['exif'].has_key('EXIF FocalPlaneResolutionUnit') \ and imageinfo['exif'].has_key('EXIF FocalPlaneXResolution') \ and imageinfo['exif'].has_key('EXIF FocalPlaneYResolution'): # Try to compute the focal using the size of the image if imageinfo['exif'].has_key('MakerNote CanonImageWidth') \ and imageinfo['exif'].has_key('MakerNote CanonImageHeight'): w = int(imageinfo['exif']['MakerNote CanonImageWidth'].printable) h = int(imageinfo['exif']['MakerNote CanonImageHeight'].printable) elif imageinfo['exif'].has_key('EXIF ExifImageWidth') \ and imageinfo['exif'].has_key('EXIF ExifImageLength'): w = int(imageinfo['exif']['EXIF ExifImageWidth'].printable) h = int(imageinfo['exif']['EXIF ExifImageLength'].printable) elif imageinfo['exif'].has_key('EXIF ImageWidth') \ and imageinfo['exif'].has_key('EXIF ImageLength'): w = int(imageinfo['exif']['EXIF ImageWidth'].printable) h = int(imageinfo['exif']['EXIF ImageLength'].printable) else: (w,h) = imageinfo['original_size'] resunit = float(imageinfo['exif']['EXIF FocalPlaneResolutionUnit'].printable) xres = exif_resolv_ratio(imageinfo['exif']['EXIF FocalPlaneXResolution']) yres = exif_resolv_ratio(imageinfo['exif']['EXIF FocalPlaneYResolution']) diag = math.hypot(w*resunit/xres, h*resunit/yres) if diag == 0.0: diag = 1 exif_string = "%.2f" % (exif_value * math.hypot(36,24) / diag) prop_name += ' (35mm)' # We can't calculate focal length, so display the value without any information else: exif_string = "%.2f" % float(exif_value) elif exif_key == 'EXIF ExposureTime': if exif_value < 0.010: exif_string = "%6.4f s" % exif_value else: exif_string = "%5.3f s" % exif_value if exif_value < 0.5: exif_string += " (1/%d)" % (0.5 + (1/exif_value)) elif exif_key in ('EXIF MaxApertureValue', 'EXIF ApertureValue'): if imageinfo['exif'].has_key('EXIF FNumber'): exif_value = exif_resolv_ratio( imageinfo['exif']['EXIF FNumber'] ) else: exif_value = exp(exif_value)*log(2)*0.5 exif_string = "f/%3.1f" % exif_value else: exif_string = exif_value exif_property['name'] = prop_name exif_property['value'] = exif_string exif_property['unit'] = unit list_of_exif_properties.append(exif_property) namespace['list_of_exif_properties'] = list_of_exif_properties if len(namespace['list_of_exif_properties']): namespace['has_exif'] = 1 # ------------------------------------------------------------------------ def get_new_movie_filename_after_hack(pathname): # """ Return the movie filename to be copied Many digital camera produce MJPEG movie. It's very easy to parse because MJPEG is just a JPEG file without the huffman table. But the file is very big to download other internet; so instead of copying the original video, we recompress it in MPEG1/2 video (not by photon but by your program). So now (until photon known how to decode movie), we want to read the MJPEG file and produce jpeg frame, and copy the MPEG1/2 file. This fucntion does this: test if the movie change """ if options['movie_replace_mov_by_mpg'] and fnmatch(pathname, "*.mov"): try : mpegfile = re.sub('^(.*)\.[^.]+$',"\\1.mpg", pathname) os.stat(mpegfile)[ST_MODE] return mpegfile except OSError: pass return pathname # ------------------------------------------------------------------------ # # Useful misc function # def safe_mkdir(pathname): # """ Create a directory only when it doesn't exist """ try : mode = os.stat(pathname)[ST_MODE] if not S_ISDIR(mode): os.mkdir(pathname) except OSError: os.mkdir(pathname) # ------------------------------------------------------------------------ def navbar_for_index_html(relativepath): # """ Transform the directory location in a navigation bar printable in HTML """ if relativepath == "": return "Home" ndirs = 1 + relativepath.count(os.sep) site_home = "../" * ndirs url = 'Albums' % site_home for d in relativepath.split(os.sep): if d == "": continue ndirs -= 1 if ndirs == 1: url += ' -> ' + d + '' elif ndirs: url += ' -> ' + d + '' else: url += ' -> ' + d + '' return url # ------------------------------------------------------------------------ def navbar_for_image_html(relativepath, images_list, index): # """ Print the navigation bar for an image.html relativepath: Path where to found the image images_list: list of the images for this directory index: index to the current image in the images_list """ # Calculate the page number where this page is located items_per_page = options['display_columns'] * options['display_lines']; current_page = index / items_per_page if relativepath == "": if current_page: return 'Albums' % current_page else: return 'Albums' ndirs = 1 + relativepath.count(os.sep) site_home = "../" * ndirs url = 'Albums' for d in relativepath.split(os.sep): if d == "": continue ndirs -= 1 if ndirs: url += ' -> ' + d + '' else: if current_page: url += ' -> %s' % (current_page, d) url += ' -> ' + images_list[index]['filename'] + '' else: url += ' -> ' + d + ' -> ' + images_list[index]['filename'] + '' return url # ------------------------------------------------------------------------ def get_htmlpage_for_image(image, resolution): # """ Return the name of the HTML page for an image and a resolution """ # Strip (extension) .jpg from the filename imagename = image['filename'] i = imagename.rfind(".") image_filename_without_ext = imagename[0:i] # If the resolution is not a standard resolution, but part of extra, then # don't create an invalid url, but link to the first image resolution if resolution not in options['sizelist']: resolution = options['sizelist'][1] # if this image is the original size, do not append the resolution to the filename if resolution == (0,0): image_html_filename="%s.html" % image_filename_without_ext; else: resolution_string = "%dx%d" % resolution image_html_filename="%s_%s.html" % (image_filename_without_ext, resolution_string); return urllib.quote(image_html_filename) # ------------------------------------------------------------------------ def get_url_for_image(image, resolution): # """ Return the url for an image and a resolution """ if resolution == (0,0): if options['forcecompress']: resolution = image['original_size'] elif image.has_key('is_raw'): return urllib.quote("original/%s" % (image['realfilename'])) else: return urllib.quote("original/%s" % (image['filename'])) return urllib.quote("%dx%d/%s" % (resolution[0], resolution[1], image['filename'])) # ------------------------------------------------------------------------ def get_url_for_movie(image): # """ Return the url for an image and a resolution """ return urllib.quote("original/%s" % image['realfilename']); # ------------------------------------------------------------------------ def file_is_newer(pathname1, pathname2): # """ Compare the last modified time from 2 files (or directory). Returns True if path2 is newer than path1 (or if path1 does not exist)""" if options['forcecreate'] > 1: # forcecreate must be 2 return True try : mode1 = os.stat(pathname1)[ST_MTIME] mode2 = os.stat(pathname2)[ST_MTIME] return (mode1 < mode2) except OSError: return True # ------------------------------------------------------------------------ def replace_if_different(existentfile, newfile): # """ Replace existentfile with newfile if the contents are different and removes newfile """ try: if ((options['forcecreate'] > 0) or (os.stat(existentfile)[ST_SIZE] != os.stat(newfile)[ST_SIZE]) or (md5.new(open(existentfile).read()).digest() != md5.new(open(newfile).read()).digest())): os.rename(newfile, existentfile) except: os.rename(newfile, existentfile) try: os.unlink(newfile) except: pass # ------------------------------------------------------------------------ def generic_image_copy(filename, output_directory): """ Copy the file only if it's different """ dest_filename = os.path.join(output_directory,filename) for f in options['data_path']: src_filename = os.path.join(f,"images",filename) try: shutil.copy(src_filename,dest_filename + ".new") except IOError, e: if e.errno == errno.ENOENT: continue # try again raise else: replace_if_different(dest_filename, dest_filename + '.new') return # This file was not found in any path print "I can't copy this file %s" % filename # ------------------------------------------------------------------------ def copy_data_files(output_directory): # """ Copy a small gif 1x1 with a transparent color Copy a 320x240 png «this image is not available in this resolution» Copy icons use to display some various information """ generic_image_copy('blank.gif',output_directory); generic_image_copy('notavailable.png',output_directory); generic_image_copy('filesave.png',output_directory); if options['javascript']: generic_image_copy('player_pause.png',output_directory); generic_image_copy('player_play.png',output_directory); generic_image_copy('help.png',output_directory); generic_image_copy('info.png',output_directory); generic_image_copy('fullscreen.png',output_directory); if options['movie']: generic_image_copy('filmholes-big-left.png',output_directory); generic_image_copy('filmholes-big-right.png',output_directory); # ------------------------------------------------------------------------ def print_skins(): # """ Print skins available """ skins = [] for d in options['data_path']: tmpl_dir = os.path.join(d,"templates") try: for entry in os.listdir(tmpl_dir): # Don't accept directory or file beginning with a dot if entry[0] == '.': continue pathname = os.path.join(tmpl_dir, entry) mode = os.stat(pathname)[ST_MODE] if S_ISDIR(mode): # TODO: check if we have all file needed # Remove duplicate entry if entry not in skins: skins.append(entry) except OSError: pass skins.sort() print "Available skins:" for s in skins: print " ",s # ------------------------------------------------------------------------ def resize_image(srcfile, destfile, size, im = None): # """ Resize the image according to option """ # Sometimes (like) with gimp, resize is done later # We have 2 choices use Gimp, use Python Library, or our internal parser if options['resize_plugin'] == "gimp": if size[0]*size[1]<64000: gimp_file = (srcfile,destfile,size,options['resize_quality_low']) else: gimp_file = (srcfile,destfile,size,options['resize_quality_high']) gimp_list.append(gimp_file) elif options['resize_plugin'] == 'pil': try: if im == None: im = Image.open(srcfile) im = im.convert('RGB') im.resize(size, Image.BICUBIC).save(destfile, 'JPEG', optimize=1, progressive=1) except IOError, err: print "Error while writing thumbnail, will try without optimization..." print "Perhaps you can try to increase ImageFile.MAXBLOCK in the source file" print err try: # Try to save the Jpeg file without progessive option im.resize(size,Image.BICUBIC).save(destfile, 'JPEG') except IOError, err: print "cannot create ", destfile, "(", err, ")" else: # Use the python internal module resize method raise("Error: resize plugin not know") # ------------------------------------------------------------------------ def make_shortcut_js(pathname): # """ Make a separate javascript file that contains shortcut functions """ try : f = open(pathname + '.new','wb') except OSError: return 0 else: f.write(""" // Some variables to autodetect browser type var ns=(document.layers); var ie=(document.all); var w3=(document.getElementById && !ie); var timeout=5000; var help_isshow=0; var fullscreen_mode=0; // Return an object function get_object(id) { if(!ns && !ie && !w3) return null; if (ie) e=eval("document.all." + id); else if (ns) e=eval('document.links[id]'); else if (w3) e=eval('document.getElementById(id)'); return e; } // Return the style of an object function get_object_style(id) { var style = null; if (ie) style=eval('document.all.'+id+'.style'); else if (ns) style=eval('document.layers[id]'); else if (w3) style=eval('document.getElementById(id).style'); return style; } function add_arg_to_url(url, arg, value) { if (url.indexOf('?')>0) url += '&' + arg + '=' + value; else url += '?' + arg + '=' + value; return url; } // Change the current page to the id found in the page function jumpto(id) { e = get_object(id); if ((e != null) && (e.href != null)) { var url = e.href; if (mytimeout) url = add_arg_to_url(url, 'slideshow', timeout); if (fullscreen_mode) { url = add_arg_to_url(url, 'fullscreen', '1'); url += '#image'; } location.href = url; } } // Change the current page function next_page() { jumpto('next_link'); } function previous_page() { jumpto('previous_link'); } function show_navbar() { navbar_style = get_object_style('navbar'); if (ie || w3) navbar_style.visibility="visible"; else if (ns) navbar_style.visibility="show"; } function hide_navbar() { navbar_style = get_object_style('navbar'); if (ie || w3) navbar_style.visibility="collapse"; // don't work in konqueror else if (ns) navbar_style.visibility="hide"; } function show_help_layer() { help_layer_style = get_object_style('helpwindow'); if (ie) { documentWidth=document.body.offsetWidth/2+document.body.scrollLeft-20; documentHeight=document.body.offsetHeight/2+document.body.scrollTop-20; help_layer_style.visibility="visible"; } else if (ns) { documentWidth=window.innerWidth/2+window.pageXOffset-20; documentHeight=window.innerHeight/2+window.pageYOffset-20; help_layer_style.visibility ="show"; } else if (w3) { documentWidth=self.innerWidth/2+window.pageXOffset-20; documentHeight=self.innerHeight/2+window.pageYOffset-20; help_layer_style.visibility="visible"; } help_layer_style.left=documentWidth-250; help_layer_style.top =documentHeight-125; help_isshow=1; } function hide_help_layer() { help_layer_style = get_object_style('helpwindow'); if (ie||w3) help_layer_style.visibility="hidden"; else help_layer_style.visibility="hide"; help_isshow=0; return false; } function toggle_help_layer() { if (help_isshow) hide_help_layer(); else show_help_layer(); return false; } // Activate the fullscreen mode (works only in a new window) function toggle_fullscreen() { if (!fullscreen_mode) { var screen_width=screen.availWidth; var screen_height=screen.availHeight; var features='width='+screen_width; features += ',height='+screen_height; var url = add_arg_to_url(location.href, 'fullscreen', '1'); url += '#image'; window.open(url,'',features); } else { window.close(); } } // Activate/Deactivate the slideshow var mytimeout = 0; function toggle_slideshow() { if (!mytimeout) { mytimeout = setTimeout("next_page()",timeout); window.status='Slideshow set to ' + (timeout/1000) + ' seconds'; e = get_object('slideicon'); if ((e != null)) e.src = "player_pause.png"; } else { clearTimeout(mytimeout); mytimeout=0; window.status='Stopping Slideshow'; e = get_object('slideicon'); if ((e != null)) e.src = "player_play.png"; } } // Manage timeout for the slideshow function modify_timeout(t) { timeout+=t; if (timeout<1000) timeout=1000; if (mytimeout) { // If the counter is active, reactivate it ! toggle_slideshow(); toggle_slideshow(); } else { window.status='Slideshow timeout set to ' + (timeout/1000) + ' seconds'; } } // Event Handler that receive Key Event function getkey(e) { if (e == null) { // IE kcode = window.event.keyCode; } else { // Mozilla kcode = e.which; } key = String.fromCharCode(kcode).toLowerCase(); switch(key) { case "n": case " ": next_page(); return false; case "p": previous_page(); return false; case "s": toggle_slideshow(); return false; case "+": modify_timeout(1000); return false; case "-": modify_timeout(-1000); return false; case "i": toggle_exif_window() return false; case "h": case "?": toggle_help_layer(); return false; } switch(kcode) { case 8: previous_page(); return false; } return true; } function common_init() { if (typeof(exif_init) == "function") exif_init() // Get arguments parameters for this page var argstr = location.search.substring(1, location.search.length) var args = argstr.split('&'); for (var i = 0; i < args.length; i++) { var arg = unescape(args[i]).split('='); if (arg[0] == "slideshow") { // ... and set timeout according to the last value timeout=parseInt(arg[1]); toggle_slideshow(); } else if (arg[0] == "fullscreen") { fullscreen_mode = 1; self.moveTo(0,0); self.resizeTo(screen.width,screen.height); //hide_navbar(); } } // Some code for preloading the next image e = get_object('next_image'); if ((e != null) && (e.href != null)) { preload_image = new Image(); preload_image.src = e.href; } } if(w3 || ie) { document.onkeypress = getkey; } else { document.captureEvents(Event.KEYUP); document.onkeyup = getkey; document.captureEvents(Event.KEYPRESS); document.onkeypress = getkey; } """) f.close() replace_if_different(pathname, pathname + '.new') # ------------------------------------------------------------------------ def make_exif_js(pathname): # """ Make a separate javascript file that contains dynamic html functions """ try : f = open(pathname + '.new', 'w') except OSError: return 0 else: f.write(""" var ns=(document.layers); var ie=(document.all); var w3=(document.getElementById && !ie); var exif_layer; var exif_isshow; function exif_init() { if(!ns && !ie && !w3) return; if (ie) exif_layer=eval('document.all.exifwindow.style'); else if (ns) exif_layer=eval('document.layers["exifwindow"]'); else if (w3) exif_layer=eval('document.getElementById("exifwindow").style'); exif_hide(); } function exif_show() { if (ie) { documentWidth =document.body.offsetWidth/2+document.body.scrollLeft-20; documentHeight =document.body.offsetHeight/2+document.body.scrollTop-20; exif_layer.visibility="visible"; } else if (ns) { documentWidth=window.innerWidth/2+window.pageXOffset-20; documentHeight=window.innerHeight/2+window.pageYOffset-20; exif_layer.visibility ="show"; } else if (w3) { documentWidth=self.innerWidth/2+window.pageXOffset-20; documentHeight=self.innerHeight/2+window.pageYOffset-20; exif_layer.visibility="visible"; } exif_layer.left=documentWidth-150; exif_layer.top =documentHeight-125; exif_isshow=1; setTimeout("exif_hide()",10000); } function exif_hide() { if (ie||w3) exif_layer.visibility="hidden"; else exif_layer.visibility="hide"; exif_isshow=0; } function toggle_exif_window() { if (exif_isshow) exif_hide() else exif_show() } """) f.close() replace_if_different(pathname, pathname + '.new') # ------------------------------------------------------------------------ def match_color_type(color): # """ Return the string when the string match a color in HTML format """ if color[0] == "#": if re.match("^#[0-9a-f]{6}$",color): return color elif re.match("^[a-z]+$",a): return color return None # ------------------------------------------------------------------------ def gimp_resize_files(fileslist,flush=0): # """ Use Gimp to resize an images list""" # Test Gimp version, script-fu changes with gimp-2.0 gimp_version = gimp_get_version(options['gimp_program']) if gimp_version is None: gimp_copy_image_function = "gimp-channel-ops-duplicate" elif gimp_version >= 0x020000: gimp_copy_image_function = "gimp-image-duplicate" else: gimp_copy_image_function = "gimp-channel-ops-duplicate" # Create our batch file for gimp batchfile = os.path.join(options['tempdir'],'photon.scm') prefix_in = os.path.join(options['tempdir'],'in') prefix_out = os.path.join(options['tempdir'],'out') try : #f = open(batchfile, "wb") f = codecs.open(batchfile,"wb",'utf-8'); except OSError: return 0 else: f.write("""; photon.scm ; Copyright: Luc Saillard 2002-2007 ; Licence: Artistic License ; Gimp Script-Fu ; Plugin to resize all files in a batch using the best quality from the gimp ; create a thumbnail from a filename (define (create-thumbnail filename_in filename_out tn_width tn_height jpeg_quality) (let* ((image (car (gimp-file-load 1 filename_in filename_in))) (drawable (car (gimp-image-active-drawable image)))) (gimp-image-undo-disable image) (gimp-image-scale image tn_width tn_height) ;(gimp_text_fontname image drawable 0 0 "Copyright: Luc Saillard" 10 TRUE 50.0 0 "Sans") (file-jpeg-save 1 image drawable filename_out filename_out jpeg_quality 0.0 1 1 "" 0 1 0 0) (gimp-image-delete image) )) ; create a thumbnail with a already loaded image (define (create-thumbnail-multiple image filename_out tn_width tn_height jpeg_quality) (let* ((image_copy (car (%s image))) (drawable_copy (car (gimp-image-active-drawable image_copy)))) (gimp-image-undo-disable image_copy) (gimp-image-scale image_copy tn_width tn_height) ;(gimp_text_fontname image drawable 0 0 "Copyright: Luc Saillard" 10 TRUE 50.0 0 "Sans") (file-jpeg-save 1 image_copy drawable_copy filename_out filename_out jpeg_quality 0.0 1 1 "" 0 1 0 0) (gimp-image-delete image_copy) )) """ % gimp_copy_image_function) # # Ok, for each files in fileslist, we generate a scrit-fu that: # read the file (let * (image (car (gimp-file-load 1 \"in.jpeg\" \"in.jpeg\")))) # create all thumbnails for this images # (create-thumbnail-multiple image \"out.jpeg\" 640 480 jpeg_quality) # free memory for the last image # We have one function that do in one shot # (create-thumbnail "in.jpeg" "out" 640 480 jpeg_quality) # The big problem is Gimp 2.x: Gimp want utf-8 string, but some file can be with # another locale. If we convert filename in UTF-8, gimp can load the file (file not found) # So to fix this problem, we link in the tempdir, all the input file. # oldfile="" i=0 while i < len(fileslist): file=fileslist[i] (nam,ext) = os.path.splitext(file[0]) filein = "%s-%d%s" % (prefix_in,i,ext) fileout = "%s-%d%s" % (prefix_out,i,ext) os.symlink(os.path.abspath(file[0]),filein) os.symlink(os.path.abspath(file[1]),fileout) vars = { 'ifnam_uni' : unicode(file[0],options['charset']), 'ifnam' : filein, 'ofnam_uni' : unicode(file[1],options['charset']), 'ofnam' : fileout, 'img_cur' : i, 'img_max' : len(fileslist)-1, 'img_width' : file[2][0], 'img_height' : file[2][1], 'img_qual' : file[3] } if (file[0] == oldfile): # The next image is in the same sequence than the one present f.write(""" (gimp-message "Saving %(ofnam_uni)s %(img_cur)d/%(img_max)d") (create-thumbnail-multiple image "%(ofnam)s" %(img_width)d %(img_height)d %(img_qual)f) """ % vars) if (i+1>=len(fileslist)) or (file[0] != fileslist[i+1][0]): f.write(" (gimp-image-delete image))\n") else: if (i+1 compressing a jpeg file ImageFile.MAXBLOCK = 1000000 # default is 64k options['fileformat_plugin'] = 'pil' if find_gimp_program() is None: options['resize_plugin'] = 'pil' except: from Photon import Image options['fileformat_plugin'] = 'internal' # ------------------------------------------------------------------------ # Detect if locale can be use to find the current charset try: locale.setlocale(locale.LC_ALL,'') # We run some external program in locale C, so fix the numeric conversion locale.setlocale(locale.LC_NUMERIC,'C') except locale.Error, x: print >> sys.stderr, '*** Warning:', x try: options['charset'] = locale.nl_langinfo(locale.CODESET) except AttributeError: if os.getenv('CHARSET') is None: print "Warning: You have an old python interpreter, and you have not defined" print "CHARSET environment variable. I will use %s by default" % options['charset'] else: options['charset'] = os.getenv('CHARSET') # ------------------------------------------------------------------------ # Try to find where data file is located on the file system, and insert the # user home directory in the head try: import distutils from distutils import sysconfig options['data_path'].insert(0,os.path.join(sysconfig.get_config_var('prefix'),'share','photon')) options['data_path'].append(os.path.dirname(sys.argv[0])) except ImportError: pass except distutils.errors.DistutilsPlatformError: options['data_path'].insert(0,os.path.join('/','usr','share','photon')) options['data_path'].append(os.path.dirname(sys.argv[0])) try: homedir = os.path.expanduser("~/.photon/") options['data_path'].insert(0, homedir) except OSError: pass template_loader = PhotonTemplateLoader() if __name__ == "__main__": # import profile # profile.run('main()') main() Photon-0.4.6/setup.cfg0000644000175000017500000000016710331233474012745 0ustar lucluc[bdist_rpm] release = 1 packager = Luc Saillard doc_files = ChangeLog README BUGS Photon-0.4.6/setup.py0000644000175000017500000000335310417741274012645 0ustar lucluc#!/usr/bin/env python import sys, glob assert sys.version >= '2', "Install Python 2.0 or greater" try: from distutils.core import setup except ImportError: raise SystemExit, "Photon requires distutils to build and install. Perhaps you need to install python-devel ?" import Photon # patch distutils if it can't cope with the "classifiers" or # "download_url" keywords if sys.version < '2.2.3': from distutils.dist import DistributionMetadata DistributionMetadata.classifiers = None DistributionMetadata.download_url = None DESCRIPTION="""Photon is a photo album with a clean design. Features: * static HTML pages (you can put all pages and images on a CD-ROM) * slideshow (use javascript optional) * can use gimp to resize picture * navigation between the image can use the keyboard (use javascript optional) * works in any browser (Mozilla, Netscape Navigator 4.x, Konqueror, Opera) * Each image can have a comment (with HTML tags) * Information about the image (if taken from a digital picture) can be draw * thumbnail image size can be choosen by the user * output images can be scalled down * movie support * control the number of thumbnail in a page.""" setup(name="Photon", version=Photon.version, author="Luc Saillard", author_email="luc@saillard.org", url="http://www.saillard.org/photon/", description="Photon is a static HTML gallery generator", long_description=DESCRIPTION, license = "GNU GPL v2", packages=['Photon'], scripts=['photon'], data_files=[('share/photon/images', glob.glob('images/*.gif')+ glob.glob('images/*.png')), ('share/photon/templates/photonv1', glob.glob('templates/photonv1/*')) ] ) Photon-0.4.6/MANIFEST0000644000175000017500000000134410713565162012261 0ustar lucluc.hgtags BUGS ChangeLog LICENSE Makefile Photon/AVI.py Photon/EXIF.py Photon/GIF.py Photon/Image.py Photon/JPEG.py Photon/PNG.py Photon/PxM.py Photon/QuickTime.py Photon/RAW.py Photon/Video.py Photon/__init__.py Photon/airspeed.py README README.templates debian/changelog debian/compat debian/control debian/copyright debian/dirs debian/docs debian/rules images/blank.gif images/filesave.png images/filmholes-big-left.png images/filmholes-big-right.png images/fullscreen.png images/help.png images/info.png images/notavailable.png images/player_pause.png images/player_play.png photon setup.cfg setup.py templates/photonv1/common_footer.html templates/photonv1/image.html templates/photonv1/index.html templates/photonv1/movie.html MANIFEST Photon-0.4.6/PKG-INFO0000644000175000017500000000170110713565311012216 0ustar luclucMetadata-Version: 1.0 Name: Photon Version: 0.4.6 Summary: Photon is a static HTML gallery generator Home-page: http://www.saillard.org/photon/ Author: Luc Saillard Author-email: luc@saillard.org License: GNU GPL v2 Description: Photon is a photo album with a clean design. Features: * static HTML pages (you can put all pages and images on a CD-ROM) * slideshow (use javascript optional) * can use gimp to resize picture * navigation between the image can use the keyboard (use javascript optional) * works in any browser (Mozilla, Netscape Navigator 4.x, Konqueror, Opera) * Each image can have a comment (with HTML tags) * Information about the image (if taken from a digital picture) can be draw * thumbnail image size can be choosen by the user * output images can be scalled down * movie support * control the number of thumbnail in a page. Platform: UNKNOWN