/* moz-images.cs - An example of different caching policies for images in a web browser * * Author: Federico Mena-Quintero * * This program is in the public domain. Do with it whatever you want. * * Compile like this: * mcs -warn:4 -out:moz-images.exe moz-images.cs -pkg:gtk-sharp * * Run like this: * mono moz-images.exe /path/with/lots/of/images [--server | --client | --compressed] */ using System; using System.IO; using System.Collections; using Gdk; using Gtk; class Image { public Image (string filename) { load (filename); } public int XPos { get { return xpos; } set { xpos = value; } } public int YPos { get { return ypos; } set { ypos = value; } } public int Width { get { return width; } } public int Height { get { return height; } } public byte[] CompressedData { get { return compressed_data; } set { compressed_data = value; } } public Gdk.Pixmap ServerPixmap { get { return server_pixmap; } set { server_pixmap = value; } } public Gdk.Pixbuf ClientPixbuf { get { return client_pixbuf; } set { client_pixbuf = value; } } public Gdk.Pixbuf MakePixbufFromCompressedData () { Gdk.Pixbuf pixbuf; using (Gdk.PixbufLoader loader = new Gdk.PixbufLoader ()) { loader.Write (compressed_data); loader.Close (); pixbuf = loader.Pixbuf; } return pixbuf; } private int xpos, ypos; private int width, height; private byte[] compressed_data; private Gdk.Pixmap server_pixmap; private Gdk.Pixbuf client_pixbuf; private void load (string filename) { using (FileStream fs = new FileStream (filename, FileMode.Open, FileAccess.Read)) { long length; bool done; long pos; byte[] buffer; length = fs.Length; compressed_data = new byte[length]; fs.Read (compressed_data, 0, (int) length); /* Feed small chunks at a time to a pixbuf loader until * it emits the "size-prepared" signal. This lets us * avoid uncompressing the whole image just to figure * out its size. */ using (Gdk.PixbufLoader loader = new Gdk.PixbufLoader ()) { loader.SizePrepared += new SizePreparedHandler ( delegate (object o, SizePreparedArgs args) { done = true; width = args.Width; height = args.Height; }); done = false; pos = 0; buffer = new byte[512]; while (!done) { long to_copy; to_copy = length - pos; if (to_copy > 512) to_copy = 512; else if (to_copy == 0) break; /* it sucks that there's no loader.Write (byte[], start_offset, length) */ Array.Copy (compressed_data, pos, buffer, 0, to_copy); loader.Write (buffer); pos += to_copy; } loader.Close (); } } } } class Rect { public static bool Intersects (int x1, int y1, int w1, int h1, int x2, int y2, int w2, int h2, out int x, out int y, out int w, out int h) { int xx, yy, ww, hh; int xr1, yb1, xr2, yb2; xx = x1 > x2 ? x1 : x2; yy = y1 > y2 ? y1 : y2; xr1 = x1 + w1; yb1 = y1 + h1; xr2 = x2 + w2; yb2 = y2 + h2; ww = (xr1 < xr2 ? xr1 : xr2) - xx; hh = (yb1 < yb2 ? yb1 : yb2) - yy; if (ww > 0 && hh > 0) { x = xx; y = yy; w = ww; h = hh; return true; } else { x = y = w = h = 0; return false; } } } interface IImagePolicy { void UpdateForArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h); void DrawInArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h); } class AlwaysInServerPolicy : IImagePolicy { public AlwaysInServerPolicy () { } public void UpdateForArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { Gdk.GC gc; if (image.ServerPixmap != null) return; image.ServerPixmap = new Gdk.Pixmap (drawable, image.Width, image.Height); gc = widget.Style.WhiteGC; image.ServerPixmap.DrawRectangle (gc, true, 0, 0, image.Width, image.Height); using (Gdk.Pixbuf pixbuf = image.MakePixbufFromCompressedData ()) { image.ServerPixmap.DrawPixbuf (gc, pixbuf, 0, 0, 0, 0, image.Width, image.Height, Gdk.RgbDither.Max, 0, 0); } image.CompressedData = null; // we have it in a pixmap now, so get rid of the compressed data } public void DrawInArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { int dx, dy, dw, dh; Gdk.GC gc; if (!Rect.Intersects (x, y, w, h, image.XPos, image.YPos, image.Width, image.Height, out dx, out dy, out dw, out dh)) return; gc = widget.Style.BlackGC; drawable.DrawDrawable (gc, image.ServerPixmap, dx - image.XPos, dy - image.YPos, dx, dy, dw, dh); } } class AlwaysUncompressedInClientPolicy : IImagePolicy { public AlwaysUncompressedInClientPolicy () { } public void UpdateForArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { if (image.ClientPixbuf != null) return; image.ClientPixbuf = image.MakePixbufFromCompressedData (); image.CompressedData = null; // we have it in a pixbuf now, so get rid of the compressed data } public void DrawInArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { int dx, dy, dw, dh; Gdk.GC gc; if (!Rect.Intersects (x, y, w, h, image.XPos, image.YPos, image.Width, image.Height, out dx, out dy, out dw, out dh)) return; gc = widget.Style.BlackGC; drawable.DrawPixbuf (gc, image.ClientPixbuf, dx - image.XPos, dy - image.YPos, dx, dy, dw, dh, Gdk.RgbDither.Max, image.XPos, image.YPos); } } class UncompressedInClientIfVisiblePolicy : IImagePolicy { public UncompressedInClientIfVisiblePolicy () { } public void UpdateForArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { int dx, dy, dw, dh; if (Rect.Intersects (x, y, w, h, image.XPos, image.YPos, image.Width, image.Height, out dx, out dy, out dw, out dh)) { if (image.ClientPixbuf == null) image.ClientPixbuf = image.MakePixbufFromCompressedData (); } else { if (image.ClientPixbuf != null) { image.ClientPixbuf.Dispose (); image.ClientPixbuf = null; } } } public void DrawInArea (Image image, Gtk.Widget widget, Gdk.Drawable drawable, int x, int y, int w, int h) { int dx, dy, dw, dh; Gdk.GC gc; if (!Rect.Intersects (x, y, w, h, image.XPos, image.YPos, image.Width, image.Height, out dx, out dy, out dw, out dh)) return; gc = widget.Style.BlackGC; drawable.DrawPixbuf (gc, image.ClientPixbuf, dx - image.XPos, dy - image.YPos, dx, dy, dw, dh, Gdk.RgbDither.Max, image.XPos, image.YPos); } } class ImageSet { public const int WRAP_WIDTH = 800; public const long MAX_COMPRESSED_MEM = 20 * 1024 * 1024; /* 20 MB of compressed images, maximum */ public ImageSet () { images = new ArrayList (); total_compressed_size = 0; total_height = 0; current_xpos = 0; current_ypos = 0; } public void AddImageFromFile (string filename) { Image image; image = new Image (filename); if (total_compressed_size + image.CompressedData.Length > MAX_COMPRESSED_MEM) return; total_compressed_size += image.CompressedData.Length; image.XPos = current_xpos; image.YPos = current_ypos; if (current_ypos + image.Height > total_height) total_height = current_ypos + image.Height; current_xpos += image.Width; if (current_xpos >= WRAP_WIDTH) { current_xpos = 0; current_ypos = total_height; } images.Add (image); } public ArrayList Images { get { return images; } } public int TotalHeight { get { return total_height; } } private ArrayList images; private long total_compressed_size; private int total_height; private int current_xpos; private int current_ypos; } class ImageWindow : Gtk.Window { public ImageWindow (ImageSet image_set, IImagePolicy policy, string title) : base (title) { this.image_set = image_set; this.policy = policy; sw = new Gtk.ScrolledWindow (null, null); this.Add (sw); darea = new Gtk.DrawingArea (); darea.SetSizeRequest (0, image_set.TotalHeight); darea.ExposeEvent += new ExposeEventHandler (darea_expose_event_cb); sw.AddWithViewport (darea); } private void darea_expose_event_cb (object o, ExposeEventArgs args) { Widget widget = (Widget) o; update_images_for_viewable_region (); foreach (Image image in image_set.Images) { // Console.Write ("[{0}{1}{2}]", // image.CompressedData == null ? "." : "C", // image.ServerPixmap == null ? "." : "P", // image.ClientPixbuf == null ? "." : "U"); policy.DrawInArea (image, widget, widget.GdkWindow, args.Event.Area.X, args.Event.Area.Y, args.Event.Area.Width, args.Event.Area.Height); } // Console.WriteLine (); args.RetVal = true; } protected override bool OnDeleteEvent (Gdk.Event ev) { Application.Quit (); return false; } private void update_images_for_viewable_region () { int width, height; int y_top; width = sw.Allocation.Width; // Not really accurate, but just we want an upper bound height = (int) sw.Vadjustment.PageSize; y_top = (int) sw.Vadjustment.Value; foreach (Image image in image_set.Images) { policy.UpdateForArea (image, darea, darea.GdkWindow, 0, y_top, width, height); } } private ImageSet image_set; private IImagePolicy policy; private ScrolledWindow sw; private DrawingArea darea; } class Driver { public static void Main (string[] args) { string dir_name; string policy_name; IImagePolicy policy; ImageSet image_set; Widget window; Application.Init (); if (args.Length != 2) { Console.WriteLine ("Usage: moz-images /dir/with/images [--server | --client | --compressed]"); Console.WriteLine (" --server Keeps all images as uncompressed pixmaps in the X server"); Console.WriteLine (" --client Keeps all images uncompressed in the client"); Console.WriteLine (" --compressed Keeps all images compressed in the client; uncompresses on demand"); Environment.Exit (1); } dir_name = args[0]; policy_name = args[1]; if (policy_name == "--server") policy = new AlwaysInServerPolicy (); else if (policy_name == "--client") policy = new AlwaysUncompressedInClientPolicy (); else if (policy_name == "--compressed") policy = new UncompressedInClientIfVisiblePolicy (); else { Console.WriteLine ("Please use \"--server\", \"--client\", or \"--compressed\""); Environment.Exit (1); policy = null; // shut up the compiler } image_set = new ImageSet (); load_images (image_set, dir_name); window = new ImageWindow (image_set, policy, "moz-images " + policy_name); window.ShowAll (); Application.Run (); } public static void load_images (ImageSet image_set, string dir_name) { load_images_with_pattern (image_set, dir_name, "*jpg"); load_images_with_pattern (image_set, dir_name, "*png"); load_images_with_pattern (image_set, dir_name, "*gif"); } public static void load_images_with_pattern (ImageSet image_set, string dir_name, string pattern) { string[] files; files = Directory.GetFiles (dir_name, pattern); foreach (string filename in files) { Console.WriteLine ("reading image {0}", filename); image_set.AddImageFromFile (filename); } } }