banner
AndrewTsui

AndrewTsui

JavaScript で Canvas にノイズを追加する

最近、クローラーにおける CAPTCHA および Canvas に関連するものを研究しており、簡単にコードスニペットを記録します。

{
  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);
}

この JavaScript コードは Canvas 要素を処理するためのものです。以下はコードの説明です。

第一部:
const getImageData = CanvasRenderingContext2D.prototype.getImageData;

この行のコードは、getImageData という名前の変数を作成し、CanvasRenderingContext2D.prototype.getImageData の参照をそれに割り当てます。これにより、後のコードで getImageData 変数を使って CanvasRenderingContext2D プロトタイプの getImageData メソッドを呼び出すことができます。

次に、noisify という関数があり、この関数は Canvas にノイズ効果を追加するためのものです。次のコードは noisify 関数の定義と実装です。

noisify 関数内部には以下のロジックがあります:

  • まず、渡されたパラメータ canvascontext を使って判断します。
  • 次に、ノイズの色のオフセットを制御するためのランダム数を含む shift オブジェクトを生成します。
  • その後、渡された Canvas の幅と高さを取得します。
  • 幅と高さが存在する場合、getImageData メソッドを使って Canvas 上のピクセルデータを取得し、imageData 変数に保存します。
  • 次に、ネストされたループを使って各ピクセルを走査し、shift オブジェクト内のランダム数に基づいてピクセルの色値をオフセットします。
  • すべてのピクセルを処理した後、"canvas-defender-alert" イベントをトリガーするためにトップウィンドウにメッセージを送信します。
  • 最後に、context.putImageData(imageData, 0, 0) を使って処理されたピクセルデータを再び Canvas に描画します。

第二部:

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);
  }
});

この部分のコードは、toBlobtoDataURLgetImageData の 3 つのメソッドを再定義し、Proxy を使用して代理処理を行います。これらのメソッドを代理することで、呼び出す際に自動的に noisify 関数を呼び出して Canvas にノイズ効果を追加します。

第三部:

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);

この部分のコードは、メッセージをリッスンするためのイベントリスナーを定義します。特定のメッセージ mkey を受信したときに、一連の操作を実行します。具体的には:

  • まず、カスタム属性 mkey をページのルート要素(document.documentElement)に追加します。
  • 次に、"message" イベントのリスナーを登録し、受信したメッセージを処理するための条件判断を行います。
  • メッセージが mkey と等しい場合、デフォルトの動作とイベントの伝播を防ぎます。
  • 次に、e.source を通じてメッセージの送信元ウィンドウオブジェクトにアクセスし、そのプロトタイプに CanvasRenderingContext2DHTMLCanvasElement のメソッドがあるかどうかを確認します。
  • もしあれば、対応するメソッドを以前に代理されたメソッドとして再定義し、これらのメソッドの呼び出しが noisify 処理を経ることを保証します。

要するに、この JavaScript コードは Canvas 要素にノイズ効果を追加する機能を実現し、特定の条件下で Canvas 関連のメソッドを再定義および代理処理します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。