Selaa lähdekoodia

Implement PPUB read

Billy Barrow 2 vuotta sitten
vanhempi
sitoutus
8b3dab7792

+ 2 - 0
.gitignore

@@ -1,4 +1,6 @@
 meson-*
 /lib
+/tools
 *.ninja
+.ninja_*
 compile_commands.json

+ 42 - 0
src/lib/Asset.vala

@@ -0,0 +1,42 @@
+using Invercargill;
+using Gee;
+
+namespace Ppub {
+
+    public class Asset {
+
+        public string name {get; set;}
+        public string mimetype {get; set;}
+        public uint64 start_location {get; set;}
+        public uint64 end_location {get; set;}
+        public uint64 size {
+            get {
+                return end_location - start_location;
+            }
+        }
+        public Collection<string> flags {get; set;}
+
+        public Asset.from_string(string line) {
+            var key_value = line.split(": ");
+            name = key_value[0];
+            var data = key_value[1].split(" ");
+
+            mimetype = data[0];
+            start_location = int.parse (data[1]);
+            end_location = int.parse (data[2]);
+
+            flags = ate(data).skip(3).to_collection();
+        }
+
+        public string to_string() {
+            var str = @"$name: $mimetype $start_location $end_location";
+            foreach (var flag in flags) {
+                str += @" $flag";
+            }
+            str += "\n";
+            return str;
+        }
+
+    }
+
+}

+ 41 - 0
src/lib/AssetStream.vala

@@ -0,0 +1,41 @@
+
+namespace Ppub {
+
+    public class AssetStream : InputStream {
+
+        private Asset asset;
+        private FileInputStream ppub_stream;
+        private uint64 asset_start;
+        private uint64 asset_end;
+
+        public AssetStream(Asset asset, FileInputStream stream, uint64 asset_loc) throws Error {
+            this.asset = asset;
+            ppub_stream = stream;
+            asset_start = asset_loc;
+            asset_end = asset.size + asset_start;
+            ppub_stream.seek ((int64)asset_loc, SeekType.SET);
+        }
+
+        public override bool close (GLib.Cancellable? cancellable) throws IOError {
+            return ppub_stream.close();
+        }
+        public override ssize_t read (uint8[] buffer, GLib.Cancellable? cancellable) throws IOError {
+            uint64 to_read = buffer.length;
+            var pos = ppub_stream.tell();
+            if(to_read + pos > asset_end) {
+                to_read = asset_end - pos;
+            }
+
+            if(to_read <= 0) {
+                return 0;
+            }
+            
+            var read_buffer = new uint8[to_read];
+            var size = ppub_stream.read (read_buffer, cancellable);
+
+            Memory.copy (buffer, read_buffer, size);
+            return size;
+        }
+
+    }
+}

+ 59 - 0
src/lib/Builder.vala

@@ -0,0 +1,59 @@
+using Invercargill;
+using Gee;
+
+namespace Ppub {
+
+    public class Builder {
+
+        private Invercargill.Sequence<BuilderAsset> assets {get; set;}
+
+        private uint64 position = 0;
+
+        public void add_metadata(Metadata metadata) {
+            var data = (uint8[])metadata.to_string().to_utf8();
+            var mis = new MemoryInputStream.from_data(data);
+            add_asset("metadata", "application/x-ppub-metadata", mis, data.length);
+        }
+
+        public void add_asset(string name, string mimetype, InputStream stream, uint64 size) {
+
+            lock(assets) {
+                var asset = new BuilderAsset();
+                asset.name = name;
+                asset.start_location = position;
+                asset.end_location = position + size;
+                asset.mimetype = mimetype;
+                asset.stream = stream;
+    
+                position += size;
+                assets.add(asset);
+            }
+
+        }
+
+        public void write(OutputStream stream) throws IOError {
+
+            var index_entries = assets.select<string>(a => a.to_string() + "\n").to_array();
+            var index_data = (uint8[])string.join("", index_entries).to_utf8();
+            var header_data = (uint8[])(@"$(index_data.length)\n".to_utf8());
+
+            stream.write(header_data);
+            stream.write(index_data);
+
+            foreach (var asset in assets.as_iterable()) {
+                stream.splice(asset.stream, OutputStreamSpliceFlags.CLOSE_SOURCE);
+            }
+
+        }
+
+
+
+    }
+
+    public class BuilderAsset : Asset {
+        
+        public InputStream stream {get; set;}
+
+    }
+
+}

+ 126 - 0
src/lib/Metadata.vala

@@ -0,0 +1,126 @@
+
+namespace Ppub {
+
+    public class Metadata {
+
+        public const string TITLE = "title";
+        public const string AUTHOR = "author";
+        public const string DATE = "date";
+        public const string DESCRIPTION = "description";
+        public const string TAGS = "tags";
+        public const string COPYRIGHT = "copyright";
+        public const string LICENCE = "licence";
+
+        private Gee.HashMap<string, string> entries {get; set;}
+
+        public string? get_value(string name) {
+            if(entries.has_key(name)) {
+                return entries.get(name);
+            }
+            return null;
+        }
+
+        public void set_value(string name, string value) {
+            entries.set(name, value);
+        }
+
+
+        public string? title {
+            owned get {
+                return get_value(TITLE);
+            }
+            set {
+                set_value(TITLE, value);
+            }
+        }
+
+        public string? author {
+            owned get {
+                return get_value(AUTHOR);
+            }
+            set {
+                set_value(AUTHOR, value);
+            }
+        }
+
+        public string? author_name {
+            owned get {
+                return author?.split("<", 1)[0].chomp();
+            }
+        }
+
+        public DateTime? date {
+            owned get {
+                var date_str = get_value(DATE);
+                if(date_str == null){
+                    return null;
+                }
+                return new DateTime.from_iso8601(date_str, null);
+            }
+            set {
+                set_value(DATE, value.format_iso8601());
+            }
+        }
+
+        public string? description {
+            owned get {
+                return get_value(DESCRIPTION);
+            }
+            set {
+                set_value(DESCRIPTION, value);
+            }
+        }
+
+        public string[]? tags {
+            owned get {
+                return get_value(TAGS).split(" ");
+            }
+            set {
+                set_value(TAGS, string.join(" ", value));
+            }
+        }
+
+        public string? copyright {
+            owned get {
+                return get_value(COPYRIGHT);
+            }
+            set {
+                set_value(COPYRIGHT, value);
+            }
+        }
+
+        public string? licence {
+            owned get {
+                return get_value(LICENCE);
+            }
+            set {
+                set_value(LICENCE, value);
+            }
+        }
+        
+        public Metadata.from_stream(InputStream stream) throws IOError {
+
+            var dis = new DataInputStream(stream);
+            entries = new Gee.HashMap<string, string>();
+
+            string line;
+            while((line = dis.read_line()) != null) {
+                print(@"METADATA: $line\n");
+                var kv = line.split(": ", 2);
+                entries.set(kv[0], kv[1]);
+            }
+
+        }
+
+        public string to_string() {
+            var str = "";
+            foreach (var item in entries) {
+                str += @"$(item.key): $(item.value)\n";
+            }
+            return str;
+        }
+
+
+    }
+
+}

+ 72 - 0
src/lib/Ppub.vala

@@ -0,0 +1,72 @@
+using Invercargill;
+using Gee;
+
+namespace Ppub {
+
+    public class Publication {
+
+        public const string MAGIC = "ppub";
+
+        public Enumerable<Asset> assets {get; private set;}
+
+        public Metadata metadata {get; private set;}
+
+        private uint64 asset_offset;
+        private File ppub_file;
+
+        public Publication(string path) throws Error {
+            ppub_file = File.new_for_path(path);
+            from_stream(ppub_file.read());
+            metadata = new Metadata.from_stream(read_asset("metadata"));
+        }
+
+        private void from_stream(InputStream stream) throws Error {
+            var dis = new DataInputStream(stream);
+            var magic = dis.read_line();
+            if(magic != MAGIC) {
+                throw new IOError.INVALID_DATA("Invalid magic number");
+            }
+
+            var asset_index_size = int.parse(dis.read_line());
+            var asset_index_data = ((string)dis.read_bytes(asset_index_size).get_data()).split("\n");
+            
+            var asset_seq = new Invercargill.Sequence<Asset>();
+            foreach (var index_data in asset_index_data) {
+                if(index_data.contains(": ")) {
+                    asset_seq.add(new Asset.from_string(index_data));
+                }
+            }
+            
+            assets = asset_seq;
+            asset_offset = dis.tell();
+            dis.close();
+        }
+
+        public Asset? get_asset(string name) {
+            return assets.where(a => a.name == name).first_or_default();
+        }
+
+        public InputStream? read_asset(string name) throws Error {
+            var asset = get_asset(name);
+            if(asset == null) {
+                return null;
+            }
+
+            var stream = ppub_file.read();
+            var asset_stream = new AssetStream(asset, stream, asset_offset + asset.start_location);
+
+            if(asset.flags.contains("gzip")) {
+                var converter = new ZlibDecompressor(GLib.ZlibCompressorFormat.GZIP);
+                return new ConverterInputStream(asset_stream, converter);
+            }
+
+            return asset_stream;
+        }
+
+        public Asset get_default_asset() {
+            return assets.skip(1).first_or_default();
+        }
+
+    }
+
+}

+ 6 - 1
src/lib/meson.build

@@ -3,9 +3,14 @@ dependencies = [
     dependency('gobject-2.0'),
     dependency('gio-2.0'),
     dependency('gee-0.8'),
+    dependency('invercargill'),
 ]
 
-sources = files('ppub.vala')
+sources = files('Ppub.vala')
+sources += files('Asset.vala')
+sources += files('Metadata.vala')
+sources += files('AssetStream.vala')
+sources += files('Builder.vala')
 
 ppub = shared_library('libppub', sources,
     name_prefix: '',

+ 0 - 0
src/lib/ppub.vala


+ 1 - 0
src/meson.build

@@ -5,5 +5,6 @@ add_project_arguments(['--disable-warnings', '--enable-checking','--vapidir', va
 
 
 subdir('lib')
+subdir('tools')
 
 dependencies += ppub_dep

+ 29 - 0
src/tools/extract/Extract.vala

@@ -0,0 +1,29 @@
+
+
+static bool verbose = true;
+
+private static void print_verbose(string text) {
+    if (verbose) {
+        print(text + "\n");
+    }
+}
+
+public static int main(string[] args) {
+
+    var ppub_file = args[1];
+
+    print_verbose(@"Opening ppub file at $ppub_file.");
+
+    var ppub = new Ppub.Publication(ppub_file);
+
+    print_verbose("Reading assets...");
+    
+    foreach (var asset in ppub.assets.as_iterable()) {
+        print_verbose(@"Extracting asset \"$(asset.name)\"...");
+        var file = File.new_for_path(asset.name);
+        var os = file.create(FileCreateFlags.NONE);
+        os.splice(ppub.read_asset(asset.name), OutputStreamSpliceFlags.CLOSE_SOURCE | OutputStreamSpliceFlags.CLOSE_TARGET);
+    }
+
+    return 0;
+}

+ 7 - 0
src/tools/extract/meson.build

@@ -0,0 +1,7 @@
+
+sources = files('Extract.vala')
+
+deps = dependencies
+deps += ppub_dep
+
+executable('ppub-extract', sources, dependencies: deps, install: true)

+ 2 - 0
src/tools/meson.build

@@ -0,0 +1,2 @@
+subdir('extract')
+subdir('viewer')

+ 52 - 0
src/tools/viewer/Main.vala

@@ -0,0 +1,52 @@
+using Gtk;
+
+int main (string[] argv) {
+    // Create a new application
+    var app = new Gtk.Application ("com.example.GtkApplication", GLib.ApplicationFlags.HANDLES_OPEN);
+    app.activate.connect (() => {
+
+    });
+
+    app.open.connect ((f) => {
+        foreach(var file in f) {
+            var window = new Gtk.ApplicationWindow (app);
+            window.width_request = 500;
+            window.height_request = 900;
+
+            var ppub = new Ppub.Publication(file.get_path());
+
+            print(ppub.metadata.title);
+            window.title = @"$(ppub.metadata.title) - PPUB Viewer";
+    
+            var text_view = new GtkCommonMark.MarkdownView();
+            text_view.set_wrap_mode (WrapMode.WORD_CHAR);
+    
+            text_view.link_activated.connect (url => print(@"Url clicked: $url\n"));
+            text_view.widget_embedded.connect ((widget, url, title) => {
+
+                var pixbuf = new Gdk.Pixbuf.from_stream (ppub.read_asset (url));
+                var image = new Gtk.Picture.for_pixbuf (pixbuf);
+                image.content_fit = Gtk.ContentFit.FILL;
+                widget.available_width_changed.connect(wid => {
+                    image.width_request = wid;
+                    var ratio = image.get_paintable().get_intrinsic_aspect_ratio ();
+                    image.height_request = (int)(wid / ratio);
+                });
+                widget.append (image);
+            });
+    
+            var document = ppub.get_default_asset ();
+            print(@"Default document: $(document.name)\n");
+            text_view.load_from_stream (ppub.read_asset (document.name));
+    
+            var srcolled_window = new Gtk.ScrolledWindow ();
+            srcolled_window.set_policy (PolicyType.NEVER, PolicyType.AUTOMATIC);
+            srcolled_window.set_child(text_view);
+            window.set_child (srcolled_window);
+    
+            window.present ();
+        }
+    });
+
+    return app.run (argv);
+}

+ 16 - 0
src/tools/viewer/meson.build

@@ -0,0 +1,16 @@
+sources = files('Main.vala')
+
+vapi_dir = meson.current_source_dir() / 'vapi'
+
+dependencies = [
+    dependency('glib-2.0'),
+    dependency('gobject-2.0'),
+    dependency('gio-2.0'),
+    dependency('gee-0.8'),
+    dependency('invercargill'),
+    dependency('gtk4'),
+    dependency('gtkcommonmark'),
+    ppub_dep
+]
+
+executable('GtkCommonMarkViewer', sources, dependencies: dependencies)