How can I run a JavaScript function when the URI source of an image renders?

Background

My big-picture goal is to display an image such that the following conditions are satisfied:

  • The entire viewport is filled by the image
  • At least one dimension of the image fits within the viewport
  • The image is not stretched

My solution is contained in the function updateRatioSettings, which works as follows:

  1. Determine the aspect ratios of the source image (using naturalWidth and naturalHeight) and the viewport
  2. If the viewport is wider than the image, width = "100%" and height = "auto".
  3. If the image is wider than the viewport, height = "100%" and width = "auto".

This works!

The Problem

Now I just need to get updateRatioSettings to run when either of the aspect ratios changes. I have a working connection to the resize event of window, so now I just need to figure out how to make the function run when I change the image source.

Each of the below code snippets shows a square centered in a horizontal viewport. A working solution will show the entire viewport filled with this nice grey color.

Attempt #1

My first strategy was to call updateRatioSettings immediately after setting img.src. However, I ran into an issue where sourceRatio, which is calculated as img.naturalWidth / img.naturalHeight, printed as NaN.

// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable

function updateRatioSettings() {
    // if the function is running, don't run the function
    if (ratioUpdateReady) {
        ratioUpdateReady = false;
        // begin debounced code
        
        const sourceRatio = img.naturalWidth / img.naturalHeight;
        const windowRatio = window.innerWidth / window.innerHeight;
        console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
        if (windowRatio > sourceRatio != windowWider) { // if something has changed
            windowWider = !windowWider // log the new status
            if (windowWider) {
                // settings for window aspect ratio > source aspect ratio
                img.style.width = "100%";
                img.style.height = "auto";
            } else {
                // settings for source aspect ratio > window aspect ratio
                img.style.height = "100%";
                img.style.width = "auto";
            }
        }
        
        // end debounced code
        ratioUpdateReady = true;
    }
}

document.addEventListener('DOMContentLoaded', function(){
    // identify the element containing the image
    img = document.getElementById("background");
    
    // load an initial image
  (function (){ // this normally executes after sending a request to another part of the chrome extension
    const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
        img.src = uri;
        updateRatioSettings(); // this gets a sourceRatio that is NaN
  })()
    
    // set up an event to update the ratio settings when the window resizes
    // commenting out for the purpose of this question, since it doesn't need debugging
    // window.addEventListener('resize', updateRatioSettings);
});
html,body{
    margin:0;
    height:100%;
    overflow:hidden;
}
img{
    height:100%;
    width:auto;
    position:absolute;
    top:-100%; bottom:-100%;
    left:-100%; right:-100%;
    margin:auto;
}
<html>
    <head>
        <title>New tab</title>
    </head>
    <body>
        <img id="background" src="" alt="">
    </body>
</html>

Attempt #2

Here I attempted to use image.onLoad to run updateRatioSettings when the image URI is loaded.

// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable

function updateRatioSettings() {
    // if the function is running, don't run the function
    if (ratioUpdateReady) {
        ratioUpdateReady = false;
        // begin debounced code
        
        const sourceRatio = img.naturalWidth / img.naturalHeight;
        const windowRatio = window.innerWidth / window.innerHeight;
        console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
        if (windowRatio > sourceRatio != windowWider) { // if something has changed
            windowWider = !windowWider // log the new status
            if (windowWider) {
                // settings for window aspect ratio > source aspect ratio
                img.style.width = "100%";
                img.style.height = "auto";
            } else {
                // settings for source aspect ratio > window aspect ratio
                img.style.height = "100%";
                img.style.width = "auto";
            }
        }
        
        // end debounced code
        ratioUpdateReady = true;
    }
}

document.addEventListener('DOMContentLoaded', function(){
    // identify the element containing the image
    img = document.getElementById("background");
    
    // load an initial image
  (function (){ // this normally executes after sending a request to another part of the chrome extension
    const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
    img.onLoad = updateRatioSettings; // this is the function I want to run when naturalWidth and naturalHeight have updated
        img.src = uri; // updateRatioSettings is not called
  })()
    
    // set up an event to update the ratio settings when the window resizes
    // commenting out for the purpose of this question, since it doesn't need debugging
    // window.addEventListener('resize', updateRatioSettings);
});
html,body{
    margin:0;
    height:100%;
    overflow:hidden;
}
img{
    height:100%;
    width:auto;
    position:absolute;
    top:-100%; bottom:-100%;
    left:-100%; right:-100%;
    margin:auto;
}
<html>
    <head>
        <title>New tab</title>
    </head>
    <body>
        <img id="background" src="" alt="">
    </body>
</html>

Attempt #3

Next, I tried using a MutationObserver to watch for the point at which naturalWidth and naturalHeight actually update, then run updateRatioSettings in repsonse. Like in Attempt #2, updateRatioSettings never actually ran.

// function to listen for window size changes and move image accordingly
let img;
let windowWider = false; // assume the screen is some ratio narrower than the image... to start at least
let ratioUpdateReady = true; // debounce variable

function updateRatioSettings() {
    // if the function is running, don't run the function
    if (ratioUpdateReady) {
        ratioUpdateReady = false;
        // begin debounced code
        
        const sourceRatio = img.naturalWidth / img.naturalHeight;
        const windowRatio = window.innerWidth / window.innerHeight;
        console.log("sourceRatio: " + sourceRatio, "windowRatio: " + windowRatio);
        if (windowRatio > sourceRatio != windowWider) { // if something has changed
            windowWider = !windowWider // log the new status
            if (windowWider) {
                // settings for window aspect ratio > source aspect ratio
                img.style.width = "100%";
                img.style.height = "auto";
            } else {
                // settings for source aspect ratio > window aspect ratio
                img.style.height = "100%";
                img.style.width = "auto";
            }
        }
        
        // end debounced code
        ratioUpdateReady = true;
    }
}

const watchingAttributes = ["naturalWidth", "naturalHeight"];

document.addEventListener('DOMContentLoaded', function(){
  // identify the element containing the image
  img = document.getElementById("background");

  // set up the image resize listener
  function processMutation(mutationRecord) {
    if (watchingAttributes.includes(mutationRecord.attributeName)) {
      updateRatioSettings();
    }
  }
  let watcher = new MutationObserver((mutations) => {
    mutations.forEach(processMutation);
  });
  watcher.observe(img, {
    attributes: true
  });

  // load an initial image
  (function (){ // this normally executes after sending a request to another part of the chrome extension
    const uri = "data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAAQEBAAA=="; // this is the string returned by the extension in my test case
    img.src = uri;
  })()
    
  // set up an event to update the ratio settings when the window resizes
  // commenting out for the purpose of this question, since it doesn't need debugging
  // window.addEventListener('resize', updateRatioSettings);
});
html,body{
    margin:0;
    height:100%;
    overflow:hidden;
}
img{
    height:100%;
    width:auto;
    position:absolute;
    top:-100%; bottom:-100%;
    left:-100%; right:-100%;
    margin:auto;
}
<html>
    <head>
        <title>New tab</title>
    </head>
    <body>
        <img id="background" src="" alt="">
    </body>
</html>

Conclusion

So… what do I do now?