Greasy Fork is available in English.

IMDB UTE: IMDB Ultimate Thumbnail Expander

Expands thumbnail images in IMDB into larger floating preview images on mouse hover.

// ==UserScript==
// @name           IMDB UTE: IMDB Ultimate Thumbnail Expander
// @description    Expands thumbnail images in IMDB into larger floating preview images on mouse hover.
// @namespace      http://myxp.anandkumar.me
// @include        http://www.imdb.com/*
// @grant          GM_getValue
// @grant          GM_setValue
// @grant          GM_registerMenuCommand
// @grant          GM_log
// @grant          GM_addStyle
// @version        1.2.5
// ==/UserScript==

/**
Change Log:
Date      Version    Notes
20131011  1.2.5      *Reposition loaded image on a timer.
20130827  1.2.4      *Revert 1.2.1 since script was corrected by author.
20130804  1.2.3      *Updated rules for video thumbnail images.
20130426  1.2.2      *Changed require URLs to https.
20130108  1.2.1      *Moved the "gm_config extras" script with corrections to anandkumar.me domain.
20120829  1.2.0      *Added @grant directives to comply with Greasemonkey 1.0.
                     *Removed dependence on custom updater since Greasemonkey now has an internal updater.
                     *Copied the floating image repositioning logic from my Flickr script.
                     *Moved all floating image related code to a single class. This is the first step towards unifying my other scripts that use the same logic.
20120606  1.1.0      *Fine-tuned the image positioning algorithm.
                     *Added "Ctrl" key override to persist preview image.
20120604  1.0.2      *Added support for a delay timer and a corresponding configuration item under settings.
                     *Added support for the setting the preview image to "Snap to Cursor".
20111112  1.0.1      *Added missing escaping to some reg-expressions.
20111103  1.0.0      *Official release of script.
 */
var DEBUG     = false;
var newWidth  = "200";
var newHeight = "200";
var GMImg_position   = "absolute";
var GMImg_marginleft = "0";
var GMImg_margintop  = "0";
var GMImg_left   = "";
var GMImg_top    = "";
var GMImg_right  = "";
var GMImg_bottom = "";
var prevTarget = null;
var floatimg;
var timerRunning = false, timerID = null;
var global_mouse_x=0, global_mouse_y=0;
var persistPreview = false;

//From Config//
var settings_max_width, settings_max_height, settings_auto_size, settings_snap_to_cursor;
var settings_expand_actorimgs,settings_expand_titleimgs,settings_expand_previewimgs;
var settings_x_factor,settings_y_factor;
var settings_timer_ms;

var xhrCnt = 0;
var debug_msg_div = "flute_debug_msg_div";

var fio;
/**
 **Config screen
*/
var lang = GM_config.gets('lang','en'); // get the language - or set it to 'en' if it was not yet stored
GM_config.init('Configuration for IMDB UTE',{
    timer_ms : { label: 'Preview Delay (milliseconds)', type: 'int', default: 0, min: 0, max: 10000 },
    max_width: { label: 'Max Display Width:',section:['Preview Image properties'], type: 'int', default: 640, min: 50, max: 5000 },
    max_height:{ label: 'Max Display Height:', type: 'int', default: 480, min: 50, max: 5000 },
    auto_size: { label: 'Enable Auto size:', type: 'checkbox', default: true },
    x_factor : { label: 'Auto-size Screen Width % :', type: 'int', default: 60, min: 1, max: 100 },
    y_factor : { label: 'Auto-size Screen Height %:', type: 'int', default: 60, min: 1, max: 100 },
    snap_to_cursor : { label: 'Snap to Cursor', type: 'checkbox', default: false }
    /*expand_actorimgs  : { label: 'Actor Images', section:['Control which images get expanded'], type: 'checkbox', default: true },
    expand_titleimgs  : { label: 'Title Images', type: 'checkbox', default: true },
    expand_previewimgs: { label: 'Preview Images', type: 'checkbox', default: true }*/
  },
  GM_config.eCSS,
  {
    open: function() {
      var idx=0;
      GM_config.addBorder(); // add a fancy border
      GM_config.resizeFrame('480px','360px'); // resize the config window
      GM_config.addTooltip(idx++,'Set the delay (in milliseconds) to wait before displaying the preview image.');
      GM_config.addTooltip(idx++,'The maximum Width, in pixels, of the displayed image. Use this if you want to retreive large images but want to constrain the displayed image size.');
      GM_config.addTooltip(idx++,'The maximum Height, in pixels, of the displayed image. Use this if you want to retreive large images but want to constrain the displayed image size.');
      GM_config.addTooltip(idx++,'When enabled, the maximum width and height of the displayed image is automatically constrained to two-thirds of the browser window size. Selecting this option will ignore the custom maximum size settings above.');
      GM_config.addTooltip(idx++,'If Auto-Size is enabled, this option specifies the screen Width % to use.');
      GM_config.addTooltip(idx++,'If Auto-Size is enabled, this option specifies the screen Height % to use.');
      GM_config.addTooltip(idx++,'Enable this option to make the preview image follow the mouse cursor.');
      // GM_config.sections2tabs(); // convert the sections to tabs
      GM_config.fadeOut(); //Fadeout the rest of the screen.
    },
    save: function() { 
        //Update the settings and close the dialog
        GetUserSettings();
        //GM_config.fadeIn();
        GM_config.close();
    },
    close: function() {
        GM_config.fadeIn();
        //GM_config.close();
    }
  }
);

/**
 **Main code starts here
*/
(function(){
    try{
        if (typeof unsafeWindow != 'object') {
          window.unsafeWindow = window;
        }
        window.addEventListener('load'     ,function(event){Init  ();},true);
        window.addEventListener('mousemove',function(event){DoMouseOver(event);},true);
        window.addEventListener('keydown'  ,function(event){DoKeyDown  (event);},true);
        window.addEventListener('keyup'    ,function(event){DoKeyUp    (event);},true);
    }
    catch(e){
        GM_log(e.message);
    }
    GM_registerMenuCommand('imute: Configuration',GM_config.open);
})();

/**
 * Get the user-settings for this script.
 */
function GetUserSettings(){
    //Correct any incorrect settings
    
    //Read the settings into local variables.
    settings_max_width  = GM_config.get('max_width');
    settings_max_height = GM_config.get('max_height');
    settings_auto_size  = GM_config.get('auto_size');
    settings_x_factor   = GM_config.get('x_factor');
    settings_y_factor   = GM_config.get('y_factor');
    settings_snap_to_cursor = GM_config.get('snap_to_cursor');
    settings_timer_ms       = GM_config.get('timer_ms');
    //settings_expand_actorimgs  = GM_config.get('expand_actorimgs');
    //settings_expand_titleimgs  = GM_config.get('expand_titleimgs');
    //settings_expand_previewimgs = GM_config.get('expand_previewimgs');
    
    //Perform any validation and re-calculation of the retreived settings here.
    settings_x_factor = settings_x_factor/100;
    settings_y_factor = settings_y_factor/100;
}

/**
 * Handle all the initializations here.
 */
function Init(){
    var isInIFrame = (window.location != window.parent.location) ? true : false;
    if(isInIFrame) return;
    //Get the user's settings
    GetUserSettings();
    //Create and attach the floating image.
    var newlink;
    var settingsimg

    fio = new FloatImg();
    
    //Create the settings icon
    settingsimg = document.createElement('IMG');
    settingsimg.className="shadow";
    settingsimg.id="imute_settings_icon";
    settingsimg.alt="imute settings";
    settingsimg.src="data:image/png;base64,"+ "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";
    settingsimg.width  = "32";
    settingsimg.height = "32";
    settingsimg.loaded = "false";
    settingsimg.addEventListener('click', function(){GM_config.open();}, true);
    document.body.appendChild(settingsimg);
    
    var debugmsg = document.createElement('div');
    debugmsg.id  = debug_msg_div;
    document.body.appendChild(debugmsg);
    
    //Set defaults.
    GMImg_left = "1";
    GMImg_top  = "1";
    GMImg_position  = "fixed";
    AddScriptCSS();
    
    RepositionTimer();
}

/**
 * Handles the global mouseover event.
 * The main entry function from where everything is kicked-off.
 */
function DoMouseOver(e){
    //If the floating image is not yet initialized, exit.
    if(fio==null) return;
    //Get the event variable.
    if (!e) var e = window.event;
    var tg = (window.event) ? e.srcElement : e.target;
    //Extract the mouse co-ordinates
    GetCoordinate(e);
    //If the mouse is over the floatimg, either accidentally or deliberately, ignore it.
    if(tg.id == fio.getId()) return;
    //If the preview image must be repositioned upon mouse move, do it here:
    if(settings_snap_to_cursor){
        if(persistPreview==false){
            fio.reposition();
        }
        else{
            return;
        }
    }
    //If this mouseover event is for the same element as before, exit.
    //We are only interested when the mouse enters/leaves an element.
    if(prevTarget !=null && tg == prevTarget) {
        return;
    }
    //Store the current target to compare against in the next mouse over event.
    prevTarget = tg;
    
    //If the mouse is over any "other" element, hide the floating image.
    ImgDoMouseOut();
    //If the target does not have a valid image, url, exit.
    var img_src = GetDisplayImageURL(tg);
    if(img_src == "") return;

    //If the current image does not meet criteria, exit.
    if(!canBeExpanded(img_src) || img_src.match(/\/nopicture|nophoto\//)){
        return;
    }
    /**END of validations**/
    
    //Once all the validations are passed, kick-start the image preview process.
    fio.srcElem = tg;
    StartImagePreviewTimer(tg);
}


/**
 * Starts the preview image display timer.
 */
function StartImagePreviewTimer(tg){
    //First, stop the global timer, if it's running.
    if(timerRunning){
       StopImagePreviewTimer();
    }
    timerRunning = true;
    timerID = setTimeout(function(){ShowImagePreview(tg);}, settings_timer_ms);
    timerID = setTimeout(function(){ShowImagePreview(tg);}, settings_timer_ms);
}

/**
 * Stops the active global timer.
 */
function StopImagePreviewTimer(){
    timerRunning = false;
    if(timerID!=null)
        clearTimeout(timerID);
}

/**
 * Before the new image loads, the image size is not known. This results in the image not positioned correctly until the user moves the mouse.
 * So, keep repositioning the image on a timer.
 */
function RepositionTimer(){
    if(fio != null && fio.isLoaded()){
        fio.reposition();
    }
    setTimeout(function(){RepositionTimer();}, 100);
}

/**
 * The main function to call to begin the process of popping-up the preview image.
 * Accepts an event object's target (usually, an image node).
 */
function ShowImagePreview(tg){
    StopImagePreviewTimer();
    
    //Invalidate current and older XHRs.
    xhrCnt++;
    //Get the expanded image url.
    var zoomedUrl = GetPreviewURL(tg, 1000, settings_max_width, settings_max_height);
    
    //message("zoomedUrl="+zoomedUrl,false,true);
    if(zoomedUrl != ""){
        //Assign the unchecked image URL so that the image displays immediately.
        fio.setUrl(zoomedUrl);
        if(persistPreview==false){
            //Do not reposition if the user is currently pressing the ctrl-key down.
            fio.reposition();
        }
    }
    else{
        fio.clear();
        return;
    }
    
    //If the image is surrounded by an anchor tag, copy its href to the floatimg's parent anchor tag.
    //This helps if the user wants to click the image but the floating image gets "in the way".
    if(tg.parentNode && tg.parentNode.href){
        fio.setLinkUrl(tg.parentNode.href);
    }
    else if(tg.href){
        fio.setLinkUrl(tg.href);
    }
    
    //Finally, show the image.
    fio.show();
}


/**
 * Hide the floating image once the mouse cursor leaves an element.
 * This function is not event-driven by the browser but is called when
 * "DoMouseOver()" detects a change of target.
 */
function ImgDoMouseOut(){
    if(persistPreview==false){
        //Stop any timers:
        StopImagePreviewTimer();
        
        //If the floating image is not yet created, exit.
        if(fio==null) return;
        fio.clear();
    }
}

/**
 * Event handler that captures control key presses.
 */
function DoKeyDown(e){
    if (e.ctrlKey){
        persistPreview = true;
    }
}

/**
 * Event handler that cleans-up actions done during the keydown actions.
 */
function DoKeyUp(e){
    if (e.keyCode == 17){
        persistPreview = false;
    }
}


/**
 * Add CSS for our custom elements.
 */
function AddScriptCSS(){
    //Common style
    var GM_style = 
      "img.GM_thumbnail { display:none; "+
        "height:auto !important;"+
        "width:auto !important; "+
        "position:   " + GMImg_position   + "   ; " +
        "margin-left:" + GMImg_marginleft + "px ; " +
        "margin-top:"  + GMImg_margintop  + "px ; " +
        "z-index:9999;"+
        "background-color:transparent !important;"+
        (GMImg_left  ==""?"":"left:"  +GMImg_left  + "px ; ") +
        (GMImg_top   ==""?"":"top:"   +GMImg_top   + "px ; ") +
        (GMImg_bottom==""?"":"bottom:"+GMImg_bottom+ "px ; ") +
        (GMImg_right ==""?"":"right:" +GMImg_right + "px ; ") +
        "} ";
    GM_style += 
        ".shadow {"+
            "-moz-box-shadow: 3px 3px 4px #000;"+
            "-webkit-box-shadow: 3px 3px 4px #000;"+
            "box-shadow: 3px 3px 4px #000;"+
        "}";
    GM_style += 
        "img#imute_settings_icon{ "+
            "position:fixed;"+
            "right:0px;"+
            "bottom:0px;"+
            "opacity:0.75"+
        "}";
    GM_style +=
        "#"+debug_msg_div+"{ "+
            "position:fixed;"+
            "left:0px;"+
            "top:0px;"+
            "background-color:white;"+
            "width:100%;"+
            "z-index:99999;"+
        "}";
    GM_addStyle(GM_style);
}


/**
 * Checks if the image can be expanded or not.
 */
function canBeExpanded(img_url){
    var bRet = false;
    if(img_url.match(/\_CR(\d+),(\d+),(\d+),(\d+)/)){ //For imdb hosted images.
        bRet = true;
    }
    else if(img_url.match(/\_SX\d+/) || img_url.match(/\_SY\d+/)){
        bRet = true;
    }
    else if(img_url.match(/\_AA\d+\_/)){ //For amazon hosted images for imdb.
        bRet = true;
    }
    else if (img_url.match(/\_SS\d+/)){
        bRet = true;
    }
    else if(img_url.match(/fbcdn-profile(.*)_q.jpg$/)){
        //Expand facebook profile images
        bRet = true
    }
    //message(img_url+".match(/fbcdn-profile(.*)_q.jpg$/) = "+img_url.match(/fbcdn-profile(.*)_q.jpg$/),true,true);
    return bRet;
}


/**
 * Given a target element, this function returns the image url associated with the element.
 * Note: The image url returned is not validated.
 */
 function GetDisplayImageURL(elem){
    var url = "";
    if (elem.nodeName == 'IMG'){
        url = elem.src;
    }
    else if (elem.nodeName == 'LI' || elem.nodeName == 'DIV'){
        if(elem.style!= null && elem.style.backgroundImage!= null) {
            try{
                url = elem.style.backgroundImage.match(/\"(.*)\"/)[1];
            }catch(e){
                d(e);
                return "";
            }
        }
    }
    return url;
 }
 

/**
* Custom zooming function for imdb after MANY trial and error!
**/
function GetPreviewURL(tg,newSize,imgWidth,imgHeight){
  var zoomedUrl;
  var url = GetDisplayImageURL(tg);
  //message("url="+url,true,true);
  if(url == "") return;
  
  //Try to expand facebook profile pictures.
  if(url.match(/fbcdn-profile(.*)_q.jpg$/)){
        zoomedUrl = url.replace(/_q.jpg$/,"_b.jpg");
        message("zoomedurl="+zoomedUrl,true,true);
        return zoomedUrl;
  }
  
  var urlSX = url.match(/\_SX\d+/)==null?"":url.match(/\_SX(\d+)/)[1];
  var urlSY = url.match(/\_SY\d+/)==null?"":url.match(/\_SY(\d+)/)[1];
  var urlSS = url.match(/\_SS\d+/)==null?"":url.match(/\_SS(\d+)/)[1];;
  //var imgType = url.split('.').pop();
  var urlC1 = "", urlC2 = "", urlC3 = "", urlC4 = "";
  var urlRatio, imgRatio;
  var newX, newY, newWidth, newHeight;
  var cropArr = url.match(/\_CR(\d+),(\d+),(\d+),(\d+)/);
  if (cropArr != null){
    urlC1 = cropArr[1];
    urlC2 = cropArr[2];
    urlC3 = cropArr[3];
    urlC4 = cropArr[4];
    newX = urlC1;
    newY = urlC2;
    /*urlRatio  = 1.00;
    newWidth  = urlC3;
    newHeight = urlC4;
    if (urlC1 != ""){
      urlRatio = newWidth/newHeight;
    }*/
  }
  else if(urlSX !="" || urlSY != ""){
    //No CR found. Using SX, SY
    /*newWidth  = parseInt(urlSX==""?newSize:urlSX);
    newHeight = parseInt(urlSY==""?newSize:urlSY);
    urlRatio  = newWidth / newHeight;*/
  }
  
  imgWidth  = parseInt(imgWidth);
  imgHeight = parseInt(imgHeight);
  imgRatio  = imgWidth / imgHeight;
  
  //Actual Zooming takes place here
  if (imgRatio < 1.0){
    newHeight  = newSize;
    newWidth = Math.round(newSize * imgRatio);
  }
  else if (imgRatio > 1.0){
    newWidth  = newSize;
    newHeight = Math.round(newSize / imgRatio);
  }
  else{
    newWidth  = newSize;
    newHeight = newSize;
  }
  
  if(urlC1 != "" && urlC1 != "0") {
    newX = parseInt(urlC1) + parseInt(newWidth);
  }
  if(urlC2 != "" && urlC2 != "0") {
    newY = 0 + parseInt(urlC2) + parseInt(newHeight);
  }

  /*
  GM_log("urlC1 = "+urlC1+", urlC2 = "+urlC2+", urlC3 = "+urlC3+", urlC4 = "+urlC4);
  GM_log( "urlRatio="+urlRatio+" , newWidth="+newWidth+" newHeight="+newHeight );
  */
  zoomedUrl = url;
  /*If we have both the CR and SS, just change the SS to preserve correct cropping.*/
  if(zoomedUrl.match(/\_CR\d+/) != null && zoomedUrl.match(/\_SS\d+_/) != null){
    zoomedUrl = zoomedUrl.replace(/\_SS\d+\_/, "_SS"+newSize+"_");
  }
  else{
    //zoomedUrl = zoomedUrl.replace(/_CR\d+,\d+,\d+,\d+_/, "_CR" + newX + "," + newY + "," + newWidth + "," + newHeight + "_");
    zoomedUrl = zoomedUrl.replace(/\_CR\d+,\d+,\d+,\d+\_/g, "_CR" + newX + "," + newY + ",0,0" + "_");
    zoomedUrl = zoomedUrl.replace(/\_SS\d+\_/, "");
  }
  /*Always replace SX and SY, if present.*/
  zoomedUrl = zoomedUrl.replace(/\_SY\d+\_/, "_SY"+newHeight+"_");
  zoomedUrl = zoomedUrl.replace(/\_SX\d+\_/, "_SX"+newWidth+"_");
  /*Prevent too large an image from being used by Squaring it.*/
  if(zoomedUrl.match(/\_CR\d+/) != null){
    if(urlC3 < newWidth || urlC4 < newHeight){
      zoomedUrl = zoomedUrl.replace(/\_SS\d+\_/, "");
    }
    else{
      zoomedUrl = zoomedUrl.replace(/\_SS\d+\_/, "_SS"+newSize+"_");
    }
  }
  if(url.match(/\_AA\d+\_/)){ //For amazon hosted images for imdb.
    zoomedUrl = zoomedUrl.replace(/\_AA(\d+)\_/, "_");
  }
  
  /*Erase any Border Overlays in the URL to get a clean image*/
  zoomedUrl = zoomedUrl.replace(/\_BO.+\_/, "");
  zoomedUrl = zoomedUrl.replace(/_PIimdb\-blackband\-.+\_/,"");
  
  /*Zero-out the SP param (needs more research)*/
  zoomedUrl = zoomedUrl.replace(/_SP\d+,\d+/,"_SP0,0");
  
  return zoomedUrl;
}


/**
 * Helper function that parses and stores the mouse positon from an event object.
 */
function GetCoordinate(e){
    global_mouse_x = e.clientX + document.body.scrollLeft;
    global_mouse_y = e.clientY + document.body.scrollTop;
    return true
}


/**
 * Custom debug output function for writing to Error Console.
 */
function d(msg,force){
    if(force==null) force=false;
    
    if(DEBUG==true || force==true){
        GM_log(msg);
    }
}

/**
 * Custom debug output function that displays log output on the webpage.
 */
function message(msg,clearPrevious,force){
    if(force==null) force=false;
    if(clearPrevious==null) clearPrevious=true;
    
    if(DEBUG == true || force == true){
        var msgdiv = document.getElementById(debug_msg_div);
        if(clearPrevious){
            msgdiv.innerHTML = msg;
        }
        else{
            msgdiv.innerHTML += "<br />"+msg;
        }
    }
}

/**
 * Common class representing the floating preview image container.
 */
function FloatImg(img){
    var element; //The image element represented by this class;
    var link_element; //The (optional) anchor element.
    var link_url; //The url to navigate to when the image is clicked.
    var srcElem; //From which element was the image url captured.
    var loaded;
    var id;
    var width, height; //Image dimensions.
    /*var url; //The image url.
    var top, bottom, left, right; //Image style co-ordinates.
    var maxWidth, maxHeight; //Image style dimentions.*/
    var thisObj = this;
    
    this.getImageElement = GetImageElement;
    this.setLinkElement  = SetLinkElement;
    this.getId       = GetId;
    this.setUrl      = LoadImg;
    this.clear       = Clear;
    this.show        = Show;
    this.reposition  = RepositionPreviewImage;
    this.setSrc      = SetSrc;
    this.setLinkUrl  = SetLinkUrl;
    this.setTop      = SetTop;
    this.getTop      = GetTop;
    this.setBottom   = SetBottom;
    this.getBottom   = GetBottom;
    this.setLeft     = SetLeft;
    this.getLeft     = GetLeft;
    this.setRight    = SetRight;
    this.getRight    = GetRight;
    this.setWidth    = SetWidth;
    this.getWidth    = GetWidth;
    this.setHeight   = SetHeight;
    this.getHeight   = GetHeight;
    this.setMaxWidth = SetMaxWidth;
    this.getMaxWidth = GetMaxWidth;
    this.setMaxHeight= SetMaxHeight;
    this.getMaxHeight= GetMaxHeight;
    this.isLoaded    = GetLoaded;
    this.setLoaded   = SetLoaded;

    //Perform object initialization.
    //"img" is the floating image element.
    if(img){
        try{
            this.element = img;
            this.id      = img.id;
            this.Clear();
        }
        catch(e){
            d(e.message);
        }
    }
    else{
        //Try to create a floating image.
        this.element = document.createElement('IMG');
        this.element.className = 'GM_thumbnail shadow';
        this.element.id  = "floatimg";
        this.element.src = "";
        this.element.alt = "Loading...";
        
        //Now create the surrounding link element.
        this.link_element = document.createElement('A');
        this.link_element.id="floatimglink";
        this.link_element.href="";
        this.link_element.appendChild(this.element);
        document.body.appendChild(this.link_element);
    }

    /**
     * Function to initiate loading of an image into the floating image container.
     * @param callback A function to call once the image has been loaded into cache. If given, then the image is displayed only once the image has been downloaded.
     */
    function LoadImg(url,callback) {
        if(callback){
            var img  = new Image();
            var that = this;
            img.onload = function(){
                /*message("New height="+this.height,true,false);
                message("New width="+this.width,true,true);*/
                that.height = this.height;
                that.width  = this.width;
                if(callback){
                    that.setSrc(url);
                    that.setLoaded(true);
                    callback();
                }
                return true;
            };
            img.onerror = function(){
                d('Image '+url+' could not be loaded...');
            };
            
            img.src = url;
            this.setLoaded(false);
        }
        else{
            this.clear(true);
            this.element.src = url;
            this.setLoaded(true);
        }
    }
    
    /**
     * Clears the image url and link, and hides the image element.
     */
    function Clear(onlyImg){
        //Clear the image.
        this.setLoaded(false);
        this.element.src = "";
        this.element.style.display='none';
        
        if(onlyImg==null || onlyImg == false){
            //Clear the link.
            this.link_url = "";
        }
    }
    
    function Show(){
        this.element.style.display = 'block';
    }
    
    /**
     * This function resizes and positions the floating preview image node.
     * The function assumes that the image is already loaded or is already loading.
     * TODO::Declare local variables for the "settings_*" variables inside the class.
     */
    function RepositionPreviewImage(){
        var pageWidth  = PageWidth();
        var pageHeight = PageHeight();
        var maxWidth, maxHeight;
        var mouseX, mouseY;
        var cursorPadding;
        mouseX = global_mouse_x;
        mouseY = global_mouse_y;
        cursorPadding = 20;
        
        //Now to compute the display size of the floating image.
        if(settings_auto_size){
            maxWidth  = Math.floor(Math.max(100,pageWidth  * settings_x_factor));
            maxHeight = Math.floor(Math.max(100,pageHeight * settings_y_factor));
        }
        else{
            maxWidth  = Math.floor(Math.max(settings_max_width ,1));
            maxHeight = Math.floor(Math.max(settings_max_height,1));
        }
        //We only set the maximum size for the image. This way, the image will keep its aspect ratio.
        this.setMaxWidth (maxWidth);
        this.setMaxHeight(maxHeight);
        
        //Reset the IMG position based on mouse position.
        if(settings_snap_to_cursor){
            var imgWidth=0, imgHeight=0, availableWidth=0, availableHeight=0;
            var displaySide = {horizontal: "right", vertical: "bottom"};
            var newTop = 0;
            
            //Find out the maximum available size in the x and y axes.
            availableWidth  = Math.max(pageWidth  - (int(mouseX) + int(cursorPadding) ), (mouseX - cursorPadding) );
            availableHeight = Math.max(pageHeight - (int(mouseY) + int(cursorPadding) ), (mouseY - cursorPadding) );
            
            //Resize the image container if the available area is not enough.
            this.setMaxWidth( Math.min(availableWidth,maxWidth) );
            //fio.setMaxHeight( Math.min(availableHeight,maxHeight) );
            
            //Do this after manipulating the image's max size. Width and Height are only available after.
            imgWidth  = this.getWidth();
            imgHeight = this.getHeight();
            
            //Choose which side of the cursor the image should be displayed.
            if( (int(imgWidth)+int(cursorPadding)) > (pageWidth-mouseX) ){
                displaySide.horizontal = "left";
            }
            /*if( (int(imgHeight)+int(cursorPadding)) > (pageHeight-mouseY) ){
                displaySide.vertical = "top";
            }*/
            
            //Now to position the image.
            //First, decide on the horizontal position for the image.
            if(displaySide.horizontal == "left"){
                this.setRight( int(mouseX) - int(cursorPadding) );
                this.setLeft( int(mouseX) - int(cursorPadding) - int(imgWidth) );
            }
            else if(displaySide.horizontal == "right"){
                this.setLeft( int(mouseX) +int(cursorPadding) );
                this.setRight( int(mouseX) + int(cursorPadding) + int(imgWidth) );
            }
            
            //Next, decide on the vertical position for the image.
            /*
             *Here's my logic with this one (after MANY trial-and-error!):
             * By default, we want the image to appear below the mouse cursor. So, we set the preview's top to the mouseY, plus some padding.
             * Next, we check if, given this new top position, the image will go further than the page's height.
             * If so, we pull up the image's top by the amount that it exceeds the page's height.
             * This will make the preview image's bottom exactly align with the page's bottom automatically.
             * TODO::Check if, after correcting the image's bottom, the image's top goes above the screen. If so, shrink the image by the overflowing amount.
             */
            newTop = int(mouseY) + int(cursorPadding);
            if(newTop + imgHeight > pageHeight){
                newTop = newTop - (newTop+imgHeight - pageHeight);
            }
            this.setTop( int(newTop) );
            
        }
        else{
            //If the image is set not to follow the cursor, choose which corner of the screen to position the image in.
            //First, decide on the horizontal corner for the image.
            if(mouseX < pageWidth/2){
                this.setLeft(); //Set to "auto"
                this.setRight(1);
            }
            else{
                this.setRight(); //Set to "auto"
                this.setLeft(1);
            }
            //Next, decide on the vertical corner for the image.
            if(mouseY < pageHeight/2){
                this.setTop(); //Set to "auto"
                this.setBottom(1);
            }
            else{
                this.setTop(1);
                this.setBottom(); //Set to "auto"
            }
        }
    }

    /**
     * Returns the page width in pixels
     */
    function PageWidth() {
        return window.innerWidth != null? window.innerWidth: document.body != null? document.body.clientWidth:null;
    }

    /**
     * Returns the page height in pixels
     */
    function PageHeight() {
        return window.innerHeight != null? window.innerHeight: document.body != null? document.body.clientHeight:null;
    }

    /**
     * Returns the integer representation of a string.
     */
    function int(str){
      return parseInt(str);
    }

    
    function SetSrc(src){
        this.element.src = src;
    }
    
    function GetId(){
        return this.id;
    }
    
    function SetLinkElement(elem){
        this.link_element = elem;
    }
    
    function SetLinkUrl(url){
        this.link_element.href = url;
    }
    
    function GetLinkUrl(){
        return this.link_element.href;
    }
    
    function GetImageElement(){
        return this.element;
    }
    
    function GetHeight(){
        return this.element.height;
    }
    
    function SetHeight(height){
        this.element.height = height;
    }
    
    function GetWidth(){
        return this.element.width;
    }
    
    function SetWidth(width){
        this.element.width = width;
    }
    
    function GetMaxWidth(){
        return this.element.style.maxWidth;
    }
    
    function SetMaxHeight(height){
        this.element.style.maxHeight = height+"px";
    }
    
    function GetMaxHeight(){
        return this.element.style.maxHeight;
    }
    
    function SetMaxWidth(width){
        this.element.style.maxWidth = width+"px";
    }
    
    function GetMaxWidth(){
        return this.element.style.maxWidth;
    }
    
    function GetTop(){
        return this.element.style.top;
    }
    
    function SetTop(top){
        if(top){
            this.element.style.top = top+"px";
        }
        else{
            this.element.style.top = "auto";
        }
    }
    
    function GetBottom(){
        return this.element.style.bottom;
    }
    
    function SetBottom(bottom){
        if(bottom){
            this.element.style.bottom = bottom+"px";
        }
        else{
            this.element.style.bottom = "auto";
        }
    }
    
    function GetLeft(){
        return this.element.style.left;
    }
    
    function SetLeft(left){
        if(left){
            this.element.style.left = left+"px";
        }
        else{
            this.element.style.left = "auto";
        }
    }
    
    function GetRight(){
        return this.element.style.right;
    }
    
    function SetRight(right){
        if(right){
            this.element.style.right = right+"px";
        }
        else{
            this.element.style.right = "auto";
        }
    }
    
    function GetLoaded(){
        return this.loaded;
    }
    
    function SetLoaded(loaded){
        this.loaded = loaded;
    }
}