summaryrefslogtreecommitdiff
path: root/src/photos
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2023-06-28 21:35:52 +0200
committerJörg Frings-Fürst <debian@jff.email>2023-06-28 21:35:52 +0200
commitb86540b743f1a87a163ffb811c8fe22a01fefa38 (patch)
treeb47cb3bb83c2377234226fb3987ab3320a987dd9 /src/photos
parentac6e0b731b9f0b2efd392e3309a5c07e2a66adad (diff)
parente905d8e16eec152d19797937f13ba3cf4b8f8aca (diff)
Merge branch 'release/debian/0.32.1-1'debian/0.32.1-1
Diffstat (limited to 'src/photos')
-rw-r--r--src/photos/AvifSupport.vala140
-rw-r--r--src/photos/BmpSupport.vala27
-rw-r--r--src/photos/GdkSupport.vala28
-rw-r--r--src/photos/GifSupport.vala27
-rw-r--r--src/photos/HeifSupport.vala150
-rw-r--r--src/photos/JfifSupport.vala108
-rw-r--r--src/photos/JpegXLSupport.vala149
-rw-r--r--src/photos/PhotoFileFormat.vala72
-rw-r--r--src/photos/PhotoFileSniffer.vala28
-rw-r--r--src/photos/PhotoMetadata.vala258
-rw-r--r--src/photos/PngSupport.vala27
-rw-r--r--src/photos/RawSupport.vala19
-rw-r--r--src/photos/TiffSupport.vala6
-rw-r--r--src/photos/WebPSupport.vala240
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;
+}
+
+}