diff options
Diffstat (limited to 'konq-plugins/imagerotation')
-rw-r--r-- | konq-plugins/imagerotation/Makefile.am | 7 | ||||
-rwxr-xr-x | konq-plugins/imagerotation/exif.py | 1079 | ||||
-rw-r--r-- | konq-plugins/imagerotation/imageconverter.desktop | 83 | ||||
-rwxr-xr-x | konq-plugins/imagerotation/jpegorient | 89 | ||||
-rw-r--r-- | konq-plugins/imagerotation/jpegorient.desktop | 307 | ||||
-rwxr-xr-x | konq-plugins/imagerotation/orient.py | 104 |
6 files changed, 1669 insertions, 0 deletions
diff --git a/konq-plugins/imagerotation/Makefile.am b/konq-plugins/imagerotation/Makefile.am new file mode 100644 index 0000000..f0686ad --- /dev/null +++ b/konq-plugins/imagerotation/Makefile.am @@ -0,0 +1,7 @@ +progdir = $(kde_datadir)/imagerotation +prog_SCRIPTS = exif.py orient.py + +bin_SCRIPTS = jpegorient + +desktop_DATA = jpegorient.desktop imageconverter.desktop +desktopdir = $(kde_datadir)/konqueror/servicemenus diff --git a/konq-plugins/imagerotation/exif.py b/konq-plugins/imagerotation/exif.py new file mode 100755 index 0000000..309df47 --- /dev/null +++ b/konq-plugins/imagerotation/exif.py @@ -0,0 +1,1079 @@ +#! /usr/bin/env python +# Library to extract EXIF information in digital camera image files +# +# Contains code from "exifdump.py" originally written by Thierry Bousch +# <bousch@topo.math.u-psud.fr> and released into the public domain. +# +# Updated and turned into general-purpose library by Gene Cash +# <gcash@cfl.rr.com> +# +# NOTE: This version has been modified by Leif Jensen +# +# 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. +# +# To do: +# * Finish Canon MakerNote format +# * Better printing of ratios + +# field type descriptions as (length, abbreviation, full name) tuples +FIELD_TYPES=( + (0, 'X', 'Dummy'), # 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', ), + 0x9000: ('ExifVersion', ), + 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'}), + 0x9208: ('LightSource', + {0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten', + 10: 'Flash', + 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: 'Fired', + 5: 'Fired (?)', # no return sensed + 7: 'Fired (!)', # return sensed + 9: 'Fill Fired', + 13: 'Fill Fired (?)', + 15: 'Fill Fired (!)', + 16: 'Off', + 24: 'Auto Off', + 25: 'Auto Fired', + 29: 'Auto Fired (?)', + 31: 'Auto Fired (!)', + 32: 'Not Available'}), + 0x920A: ('FocalLength', ), + 0x927C: ('MakerNote', ), + 0x9286: ('UserComment', ), + 0x9290: ('SubSecTime', ), + 0x9291: ('SubSecTimeOriginal', ), + 0x9292: ('SubSecTimeDigitized', ), + 0xA000: ('FlashPixVersion', ), + 0xA001: ('ColorSpace', ), + 0xA002: ('ExifImageWidth', ), + 0xA003: ('ExifImageLength', ), + 0xA005: ('InteroperabilityOffset', ), + 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP + 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - - + 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - - + 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - - + 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - - + 0xA214: ('SubjectLocation', ), # 0x9214 - - + 0xA215: ('ExposureIndex', ), # 0x9215 - - + 0xA217: ('SensingMethod', ), # 0x9217 - - + 0xA300: ('FileSource', + {3: 'Digital Camera'}), + 0xA301: ('SceneType', + {1: 'Directly Photographed'}), + } + +# 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'} + 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'}), + 0x0202: ('Macro', + {0: 'Normal', + 1: 'Macro'}), + 0x0204: ('DigitalZoom', ), + 0x0207: ('SoftwareRelease', ), + 0x0208: ('PictureInfo', ), + # print as string + 0x0209: ('CameraID', lambda x: ''.join(map(chr, x))), + 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', lambda x: ''.join(map(chr, x))), + 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', ) + } + +# extract multibyte integer in Motorola format (little endian) +def s2n_motorola(str): + x=0 + for c in str: + x=(long(x) << 8) | ord(c) + return x + +# extract multibyte integer in Intel format (big endian) +def s2n_intel(str): + x=0 + y=0 + 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() # ugh, 259/250 worse 1036/1000 + if self.den == 1: + return str(self.num) + return '%d/%d' % (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): + self.printable=printable + self.tag=tag + self.field_type=field_type + self.field_offset=field_offset + self.field_length=field_length + 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=1L << (8*length-1) + if val & msb: + val=val-(msb << 1) + 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='' + elif tag == 0x927C or tag == 0x9286: # MakerNote or UserComment +# elif tag == 0x9286: # UserComment + 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) + for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), + ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)): + if self.debug: + print ' SubMakerNote BitSet for ' +i[0] + self.canon_decode_tag(self.tags[i[0]].values, i[1]) + 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] + if self.debug: + print ' '+name+':', val + self.tags['MakerNote '+name]=val + +# 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, noclose=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.tags['Exif Offset'] = offset + hdr.tags['Exif Endian'] = endian + hdr.tags[IFD_name+' IFDOffset'] = 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') + intr_off=hdr.tags.get('EXIF 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) + # deal with MakerNote contained in EXIF IFD + if hdr.tags.has_key('EXIF MakerNote'): + if debug: + print ' EXIF MakerNote SubSubIFD at offset %d:' \ + % intr_off.values[0] + hdr.decode_maker_note() + # GPS IFD + gps_off=hdr.tags.get(IFD_name+' GPSInfoOffset') + 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) + + # 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) + + if noclose == 0: + file.close() + 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, 1) + if not data: + print 'No EXIF information found' + continue + +# x=data.keys() +# x.sort() +# for i in x: +# if i in ('JPEGThumbnail', 'TIFFThumbnail'): +# continue +# print ' %s (%s): %s' % \ +# (i, FIELD_TYPES[data[i].field_type][2], data[i].printable) +# if data.has_key('JPEGThumbnail'): +# print 'File has JPEG thumbnail' +# print diff --git a/konq-plugins/imagerotation/imageconverter.desktop b/konq-plugins/imagerotation/imageconverter.desktop new file mode 100644 index 0000000..07cb700 --- /dev/null +++ b/konq-plugins/imagerotation/imageconverter.desktop @@ -0,0 +1,83 @@ +# +# Servicemenu image converter, by Jens Benecke <jens-kde@spamfreemail.de>. +# Released under the same license as the KDE core distribution (GPL 2.0). +# +[Desktop Entry] +ServiceTypes=image/* +Actions=convToJPEG;convToPNG;convToGIF;convToTIF; +X-KDE-Submenu=Convert To +X-KDE-Submenu[bg]=Конвертиране в +X-KDE-Submenu[ca]=Converteix a +X-KDE-Submenu[cs]=Převést na +X-KDE-Submenu[da]=Konvertér til +X-KDE-Submenu[de]=Konvertieren in +X-KDE-Submenu[el]=Μετατροπή σε +X-KDE-Submenu[eo]=Konvertu al +X-KDE-Submenu[es]=Convertir a +X-KDE-Submenu[et]=Teisendamine +X-KDE-Submenu[eu]=Bihurtu honetara +X-KDE-Submenu[fa]=تبدیل به +X-KDE-Submenu[fi]=Muunna +X-KDE-Submenu[fr]=Convertir en +X-KDE-Submenu[fy]=Konvertearje nei +X-KDE-Submenu[ga]=Tiontaigh Go +X-KDE-Submenu[gl]=Converter En +X-KDE-Submenu[hr]=Pretvori u +X-KDE-Submenu[hu]=Átalakítás +X-KDE-Submenu[is]=Umbreyta í +X-KDE-Submenu[it]=Converti in +X-KDE-Submenu[ja]=変換 +X-KDE-Submenu[ka]=კონვერტაცია +X-KDE-Submenu[kk]=Мынаған айналдыру +X-KDE-Submenu[km]=បម្លែងទៅជា +X-KDE-Submenu[lt]=Konvertuoti į +X-KDE-Submenu[mk]=Претвори во +X-KDE-Submenu[nb]=Konverter til +X-KDE-Submenu[nds]=Ümwanneln na +X-KDE-Submenu[ne]=यसमा बदल्नुहोस् +X-KDE-Submenu[nl]=Converteren naar +X-KDE-Submenu[nn]=Konverter til +X-KDE-Submenu[pl]=Konwertuj do +X-KDE-Submenu[pt]=Converter Para +X-KDE-Submenu[pt_BR]=Converter Para +X-KDE-Submenu[ru]=Сохранить как +X-KDE-Submenu[sk]=Konvertovať na +X-KDE-Submenu[sl]=Pretvori v +X-KDE-Submenu[sr]=Претвори у +X-KDE-Submenu[sr@Latn]=Pretvori u +X-KDE-Submenu[sv]=Konvertera till +X-KDE-Submenu[tr]=Dönüştür +X-KDE-Submenu[uk]=Перетворити в +X-KDE-Submenu[vi]=Chuyển đổi sang +X-KDE-Submenu[zh_CN]=转换为 +X-KDE-Submenu[zh_TW]=轉換為 +TryExec=convert + +[Desktop Action convToJPEG] +Name=JPEG +Name[hi]=जेपीईजी +Name[ne]=जेपीईजी +Icon=image +Exec=convert %f "`echo %f | perl -pe 's/\.[^.]+$//'`.jpg" + +[Desktop Action convToPNG] +Name=PNG +Name[hi]=पीएनजी +Name[ne]=पीएनजी +Icon=image +Exec=convert %f "`echo %f | perl -pe 's/\.[^.]+$//'`.png" + +[Desktop Action convToTIF] +Name=TIF +Name[hi]=टिफ़ +Name[ne]=टीआईएफ +Icon=image +Exec=convert %f "`echo %f | perl -pe 's/\.[^.]+$//'`.tif" + +[Desktop Action convToGIF] +Name=GIF +Name[hi]=जिफ़ +Name[ne]=जीआईएफ +Icon=image +Exec=convert %f "`echo %f | perl -pe 's/\.[^.]+$//'`.gif" + diff --git a/konq-plugins/imagerotation/jpegorient b/konq-plugins/imagerotation/jpegorient new file mode 100755 index 0000000..6323289 --- /dev/null +++ b/konq-plugins/imagerotation/jpegorient @@ -0,0 +1,89 @@ +#!/bin/sh + +if test "$#" -lt 2; then + echo "Usage: $0 '##|v|h' jpegs" + exit 2 +fi + +die() { + echo "$@"; exit 1 +} + +notify() { + case "$1" in + /*) url=file:"$1" ;; + *) url=file:"$PWD"/"$1" ;; + esac + + konq=`dcop konqueror-\*` + for k in $konq; do + notify=`dcop $k KDirNotify-\*` + for n in $notify; do + dcop $k $n FilesChanged [ "$url" ] # $1 must be a url + done + done +} + +IFS=: +for path in `kde-config --path data --expandvars`; do + PATH=$path/imagerotation:$PATH +done +export PATH +unset IFS + +action=$1 + +case $action in +[1-8]|+[1-8]) o=$action ;; +90|[+-]90) o=+6 ;; # Use + for all these +270|[+-]270) o=+8 ;; +180|[+-]180) o=+3 ;; +v|-v) o=+4 ;; +h|-h) o=+2 ;; +*) die cannot understand transformation "$action" ;; +esac + +shift + +for file in "$@"; do + +if orient.py $o "$file" | grep 'orientation changed' >/dev/null 2>&1; then + notify "$file" + continue +fi + +### try jpegtran instead +if which jpegtran-mmx >/dev/null 2>&1; then + JPEGTRAN=jpegtran-mmx +else + if which jpegtran >/dev/null 2>&1; then + JPEGTRAN=jpegtran + else + die could not change orientation + fi +fi + +case $action in +v|-v) c='-flip vertical' ;; +h|-h) c='-flip horizontal' ;; +*90) c='-rotate 90' ;; +*180) c='-rotate 180' ;; +*270) c='-rotate 270' ;; ++5) c='-transpose' ;; ++7) c='-transverse' ;; +*) die cannot understand transformation "$action" using jpegtran ;; +esac # others could be emulated, but ... + +tmp="$file.a.$$" + +cleandie() { + mv -i "$tmp" $"2" </dev/null 2>/dev/null; die "$@" +} + +mv -i "$file" "$tmp" </dev/null 2>/dev/null || die unable to move temp files +$JPEGTRAN -copy all -outfile "$file" $c "$tmp" || cleandie error using jpegtran +rm "$tmp" +notify "$file" + +done + diff --git a/konq-plugins/imagerotation/jpegorient.desktop b/konq-plugins/imagerotation/jpegorient.desktop new file mode 100644 index 0000000..3f5b26c --- /dev/null +++ b/konq-plugins/imagerotation/jpegorient.desktop @@ -0,0 +1,307 @@ +[Desktop Entry] +ServiceTypes=image/jpeg +Actions=jpegRot90;jpegRot270;jpegFlipV;jpegFlipH; +X-KDE-Submenu=Transform Image +X-KDE-Submenu[bg]=Конвертиране на изображението +X-KDE-Submenu[ca]=Transforma la imatge +X-KDE-Submenu[cs]=Převést obrázek +X-KDE-Submenu[da]=Transformér billede +X-KDE-Submenu[de]=Bild transformieren +X-KDE-Submenu[el]=Μετασχηματισμός εικόνας +X-KDE-Submenu[eo]=Transformu bildon +X-KDE-Submenu[es]=Transformar imagen +X-KDE-Submenu[et]=Pildi muutmine +X-KDE-Submenu[eu]=Irudia eraldatu +X-KDE-Submenu[fa]=تبدیل تصویر +X-KDE-Submenu[fi]=Muuta kuva +X-KDE-Submenu[fr]=Transformer l'image +X-KDE-Submenu[fy]=Ofbyld transformearje +X-KDE-Submenu[ga]=Trasfhoirmigh Íomhá +X-KDE-Submenu[gl]=Transformar a Imaxe +X-KDE-Submenu[hr]=Pretvaranje slike +X-KDE-Submenu[hu]=Képátalakítás +X-KDE-Submenu[is]=Ummynda +X-KDE-Submenu[it]=Trasforma immagine +X-KDE-Submenu[ja]=画像を変換 +X-KDE-Submenu[ka]=გამოსახულებათა ტრანსფორმაცია +X-KDE-Submenu[kk]=Кескінді түрлендіру +X-KDE-Submenu[km]=ប្លែងរូបភាព +X-KDE-Submenu[lt]=Transformuoti paveikslėlį +X-KDE-Submenu[mk]=Трансформирај слика +X-KDE-Submenu[nb]=Transformer bilde +X-KDE-Submenu[nds]=Bild ännern +X-KDE-Submenu[ne]=छवि रुपान्तरण गर्नुहोस् +X-KDE-Submenu[nl]=Afbeelding transformeren +X-KDE-Submenu[nn]=Forvandla bilete +X-KDE-Submenu[pl]=Przekształć obrazek +X-KDE-Submenu[pt]=Transformar a Imagem +X-KDE-Submenu[pt_BR]=Transformar Imagem +X-KDE-Submenu[ru]=Преобразовать +X-KDE-Submenu[sk]=Transformovať obrázok +X-KDE-Submenu[sl]=Preoblikuj sliko +X-KDE-Submenu[sr]=Трансформиши слику +X-KDE-Submenu[sr@Latn]=Transformiši sliku +X-KDE-Submenu[sv]=Ändra bild +X-KDE-Submenu[tr]=Resmi Döndür +X-KDE-Submenu[uk]=Перетворити зображення +X-KDE-Submenu[vi]=Chuyển đổi ảnh +X-KDE-Submenu[zh_CN]=图像变形 +X-KDE-Submenu[zh_TW]=轉換影像 +X-KDE-Require=Write + +[Desktop Action jpegRot90] +Name=Rotate Clockwise +Name[ar]=تدوير في اتجاه الساعة +Name[az]=Saat Əqrəbi İstiqamətində Fırlat +Name[bg]=Завъртане по посока на часов. стрелка +Name[bs]=Rotiraj u smjeru kazaljke +Name[ca]=Gira 90 graus +Name[cs]=Rotovat ve směru hod. ručiček +Name[cy]=Cylchdroi yn Glocwedd +Name[da]=Rotér med uret +Name[de]=Im Uhrzeigersinn drehen (nach rechts) +Name[el]=Περιστροφή δεξιόστροφα +Name[eo]=Turnu laŭhorloĝe +Name[es]=Girar en el sentido de las agujas del reloj +Name[et]=Pööra päripäeva +Name[eu]=Biratu erloju orratzen norabidean +Name[fa]=چرخش در جهت ساعت +Name[fi]=Käännä myötäpäivään +Name[fr]=Tourner en sens horaire +Name[fy]=Mei de wizers fan'e klok mei draaie +Name[ga]=Rothlaigh go Deisealach +Name[gl]=Xirar en Sentido Horário +Name[he]=סובב עם כיוון השעון +Name[hi]=घड़ी की दिशा में घुमाएँ +Name[hr]=Okret udesno +Name[hu]=Forgatás jobbra +Name[is]=Snúa réttsælis +Name[it]=Ruota in senso orario +Name[ja]=右回転 +Name[ka]=საათის ისრის მიმართულებით მოტრიალება +Name[kk]=Сағаттың тіліне сәйкес айналдыру +Name[km]=បង្វិលស្របទ្រនិចនាឡិកា +Name[lt]=Sukti pagal laikrodžio rodyklę +Name[mk]=Ротирај кон стрелката на часовникот +Name[ms]=Putar Ikut Jam +Name[nb]=Roter medurs +Name[nds]=Na rechts dreihen +Name[ne]=घडिको दिशामा घुमाउनुहोस् +Name[nl]=Klokgewijs draaien +Name[nn]=Roter med klokka +Name[pa]=ਸੱਜਾ ਦਾਅ ਘੁੰਮਾਉ +Name[pl]=Obróć zgodnie z ruchem wskazówek zegara +Name[pt]=Rodar no Sentido Horário +Name[pt_BR]=Rodar Relógio Sentido Horário +Name[ro]=Roteşte la dreapta +Name[ru]=Повернуть по часовой стрелке +Name[sk]=Otočiť v smere hodinových ručičiek +Name[sl]=Zasuči v smeri urinega kazalca +Name[sr]=Ротирај у смеру казаљке на часовнику +Name[sr@Latn]=Rotiraj u smeru kazaljke na časovniku +Name[sv]=Rotera medurs +Name[ta]=வலதுபுறமாக சுழற்று +Name[tg]=Чархиш бо ақрабаки соат +Name[tr]=Saat Yönünde Döndür +Name[uk]=Обернути за годинниковою стрілкою +Name[uz]=Soat koʻrsatgichi boʻyicha burish +Name[uz@cyrillic]=Соат кўрсатгичи бўйича буриш +Name[vi]=Xoay xuôi chiều +Name[zh_CN]=顺时针旋转 +Name[zh_TW]=順時針旋轉 +Icon=rotate_cw +Exec=jpegorient +90 %F + +[Desktop Action jpegRot270] +Name=Rotate Counter-Clockwise +Name[ar]=تدوير عكس اتجاه الساعة +Name[az]=Saat Əqrəbinə Tərs İstiqamətdə Fırlat +Name[bg]=Завъртане обратно на часов. стрелка +Name[bs]=Rotiraj u smjeru suprotnom od kazaljke +Name[ca]=Gira 270 graus +Name[cs]=Rotovat proti směru hod. ručiček +Name[cy]=Cylchdroi yn Wrthglocwedd +Name[da]=Rotér mod uret +Name[de]=Gegen den Uhrzeigersinn drehen (nach links) +Name[el]=Περιστροφή αριστερόστροφα +Name[en_GB]=Rotate Anti-Clockwise +Name[eo]=Turnu kontraŭhorloĝe +Name[es]=Girar en el sentido contrario a las agujas del reloj +Name[et]=Pööra vastupäeva +Name[eu]=Biratu erloju orratzen aurkako norabidean +Name[fa]=چرخش در خلاف جهت ساعت +Name[fi]=Käännä vastapäivään +Name[fr]=Tourner en sens anti-horaire +Name[fy]=Tsjin'e wizers fan de klok yn draaie +Name[ga]=Rothlaigh go Tuathalach +Name[gl]=Xirar en Sentido Antiorário +Name[he]=סובב נגד כיוון השעון +Name[hi]=घड़ी की उलटी दिशा में घुमाएँ +Name[hr]=Okret ulijevo +Name[hu]=Forgatás balra +Name[is]=Snúa rangsælis +Name[it]=Ruota in senso anti-orario +Name[ja]=左回転 +Name[ka]=საათის ისრის მიმართულებით მოტრიალება +Name[kk]=Сағаттың тіліне қарсы айналдыру +Name[km]=បង្វិលច្រាសទ្រនិចនាឡិកា +Name[lt]=Sukti prieš laikrodžio rodyklę +Name[mk]=Ротирај обратно од стрелката на часовникот +Name[ms]=Putar Lawan Jam +Name[nb]=Roter moturs +Name[nds]=Na links dreihen +Name[ne]=घडिको विपरित दिशामा घुमाउनुहोस् +Name[nl]=Anti-klokgewijs draaien +Name[nn]=Roter mot klokka +Name[pa]=ਖੱਬੇ ਦਾਅ ਘੁੰਮਾਓ +Name[pl]=Obróć przeciwnie do ruchu wskazówek zegara +Name[pt]=Rodar no Sentido Anti-Horário +Name[pt_BR]=Rodar Contador de Relógio +Name[ro]=Roteşte la stînga +Name[ru]=Повернуть против часовой стрелки +Name[sk]=Otočiť proti smeru hodinových ručičiek +Name[sl]=Zasuči v nasportni smeri urinega kazalca +Name[sr]=Ротирај супротно смеру казаљке на часовнику +Name[sr@Latn]=Rotiraj suprotno smeru kazaljke na časovniku +Name[sv]=Rotera moturs +Name[ta]=கடிகார ஓட்டத்திற்கெதிராக சுழற்று +Name[tg]=Чархиш ба муқобили ақрабаки соат +Name[tr]=Saat Yönünün Tesine Döndür +Name[uk]=Обернути проти годинникової стрілки +Name[uz]=Soat koʻrsatgichiga qarshi burish +Name[uz@cyrillic]=Соат кўрсатгичига қарши буриш +Name[vi]=Xoay ngược chiều +Name[zh_CN]=逆时针旋转 +Name[zh_TW]=逆時針旋轉 +Icon=rotate_ccw +Exec=jpegorient +270 %F + +#[Desktop Action jpegRot180] +#Name=Rotate 180 +#Icon=misc +#Exec=jpegorient +180 %F + +[Desktop Action jpegFlipV] +Name=Flip Vertically +Name[ar]=تدوير رأسي +Name[az]=Şaquli Olaraq Çevir +Name[bg]=Вертикално обръщане +Name[bs]=Obrni vertikalno +Name[ca]=Torna vertical +Name[cs]=Převrátit svisle +Name[cy]=Troi Drosodd yn Fertigol +Name[da]=Flip lodret +Name[de]=Vertikal kippen +Name[el]=Αναστροφή κατακόρυφα +Name[eo]=Inversigu vertikale +Name[es]=Reflejar verticalmente +Name[et]=Keera ümber vertikaalselt +Name[eu]=Buruz behera ipini +Name[fa]=قرینۀ عمودی +Name[fi]=Käännä pystysuorassa +Name[fr]=Retourner verticalement +Name[fy]=Fertikaal omdraaie +Name[ga]=Smeach go hIngearach +Name[gl]=Inverter Verticalmente +Name[he]=שקף אנכית +Name[hi]=खड़ा पलटें +Name[hr]=Prevrni uspravno +Name[hu]=Tükrözés függőlegesen +Name[is]=Snúa við um miðju lóðrétt +Name[it]=Fletti verticalmente +Name[ja]=上下反転 +Name[ka]=ვერტიკალურად შეტრიალება +Name[kk]=Тігінен терістеу +Name[km]=ត្រឡប់បញ្ឈរ +Name[lt]=Versti vertikaliai +Name[mk]=Преврти вертикално +Name[nb]=Speilvend loddrett +Name[nds]=Pielliek ümdreihen +Name[ne]=ठाडो गरी फ्लिप गर्नुहोस् +Name[nl]=Verticaal omdraaien +Name[nn]=Snu loddrett +Name[pa]=ਲੰਬਕਾਰੀ ਝਟਕੋ +Name[pl]=Odwróć pionowo +Name[pt]=Inverter Verticalmente +Name[pt_BR]=Refletir Verticalmente +Name[ro]=Întoarce pe verticală +Name[ru]=Отразить вертикально +Name[sk]=Preklopiť vertikálne +Name[sl]=Obrni navpično +Name[sr]=Преврни усправно +Name[sr@Latn]=Prevrni uspravno +Name[sv]=Vänd vertikalt +Name[ta]=மேல்கீழாக திருப்பு +Name[tg]=Баргардондан ба амудӣ +Name[tr]=Dik Olarak Dağıt +Name[uk]=Перекинути вертикально +Name[uz]=Eni boʻyicha burish +Name[uz@cyrillic]=Эни бўйича буриш +Name[vi]=Lật dọc +Name[zh_CN]=上下翻转 +Name[zh_TW]=垂直翻轉 +Icon=2uparrow +Exec=jpegorient v %F + +[Desktop Action jpegFlipH] +Name=Flip Horizontally +Name[ar]=تدوير أفقي +Name[az]=Üfüqi Olaraq Çevir +Name[bg]=Хоризонтално обръщане +Name[bs]=Obrni horizontalno +Name[ca]=Torna horitzontal +Name[cs]=Převrátit vodorovně +Name[cy]=Troi Drosodd yn Llorweddol +Name[da]=Flip vandret +Name[de]=Horizontal kippen +Name[el]=Αναστροφή οριζόντια +Name[eo]=Inversigu horizontale +Name[es]=Reflejar horizontalmente +Name[et]=Keera ümber horisontaalselt +Name[eu]=Biratu horizontalki +Name[fa]=قرینۀ افقی +Name[fi]=Käännä vaakasuorassa +Name[fr]=Retourner horizontalement +Name[fy]=Horizontaal omdraaie +Name[ga]=Smeach go Cothrománach +Name[gl]=Inverter Horizontalmente +Name[he]=שקף אופקית +Name[hi]=आड़ा पलटें +Name[hr]=Prevrni vodoravno +Name[hu]=Tükrözés vízszintesen +Name[is]=Snúa við um miðju lárétt +Name[it]=Fletti orizzontalmente +Name[ja]=左右反転 +Name[ka]=ჰორიზონტალურად შეტრიალება +Name[kk]=Төңкеру +Name[km]=ត្រឡប់ផ្ដេក +Name[lt]=Versti horizontaliai +Name[mk]=Преврти хоризонтално +Name[ms]=Balikkan Melintang +Name[nb]=Speilvend vannrett +Name[nds]=Waagrecht ümdreihen +Name[ne]=तेर्सो गरी फ्लिप गर्नुहोस् +Name[nl]=Horizontaal omdraaien +Name[nn]=Snu vassrett +Name[pa]=ਖਿਤਿਜੀ ਝਟਕੋ +Name[pl]=Odwróć poziomo +Name[pt]=Inverter Horizontalmente +Name[pt_BR]=Refletir Horizontalmente +Name[ro]=Întoarce pe orizontală +Name[ru]=Отразить горизонтально +Name[sk]=Preklopiť horizontálne +Name[sl]=Obrni vodoravno +Name[sr]=Преврни водоравно +Name[sr@Latn]=Prevrni vodoravno +Name[sv]=Vänd horisontellt +Name[ta]=இடம்வலமாக திருப்புக +Name[tg]=Баргардондан ба уфуқӣ +Name[tr]=Yatay Olarak Dağıt +Name[uk]=Перекинути горизонтально +Name[uz]=Boʻyi boʻyicha burish +Name[uz@cyrillic]=Бўйи бўйича буриш +Name[vi]=Lật ngang +Name[zh_CN]=左右翻转 +Name[zh_TW]=水平翻轉 +Icon=2rightarrow +Exec=jpegorient h %F diff --git a/konq-plugins/imagerotation/orient.py b/konq-plugins/imagerotation/orient.py new file mode 100755 index 0000000..0671876 --- /dev/null +++ b/konq-plugins/imagerotation/orient.py @@ -0,0 +1,104 @@ +#! /usr/bin/env python +import os +import sys +import exif + +def compose(delta, old): + map=[0, 4, 2, 6, 5, 1, 7, 3] + unmap=[1, 6, 3, 8, 2, 5, 4, 7] + x = map[delta-1] + y = map[old-1] + z = ((x^y)&4) + ((y+(x&3)*(((y&4)>>1)+1))&3) + return unmap[z] + +def deg2o(d): + map={90:6, 270:8, 180:3} + if map.has_key(d): + return map[d] + else: + return 0 + +if len(sys.argv) < 2: + print 'Usage: %s [[+]orientnum] file\n' % sys.argv[0] + sys.exit(1) +try: + if len(sys.argv) == 2: + filename=sys.argv[1] + file=open(filename, "r"); + else: + filename=sys.argv[2] + mod=sys.argv[1] + fd = os.open(filename, os.O_RDWR) + file=os.fdopen(fd,'r') + # check file exists and is readable + file.read(1) + file.seek(0,0) +except: + print 'Cannot open', filename + sys.exit(1) + +tags=exif.process_file(file,0,1) +if not tags: + print 'no EXIF information in', filename + sys.exit(1) +if not tags.has_key('Exif Offset') \ + or not tags.has_key('Image Orientation'): + print 'cannot get orientation info in', filename + sys.exit(1) + +exifp = tags['Exif Offset'] +endian = tags['Exif Endian'] +tagp = tags['Image Orientation'].field_offset + +orientp = exifp + tagp + +if endian == 'M': # MM byte order + orientp += 1 + +file.seek(orientp) +o = ord(file.read(1)) + +if o < 1 or o > 8: + print 'orientation out of range', o + sys.exit(1) + +if len(sys.argv) == 2: + print 'orientation is', o + sys.exit(0) + +try: + if mod[0] == '+': + deltao = int(mod) + if 1 <= deltao and deltao <= 8: + newo = compose(deltao, o) + elif deg2o(deltao) != 0: + newo = compose(deg2o(deltao), o) + else: + print 'cannot understand orientation modification', mod + sys.exit(1) # it will still hit the except ... how to fix? + else: + newo = int(mod) +except: + print 'expected numeric orientation and got',mod + sys.exit(1) + +if newo < 1 or newo > 8: + newo = deg2o(newo) + if newo == 0: + print 'cannot understand orientation', deltao + sys.exit(1) + +os.lseek(fd,orientp,0) +os.write(fd,chr(newo)) + +# Thumbnail orientation : +thumb_ifdp = 0 +if tags.has_key('Thumbnail Orientation'): + thumb_tagp = tags['Thumbnail Orientation'].field_offset + thumb_orientp = exifp + thumb_tagp + if endian == 'M': # MM byte order + thumb_orientp += 1 + os.lseek(fd,thumb_orientp,0) + os.write(fd,chr(newo)) + +print 'orientation changed from', o, 'to', newo |