[shotwell/wip/phako/heif: 173/174] wip: Add HEIF support



commit 3e31e55f3781d95c4b7c856af69b298930acee7b
Author: Jens Georg <mail jensge org>
Date:   Sat Feb 9 18:30:13 2019 +0100

    wip: Add HEIF support

 meson.build                     |   2 +
 src/VideoMetadata.vala          |  12 +-
 src/meson.build                 |   4 +-
 src/photos/HeicSupport.vala     | 272 ++++++++++++++++++++++++++++++++++++++++
 src/photos/PhotoFileFormat.vala |  19 ++-
 vapi/libheif.vapi               |  68 ++++++++++
 6 files changed, 372 insertions(+), 5 deletions(-)
---
diff --git a/meson.build b/meson.build
index d86f5b11..a2924725 100644
--- a/meson.build
+++ b/meson.build
@@ -72,6 +72,8 @@ clutter_gtk = dependency('clutter-gtk-1.0')
 webpdemux = dependency('libwebpdemux')
 webp = dependency('libwebp')
 
+heif = dependency('libheif')
+
 unity_available = false
 if unity.found() and get_option('unity-support')
   unity_available = true
diff --git a/src/VideoMetadata.vala b/src/VideoMetadata.vala
index 6e6040b0..7b044e5f 100644
--- a/src/VideoMetadata.vala
+++ b/src/VideoMetadata.vala
@@ -170,7 +170,7 @@ private class QuickTimeMetadataLoader {
     }
 }
 
-private class QuickTimeAtom {
+public class QuickTimeAtom {
     private GLib.File file = null;
     private string section_name = "";
     private uint64 section_size = 0;
@@ -186,6 +186,14 @@ private class QuickTimeAtom {
         this.input = input;
         this.parent = parent;
     }
+
+    public QuickTimeAtom.from_stream(GLib.InputStream input_stream) throws GLib.Error {
+        input = new GLib.DataInputStream(input_stream);
+        input.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
+        section_size = 0;
+        section_offset = 0;
+        section_name = "";
+    }
     
     public void open_file() throws GLib.Error {
         close_file();
@@ -195,7 +203,7 @@ private class QuickTimeAtom {
         section_offset = 0;
         section_name = "";
     }
-    
+
     public void close_file() throws GLib.Error {
         if (null != input) {
             input.close();
diff --git a/src/meson.build b/src/meson.build
index b9729445..da4fd3fd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -38,7 +38,8 @@ endif
 shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2,
                  gstreamer_pbu, gio_unix, gudev, gexiv2, gmodule,
                  libraw, libexif, sw_plugin, webpdemux, webp, version,
-                 clutter, clutter_gtk, champlain, champlain_gtk]
+                 clutter, clutter_gtk, champlain, champlain_gtk,
+                 heif]
 
 if unity_available
     shotwell_deps += [unity]
@@ -91,6 +92,7 @@ executable('shotwell',
             'photos/PngSupport.vala',
             'photos/TiffSupport.vala',
             'photos/WebPSupport.vala',
+            'photos/HeicSupport.vala',
             'plugins/Plugins.vala',
             'plugins/StandardHostInterface.vala',
             'plugins/ManifestWidget.vala',
diff --git a/src/photos/HeicSupport.vala b/src/photos/HeicSupport.vala
new file mode 100644
index 00000000..9c10ca1e
--- /dev/null
+++ b/src/photos/HeicSupport.vala
@@ -0,0 +1,272 @@
+/* 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 HeicFileFormatDriver : PhotoFileFormatDriver {
+    private static HeicFileFormatDriver instance = null;
+
+    public static void init() {
+        instance = new HeicFileFormatDriver();
+        HeicFileFormatProperties.init();
+    }
+
+    public static HeicFileFormatDriver get_instance() {
+        return instance;
+    }
+
+    public override PhotoFileFormatProperties get_properties() {
+        return HeicFileFormatProperties.get_instance();
+    }
+
+    public override PhotoFileReader create_reader(string filepath) {
+        return new HeicReader(filepath);
+    }
+
+    public override PhotoMetadata create_metadata() {
+        return new PhotoMetadata();
+    }
+
+    public override bool can_write_image() {
+        return false;
+    }
+
+    public override bool can_write_metadata() {
+        return false;
+    }
+
+    public override PhotoFileWriter? create_writer(string filepath) {
+        return null;
+    }
+
+    public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
+        return null;
+    }
+
+    public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
+        return new HeicSniffer(file, options);
+    }
+}
+
+private class HeicFileFormatProperties : PhotoFileFormatProperties {
+    private static string[] KNOWN_EXTENSIONS = {
+        "heic", "heif"
+    };
+
+    private static string[] KNOWN_MIME_TYPES = {
+        "image/heif", "image/heic"
+    };
+
+    private static HeicFileFormatProperties instance = null;
+
+    public static void init() {
+        instance = new HeicFileFormatProperties();
+    }
+
+    public static HeicFileFormatProperties get_instance() {
+        return instance;
+    }
+
+    public override PhotoFileFormat get_file_format() {
+        return PhotoFileFormat.HEIC;
+    }
+
+    public override PhotoFileFormatFlags get_flags() {
+        return PhotoFileFormatFlags.NONE;
+    }
+
+    public override string get_default_extension() {
+        return "heic";
+    }
+
+    public override string get_user_visible_name() {
+        return _("HEIC");
+    }
+
+    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 HeicSniffer : PhotoFileSniffer {
+    private DetectedPhotoInformation detected = null;
+
+    public HeicSniffer(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();
+        #if 0
+        for (;;) {
+            size_t bytes_read = fins.read(buffer, null);
+            if (bytes_read <= 0)
+                break;
+
+            ba.append(buffer[0:bytes_read]);
+            mins.add_data(buffer[0:bytes_read]);
+
+            count += bytes_read;
+
+            if (calc_md5)
+                md5_checksum.update(buffer, bytes_read);
+
+            d.bytes = ba.data;
+
+            try {
+                atom.read_atom();
+            } catch (Error error) {
+                is_corrupted = true;
+                break;
+            }
+
+            if (state > WebP.ParsingState.PARSED_HEADER) {
+                detected.file_format = PhotoFileFormat.HEIC;
+                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;
+            }
+        }
+        #endif
+
+        if (fins != null)
+            fins.close(null);
+
+        if (calc_md5)
+            detected.md5 = md5_checksum.get_string();
+
+        return detected;
+    }
+}
+
+private class HeicReader : PhotoFileReader {
+    public HeicReader(string filepath) {
+        base (filepath, PhotoFileFormat.HEIC);
+    }
+
+    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);
+        var handle = new Heif.Context();
+        var result = handle.read_from_memory_without_copy (buffer);
+        if (result.code != Heif.Error.OK) {
+            throw new PhotoFormatError.DECODE_ERROR(result.message);
+        }
+
+        unowned Heif.ImageHandle hdl;
+        result = handle.get_primary_image_handle(out hdl);
+        if (result.code != Heif.Error.OK) {
+            throw new PhotoFormatError.DECODE_ERROR(result.message);
+        }
+
+        unowned Heif.Image image;
+        result = hdl.decode_image(out image, Heif.Colorspace.RGB, Heif.Chroma.INTERLEAVED_RGBA);
+        if (result.code != Heif.Error.OK) {
+            throw new PhotoFormatError.DECODE_ERROR(result.message);
+        }
+
+        var width = image.get_width(Heif.Channel.INTERLEAVED);
+        var height = image.get_height(Heif.Channel.INTERLEAVED);
+
+        int stride;
+        var data = image.get_plane_readonly(Heif.Channel.INTERLEAVED, out stride);
+
+        // TODO: Avoid copy by fixing the issue with libheif context not being ref-counted
+        data.length = height * stride;
+        var bytes = new Bytes(data);
+        var pixbuf = new Gdk.Pixbuf.from_bytes(bytes, Gdk.Colorspace.RGB, true, 8, width, height, stride);
+
+        return pixbuf;
+    }
+}
+
+public bool is_heic(File file, Cancellable? cancellable = null) throws Error {
+    var ins = file.read();
+
+    uint8 buffer[12];
+    try {
+        ins.read(buffer, null);
+
+        if (buffer[4] != 'f' ||
+            buffer[5] != 't' ||
+            buffer[6] != 'y' ||
+            buffer[7] != 'p')
+            return false;
+
+        if (buffer.length >= 12) {
+            var brand = "%c%c%c%c".printf(buffer[8], buffer[9], buffer[10], buffer[11]);
+
+            if (brand == "heic")
+                return true;
+            else if (brand == "mif1")
+                return true;
+
+            return false;
+        }
+
+        return false;
+    } catch (Error error) {
+        debug ("Failed to read from file %s: %s", file.get_path (), error.message);
+    }
+
+    return false;
+}
+
+}
diff --git a/src/photos/PhotoFileFormat.vala b/src/photos/PhotoFileFormat.vala
index 94ca7521..81d18d69 100644
--- a/src/photos/PhotoFileFormat.vala
+++ b/src/photos/PhotoFileFormat.vala
@@ -5,7 +5,8 @@
  */
 
 public errordomain PhotoFormatError {
-    READ_ONLY
+    READ_ONLY,
+    DECODE_ERROR
 }
 
 //
@@ -59,12 +60,13 @@ public enum PhotoFileFormat {
     BMP,
     GIF,
     WEBP,
+    HEIC,
     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, WEBP };
+        return { JFIF, RAW, PNG, TIFF, BMP, GIF, WEBP, HEIC };
     }
     
     public static PhotoFileFormat[] get_writeable() {
@@ -146,6 +148,9 @@ public enum PhotoFileFormat {
             case WEBP:
                 return 6;
 
+            case HEIC:
+                return 7;
+
             case UNKNOWN:
             default:
                 return -1;
@@ -176,6 +181,9 @@ public enum PhotoFileFormat {
             case 6:
                 return WEBP;
 
+            case 7:
+                return HEIC;
+
             default:
                 return UNKNOWN;
         }
@@ -260,6 +268,10 @@ public enum PhotoFileFormat {
                 Photos.WebpFileFormatDriver.init();
                 break;
 
+            case HEIC:
+                Photos.HeicFileFormatDriver.init();
+                break;
+
             default:
                 error("Unsupported file format %s", this.to_string());
         }
@@ -288,6 +300,9 @@ public enum PhotoFileFormat {
             case WEBP:
                 return Photos.WebpFileFormatDriver.get_instance();
 
+            case HEIC:
+                return Photos.HeicFileFormatDriver.get_instance();
+
             default:
                 error("Unsupported file format %s", this.to_string());
         }
diff --git a/vapi/libheif.vapi b/vapi/libheif.vapi
new file mode 100644
index 00000000..94af0107
--- /dev/null
+++ b/vapi/libheif.vapi
@@ -0,0 +1,68 @@
+[CCode (cheader_filename = "libheif/heif.h")]
+namespace Heif {
+    [CCode (
+        cname="struct heif_error",
+        free_function="(void)",
+        destroy_function="(void)"
+    )]
+    [SimpleType]
+    public struct Error {
+        [CCode (cname="heif_error_Ok")]
+        public const int OK;
+        public int code;
+        public int subcode;
+        public string message;
+    }
+
+    [Compact]
+    [CCode (
+        cname="struct heif_context",
+        free_function="heif_context_free"
+    )]
+    public class Context {
+        [CCode (cname="heif_context_alloc")]
+        public Context();
+        public Error read_from_memory_without_copy (uint8[] buffer, void *param = null);
+        public Error get_primary_image_handle(out unowned ImageHandle hdl);
+    }
+
+    [CCode (cname="enum heif_colorspace")]
+    public enum Colorspace {
+        [CCode (cname="heif_colorspace_RGB")]
+        RGB
+    }
+
+    [CCode (cname="enum heif_chroma")]
+    public enum Chroma {
+        [CCode (cname="heif_chroma_interleaved_RGBA")]
+        INTERLEAVED_RGBA
+    }
+
+    [CCode (cname="enum heif_channel")]
+    public enum Channel {
+        [CCode (cname="heif_channel_interleaved")]
+        INTERLEAVED
+    }
+
+    [Compact]
+    [CCode (
+        cname="struct heif_image_context",
+        free_function="heif_image_context_release"
+    )]
+    public class ImageHandle {
+        [CCode (cname="heif_decode_image")]
+        public Error decode_image(out unowned Heif.Image image, Heif.Colorspace colorspace, Heif.Chroma 
chroma, void *options = null);
+    }
+
+    [Compact]
+    [CCode (
+        cname="struct heif_image",
+        free_function="heif_image_release"
+    )]
+    public class Image {
+        public int get_width(Heif.Channel channel);
+        public int get_height(Heif.Channel channel);
+        [CCode (array_length = false)]
+        public uint8[] get_plane_readonly(Heif.Channel channel, out int stride);
+    }
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]