まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:javascript

タイトルの通りです。例えば以下のような独自定義関数があったとします:
// n 未満の素数を配列で取得する関数
function pnum( n ){
  var results = [];

  for( var i = 2; i < n; i ++ ){
    var b = true;
    for( var j = 2; j < i && b; j ++ ){
      b = i % j;
    }
    if( b ){
      results.push( i );
    }
  }

  return results;
}

関数名は pnum() 、パラメータを1つ受け取り、その値未満の素数配列を返す、という内容です。関数の処理内容はもっと複雑でも構いません。

例えば上記内容を pnum.js というファイルで保存した場合、同じフォルダにある HTML ファイルから以下のように使うことができます:
<script src="./pnum.js"></script>
<script>
var results = pnum( 1000 );  // 1000 未満の素数の配列
</script>

では同じファイルの同じ関数を Node.js からも使いたくなった場合はどうすればいいでしょう? あるいは逆に Node.js から require や import で使える JavaScript ファイルをブラウザから読み込んで使いたい場合はどうすればいいでしょう? これが今日のブログテーマです。


答えはシンプルなんですが、まず Node.js から使う場合は、このようにクラス化して、クラスを export するのが定番的なやり方だと思っています:
class MyClass {
  constructor(){
  }

  pnum( n ){
    var results = [];

    for( var i = 2; i < n; i ++ ){
      var b = true;
      for( var j = 2; j < i && b; j ++ ){
        b = i % j;
      }
      if( b ){
        results.push( i );
      }
    }

    return results;
  }
}

module.exports = MyClass;

この上の内容を myclass.js という名前で保存したとすると、Node.js の呼び出し元からは以下のようにして利用することができます:
var MyClass = require( './myclass' );
var myClass = new MyClass(); var results = myClass.pnum( 1000 );

では同じ myclass.js をブラウザの JavaScript から <script> タグで使えばいいのでは・・・ と思いますが、その場合は myclass.js の最終行である module.exports = MyClass; の所でエラーになります(module が定義されていないからです)。

そこでこのエラーを回避するために以下のようにします。module が object として定義済みである場合のみ最終行を実行するようにします:
class MyClass {
  constructor(){
  }

  pnum( n ){
    var results = [];

    for( var i = 2; i < n; i ++ ){
      var b = true;
      for( var j = 2; j < i && b; j ++ ){
        b = i % j;
      }
      if( b ){
        results.push( i );
      }
    }

    return results;
  }
}

if( typeof module === 'object' ){
  module.exports = MyClass;
}

こうしておくとブラウザ側も Node.js と同様に実行することができます:
<script src="./myclass.js"></script>
<script>
var myClass = new MyClass();
var results = myClass.pnum( 1000 );
</script>

要は「クラス化して、module が object として定義済みであればクラスをエクスポート指定」することで Node.js からもブラウザ JavaScript からも共通利用できるようになります。




 

JavaScript で乱数を扱う際の話です。

「乱数」はその名前の通り「事前に予測できないランダムな値」のことや、その生成の仕組みのことを呼びます。JavaScript にも乱数を生成する機能は標準で用意されており、Math.random() という関数を実行することで 0 から 1 の間のランダムな小数を取得することができます。

もし0から1の間の小数の乱数ではなく、「10以上20未満のランダムな整数の乱数」が必要な場合は以下のように Math.floor() (小数部分を切り捨てて整数化する関数)と組み合わせることで目的の乱数を得ることができます:
Math.floor( Math.random() * 10 ) + 10

この JavaScript の乱数は標準機能として備わっていて、すぐに使えるという点では便利です。しかしシード(seed)に対応していない、という、場合によっては困る点があります。

シードは乱数を初期化する時に指定する値です。同じシードで初期化した乱数システムは同じ乱数を返すようになります。例えば1という値をシードに指定して乱数を初期化して3回乱数を発生させたとします(発生した値を a1, a2, a3 とします)。別の機会に同じ1をシードに指定して乱数を初期化してから3回乱数を発生させて、それらの値をそれぞれ b1, b2, b3 とします。この時、同じシードを指定してから実行しているので、a1 = b1, a2 = b2, a3 = b3 が成立します。a1 も a2 も a3 も乱数なので事前に予測することはできませんが、同じシードを指定して実行したのであれば b1, b2, b3 がそれぞれ a1, a2, a3 と同じ値が取得できることが事前に保障されます。このように再現性のある乱数を発生させる仕組みが必要になることがある※のですが、JavaScript の Math.random() はこのシードには対応していない、という問題があります。

※例えば「ウェブページ内に1日ごとにランダムな画像を表示する(同じ日にアクセスした場合は同じ画像が表示される)」という仕組みを作ろうとした場合、本当にランダムな関数を使って表示画像を選ぼうとすると、アクセスするたびに異なる画像が選ばれてしまうことになります。 一方、シードに対応した乱数であれば、例えば日付からタイムスタンプ値を取るなどして数値化し、その数値をシードに指定して乱数を1個取得すれば、その値は事前に予測はできませんが(同じシードで初期化しているので)同じ日に実行していれば同じ値になります。ということは同じ画像を選んで表示することができるようになる、というものです。

で、このような再現性のある乱数を JavaScript で実現するにはどうすればよいか? という問題です。答としては「自分で用意する」ことになります。例えば以下のような感じ(こちらの記事を参考にしています):

class Random {
  constructor(seed = 19681106) {
    this.x = 31415926535;
    this.y = 8979323846;
    this.z = 2643383279;
    this.w = seed;
  }
  
  // XorShift
  next() {
    let t;
 
    t = this.x ^ (this.x << 11);
    this.x = this.y; this.y = this.z; this.z = this.w;
    return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8)); 
  }
  
  // min以上max以下の乱数を生成する
  nextInt(min, max) {
    const r = Math.abs(this.next());
    return min + (r % (max + 1 - min));
  }
}

上述のようなクラスを事前に定義しておきます(x, y, z の値は自由に変更してかまいません。ちなみに↑の例は円周率の最初の31桁を使っています)。その上で以下のように使います:
  //. 今日の日付
  var dt = new Date();
  var y = dt.getFullYear();
  var m = dt.getMonth() + 1;
  m = ( ( m < 10 ) ? '0' : '' ) + m;
  var d = dt.getDate();
  d = ( ( d < 10 ) ? '0' : '' ) + d;

  //. 今日の午前零時のタイムスタンプをシードとして取得
  dt = new Date( y + '-' + m + '-' + d + ' 00:00:00' );
  var seed = dt.getTime();

  //. 今日の午前零時のタイムスタンプをシードに関数を初期化
  var random = new Random( seed );

  //. 0以上100未満の乱数を発生させる
  var value = random.nextInt( 0, 100 );

(かなり無理やり感ありますが・・)このように JavaScript を記述すると日が変わるまでの間は同じシード値を使って初期化することになります。したがって最後の行で乱数を発生させていますが、ここで取得する乱数値は同じ日に実行する間は同じ結果になる、というものです。

2022 年最初のブログエントリとなります。遅ればせながら本年もよろしくお願いいたします。

今年最初のエントリは React や Angular といったフロントエンドフレームワークにまつわるネタです。最近流行りのこれらのフロントエンドフレームワークを使うことで、比較的簡単に SPA(Single Page Application) を作ることができます。SPA 化のメリット/デメリットを理解した上で作る必要があるとは思いますが、SPA 化することによる大きなメリットの1つに「Amazon S3 などのオブジェクトストレージや Github ページといった、安価かつ滅多にサービスダウンしない環境でフロントエンドのウェブホスティングができる」ことがあると思っています。

この点を少し補足しておきます。「自分が作って管理しているサービスやアプリケーションを安定運用したい」というのは誰でも思うことだと思っています。ただ現実にはこれは簡単なことではありません。サービスやアプリケーションは利用者が直接参照するフロントエンド部分に加えて、データベースなどのバックエンド部分、そしてこれらを繋ぐ API サーバーなどから成り立っていて、これら全てを安定運用するのは簡単なことではありません。特にフロントエンドや API サーバーについては最近よく耳にするコンテナオーケストレーションなどによって比較的安価に安定運用することもできるようになりました。ただフロントエンド部がアプリケーションサーバーを持たないシンプルな静的ウェブコンテンツであれば、上述したような Amazon S3 のウェブコンテンツ機能を使ったり、Github ページ機能を使うことで、滅多にサービスダウンしないウェブページとして公開するという方法もあります。現実問題として、この方法だとフロントエンドページの公開は簡単ですが、API サーバーなどのバックエンドとの通信時に CORS を考慮する必要が生じたりして、これはこれで面倒な設定も必要になるのですが、一方で「ほぼ無料」レベルの安価で利用できるウェブサーバーにしてはかなり安定しているのも事実で、用途によっては(「面倒な設定」の面倒レベルがあまり高くならないケースであれば)運用方法の考慮に含めてもいいのではないかと思っています。補足は以上。


一方、最近のウェブアプリケーション開発において IDaaS の利用ケースもよくあります(個人的にも業務で多く使っている印象です)。ログインや認証、ユーザー管理といった ID 周りの機能がマネージドサービスとして提供されていて、自分で面倒な開発をしなくても API や SDK から利用可能になる、というものです。具体的なサービスとしては AWS CognitoAuth0 などが有名どころでしょうか。IBM Cloud からも AppID という IDaaS が提供されています。

そしてフロントエンドアプリケーションと IDaaS の組み合わせというのが実は相性が悪かったりします(苦笑)。多くの IDaaS では OAuth と呼ばれるプロトコルでの認証/認可が主流となっているのですが、アプリケーションサーバーを持たない静的なフロントエンドアプリケーションでは OAuth のコールバックの仕組みとの相性が悪く、実装が非常に難しいという事情があります。その結果として、各 IDaaS ベンダーから認証専用の JavaScript SDK が提供され、それらを使って認証機能を実装することになります。IBM Cloud の AppID も専用の JavaScript SDK が用意され、React などで作ったフロントエンドに組み込むことで AppID へのログインや認証を実現できます(この SDK では PKCE と呼ばれる方法を使ってログインを実現しています)。

で、ここまでのアプリ開発方法に関する内容についてはこちらのページでも紹介されているのですが、問題はこの先にありました。この方法で作った React のフロントエンドアプリを Github ページにホスティングして運用しようとすると・・・結論としては少し特殊なリダイレクト URI の設定が必要でした。これを知らないと Github ページでの運用時にトラブルが起こりそうだと思ったので、将来の自分への備忘録の意味も含めてブログに設定内容を記載しておくことにしました。

前段部分の長いブログですが、といった背景の中で「IBM AppID の JavaScript SDK を使って作った SPA アプリを Github ページで動かす」ためのアプリ開発手順と設定内容について、順に紹介します。


【React で SPA アプリを作成する】
この部分は上述ページでも紹介されている内容ではありますが、一般的な内容でもあるので、特にコメントに色をつけずに紹介します。以下のコマンドで react-appid というフォルダに React のサンプルアプリを作成します:
$ npx create-react-app react-appid

$ cd react-appid

ここまでは普通の React アプリと同じです。ここから下で AppID の認証に対応したり、Github ページで運用する場合特有の設定を加えていきます。


【IBM AppID サービスインスタンスの準備を行う】
ここは上述ページでも触れられてはいるのですが、あまり詳しくは紹介されていないので、ここで改めて手順を紹介します。

IBM Cloud にログインして AppID サービスを作成後にインスタンスを開き、「アプリケーション」タブから「アプリケーションの追加」ボタンで対象アプリケーションを追加します。その際にアプリケーションのタイプを「単一ページ・アプリケーション」(SPA) として追加するようにしてください:
2022011301


追加したアプリケーションを選択して、内容を確認します。type の値が "singlepageapp" となっていることを確認してください。確認できたらこの中の "clientId" 値と "discoveryEndpoint" 値を後で使うことになるのでメモしておきます:
2022011303


まだ AppID にユーザーを登録していない場合はここでユーザーも登録しておきましょう。メニューの「クラウド・ディレクトリー」から「ユーザー」を選択し、「ユーザーの追加」からログイン可能なユーザー(の ID とパスワード)を登録しておきます:
2022011302


またリダイレクト URL を登録しておきましょう。「認証の管理」から「認証設定」を選択して、リダイレクト URL に http://localhost:3000/ を追加しておきます:
2022011303



IBM AppID 側で準備する内容は以上です。取得した情報を使ってアプリ開発を続けます。


【React アプリに IBM AppID モジュールを追加して、AppID のログイン機能を追加する】
ここは上述ページでも詳しく記載されている内容です。まずは IBM AppID を利用するためのライブラリを追加します:
$ npm install ibmcloud-appid-js

そしてソースコードの src/App.js をテキストエディタで開き、以下の内容に書き換えます(詳しい内容は上述ページを参照してください)。この時に太字で示している clientId の値と discoveryEndpoint の値には先程 AppID の画面で確認した clientId 値と discoveryEndpoint 値をコピペして指定してください:
import React from 'react';
import logo from './logo.svg';
import './App.css';
import AppID from 'ibmcloud-appid-js';
function App() {
  const appID = React.useMemo(() => {
    return new AppID()
  }, []);
  const [errorState, setErrorState] = React.useState(false);
  const [errorMessage, setErrorMessage] = React.useState('');
  (async () => {
    try {
      await appID.init({
        clientId: 'AppID の clientID の値',
        discoveryEndpoint: 'AppID の discoveryEndpoint の値'
      });
    } catch (e) {
      setErrorState(true);
      setErrorMessage(e.message);
    }
  })();
  const [welcomeDisplayState, setWelcomeDisplayState] = React.useState(false);
  const [loginButtonDisplayState, setLoginButtonDisplayState] = React.useState(true);
  const [userName, setUserName] = React.useState('');
  const loginAction = async () => {
    try {
      const tokens = await appID.signin();
      setErrorState(false);
      setLoginButtonDisplayState(false);
      setWelcomeDisplayState(true);
      setUserName(tokens.idTokenPayload.name);
    } catch (e) {
      setErrorState(true);
      setErrorMessage(e.message);
    }
  };
  return (
    <div  classname="App">
    <header  classname="App-header">
      <img  alt="logo" classname="App-logo" src="{logo}">
        {welcomeDisplayState && <div> Welcome {userName}! You are now authenticated.</div>}
        {loginButtonDisplayState && <button  onclick="{loginAction}" id="login" style="{{fontSize:">Login</button>}
        {errorState && <div  style="{{color:">{errorMessage}</div>}
    </header>
    </div>
  );
}
export default App;

この状態で一度動作確認します:
$ npm start

ウェブブラウザが起動して http://localhost:3000/ が開き、以下のような画面が表示されます:
2022011301


"Login" と書かれた箇所をクリックするとログイン画面がポップアップ表示されます。ここで AppID に登録済みのユーザー ID とパスワードを指定してサインインします:
2022011304


正しくログインできると先程の画面に戻り、登録したユーザーの氏名が表示とともに "You are now authenticated." と表示され、ログインが成功したことが明示されます:
2022011305


【React アプリをビルドして、SPA アプリでもログインできることを確認する】
今作成した React アプリをビルドして index.html にまとめた SPA アプリにしたあとでも AppID でログインできることを確認します。nginx などの HTTP サーバーがあればそれを使ってもいいのですが、ここでは Node.js のシンプルなウェブサーバーを使って動作確認します。以下のような内容で app.js を作成し、react-appid フォルダ直下(README.md などと同じ階層)に保存します:
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/build' ) );

var port = process.env.PORT || 3000;
app.listen( port );
console.log( "server starting on " + port + " ..." );

↑ build/ フォルダをコンテキストルートにして 3000 番ポートで HTTP リクエストを待ち受けるだけのシンプルなコードです。

app.js が準備できたら、まずは一度 React コードをビルドして SPA アプリ化します:
$ npm run build

ビルドが完了すると React アプリが SPA 化されて圧縮されて build/ フォルダにまとめられますので、これを app.js を使って起動します:
$ node app

改めてウェブブラウザで http://localhost:3000/ にアクセスして同じアプリが起動していることを確認し、Login から AppID アカウントでログインできることを確認します。アプリケーション・サーバーを持たない SPA アプリでも IBM AppID を使って認証できることが確認できました:
2022011305


【React アプリをビルドした SPA アプリを Github ページでも動くように調整する】
ここまでできれば後は build/ フォルダを Github ページで公開するだけで動くんじゃないか? と思うかもしれませんが、実は期待通りに動くようになるまではいくつかの落とし穴があります。1つずつ解いていきましょう。

(1)絶対パス→相対パスへの書き換え

React の SPA アプリをただビルドしただけだと、コンテキストルート直下で動く想定のアプリになってしまいます。どういうことかというと、例えば http://localhost:3000/ とか https://xxx.xxxxxxx.com/ といったサブディレクトリを持たない GET / というリクエストに対して動くアプリになります(要はビルドで作成される index.html 内で参照される外部 CSS や JavaScript は "/" ではじまる絶対パスになっています)。一方、Github ページで動かす際は https://dotnsf.github.io/react-appid/ のようなサブディレクトリ上で動くことになります。ここがコンフリクトになってしまい、絶対パスで指定された CSS や JavaScript のロードエラーが発生してしまうのでした。これを回避するために index.html 内の該当箇所を絶対パス指定から相対パス指定に変更する必要があるのでした。

具体的にはこの下の(3)までの作業後に改めて $ npm run build を実行してから、build/index.html の以下の <head> 部分で頭に . を付けて、絶対パス指定から相対パス指定に書き換えてください:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="icon" href="./favicon.ico"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="theme-color" content="#000000"/>
<meta name="description" content="Web site created using create-react-app"/>
<link rel="apple-touch-icon" href="./logo192.png"/>
<link rel="manifest" href="./manifest.json"/>
<title>React App</title>
<script defer="defer" src="./static/js/main.85d3d620.js"></script>
<link href="./static/css/main.073c9b0a.css" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>



(2)回転する画像の URL を絶対指定

(1)と似た内容ですが、SPA アプリ内で表示される画像も絶対パスのままだと正しく表示されません。ただこちらは JavaScript 内で {logo} と指定されている画像なので単純に絶対パスを相対パスにすればよいという対処が使えないのでした。 まあアプリケーションとしては必須ではないので単純に画像ごと削除してしまってもいいのですが、残したい場合は Git リポジトリ上の画像 URL を直接指定する、などの方法で対処する必要があります。よくわからない人は以下の例でそのまま書き換えてください:
    :
  return (
    <div className='App'>
    <header className='App-header'>
      <img src="https://raw.githubusercontent.com/dotnsf/react-appid/main/src/logo.svg" className='App-logo' alt='logo' />
        {welcomeDisplayState && <div> Welcome {userName}! You are now authenticated.</div>}
        {loginButtonDisplayState && <button style={{fontSize: '24px', backgroundColor: 'skyblue', 
          border: 'none', cursor: 'pointer'}} id='login' onClick={loginAction}>Login</button>}
        {errorState && <div style={{color: 'red'}}>{errorMessage}</div>}
    </header>
    </div>
  );
    :


(3)デフォルトの .gitignore を変更

$ npx create-react-app コマンドで作成した React プロジェクトにははじめから .gitignore ファイルが含まれています。が、この .gitignore では /build フォルダを除外するよう記述されています。今回は build/ フォルダを Github ページで運用することが目的なので、/build フォルダが除外されてしまっては意味がありません。.gitignore ファイルを編集して、/build フォルダを含めるよう(コメントアウトするなど)してください:
  :
  :
#/build
  :
  :

(1)でも紹介しましたが、ここまでの変更を行ったら再度 SPA アプリケーションをビルドし、その後に build/index.html ファイルに対して(1)の変更を行ってください。


(4)AppID のリダイレクト URL の設定が特殊

これまではアプリケーションを http://localhost:3000/ という開発時専用の URL で実行していたので、AppID のリダイレクト URL も http://localhost:3000 だけを登録して動作確認できました。では実際に Github ページでアプリケーションを動かす際にはどのようなリダイレクト URL を指定すればいいのでしょうか? 実はここがくせ者でした。

例えば私(Github ID は dotnsf )が react-appid という github リポジトリを作って、このリポジトリを Github ページとして公開して運用しようとすると、アプリケーションの URL は以下のようになります:
 https://dotnsf.github.io/react-appid

2022011306


ということは AppID に設定するリダイレクト URL にもこの値を指定するべき・・・だと思っていたのですが、なんとリポジトリ名部分が不要で、 https://dotnsf.github.io/ を指定するのが正しい設定内容のようでした:
2022011308


この URL がリダイレクト URL として設定されていれば AppID SDK が正しく動作して、認証も正しく実行できるようになりました(自分は実はここで結構つまずきました):
2022011307


(5)build フォルダを Github ページとして公開する方法
最後に作成したプロジェクトを Github に登録して、build フォルダを Github ページで公開する方法についてです。root フォルダや docs フォルダを Github ページで公開する場合は選択肢から選ぶだけなんですが、任意のフォルダを Github ページで公開するのは少しコツが必要です。

例えば react-appid というリポジトリを使う場合は、まず Github 上でこのリポジトリを公開設定で作成します。そして普通に main リポジトリに登録します:
$ git init

$ git add .

$ git commit -m 'first commit'

$ git branch -M main

$ git remote add origin https://github.com/dotnsf/react-appid.git

$ git push -u origin main

その後、build フォルダを Github ページで公開するには以下のコマンドを実行します:
$ git subtree push --prefix build origin gh-pages

これで該当フォルダが Github ページとして公開されます。実際に以下のページはこの設定を使って公開しています:

https://dotnsf.github.io/react-appid/

2022011306


ID: username1@test.com, パスワード: password1 でログインできるので、よければ試してみてください:
2022011307


React で作成した SPA アプリケーションの認証をどうするか、という問題を IBM AppID (と SDK)で解決する方法と、ビルドした SPA アプリを Github ページで運用する場合の特殊な設定やコマンドについて紹介しました。




JavaScript でクリップボードを操作することは過去に何度かやっていたのですが、これまでは全てテキスト型の情報を扱っていて、バイナリ情報を扱ったことがありませんでした。

JavaScript でバイナリ情報をクリックボードにコピーすることができないのか? というと、そういうことはなく、とりあえず実現できそうだったので共有目的でブログを書きました。

具体的には以下のようなコードを実行することでクリップボードに画像がコピーされた状態を作ることができます。PNG 画像の情報が buffer 変数に入っている状態で、以下のコードを実行します:
  //. Canvas
  var canvas = document.getElementById( 'mycanvas' );
  if( !canvas || !canvas.getContext ){
    return false;
  }

  //. Canvas の内容を PNG 画像として取得
  var png = canvas.toDataURL( 'image/png' );
  png = png.replace( /^.*,/, '' );

  //. バイナリ変換
  var bin = atob( png );
  var buffer = new Uint8Array( bin.length );
  for( var i = 0; i < bin.length; i ++ ){
    buffer[i] = bin.charCodeAt( i );
  }
  var blob = new Blob( [buffer], { type: 'image/png' } ); //. イメージバッファから Blob を生成

  :

 try{
    navigator.clipboard.write([
      new ClipboardItem({
        'image/png': blob
      })
    ]);
  }catch( err ){
    console.log( err );
  }

最初にバイナリデータを Blob 型変数に変換して、ClipboardItem 型変数にしてから navigator.clipboard.write() を実行する、という流れです。最初のバイナリデータは HTML であれば Canvas などから取得したものを想定しています。


このコードを拙作のお絵描きアプリ MyDoodles ※にも実装してみました:
https://mydoodles.herokuapp.com/

※初回はサインアップしてアカウントを作成する必要があります。作成したアカウントでログインすることで PC やスマホでお絵描きが可能になりますが、今回はクリップボードを使う前提で紹介するので、この機能を試す場合は PC のブラウザからログインして、マウスやタッチパネルでお絵描きしてください。


線の色や太さを変えながら、適当なお絵描きをして、最後に「保存」します。保存処理の一部として、上述のクリップボードコピーが実行されます:
2021100701


保存できました。同時に画像がクリップボードにコピーされているはずです:
2021100702


そのまま画像をペーストできるアプリを開いてペースト(CTRL+V など)を実行すると、クリップボードにコピーされたお絵描き画像がペーストされます。画像は特に背景色を指定しない限りは背景が透明な状態でコピーされているので、ペーストすると透過背景の画像として表示されます:
2021100703



JavaScript でもバイナリデータをクリップボードにコピーできることが確認できました。

タイトルそのままです。なんらかの画像をコピーしてクリップボードに格納された状態から、ブラウザ画面内の <canvas> 要素内に画像データをペーストして表示する、という処理が実現できないか試してみました。

結論としてはなんとなく実現できていると思います。サンプルを公開しているのでまずは挙動を試してみてください。


PC でもスマホでも、まず対象の画像をコピーします。今回は「いらすとや」さまのこの画像を使って試してみることにします:
https://www.irasutoya.com/2021/04/blog-post_12.html

2021041402


まずは画像をコピーします。PC の場合は右クリックから「コピー」、スマホの場合は対象画像を長押しするとコピーできると思います:
2021041403


その後、こちらのページを開きます:
https://dotnsf.github.io/web_image_paste/

2021041401



表示された画面内の「ペースト」と書かれた箇所にカーソルを移動した上でペースト(貼り付け)してください(スマホの場合は「ペースト」と書かれた箇所を長押ししてペーストを選択してください):
2021041404


うまくいくと最初にコピーした画像がブラウザ画面内の矩形部分(<canvas>)のサイズに合わせてペーストされます:
2021041405


このサンプルのソースコードはこちらで公開しています:
https://github.com/dotnsf/web_image_paste

このソースコードの中で特に該当の機能を実現しているファイルが index.html です。以下、解説を加えながらこのファイルの内容を紹介します。

まず該当部分の HTML は以下のようになっています:
<div class="container">
  <div id="canvas_div">
    <div id="cdiv">
      <div id="box" contenteditable="true">
        ペースト
      </div>
      <canvas width="80%" height="60%" id="mycanvas"></canvas>
    </div>
  </div>
</div>

画面内の「ペースト」と書かれている部分の <div id="box"> 要素に contenteditable="true" という属性がついています。これによって、この要素部分はペースト可能(Ctrl+V や右クリックメニューでペーストできる)として扱うことができるようになります。単にこの部分に画像をペーストして表示できるようにするだけであれば、この HTML だけで実現できます。

問題はここでペースト処理された画像を、この <div id="box"> 内に表示するのではなく(そのままだと <div id="box"> 内に画像がペーストされて表示されてしまうので、表示しないような処理を加えた上で)代わりにすぐ下の <canvas> 要素内に表示したい、という点です。

今回のサンプルではそういった処理は JavaScript で実現しています。まず「ペースト」と書かれた <div id="box"> 要素にペースト処理が実行されたイベントをフックして、imagePaste() 関数(後述)を実行するように指示しています。加えて false を返すことでフックしたイベントをキャンセルし、通常処理(この場合は <div id="box"> へのペースト処理)が実行されないようにしています(false を返さないと、<canvas> に画像をペーストした後でも <div id="box"> 内にも画像が残ってしまうので、それを避けるための処理です):
$(function(){
  $('#box').on( "paste", function( e ){
    imagePaste( e );
    return false;
  });

  :
  :

そして imagePaste() 関数の実装がこちらです:
function imagePaste( event ){
  var blobimg = null;
  var items = ( event.clipboardData || event.originalEvent.clipboardData ).items;
  for( var i = 0; i < items.length; i ++ ){
    if( items[i].type.indexOf( "image" ) == 0 ){
      blobimg = items[i].getAsFile();
    }
  }

  if( blobimg != null ){
    var bloburl = URL.createObjectURL( blobimg );

    var canvas = document.getElementById( "mycanvas" );
    var ctx = canvas.getContext( '2d' );

    var img = new Image();
    img.src = bloburl;
    img.onload = function(){
      var sw = img.naturalWidth;
      var sh = img.naturalHeight;
      var dw = canvas.width;
      var dh = canvas.height;
      ctx.drawImage( img, 0, 0, sw, sh, 0, 0, dw, dh );
    };
  }
}

まずクリップボード内に登録されているデータを配列で取得し、その中に画像("image")が含まれているかどうかを調べます。存在している場合はそのバイナリデータを取得します。

このバイナリデータが見つかった場合は URL.createObjectURL で画像データの URL を生成して画像化し、<canvas> 内に drawImage() 関数を使って描画します。その際に画像全体の高さや幅を <canvas> 全体の高さや幅に調整して表示するので、それらの情報を取得した上でパラメータ指定しています。

これによって「ペースト」のエリアにペーストした画像データを <canvas> 内に描画し、もとの「ペースト」のエリアには描画しない、という処理が実現できました。


本当は「ペースト」のためのエリアを使わずに <canvas> だけでここに画像を直接ペーストできるようになるのが理想なんですが、実現の可否含めてその方法がわかっていません。実現方法のヒントがありましたら教えていただけると嬉しいです。

 

このページのトップヘ