[gnome-boxes] installer: Use libosinfo APIs for the automated installations



commit cd3a70548fba61ec279b2aeb7c092c6d947e2b3b
Author: Fabiano FidÃncio <fabiano fidencio org>
Date:   Thu Nov 29 03:14:51 2012 +0200

    installer: Use libosinfo APIs for the automated installations
    
    Let's drop all OSes-specific automation code in favor of new
    automated installation API provided by libosinfo.
    
    Known issues:
    
    1. Direct boot commandline should come from libosinfo.
    2. We still have os-specific code for deciding what disk to use for
       unatteded files.
    3. This breaks viostor drivers installation.
    
    I'll work on #1 next but hardcoded value for now isn't that bad as what
    we do currently isn't any different. Same for #2 and IMO the solution for
    that would go under (yet to be created) libvirt-builder.
    
    As for #3, a following patch in this series fixes that using new
    libosinfo API.
    
    co-author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
    
    https://bugzilla.gnome.org/show_bug.cgi?id=676537

 src/media-manager.vala        |   29 +--
 src/unattended-installer.vala |  420 ++++++++++++++++++++++++++++-------------
 2 files changed, 300 insertions(+), 149 deletions(-)
---
diff --git a/src/media-manager.vala b/src/media-manager.vala
index bc4d67a..f9091a4 100644
--- a/src/media-manager.vala
+++ b/src/media-manager.vala
@@ -151,27 +151,14 @@ private class Boxes.MediaManager : Object {
         if (media.os == null)
             return media;
 
-        switch (media.os.distro) {
-        case "fedora":
-            return new FedoraInstaller.from_media (media);
-
-        case "win":
-            switch (media.os.short_id) {
-            case "win7":
-            case "win2k8":
-                return new Win7Installer.from_media (media);
-
-            case "winxp":
-            case "win2k":
-            case "win2k3":
-                return new WinXPInstaller.from_media (media);
-
-            default:
-                return media;
-            }
-
-        default:
+        var install_scripts = media.os.get_install_script_list ();
+        var filter = new Filter ();
+        filter.add_constraint (INSTALL_SCRIPT_PROP_PROFILE, INSTALL_SCRIPT_PROFILE_DESKTOP);
+        install_scripts = install_scripts.new_filtered (filter);
+
+        if (install_scripts.get_length () > 0)
+            return new UnattendedInstaller.from_media (media, install_scripts);
+        else
             return media;
-        }
     }
 }
diff --git a/src/unattended-installer.vala b/src/unattended-installer.vala
index 86c8681..c5a43b9 100644
--- a/src/unattended-installer.vala
+++ b/src/unattended-installer.vala
@@ -1,19 +1,26 @@
 // This file is part of GNOME Boxes. License: LGPLv2+
 
 using GVirConfig;
+using Osinfo;
 
 public errordomain UnattendedInstallerError {
     SETUP_INCOMPLETE
 }
 
-private abstract class Boxes.UnattendedInstaller: InstallerMedia {
+private class Boxes.UnattendedInstaller: InstallerMedia {
     public override bool need_user_input_for_vm_creation {
         get {
             return !live; // No setup required by live media (and unknown medias are not UnattendedInstaller instances)
         }
     }
 
-    public virtual bool ready_for_express { get { return username != ""; } }
+    public bool ready_for_express {
+        get {
+            return username != "" &&
+                   (product_key_format == null ||
+                    key_entry.text_length == product_key_format.length);
+        }
+    }
 
     public override bool ready_to_create {
         get {
@@ -21,6 +28,12 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         }
     }
 
+    public override bool supports_virtio_disk {
+        get {
+            return base.supports_virtio_disk || has_viostor_drivers;
+        }
+    }
+
     public bool express_install {
         get { return express_toggle.active; }
     }
@@ -46,16 +59,17 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         }
     }
 
-    public DataStreamNewlineType newline_type;
     public File? disk_file;
+    public File? kernel_file;
+    public File? initrd_file;
+    public InstallConfig config;
+    public InstallScriptList scripts;
 
-    private string _extra_iso;
-    protected string extra_iso {
-        owned get { return lookup_extra_iso (_extra_iso); }
-        set { _extra_iso = value; }
-    }
+    private bool has_viostor_drivers;
+    private string? product_key_format;
 
     protected GLib.List<UnattendedFile> unattended_files;
+    private UnattendedAvatarFile avatar_file;
 
     protected Gtk.Grid setup_grid;
     protected int setup_grid_n_rows;
@@ -65,33 +79,17 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
     protected Gtk.Switch express_toggle;
     protected Gtk.Entry username_entry;
     protected Gtk.Entry password_entry;
+    private Gtk.Entry key_entry;
 
     protected string timezone;
     protected string lang;
     protected string hostname;
+    private string kbd;
 
-    protected AvatarFormat avatar_format;
+    ulong key_inserted_id; // ID of key_entry.insert_text signal handler
 
-    private static Regex username_regex;
-    private static Regex password_regex;
-    private static Regex timezone_regex;
-    private static Regex lang_regex;
-    private static Regex host_regex;
     private static Fdo.Accounts? accounts;
 
-    static construct {
-        try {
-            username_regex = new Regex ("BOXES_USERNAME");
-            password_regex = new Regex ("BOXES_PASSWORD");
-            timezone_regex = new Regex ("BOXES_TZ");
-            lang_regex = new Regex ("BOXES_LANG");
-            host_regex = new Regex ("BOXES_HOSTNAME");
-        } catch (RegexError error) {
-            // This just can't fail
-            assert_not_reached ();
-        }
-    }
-
     construct {
         /* We can't do this in the class constructor as the sync call can
            cause deadlocks, see bug #676679. */
@@ -104,10 +102,7 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         }
     }
 
-    public UnattendedInstaller.from_media (InstallerMedia media,
-                                           string         unattended_src_path,
-                                           string         unattended_dest_name,
-                                           AvatarFormat?  avatar_format = null) throws GLib.Error {
+    public UnattendedInstaller.from_media (InstallerMedia media, InstallScriptList scripts) throws GLib.Error {
         os = media.os;
         os_media = media.os_media;
         label = media.label;
@@ -116,10 +111,15 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         mount_point = media.mount_point;
         resources = media.resources;
 
-        newline_type = DataStreamNewlineType.LF;
+        this.scripts = scripts;
+        config = new InstallConfig ("http://live.gnome.org/Boxes/unattended";);
 
         unattended_files = new GLib.List<UnattendedFile> ();
-        add_unattended_file (new UnattendedTextFile (this, unattended_src_path, unattended_dest_name));
+        foreach (var s in scripts.get_elements ()) {
+            var script = s as InstallScript;
+            var filename = script.get_expected_filename ();
+            add_unattended_file (new UnattendedTextFile (this, script, filename));
+        }
 
         var time = TimeVal ();
         var date = new DateTime.from_timeval_local (time);
@@ -127,10 +127,8 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
 
         var langs = Intl.get_language_names ();
         lang = langs[0];
-
-        this.avatar_format = avatar_format;
-        if (avatar_format == null)
-            this.avatar_format = new AvatarFormat ();
+        kbd = lang;
+        product_key_format = get_product_key_format ();
 
         setup_ui ();
     }
@@ -148,6 +146,15 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
 
             foreach (var unattended_file in unattended_files)
                 yield unattended_file.copy (cancellable);
+
+            //FIXME: Linux-specific. Any generic way to achieve this?
+            if (os_media.kernel_path != null && os_media.initrd_path != null) {
+                var extractor = new ISOExtractor (device_file);
+
+                yield extractor.mount_media (cancellable);
+
+                yield extract_boot_files (extractor, cancellable);
+            }
         } catch (GLib.Error error) {
             clean_up ();
             // An error occurred when trying to setup unattended installation, but it's likely that a non-unattended
@@ -165,18 +172,43 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         base.setup_domain_config (domain);
 
         var disk = get_unattended_disk_config ();
-        if (disk == null)
-            return;
-
-        domain.add_device (disk);
+        if (disk != null)
+            domain.add_device (disk);
     }
 
-    // Allows us to add an extra install media, for example with more windows drivers
-    public void add_extra_iso (Domain domain) {
-        if (extra_iso == null)
-            return;
+    public void configure_install_script (InstallScript script) {
+        if (password != null) {
+            config.set_user_password (password);
+            config.set_admin_password (password);
+        }
+        if (username != null) {
+            config.set_user_login (username);
+            config.set_user_realname (username);
+        }
+        if (key_entry != null && key_entry.text != null)
+            config.set_reg_product_key (key_entry.text);
+        config.set_l10n_timezone (timezone);
+        config.set_l10n_language (lang);
+        config.set_l10n_keyboard (kbd);
+        config.set_hostname (hostname);
+        config.set_hardware_arch (os_media.architecture);
+
+        // FIXME: Ideally, we shouldn't need to check for distro
+        if (os.distro == "win")
+            config.set_target_disk ("C");
+        else
+            config.set_target_disk (supports_virtio_disk? "vda" : "sda");
+
+        string device_name;
+        get_unattended_disk_info (script.path_format, out device_name);
+        config.set_script_disk (device_name);
+
+        if (avatar_file != null) {
+            var location = ((script.path_format == PathFormat.UNIX)? "/" : "\\") + avatar_file.dest_name;
+            config.set_avatar_location (location);
+            config.set_avatar_disk (config.get_script_disk ());
+        }
 
-        add_cd_config (domain, DomainDiskType.FILE, extra_iso, "hdd", false);
     }
 
     public override void populate_setup_vbox (Gtk.VBox setup_vbox) {
@@ -188,7 +220,7 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         setup_vbox.show_all ();
     }
 
-    public override List<Pair> get_vm_properties () {
+    public override GLib.List<Pair> get_vm_properties () {
         var properties = base.get_vm_properties ();
 
         if (express_install) {
@@ -199,14 +231,17 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         return properties;
     }
 
-    public virtual string fill_unattended_data (string data) throws RegexError {
-        var str = username_regex.replace (data, data.length, 0, username_entry.text);
-        str = password_regex.replace (str, str.length, 0, password);
-        str = timezone_regex.replace (str, str.length, 0, timezone);
-        str = lang_regex.replace (str, str.length, 0, lang);
-        str = host_regex.replace (str, str.length, 0, hostname);
+    public override void set_direct_boot_params (GVirConfig.DomainOs os) {
+        if (kernel_file == null || initrd_file == null)
+            return;
+
+        // FIXME: This commandline should come from libosinfo somehow
+        var script = scripts.get_nth (0) as InstallScript;
+        var cmdline = "ks=hd:sda:/" + script.get_expected_filename ();
 
-        return str;
+        os.set_kernel (kernel_file.get_path ());
+        os.set_ramdisk (initrd_file.get_path ());
+        os.set_cmdline (cmdline);
     }
 
     public string get_user_unattended (string? suffix = null) {
@@ -307,14 +342,102 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         foreach (var child in setup_grid.get_children ())
             if (child != express_toggle)
                 express_toggle.bind_property ("active", child, "sensitive", BindingFlags.SYNC_CREATE);
+
+        if (product_key_format == null)
+            return;
+
+        label = new Gtk.Label (_("Product Key"));
+        label.margin_top = 15;
+        label.margin_right = 10;
+        label.halign = Gtk.Align.END;
+        label.valign = Gtk.Align.CENTER;
+        setup_grid.attach (label, 0, setup_grid_n_rows, 2, 1);
+        express_toggle.bind_property ("active", label, "sensitive", 0);
+
+        key_entry = create_input_entry ("");
+        key_entry.width_chars = product_key_format.length;
+        key_entry.max_length =  product_key_format.length;
+        key_entry.margin_top = 15;
+        key_entry.halign = Gtk.Align.FILL;
+        key_entry.valign = Gtk.Align.CENTER;
+        key_entry.get_style_context ().add_class ("boxes-product-key-entry");
+        setup_grid.attach (key_entry, 2, setup_grid_n_rows, 1, 1);
+        express_toggle.bind_property ("active", key_entry, "sensitive", 0);
+        setup_grid_n_rows++;
+
+        key_inserted_id = key_entry.insert_text.connect (on_key_text_inserted);
+    }
+
+    private void on_key_text_inserted (string text, int text_length, ref int position) {
+        var result = "";
+
+        for (uint i = 0, j = position; i < text_length && j < product_key_format.length; ) {
+            var character = text.get (i);
+            var allowed_char = product_key_format.get (j);
+
+            var skip_input_char = false;
+            switch (allowed_char) {
+            case '@': // Any character
+                break;
+
+            case '%': // Alphabet
+                if (!character.isalpha ())
+                    skip_input_char = true;
+                break;
+
+            case '#': // Numeric
+                if (!character.isdigit ())
+                    skip_input_char = true;
+                break;
+
+            case '$': // Alphnumeric
+                if (!character.isalnum ())
+                    skip_input_char = true;
+                break;
+
+            default: // Hardcoded character required
+                if (character != allowed_char) {
+                    result += allowed_char.to_string ();
+                    j++;
+
+                    continue;
+                }
+
+                break;
+            }
+
+            i++;
+            if (skip_input_char)
+                continue;
+
+            result += character.to_string ();
+            j++;
+        }
+
+        if (result != "") {
+            SignalHandler.block (key_entry, key_inserted_id);
+            key_entry.insert_text (result.up (), result.length, ref position);
+            SignalHandler.unblock (key_entry, key_inserted_id);
+        }
+
+        Signal.stop_emission_by_name (key_entry, "insert-text");
     }
 
     protected virtual void clean_up () throws GLib.Error {
         if (disk_file != null) {
             delete_file (disk_file);
-
             disk_file = null;
         }
+
+        if (kernel_file != null) {
+            delete_file (kernel_file);
+            kernel_file = null;
+        }
+
+        if (initrd_file != null) {
+            delete_file (initrd_file);
+            initrd_file = null;
+        }
     }
 
     protected virtual DomainDisk? get_unattended_disk_config () {
@@ -325,15 +448,35 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
 
         var disk = new DomainDisk ();
         disk.set_type (DomainDiskType.FILE);
-        disk.set_guest_device_type (DomainDiskGuestDeviceType.DISK);
         disk.set_driver_name ("qemu");
         disk.set_driver_type ("raw");
         disk.set_source (disk_file.get_path ());
-        disk.set_target_dev ("sdb");
+
+        string device_name;
+        var device_type = get_unattended_disk_info (PathFormat.UNIX, out device_name);
+        disk.set_guest_device_type (device_type);
+        disk.set_target_dev (device_name);
 
         return disk;
     }
 
+    private DomainDiskGuestDeviceType get_unattended_disk_info (PathFormat path_format, out string device_name) {
+        // FIXME: Ideally, we shouldn't need to check for distro
+        if (os.distro == "win") {
+            device_name = (path_format == PathFormat.DOS)? "A" : "fd";
+
+            return DomainDiskGuestDeviceType.FLOPPY;
+        } else {
+            // Path format checks below are most probably practically redundant but a small price for future safety
+            if (supports_virtio_disk)
+                device_name = (path_format == PathFormat.UNIX)? "sda" : "E";
+            else
+                device_name = (path_format == PathFormat.UNIX)? "sdb" : "E";
+
+            return DomainDiskGuestDeviceType.DISK;
+        }
+    }
+
     protected void add_unattended_file (UnattendedFile file) {
         unattended_files.append (file);
     }
@@ -364,20 +507,6 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         debug ("Floppy image for unattended installation created at '%s'", disk_path);
     }
 
-    private string? lookup_extra_iso (string name)
-    {
-        var path = get_user_pkgcache (name);
-
-        if (FileUtils.test (path, FileTest.IS_REGULAR))
-            return path;
-
-        path = get_unattended (name);
-        if (FileUtils.test (path, FileTest.IS_REGULAR))
-            return path;
-
-        return null;
-    }
-
     private async void fetch_user_avatar (Gtk.Image avatar) {
         if (accounts == null)
             return;
@@ -396,11 +525,61 @@ private abstract class Boxes.UnattendedInstaller: InstallerMedia {
         try {
             var pixbuf = new Gdk.Pixbuf.from_file_at_scale (avatar_path, 64, 64, true);
             avatar.pixbuf = pixbuf;
-            add_unattended_file (new UnattendedAvatarFile (this, avatar_path, avatar_format));
+
+            AvatarFormat avatar_format = null;
+            foreach (var s in scripts.get_elements ()) {
+                var script = s as InstallScript;
+                avatar_format = script.avatar_format;
+                if (avatar_format != null)
+                    break;
+            }
+
+            avatar_file = new UnattendedAvatarFile (this, avatar_path, avatar_format);
+            add_unattended_file (avatar_file);
         } catch (GLib.Error error) {
             debug ("Failed to load user avatar file '%s': %s", avatar_path, error.message);
         }
     }
+
+    private async void extract_boot_files (ISOExtractor extractor, Cancellable cancellable) throws GLib.Error {
+        string src_path = extractor.get_absolute_path (os_media.kernel_path);
+        string dest_path = get_user_unattended ("kernel");
+        kernel_file = yield copy_file (src_path, dest_path, cancellable);
+
+        src_path = extractor.get_absolute_path (os_media.initrd_path);
+        dest_path = get_user_unattended ("initrd");
+        initrd_file = yield copy_file (src_path, dest_path, cancellable);
+    }
+
+    private async File copy_file (string src_path, string dest_path, Cancellable cancellable) throws GLib.Error {
+        var src_file = File.new_for_path (src_path);
+        var dest_file = File.new_for_path (dest_path);
+
+        try {
+            debug ("Copying '%s' to '%s'..", src_path, dest_path);
+            yield src_file.copy_async (dest_file, 0, Priority.DEFAULT, cancellable);
+            debug ("Copied '%s' to '%s'.", src_path, dest_path);
+        } catch (IOError.EXISTS error) {}
+
+        return dest_file;
+    }
+
+    private string? get_product_key_format () {
+        // FIXME: We don't support the case of multiple scripts requiring different kind of product keys.
+        foreach (var s in scripts.get_elements ()) {
+            var script = s as InstallScript;
+
+            var param = script.get_config_param (INSTALL_CONFIG_PROP_REG_PRODUCTKEY);
+            if (param == null || param.is_optional ())
+                continue;
+
+            var format = script.get_product_key_format ();
+            if (format != null)
+                return format;
+        }
+
+        return null;
+    }
 }
 
 private interface Boxes.UnattendedFile : GLib.Object {
@@ -445,13 +624,14 @@ private class Boxes.UnattendedTextFile : GLib.Object, Boxes.UnattendedFile {
     public string dest_name { get; set; }
     public string src_path { get; set; }
 
-    protected UnattendedInstaller installer  { get; set; }
+    protected UnattendedInstaller installer { get; set; }
+    protected InstallScript script { get; set; }
 
     private File unattended_tmp;
 
-    public UnattendedTextFile (UnattendedInstaller installer, string src_path, string dest_name) {
+    public UnattendedTextFile (UnattendedInstaller installer, InstallScript script, string dest_name) {
        this.installer = installer;
-       this.src_path = src_path;
+       this.script = script;
        this.dest_name = dest_name;
     }
 
@@ -464,32 +644,12 @@ private class Boxes.UnattendedTextFile : GLib.Object, Boxes.UnattendedFile {
     }
 
     protected async File get_source_file (Cancellable? cancellable)  throws GLib.Error {
-        var source = File.new_for_path (src_path);
-        var destination_path = installer.get_user_unattended (dest_name);
-        var destination = File.new_for_path (destination_path);
-
-        debug ("Creating unattended file at '%s'..", destination.get_path ());
-        var input_stream = yield source.read_async (Priority.DEFAULT, cancellable);
-        var output_stream = yield destination.replace_async (null,
-                                                             false,
-                                                             FileCreateFlags.REPLACE_DESTINATION,
-                                                             Priority.DEFAULT,
-                                                             cancellable);
-        var data_stream = new DataInputStream (input_stream);
-        data_stream.newline_type = DataStreamNewlineType.ANY;
-        string? str;
-        while ((str = yield data_stream.read_line_async (Priority.DEFAULT, cancellable)) != null) {
-            str = installer.fill_unattended_data (str);
-
-            str += (installer.newline_type == DataStreamNewlineType.LF) ? "\n" : "\r\n";
-
-            yield output_stream.write_async (str.data, Priority.DEFAULT, cancellable);
-        }
-        yield output_stream.close_async (Priority.DEFAULT, cancellable);
-        debug ("Created unattended file at '%s'..", destination.get_path ());
-        unattended_tmp = destination;
+        installer.configure_install_script (script);
+        var output_dir = File.new_for_path (get_user_pkgcache ());
 
-        return destination;
+        unattended_tmp = script.generate_output (installer.os, installer.config, output_dir, cancellable);
+
+        return unattended_tmp;
     }
 }
 
@@ -501,13 +661,36 @@ private class Boxes.UnattendedAvatarFile : GLib.Object, Boxes.UnattendedFile {
 
     private File unattended_tmp;
 
-    private AvatarFormat dest_format;
+    private AvatarFormat? avatar_format;
+    private Gdk.PixbufFormat pixbuf_format;
 
-    public UnattendedAvatarFile (UnattendedInstaller installer, string src_path, AvatarFormat dest_format) {
+    public UnattendedAvatarFile (UnattendedInstaller installer, string src_path, AvatarFormat? avatar_format)
+                                 throws Boxes.Error {
         this.installer = installer;
         this.src_path = src_path;
 
-        this.dest_format = dest_format;
+        this.avatar_format = avatar_format;
+
+        foreach (var format in Gdk.Pixbuf.get_formats ()) {
+            if (avatar_format != null) {
+                foreach (var mime_type in avatar_format.mime_types) {
+                    if (mime_type in format.get_mime_types ()) {
+                        pixbuf_format = format;
+
+                        break;
+                    }
+                }
+            } else if (format.get_name () == "png")
+                pixbuf_format = format; // Fallback to PNG if supported
+
+            if (pixbuf_format != null)
+                break;
+        }
+
+        if (pixbuf_format == null)
+            throw new Boxes.Error.INVALID ("Failed to find suitable format to save user avatar file in.");
+
+        dest_name = installer.username + "." + pixbuf_format.get_extensions ()[0];
     }
 
     ~UnattendedAvatarFile () {
@@ -518,18 +701,19 @@ private class Boxes.UnattendedAvatarFile : GLib.Object, Boxes.UnattendedFile {
         }
     }
 
-    protected async File get_source_file (Cancellable? cancellable)  throws GLib.Error {
-        dest_name = installer.username + dest_format.extension;
+    protected async File get_source_file (Cancellable? cancellable) throws GLib.Error {
         var destination_path = installer.get_user_unattended (dest_name);
 
         try {
-            var pixbuf = new Gdk.Pixbuf.from_file_at_scale (src_path, dest_format.width, dest_format.height, true);
+            var width = (avatar_format != null)? avatar_format.width : -1;
+            var height = (avatar_format != null)? avatar_format.height : -1;
+            var pixbuf = new Gdk.Pixbuf.from_file_at_scale (src_path, width, height, true);
 
-            if (!dest_format.alpha && pixbuf.get_has_alpha ())
+            if (avatar_format != null && !avatar_format.alpha && pixbuf.get_has_alpha ())
                 pixbuf = remove_alpha (pixbuf);
 
             debug ("Saving user avatar file at '%s'..", destination_path);
-            pixbuf.save (destination_path, dest_format.type);
+            pixbuf.save (destination_path, pixbuf_format.get_name ());
             debug ("Saved user avatar file at '%s'.", destination_path);
         } catch (GLib.Error error) {
             warning ("Failed to save user avatar: %s.", error.message);
@@ -540,23 +724,3 @@ private class Boxes.UnattendedAvatarFile : GLib.Object, Boxes.UnattendedFile {
         return unattended_tmp;
     }
 }
-
-private class AvatarFormat {
-    public string type;
-    public string extension;
-    public bool alpha;
-    public int width;
-    public int height;
-
-    public AvatarFormat (string type = "png",
-                         string extension = "",
-                         bool   alpha = true,
-                         int    width = -1,
-                         int    height = -1) {
-        this.type = type;
-        this.extension = extension;
-        this.alpha = alpha;
-        this.width = width;
-        this.height = height;
-    }
-}



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