summaryrefslogtreecommitdiff
path: root/src/photos
diff options
context:
space:
mode:
Diffstat (limited to 'src/photos')
-rw-r--r--src/photos/BmpSupport.vala187
-rw-r--r--src/photos/GRaw.vala307
-rw-r--r--src/photos/GdkSupport.vala132
-rw-r--r--src/photos/JfifSupport.vala239
-rw-r--r--src/photos/PhotoFileAdapter.vala112
-rw-r--r--src/photos/PhotoFileFormat.vala410
-rw-r--r--src/photos/PhotoFileSniffer.vala104
-rw-r--r--src/photos/PhotoMetadata.vala1169
-rw-r--r--src/photos/Photos.vala31
-rw-r--r--src/photos/PngSupport.vala184
-rw-r--r--src/photos/RawSupport.vala350
-rw-r--r--src/photos/TiffSupport.vala183
-rw-r--r--src/photos/mk/photos.mk38
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
+