0

This is the code I am using. (code is at way bottom of this post but here is link to GitHubGist :: Noitidart / _ff-addon-snippet-browseForBadgeThenCreateSaveAnApply.js) It is copy pastatble to scratchpad (i tried fiddle but it needs privelage scope). When you run it will ask you to select a 16x16 image. Then it will take the firefox icon and put it on a canvas and then take the icon you browsed to and overlay it on the bottom right. Then it will convert it to .ico and save to your desktop as profilist16.ico and profilist32.ico. It will then change the icons of all your firefox windows.

After you do the above, please open a new firefox window and then in alt+tab you'll see the firefox logo of the badged icon is dirtier.

On the bottom you see the original canvas drawing (it looks blurry but i think thats my zoom level on firefox). The icon is crisp but if you notice the badged icon (on right) on the edges (especially top) you see dirt, like black jagged stuff which is not seen in the usual icon (at left)

comparing images

var win = Services.wm.getMostRecentWindow(null);
var me = win;
//these should be global vars
var sizes = []; //os dependent 
var img = {}; //holds Image for each size image
var osIconFileType = 'ico'; //os dependent
var cOS = 'Windows';

function badgeIt() {
    var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    fp.init(win, "Select Badge Image", Ci.nsIFilePicker.modeOpen);

    var fpCallback = function(rv) {
        if (rv == Ci.nsIFilePicker.returnOK || rv == Ci.nsIFilePicker.returnReplace) {
            if (sizes.length == 0) {
               //figure out what os this is and populate sizes withthe sizes needed for this os
               sizes = [32, 16]; //note: ask on SO how to determine what sizes the os uses for its icons?
            }
            loadBadgeImage();
        } else {
            //user did not select an file to badge with
        }
    }

    var ranOnce0 = false;
    var checkAllDefaultImagesLoaded = function() {
        for (var i=0; i<sizes.length; i++) {
            //console.log('img.sizes[i].loaded for i = ' + sizes[i] + ' is == ' + uneval(img[sizes[i]]));
            if (!img[sizes[i]] || !img[sizes[i]].loaded) {
                console.log('returning false as sizes[i]', sizes[i], 'is not loaded yet')
                return false; //return as not yet all are done
            }
            //me.alert('all img sizes loaded');
        }
        //ok all sizes loaded
        if (ranOnce0) {
            alert('already ranOnce0 so return false');
            return false;
        }
        ranOnce0 = true;
        return true;
    }

    var loadDefaultImages = function() {
        for (var i=0; i<sizes.length; i++) {
            img[sizes[i]] = {};
            img[sizes[i]].Image = new Image();
            img[sizes[i]].Image.onload = function(iBinded) {
                console.log('i', iBinded);
                //console.log('img', img);
                console.log('sizes[i]', sizes[iBinded]);
                console.log('img[sizes[iBinded]].loaded=', uneval(img[sizes[iBinded]]), 'will now set it to true')
                img[sizes[iBinded]].loaded = true;
                console.log('just loaded size of (sizes[iBinded]) = ' + sizes[iBinded]);
                var allLoaded = checkAllDefaultImagesLoaded();
                if (allLoaded == true) {
                    console.log('allLoaded == true so createAndSave')
                    createAndSaveIcons();
                } else {
                    console.warn('allLoaded is false so dont create')
                }
            }.bind(null, i)
            img[sizes[i]].Image.src = 'chrome://branding/content/icon' + sizes[i] + '.png';
        }

    }

    var loadBadgeImage = function() {
        console.log('loadBadgeImage')
        img.badge = {};
        img.badge.Image = new Image();
        img.badge.Image.onload = function() {
            console.log('bagde image loaded')
            img.badge.loaded = true;
            if (checkAllDefaultImagesLoaded()) {
                console.log('all dfault images PRELOADED so continue to createAndSaveIcons')
                createAndSaveIcons();
            } else {
                console.log('all default images not loaded so start loading them')
                loadDefaultImages();
            }
        }
        img.badge.Image.src = Services.io.newFileURI(fp.file).spec;
    }

    var badgedIconMade = {};
    var ranOnce = false;
    var checkAllBadgedIconsMade = function() {
       for (var i=0; i<sizes.length; i++) {
           if (!badgedIconMade[sizes[i]]) {
               return; //not yt done making
           }
       }
        if (ranOnce) {
            alert('already ranOnce so return');
            return;
        }
        ranOnce = true;
        // all badged icons made
        applyIcons();
    }

    var blobCallback = function(size) {
        return function (b) {
            var r = new FileReader();
            r.onloadend = function () {
                // r.result contains the ArrayBuffer.
                //alert(r.result)
                img[size].ArrayBuffer = r.result;
                badgedIconMade[size] = true;
                //checkAllBadgedIconsMade();
                Cu.import('resource://gre/modules/osfile.jsm');
                var writePath = OS.Path.join(OS.Constants.Path.desktopDir, 'profilist' + size + '.' + osIconFileType);
                console.log('writePath', writePath)
                var promise = OS.File.writeAtomic(writePath, new Uint8Array(r.result), {tmpPath:writePath + '.tmp'});
                promise.then(
                   function() {
                       //win.alert('success')
                       checkAllBadgedIconsMade();
                   },
                   function() {
                       //win.alert('failure')
                   }
                );
            };
            //var url = window.URL.createObjectURL(b)
            //img[size].blobUrl = url;
            //prompt('', url)
            r.readAsArrayBuffer(b);
        }
    }

    var createAndSaveIcons = function() {
        console.log('createAndSave')
       var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
       var ctx = canvas.getContext('2d');
       gBrowser.contentDocument.documentElement.appendChild(canvas);

       var badgeDim = { //holds key which is size of default icon, and the value is the dimension to draw the badge for that default icon size //this is set by me the dev, maybe make preference for this for user
           '16': 10,
           '32': 16
       };

       for (var i=0; i<sizes.length; i++) {
           canvas.width = sizes[i];
           canvas.height = sizes[i];
           ctx.clearRect(0, 0, sizes[i], sizes[i]);
           ctx.drawImage(img[sizes[i]].Image, 0, 0);
           if (sizes[i] in badgeDim) {
               if (badgeDim[sizes[i]] != sizes[i]) { //before i had `img.badge.Image.width` in place of `sizes[i]`, but can just use sizes[i] because thats the dim of the default icon duh
                  ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]], badgeDim[sizes[i]], badgeDim[sizes[i]]);
               } else {
                   //the redim size is same as icon size anyways so just draw it
                  ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]]);
               }
           } else {
               //sizes[i] is not in badgeDim meaning i dont care what size the badge is on this size of icon
               ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]]);
           }
           //canvas.mozFetchAsStream(mfasCallback(sizes[i]), 'image/vnd.microsoft.icon')
           canvas.toBlob(blobCallback(sizes[i]), "image/vnd.microsoft.icon", "-moz-parse-options:format=bmp;bpp=32");

       }
    }

    var applyIcons = function() {
        if (cOS == 'Windows') {
            Cu.import('resource://gre/modules/ctypes.jsm');

            var user32 = ctypes.open('user32.dll');

            /* http://msdn.microsoft.com/en-us/library/windows/desktop/ms644950%28v=vs.85%29.aspx
             * LRESULT WINAPI SendMessage(
             * __in HWND hWnd,
             * __in UINT Msg,
             * __in WPARAM wParam,
             * __in LPARAM lParam
             * );
             */
            var SendMessage = user32.declare('SendMessageW', ctypes.winapi_abi, ctypes.uintptr_t,
                ctypes.voidptr_t,
                ctypes.unsigned_int,
                ctypes.int32_t,
                ctypes.voidptr_t
            );

            /* http://msdn.microsoft.com/en-us/library/windows/desktop/ms648045%28v=vs.85%29.aspx
             * HANDLE WINAPI LoadImage(
             * __in_opt_  HINSTANCE hinst,
             * __in_      LPCTSTR lpszName,
             * __in_      UINT uType,
             * __in_      int cxDesired,
             * __in_      int cyDesired,
             * __in_      UINT fuLoad
             * );
             */
            var LoadImage = user32.declare('LoadImageA', ctypes.winapi_abi, ctypes.voidptr_t,
                ctypes.voidptr_t,
                ctypes.char.ptr,
                ctypes.unsigned_int,
                ctypes.int,
                ctypes.int,
                ctypes.unsigned_int
            );

            var IMAGE_BITMAP = 0;
            var IMAGE_ICON = 1;
            var LR_LOADFROMFILE = 16;

            var DOMWindows = Services.wm.getEnumerator(null);
            while (DOMWindows.hasMoreElements()) {
                var aDOMWindow = DOMWindows.getNext();
                var baseWindow = aDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                           .getInterface(Ci.nsIWebNavigation)
                                           .QueryInterface(Ci.nsIDocShellTreeItem)
                                           .treeOwner
                                           .QueryInterface(Ci.nsIInterfaceRequestor)
                                           .nsIBaseWindow;

                var nativeHandle = baseWindow.nativeHandle;
                var targetWindow_handle = ctypes.voidptr_t(ctypes.UInt64(nativeHandle));

                console.log('aappplying now')
                var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 32, 32, LR_LOADFROMFILE); //MUST BE A FILEPATH TO A ICO!!!
                var hIconSmall = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist16.' + osIconFileType), IMAGE_ICON, 16, 16, LR_LOADFROMFILE); //MUST BE A FILEPATH TO A ICO!!!

                var successSmall = SendMessage(targetWindow_handle, 0x0080 /** WM_SETICON **/ , 0 /** ICON_SMALL **/ , hIconSmall); //if it was success it will return 0? im not sure. on first time running it, and it was succesful it returns 0 for some reason
                var successBig = SendMessage(targetWindow_handle, 0x0080 /** WM_SETICON **/ , 1 /** ICON_BIG **/ , hIconBig); //if it was success it will return 0? im not sure. on first time running it, and it was succesful it returns 0 for some reason   

            }

            user32.close();
        }
    }

    fp.open(fpCallback);
}

badgeIt();
Noitidart
  • 35,443
  • 37
  • 154
  • 323
  • here are the bits of the `.ico`'s: http://jsfiddle.net/Noitidart/e7LQ9/ (i think lol) – Noitidart Jun 17 '14 at 03:39
  • This looks like the alpha channel is missing from the outputted image. I've not got time to look into it further at the moment but if you want to poke around a bit more I'd suggest trying to output a png from canvas.toBlob() and verifying that it contains an alpha channel. Perhaps there are some different parameters you can use in canvas.toBlob to ensure the output includes the channel. – Luckyrat Jun 17 '14 at 13:30

2 Answers2

2

Alright. This is actually quite reproducible, but only when using BMP icons, but not PNG icons.

Seems the icon encoder that Firefox ships is pretty bad/buggy indeed (for RGBA stuff). Well, actually it is the BMP encoder that the ICO encoder uses...

So since Belgium/Algeria (the game, football, not American) was mostly boring just now, I wrote my own icon encoder, which isn't too hard actually.

So here is my complete example code incl. icon encoder (just setting the 32x32 icon), but which lacks deposing of icons. But as a bonus, it shows how to set the icon via the WNDCLASS.

Cu.import('resource://gre/modules/ctypes.jsm');
Cu.import('resource://gre/modules/osfile.jsm');

let IMAGE_BITMAP = 0;
let IMAGE_ICON = 1;
let WM_SETICON = 128;
let GCLP_HICON = -14;

let user32 = ctypes.open('user32.dll');
let SendMessage = user32.declare(
    'SendMessageW',
    ctypes.winapi_abi,
    ctypes.intptr_t,
    ctypes.voidptr_t, // HWND
    ctypes.uint32_t, // MSG
    ctypes.uintptr_t, // WPARAM
    ctypes.intptr_t // LPARAM
);
let CreateIconFromResourceEx = user32.declare(
    'CreateIconFromResourceEx',
    ctypes.winapi_abi,
    ctypes.voidptr_t,
    ctypes.uint8_t.ptr, // icon
    ctypes.uint32_t, // size
    ctypes.int32_t, // icon
    ctypes.uint32_t, // dwVersion
    ctypes.int, // dx
    ctypes.int, // dy
    ctypes.uint32_t // flags
);
let SetClassLongPtr = user32.declare(
    ctypes.intptr_t.size == 8 ? 'SetClassLongPtrW' : 'SetClassLongW',
    ctypes.winapi_abi,
    ctypes.uintptr_t,
    ctypes.voidptr_t, // HWND
    ctypes.int, // index
    ctypes.uintptr_t // value
);

let gdi32 = ctypes.open('gdi32.dll');
let DeleteObject = gdi32.declare(
    'DeleteObject',
    ctypes.winapi_abi,
    ctypes.int,
    ctypes.voidptr_t // Object
);

let setPerWindow = false;

let badges = [
    'chrome://browser/skin/places/starred48.png',
    'chrome://browser/skin/places/downloads.png',
    'chrome://browser/skin/places/tag.png',
    'chrome://browser/skin/places/livemark-item.png',
    'chrome://browser/skin/places/query.png',
    'chrome://browser/skin/pluginInstall-64.png',
    'chrome://browser/skin/pluginInstall-16.png',    
];

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

Task.spawn(function* setIcon() {
    "use strict";
    try {
       let p = Promise.defer();
       let img = new Image();
       img.onload = () => p.resolve();
       img.src = 'chrome://branding/content/icon32.png';
       yield p.promise;

       p = Promise.defer();
       let badge = new Image();
       badge.onload = () => p.resolve();
       badge.src = badges[getRandomInt(0, badges.length - 1)];
       console.log(badge.src);
       yield p.promise;

       let canvas = document.createElementNS(
          'http://www.w3.org/1999/xhtml',
          'canvas');
       canvas.width = img.naturalWidth;
       canvas.height = img.naturalHeight;
       let ctx = canvas.getContext('2d');
       ctx.drawImage(img, 0, 0);
       let onethird = canvas.width / 3;
       ctx.drawImage(
          badge,
          onethird,
          onethird,
          canvas.width - onethird,
          canvas.height - onethird);

       // Our own little ico encoder
       // http://msdn.microsoft.com/en-us/library/ms997538.aspx
       // Note: We would have been able to skip ICONDIR/ICONDIRENTRY,
       // if we were to use CreateIconFromResourceEx only instead of also
       // writing the icon to a file.
       let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
       let XOR = data.length;
       let AND = canvas.width * canvas.height / 8;
       let size = 22 /* ICONDIR + ICONDIRENTRY */ + 40 /* BITMAPHEADER */ + XOR + AND;
       let buffer = new ArrayBuffer(size);

       // ICONDIR
       let view = new DataView(buffer);
       view.setUint16(2, 1, true); // type 1
       view.setUint16(4, 1, true); // count;

       // ICONDIRENTRY
       view = new DataView(buffer, 6);
       view.setUint8(0, canvas.width % 256);
       view.setUint8(1, canvas.height % 256);
       view.setUint16(4, 1, true); // Planes
       view.setUint16(6, 32, true); // BPP
       view.setUint32(8, 40 + XOR + AND, true); // data size
       view.setUint32(12, 22, true); // data start

       // BITMAPHEADER
       view = new DataView(buffer, 22);
       view.setUint32(0, 40, true); // BITMAPHEADER size
       view.setInt32(4, canvas.width, true);
       view.setInt32(8, canvas.height * 2, true);
       view.setUint16(12, 1, true); // Planes
       view.setUint16(14, 32, true); // BPP
       view.setUint32(20, XOR + AND, true); // size of data

       // Reorder RGBA -> BGRA
       for (let i = 0; i < XOR; i += 4) {
          let temp = data[i];
          data[i] = data[i + 2];
          data[i + 2] = temp;
       }
       let ico = new Uint8Array(buffer, 22 + 40);
       let stride = canvas.width * 4;
       // Write bottom to top
       for (let i = 0; i < canvas.height; ++i) {
          let su = data.subarray(XOR - i * stride, XOR - i * stride + stride);
          ico.set(su, i * stride);
       }

       // Write the icon to inspect later. (We don't really need to write it at all)
       let writePath = OS.Path.join(OS.Constants.Path.desktopDir, 'icon32.ico');
       yield OS.File.writeAtomic(writePath, new Uint8Array(buffer), {
          tmpPath: writePath + '.tmp'
       });

       // Cut off ICONDIR/ICONDIRENTRY for CreateIconFromResourceEx
       buffer = buffer.slice(22);
       let hicon = CreateIconFromResourceEx(
          ctypes.uint8_t.ptr(buffer),
          buffer.byteLength,
          IMAGE_ICON,
          0x30000,
          0,
          0,
          0);
       if (hicon.isNull()) {
          throw new Error("Failed to load icon");
       }
       if (setPerWindow) {
           let DOMWindows = Services.wm.getEnumerator(null);
           while (DOMWindows.hasMoreElements()) {
              let win = DOMWindows.getNext().QueryInterface(Ci.nsIInterfaceRequestor).
                 getInterface(Ci.nsIWebNavigation).
                 QueryInterface(Ci.nsIDocShellTreeItem).
                 treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
                 getInterface(Ci.nsIBaseWindow);
              let handle = ctypes.voidptr_t(ctypes.UInt64(win.nativeHandle));
              if (handle.isNull()) {
                 console.error("Failed to get window handle");
                 continue;
              }
              var lparam = ctypes.cast(hicon, ctypes.intptr_t);
              var oldIcon = SendMessage(handle, WM_SETICON, 1, lparam);
              if (ctypes.voidptr_t(oldIcon).isNull()) {
                 console.log("There was no old icon", oldIcon.toString());
              }
              else {
                 console.log("There was an old icon already", oldIcon.toString());
                 // In a perfect world, we should actually kill our old icons
                 // using DeleteObject...
              }
           }
       }
       else {    
           let win = Services.wm.getMostRecentWindow(null).
              QueryInterface(Ci.nsIInterfaceRequestor).
              getInterface(Ci.nsIWebNavigation).
              QueryInterface(Ci.nsIDocShellTreeItem).
              treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
              getInterface(Ci.nsIBaseWindow);
           let handle = ctypes.voidptr_t(ctypes.UInt64(win.nativeHandle));
           if (handle.isNull()) {
               throw new Error("Failed to get window handle");
           }
           let oldIcon = SetClassLongPtr(handle, GCLP_HICON, ctypes.cast(hicon, ctypes.uintptr_t));
           if (ctypes.voidptr_t(oldIcon).isNull()) {
               console.log("There was no old icon", oldIcon.toString());
           }
           else {
               console.log("There was an old icon already", oldIcon.toString());
               // In a perfect world, we should actually kill our old icons
               // using DeleteObject...
           }
       }
       console.log("done", badge.src);
    } 
    catch (ex) {
       console.error(ex);
    }
});

PS: Here is a screenshot from the Task Switcher on XP:

Composed icons as displayed in XP Task Switcher

Composed icons as displayed in Win7 Task Switcher

nmaier
  • 32,336
  • 5
  • 63
  • 78
  • Oh my gosh so much awesome stuff to learn from in here! Thanks man!!! Should we report on bugzilla that they need to fix their bmp encoder? – Noitidart Jun 17 '14 at 21:06
  • The bonus via `WNDCLASS` is SUPERB!!! I was having to run jsctypes every window open but not with this way, with this way new windows automatically take the icon!! – Noitidart Jun 17 '14 at 21:06
  • I don't undertsand this line: `ctypes.intptr_t.size == 8 ? 'SetClassLongPtrW' : 'SetClassLongW',` what are you testing for and why the difference? I checked MSDN I can't figure it out :( – Noitidart Jun 17 '14 at 21:07
  • Oh man do you have win xp machine? Can you please help with this issue at ask.m.o: https://ask.mozilla.org/question/781/winxp-crashing-on-this-line-of-js-ctypes-no-idea-why/ – Noitidart Jun 17 '14 at 21:21
  • 1
    `SetClassLongPtr` is a real function on 64-bit (`.size == 8`), but is just a `#define SetClassLongPtrW SetClassLongW` on 32-bit, so there isn't actually a `SetClassLongPtrW` in win32, but there is in win64. And I don't really have a WinXP machine, but I have a partly broken virtual machine LOL Maybe that's enough. :p – nmaier Jun 17 '14 at 21:24
  • Thanks man. haha about ur vm :P Also question: I noticed you only made 32x32 icon, is there anyway i can put in 16x16 and other sizes into that same icon? The 16x16 icon thats now displaying in the corner of windows seems awkward. – Noitidart Jun 18 '14 at 04:36
  • 1
    Well, execute the same code again, this time composing a small icon (`icon16.png` or whatever). Change the `SendMessage` call appropriately (`ICON_SMALL`, which my code actually incorrectly used, D'oh), and/or `SetClassLongPtr(..., GCLP_HICONSM, ...)` – nmaier Jun 18 '14 at 13:49
  • This code is so awesome @nameir thanks for it, im using it right now to put multiple images into an icon, so one file can support 16x16 and 32x32, 48,48, etc etc! :) Ill share when i finish :D – Noitidart Jan 04 '15 at 20:31
  • Woohoo thanks for such an awesome solution. I used it to create a function that takes paths to images and it will then convert it to ico and save to desktop: [GitHubGIST :: Noitidart / MakeIcoOfPaths.js](https://gist.github.com/Noitidart/e3d9f61bd268368df37a) thanks a million man! Now my challenge is to do this for XPM for Linux and ICNS for Mac haha wish me luck I might come knocking Stackoveflow's door for help :P – Noitidart Jan 06 '15 at 10:38
0

Tested the code on Win7, the icon once applied is real real crappy. In image below, we see the icon in canvas is perfect. In the alt+tab menu the first icon is SUPER crappy, second icon is unbadged so perfect, and third and fifth icons are normal crappy.

Image is here: https://i.stack.imgur.com/47dIr.png

Edit: Fixed the SUPER crappiness by changing this line for win7, it was 32, 32, i made it 256, 256, no clue why it fixed the SUPER crap:

var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 256, 256, LR_LOADFROMFILE);

before it was: var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 32, 32, LR_LOADFROMFILE); However the usual crap, black rough edge remains.

  • 1
    Your "fix" just upscaled and then downscaled again the icon, hiding artifacts in the process as *details* are lost due to the scaling. Not really a solution... – nmaier Jun 17 '14 at 19:53
  • Your accepted solution above works nice on WinXP but on Win7 it is giving low quality image, its weird one way works for xp and other for 7: http://i.stack.imgur.com/zADBB.png (looking at the firefox logo in bg, the badge is resized so thats of course blurry no sweat) – user3749566 Jun 17 '14 at 23:12
  • Seems to be a combination of lower color mode and the color conversation WIndows has to do and some hinting (AND-mask?) in the original ico (from `firefox.exe`). Also the canvas code might do something stupid in this case (the canvas IIRC does not keep raw-data but only composed surfaces, which may be device dependent). – nmaier Jun 18 '14 at 13:36