import Viewer from "./Viewer";
import Dom from "./util/Dom";
import CredentialMode from "./CredentialMode";
import { Vector4 } from "./GeoMath";
import Color from "./util/Color";
/**
* 画面のキャプチャ機能を提供するクラス
* @example
* ```ts
* const capture = new Capture( viewer );
* capture.setAttribution( options: CaptureOption );
* const image1 = await capture.shoot();
* const image2 = await capture.shoot();
* ```
*/
class Capture {
private _viewer: Viewer;
private _mime_type: 'image/png' | 'image/jpeg';
private _sync: boolean;
private _attribution_content: string[];
private _attribution_font_color: string;
private _attribution_bg_color: string;
private _attribution_font_size: number;
private _attribution_h_margin: number;
private _attribution_v_margin: number;
private _attribution_h_spacing: number;
/**
* 表示する著作権情報を管理するarray。
* undefined の場合は、キャッシュが生成されていないことを表す
*/
private _attribution_array?: Capture.Attribution[];
private _attribution_height: number;
private _attribution_width: number;
/**
* コンストラクタ
* @param viewer Viewer インスタンス
*/
constructor( viewer: Viewer, options: Capture.Option = {} )
{
this._viewer = viewer;
const type = options.type ?? 'jpeg';
this._mime_type = type === 'png' ? 'image/png' : 'image/jpeg';
this._sync = options.sync ?? false;
const att_options = options.attribution ?? {};
this._attribution_content = (
Array.isArray( att_options.content ) ? att_options.content:
typeof( att_options.content ) === "string" ? [att_options.content]:
[]
);
this._attribution_font_color = Color.toRGBString( att_options.font_color ?? [ 0, 0, 0, 1 ] );
this._attribution_bg_color = Color.toRGBString( att_options.bg_color ?? [ 1, 1, 1, 0.5 ] );
this._attribution_font_size = att_options.font_size ?? 12;
this._attribution_h_margin = att_options.h_margin ?? 10;
this._attribution_v_margin = att_options.v_margin ?? 4;
this._attribution_h_spacing = att_options.h_spacing ?? 10;
this._attribution_array = undefined;
this._attribution_height = 0;
this._attribution_width = 0;
}
/**
* 著作権コンテナに表示する著作権を指定
* @example
* ```ts
* setAttribution([
* '
attribution sample
',
* '1234567890',
* '
',
* ])
* ```
* ```ts
* setAttribution( '1234567890
' );
* ```
*/
setAttributionContent( attribution: string | string[] ): void
{
this._attribution_content = (
Array.isArray( attribution ) ? attribution:
typeof( attribution ) === "string" ? [attribution]:
[]
);
this._attribution_array = undefined;
}
/**
* 文字色を指定
* 値は0~1.0の正規化された色値
*/
setAttributionFontColor( font_color: Vector4 ): void
{
this._attribution_font_color = Color.toRGBString( font_color );
}
/**
* 背景色を指定
* 値は0~1.0の正規化された色値
*/
setAttributionBackgroundColor( color: Vector4 ): void
{
this._attribution_bg_color = Color.toRGBString( color );
}
/**
* 文字サイズのpixel値
*/
setAttributionFontSize( font_size: number ): void
{
this._attribution_font_size = font_size;
}
/**
* マージン
*/
setAttributionSize( font_size: number ): void
{
this._attribution_font_size = font_size;
}
/**
* 画面のキャプチャ
*
* @return キャプチャ画像データ (Blob)
*/
async shoot(): Promise
{
if ( !this._viewer.canvas_element ) {
throw new Error('Canvas is null.');
}
const context = (this._sync ?
await new Promise( resolve => {
let counter = 0; // フレーム安定カウンタ
this._viewer.addPostProcess( () => {
if ( this._viewer.load_status.total_loading > 0 ) {
counter = 0;
return true;
}
if ( counter++ < 4 ) {
return true;
}
OFFSCREEN_CONTEXT = Dom.copyTo2dCanvasContext( this._viewer.canvas_element, OFFSCREEN_CONTEXT );
resolve( OFFSCREEN_CONTEXT );
return false;
});
}):
await new Promise( resolve => {
this._viewer.addPostProcess( () => {
OFFSCREEN_CONTEXT = Dom.copyTo2dCanvasContext( this._viewer.canvas_element, OFFSCREEN_CONTEXT );
resolve( OFFSCREEN_CONTEXT );
return false;
});
})
);
await this._postRenderForCapture( context );
return await Dom.convertCanvasToBlob( context.canvas, this._mime_type );
}
/**
* キャプチャ画像にロゴやアノテーションを描画
* @param context 書き込む2Dキャンバスコンテキスト
*/
private async _postRenderForCapture( context: CanvasRenderingContext2D ): Promise
{
const width = context.canvas.width;
const height = context.canvas.height;
context.font = `${this._attribution_font_size}px Noto Sans JP,sans-serif`;
context.textBaseline = 'alphabetic';
context.textAlign = 'left';
let attribution_array: Capture.Attribution[];
if ( this._attribution_array ) {
attribution_array = this._attribution_array;
}
else { // Generate Attributions
attribution_array = [];
for ( const attribution of this._attribution_content ) {
await this._addCaptureAttribution( attribution, context, attribution_array );
}
this._attribution_array = attribution_array;
this._attribution_height = 0;
this._attribution_width = 0;
for ( const attr of this._attribution_array ) {
this._attribution_height = Math.max( this._attribution_height, attr.height );
this._attribution_width += attr.width;
}
this._attribution_width += ( this._attribution_array.length - 1 ) * this._attribution_h_spacing;
}
// draw attributions
if ( attribution_array.length > 0 ) {
// fill bg-rect
context.fillStyle = this._attribution_bg_color;
context.fillRect(
width - this._attribution_width - this._attribution_h_margin * 2,
height - this._attribution_height - this._attribution_v_margin * 2,
this._attribution_width + this._attribution_h_margin * 2,
this._attribution_height + this._attribution_v_margin * 2
);
// draw text & img
let attribution_start_h = width - this._attribution_width - this._attribution_h_margin;
const attribution_start_v = height - this._attribution_v_margin;
for ( const attr of this._attribution_array ) {
if ( attr.type === "image" ) {
context.drawImage( attr.img, attribution_start_h, attribution_start_v - attr.height, attr.width, attr.height );
attribution_start_h += attr.width + this._attribution_h_spacing;
}
else if ( attr.type === "text" ) {
context.fillStyle = this._attribution_font_color;
const metrics:TextMetrics = context.measureText( attr.text );
context.fillText( attr.text, attribution_start_h, attribution_start_v - metrics.fontBoundingBoxDescent );
attribution_start_h += attr.width + this._attribution_h_spacing;
}
}
}
// draw logo
{
const img = await this._viewer.logo_controller.getLogoImage({
mini: width - this._attribution_width - this._attribution_h_margin * 2 < 180
});
context.drawImage( img, 6, height - img.height - 4, img.width, img.height );
}
}
/**
* キャプチャ画像用アノテーションを管理Arrayに追加(imgとtextを分離)
* @param attribution_string
* @param context
*/
private async _addCaptureAttribution( attribution_string: string, context: CanvasRenderingContext2D, attribution_array: Capture.Attribution[] )
{
const split_strings = attribution_string.match(new RegExp( "([^<]*)(
]*>)(.*)" ));
if ( split_strings ) {
if ( split_strings[1] ) {
await this._addCaptureAttributionText( split_strings[1], context, attribution_array );
}
await this._addCaptureAttributionImg( split_strings[2], context, attribution_array );
if ( split_strings[3] ) {
await this._addCaptureAttribution( split_strings[3], context, attribution_array );
}
}
else {
await this._addCaptureAttributionText( attribution_string, context, attribution_array );
}
}
/**
* キャプチャ画像用アノテーション(img)を管理Arrayに追加
* @param img_string
* @param context
*/
private async _addCaptureAttributionImg( img_tag: string, context: CanvasRenderingContext2D, attribution_array: Capture.Attribution[] )
{
let img_src: string | undefined = undefined;
let img_height: string | undefined = undefined;
let img_width: string | undefined = undefined;
let img_cors: string | undefined = undefined;
const params = img_tag.matchAll( new RegExp( "(\\S+)=[\"'](.*?)[\"']", "g" ));
const skipped = [];
for ( const [, key, value] of params ) {
switch ( key.toLowerCase() ) {
case 'src': img_src = value; break;
case 'width': img_width = value; break;
case 'height': img_height = value; break;
case 'crossorigin': img_cors = value; break;
default: if (skipped) skipped.push(key);
}
}
if ( skipped.length > 0 ) console.log( "Unsupported Attributes: " + skipped.join( ", " ) );
if ( !img_src ) {
console.log( "src attribute of image tag is missing" );
return;
}
const img = await Dom.loadImage( img_src, {
credentials: (
img_cors === '' || img_cors === 'anonymous' ? CredentialMode.SAME_ORIGIN:
img_cors === 'use-credentials' ? CredentialMode.INCLUDE:
CredentialMode.OMIT
),
});
let width: number;
let height: number;
if ( img_width && img_height ) {
width = Number( img_width );
height = Number( img_height );
}
else if ( img_width ) {
width = Number( img_width );
height = img.height * ( Number( img_width ) / img.width );
}
else if ( img_height ) {
height = Number( img_height );
width = img.width * ( Number( img_height ) / img.height );
}
else {
width = img.width;
height = img.height;
}
attribution_array.push({ type: "image", img, width, height });
}
/**
* キャプチャ画像用アノテーション(text)を管理Arrayに追加
* @param text
* @param context
*/
private async _addCaptureAttributionText( text: string, context: CanvasRenderingContext2D, attribution_array: Capture.Attribution[] )
{
if ( text.length > 0 ) {
const metrics:TextMetrics = context.measureText( text );
const width = metrics.width;
const height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
attribution_array.push({ type: "text", text, width, height });
}
}
}
// hidden properties
let OFFSCREEN_CONTEXT: CanvasRenderingContext2D | undefined = undefined;
namespace Capture {
export interface Option {
/**
* 画像の拡張子
*/
type?: "jpeg" | "png";
/**
* 画面の読み込みを待つかを決定する真偽値
*/
sync?: boolean;
/**
* 著作権情報オプション
*/
attribution?: AttributionOption;
}
export interface AttributionOption {
/**
* 著作権コンテナに表示する著作権を指定
* @example
* ```ts
* content: [
* '
attribution sample
',
* '1234567890',
* '
',
* ]
* ```
*/
content?: string | string[];
/**
* 文字色を指定
* 値は0~1.0の正規化された色値
*/
font_color?: Vector4,
/**
* 背景色を指定
* 値は0~1.0の正規化された色値
*/
bg_color?: Vector4,
/**
* 文字サイズのpixel値
*/
font_size?: number,
/**
* 水平方向のmarginのpixel値
*/
h_margin?: number,
/**
* 垂直方向のmarginのpixel値
*/
v_margin?: number,
/**
* attribution間のスペースのpixel値
*/
h_spacing?: number,
}
/**
* capture用 image attribution
* @private
* */
export interface ImageAttribution {
type: "image";
img: HTMLImageElement;
width: number;
height: number;
}
/**
* capture用 text attribution
* @private
* */
export interface TextAttribution {
type: "text";
text: string;
width: number;
height: number;
}
export type Attribution = TextAttribution | ImageAttribution;
} // namespace Capture
export default Capture;