2024年11月14日木曜日

HTMLの画像に重ねて矩形を描画する手順のまとめ

HTMLのページに表示した画像に重ねて矩形を描画する、いくつかの異なった方法がありました。それぞれの実装方法を後で参照できるように、ひとつのAPEXアプリケーションにまとめました。

以下の3つの方法を実装しています。

  • 画像をキャンバスに描画し、そのキャンバスに矩形を描画する。
  • 画像にキャンバスを重ねて、そのキャンバスに矩形を描画する。
  • 画像に矩形を表示するDIV要素を重ねる。
作成したアプリケーションは以下のように動作します。それぞれの異なる処理を実装していますが、見た目はほぼ同じです。


作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/draw-rectangle-on-image.zip

アプリケーションはすべてホーム・ページに実装しています。

DIV要素でバウンディング・ボックスとなる矩形を表示するため、ページ・プロパティCSSインラインに以下を記述します。

.bounding-box {
border-color: #0F0;
position: absolute;
box-sizing: border-box;
border-width: 2px;
border-style: solid;
}
.bounding-box-label {
color: black;
background-color: #0F0;
position: absolute;
top: 0;
left: 0;
font-size: 12px;
padding: 1px;
}

画像の選択にはページ・アイテムP1_IMAGEを使います。タイプイメージ・アップロードです。これは、実質的にはtype="file"のINPUT要素になります。表示形式としてネイティブ・ファイル参照が選択されている場合はINPUT要素がそのまま生成されますが、その他の表示形式ではカスタム要素が生成されます。しかし表示形式に関わらず、ページ・アイテムの要素からfiles(の配列)属性を参照することができます。


ページ・アイテムP1_X0P1_X1P1_Y0P1_Y1で描画する矩形の座標を設定します。

それぞれ0から1の間の数値を指定します。



画像をキャンバスに描画し、そのキャンバスに矩形を描画する。



描画の対象は、静的コンテンツのリージョンCONTAINER_CANVAS_ONLYに以下のように設定します。

<canvas id="canvas-only" class="w100p"><canvas/>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。

/*
* ページ・アイテムP1_IMAGEに選択された画像のURLを取り出す。
*/
const imageFile = document.getElementById("P1_IMAGE").files[0];
const imageUrl = URL.createObjectURL(imageFile);
const img = new Image();
/*
* Canvas要素への画像の描画に続き、矩形を描画する。
*/
img.onload = () => {
// 画像の描画
const canvas = document.getElementById("canvas-only");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas.height = Math.ceil(img.height * ( canvas.width / img.width ));
// 解像度を上げる。
const dpr = window.devicePixelRatio;
const cw = canvas.width;
const ch = canvas.height;
canvas.width = cw * dpr;
canvas.height = ch * dpr;
// canvasに設定した自動スケールに任せる。
// ctx.scale(dpr, dpr);
// 画像の描画
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 矩形の描画
const x0 = Number(apex.items.P1_X0.value);
const y0 = Number(apex.items.P1_Y0.value);
const x1 = Number(apex.items.P1_X1.value);
const y1 = Number(apex.items.P1_Y1.value);
const startX = x0 * canvas.width;
const startY = y0 * canvas.height;
const width = (x1 - x0) * canvas.width;
const height = (y1 - y0) * canvas.height;
ctx.strokeStyle = "#0F0";
ctx.lineWidth = 4;
ctx.strokeRect(startX, startY, width, height);
}
/*
* ページ・アイテムに選択されている画像を読み込む。
*/
img.src = imageUrl;
view raw canvas-only.js hosted with ❤ by GitHub


画像にキャンバスを重ねて、そのキャンバスに矩形を描画する。



描画の対象は、静的コンテンツのリージョンCONTAINER_CANVAS_ON_IMGに以下のように設定します。

<div style="position: relative;" class="w100p">
    <img id="canvas-on-img-image" src="" width="100%" />
    <canvas id="canvas-on-img-canvas" style="position: absolute; left: 0; "></canvas>
</div>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。

/*
* ページ・アイテムP1_IMAGEに選択された画像をIMG要素に描画する。
*/
const imageFile = document.getElementById("P1_IMAGE").files[0];
const imageUrl = URL.createObjectURL(imageFile);
const img = document.getElementById("canvas-on-img-image");
/*
* IMG要素に画像が表示された後にCANVAS要素に矩形を描画する。
*/
img.onload = () => {
const x0 = Number(apex.items.P1_X0.value);
const y0 = Number(apex.items.P1_Y0.value);
const x1 = Number(apex.items.P1_X1.value);
const y1 = Number(apex.items.P1_Y1.value);
const canvas = document.getElementById("canvas-on-img-canvas");
const imgWidth = img.width;
const imgHeight = img.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
const startX = x0 * imgWidth;
const startY = y0 * imgHeight;
const width = (x1 - x0) * imgWidth;
const height = (y1 - y0) * imgHeight;
const ctx = canvas.getContext('2d');
ctx.strokeStyle = '#0F0';
ctx.lineWidth = 4;
ctx.globalCompositeOperation = 'destination-over';
ctx.strokeRect(startX, startY, width, height);
// キャプションの印刷
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = '#0F0';
ctx.font = "12px sans-serif"; 1
ctx.textBaseline = "top";
const textHeight = 12;
const textPad = 2;
const label = "サンプルのラベル";
const textWidth = ctx.measureText(label).width;
ctx.fillRect(
startX,
startY,
textWidth + textPad,
textHeight + textPad
);
ctx.fillStyle = '#000000';
ctx.fillText( label, startX, startY );
}
/*
* ページ・アイテムに選択されている画像を読み込む。
*/
img.src = imageUrl;



画像に矩形を表示するDIV要素を重ねる。



描画の対象は、静的コンテンツのリージョンCONTAINER_DIV_ON_IMGに以下のように設定します。

<div id="image-container" class="w100p">
<img id="image" src="" width="100%"></img>
</div>

ボタンRunをクリックしたときに実行するJavaScriptのコードは以下です。

/*
* ページ・アイテムP1_IMAGEに選択された画像をIMG要素に描画する。
*/
const imageFile = document.getElementById("P1_IMAGE").files[0];
const imageUrl = URL.createObjectURL(imageFile);
const imageContainer = document.getElementById("image-container");
const img = document.getElementById("image");
/*
* IMG要素に画像が表示された後にCANVAS要素に矩形を描画する。
*/
img.onload = () => {
const x0 = Number(apex.items.P1_X0.value);
const y0 = Number(apex.items.P1_Y0.value);
const x1 = Number(apex.items.P1_X1.value);
const y1 = Number(apex.items.P1_Y1.value);
// 描画する矩形を定義する。
const boxElement = document.createElement("div");
boxElement.className = "bounding-box";
Object.assign(boxElement.style, {
left: 100 * x0 + '%',
top: 100 * y0 + '%',
width: 100 * (x1 - x0) + '%',
height: 100 * (y1 - y0) + '%',
});
// 描画する矩形の左上にラベルをつける。
const labelElement = document.createElement('span');
labelElement.textContent = 'サンプルのラベル';
labelElement.className = 'bounding-box-label';
boxElement.appendChild(labelElement);
// 矩形を描画する。
imageContainer.appendChild(boxElement);
}
/*
* ページ・アイテムに選択されている画像を読み込む。
*/
img.src = imageUrl;
view raw div-on-img.js hosted with ❤ by GitHub


注意点


IMG要素にDIV要素を重ねて矩形を表示する際に、親となるリージョンのテンプレート・オプションマージンが設定されていると、矩形を描画する位置にズレが生じました。

本来、矩形の描画を指定した位置は以下です。


画像を含むリージョン(今回の実装ではCONTAINER_DIV_ON_IMG)のテンプレート・オプションにて、Top MarginLeft MarginLargeを設定します。


画像および矩形の表示は以下のように変わり、意図した位置からズレて矩形が描画されます。


比較対象がないと見つかりにくい現象ではないかと思います。

今回の記事は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。