banner
AndrewTsui

AndrewTsui

Adding Noise to Canvas in JavaScript

Recently, I have been studying captcha and Canvas-related things in web scraping. Here is a simple code snippet:

{
  const getImageData = CanvasRenderingContext2D.prototype.getImageData;
  //
  const noisify = function (canvas, context) {
    if (context) {
      const shift = {
        'r': Math.floor(Math.random() * 10) - 5,
        'g': Math.floor(Math.random() * 10) - 5,
        'b': Math.floor(Math.random() * 10) - 5,
        'a': Math.floor(Math.random() * 10) - 5
      };
      //
      const width = canvas.width;
      const height = canvas.height;
      //
      if (width && height) {
        const imageData = getImageData.apply(context, [0, 0, width, height]);
        //
        for (let i = 0; i < height; i++) {
          for (let j = 0; j < width; j++) {
            const n = ((i * (width * 4)) + (j * 4));
            imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
            imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
            imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
            imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
          }
        }
        //
        window.top.postMessage("canvas-defender-alert", '*');
        context.putImageData(imageData, 0, 0); 
      }
    }
  };
  //
  HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
    apply(target, self, args) {
      noisify(self, self.getContext("2d"));
      //
      return Reflect.apply(target, self, args);
    }
  });
  //
  HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
    apply(target, self, args) {
      noisify(self, self.getContext("2d"));
      //
      return Reflect.apply(target, self, args);
    }
  });
  //
  CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
    apply(target, self, args) {
      noisify(self.canvas, self);
      //
      return Reflect.apply(target, self, args);
    }
  });
}

{
  const mkey = "canvas-defender-sandboxed-frame";
  document.documentElement.setAttribute(mkey, '');
  //
  window.addEventListener("message", function (e) {
    if (e.data && e.data === mkey) {
      e.preventDefault();
      e.stopPropagation();
      //
      if (e.source) {
        if (e.source.CanvasRenderingContext2D) {
          e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
        }
        //
        if (e.source.HTMLCanvasElement) {
          e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
          e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
        }
      }
    }
  }, false);
}

This JavaScript code is used to manipulate Canvas elements. The code can be explained as follows:

Part 1:
const getImageData = CanvasRenderingContext2D.prototype.getImageData;

This line of code creates a variable named getImageData and assigns it the reference of CanvasRenderingContext2D.prototype.getImageData. This means that in the subsequent code, the getImageData variable can be used to call the getImageData method on the prototype of CanvasRenderingContext2D.

Next is a function named noisify, which is used to add a noise effect to the Canvas. The following code defines and implements the noisify function.

Inside the noisify function, the following logic exists:

  • First, check the canvas and context parameters.
  • Then, generate a shift object containing random numbers to control the color offset of the noise.
  • Next, get the width and height of the passed-in Canvas.
  • If both width and height exist, use the getImageData method to retrieve the pixel data from the Canvas and store it in the imageData variable.
  • Then, use nested loops to iterate through each pixel and apply the color offset based on the random numbers in the shift object.
  • After processing all the pixels, send a message to the top-level window to trigger the "canvas-defender-alert" event.
  • Finally, use context.putImageData(imageData, 0, 0) to redraw the processed pixel data onto the Canvas.

Part 2:

HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
  apply(target, self, args) {
    noisify(self, self.getContext("2d"));
    return Reflect.apply(target, self, args);
  }
});

HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
  apply(target, self, args) {
    noisify(self, self.getContext("2d"));
    return Reflect.apply(target, self, args);
  }
});

CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
  apply(target, self, args) {
    noisify(self.canvas, self);
    return Reflect.apply(target, self, args);
  }
});

This part of the code redefines three methods: toBlob, toDataURL, and getImageData, and uses Proxy to handle the proxying. By proxying these methods, the noisify function will be automatically called when these methods are invoked, adding the noise effect to the Canvas.

Part 3:

const mkey = "canvas-defender-sandboxed-frame";
document.documentElement.setAttribute(mkey, '');

window.addEventListener("message", function (e) {
  if (e.data && e.data === mkey) {
    e.preventDefault();
    e.stopPropagation();

    if (e.source) {
      if (e.source.CanvasRenderingContext2D) {
        e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
      }

      if (e.source.HTMLCanvasElement) {
        e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
        e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
      }
    }
  }
}, false);

This part of the code defines an event listener to listen for messages. When a specific message mkey is received, a series of operations are performed. Specifically:

  • First, add a custom attribute mkey to the root element of the page (document.documentElement).
  • Then, register a listener for the "message" event and handle the received message based on a condition.
  • If the message is equal to mkey, prevent the default behavior and event propagation.
  • Next, access the window object of the message source through e.source and check if it has the CanvasRenderingContext2D and HTMLCanvasElement methods on their prototypes.
  • If so, redefine the corresponding methods as the previously proxied methods to ensure that calls to these methods go through the noisify function.

In summary, this JavaScript code adds a noise effect to Canvas elements and redefines and proxies certain Canvas-related methods under specific conditions.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.