diff options
Diffstat (limited to 'src/photos')
-rw-r--r-- | src/photos/BmpSupport.vala | 187 | ||||
-rw-r--r-- | src/photos/GRaw.vala | 307 | ||||
-rw-r--r-- | src/photos/GdkSupport.vala | 132 | ||||
-rw-r--r-- | src/photos/JfifSupport.vala | 239 | ||||
-rw-r--r-- | src/photos/PhotoFileAdapter.vala | 112 | ||||
-rw-r--r-- | src/photos/PhotoFileFormat.vala | 410 | ||||
-rw-r--r-- | src/photos/PhotoFileSniffer.vala | 104 | ||||
-rw-r--r-- | src/photos/PhotoMetadata.vala | 1169 | ||||
-rw-r--r-- | src/photos/Photos.vala | 31 | ||||
-rw-r--r-- | src/photos/PngSupport.vala | 184 | ||||
-rw-r--r-- | src/photos/RawSupport.vala | 350 | ||||
-rw-r--r-- | src/photos/TiffSupport.vala | 183 | ||||
-rw-r--r-- | src/photos/mk/photos.mk | 38 |
13 files changed, 3446 insertions, 0 deletions
diff --git a/src/photos/BmpSupport.vala b/src/photos/BmpSupport.vala new file mode 100644 index 0000000..dbeb64c --- /dev/null +++ b/src/photos/BmpSupport.vala @@ -0,0 +1,187 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Photos { + +class BmpFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { "bmp", "dib" }; + private static string[] KNOWN_MIME_TYPES = { GPhoto.MIME.BMP }; + + private static BmpFileFormatProperties instance = null; + + public static void init() { + instance = new BmpFileFormatProperties(); + } + + public static BmpFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.BMP; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_user_visible_name() { + return _("BMP"); + } + + public override string get_default_extension() { + return KNOWN_EXTENSIONS[0]; + } + + public override string[] get_known_extensions() { + return KNOWN_EXTENSIONS; + } + + public override string get_default_mime_type() { + return KNOWN_MIME_TYPES[0]; + } + + public override string[] get_mime_types() { + return KNOWN_MIME_TYPES; + } +} + +public class BmpSniffer : GdkSniffer { + private const uint8[] MAGIC_SEQUENCE = { 0x42, 0x4D }; + + public BmpSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + private static bool is_bmp_file(File file) throws Error { + FileInputStream instream = file.read(null); + + uint8[] file_lead_sequence = new uint8[MAGIC_SEQUENCE.length]; + + instream.read(file_lead_sequence, null); + + for (int i = 0; i < MAGIC_SEQUENCE.length; i++) { + if (file_lead_sequence[i] != MAGIC_SEQUENCE[i]) + return false; + } + + return true; + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // Rely on GdkSniffer to detect corruption + is_corrupted = false; + + if (!is_bmp_file(file)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.BMP) ? detected : null; + } +} + +public class BmpReader : GdkReader { + public BmpReader(string filepath) { + base (filepath, PhotoFileFormat.BMP); + } + + public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error { + Gdk.Pixbuf result = null; + /* if we encounter a situation where there are two orders of magnitude or more of + difference between the full image size and the scaled size, and if the full image + size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can + fail due to what appear to be floating-point round-off issues. This isn't surprising, + since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In + this case, we prefetch the image at a larger scale and then downsample it to the + desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy + scaling code. */ + if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) || + (scaled.height < 100))) { + Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000, + ScaleConstraint.DIMENSIONS); + + result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width, + prefetch_dimensions.height, false); + + result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER); + } else { + result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width, + scaled.height, false); + } + + return result; + } +} + +public class BmpWriter : PhotoFileWriter { + public BmpWriter(string filepath) { + base (filepath, PhotoFileFormat.BMP); + } + + public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error { + pixbuf.save(get_filepath(), "bmp", null); + } +} + +public class BmpMetadataWriter : PhotoFileMetadataWriter { + public BmpMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.BMP); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + // Metadata writing isn't supported for .BMPs, so this is a no-op. + } +} + +public class BmpFileFormatDriver : PhotoFileFormatDriver { + private static BmpFileFormatDriver instance = null; + + public static void init() { + instance = new BmpFileFormatDriver(); + BmpFileFormatProperties.init(); + } + + public static BmpFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return BmpFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new BmpReader(filepath); + } + + public override bool can_write_image() { + return true; + } + + public override bool can_write_metadata() { + return false; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return new BmpWriter(filepath); + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new BmpMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new BmpSniffer(file, options); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } +} + +} diff --git a/src/photos/GRaw.vala b/src/photos/GRaw.vala new file mode 100644 index 0000000..915a861 --- /dev/null +++ b/src/photos/GRaw.vala @@ -0,0 +1,307 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace GRaw { + +public const double HD_POWER = 2.222; +public const double HD_SLOPE = 4.5; + +public const double SRGB_POWER = 2.4; +public const double SRGB_SLOPE = 12.92; + +public enum Colorspace { + RAW = 0, + SRGB = 1, + ADOBE = 2, + WIDE = 3, + PROPHOTO = 4, + XYZ = 5 +} + +public errordomain Exception { + UNSPECIFIED, + UNSUPPORTED_FILE, + NONEXISTANT_IMAGE, + OUT_OF_ORDER_CALL, + NO_THUMBNAIL, + UNSUPPORTED_THUMBNAIL, + OUT_OF_MEMORY, + DATA_ERROR, + IO_ERROR, + CANCELLED_BY_CALLBACK, + BAD_CROP, + SYSTEM_ERROR +} + +public enum Flip { + FROM_SOURCE = -1, + NONE = 0, + UPSIDE_DOWN = 3, + COUNTERCLOCKWISE = 5, + CLOCKWISE = 6 +} + +public enum FujiRotate { + USE = -1, + DONT_USE = 0 +} + +public enum HighlightMode { + CLIP = 0, + UNCLIP = 1, + BLEND = 2, + REBUILD = 3 +} + +public enum InterpolationQuality { + LINEAR = 0, + VNG = 1, + PPG = 2, + AHD = 3 +} + +public class ProcessedImage { + private LibRaw.ProcessedImage image; + private Gdk.Pixbuf pixbuf = null; + + public ushort width { + get { + return image.width; + } + } + + public ushort height { + get { + return image.height; + } + } + + public ushort colors { + get { + return image.colors; + } + } + + public ushort bits { + get { + return image.bits; + } + } + + public uint8* data { + get { + return image.data; + } + } + + public uint data_size { + get { + return image.data_size; + } + } + + public ProcessedImage(LibRaw.Processor proc) throws Exception { + LibRaw.Result result = LibRaw.Result.SUCCESS; + image = proc.make_mem_image(ref result); + throw_exception("ProcessedImage", result); + assert(image != null); + + // A regular mem image comes back with raw RGB data ready for pixbuf (data buffer is shared + // between the ProcessedImage and the Gdk.Pixbuf) + pixbuf = new Gdk.Pixbuf.with_unowned_data(image.data, Gdk.Colorspace.RGB, false, image.bits, + image.width, image.height, image.width * image.colors, null); + } + + public ProcessedImage.from_thumb(LibRaw.Processor proc) throws Exception { + LibRaw.Result result = LibRaw.Result.SUCCESS; + image = proc.make_mem_thumb(ref result); + throw_exception("ProcessedImage.from_thumb", result); + assert(image != null); + + // A mem thumb comes back as the raw bytes from the data segment in the file -- this needs + // to be decoded before being useful. This will throw an error if the format is not + // supported + try { + pixbuf = new Gdk.Pixbuf.from_stream(new MemoryInputStream.from_data(image.data, null), + null); + } catch (Error err) { + throw new Exception.UNSUPPORTED_THUMBNAIL(err.message); + } + + // fix up the ProcessedImage fields (which are unset when decoding the thumb) + image.width = (ushort) pixbuf.width; + image.height = (ushort) pixbuf.height; + image.colors = (ushort) pixbuf.n_channels; + image.bits = (ushort) pixbuf.bits_per_sample; + } + + // This method returns a copy of a pixbuf representing the ProcessedImage. + public Gdk.Pixbuf get_pixbuf_copy() { + return pixbuf.copy(); + } +} + +public class Processor { + public LibRaw.OutputParams* output_params { + get { + return &proc.params; + } + } + + private LibRaw.Processor proc; + + public Processor(LibRaw.Options options = LibRaw.Options.NONE) { + proc = new LibRaw.Processor(options); + } + + public void adjust_sizes_info_only() throws Exception { + throw_exception("adjust_sizes_info_only", proc.adjust_sizes_info_only()); + } + + public unowned LibRaw.ImageOther get_image_other() { + return proc.get_image_other(); + } + + public unowned LibRaw.ImageParams get_image_params() { + return proc.get_image_params(); + } + + public unowned LibRaw.ImageSizes get_sizes() { + return proc.get_sizes(); + } + + public unowned LibRaw.Thumbnail get_thumbnail() { + return proc.get_thumbnail(); + } + + public ProcessedImage make_mem_image() throws Exception { + return new ProcessedImage(proc); + } + + public ProcessedImage make_thumb_image() throws Exception { + return new ProcessedImage.from_thumb(proc); + } + + public void open_buffer(uint8[] buffer) throws Exception { + throw_exception("open_buffer", proc.open_buffer(buffer)); + } + + public void open_file(string filename) throws Exception { + throw_exception("open_file", proc.open_file(filename)); + } + + public void process() throws Exception { + throw_exception("process", proc.process()); + } + + public void ppm_tiff_writer(string filename) throws Exception { + throw_exception("ppm_tiff_writer", proc.ppm_tiff_writer(filename)); + } + + public void thumb_writer(string filename) throws Exception { + throw_exception("thumb_writer", proc.thumb_writer(filename)); + } + + public void recycle() { + proc.recycle(); + } + + public void unpack() throws Exception { + throw_exception("unpack", proc.unpack()); + } + + public void unpack_thumb() throws Exception { + throw_exception("unpack_thumb", proc.unpack_thumb()); + } + + // This configures output_params for reasonable settings for turning a RAW image into an + // RGB ProcessedImage suitable for display. Tweaks can occur after this call and before + // process(). + public void configure_for_rgb_display(bool half_size) { + // Fields in comments are left to their defaults and/or should be modified by the caller. + // These fields are set to reasonable defaults by libraw. + + // greybox + output_params->set_chromatic_aberrations(1.0, 1.0); + output_params->set_gamma_curve(GRaw.SRGB_POWER, GRaw.SRGB_SLOPE); + // user_mul + // shot_select + // multi_out + output_params->bright = 1.0f; + // threshold + output_params->half_size = half_size; + // four_color_rgb + output_params->highlight = GRaw.HighlightMode.CLIP; + output_params->use_auto_wb = true; + output_params->use_camera_wb = true; + output_params->use_camera_matrix = true; + output_params->output_color = GRaw.Colorspace.SRGB; + // output_profile + // camera_profile + // bad_pixels + // dark_frame + output_params->output_bps = 8; + // output_tiff + output_params->user_flip = GRaw.Flip.FROM_SOURCE; + output_params->user_qual = GRaw.InterpolationQuality.PPG; + // user_black + // user_sat + // med_passes + output_params->no_auto_bright = true; + output_params->auto_bright_thr = 0.01f; + output_params->use_fuji_rotate = GRaw.FujiRotate.USE; + } +} + +private void throw_exception(string caller, LibRaw.Result result) throws Exception { + if (result == LibRaw.Result.SUCCESS) + return; + else if (result > 0) + throw new Exception.SYSTEM_ERROR("%s: System error %d: %s", caller, (int) result, strerror(result)); + + string msg = "%s: %s".printf(caller, result.to_string()); + + switch (result) { + case LibRaw.Result.UNSPECIFIED_ERROR: + throw new Exception.UNSPECIFIED(msg); + + case LibRaw.Result.FILE_UNSUPPORTED: + throw new Exception.UNSUPPORTED_FILE(msg); + + case LibRaw.Result.REQUEST_FOR_NONEXISTENT_IMAGE: + throw new Exception.NONEXISTANT_IMAGE(msg); + + case LibRaw.Result.OUT_OF_ORDER_CALL: + throw new Exception.OUT_OF_ORDER_CALL(msg); + + case LibRaw.Result.NO_THUMBNAIL: + throw new Exception.NO_THUMBNAIL(msg); + + case LibRaw.Result.UNSUPPORTED_THUMBNAIL: + throw new Exception.UNSUPPORTED_THUMBNAIL(msg); + + case LibRaw.Result.UNSUFFICIENT_MEMORY: + throw new Exception.OUT_OF_MEMORY(msg); + + case LibRaw.Result.DATA_ERROR: + throw new Exception.DATA_ERROR(msg); + + case LibRaw.Result.IO_ERROR: + throw new Exception.IO_ERROR(msg); + + case LibRaw.Result.CANCELLED_BY_CALLBACK: + throw new Exception.CANCELLED_BY_CALLBACK(msg); + + case LibRaw.Result.BAD_CROP: + throw new Exception.BAD_CROP(msg); + + default: + return; + } +} + +} + diff --git a/src/photos/GdkSupport.vala b/src/photos/GdkSupport.vala new file mode 100644 index 0000000..ed2ff63 --- /dev/null +++ b/src/photos/GdkSupport.vala @@ -0,0 +1,132 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public abstract class GdkReader : PhotoFileReader { + public GdkReader(string filepath, PhotoFileFormat file_format) { + base (filepath, file_format); + } + + public override PhotoMetadata read_metadata() throws Error { + PhotoMetadata metadata = new PhotoMetadata(); + metadata.read_from_file(get_file()); + + return metadata; + } + + public override Gdk.Pixbuf unscaled_read() throws Error { + return new Gdk.Pixbuf.from_file(get_filepath()); + } + + public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error { + return new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width, scaled.height, false); + } +} + +public abstract class GdkSniffer : PhotoFileSniffer { + private DetectedPhotoInformation detected = null; + private bool size_ready = false; + private bool area_prepared = false; + + public GdkSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + detected = new DetectedPhotoInformation(); + + Gdk.PixbufLoader pixbuf_loader = new Gdk.PixbufLoader(); + pixbuf_loader.size_prepared.connect(on_size_prepared); + pixbuf_loader.area_prepared.connect(on_area_prepared); + + // valac chokes on the ternary operator here + Checksum? md5_checksum = null; + if (calc_md5) + md5_checksum = new Checksum(ChecksumType.MD5); + + detected.metadata = new PhotoMetadata(); + try { + detected.metadata.read_from_file(file); + } catch (Error err) { + // no metadata detected + detected.metadata = null; + } + + if (calc_md5 && detected.metadata != null) { + uint8[]? flattened_sans_thumbnail = detected.metadata.flatten_exif(false); + if (flattened_sans_thumbnail != null && flattened_sans_thumbnail.length > 0) + detected.exif_md5 = md5_binary(flattened_sans_thumbnail, flattened_sans_thumbnail.length); + + uint8[]? flattened_thumbnail = detected.metadata.flatten_exif_preview(); + if (flattened_thumbnail != null && flattened_thumbnail.length > 0) + detected.thumbnail_md5 = md5_binary(flattened_thumbnail, flattened_thumbnail.length); + } + + // if no MD5, don't read as much, as the needed info will probably be gleaned + // in the first 8K to 16K + uint8[] buffer = calc_md5 ? new uint8[64 * 1024] : new uint8[8 * 1024]; + size_t count = 0; + + // loop through until all conditions we're searching for are met + FileInputStream fins = file.read(null); + for (;;) { + size_t bytes_read = fins.read(buffer, null); + if (bytes_read <= 0) + break; + + count += bytes_read; + + if (calc_md5) + md5_checksum.update(buffer, bytes_read); + + // keep parsing the image until the size is discovered + if (!size_ready || !area_prepared) + pixbuf_loader.write(buffer[0:bytes_read]); + + // if not searching for anything else, exit + if (!calc_md5 && size_ready && area_prepared) + break; + } + + // PixbufLoader throws an error if you close it with an incomplete image, so trap this + try { + pixbuf_loader.close(); + } catch (Error err) { + } + + if (fins != null) + fins.close(null); + + if (calc_md5) + detected.md5 = md5_checksum.get_string(); + + // if size and area are not ready, treat as corrupted file (entire file was read) + is_corrupted = !size_ready || !area_prepared; + + return detected; + } + + private void on_size_prepared(Gdk.PixbufLoader loader, int width, int height) { + detected.image_dim = Dimensions(width, height); + size_ready = true; + } + + private void on_area_prepared(Gdk.PixbufLoader pixbuf_loader) { + Gdk.Pixbuf? pixbuf = pixbuf_loader.get_pixbuf(); + if (pixbuf == null) + return; + + detected.colorspace = pixbuf.get_colorspace(); + detected.channels = pixbuf.get_n_channels(); + detected.bits_per_channel = pixbuf.get_bits_per_sample(); + + unowned Gdk.PixbufFormat format = pixbuf_loader.get_format(); + detected.format_name = format.get_name(); + detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name); + + area_prepared = true; + } +} + diff --git a/src/photos/JfifSupport.vala b/src/photos/JfifSupport.vala new file mode 100644 index 0000000..d721441 --- /dev/null +++ b/src/photos/JfifSupport.vala @@ -0,0 +1,239 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public class JfifFileFormatDriver : PhotoFileFormatDriver { + private static JfifFileFormatDriver instance = null; + + public static void init() { + instance = new JfifFileFormatDriver(); + JfifFileFormatProperties.init(); + } + + public static JfifFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return JfifFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new JfifReader(filepath); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } + + public override bool can_write_image() { + return true; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return new JfifWriter(filepath); + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new JfifMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new JfifSniffer(file, options); + } +} + +public class JfifFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { + "jpg", "jpeg", "jpe" + }; + + private static string[] KNOWN_MIME_TYPES = { + "image/jpeg" + }; + + private static JfifFileFormatProperties instance = null; + + public static void init() { + instance = new JfifFileFormatProperties(); + } + + public static JfifFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.JFIF; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_default_extension() { + return "jpg"; + } + + public override string get_user_visible_name() { + return _("JPEG"); + } + + public override string[] get_known_extensions() { + return KNOWN_EXTENSIONS; + } + + public override string get_default_mime_type() { + return KNOWN_MIME_TYPES[0]; + } + + public override string[] get_mime_types() { + return KNOWN_MIME_TYPES; + } +} + +public class JfifSniffer : GdkSniffer { + public JfifSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // Rely on GdkSniffer to detect corruption + is_corrupted = false; + + if (!Jpeg.is_jpeg(file)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null; + } +} + +public class JfifReader : GdkReader { + public JfifReader(string filepath) { + base (filepath, PhotoFileFormat.JFIF); + } +} + +public class JfifWriter : PhotoFileWriter { + public JfifWriter(string filepath) { + base (filepath, PhotoFileFormat.JFIF); + } + + public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error { + pixbuf.save(get_filepath(), "jpeg", "quality", quality.get_pct_text()); + } +} + +public class JfifMetadataWriter : PhotoFileMetadataWriter { + public JfifMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.JFIF); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +namespace Jpeg { + public const uint8 MARKER_PREFIX = 0xFF; + + public enum Marker { + // Could also be 0xFF according to spec + INVALID = 0x00, + + SOI = 0xD8, + EOI = 0xD9, + + APP0 = 0xE0, + APP1 = 0xE1; + + public uint8 get_byte() { + return (uint8) this; + } + } + + public enum Quality { + LOW = 50, + MEDIUM = 75, + HIGH = 90, + MAXIMUM = 100; + + public int get_pct() { + return (int) this; + } + + public string get_pct_text() { + return "%d".printf((int) this); + } + + public static Quality[] get_all() { + return { LOW, MEDIUM, HIGH, MAXIMUM }; + } + + public string? to_string() { + switch (this) { + case LOW: + return _("Low (%d%%)").printf((int) this); + + case MEDIUM: + return _("Medium (%d%%)").printf((int) this); + + case HIGH: + return _("High (%d%%)").printf((int) this); + + case MAXIMUM: + return _("Maximum (%d%%)").printf((int) this); + } + + warn_if_reached(); + + return null; + } + } + + public bool is_jpeg(File file) throws Error { + FileInputStream fins = file.read(null); + + Marker marker; + int segment_length = read_marker(fins, out marker); + + // for now, merely checking for SOI + return (marker == Marker.SOI) && (segment_length == 0); + } + + private int read_marker(FileInputStream fins, out Jpeg.Marker marker) throws Error { + marker = Jpeg.Marker.INVALID; + + DataInputStream dins = new DataInputStream(fins); + dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + + if (dins.read_byte() != Jpeg.MARKER_PREFIX) + return -1; + + marker = (Jpeg.Marker) dins.read_byte(); + if ((marker == Jpeg.Marker.SOI) || (marker == Jpeg.Marker.EOI)) { + // no length + return 0; + } + + uint16 length = dins.read_uint16(); + if (length < 2) { + debug("Invalid length %Xh at ofs %" + int64.FORMAT + "Xh", length, fins.tell() - 2); + + return -1; + } + + // account for two length bytes already read + return length - 2; + } +} + diff --git a/src/photos/PhotoFileAdapter.vala b/src/photos/PhotoFileAdapter.vala new file mode 100644 index 0000000..38b00ea --- /dev/null +++ b/src/photos/PhotoFileAdapter.vala @@ -0,0 +1,112 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// PhotoFileAdapter +// +// PhotoFileAdapter (and its immediate children, PhotoFileReader and PhotoFileWriter) are drivers +// hiding details of reading and writing image files and their metadata. They should keep +// minimal state beyond the filename, if any stat at all. In particular, they should avoid caching +// values, especially the readers, as writers may be created at any time and invalidate that +// information, unless the readers monitor the file for these changes. +// +// PhotoFileAdapters should be entirely thread-safe. They are not, however, responsible for +// atomicity on the filesystem. +// + +public abstract class PhotoFileAdapter { + private string filepath; + private PhotoFileFormat file_format; + private File file = null; + + public PhotoFileAdapter(string filepath, PhotoFileFormat file_format) { + this.filepath = filepath; + this.file_format = file_format; + } + + public bool file_exists() { + return FileUtils.test(filepath, FileTest.IS_REGULAR); + } + + public string get_filepath() { + return filepath; + } + + public File get_file() { + File result; + lock (file) { + if (file == null) + file = File.new_for_path(filepath); + + result = file; + } + + return result; + } + + public PhotoFileFormat get_file_format() { + return file_format; + } +} + +// +// PhotoFileReader +// + +public abstract class PhotoFileReader : PhotoFileAdapter { + protected PhotoFileReader(string filepath, PhotoFileFormat file_format) { + base (filepath, file_format); + } + + public PhotoFileWriter create_writer() throws PhotoFormatError { + return get_file_format().create_writer(get_filepath()); + } + + public PhotoFileMetadataWriter create_metadata_writer() throws PhotoFormatError { + return get_file_format().create_metadata_writer(get_filepath()); + } + + public abstract PhotoMetadata read_metadata() throws Error; + + public abstract Gdk.Pixbuf unscaled_read() throws Error; + + public virtual Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error { + return resize_pixbuf(unscaled_read(), scaled, Gdk.InterpType.BILINEAR); + } +} + +// +// PhotoFileWriter +// + +public abstract class PhotoFileWriter : PhotoFileAdapter { + protected PhotoFileWriter(string filepath, PhotoFileFormat file_format) { + base (filepath, file_format); + } + + public PhotoFileReader create_reader() { + return get_file_format().create_reader(get_filepath()); + } + + public abstract void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error; +} + +// +// PhotoFileMetadataWriter +// + +public abstract class PhotoFileMetadataWriter : PhotoFileAdapter { + protected PhotoFileMetadataWriter(string filepath, PhotoFileFormat file_format) { + base (filepath, file_format); + } + + public PhotoFileReader create_reader() { + return get_file_format().create_reader(get_filepath()); + } + + public abstract void write_metadata(PhotoMetadata metadata) throws Error; +} + diff --git a/src/photos/PhotoFileFormat.vala b/src/photos/PhotoFileFormat.vala new file mode 100644 index 0000000..2ab2f00 --- /dev/null +++ b/src/photos/PhotoFileFormat.vala @@ -0,0 +1,410 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public errordomain PhotoFormatError { + READ_ONLY +} + +// +// PhotoFileFormat +// + +namespace PhotoFileFormatData { + private static PhotoFileFormat[] writeable = null; + private static PhotoFileFormat[] image_writeable = null; + private static PhotoFileFormat[] metadata_writeable = null; + + private delegate bool ApplicableTest(PhotoFileFormat format); + + private PhotoFileFormat[] find_applicable(ApplicableTest test) { + PhotoFileFormat[] applicable = new PhotoFileFormat[0]; + foreach (PhotoFileFormat format in PhotoFileFormat.get_supported()) { + if (test(format)) + applicable += format; + } + + return applicable; + } + + public PhotoFileFormat[] get_writeable() { + if (writeable == null) + writeable = find_applicable((format) => { return format.can_write(); }); + + return writeable; + } + + public static PhotoFileFormat[] get_image_writeable() { + if (image_writeable == null) + image_writeable = find_applicable((format) => { return format.can_write_image(); }); + + return image_writeable; + } + + public static PhotoFileFormat[] get_metadata_writeable() { + if (metadata_writeable == null) + metadata_writeable = find_applicable((format) => { return format.can_write_metadata(); }); + + return metadata_writeable; + } +} + +public enum PhotoFileFormat { + JFIF, + RAW, + PNG, + TIFF, + BMP, + UNKNOWN; + + // This is currently listed in the order of detection, that is, the file is examined from + // left to right. (See PhotoFileInterrogator.) + public static PhotoFileFormat[] get_supported() { + return { JFIF, RAW, PNG, TIFF, BMP }; + } + + public static PhotoFileFormat[] get_writeable() { + return PhotoFileFormatData.get_writeable(); + } + + public static PhotoFileFormat[] get_image_writeable() { + return PhotoFileFormatData.get_image_writeable(); + } + + public static PhotoFileFormat[] get_metadata_writeable() { + return PhotoFileFormatData.get_metadata_writeable(); + } + + public static PhotoFileFormat get_by_basename_extension(string basename) { + string name, ext; + disassemble_filename(basename, out name, out ext); + + if (is_string_empty(ext)) + return UNKNOWN; + + foreach (PhotoFileFormat file_format in get_supported()) { + if (file_format.get_driver().get_properties().is_recognized_extension(ext)) + return file_format; + } + + return UNKNOWN; + } + + public static bool is_file_supported(File file) { + return is_basename_supported(file.get_basename()); + } + + public static bool is_basename_supported(string basename) { + string name, ext; + disassemble_filename(basename, out name, out ext); + + if (is_string_empty(ext)) + return false; + + foreach (PhotoFileFormat format in get_supported()) { + if (format.get_driver().get_properties().is_recognized_extension(ext)) + return true; + } + + return false; + } + + // Guaranteed to be writeable. + public static PhotoFileFormat get_system_default_format() { + return JFIF; + } + + public static PhotoFileFormat get_by_file_extension(File file) { + return get_by_basename_extension(file.get_basename()); + } + + // These values are persisted in the database. DO NOT CHANGE THE INTEGER EQUIVALENTS. + public int serialize() { + switch (this) { + case JFIF: + return 0; + + case RAW: + return 1; + + case PNG: + return 2; + + case TIFF: + return 3; + + case BMP: + return 4; + + case UNKNOWN: + default: + return -1; + } + } + + // These values are persisted in the database. DO NOT CHANGE THE INTEGER EQUIVALENTS. + public static PhotoFileFormat unserialize(int value) { + switch (value) { + case 0: + return JFIF; + + case 1: + return RAW; + + case 2: + return PNG; + + case 3: + return TIFF; + + case 4: + return BMP; + + default: + return UNKNOWN; + } + } + + public static PhotoFileFormat from_gphoto_type(string type) { + switch (type) { + case GPhoto.MIME.JPEG: + return PhotoFileFormat.JFIF; + + case GPhoto.MIME.RAW: + case GPhoto.MIME.CRW: + return PhotoFileFormat.RAW; + + case GPhoto.MIME.PNG: + return PhotoFileFormat.PNG; + + case GPhoto.MIME.TIFF: + return PhotoFileFormat.TIFF; + + case GPhoto.MIME.BMP: + return PhotoFileFormat.BMP; + + default: + // check file extension against those we support + return PhotoFileFormat.UNKNOWN; + } + } + + // Converts GDK's pixbuf library's name to a PhotoFileFormat + public static PhotoFileFormat from_pixbuf_name(string name) { + switch (name) { + case "jpeg": + return PhotoFileFormat.JFIF; + + case "png": + return PhotoFileFormat.PNG; + + case "tiff": + return PhotoFileFormat.TIFF; + + case "bmp": + return PhotoFileFormat.BMP; + + default: + return PhotoFileFormat.UNKNOWN; + } + } + + public void init() { + switch (this) { + case JFIF: + JfifFileFormatDriver.init(); + break; + + case RAW: + RawFileFormatDriver.init(); + break; + + case PNG: + PngFileFormatDriver.init(); + break; + + case TIFF: + Photos.TiffFileFormatDriver.init(); + break; + + case BMP: + Photos.BmpFileFormatDriver.init(); + break; + + default: + error("Unsupported file format %s", this.to_string()); + } + } + + private PhotoFileFormatDriver get_driver() { + switch (this) { + case JFIF: + return JfifFileFormatDriver.get_instance(); + + case RAW: + return RawFileFormatDriver.get_instance(); + + case PNG: + return PngFileFormatDriver.get_instance(); + + case TIFF: + return Photos.TiffFileFormatDriver.get_instance(); + + case BMP: + return Photos.BmpFileFormatDriver.get_instance(); + + default: + error("Unsupported file format %s", this.to_string()); + } + } + + public PhotoFileFormatProperties get_properties() { + return get_driver().get_properties(); + } + + // Supplied with a name, returns the name with the file format's default extension. + public string get_default_basename(string name) { + return "%s.%s".printf(name, get_properties().get_default_extension()); + } + + public PhotoFileReader create_reader(string filepath) { + return get_driver().create_reader(filepath); + } + + // This means the image and its metadata are writeable. + public bool can_write() { + return can_write_image() && can_write_metadata(); + } + + public bool can_write_image() { + return get_driver().can_write_image(); + } + + public bool can_write_metadata() { + return get_driver().can_write_metadata(); + } + + public PhotoFileWriter create_writer(string filepath) throws PhotoFormatError { + PhotoFileWriter writer = get_driver().create_writer(filepath); + if (writer == null) + throw new PhotoFormatError.READ_ONLY("File format %s is read-only", this.to_string()); + + return writer; + } + + public PhotoFileMetadataWriter create_metadata_writer(string filepath) throws PhotoFormatError { + PhotoFileMetadataWriter writer = get_driver().create_metadata_writer(filepath); + if (writer == null) + throw new PhotoFormatError.READ_ONLY("File format %s metadata is read-only", this.to_string()); + + return writer; + } + + public PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return get_driver().create_sniffer(file, options); + } + + public PhotoMetadata create_metadata() { + return get_driver().create_metadata(); + } + + public string get_default_mime_type() { + return get_driver().get_properties().get_default_mime_type(); + } + + public string[] get_mime_types() { + return get_driver().get_properties().get_mime_types(); + } + + public static string[] get_editable_mime_types() { + string[] mime_types = {}; + + foreach (PhotoFileFormat file_format in PhotoFileFormat.get_supported()) { + foreach (string mime_type in file_format.get_mime_types()) + mime_types += mime_type; + } + + return mime_types; + } +} + +// +// PhotoFileFormatDriver +// +// Each supported file format is expected to have a PhotoFileFormatDriver that returns all possible +// resources that are needed to operate on file of its particular type. It's expected that each +// format subsystem will only create and cache a single instance of this driver, although it's +// not required. +// +// Like the other elements in the PhotoFileFormat family, this class should be thread-safe. +// + +public abstract class PhotoFileFormatDriver { + public abstract PhotoFileFormatProperties get_properties(); + + public abstract PhotoFileReader create_reader(string filepath); + + public abstract PhotoMetadata create_metadata(); + + public abstract bool can_write_image(); + + public abstract bool can_write_metadata(); + + public abstract PhotoFileWriter? create_writer(string filepath); + + public abstract PhotoFileMetadataWriter? create_metadata_writer(string filepath); + + public abstract PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options); +} + +// +// PhotoFileFormatProperties +// +// Although each PhotoFileFormatProperties is expected to be largely static and immutable, these +// classes should be thread-safe. +// + +public enum PhotoFileFormatFlags { + NONE = 0x00000000, +} + +public abstract class PhotoFileFormatProperties { + public abstract PhotoFileFormat get_file_format(); + + public abstract PhotoFileFormatFlags get_flags(); + + // Default implementation will search for ext in get_known_extensions(), assuming they are + // all stored in lowercase. + public virtual bool is_recognized_extension(string ext) { + return is_in_ci_array(ext, get_known_extensions()); + } + + public abstract string get_default_extension(); + + public abstract string[] get_known_extensions(); + + public abstract string get_default_mime_type(); + + public abstract string[] get_mime_types(); + + // returns the user-visible name of the file format -- this name is used in user interface + // strings whenever the file format needs to named. This name is not the same as the format + // enum value converted to a string. The format enum value is meaningful to developers and is + // constant across languages (e.g. "JFIF", "TGA") whereas the user-visible name is translatable + // and is meaningful to users (e.g. "JPEG", "Truevision TARGA") + public abstract string get_user_visible_name(); + + // Takes a given file and returns one with the file format's default extension, unless it + // already has one of the format's known extensions + public File convert_file_extension(File file) { + string name, ext; + disassemble_filename(file.get_basename(), out name, out ext); + if (ext != null && is_recognized_extension(ext)) + return file; + + return file.get_parent().get_child("%s.%s".printf(name, get_default_extension())); + } +} + diff --git a/src/photos/PhotoFileSniffer.vala b/src/photos/PhotoFileSniffer.vala new file mode 100644 index 0000000..3f65ac2 --- /dev/null +++ b/src/photos/PhotoFileSniffer.vala @@ -0,0 +1,104 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public class DetectedPhotoInformation { + public PhotoFileFormat file_format = PhotoFileFormat.UNKNOWN; + public PhotoMetadata? metadata = null; + public string? md5 = null; + public string? exif_md5 = null; + public string? thumbnail_md5 = null; + public string? format_name = null; + public Dimensions image_dim = Dimensions(); + public Gdk.Colorspace colorspace = Gdk.Colorspace.RGB; + public int channels = 0; + public int bits_per_channel = 0; +} + +// +// A PhotoFileSniffer is expected to examine the supplied file as efficiently as humanly possible +// to detect (a) if it is of a file format supported by the particular sniffer, and (b) fill out +// a DetectedPhotoInformation record and return it to the caller. +// +// The PhotoFileSniffer is not expected to cache information. It should return a fresh +// DetectedPhotoInformation record each time. +// +// PhotoFileSniffer must be thread-safe. Like PhotoFileAdapters, it is not expected to guarantee +// atomicity with respect to the filesystem. +// + +public abstract class PhotoFileSniffer { + public enum Options { + GET_ALL = 0x00000000, + NO_MD5 = 0x00000001 + } + + protected File file; + protected Options options; + protected bool calc_md5; + + public PhotoFileSniffer(File file, Options options) { + this.file = file; + this.options = options; + + calc_md5 = (options & Options.NO_MD5) == 0; + } + + public abstract DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error; +} + +// +// PhotoFileInterrogator +// +// A PhotoFileInterrogator is merely an aggregator of PhotoFileSniffers. It will create sniffers +// for each supported PhotoFileFormat and see if they recognize the file. +// +// The PhotoFileInterrogator is not thread-safe. +// + +public class PhotoFileInterrogator { + private File file; + private PhotoFileSniffer.Options options; + private DetectedPhotoInformation? detected = null; + private bool is_photo_corrupted = false; + + public PhotoFileInterrogator(File file, + PhotoFileSniffer.Options options = PhotoFileSniffer.Options.GET_ALL) { + this.file = file; + this.options = options; + } + + // This should only be called after interrogate(). Will return null every time, otherwise. + // If called after interrogate and returns null, that indicates the file is not an image file. + public DetectedPhotoInformation? get_detected_photo_information() { + return detected; + } + + // Call after interrogate(). + public bool get_is_photo_corrupted() { + return is_photo_corrupted; + } + + public void interrogate() throws Error { + foreach (PhotoFileFormat file_format in PhotoFileFormat.get_supported()) { + PhotoFileSniffer sniffer = file_format.create_sniffer(file, options); + + bool is_corrupted; + detected = sniffer.sniff(out is_corrupted); + if (detected != null && !is_corrupted) { + assert(detected.file_format == file_format); + + break; + } else if (is_corrupted) { + message("Sniffing halted for %s: potentially corrupted image file", file.get_path()); + is_photo_corrupted = true; + detected = null; + + break; + } + } + } +} + diff --git a/src/photos/PhotoMetadata.vala b/src/photos/PhotoMetadata.vala new file mode 100644 index 0000000..37804bf --- /dev/null +++ b/src/photos/PhotoMetadata.vala @@ -0,0 +1,1169 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// +// PhotoMetadata +// +// PhotoMetadata is a wrapper class around gexiv2. The reasoning for this is (a) to facilitiate +// interface changes to meet Shotwell's requirements without needing modifications of the library +// itself, and (b) some requirements for this class (i.e. obtaining raw metadata) is not available +// in gexiv2, and so must be done by hand. +// +// Although it's perceived that Exiv2 will remain Shotwell's metadata library of choice, this +// may change in the future, and so this wrapper helps with that as well. +// +// There is no expectation of thread-safety in this class (yet). +// +// Tags come from Exiv2's naming scheme: +// http://www.exiv2.org/metadata.html +// + +public enum MetadataDomain { + UNKNOWN, + EXIF, + XMP, + IPTC +} + +public class HierarchicalKeywordField { + public string field_name; + public string path_separator; + public bool wants_leading_separator; + public bool is_writeable; + + public HierarchicalKeywordField(string field_name, string path_separator, + bool wants_leading_separator, bool is_writeable) { + this.field_name = field_name; + this.path_separator = path_separator; + this.wants_leading_separator = wants_leading_separator; + this.is_writeable = is_writeable; + } +} + +public abstract class PhotoPreview { + private string name; + private Dimensions dimensions; + private uint32 size; + private string mime_type; + private string extension; + + public PhotoPreview(string name, Dimensions dimensions, uint32 size, string mime_type, string extension) { + this.name = name; + this.dimensions = dimensions; + this.size = size; + this.mime_type = mime_type; + this.extension = extension; + } + + public string get_name() { + return name; + } + + public Dimensions get_pixel_dimensions() { + return dimensions; + } + + public uint32 get_size() { + return size; + } + + public string get_mime_type() { + return mime_type; + } + + public string get_extension() { + return extension; + } + + public abstract uint8[] flatten() throws Error; + + public virtual Gdk.Pixbuf? get_pixbuf() throws Error { + uint8[] flattened = flatten(); + + // Need to create from stream or file for decode ... catch decode error and return null, + // different from an I/O error causing the problem + try { + return new Gdk.Pixbuf.from_stream(new MemoryInputStream.from_data(flattened, null), + null); + } catch (Error err) { + warning("Unable to decode thumbnail for %s: %s", name, err.message); + + return null; + } + } +} + +public class PhotoMetadata : MediaMetadata { + public enum SetOption { + ALL_DOMAINS, + ONLY_IF_DOMAIN_PRESENT, + AT_LEAST_DEFAULT_DOMAIN + } + + private const PrepareInputTextOptions PREPARE_STRING_OPTIONS = + PrepareInputTextOptions.INVALID_IS_NULL + | PrepareInputTextOptions.EMPTY_IS_NULL + | PrepareInputTextOptions.STRIP + | PrepareInputTextOptions.STRIP_CRLF + | PrepareInputTextOptions.NORMALIZE + | PrepareInputTextOptions.VALIDATE; + + private class InternalPhotoPreview : PhotoPreview { + public PhotoMetadata owner; + public uint number; + + public InternalPhotoPreview(PhotoMetadata owner, string name, uint number, + GExiv2.PreviewProperties props) { + base (name, Dimensions((int) props.get_width(), (int) props.get_height()), + props.get_size(), props.get_mime_type(), props.get_extension()); + + this.owner = owner; + this.number = number; + } + + public override uint8[] flatten() throws Error { + unowned GExiv2.PreviewProperties?[] props = owner.exiv2.get_preview_properties(); + assert(props != null && props.length > number); + + return owner.exiv2.get_preview_image(props[number]).get_data(); + } + } + + private GExiv2.Metadata exiv2 = new GExiv2.Metadata(); + private Exif.Data? exif = null; + string source_name = "<uninitialized>"; + + public PhotoMetadata() { + } + + public override void read_from_file(File file) throws Error { + exiv2 = new GExiv2.Metadata(); + exif = null; + + exiv2.open_path(file.get_path()); + exif = Exif.Data.new_from_file(file.get_path()); + source_name = file.get_basename(); + } + + public void write_to_file(File file) throws Error { + exiv2.save_file(file.get_path()); + } + + public void read_from_buffer(uint8[] buffer, int length = 0) throws Error { + if (length <= 0) + length = buffer.length; + + assert(buffer.length >= length); + + exiv2 = new GExiv2.Metadata(); + exif = null; + + exiv2.open_buf(buffer, length); + exif = Exif.Data.new_from_data(buffer, length); + source_name = "<memory buffer %d bytes>".printf(length); + } + + public void read_from_app1_segment(uint8[] buffer, int length = 0) throws Error { + if (length <= 0) + length = buffer.length; + + assert(buffer.length >= length); + + exiv2 = new GExiv2.Metadata(); + exif = null; + + exiv2.from_app1_segment(buffer, length); + exif = Exif.Data.new_from_data(buffer, length); + source_name = "<app1 segment %d bytes>".printf(length); + } + + public static MetadataDomain get_tag_domain(string tag) { + if (GExiv2.Metadata.is_exif_tag(tag)) + return MetadataDomain.EXIF; + + if (GExiv2.Metadata.is_xmp_tag(tag)) + return MetadataDomain.XMP; + + if (GExiv2.Metadata.is_iptc_tag(tag)) + return MetadataDomain.IPTC; + + return MetadataDomain.UNKNOWN; + } + + public bool has_domain(MetadataDomain domain) { + switch (domain) { + case MetadataDomain.EXIF: + return exiv2.has_exif(); + + case MetadataDomain.XMP: + return exiv2.has_xmp(); + + case MetadataDomain.IPTC: + return exiv2.has_iptc(); + + case MetadataDomain.UNKNOWN: + default: + return false; + } + } + + public bool has_exif() { + return has_domain(MetadataDomain.EXIF); + } + + public bool has_xmp() { + return has_domain(MetadataDomain.XMP); + } + + public bool has_iptc() { + return has_domain(MetadataDomain.IPTC); + } + + public bool can_write_to_domain(MetadataDomain domain) { + switch (domain) { + case MetadataDomain.EXIF: + return exiv2.get_supports_exif(); + + case MetadataDomain.XMP: + return exiv2.get_supports_xmp(); + + case MetadataDomain.IPTC: + return exiv2.get_supports_iptc(); + + case MetadataDomain.UNKNOWN: + default: + return false; + } + } + + public bool can_write_exif() { + return can_write_to_domain(MetadataDomain.EXIF); + } + + public bool can_write_xmp() { + return can_write_to_domain(MetadataDomain.XMP); + } + + public bool can_write_iptc() { + return can_write_to_domain(MetadataDomain.IPTC); + } + + public bool has_tag(string tag) { + return exiv2.has_tag(tag); + } + + private Gee.Set<string> create_string_set(owned CompareDataFunc<string>? compare_func) { + // ternary doesn't work here + if (compare_func == null) + return new Gee.HashSet<string>(); + else + return new Gee.TreeSet<string>((owned) compare_func); + } + + public Gee.Collection<string>? get_tags(MetadataDomain domain, + owned CompareDataFunc<string>? compare_func = null) { + string[] tags = null; + switch (domain) { + case MetadataDomain.EXIF: + tags = exiv2.get_exif_tags(); + break; + + case MetadataDomain.XMP: + tags = exiv2.get_xmp_tags(); + break; + + case MetadataDomain.IPTC: + tags = exiv2.get_iptc_tags(); + break; + } + + if (tags == null || tags.length == 0) + return null; + + Gee.Collection<string> collection = create_string_set((owned) compare_func); + foreach (string tag in tags) + collection.add(tag); + + return collection; + } + + public Gee.Collection<string> get_all_tags( + owned CompareDataFunc<string>? compare_func = null) { + Gee.Collection<string> all_tags = create_string_set((owned) compare_func); + + Gee.Collection<string>? exif_tags = get_tags(MetadataDomain.EXIF); + if (exif_tags != null && exif_tags.size > 0) + all_tags.add_all(exif_tags); + + Gee.Collection<string>? xmp_tags = get_tags(MetadataDomain.XMP); + if (xmp_tags != null && xmp_tags.size > 0) + all_tags.add_all(xmp_tags); + + Gee.Collection<string>? iptc_tags = get_tags(MetadataDomain.IPTC); + if (iptc_tags != null && iptc_tags.size > 0) + all_tags.add_all(iptc_tags); + + return all_tags.size > 0 ? all_tags : null; + } + + public string? get_tag_label(string tag) { + return GExiv2.Metadata.get_tag_label(tag); + } + + public string? get_tag_description(string tag) { + return GExiv2.Metadata.get_tag_description(tag); + } + + public string? get_string(string tag, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) { + return prepare_input_text(exiv2.get_tag_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH); + } + + public string? get_string_interpreted(string tag, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) { + return prepare_input_text(exiv2.get_tag_interpreted_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH); + } + + public string? get_first_string(string[] tags) { + foreach (string tag in tags) { + string? value = get_string(tag); + if (value != null) + return value; + } + + return null; + } + + public string? get_first_string_interpreted(string[] tags) { + foreach (string tag in tags) { + string? value = get_string_interpreted(tag); + if (value != null) + return value; + } + + return null; + } + + // Returns a List that has been filtered through a Set, so no duplicates will be returned. + // + // NOTE: get_tag_multiple() in gexiv2 currently does not work with EXIF tags (as EXIF can + // never return a list of strings). It will quietly return NULL if attempted. Until fixed + // (there or here), don't use this function to access EXIF. See: + // http://trac.yorba.org/ticket/2966 + public Gee.List<string>? get_string_multiple(string tag) { + string[] values = exiv2.get_tag_multiple(tag); + if (values == null || values.length == 0) + return null; + + Gee.List<string> list = new Gee.ArrayList<string>(); + + Gee.HashSet<string> collection = new Gee.HashSet<string>(); + foreach (string value in values) { + string? prepped = prepare_input_text(value, PREPARE_STRING_OPTIONS, + DEFAULT_USER_TEXT_INPUT_LENGTH); + + if (prepped != null && !collection.contains(prepped)) { + list.add(prepped); + collection.add(prepped); + } + } + + return list.size > 0 ? list : null; + } + + // Returns a List that has been filtered through a Set, so no duplicates will be found. + // + // NOTE: get_tag_multiple() in gexiv2 currently does not work with EXIF tags (as EXIF can + // never return a list of strings). It will quietly return NULL if attempted. Until fixed + // (there or here), don't use this function to access EXIF. See: + // http://trac.yorba.org/ticket/2966 + public Gee.List<string>? get_first_string_multiple(string[] tags) { + foreach (string tag in tags) { + Gee.List<string>? values = get_string_multiple(tag); + if (values != null && values.size > 0) + return values; + } + + return null; + } + + public void set_string(string tag, string value, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) { + string? prepped = prepare_input_text(value, options, DEFAULT_USER_TEXT_INPUT_LENGTH); + if (prepped == null) { + warning("Not setting tag %s to string %s: invalid UTF-8", tag, value); + + return; + } + + if (!exiv2.set_tag_string(tag, prepped)) + warning("Unable to set tag %s to string %s from source %s", tag, value, source_name); + } + + private delegate void SetGenericValue(string tag); + + private void set_all_generic(string[] tags, SetOption option, SetGenericValue setter) { + bool written = false; + foreach (string tag in tags) { + if (option == SetOption.ALL_DOMAINS || has_domain(get_tag_domain(tag))) { + setter(tag); + written = true; + } + } + + if (option == SetOption.AT_LEAST_DEFAULT_DOMAIN && !written && tags.length > 0) { + MetadataDomain default_domain = get_tag_domain(tags[0]); + + // write at least the first one, as it's the default + setter(tags[0]); + + // write the remainder, if they are of the same domain + for (int ctr = 1; ctr < tags.length; ctr++) { + if (get_tag_domain(tags[ctr]) == default_domain) + setter(tags[ctr]); + } + } + } + + public void set_all_string(string[] tags, string value, SetOption option) { + set_all_generic(tags, option, (tag) => { set_string(tag, value); }); + } + + public void set_string_multiple(string tag, Gee.Collection<string> collection) { + string[] values = new string[0]; + foreach (string value in collection) { + string? prepped = prepare_input_text(value, PREPARE_STRING_OPTIONS,-1); + if (prepped != null) + values += prepped; + else + warning("Unable to set string %s to %s: invalid UTF-8", value, tag); + } + + if (values.length == 0) + return; + + // append a null pointer to the end of the string array -- this is a necessary + // workaround for http://trac.yorba.org/ticket/3264. See also + // http://trac.yorba.org/ticket/3257, which describes the user-visible behavior + // seen in the Flickr Connector as a result of the former bug. + values += null; + + if (!exiv2.set_tag_multiple(tag, values)) + warning("Unable to set %d strings to tag %s from source %s", values.length, tag, source_name); + } + + public void set_all_string_multiple(string[] tags, Gee.Collection<string> values, SetOption option) { + set_all_generic(tags, option, (tag) => { set_string_multiple(tag, values); }); + } + + public bool get_long(string tag, out long value) { + if (!has_tag(tag)) { + value = 0; + + return false; + } + + value = exiv2.get_tag_long(tag); + + return true; + } + + public bool get_first_long(string[] tags, out long value) { + foreach (string tag in tags) { + if (get_long(tag, out value)) + return true; + } + + value = 0; + + return false; + } + + public void set_long(string tag, long value) { + if (!exiv2.set_tag_long(tag, value)) + warning("Unable to set tag %s to long %ld from source %s", tag, value, source_name); + } + + public void set_all_long(string[] tags, long value, SetOption option) { + set_all_generic(tags, option, (tag) => { set_long(tag, value); }); + } + + public bool get_rational(string tag, out MetadataRational rational) { + int numerator, denominator; + bool result = exiv2.get_exif_tag_rational(tag, out numerator, out denominator); + + rational = MetadataRational(numerator, denominator); + + return result; + } + + public bool get_first_rational(string[] tags, out MetadataRational rational) { + foreach (string tag in tags) { + if (get_rational(tag, out rational)) + return true; + } + + rational = MetadataRational(0, 0); + + return false; + } + + public void set_rational(string tag, MetadataRational rational) { + if (!exiv2.set_exif_tag_rational(tag, rational.numerator, rational.denominator)) { + warning("Unable to set tag %s to rational %s from source %s", tag, rational.to_string(), + source_name); + } + } + + public void set_all_rational(string[] tags, MetadataRational rational, SetOption option) { + set_all_generic(tags, option, (tag) => { set_rational(tag, rational); }); + } + + public MetadataDateTime? get_date_time(string tag) { + string? value = get_string(tag); + if (value == null) + return null; + + try { + switch (get_tag_domain(tag)) { + case MetadataDomain.XMP: + return new MetadataDateTime.from_xmp(value); + + // TODO: IPTC date/time support (which is tricky here, because date/time values + // are stored in separate tags) + case MetadataDomain.IPTC: + return null; + + case MetadataDomain.EXIF: + default: + return new MetadataDateTime.from_exif(value); + } + } catch (Error err) { + warning("Unable to read date/time %s from source %s: %s", tag, source_name, err.message); + + return null; + } + } + + public MetadataDateTime? get_first_date_time(string[] tags) { + foreach (string tag in tags) { + MetadataDateTime? date_time = get_date_time(tag); + if (date_time != null) + return date_time; + } + + return null; + } + + public void set_date_time(string tag, MetadataDateTime date_time) { + switch (get_tag_domain(tag)) { + case MetadataDomain.EXIF: + set_string(tag, date_time.get_exif_label()); + break; + + case MetadataDomain.XMP: + set_string(tag, date_time.get_xmp_label()); + break; + + // TODO: Support IPTC date/time (which are stored in separate tags) + case MetadataDomain.IPTC: + default: + warning("Cannot set date/time for %s from source %s: unsupported metadata domain %s", tag, + source_name, get_tag_domain(tag).to_string()); + break; + } + } + + public void set_all_date_time(string[] tags, MetadataDateTime date_time, SetOption option) { + set_all_generic(tags, option, (tag) => { set_date_time(tag, date_time); }); + } + + // Returns raw bytes of EXIF metadata, including signature and optionally the preview (if present). + public uint8[]? flatten_exif(bool include_preview) { + if (exif == null) + return null; + + // save thumbnail to strip if no attachments requested (so it can be added back and + // deallocated automatically) + uchar *thumbnail = exif.data; + uint thumbnail_size = exif.size; + if (!include_preview) { + exif.data = null; + exif.size = 0; + } + + uint8[]? flattened = null; + + // save the struct to a buffer and copy into a Vala-friendly one + uchar *saved_data = null; + uint saved_size = 0; + exif.save_data(&saved_data, &saved_size); + if (saved_size > 0 && saved_data != null) { + flattened = new uint8[saved_size]; + Memory.copy(flattened, saved_data, saved_size); + + Exif.Mem.new_default().free(saved_data); + } + + // restore thumbnail (this works in either case) + exif.data = thumbnail; + exif.size = thumbnail_size; + + return flattened; + } + + // Returns raw bytes of EXIF preview, if present + public uint8[]? flatten_exif_preview() { + uchar[] buffer; + return exiv2.get_exif_thumbnail(out buffer) ? buffer : null; + } + + public uint get_preview_count() { + unowned GExiv2.PreviewProperties?[] props = exiv2.get_preview_properties(); + + return (props != null) ? props.length : 0; + } + + // Previews are sorted from smallest to largest (width x height) + public PhotoPreview? get_preview(uint number) { + unowned GExiv2.PreviewProperties?[] props = exiv2.get_preview_properties(); + if (props == null || props.length <= number) + return null; + + return new InternalPhotoPreview(this, source_name, number, props[number]); + } + + public void remove_exif_thumbnail() { + exiv2.erase_exif_thumbnail(); + if (exif != null) { + Exif.Mem.new_default().free(exif.data); + exif.data = null; + exif.size = 0; + } + } + + public void remove_tag(string tag) { + exiv2.clear_tag(tag); + } + + public void remove_tags(string[] tags) { + foreach (string tag in tags) + remove_tag(tag); + } + + public void clear_domain(MetadataDomain domain) { + switch (domain) { + case MetadataDomain.EXIF: + exiv2.clear_exif(); + break; + + case MetadataDomain.XMP: + exiv2.clear_xmp(); + break; + + case MetadataDomain.IPTC: + exiv2.clear_iptc(); + break; + } + } + + public void clear() { + exiv2.clear(); + } + + private static string[] DATE_TIME_TAGS = { + "Exif.Image.DateTime", + "Xmp.tiff.DateTime", + "Xmp.xmp.ModifyDate" + }; + + public MetadataDateTime? get_modification_date_time() { + return get_first_date_time(DATE_TIME_TAGS); + } + + public void set_modification_date_time(MetadataDateTime? date_time, + SetOption option = SetOption.ALL_DOMAINS) { + if (date_time != null) + set_all_date_time(DATE_TIME_TAGS, date_time, option); + else + remove_tags(DATE_TIME_TAGS); + } + + private static string[] EXPOSURE_DATE_TIME_TAGS = { + "Exif.Photo.DateTimeOriginal", + "Xmp.exif.DateTimeOriginal", + "Xmp.xmp.CreateDate", + "Exif.Photo.DateTimeDigitized", + "Xmp.exif.DateTimeDigitized", + "Exif.Image.DateTime" + }; + + public MetadataDateTime? get_exposure_date_time() { + return get_first_date_time(EXPOSURE_DATE_TIME_TAGS); + } + + public void set_exposure_date_time(MetadataDateTime? date_time, + SetOption option = SetOption.ALL_DOMAINS) { + if (date_time != null) + set_all_date_time(EXPOSURE_DATE_TIME_TAGS, date_time, option); + else + remove_tags(EXPOSURE_DATE_TIME_TAGS); + } + + private static string[] DIGITIZED_DATE_TIME_TAGS = { + "Exif.Photo.DateTimeDigitized", + "Xmp.exif.DateTimeDigitized" + }; + + public MetadataDateTime? get_digitized_date_time() { + return get_first_date_time(DIGITIZED_DATE_TIME_TAGS); + } + + public void set_digitized_date_time(MetadataDateTime? date_time, + SetOption option = SetOption.ALL_DOMAINS) { + if (date_time != null) + set_all_date_time(DIGITIZED_DATE_TIME_TAGS, date_time, option); + else + remove_tags(DIGITIZED_DATE_TIME_TAGS); + } + + public override MetadataDateTime? get_creation_date_time() { + MetadataDateTime? creation = get_exposure_date_time(); + if (creation == null) + creation = get_digitized_date_time(); + + return creation; + } + + private static string[] WIDTH_TAGS = { + "Exif.Photo.PixelXDimension", + "Xmp.exif.PixelXDimension", + "Xmp.tiff.ImageWidth", + "Xmp.exif.PixelXDimension" + }; + + public static string[] HEIGHT_TAGS = { + "Exif.Photo.PixelYDimension", + "Xmp.exif.PixelYDimension", + "Xmp.tiff.ImageHeight", + "Xmp.exif.PixelYDimension" + }; + + public Dimensions? get_pixel_dimensions() { + // walk the tag arrays concurrently, returning the dimensions of the first found pair + assert(WIDTH_TAGS.length == HEIGHT_TAGS.length); + for (int ctr = 0; ctr < WIDTH_TAGS.length; ctr++) { + // Can't turn this into a single if statement with an || bailing out due to this bug: + // https://bugzilla.gnome.org/show_bug.cgi?id=565385 + long width; + if (!get_long(WIDTH_TAGS[ctr], out width)) + continue; + + long height; + if (!get_long(HEIGHT_TAGS[ctr], out height)) + continue; + + return Dimensions((int) width, (int) height); + } + + return null; + } + + public void set_pixel_dimensions(Dimensions? dim, SetOption option = SetOption.ALL_DOMAINS) { + if (dim != null) { + set_all_long(WIDTH_TAGS, dim.width, option); + set_all_long(HEIGHT_TAGS, dim.height, option); + } else { + remove_tags(WIDTH_TAGS); + remove_tags(HEIGHT_TAGS); + } + } + + // + // A note regarding titles and descriptions: + // + // iPhoto stores its title in Iptc.Application2.ObjectName and its description in + // Iptc.Application2.Caption. Most others use .Caption for the title and another + // (sometimes) appropriate tag for the description. And there's general confusion about + // whether Exif.Image.ImageDescription is a description (which is what the tag name + // suggests) or a title (which is what the specification states). + // See: http://trac.yorba.org/wiki/PhotoTags + // + // Hence, the following logic tries to do the right thing in most of these cases. If + // the iPhoto title tag is detected, it and the iPhoto description tag are used. Otherwise, + // the title/description are searched out from a list of standard tags. + // + // Exif.Image.ImageDescription seems to be abused, both in that iPhoto uses it as a multiline + // description and that some cameras insert their make & model information there (IN ALL CAPS, + // to really rub it in). We are ignoring the field until a compelling reason to support it + // is found. + // + + private const string IPHOTO_TITLE_TAG = "Iptc.Application2.ObjectName"; + + private static string[] STANDARD_TITLE_TAGS = { + "Iptc.Application2.Caption", + "Xmp.dc.title", + "Iptc.Application2.Headline", + "Xmp.photoshop.Headline" + }; + + public override string? get_title() { + // using get_string_multiple()/get_first_string_multiple() because it's possible for + // multiple strings to be specified in XMP for different language codes, and want to + // retrieve only the first one (other get_string variants will return ugly strings like + // + // lang="x-default" Xyzzy + // + // but get_string_multiple will return a list of titles w/o language information + Gee.List<string>? titles = has_tag(IPHOTO_TITLE_TAG) + ? get_string_multiple(IPHOTO_TITLE_TAG) + : get_first_string_multiple(STANDARD_TITLE_TAGS); + + // use the first string every time (assume it's default) + // TODO: We could get a list of all titles by their lang="<iso code>" and attempt to find + // the right one for the user's locale, but this does not seem to be a normal use case + string? title = (titles != null && titles.size > 0) ? titles[0] : null; + + // strip out leading and trailing whitespace + if (title != null) + title = title.strip(); + + // check for \n and \r to prevent multiline titles, which have been spotted in the wild + return (!is_string_empty(title) && !title.contains("\n") && !title.contains("\r")) ? + title : null; + } + + public void set_title(string? title, SetOption option = SetOption.ALL_DOMAINS) { + if (!is_string_empty(title)) { + if (has_tag(IPHOTO_TITLE_TAG)) + set_string(IPHOTO_TITLE_TAG, title); + else + set_all_string(STANDARD_TITLE_TAGS, title, option); + } else { + remove_tags(STANDARD_TITLE_TAGS); + } + } + + public override string? get_comment() { + return get_string_interpreted("Exif.Photo.UserComment", PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.STRIP_CRLF); + } + + public void set_comment(string? comment) { + if (!is_string_empty(comment)) + set_string("Exif.Photo.UserComment", comment, PrepareInputTextOptions.DEFAULT & ~PrepareInputTextOptions.STRIP_CRLF); + else + remove_tag("Exif.Photo.UserComment"); + } + + private static string[] KEYWORD_TAGS = { + "Xmp.dc.subject", + "Iptc.Application2.Keywords" + }; + + private static HierarchicalKeywordField[] HIERARCHICAL_KEYWORD_TAGS = { + // Xmp.lr.hierarchicalSubject should be writeable but isn't due to this bug + // in libexiv2: http://dev.exiv2.org/issues/784 + new HierarchicalKeywordField("Xmp.lr.hierarchicalSubject", "|", false, false), + new HierarchicalKeywordField("Xmp.digiKam.TagsList", "/", false, true), + new HierarchicalKeywordField("Xmp.MicrosoftPhoto.LastKeywordXMP", "/", false, true) + }; + + public Gee.Set<string>? get_keywords(owned CompareDataFunc<string>? compare_func = null) { + Gee.Set<string> keywords = null; + foreach (string tag in KEYWORD_TAGS) { + Gee.Collection<string>? values = get_string_multiple(tag); + if (values != null && values.size > 0) { + if (keywords == null) + keywords = create_string_set((owned) compare_func); + + foreach (string current_value in values) + keywords.add(HierarchicalTagUtilities.make_flat_tag_safe(current_value)); + } + } + + return (keywords != null && keywords.size > 0) ? keywords : null; + } + + private void internal_set_hierarchical_keywords(HierarchicalTagIndex? index) { + foreach (HierarchicalKeywordField current_field in HIERARCHICAL_KEYWORD_TAGS) + remove_tag(current_field.field_name); + + if (index == null) + return; + + foreach (HierarchicalKeywordField current_field in HIERARCHICAL_KEYWORD_TAGS) { + if (!current_field.is_writeable) + continue; + + Gee.Set<string> writeable_set = new Gee.TreeSet<string>(); + + foreach (string current_path in index.get_all_paths()) { + string writeable_path = current_path.replace(Tag.PATH_SEPARATOR_STRING, + current_field.path_separator); + if (!current_field.wants_leading_separator) + writeable_path = writeable_path.substring(1); + + writeable_set.add(writeable_path); + } + + set_string_multiple(current_field.field_name, writeable_set); + } + } + + public void set_keywords(Gee.Collection<string>? keywords, SetOption option = SetOption.ALL_DOMAINS) { + HierarchicalTagIndex htag_index = new HierarchicalTagIndex(); + Gee.Set<string> flat_keywords = new Gee.TreeSet<string>(); + + if (keywords != null) { + foreach (string keyword in keywords) { + if (keyword.has_prefix(Tag.PATH_SEPARATOR_STRING)) { + Gee.Collection<string> path_components = + HierarchicalTagUtilities.enumerate_path_components(keyword); + foreach (string component in path_components) + htag_index.add_path(component, keyword); + } else { + flat_keywords.add(keyword); + } + } + + flat_keywords.add_all(htag_index.get_all_tags()); + } + + if (keywords != null) { + set_all_string_multiple(KEYWORD_TAGS, flat_keywords, option); + internal_set_hierarchical_keywords(htag_index); + } else { + remove_tags(KEYWORD_TAGS); + internal_set_hierarchical_keywords(null); + } + } + + public bool has_hierarchical_keywords() { + foreach (HierarchicalKeywordField field in HIERARCHICAL_KEYWORD_TAGS) { + Gee.Collection<string>? values = get_string_multiple(field.field_name); + + if (values != null && values.size > 0) + return true; + } + + return false; + } + + public Gee.Set<string> get_hierarchical_keywords() { + assert(has_hierarchical_keywords()); + + Gee.Set<string> h_keywords = create_string_set(null); + + foreach (HierarchicalKeywordField field in HIERARCHICAL_KEYWORD_TAGS) { + Gee.Collection<string>? values = get_string_multiple(field.field_name); + + if (values == null || values.size < 1) + continue; + + foreach (string current_value in values) { + string? canonicalized = HierarchicalTagUtilities.canonicalize(current_value, + field.path_separator); + + if (canonicalized != null) + h_keywords.add(canonicalized); + } + } + + return h_keywords; + } + + public bool has_orientation() { + return exiv2.get_orientation() == GExiv2.Orientation.UNSPECIFIED; + } + + // If not present, returns Orientation.TOP_LEFT. + public Orientation get_orientation() { + // GExiv2.Orientation is the same value-wise as Orientation, with one exception: + // GExiv2.Orientation.UNSPECIFIED must be handled + GExiv2.Orientation orientation = exiv2.get_orientation(); + if (orientation == GExiv2.Orientation.UNSPECIFIED || orientation < Orientation.MIN || + orientation > Orientation.MAX) + return Orientation.TOP_LEFT; + else + return (Orientation) orientation; + } + + public void set_orientation(Orientation orientation) { + // GExiv2.Orientation is the same value-wise as Orientation + exiv2.set_orientation((GExiv2.Orientation) orientation); + } + + public bool get_gps(out double longitude, out string long_ref, out double latitude, out string lat_ref, + out double altitude) { + if (!exiv2.get_gps_info(out longitude, out latitude, out altitude)) { + long_ref = null; + lat_ref = null; + + return false; + } + + long_ref = get_string("Exif.GPSInfo.GPSLongitudeRef"); + lat_ref = get_string("Exif.GPSInfo.GPSLatitudeRef"); + + return true; + } + + public bool get_exposure(out MetadataRational exposure) { + return get_rational("Exif.Photo.ExposureTime", out exposure); + } + + public string? get_exposure_string() { + MetadataRational exposure_time; + if (!get_rational("Exif.Photo.ExposureTime", out exposure_time)) + return null; + + if (!exposure_time.is_valid()) + return null; + + return get_string_interpreted("Exif.Photo.ExposureTime"); + } + + public bool get_iso(out long iso) { + bool fetched_ok = get_long("Exif.Photo.ISOSpeedRatings", out iso); + + if (fetched_ok == false) + return false; + + // lower boundary is original (ca. 1935) Kodachrome speed, the lowest ISO rated film ever + // manufactured; upper boundary is 4 x fastest high-speed digital camera speeds + if ((iso < 6) || (iso > 409600)) + return false; + + return true; + } + + public string? get_iso_string() { + long iso; + if (!get_iso(out iso)) + return null; + + return get_string_interpreted("Exif.Photo.ISOSpeedRatings"); + } + + public bool get_aperture(out MetadataRational aperture) { + return get_rational("Exif.Photo.FNumber", out aperture); + } + + public string? get_aperture_string(bool pango_formatted = false) { + MetadataRational aperture; + if (!get_aperture(out aperture)) + return null; + + double aperture_value = ((double) aperture.numerator) / ((double) aperture.denominator); + aperture_value = ((int) (aperture_value * 10.0)) / 10.0; + + return (pango_formatted ? "<i>f</i>/" : "f/") + + ((aperture_value % 1 == 0) ? "%.0f" : "%.1f").printf(aperture_value); + } + + public string? get_camera_make() { + return get_string_interpreted("Exif.Image.Make"); + } + + public string? get_camera_model() { + return get_string_interpreted("Exif.Image.Model"); + } + + public bool get_flash(out long flash) { + // Exif.Image.Flash does not work for some reason + return get_long("Exif.Photo.Flash", out flash); + } + + public string? get_flash_string() { + // Exif.Image.Flash does not work for some reason + return get_string_interpreted("Exif.Photo.Flash"); + } + + public bool get_focal_length(out MetadataRational focal_length) { + return get_rational("Exif.Photo.FocalLength", out focal_length); + } + + public string? get_focal_length_string() { + return get_string_interpreted("Exif.Photo.FocalLength"); + } + + private static string[] ARTIST_TAGS = { + "Exif.Image.Artist", + "Exif.Canon.OwnerName" // Custom tag used by Canon DSLR cameras + }; + + public string? get_artist() { + return get_first_string_interpreted(ARTIST_TAGS); + } + + public string? get_copyright() { + return get_string_interpreted("Exif.Image.Copyright"); + } + + public string? get_software() { + return get_string_interpreted("Exif.Image.Software"); + } + + public void set_software(string software, string version) { + // always set this one, even if EXIF not present + set_string("Exif.Image.Software", "%s %s".printf(software, version)); + + if (has_iptc()) { + set_string("Iptc.Application2.Program", software); + set_string("Iptc.Application2.ProgramVersion", version); + } + } + + public void remove_software() { + remove_tag("Exif.Image.Software"); + remove_tag("Iptc.Application2.Program"); + remove_tag("Iptc.Application2.ProgramVersion"); + } + + public string? get_exposure_bias() { + return get_string_interpreted("Exif.Photo.ExposureBiasValue"); + } + + private static string[] RATING_TAGS = { + "Xmp.xmp.Rating", + "Iptc.Application2.Urgency", + "Xmp.photoshop.Urgency", + "Exif.Image.Rating" + }; + + public Rating get_rating() { + string? rating_string = get_first_string(RATING_TAGS); + if(rating_string != null) + return Rating.unserialize(int.parse(rating_string)); + + rating_string = get_string("Exif.Image.RatingPercent"); + if(rating_string == null) { + return Rating.UNRATED; + } + + int int_percent_rating = int.parse(rating_string); + for(int i = 5; i >= 0; --i) { + if(int_percent_rating >= Resources.rating_thresholds[i]) + return Rating.unserialize(i); + } + return Rating.unserialize(-1); + } + + // Among photo managers, Xmp.xmp.Rating tends to be the standard way to represent ratings. + // Other photo managers, notably F-Spot, take hints from Urgency fields about what the rating + // of an imported photo should be, and we have decided to do as well. Xmp.xmp.Rating is the only + // field we've seen photo manages export ratings to, while Urgency fields seem to have a fundamentally + // different meaning. See http://trac.yorba.org/wiki/PhotoTags#Rating for more information. + public void set_rating(Rating rating) { + int int_rating = rating.serialize(); + set_string("Xmp.xmp.Rating", int_rating.to_string()); + set_string("Exif.Image.Rating", int_rating.to_string()); + + if( 0 <= int_rating ) + set_string("Exif.Image.RatingPercent", Resources.rating_thresholds[int_rating].to_string()); + else // in this case we _know_ int_rating is -1 + set_string("Exif.Image.RatingPercent", int_rating.to_string()); + } +} + diff --git a/src/photos/Photos.vala b/src/photos/Photos.vala new file mode 100644 index 0000000..3033b92 --- /dev/null +++ b/src/photos/Photos.vala @@ -0,0 +1,31 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +/* This file is the master unit file for the Photo unit. It should be edited to include + * whatever code is deemed necessary. + * + * The init() and terminate() methods are mandatory. + * + * If the unit needs to be configured prior to initialization, add the proper parameters to + * the preconfigure() method, implement it, and ensure in init() that it's been called. + */ + +namespace Photos { + +// preconfigure may be deleted if not used. +public void preconfigure() { +} + +public void init() throws Error { + foreach (PhotoFileFormat format in PhotoFileFormat.get_supported()) + format.init(); +} + +public void terminate() { +} + +} + diff --git a/src/photos/PngSupport.vala b/src/photos/PngSupport.vala new file mode 100644 index 0000000..2cde6a2 --- /dev/null +++ b/src/photos/PngSupport.vala @@ -0,0 +1,184 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +class PngFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { "png" }; + private static string[] KNOWN_MIME_TYPES = { "image/png" }; + + private static PngFileFormatProperties instance = null; + + public static void init() { + instance = new PngFileFormatProperties(); + } + + public static PngFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.PNG; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_user_visible_name() { + return _("PNG"); + } + + public override string get_default_extension() { + return KNOWN_EXTENSIONS[0]; + } + + public override string[] get_known_extensions() { + return KNOWN_EXTENSIONS; + } + + public override string get_default_mime_type() { + return KNOWN_MIME_TYPES[0]; + } + + public override string[] get_mime_types() { + return KNOWN_MIME_TYPES; + } +} + +public class PngSniffer : GdkSniffer { + private const uint8[] MAGIC_SEQUENCE = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + public PngSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + private static bool is_png_file(File file) throws Error { + FileInputStream instream = file.read(null); + + uint8[] file_lead_sequence = new uint8[MAGIC_SEQUENCE.length]; + + instream.read(file_lead_sequence, null); + + for (int i = 0; i < MAGIC_SEQUENCE.length; i++) { + if (file_lead_sequence[i] != MAGIC_SEQUENCE[i]) + return false; + } + + return true; + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // Rely on GdkSniffer to detect corruption + is_corrupted = false; + + if (!is_png_file(file)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.PNG) ? detected : null; + } +} + +public class PngReader : GdkReader { + public PngReader(string filepath) { + base (filepath, PhotoFileFormat.PNG); + } + + public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error { + Gdk.Pixbuf result = null; + /* if we encounter a situation where there are two orders of magnitude or more of + difference between the full image size and the scaled size, and if the full image + size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can + fail due to what appear to be floating-point round-off issues. This isn't surprising, + since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In + this case, we prefetch the image at a larger scale and then downsample it to the + desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy + scaling code. */ + if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) || + (scaled.height < 100))) { + Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000, + ScaleConstraint.DIMENSIONS); + + result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width, + prefetch_dimensions.height, false); + + result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER); + } else { + result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width, + scaled.height, false); + } + + return result; + } +} + +public class PngWriter : PhotoFileWriter { + public PngWriter(string filepath) { + base (filepath, PhotoFileFormat.PNG); + } + + public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error { + pixbuf.save(get_filepath(), "png", "compression", "9", null); + } +} + +public class PngMetadataWriter : PhotoFileMetadataWriter { + public PngMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.PNG); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public class PngFileFormatDriver : PhotoFileFormatDriver { + private static PngFileFormatDriver instance = null; + + public static void init() { + instance = new PngFileFormatDriver(); + PngFileFormatProperties.init(); + } + + public static PngFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return PngFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new PngReader(filepath); + } + + public override bool can_write_image() { + return true; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return new PngWriter(filepath); + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new PngMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new PngSniffer(file, options); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } +} + diff --git a/src/photos/RawSupport.vala b/src/photos/RawSupport.vala new file mode 100644 index 0000000..98bc982 --- /dev/null +++ b/src/photos/RawSupport.vala @@ -0,0 +1,350 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class RawFileFormatDriver : PhotoFileFormatDriver { + private static RawFileFormatDriver instance = null; + + public static void init() { + instance = new RawFileFormatDriver(); + RawFileFormatProperties.init(); + } + + public static RawFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return RawFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new RawReader(filepath); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } + + public override bool can_write_image() { + return false; + } + + public override bool can_write_metadata() { + return false; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return null; + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return null; + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new RawSniffer(file, options); + } +} + +public class RawFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { + "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cap", "iiq", "eip", "dcs", "dcr", "drf", + "k25", "kdc", "dng", "erf", "fff", "mef", "mos", "mrw", "nef", "nrw", "orf", "ptx", "pef", + "pxn", "r3d", "raf", "raw", "rw2", "raw", "rwl", "rwz", "x3f", "srw" + }; + + private static string[] KNOWN_MIME_TYPES = { + /* a catch-all MIME type for all formats supported by the dcraw command-line + tool (and hence libraw) */ + "image/x-dcraw", + + /* manufacturer blessed MIME types */ + "image/x-canon-cr2", + "image/x-canon-crw", + "image/x-fuji-raf", + "image/x-adobe-dng", + "image/x-panasonic-raw", + "image/x-raw", + "image/x-minolta-mrw", + "image/x-nikon-nef", + "image/x-olympus-orf", + "image/x-pentax-pef", + "image/x-sony-arw", + "image/x-sony-srf", + "image/x-sony-sr2", + "image/x-samsung-raw", + + /* generic MIME types for file extensions*/ + "image/x-3fr", + "image/x-arw", + "image/x-srf", + "image/x-sr2", + "image/x-bay", + "image/x-crw", + "image/x-cr2", + "image/x-cap", + "image/x-iiq", + "image/x-eip", + "image/x-dcs", + "image/x-dcr", + "image/x-drf", + "image/x-k25", + "image/x-kdc", + "image/x-dng", + "image/x-erf", + "image/x-fff", + "image/x-mef", + "image/x-mos", + "image/x-mrw", + "image/x-nef", + "image/x-nrw", + "image/x-orf", + "image/x-ptx", + "image/x-pef", + "image/x-pxn", + "image/x-r3d", + "image/x-raf", + "image/x-raw", + "image/x-rw2", + "image/x-raw", + "image/x-rwl", + "image/x-rwz", + "image/x-x3f", + "image/x-srw" + }; + + private static RawFileFormatProperties instance = null; + + public static void init() { + instance = new RawFileFormatProperties(); + } + + public static RawFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.RAW; + } + + public override string get_user_visible_name() { + return _("RAW"); + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_default_extension() { + // Because RAW is a smorgasbord of file formats and exporting to a RAW file is + // not expected, this function should probably never be called. However, need to pick + // one, so here it is. + return "raw"; + } + + public override string[] get_known_extensions() { + return KNOWN_EXTENSIONS; + } + + public override string get_default_mime_type() { + return KNOWN_MIME_TYPES[0]; + } + + public override string[] get_mime_types() { + return KNOWN_MIME_TYPES; + } +} + +public class RawSniffer : PhotoFileSniffer { + public RawSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // this sniffer doesn't detect corrupted files + is_corrupted = false; + + DetectedPhotoInformation detected = new DetectedPhotoInformation(); + + GRaw.Processor processor = new GRaw.Processor(); + processor.output_params->user_flip = GRaw.Flip.NONE; + + try { + processor.open_file(file.get_path()); + processor.unpack(); + processor.adjust_sizes_info_only(); + } catch (GRaw.Exception exception) { + if (exception is GRaw.Exception.UNSUPPORTED_FILE) + return null; + + throw exception; + } + + detected.image_dim = Dimensions(processor.get_sizes().iwidth, processor.get_sizes().iheight); + detected.colorspace = Gdk.Colorspace.RGB; + detected.channels = 3; + detected.bits_per_channel = 8; + + RawReader reader = new RawReader(file.get_path()); + try { + detected.metadata = reader.read_metadata(); + } catch (Error err) { + // ignored + } + + if (detected.metadata != null) { + uint8[]? flattened_sans_thumbnail = detected.metadata.flatten_exif(false); + if (flattened_sans_thumbnail != null && flattened_sans_thumbnail.length > 0) + detected.exif_md5 = md5_binary(flattened_sans_thumbnail, flattened_sans_thumbnail.length); + + uint8[]? flattened_thumbnail = detected.metadata.flatten_exif_preview(); + if (flattened_thumbnail != null && flattened_thumbnail.length > 0) + detected.thumbnail_md5 = md5_binary(flattened_thumbnail, flattened_thumbnail.length); + } + + if (calc_md5) + detected.md5 = md5_file(file); + + detected.format_name = "raw"; + detected.file_format = PhotoFileFormat.RAW; + + return detected; + } +} + +public class RawReader : PhotoFileReader { + public RawReader(string filepath) { + base (filepath, PhotoFileFormat.RAW); + } + + public override PhotoMetadata read_metadata() throws Error { + PhotoMetadata metadata = new PhotoMetadata(); + metadata.read_from_file(get_file()); + + return metadata; + } + + public override Gdk.Pixbuf unscaled_read() throws Error { + GRaw.Processor processor = new GRaw.Processor(); + processor.configure_for_rgb_display(false); + processor.output_params->user_flip = GRaw.Flip.NONE; + + processor.open_file(get_filepath()); + processor.unpack(); + processor.process(); + + return processor.make_mem_image().get_pixbuf_copy(); + } + + public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error { + double width_proportion = (double) scaled.width / (double) full.width; + double height_proportion = (double) scaled.height / (double) full.height; + bool half_size = width_proportion < 0.5 && height_proportion < 0.5; + + GRaw.Processor processor = new GRaw.Processor(); + processor.configure_for_rgb_display(half_size); + processor.output_params->user_flip = GRaw.Flip.NONE; + + processor.open_file(get_filepath()); + processor.unpack(); + processor.process(); + + GRaw.ProcessedImage image = processor.make_mem_image(); + + return resize_pixbuf(image.get_pixbuf_copy(), scaled, Gdk.InterpType.BILINEAR); + } +} + +// Development mode of a RAW photo. +public enum RawDeveloper { + SHOTWELL = 0, // Developed internally by Shotwell + CAMERA, // JPEG from RAW+JPEG pair (if available) + EMBEDDED; // Largest-size + + public static RawDeveloper[] as_array() { + return { SHOTWELL, CAMERA, EMBEDDED }; + } + + public string to_string() { + switch (this) { + case SHOTWELL: + return "SHOTWELL"; + case CAMERA: + return "CAMERA"; + case EMBEDDED: + return "EMBEDDED"; + default: + assert_not_reached(); + } + } + + public static RawDeveloper from_string(string value) { + switch (value) { + case "SHOTWELL": + return SHOTWELL; + case "CAMERA": + return CAMERA; + case "EMBEDDED": + return EMBEDDED; + default: + assert_not_reached(); + } + } + + public string get_label() { + switch (this) { + case SHOTWELL: + return _("Shotwell"); + case CAMERA: + case EMBEDDED: + return _("Camera"); + default: + assert_not_reached(); + } + } + + // Determines if two RAW developers are equivalent, treating camera and embedded + // as the same. + public bool is_equivalent(RawDeveloper d) { + if (this == d) + return true; + + if ((this == RawDeveloper.CAMERA && d == RawDeveloper.EMBEDDED) || + (this == RawDeveloper.EMBEDDED && d == RawDeveloper.CAMERA)) + return true; + + return false; + } + + // Creates a backing JPEG. + // raw_filepath is the full path of the imported RAW file. + public BackingPhotoRow create_backing_row_for_development(string raw_filepath, + string? camera_development_filename = null) throws Error { + BackingPhotoRow ns = new BackingPhotoRow(); + File master = File.new_for_path(raw_filepath); + string name, ext; + disassemble_filename(master.get_basename(), out name, out ext); + + string basename; + + // If this image is coming in with an existing development, use its existing + // filename instead. + if (camera_development_filename == null) { + basename = name + "_" + ext + + (this != CAMERA ? ("_" + this.to_string().down()) : "") + ".jpg"; + } else { + basename = camera_development_filename; + } + + bool c; + File? new_back = generate_unique_file(master.get_parent(), basename, out c); + claim_file(new_back); + ns.file_format = PhotoFileFormat.JFIF; + ns.filepath = new_back.get_path(); + + return ns; + } +} diff --git a/src/photos/TiffSupport.vala b/src/photos/TiffSupport.vala new file mode 100644 index 0000000..ee8b087 --- /dev/null +++ b/src/photos/TiffSupport.vala @@ -0,0 +1,183 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Photos { + +public class TiffFileFormatDriver : PhotoFileFormatDriver { + private static TiffFileFormatDriver instance = null; + + public static void init() { + instance = new TiffFileFormatDriver(); + TiffFileFormatProperties.init(); + } + + public static TiffFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return TiffFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new TiffReader(filepath); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } + + public override bool can_write_image() { + return true; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return new TiffWriter(filepath); + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new TiffMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new TiffSniffer(file, options); + } +} + +private class TiffFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { + "tif", "tiff" + }; + + private static string[] KNOWN_MIME_TYPES = { + "image/tiff" + }; + + private static TiffFileFormatProperties instance = null; + + public static void init() { + instance = new TiffFileFormatProperties(); + } + + public static TiffFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.TIFF; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_default_extension() { + return "tif"; + } + + public override string get_user_visible_name() { + return _("TIFF"); + } + + public override string[] get_known_extensions() { + return KNOWN_EXTENSIONS; + } + + public override string get_default_mime_type() { + return KNOWN_MIME_TYPES[0]; + } + + public override string[] get_mime_types() { + return KNOWN_MIME_TYPES; + } +} + +private class TiffSniffer : GdkSniffer { + public TiffSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // Rely on GdkSniffer to detect corruption + is_corrupted = false; + + if (!is_tiff(file)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.TIFF) ? detected : null; + } +} + +private class TiffReader : GdkReader { + public TiffReader(string filepath) { + base (filepath, PhotoFileFormat.TIFF); + } +} + +private class TiffWriter : PhotoFileWriter { + private const string COMPRESSION_NONE = "1"; + private const string COMPRESSION_HUFFMAN = "2"; + private const string COMPRESSION_LZW = "5"; + private const string COMPRESSION_JPEG = "7"; + private const string COMPRESSION_DEFLATE = "8"; + + public TiffWriter(string filepath) { + base (filepath, PhotoFileFormat.TIFF); + } + + public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error { + pixbuf.save(get_filepath(), "tiff", "compression", COMPRESSION_LZW); + } +} + +private class TiffMetadataWriter : PhotoFileMetadataWriter { + public TiffMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.TIFF); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public bool is_tiff(File file, Cancellable? cancellable = null) throws Error { + DataInputStream dins = new DataInputStream(file.read()); + + // first two bytes: "II" (0x4949, for Intel) or "MM" (0x4D4D, for Motorola) + DataStreamByteOrder order; + switch (dins.read_uint16(cancellable)) { + case 0x4949: + order = DataStreamByteOrder.LITTLE_ENDIAN; + break; + + case 0x4D4D: + order = DataStreamByteOrder.BIG_ENDIAN; + break; + + default: + return false; + } + + dins.set_byte_order(order); + + // second two bytes: some random number + uint16 lue = dins.read_uint16(cancellable); + if (lue != 42) + return false; + + // remaining bytes are offset of first IFD, which doesn't matter for our purposes + return true; +} + +} diff --git a/src/photos/mk/photos.mk b/src/photos/mk/photos.mk new file mode 100644 index 0000000..6be33a4 --- /dev/null +++ b/src/photos/mk/photos.mk @@ -0,0 +1,38 @@ + +# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with +# a init() and terminate() function declared in the namespace. +UNIT_NAME := Photos + +# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all +# lowercase. The name of this file should be UNIT_DIR.mk. +UNIT_DIR := photos + +# All Vala files in the unit should be listed here with no subdirectory prefix. +# +# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala. +UNIT_FILES := \ + PhotoFileAdapter.vala \ + PhotoFileFormat.vala \ + PhotoFileSniffer.vala \ + PhotoMetadata.vala \ + GRaw.vala \ + GdkSupport.vala \ + JfifSupport.vala \ + BmpSupport.vala \ + RawSupport.vala \ + PngSupport.vala \ + TiffSupport.vala + +# Any unit this unit relies upon (and should be initialized before it's initialized) should +# be listed here using its Vala namespace. +# +# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here. +UNIT_USES := + +# List any additional files that are used in the build process as a part of this unit that should +# be packaged in the tarball. File names should be relative to the unit's home directory. +UNIT_RC := + +# unitize.mk must be called at the end of each UNIT_DIR.mk file. +include unitize.mk + |