BDSMLR - clickable links to original high-res images and display timestamps

This script modifies images to link to their original ("-og") high-res version. The link is available as soon as a border appears around an image. The color of the box indicates the range of the image height. In addition, the script unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only).

Versão de: 26/03/2021. Veja: a última versão.

// ==UserScript==
// @name         BDSMLR - clickable links to original high-res images and display timestamps
// @namespace    bdsmlr_linkify
// @version      3.2
// @license      GNU AGPLv3
// @description  This script modifies images to link to their original ("-og") high-res version. The link is available as soon as a border appears around an image. The color of the box indicates the range of the image height. In addition, the script unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only).
// @author       marp
// @homepageURL  https://greasyfork.org/en/users/204542-marp
// @include      https://bdsmlr.com/
// @include      https://bdsmlr.com/?group*
// @include      https://bdsmlr.com/dashboard*
// @include      https://bdsmlr.com/?latest*
// @include      https://*.bdsmlr.com/
// @include      https://*.bdsmlr.com/post/*
// @include      https://bdsmlr.com/search/*
// @include      https://*.bdsmlr.com/search/*
// @include      https://bdsmlr.com/blog/*
// @include      https://bdsmlr.com/originalblogposts/*
// @include      https://bdsmlr.com/likes*
// @include      https://bdsmlr.com//*
// @run-at document-end
// ==/UserScript==

// jshint esversion:8


//console.info("START href: ", window.location.href);



//------------------------------------------------------------
// FIRST PART OF SCRIPT #2 - function that gets called by event oberver that is registered as part of 1st part #1 (see very end of this script)
//------------------------------------------------------------

function createImageLinks(myDoc, myContext) {

//console.info("createImageLinks: ", myContext);
  
  if (myDoc===null) myDoc = myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext = myDoc;
  
  var tmpstr;
  var singlematch;
  var origpostlink;
  var origbloglink;
  var origblog;
  var matches, matches2;
  var imageurl;
  var imagesrc;
  var cdnmatches;
  var cdnnumber;

  // iterate over all posts within the supplied context
  matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')]"
                           + " | " +
                           "./descendant-or-self::div[contains(@class,'post_content')]"
                           + " | " +
                           "./descendant-or-self::div[contains(@class,'commenttext')]"
                           , myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  for(var i=0, el; (i<matches.snapshotLength); i++) {
    el = matches.snapshotItem(i);
    if (el) {
      try {
        
        // try to find info about original poster (if this is a reblog) as well as the link to the individual (potentially reblogged) post
        // both info only seem to be present on dashboard and on rightside overlay blogs - but not always on individual blogs (xxx.bdsmlr.com) or on individual blog post URLs :-(
        // This info is needed because in VERY early BDSMLR days the full resolution "-og" image sometimes only existed on the original blog hostname...
        // ...and these very old posts are still around...
        origblog = null;
			  singlematch = myDoc.evaluate(".//div[contains(@class,'originalposter')]/a[contains(@href,'.bdsmlr.com/post/')]" +
                                     " | " +
                                     ".//div[contains(@class,'original')]/a[contains(@href,'.bdsmlr.com')]",
                                     el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com/post/yyyyyyyy
        if (origpostlink) {
          tmpstr = origpostlink.getAttribute("href"); //everything after and including "/post" gets truncated away later anyway
          if ( tmpstr && (tmpstr.length > 10) &&
              !(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
            origblog = tmpstr;
          }
        }
        if (origblog === null) {
          //second method might find the originial blog URL (xxxx.bdsmlr.com) - however, often this is just a re-poster - not the original
          singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]//i[contains(@class,'retweet') or contains(@class,'rbthis')]" + 
                                       "/following-sibling::a[contains(@class,'adata') or contains(@class,'ndata')]",
                                       el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
          origbloglink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
          if (origbloglink) {
          	tmpstr = origbloglink.getAttribute("href");
            if ( tmpstr && (tmpstr.length > 10) &&
                 !(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
                origblog = tmpstr;
            }
          }
          if (origblog === null) {
            // if neither of the two above find anything then this is likely NOT a reblogged post but the original post -> get the orginial blog post URL
            singlematch = myDoc.evaluate(".//a[(contains(@class,'adata') or contains(@class,'ndata')) and contains(@href,'.bdsmlr.com/post/')]",
                                         el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
            origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
            if (origpostlink) {
          	  tmpstr = origpostlink.getAttribute("href");
              if ( tmpstr && (tmpstr.length > 10) &&
                   !(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...")
                  origblog = tmpstr;
              }
            }
            if (origblog === null) {
              if ( !window.location.href.startsWith("https://bdsmlr.com") ) {
                // if no link to neither original blog nor original blog post was found then we assume that this is the original blog post or blog itself (this is a rather shaky assumtion - fingers crossed...)
                origblog = window.location.href;
              }
            }
          }
        } // if none of the above worked then we're out of luck and origblog remains null
        

				// iterate over all links to images (i.e. does NOT (yet) create links to images where none exist in the first place)
        // skip over items that already have a link to a "non-cdn" bdsmlr url or that are not bdsmlr links at all
        matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + 
                                    "//a[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]/img" +
                                  " | " +
                                  ".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + 
                                    "//div[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]/img" +
                                  " | " +
                                  ".//div[contains(@class,'image_container') or contains(@class,'image_content')]//div[contains(@class,'textcontent')]" + 
                                    "//img[contains(@src,'https://cdn') and contains(@src,'.bdsmlr.com')]" +
                                  " | " +
                                  "./descendant-or-self::div[contains(@class,'singlecomment') or contains(@class,'commenttext')]" + 
                                    "//img[contains(@src,'https://cdn') and contains(@src,'.bdsmlr.com') and (contains(@class,'fr-dib') or contains(@class,'fr-fic'))]"
                                  ,
                                  el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        for(var j=0, image, imageparent; (j<matches2.snapshotLength); j++) {
          image=matches2.snapshotItem(j);
          if (image) {
            imagesrc = image.src;
            imageparent = image.parentNode;
            imageurl = imageparent.getAttribute("href");
            if (imageurl === null || imageurl.length < 5) {
              imageurl = image.getAttribute("src");
              // No idea why this is needed... DevTools inspector always shows a valid image src attribute... but at script execution time... apparently not... seems to be some bdsmlr JavaScript post-processing...
              if (imageurl === null || imageurl.length < 5) {
                imageurl = image.getAttribute("data-echo"); 
              }  
            }
            if (imageurl && imageurl.length > 5) {
              getBestImageUrlPromise(imagesrc, imageurl, origblog, image)
                .then( (result) => {
                
                	if ( (result !== null) && (result.image !== null) && (result.url !== null) ) {
                    var linkelem;
                    var divelem;
                    // Sometimes, images in comments are weirdly structured in hierarchies with paragraph elements -> it's better to create the link as direct parent of the image itself
                    // same goes for the element that the colored border box can be added to - there is no appropriate div element - so we use the image itself
                    if (  (result.image.parentNode.nodeName.toLowerCase() == "p")  ||
                         ((result.image.previousSibling !== null) && (result.image.previousSibling.nodeName.toLowerCase() == "p")) ||
                         ((result.image.nextSibling !== null) && (result.image.nextSibling.nodeName.toLowerCase() == "p")) ) {
                      linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image, result.url);  
                      divelem = result.image;
                    } else { 
                      linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image.parentNode, result.url);
                      var divmatch = result.image.ownerDocument.evaluate("./ancestor::div[(contains(@class,'hide') or contains(@class,'earlycomments')) and ancestor::div[contains(@class,'post')]]",
                                                                         linkelem, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
                      divelem = divmatch.singleNodeValue; 
                    }

                    divelem.style = "border: 5px solid grey;";
                    divelem.setAttribute("title", getSizeText(result.size)); 

                    getImageDimensionsPromise(result.url, divelem, result.size)
                      .then( (result2) => {

                        result2.element.style = "border: 5px solid " + result2.color + ";";
                        result2.element.setAttribute("title", getSizeText(result2.size) + " - " + result2.width + " x " + result2.height); 
                    });
                  };
              });
            }
          }
        }
        
        
        // multi-image posts - unhide all images (instead of having to manually click on "show x more images"
        matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + 
                                    "/div[contains(@style,'display:none')]",
                                 el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        for(var j=0, node; (j<matches2.snapshotLength); j++) {
          node=matches2.snapshotItem(j);
          if (node) {
            node.style.display = "initial";
          }
        }
        // multi-image posts - hide the "show x more images" element
        matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + 
                                    "/following-sibling::div[contains(@class,'viewAll')]",
                                 el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        for(var j=0, node; (j<matches2.snapshotLength); j++) {
          node=matches2.snapshotItem(j);
          if (node) {
            node.style.display = "none";
          }
        }
        
          
      } catch (e) { console.warn("error: ", e); }
    }
	}

}


// try to find the timestamp info and display in upper right corner
function displayTimestamps(myDoc, myContext) {

//console.info("displayTimestamps: ", myContext);
  
  if (myDoc===null) myDoc = myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext = myDoc;
  
  var matches;
  var tmpstr;
  var singlematch;
  var postinfo;
  var timestamp;
  var newnode;
  
  matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'feed') and @title]",
                           myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  for(var i=0, el; (i<matches.snapshotLength); i++) {
    el = matches.snapshotItem(i);
    if (el) {
      try {

        timestamp = el.getAttribute("title");
				if (timestamp && timestamp.length>5 && timestamp.length<70) {

          singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]",
                                       el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
          postinfo = singlematch.singleNodeValue; 
          if (postinfo) {

            newnode = myDoc.createElement("div");
            newnode.setAttribute("style", "float:right; margin-right: 10px; padding-right: 10px;");
            newnode.innerHTML = timestamp;
            postinfo.appendChild(newnode);
          }
        }
  
      } catch (e) { console.warn("error: ", e); }
    }
	}
}



function getOriginalPosterImageURL(imageurl, originalposter) {
  if (originalposter === null) {
    return imageurl;
  }
  var pos = imageurl.toLowerCase().indexOf(".bdsmlr.com");
  var pos2 = originalposter.toLowerCase().indexOf(".bdsmlr.com");
  if (pos > 0 && pos2 > 0) {
    return originalposter.substring(0, pos2) + imageurl.substring(pos);
  } else {
    return imageurl;
  }
}


function isOriginalImageURL(imageurl) {
  if (imageurl === null) {
    return false;
  }
  var tmpstr = imageurl.toLowerCase();
  if (tmpstr.includes("bdsmlr.com/")) {
    var pos = tmpstr.lastIndexOf(".");
    var pos2 = tmpstr.lastIndexOf("-og.");
    if (pos > 0 && (pos2+3)==pos) {
      return true;
    }
  }
  return false;
}


function getOriginalImageURL(imageurl) {
  if (imageurl === null) {
    return imageurl;
  }
  var pos = imageurl.lastIndexOf(".");
  var pos2 = imageurl.lastIndexOf("-og.");
  if (pos > 0 && (pos2+3)!=pos) {
    return imageurl.substring(0, pos) + "-og" + imageurl.substring(pos);
  } else {
    return imageurl;
  }
}


function insertOrChangeLinkElement(myDoc, wrapElement, linkTarget) {
  if (wrapElement.nodeName.toLowerCase() == "a") {
    wrapElement.setAttribute("href", linkTarget);
    wrapElement.setAttribute("target", "_blank");
    return wrapElement;
  } else {
    var parentNode = wrapElement.parentNode;
    var newNode = myDoc.createElement("a");
    newNode.setAttribute("href", linkTarget);
    newNode.setAttribute("target", "_blank");
    parentNode.replaceChild(newNode, wrapElement);
    newNode.appendChild(wrapElement);
    return newNode;
  }
}


function getSizeText(sizeInBytes) {
  if (sizeInBytes === null) {
    return "";
  }
  if (sizeInBytes >= 1048576) {
    return (sizeInBytes / 1048576).toFixed(1) + " MB";
  }
  else if (sizeInBytes >= 1024) {
    return (sizeInBytes / 1024).toFixed(0) + " KB";
  }
  else {
    return sizeInBytes.toFixed(0) + " B";
  }
}



// This ASYNC method returns a promise to retrieve the HTTP response header data for the supplied URL.
// It uses an "HTTP HEAD" request which does NOT download the response payload (to minimize network traffic)
async function checkUrlHeaderOnlyPromise(url, allowredirect) {

  return fetch(url, (allowredirect ? { redirect: 'follow', method: 'HEAD' } : { redirect: 'error', method: 'HEAD' } ) ).then(
    function(response) {
      if (response.ok) {
        var contentLength = parseInt(response.headers.get('Content-Length'), 10);
        if (isNaN(contentLength))
          contentLength = 0;
        if (response.redirected && allowredirect) {
          return { url: response.url, size: contentLength }
        } else {
          return { url: url, size: contentLength };
        }
      } else {
        return { url: url, size: -1 };
      }
    },
    function(rejectreason) {
      return { url: url, size: -1 };
    });
}





// This ASYNC method returns a promise to determine the url of the originally uploaded image (the one with a suffix of "-og" in thre name)
// This requires "testing" a lot of URLs, i.e. it causes server traffic (especially if it is an image on the "old" CDN servers from BDSMLR's early times).
// To minimize network traffic, this method only requests the HTTP headers for all these URLs - the image itself nor error webpages (404) are fully downloaded.
// If multiple "-og" variants are found, the onre with the largest size (in bytes, not image dimensions!) is chosen. 
// If the og version is smaller than the non-og version it still sticks with the og (the non-og is typically upscaled and worse quality than the og)
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise then function of the caller
async function getBestImageUrlPromise(imageurl, linkurl, blogurl, imageelement) {
  var knownImagePromise;
  var urlsToCheckPromises = [];
  var matches;
  var imageurl_cdnnum;
  var linkurl_cdnnum;
  var imageurl_cdnnumstr;
  var linkurl_cdnnumstr;
  var imageurl_path;
  var linkurl_path;
  var blogurl_base;
  var knownImageResult;
  var bestImageUrl;
  var bestImageSize;
  var bestImageIsOG;

//console.info("checkAllUrlheaders-Enter: ", "imageurl:" + imageurl + "   linkurl:" + linkurl + "   blogurl:" + blogurl);
  
  // get CDN image server number as int and as string
  matches = imageurl.toLowerCase().match("https?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/"); 
  if (matches !== null) {
    imageurl_cdnnumstr = matches[1];
    imageurl_cdnnum = parseInt(matches[1], 10);
  }  else {
    imageurl_cdnnum = NaN;
  }
  // get CDNO image server number as int and as string
  matches = linkurl.toLowerCase().match("https?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/"); 
  if (matches !== null) {
    linkurl_cdnnumstr = matches[1];
    linkurl_cdnnum = parseInt(matches[1], 10);
  }  else {
    linkurl_cdnnum = NaN;
  }

  // get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
  matches = imageurl.toLowerCase().match("https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)");
  if (matches !== null) {
    imageurl_path = matches[1];
  } else {
    imageurl_path = null;
  }
  // get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
  matches = linkurl.toLowerCase().match("https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)"); 
  if (matches !== null) {                                                            
    linkurl_path = matches[1];
  } else {
    linkurl_path = null;
  }
  
  // get base hostname for the originating blog
  if (blogurl !== null) {
    matches = blogurl.toLowerCase().match("https?:\/\/([^./]+\.bdsmlr\.com)"); 
    if (matches !== null) {
      blogurl_base = matches[1];
    } else {
      blogurl_base = null;
    }
  } else {
    blogurl_base = null;
  }
  
  // fetch promise for the image that is currently shown in the webpage (ALLOW redirection on this URL)
  knownImagePromise = checkUrlHeaderOnlyPromise(imageurl, true);

  // image to which the current unmodified link in the webpage is pointing to (ALLOW redirection on this URL)
  urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(linkurl, true));
  
  // Use a Set to collect all other URLs that are to be tested - this automatically eliminates dups that the URL selection logic below might create
  var uniqueset = new Set();

  // append "-og" suffix to the original image and link urls (without modifying the hostname)
  uniqueset.add(getOriginalImageURL(imageurl));
  uniqueset.add(getOriginalImageURL(linkurl));

  if (isNaN(imageurl_cdnnum) || imageurl_cdnnum <= 5) {
    //old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location) 
    //  -> "Wild West" as to where the "-og" image variant might be "hiding"
    uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + imageurl_path));
		if (blogurl_base !== null) {
      uniqueset.add(getOriginalImageURL("https://" + blogurl_base + imageurl_path));
    }  
    uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + imageurl_path));
  } 

  if (isNaN(linkurl_cdnnum) || linkurl_cdnnum <= 5) {
    //old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location) 
    //  -> "Wild West" as to where the "-og" image variant might be "hiding"
    uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + linkurl_path));
		if (blogurl_base !== null) {
      uniqueset.add(getOriginalImageURL("https://" + blogurl_base + linkurl_path));
    }  
    uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + linkurl_path));
  } 
    
  if (!isNaN(imageurl_cdnnum) && imageurl_cdnnum > 5) {
    //new CDN servers (cdn06+)
    uniqueset.add(getOriginalImageURL("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
  } 
    
  if (!isNaN(linkurl_cdnnum) && linkurl_cdnnum > 5) {
    //new CDN servers (cdn06+)
    uniqueset.add(getOriginalImageURL("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
  } 
    
  for (var str of uniqueset) {
    // some BDSMLR image servers have faulty redirections that are circular - causing huge delay and not resulting in anything useful
    // Thus, all URLs in the Set are to be tested WITHOUT allowing redirection
    urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(str, false));
  }
  
  // Get the  data for the image currently shown in the webpage as starting point
  knownImageResult = await knownImagePromise;
  bestImageUrl = knownImageResult.url;
  bestImageSize = knownImageResult.size;
  bestImageIsOG = isOriginalImageURL(bestImageUrl);

  // wait until all URLs have resolved (i.e. have their HTTP headers loaded with image or error info)
  await Promise.allSettled(urlsToCheckPromises).
    then( (results) => results.forEach((result) => {
      // If this result is a better image than currently known - replace and use this one as next best known image
      if ( (isOriginalImageURL(result.value.url) && !bestImageIsOG && (result.value.size > 0)) ||
           ( (result.value.size > bestImageSize) && (isOriginalImageURL(result.value.url) == bestImageIsOG) ) ) {
        bestImageSize = result.value.size;
        bestImageUrl = result.value.url;
        bestImageIsOG = isOriginalImageURL(bestImageUrl);
      }  
    })); 
    
  return {url: bestImageUrl, size: bestImageSize, isOG: bestImageIsOG, image: imageelement}; 
}



// This ASYNC method get the natural dimensions of the supplied image
// This means the image needs to be downloaded fully, unfortunately
// Thus, a delay is to be expected, except if the image is already cached
// Depending on the image height,m the method suggests a "markup color" and then discards the downloaded image again.
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise then function of the caller
async function getImageDimensionsPromise(imageurl, divelement, imagesize) {
  var image;
  var imageH;
  var imageW;
  var color;
  
  // sanity check - skip full download of image if it is larger than 20MB
  if ( (imagesize !== null) && (imagesize > 20971520) ) {
    return {url: imageurl, element: divelement, width: "unknown", height: "unknown", color: "Grey", size:imagesize}; 
  }
    
  image = new Image();
  image.src = imageurl;
  
  await image.decode().then(function() {
    imageH = image.naturalHeight;
    imageW = image.naturalWidth;
  });
  
  image.src = "data:,"; // clear the image now that we no longer need it

  if (imageH >= 2160) {
    color = "SpringGreen";
  } else if (imageH >= 1080) {
    color = "Green";
  } else if (imageH >= 810) {
    color = "YellowGreen";
  } else if (imageH >= 540) {
    color = "Yellow";
  } else if (imageH >=270 ) {
    color = "Orange";
  } else if (imageH < 270 && imageH > 0 ) {
    color = "Red";
  } else {
    color = "Grey";
  };
    
  return {url: imageurl, element: divelement, width: imageW, height: imageH, color: color, size:imagesize}; 
}







//------------------------------------------------------------
// FIRST PART OF SCRIPT #1 - initial statement and registration of event observer
//------------------------------------------------------------
// script runs NOT in the context of an image - i.e. dashboard, blog stream, individual post
// -> register event observer for future to-be-loaded posts (endless scrolling) and execute first part of script (createImageLinks & displayTimestamps) on already loaded posts

// fix buggy redirection from BDSMLR
if  ( window.location.href.includes('bdsmlr.com//') ) {
  var tmpstr = window.location.href;
  var pos = tmpstr.indexOf('bdsmlr.com//');
  // remove the double //
  window.location.assign( tmpstr.substring(0, pos+11) + tmpstr.substring(pos+12) );
}
// prevent running for URL to image or media
else if ( !(window.location.href.includes('bdsmlr.com/uploads/')) ) {
  
  // create an observer instance and iterate through each individual new node
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      mutation.addedNodes.forEach(function(addedNode) {
        createImageLinks(mutation.target.ownerDocument, addedNode);
        displayTimestamps(mutation.target.ownerDocument, addedNode);
      });
    });    
  });

  // configuration of the observer
  // "theme1" is the class used by the feed root node for individual user's blog (xxxx.bdsmlr.com) -> seems unstable/temporary name -> might be changed by bdsmlr
  var config = { attributes: false, childList: true, characterData: false, subtree: true };
  // pass in the target node (<div> element contains all stream posts), as well as the observer options
  var postsmatch = document.evaluate(".//div[contains(@class,'newsfeed')] | .//div[contains(@class,'theme1')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  var postsnode = postsmatch.singleNodeValue;

  //process already loaded nodes (the initial posts before scrolling down for the first time)
  createImageLinks(document, postsnode);
  displayTimestamps(document, postsnode);

  //start the observer for new nodes
  observer.observe(postsnode, config);


  // also observe the right sidebar blog stream on the dashboard
  // pass in the target node, as well as the observer options (subtree has to be true here - target nodes are further down the hierarchy)
  var config2 = { attributes: false, childList: true, characterData: false, subtree: true };
  var sidepostsmatch = document.evaluate(".//div[@id='rightposts']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  var sidepostsnode = sidepostsmatch.singleNodeValue;
  // sidebar does only exist on dashboard
  if (sidepostsnode) {
    //start the observer for overlays
    observer.observe(sidepostsnode, config2);
  }

}