dW

Dec 3rd 2009

Scissors & Glue

EDIT: This is outdated. Opera 11 has introduced extensions, and this is better suited as an extension. Due to changes on iStockPhoto’s end the code hosted here won’t even work. I’ll point to an extension once I have a working one available.

One of the most powerful features of Opera is one that is not well-promoted by either Opera Software or its users. There are many features within the browser which feel abandoned and clunky, but this one isn’t the case.

User Javascript

User Javascript is a method for end users to modify the behavior of websites by running local JavaScripts. Opera’s User Javascript has existed since beta releases of Opera 8.0. In fact its inclusion in the beta was the inspiration for Greasemonkey, an add-on that adds limited user scripting support to Firefox. Opera’s implementation is far superior in capability, providing functionality similar to what would be required using an add-on in Firefox.

I personally would wish that Opera Software would promote this infrequently used feature by providing a place to download user scripts similar to what is already done with skins, widgets, and now Unite applications. There is userjs.org, but it hasn’t been maintained in years and many of the scripts are likely not to work anymore. EDIT: However, ExtendOpera is still sustained.1 There just needs to be one organized and moderated by Opera Software. One of the biggest criticisms of Opera is its absence of an add-on system similar to Firefox, but what many do not know is that the capabilities to reproduce the functionality of most Firefox add-ons are already present within Opera but just in different (and mostly superior) ways. It’s just not properly promoted, and that is quite unfortunate.

In addition management of User Javascript isn’t intuitive. Opera already contains methods for automatically installing skins and UI buttons like what I demonstrated in “Amazon’s Universal Wish List & Opera”. Something akin to those should exist for User Javascript to make installation of scripts painless. Also a management panel similar to the one available for Unite would be appreciated so that users can manage their installed scripts from within the browser. A Unite Application exists just for that purpose, but it looks like shit and really isn’t all that intuitive; however, it’s in active development and is improving quickly. Currently User Javascripts are managed by placing scripts within a specified folder whose preference setting is buried deep within the preferences dialog.

iStockGlue

Screenshot of iStockPhoto’s Zooming Toggle

Needless to say I’ve decided to write my own script, and it’s called “iStockGlue”. Necessity is usually the mother of invention, and frequently at work I have the necessity to mock up something to send to a customer. Typically I’m not given anything resembling an appropriate timeframe to complete the job either due to my employer’s greediness or due to a customer’s complete disregard for what I do combined with an ignorance of the process. I sometimes will fall back to iStockPhoto, download a comp of a stock image there, and send the customer a mockup utilizing that image. If approved my employer will purchase the image for the job. Typically when customers view these comps the first thing they ask is “What are the watermarks?” followed by “Why is the image so choppy?”. I can’t do anything about the watermarks, but I thought something could be done about the resolution. If logged in I’m usually able to zoom into the image, but there’s no way to download the entire zoomed image as it’s sliced into multiple images. Bummer.

I stumbled upon a Firefox add-on called iStockZOOM which allows me to download composites of the image slices. I’ve used it for quite some time, but I have a deep loathing of Firefox simply because it’s so damn slow, especially when starting up. The repetitive dialog boxes asking to update Firefox or some add-on along with the long wait for Firefox just to finish loading and get to a state where it can accept user input adds to the frustration. I need to get work done, not wait for a program to load or manually close dialog boxes to a nauseating degree. I finally decided to break down and program something that would work similarly except for Opera and not Firefox. iStockZOOM works well, but it’s clunky and feels like it’s a hastily slapped together hack. Having to clutter up my toolbar with an ugly UI button to activate it wasn’t pleasant either. However, it worked and provided me with a method of grabbing images. I just wanted something better while working in a faster piece of software.

Screenshot of iStockGlue’s Composite Image

I’ve written “iStockGlue” for that reason. If logged into iStockPhoto and having clicked on the image to zoom it automatically zooms in all the way, grabs the image slices, assembles them in an invisible canvas element, and then replaces the document with the image contained within a document structure resembling Opera’s standard image document. There’s no button to clutter your toolbar up, and there’s no added bullshit to the document:

// ==UserScript==
// @name iStockGlue
// @author Dustin Wilson
// @description Creates a composite of the iStockPhoto zoom image slices so it can be downloaded.
// @include http://*.istockphoto.com/stock-photo*.php
// @include http://*.istockphoto.com/stock-illustration*.php
// @include http://*.istockphoto.com/file_closeup.php*
// ==/UserScript==

/*
Copyright (c) 2009 Dustin Wilson

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

// To prevent any potential conflicts.
userJS=(typeof userJS=='undefined') ? {} : userJS;
userJS.iStockGlue={};

function hijackZoom(ev)
{
 // Grab the closest zooming level and then determine the images' div id number.
 userJS.iStockGlue.size=zoomFile.availSizes[zoomFile.availSizes.length-1];
 userJS.iStockGlue.div=(zoomFile.availSizes[userJS.iStockGlue.size]) ? zoomFile.availSizes[userJS.iStockGlue.size] : ((userJS.iStockGlue.size == 3) ? 2 : 1);

 // Reconstruct the zoom mechanism's events and force it to zoom all the way in.
 if(ev.target.id=='ZoomHoverDiv')
 {
  $("ZoomHoverDiv").stopObserving("mouseover");
  zoomFile.toggleZoomHover(true);
 }

 zoomFile.doMove(ev);
 zoomFile.nextSize=userJS.iStockGlue.size;
 zoomFile.doMove($('ZoomDraggableDiv'));
}

function hijackImages(ev)
{
 // Only do anything if the loading element is one of the zoom image slices.
 var evid=ev.event.target.id;
 if(ev.event.target.id!=null && ev.event.target.id.match(new RegExp('s'+userJS.iStockGlue.div+'r[0-9]+c[0-9]+')))
 {
  // Get the json, and if it's null stop processing to try again.
  userJS.iStockGlue.json=zoomFile.myRequest.transport.responseText;
  if(userJS.iStockGlue.json==null) return;

  // Grab all of the zoom image slices.
  userJS.iStockGlue.images=document.querySelectorAll('#ZoomDraggableDiv #s'+userJS.iStockGlue.div+' img');

  // Create the canvas element, resize it, but do not add it to the document.
  var c=document.createElement('canvas');
  c.setAttribute('width',/"targetWidth":(\d+),/.exec(userJS.iStockGlue.json)[1]);
  c.setAttribute('height',/"targetHeight":(\d+),/.exec(userJS.iStockGlue.json)[1]);
  var ctx=c.getContext('2d');

  var slice=new Array();
  // Iterate through each image and store it in an array.
  for(var loop=0;loop<userJS.iStockGlue.images.length;loop++)
  {
   img=new Array();
   img['obj']=userJS.iStockGlue.images[loop];
   img['id']=img['obj'].id;
   img['width']=img['obj'].style.width;
   img['height']=img['obj'].style.height;
   img['match']=/s[0-9]+r([0-9]+)c([0-9]+)/.exec(img['id']);
   img['x']=img['match'][2]*zoomFile.viewport.width-zoomFile.viewport.width/2;
   img['y']=img['match'][1]*zoomFile.viewport.height-zoomFile.viewport.height/2;
   img['url']=zoomFile.imgURL+'/file_inspector_view/'+zoomFile.fileID+'/'+userJS.iStockGlue.size+'/'+img['x']+'/'+img['y']+'/zoom_'+zoomFile.fileID+'.jpg';

   slice[loop]=new Image();
   slice[loop].src=img['url'];
   slice[loop]['position']=Position.positionedOffset(img['obj']);
  }

  // Remove the img array from memory.
  delete img;

  // Draw each image to the canvas.
  for(loop=0;loop<slice.length;loop++)
  {
   ctx.drawImage(slice[loop],slice[loop]['position'][0],slice[loop]['position'][1]);
   // Removing data from memory as we go.
   delete slice[loop];
  }

  // Hopefully remove as much as possible from memory before finishing.
  delete slice;

  // Remove the listener so it has no chance of firing again even when navigating history.
  opera.removeEventListener('AfterEvent.load',hijackImages,false);  

  document.write('<html><head><link rel="stylesheet" media="screen,projection,tv,handheld" href="opera:style/image.css"/></head><body><div><img src="'+c.toDataURL('image/jpeg',1)+'"/></div></body></html>');

  // Fire J. King's imagesizer if available.
  if(userJS.imgSizer)
   userJS.imgSizer.init();
 }
}

opera.addEventListener('AfterEvent.DOMContentLoaded',function(ev)
{
 // Remove the preset events for #ZoomImage and #ZoomHoverDiv then reconstruct them.
 var zoomImg=$('ZoomImage');
 var zoomHoverDiv=$('ZoomHoverDiv');

 if(zoomImg && zoomHoverDiv)
 {
  zoomImg.stopObserving('click');
  zoomHoverDiv.stopObserving('click');
  zoomImg.addEventListener('click',hijackZoom,false);
  zoomHoverDiv.addEventListener('click',hijackZoom,false);
 }
},false);

opera.addEventListener('AfterEvent.load',hijackImages,false);

User Javascript in Opera can be enabled by going to opera:config#UserPrefs|UserJavaScript, checking the box, and clicking on the “Save” button. A folder then needs to be specified, and can be done so at opera:config#UserPrefs|UserJavaScriptFile. User Javascripts — including this one — can be dropped into that folder to be executed by Opera.

Caveats

The script has some caveats, though. It has a tendency of using large amounts of memory and CPU cycles because images in numbers in excess of 100 or more can be loaded into memory, so Opera will be unresponsive during the latter part of the script’s running. However, indications of progress are available while Opera is busy as Opera’s progress indicator will provide feedback on how many elements are loaded in the document. There will be situations where the script will be unable to start due problems on iStockPhoto’s end such as when some of the image slices are missing. Additionally at times the script will fire despite image slices’ being missing; the resultant composite image will contain blocks of grey. Nevertheless, I have found that those situations are rare and usually are remedied in a short period of time on iStockPhoto’s end.

If Opera supported saving the canvas as an image by right-clicking on it like Firefox does the amount of memory required by this application can be reduced drastically because I wouldn’t have to use the toDataURL() method; it is the cause of the majority of the memory usage in this script.

I can’t guarantee this script will work, but it has worked for a few who’ve been testing it for me. I will maintain this code for as long as I see fit which might be never. Well, maybe not never. Ha. Hopefully Opera will create some respository which will make the process of maintaining this code a bit easier.

EDIT: Added code that allows the script to utilize Jeff King’s autosizer script. My script has also been uploaded to ExtendOpera. The script on this page should reflect the current version of iStockGlue despite any lack of edits to specify changes.


  1. Thanks to Andrey Petrov for pointing this out.