diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2023-06-14 20:36:37 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2023-06-14 20:36:37 +0200 |
commit | bb80d3feebdc9acc52e3f4ad24084d8425f043a2 (patch) | |
tree | 2084a84c39f159c6aea254775dc0880d52579d45 /src/photos | |
parent | b26ff0798252a1a8072dd2c7a67f6205de9fde11 (diff) | |
parent | 31804433d72460cbe0a39f9f8ea5e76058d84cda (diff) |
Merge branch 'feature/upstream' into develop
Diffstat (limited to 'src/photos')
-rw-r--r-- | src/photos/AvifSupport.vala | 140 | ||||
-rw-r--r-- | src/photos/BmpSupport.vala | 27 | ||||
-rw-r--r-- | src/photos/GdkSupport.vala | 28 | ||||
-rw-r--r-- | src/photos/GifSupport.vala | 27 | ||||
-rw-r--r-- | src/photos/HeifSupport.vala | 150 | ||||
-rw-r--r-- | src/photos/JfifSupport.vala | 108 | ||||
-rw-r--r-- | src/photos/JpegXLSupport.vala | 149 | ||||
-rw-r--r-- | src/photos/PhotoFileFormat.vala | 72 | ||||
-rw-r--r-- | src/photos/PhotoFileSniffer.vala | 28 | ||||
-rw-r--r-- | src/photos/PhotoMetadata.vala | 258 | ||||
-rw-r--r-- | src/photos/PngSupport.vala | 27 | ||||
-rw-r--r-- | src/photos/RawSupport.vala | 19 | ||||
-rw-r--r-- | src/photos/TiffSupport.vala | 6 | ||||
-rw-r--r-- | src/photos/WebPSupport.vala | 240 |
14 files changed, 1094 insertions, 185 deletions
diff --git a/src/photos/AvifSupport.vala b/src/photos/AvifSupport.vala new file mode 100644 index 0000000..842f0fc --- /dev/null +++ b/src/photos/AvifSupport.vala @@ -0,0 +1,140 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +class AvifFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { "avif" }; + private static string[] KNOWN_MIME_TYPES = { "image/avif" }; + + private static AvifFileFormatProperties instance = null; + + public static void init() { + instance = new AvifFileFormatProperties(); + } + + public static AvifFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.AVIF; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_user_visible_name() { + return _("AVIF"); + } + + 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 AvifSniffer : GdkSniffer { + public AvifSniffer(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_supported_bmff_with_variants(file, {"avif", "avis"})) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.AVIF) ? detected : null; + } +} + +public class AvifReader : GdkReader { + public AvifReader(string filepath) { + base (filepath, PhotoFileFormat.AVIF); + } +} + +public class AvifWriter : PhotoFileWriter { + public AvifWriter(string filepath) { + base (filepath, PhotoFileFormat.AVIF); + } + + public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error { + pixbuf.save(get_filepath(), "avif", "quality", "90", null); + } +} + +public class AvifMetadataWriter : PhotoFileMetadataWriter { + public AvifMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.AVIF); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public class AvifFileFormatDriver : PhotoFileFormatDriver { + private static AvifFileFormatDriver instance = null; + + public static void init() { + instance = new AvifFileFormatDriver(); + AvifFileFormatProperties.init(); + } + + public static AvifFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return AvifFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new AvifReader(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 AvifWriter(filepath); + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new AvifMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new AvifSniffer(file, options); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } +} + diff --git a/src/photos/BmpSupport.vala b/src/photos/BmpSupport.vala index a59a4d9..26ec911 100644 --- a/src/photos/BmpSupport.vala +++ b/src/photos/BmpSupport.vala @@ -90,33 +90,6 @@ 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 { diff --git a/src/photos/GdkSupport.vala b/src/photos/GdkSupport.vala index f7e18d5..64a08d6 100644 --- a/src/photos/GdkSupport.vala +++ b/src/photos/GdkSupport.vala @@ -21,7 +21,30 @@ public abstract class GdkReader : PhotoFileReader { } 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); + 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; } } @@ -112,13 +135,14 @@ public abstract class GdkSniffer : PhotoFileSniffer { 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(); + debug("Pixbuf detected format name: %s", detected.format_name); detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name); area_prepared = true; diff --git a/src/photos/GifSupport.vala b/src/photos/GifSupport.vala index bd6ef6a..b49b4f2 100644 --- a/src/photos/GifSupport.vala +++ b/src/photos/GifSupport.vala @@ -86,33 +86,6 @@ public class GifReader : GdkReader { public GifReader(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 GifMetadataWriter : PhotoFileMetadataWriter { diff --git a/src/photos/HeifSupport.vala b/src/photos/HeifSupport.vala new file mode 100644 index 0000000..0c05e02 --- /dev/null +++ b/src/photos/HeifSupport.vala @@ -0,0 +1,150 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +class HeifFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { "heif", "heic" }; + private static string[] KNOWN_MIME_TYPES = { "image/heif" }; + + private static HeifFileFormatProperties instance = null; + + public static void init() { + instance = new HeifFileFormatProperties(); + } + + public static HeifFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.HEIF; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_user_visible_name() { + return _("HEIF"); + } + + 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 HeifSniffer : GdkSniffer { + private const string[] MAGIC_SEQUENCES = { "heic", "heix", "hevc", "heim", "heis", "hevm", "hevs", "mif1", "msf1"}; + + public HeifSniffer(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_supported_bmff_with_variants(file, MAGIC_SEQUENCES)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + if (detected.file_format == PhotoFileFormat.AVIF) + detected.file_format = PhotoFileFormat.HEIF; + + // Heif contains its own rotation information, so we need to ignore the EXIF rotation+ + if (detected.metadata != null) { + detected.metadata.set_orientation(Orientation.TOP_LEFT); + } + + return (detected.file_format == PhotoFileFormat.HEIF) ? detected : null; + } + +} + +public class HeifReader : GdkReader { + public HeifReader(string filepath) { + base (filepath, PhotoFileFormat.HEIF); + } + + public override PhotoMetadata read_metadata() throws Error { + PhotoMetadata metadata = new PhotoMetadata(); + metadata.read_from_file(get_file()); + // Heif contains its own rotation information, so we need to ignore the EXIF rotation + metadata.set_orientation(Orientation.TOP_LEFT); + return metadata; + } + +} + +public class HeifMetadataWriter : PhotoFileMetadataWriter { + public HeifMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.HEIF); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public class HeifFileFormatDriver : PhotoFileFormatDriver { + private static HeifFileFormatDriver instance = null; + + public static void init() { + instance = new HeifFileFormatDriver(); + HeifFileFormatProperties.init(); + } + + public static HeifFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return HeifFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new HeifReader(filepath); + } + + public override bool can_write_image() { + return false; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return null; + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new HeifMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new HeifSniffer(file, options); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } +} + diff --git a/src/photos/JfifSupport.vala b/src/photos/JfifSupport.vala index 5ea64a5..0de45f8 100644 --- a/src/photos/JfifSupport.vala +++ b/src/photos/JfifSupport.vala @@ -103,17 +103,78 @@ public class JfifSniffer : GdkSniffer { } 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) + if (!calc_md5) { + return fast_sniff (out is_corrupted); + } else { + if (!Jpeg.is_jpeg(file)) { + return null; + } + + // Rely on GdkSniffer to detect corruption + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null; + } + } + + private DetectedPhotoInformation? fast_sniff(out bool is_corrupted) throws Error { + is_corrupted = false; + var detected = new DetectedPhotoInformation(); + + detected.metadata = new PhotoMetadata(); + try { + detected.metadata.read_from_file(file); + } catch (Error err) { + // no metadata detected + detected.metadata = null; + } + + var fins = file.read(null); + var dins = new DataInputStream(fins); + dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + var seekable = (Seekable) dins; + + var marker = Jpeg.Marker.INVALID; + var length = Jpeg.read_marker_2(dins, out marker); + + if (marker != Jpeg.Marker.SOI) { return null; - - return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null; + } + + length = Jpeg.read_marker_2(dins, out marker); + while (!marker.is_sof() && length > 0) { + seekable.seek(length, SeekType.CUR, null); + length = Jpeg.read_marker_2(dins, out marker); + } + + if (marker.is_sof()) { + if (length < 6) { + is_corrupted = true; + return null; + } + + // Skip precision + dins.read_byte(); + + // Next two 16 bytes are image dimensions + uint16 height = dins.read_uint16(); + uint16 width = dins.read_uint16(); + + detected.image_dim = Dimensions(width, height); + detected.colorspace = Gdk.Colorspace.RGB; + detected.channels = 3; + detected.bits_per_channel = 8; + detected.format_name = "jpeg"; + detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name); + } else { + is_corrupted = true; + } + + return detected; } } @@ -159,6 +220,16 @@ namespace Jpeg { public uint8 get_byte() { return (uint8) this; } + + public bool is_sof() { + // FFCn is SOF unless n is a multiple of 4 > 0 (FFC4, FFC8, FFCC) + if ((this & 0xC0) != 0xC0) { + return false; + } + + var variant = this & 0x0F; + return variant == 0 || variant % 4 != 0; + } } public enum Quality { @@ -219,12 +290,9 @@ namespace Jpeg { return is_jpeg_stream(mins); } - private int read_marker(InputStream fins, out Jpeg.Marker marker) throws Error { + private int32 read_marker_2(DataInputStream dins, 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; @@ -235,9 +303,10 @@ namespace Jpeg { } uint16 length = dins.read_uint16(); - if (length < 2 && fins is Seekable) { + var seekable = dins as Seekable; + if (length < 2 && dins != null) { debug("Invalid length %Xh at ofs %" + int64.FORMAT + "Xh", length, - (fins as Seekable).tell() - 2); + seekable.tell() - 2); return -1; } @@ -245,5 +314,12 @@ namespace Jpeg { // account for two length bytes already read return length - 2; } + + private int read_marker(InputStream fins, out Jpeg.Marker marker) throws Error { + DataInputStream dins = new DataInputStream(fins); + dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + + return read_marker_2(dins, out marker); + } } diff --git a/src/photos/JpegXLSupport.vala b/src/photos/JpegXLSupport.vala new file mode 100644 index 0000000..eed220c --- /dev/null +++ b/src/photos/JpegXLSupport.vala @@ -0,0 +1,149 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +class JpegXLFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { "jxl", "jpegxl" }; + private static string[] KNOWN_MIME_TYPES = { "image/jxl" }; + + private static JpegXLFileFormatProperties instance = null; + + public static void init() { + instance = new JpegXLFileFormatProperties(); + } + + public static JpegXLFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.JPEGXL; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_user_visible_name() { + return _("JPEGXL"); + } + + 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 JpegXLSniffer : GdkSniffer { + // See https://github.com/ImageMagick/jpeg-xl/blob/main/doc/format_overview.md#file-format + private const uint8[] CODESTREAM_MAGIC_SEQUENCE = { 0xff, 0x0a }; + private const uint8[] BMFF_MAGIC_SEQUENCE = {0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A}; + + + public JpegXLSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + } + + private static bool is_jpegxl_file(File file) throws Error { + FileInputStream instream = file.read(null); + + // Read out first four bytes + uint8[] file_lead_sequence = new uint8[BMFF_MAGIC_SEQUENCE.length]; + + var size = instream.read(file_lead_sequence, null); + + return size == BMFF_MAGIC_SEQUENCE.length && (Memory.cmp(CODESTREAM_MAGIC_SEQUENCE, file_lead_sequence, CODESTREAM_MAGIC_SEQUENCE.length) == 0 || + Memory.cmp(BMFF_MAGIC_SEQUENCE, file_lead_sequence, BMFF_MAGIC_SEQUENCE.length) == 0); + + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + // Rely on GdkSniffer to detect corruption + is_corrupted = false; + + if (!is_jpegxl_file(file)) + return null; + + DetectedPhotoInformation? detected = base.sniff(out is_corrupted); + if (detected == null) + return null; + + return (detected.file_format == PhotoFileFormat.JPEGXL) ? detected : null; + } + +} + +public class JpegXLReader : GdkReader { + public JpegXLReader(string filepath) { + base (filepath, PhotoFileFormat.JPEGXL); + } +} + +public class JpegXLMetadataWriter : PhotoFileMetadataWriter { + public JpegXLMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.JPEGXL); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public class JpegXLFileFormatDriver : PhotoFileFormatDriver { + private static JpegXLFileFormatDriver instance = null; + + public static void init() { + instance = new JpegXLFileFormatDriver(); + JpegXLFileFormatProperties.init(); + } + + public static JpegXLFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return JpegXLFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new JpegXLReader(filepath); + } + + public override bool can_write_image() { + return false; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return null; + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new JpegXLMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new JpegXLSniffer(file, options); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } +} + diff --git a/src/photos/PhotoFileFormat.vala b/src/photos/PhotoFileFormat.vala index e642008..4c69de3 100644 --- a/src/photos/PhotoFileFormat.vala +++ b/src/photos/PhotoFileFormat.vala @@ -58,12 +58,16 @@ public enum PhotoFileFormat { TIFF, BMP, GIF, + WEBP, + AVIF, + HEIF, + JPEGXL, 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, GIF }; + return { JFIF, RAW, PNG, TIFF, BMP, GIF, WEBP, AVIF, HEIF, JPEGXL }; } public static PhotoFileFormat[] get_writeable() { @@ -141,7 +145,19 @@ public enum PhotoFileFormat { case GIF: return 5; - + + case WEBP: + return 6; + + case AVIF: + return 7; + + case HEIF: + return 8; + + case JPEGXL: + return 9; + case UNKNOWN: default: return -1; @@ -169,6 +185,18 @@ public enum PhotoFileFormat { case 5: return GIF; + case 6: + return WEBP; + + case 7: + return AVIF; + + case 8: + return HEIF; + + case 9: + return JPEGXL; + default: return UNKNOWN; } @@ -217,7 +245,17 @@ public enum PhotoFileFormat { case "gif": return PhotoFileFormat.GIF; - + + case "heif/avif": + case "avif": + return PhotoFileFormat.AVIF; + + case "heif": + return PhotoFileFormat.HEIF; + + case "jxl": + return PhotoFileFormat.JPEGXL; + default: return PhotoFileFormat.UNKNOWN; } @@ -249,6 +287,22 @@ public enum PhotoFileFormat { Photos.GifFileFormatDriver.init(); break; + case WEBP: + Photos.WebpFileFormatDriver.init(); + break; + + case AVIF: + AvifFileFormatDriver.init(); + break; + + case HEIF: + HeifFileFormatDriver.init(); + break; + + case JPEGXL: + JpegXLFileFormatDriver.init(); + break; + default: error("Unsupported file format %s", this.to_string()); } @@ -274,6 +328,18 @@ public enum PhotoFileFormat { case GIF: return Photos.GifFileFormatDriver.get_instance(); + case WEBP: + return Photos.WebpFileFormatDriver.get_instance(); + + case AVIF: + return AvifFileFormatDriver.get_instance(); + + case HEIF: + return HeifFileFormatDriver.get_instance(); + + case JPEGXL: + return JpegXLFileFormatDriver.get_instance(); + default: error("Unsupported file format %s", this.to_string()); } diff --git a/src/photos/PhotoFileSniffer.vala b/src/photos/PhotoFileSniffer.vala index 7442fde..6358920 100644 --- a/src/photos/PhotoFileSniffer.vala +++ b/src/photos/PhotoFileSniffer.vala @@ -47,6 +47,34 @@ public abstract class PhotoFileSniffer { } public abstract DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error; + + protected static bool is_supported_bmff_with_variants(File file, string[] variants) throws Error { + + FileInputStream instream = file.read(null); + + // Skip the first four bytes + if (instream.skip(4) != 4) { + return false; + } + + // The next four bytes need to be ftyp + var buf = new uint8[4]; + if (instream.read(buf, null) != 4) { + return false; + } + + if (Memory.cmp("ftyp".data, buf, 4) != 0) { + return false; + } + + if (instream.read(buf, null) != 4) { + return false; + } + + buf += '\0'; + + return (string)buf in variants; + } } // diff --git a/src/photos/PhotoMetadata.vala b/src/photos/PhotoMetadata.vala index a9b7457..3bf77d6 100644 --- a/src/photos/PhotoMetadata.vala +++ b/src/photos/PhotoMetadata.vala @@ -241,9 +241,13 @@ public class PhotoMetadata : MediaMetadata { public override Bytes flatten() throws Error { unowned GExiv2.PreviewProperties?[] props = owner.exiv2.get_preview_properties(); assert(props != null && props.length > number); - - return new - Bytes(owner.exiv2.get_preview_image(props[number]).get_data()); + + try { + return new + Bytes(owner.exiv2.try_get_preview_image(props[number]).get_data()); + } catch (Error err) { + return new Bytes(null); + } } } @@ -278,12 +282,8 @@ public class PhotoMetadata : MediaMetadata { exiv2 = new GExiv2.Metadata(); exif = null; -#if NEW_GEXIV2_API exiv2.open_buf(buffer[0:length]); -#else - exiv2.open_buf(buffer, length); -#endif - exif = Exif.Data.new_from_data(buffer); + exif = Exif.Data.new_from_data(buffer[0:length]); source_name = "<memory buffer %d bytes>".printf(length); } @@ -291,11 +291,8 @@ public class PhotoMetadata : MediaMetadata { exiv2 = new GExiv2.Metadata(); exif = null; -#if NEW_GEXIV2_API exiv2.from_app1_segment(buffer.get_data()); -#else exif = Exif.Data.new_from_data(buffer.get_data()); -#endif source_name = "<app1 segment %zu bytes>".printf(buffer.get_size()); } @@ -371,7 +368,11 @@ public class PhotoMetadata : MediaMetadata { } public bool has_tag(string tag) { - return exiv2.has_tag(tag); + try { + return exiv2.try_has_tag(tag); + } catch (Error error) { + return false; + } } private Gee.Set<string> create_string_set(owned CompareDataFunc<string>? compare_func) { @@ -397,6 +398,9 @@ public class PhotoMetadata : MediaMetadata { case MetadataDomain.IPTC: tags = exiv2.get_iptc_tags(); break; + default: + // Just ignore any other unknown tags + break; } if (tags == null || tags.length == 0) @@ -429,19 +433,35 @@ public class PhotoMetadata : MediaMetadata { } public string? get_tag_label(string tag) { - return GExiv2.Metadata.get_tag_label(tag); + try { + return GExiv2.Metadata.try_get_tag_label(tag); + } catch (Error error) { + return null; + } } public string? get_tag_description(string tag) { - return GExiv2.Metadata.get_tag_description(tag); + try { + return GExiv2.Metadata.try_get_tag_description(tag); + } catch (Error error) { + return null; + } } 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); + try { + return prepare_input_text(exiv2.try_get_tag_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH); + } catch (Error error) { + return null; + } } 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); + try { + return prepare_input_text(exiv2.try_get_tag_interpreted_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH); + } catch (Error error) { + return null; + } } public string? get_first_string(string[] tags) { @@ -469,26 +489,30 @@ public class PhotoMetadata : MediaMetadata { // 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 + // https://gitlab.gnome.org/GNOME/gexiv2/issues/10 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); + try { + string[] values = exiv2.try_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; + } catch (Error error) { + return null; } - - return list.size > 0 ? list : null; } // Returns a List that has been filtered through a Set, so no duplicates will be found. @@ -496,7 +520,7 @@ public class PhotoMetadata : MediaMetadata { // 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 + // https://gitlab.gnome.org/GNOME/gexiv2/issues/10 public Gee.List<string>? get_first_string_multiple(string[] tags) { foreach (string tag in tags) { Gee.List<string>? values = get_string_multiple(tag); @@ -507,16 +531,20 @@ public class PhotoMetadata : MediaMetadata { 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); + public void set_string(string tag, string value, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS, + int length = DEFAULT_USER_TEXT_INPUT_LENGTH) { + string? prepped = prepare_input_text(value, options, 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); + try { + exiv2.try_set_tag_string(tag, prepped); + } catch (Error error) { + warning("Unable to set tag %s to string %s from source %s: %s", tag, value, source_name, error.message); + } } private delegate void SetGenericValue(string tag); @@ -562,13 +590,16 @@ public class PhotoMetadata : MediaMetadata { 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. + // workaround for https://bugzilla.gnome.org/show_bug.cgi?id=712479. See also + // https://bugzilla.gnome.org/show_bug.cgi?id=717438, 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); + try { + exiv2.try_set_tag_multiple(tag, values); + } catch (Error err) { + warning("Unable to set %d strings to tag %s from source %s: %s", values.length, tag, source_name, err.message); + } } public void set_all_string_multiple(string[] tags, Gee.Collection<string> values, SetOption option) { @@ -576,13 +607,16 @@ public class PhotoMetadata : MediaMetadata { } public bool get_long(string tag, out long value) { + value = 0; if (!has_tag(tag)) { - value = 0; - return false; } - value = exiv2.get_tag_long(tag); + try { + value = exiv2.try_get_tag_long(tag); + } catch (Error error) { + return false; + } return true; } @@ -599,8 +633,11 @@ public class PhotoMetadata : MediaMetadata { } 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); + try { + exiv2.try_set_tag_long(tag, value); + } catch (Error err) { + warning("Unable to set tag %s to long %ld from source %s: %s", tag, value, source_name, err.message); + } } public void set_all_long(string[] tags, long value, SetOption option) { @@ -609,11 +646,19 @@ public class PhotoMetadata : MediaMetadata { 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; + try { + if (exiv2.try_get_exif_tag_rational(tag, out numerator, out denominator)) { + rational = MetadataRational(numerator, denominator); + } else { + rational = MetadataRational.invalid(); + return false; + } + } catch (Error error) { + rational = MetadataRational.invalid(); + return false; + } + + return true; } public bool get_first_rational(string[] tags, out MetadataRational rational) { @@ -628,9 +673,11 @@ public class PhotoMetadata : MediaMetadata { } 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); + try { + exiv2.try_set_exif_tag_rational(tag, rational.numerator, rational.denominator); + } catch (Error err) { + warning("Unable to set tag %s to rational %s from source %s: %s", tag, rational.to_string(), + source_name, err.message); } } @@ -769,7 +816,10 @@ public class PhotoMetadata : MediaMetadata { } public void remove_exif_thumbnail() { - exiv2.erase_exif_thumbnail(); + try { + exiv2.try_erase_exif_thumbnail(); + } catch (Error err) { } + if (exif != null) { Exif.Mem.new_default().free(exif.data); exif.data = null; @@ -778,7 +828,9 @@ public class PhotoMetadata : MediaMetadata { } public void remove_tag(string tag) { - exiv2.clear_tag(tag); + try { + exiv2.try_clear_tag(tag); + } catch (Error err){} } public void remove_tags(string[] tags) { @@ -799,6 +851,9 @@ public class PhotoMetadata : MediaMetadata { case MetadataDomain.IPTC: exiv2.clear_iptc(); break; + default: + // Just ignore any unknown tags + break; } } @@ -881,7 +936,7 @@ public class PhotoMetadata : MediaMetadata { public static string[] HEIGHT_TAGS = { "Exif.Photo.PixelYDimension", "Xmp.exif.PixelYDimension", - "Xmp.tiff.ImageHeight", + "Xmp.tiff.ImageLength", "Xmp.exif.PixelYDimension" }; @@ -923,7 +978,7 @@ public class PhotoMetadata : MediaMetadata { // (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 + // See: https://wiki.gnome.org/Apps/Shotwell/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, @@ -997,8 +1052,9 @@ public class PhotoMetadata : MediaMetadata { * newlines from comments */ if (!is_string_empty(comment)) set_all_generic(COMMENT_TAGS, option, (tag) => { + // 4095 is coming from acdsee.notes which is limited to that set_string(tag, comment, PREPARE_STRING_OPTIONS & - ~PrepareInputTextOptions.STRIP_CRLF); + ~PrepareInputTextOptions.STRIP_CRLF, 4095); }); else remove_tags(COMMENT_TAGS); @@ -1139,24 +1195,37 @@ public class PhotoMetadata : MediaMetadata { } public bool has_orientation() { - return exiv2.get_orientation() == GExiv2.Orientation.UNSPECIFIED; + try { + return exiv2.try_get_orientation() == GExiv2.Orientation.UNSPECIFIED; + } catch (Error err) { + debug("Failed to get orientation: %s", err.message); + return false; + } } // 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) + try { + GExiv2.Orientation orientation = exiv2.try_get_orientation(); + if (orientation == GExiv2.Orientation.UNSPECIFIED || orientation < Orientation.MIN || + orientation > Orientation.MAX) + return Orientation.TOP_LEFT; + else + return (Orientation) orientation; + } catch (Error error) { 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); + try { + exiv2.try_set_orientation((GExiv2.Orientation) orientation); + } catch (Error err) { + debug("Failed to set the orientation: %s", err.message); + } } public bool get_gps(out double longitude, out string long_ref, out double latitude, out string lat_ref, @@ -1164,14 +1233,22 @@ public class PhotoMetadata : MediaMetadata { longitude = 0.0; latitude = 0.0; altitude = 0.0; - if (!exiv2.get_gps_longitude(out longitude) || !exiv2.get_gps_latitude(out latitude)) { - long_ref = null; - lat_ref = null; - - return false; + try { + if (!exiv2.try_get_gps_longitude(out longitude) || !exiv2.try_get_gps_latitude(out latitude)) { + long_ref = null; + lat_ref = null; + + return false; + } + } catch (Error err) { + debug("Failed to get GPS lon/lat: %s", err.message); } - exiv2.get_gps_altitude(out altitude); + try { + exiv2.try_get_gps_altitude(out altitude); + } catch (Error err) { + debug("Failed to get GPS altitude: %s", err.message); + } long_ref = get_string("Exif.GPSInfo.GPSLongitudeRef"); lat_ref = get_string("Exif.GPSInfo.GPSLatitudeRef"); @@ -1179,6 +1256,37 @@ public class PhotoMetadata : MediaMetadata { return true; } + public GpsCoords get_gps_coords() { + GpsCoords gps_coords = GpsCoords(); + try { + double altitude; + gps_coords.has_gps = exiv2.try_get_gps_info(out gps_coords.longitude, out gps_coords.latitude, out altitude) ? 1 : 0; + if (gps_coords.has_gps > 0) { + if (get_string("Exif.GPSInfo.GPSLongitudeRef") == "W" && gps_coords.longitude > 0) + gps_coords.longitude = -gps_coords.longitude; + if (get_string("Exif.GPSInfo.GPSLatitudeRef") == "S" && gps_coords.latitude > 0) + gps_coords.latitude = -gps_coords.latitude; + } + } catch (Error err) { + gps_coords.has_gps = 0; + } + + return gps_coords; + } + + public void set_gps_coords(GpsCoords gps_coords) { + try { + if (gps_coords.has_gps > 0) { + var altitude = 0.0; + exiv2.try_get_gps_altitude(out altitude); + exiv2.try_set_gps_info(gps_coords.longitude, gps_coords.latitude, altitude); + } else + exiv2.try_delete_gps_info(); + } catch (Error err) { + debug("Failed to set or remove GPS info: %s", err.message); + } + } + public bool get_exposure(out MetadataRational exposure) { return get_rational("Exif.Photo.ExposureTime", out exposure); } @@ -1326,7 +1434,7 @@ public class PhotoMetadata : MediaMetadata { // 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. + // different meaning. See https://wiki.gnome.org/Apps/Shotwell/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()); diff --git a/src/photos/PngSupport.vala b/src/photos/PngSupport.vala index c891136..e154fc4 100644 --- a/src/photos/PngSupport.vala +++ b/src/photos/PngSupport.vala @@ -88,33 +88,6 @@ 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 { diff --git a/src/photos/RawSupport.vala b/src/photos/RawSupport.vala index 8c23826..538c949 100644 --- a/src/photos/RawSupport.vala +++ b/src/photos/RawSupport.vala @@ -51,7 +51,7 @@ public class RawFileFormatDriver : PhotoFileFormatDriver { public class RawFileFormatProperties : PhotoFileFormatProperties { private static string[] KNOWN_EXTENSIONS = { - "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cap", "iiq", "eip", "dcs", "dcr", "drf", + "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cr3", "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" }; @@ -63,6 +63,7 @@ public class RawFileFormatProperties : PhotoFileFormatProperties { /* manufacturer blessed MIME types */ "image/x-canon-cr2", + "image/x-canon-cr3", "image/x-canon-crw", "image/x-fuji-raf", "image/x-adobe-dng", @@ -85,6 +86,7 @@ public class RawFileFormatProperties : PhotoFileFormatProperties { "image/x-bay", "image/x-crw", "image/x-cr2", + "image/x-cr3", "image/x-cap", "image/x-iiq", "image/x-eip", @@ -174,7 +176,6 @@ public class RawSniffer : PhotoFileSniffer { 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) @@ -195,7 +196,7 @@ public class RawSniffer : PhotoFileSniffer { // ignored } - if (detected.metadata != null) { + if (calc_md5 && detected.metadata != null) { detected.exif_md5 = detected.metadata.exif_hash(); detected.thumbnail_md5 = detected.metadata.thumbnail_hash(); } @@ -211,15 +212,19 @@ public class RawSniffer : PhotoFileSniffer { } public class RawReader : PhotoFileReader { + private PhotoMetadata? cached_metadata = null; + 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; + if (cached_metadata == null) { + PhotoMetadata metadata = new PhotoMetadata(); + metadata.read_from_file(get_file()); + cached_metadata = metadata; + } + return cached_metadata; } public override Gdk.Pixbuf unscaled_read() throws Error { diff --git a/src/photos/TiffSupport.vala b/src/photos/TiffSupport.vala index 7ed8b98..cadcd0e 100644 --- a/src/photos/TiffSupport.vala +++ b/src/photos/TiffSupport.vala @@ -151,6 +151,9 @@ private class TiffMetadataWriter : PhotoFileMetadataWriter { } } +private const uint16 FILE_MARKER_TIFF = 42; +private const uint16 FILE_MARKER_BIGTIFF = 43; + public bool is_tiff(File file, Cancellable? cancellable = null) throws Error { DataInputStream dins = new DataInputStream(file.read()); @@ -173,8 +176,9 @@ public bool is_tiff(File file, Cancellable? cancellable = null) throws Error { // second two bytes: some random number uint16 lue = dins.read_uint16(cancellable); - if (lue != 42) + if (lue != FILE_MARKER_TIFF && lue != FILE_MARKER_BIGTIFF) { return false; + } // remaining bytes are offset of first IFD, which doesn't matter for our purposes return true; diff --git a/src/photos/WebPSupport.vala b/src/photos/WebPSupport.vala new file mode 100644 index 0000000..2f4723c --- /dev/null +++ b/src/photos/WebPSupport.vala @@ -0,0 +1,240 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +namespace Photos { + +public class WebpFileFormatDriver : PhotoFileFormatDriver { + private static WebpFileFormatDriver instance = null; + + public static void init() { + instance = new WebpFileFormatDriver(); + WebpFileFormatProperties.init(); + } + + public static WebpFileFormatDriver get_instance() { + return instance; + } + + public override PhotoFileFormatProperties get_properties() { + return WebpFileFormatProperties.get_instance(); + } + + public override PhotoFileReader create_reader(string filepath) { + return new WebpReader(filepath); + } + + public override PhotoMetadata create_metadata() { + return new PhotoMetadata(); + } + + public override bool can_write_image() { + return false; + } + + public override bool can_write_metadata() { + return true; + } + + public override PhotoFileWriter? create_writer(string filepath) { + return null; + } + + public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) { + return new WebpMetadataWriter(filepath); + } + + public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) { + return new WebpSniffer(file, options); + } +} + +private class WebpFileFormatProperties : PhotoFileFormatProperties { + private static string[] KNOWN_EXTENSIONS = { + "webp" + }; + + private static string[] KNOWN_MIME_TYPES = { + "image/webp" + }; + + private static WebpFileFormatProperties instance = null; + + public static void init() { + instance = new WebpFileFormatProperties(); + } + + public static WebpFileFormatProperties get_instance() { + return instance; + } + + public override PhotoFileFormat get_file_format() { + return PhotoFileFormat.WEBP; + } + + public override PhotoFileFormatFlags get_flags() { + return PhotoFileFormatFlags.NONE; + } + + public override string get_default_extension() { + return "webp"; + } + + public override string get_user_visible_name() { + return _("WebP"); + } + + 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 WebpSniffer : PhotoFileSniffer { + private DetectedPhotoInformation detected = null; + + public WebpSniffer(File file, PhotoFileSniffer.Options options) { + base (file, options); + detected = new DetectedPhotoInformation(); + } + + public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error { + is_corrupted = false; + + if (!is_webp(file)) + return null; + + // 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) { + debug("Failed to load meta-data from file: %s", err.message); + // no metadata detected + detected.metadata = null; + } + + if (calc_md5 && detected.metadata != null) { + detected.exif_md5 = detected.metadata.exif_hash(); + detected.thumbnail_md5 = detected.metadata.thumbnail_hash(); + } + + // 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); + var ba = new ByteArray(); + for (;;) { + size_t bytes_read = fins.read(buffer, null); + if (bytes_read <= 0) + break; + + ba.append(buffer[0:bytes_read]); + + count += bytes_read; + + if (calc_md5) + md5_checksum.update(buffer, bytes_read); + + WebP.Data d = WebP.Data(); + d.bytes = ba.data; + + WebP.ParsingState state; + var demux = new WebP.Demuxer.partial(d, out state); + + if (state == WebP.ParsingState.PARSE_ERROR) { + is_corrupted = true; + break; + } + + if (state > WebP.ParsingState.PARSED_HEADER) { + detected.file_format = PhotoFileFormat.WEBP; + detected.format_name = "WebP"; + detected.channels = 4; + detected.bits_per_channel = 8; + detected.image_dim.width = (int) demux.get(WebP.FormatFeature.CANVAS_WIDTH); + detected.image_dim.height = (int) demux.get(WebP.FormatFeature.CANVAS_HEIGHT); + + // if not searching for anything else, exit + if (!calc_md5) + break; + } + } + + if (fins != null) + fins.close(null); + + if (calc_md5) + detected.md5 = md5_checksum.get_string(); + + return detected; + } +} + +private class WebpReader : PhotoFileReader { + public WebpReader(string filepath) { + base (filepath, PhotoFileFormat.WEBP); + } + + 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 { + uint8[] buffer; + + FileUtils.get_data(this.get_filepath(), out buffer); + int width, height; + var pixdata = WebP.DecodeRGBA(buffer, out width, out height); + pixdata.length = width * height * 4; + + return new Gdk.Pixbuf.from_data(pixdata, Gdk.Colorspace.RGB, true, 8, width, height, width * 4); + } +} + +private class WebpMetadataWriter : PhotoFileMetadataWriter { + public WebpMetadataWriter(string filepath) { + base (filepath, PhotoFileFormat.WEBP); + } + + public override void write_metadata(PhotoMetadata metadata) throws Error { + metadata.write_to_file(get_file()); + } +} + +public bool is_webp(File file, Cancellable? cancellable = null) throws Error { + var ins = file.read(); + + uint8 buffer[12]; + try { + ins.read(buffer, null); + if (buffer[0] == 'R' && buffer[1] == 'I' && buffer[2] == 'F' && buffer[3] == 'F' && + buffer[8] == 'W' && buffer[9] == 'E' && buffer[10] == 'B' && buffer[11] == 'P') + return true; + } catch (Error error) { + debug ("Failed to read from file %s: %s", file.get_path (), error.message); + } + + return false; +} + +} |