From: Jens Georg Date: Wed, 30 Aug 2017 21:46:55 +0200 Subject: Support reading WEBP https://bugzilla.gnome.org/show_bug.cgi?id=717880 Requires a gexiv2 linked against exiv2 0.26 which currently works in the flatpak and on F28, but NOT on Debian/Ubuntu 18.04 (cherry picked from commit f032a58dca391b1833c6ea70785bb3b63abc68c7) --- meson.build | 3 + src/meson.build | 3 +- src/photos/PhotoFileFormat.vala | 18 ++- src/photos/WebPSupport.vala | 240 ++++++++++++++++++++++++++++++++++++++++ vapi/libwebp.vapi | 5 + vapi/libwebpdemux.vapi | 43 +++++++ 6 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 src/photos/WebPSupport.vala create mode 100644 vapi/libwebp.vapi create mode 100644 vapi/libwebpdemux.vapi diff --git a/meson.build b/meson.build index 5d08d30..2316377 100644 --- a/meson.build +++ b/meson.build @@ -66,6 +66,9 @@ libexif = dependency('libexif', version : '>= 0.6.16') unity = dependency('unity', required : false) portal = [ dependency('libportal', version: '>= 0.5'), dependency('libportal-gtk3', version: '>= 0.5')] +webpdemux = dependency('libwebpdemux') +webp = dependency('libwebp') + unity_available = false if unity.found() and get_option('unity-support') unity_available = true diff --git a/src/meson.build b/src/meson.build index a532eec..8cab77d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -29,7 +29,7 @@ face_sources = (['faces/FacesBranch.vala', shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2, gstreamer_pbu, gio_unix, gudev, gexiv2, gmodule, - libraw, libexif, sw_plugin, portal, version] + libraw, libexif, sw_plugin, portal, version, webpdemux, webp] if unity_available shotwell_deps += [unity] endif @@ -73,6 +73,7 @@ executable('shotwell', 'photos/RawSupport.vala', 'photos/PngSupport.vala', 'photos/TiffSupport.vala', + 'photos/WebPSupport.vala', 'plugins/Plugins.vala', 'plugins/StandardHostInterface.vala', 'plugins/ManifestWidget.vala', diff --git a/src/photos/PhotoFileFormat.vala b/src/photos/PhotoFileFormat.vala index e642008..94ca752 100644 --- a/src/photos/PhotoFileFormat.vala +++ b/src/photos/PhotoFileFormat.vala @@ -58,12 +58,13 @@ public enum PhotoFileFormat { TIFF, BMP, GIF, + WEBP, 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 }; } public static PhotoFileFormat[] get_writeable() { @@ -141,7 +142,10 @@ public enum PhotoFileFormat { case GIF: return 5; - + + case WEBP: + return 6; + case UNKNOWN: default: return -1; @@ -169,6 +173,9 @@ public enum PhotoFileFormat { case 5: return GIF; + case 6: + return WEBP; + default: return UNKNOWN; } @@ -249,6 +256,10 @@ public enum PhotoFileFormat { Photos.GifFileFormatDriver.init(); break; + case WEBP: + Photos.WebpFileFormatDriver.init(); + break; + default: error("Unsupported file format %s", this.to_string()); } @@ -274,6 +285,9 @@ public enum PhotoFileFormat { case GIF: return Photos.GifFileFormatDriver.get_instance(); + case WEBP: + return Photos.WebpFileFormatDriver.get_instance(); + default: error("Unsupported file format %s", this.to_string()); } diff --git a/src/photos/WebPSupport.vala b/src/photos/WebPSupport.vala new file mode 100644 index 0000000..093f196 --- /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.TIFF); + } + + 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; +} + +} diff --git a/vapi/libwebp.vapi b/vapi/libwebp.vapi new file mode 100644 index 0000000..a19fbcf --- /dev/null +++ b/vapi/libwebp.vapi @@ -0,0 +1,5 @@ +[CCode (cheader_filename = "webp/decode.h")] +namespace WebP { + [CCode (array_length = false, cname="WebPDecodeRGBA")] + public static uint8[] DecodeRGBA([CCode (array_length_pos=1)]uint8[] data, out int width, out int height); +} diff --git a/vapi/libwebpdemux.vapi b/vapi/libwebpdemux.vapi new file mode 100644 index 0000000..7612b42 --- /dev/null +++ b/vapi/libwebpdemux.vapi @@ -0,0 +1,43 @@ +namespace WebP { + [CCode (has_type_id = false)] + public struct Data { + [CCode (array_length_cname = "size")] + public unowned uint8[] bytes; + + public size_t size; + + [CCode (cname = "WebPDataClear")] + public void clear(); + } + + [CCode (cprefix = "WEBP_DEMUX_", cname = "WebPDemuxState")] + public enum ParsingState { + PARSE_ERROR, + PARSING_HEADER, + PARSED_HEADER, + DONE + } + + [CCode (cprefix = "WEBP_FF_")] + public enum FormatFeature { + FORMAT_FLAGS, + CANVAS_WIDTH, + CANVAS_HEIGHT, + LOOP_COUNT, + BACKGROUND_COLOR, + FRAME_COUNT + } + + [Compact] + [CCode (free_function = "WebPDemuxDelete", cname = "WebPDemuxer", cheader_filename = "webp/demux.h", has_type_id = false)] + public class Demuxer { + [CCode (cname="WebPDemux")] + public Demuxer(Data data); + + [CCode (cname="WebPDemuxPartial")] + public Demuxer.partial(Data data, out ParsingState state); + + [CCode (cname="WebPDemuxGetI")] + public uint32 get(FormatFeature feature); + } +}