PHP
laravel
GD

phpのGDライブラリを試してみました!

どうもはじめまして!
Qiitaへの初投稿にガクブルしながら記事を書いているMashikaです!

もはやテスト投稿的な感じで、自分の備忘録を記載しておこうと思います。
内容としてはタイトルの通り、phpの画像関連ライブラリであるGDライブラリを使って、画像加工のWebアプリケーションを作った時のことを書いていきます。

ちなみに、Webアプリケーションを作成したきっかけは、とある友人へ仲間達から各自で自撮りした写真へメッセージを添えた画像をアルバムにして渡そうという話しが出た時に、各自画像編集アプリを落としてメッセージを入れてというのが、めんどくさくて頑張ってくれない人もいたため、できるだけ簡単に画像を作れればと思って開発を開始いたしました。

せっかく身に着けたITスキルを身近なところで活用できた時に、エンジニアになって良かったって思いますよね。多分。
ただ、結構独学で身に着けた部分が多いので、間違いやよりよい方法があれば、ご指摘いただけると嬉しいです!

開発環境


ということで、以下の開発環境で開発を行いました。
・サーバーはエックスサーバーからレンタル
・ドメインはお名前.comで取得
・ローカルの開発環境は、xamppとVisual Studio Codeを使用
・言語はPHP(7.2.6)でフレームワークにLaravel(5.6)を使用
・エックスサーバーへのデプロイは、GitHubを用いて実施

ぶっちゃけ開発云々より、エックスサーバー上にデプロイして、稼働させるのが1番苦労しました。
ほぼ全てがエックスサーバー上のPHPのバージョンとlaravelのバージョンが合ってないことが問題だったので、単純に私の経験不足なんだと思いますが。。
エックスサーバーのサーバーパネル上で変更するPHPのバージョンとSSH接続した時のCLI上で認識されるPHPのバージョンが違うこととかは、一瞬こういうのが詰みなんだと思いました。。
今となってはいい思い出。それこそがエンジニアライフ。あはは

開発物


開発したWebアプリケーションとしては、入力フォームを入力すると・・・
こんな画像とか
FORM1.PNG
1536823013_ad68aa6edcdad506fdcebc90885e130eef33c315.jpg

こんな画像が
FORM2.PNG
1536823224_f790cc5345e55409f80a9168b13dd07810659479.jpg

簡単に作れちゃうよーっていうものです。
まぁ、今回みたいな尖った条件じゃなかったら、全然いらないWebアプリですよね。笑

GDライブラリについて


「PHP 画像加工 ライブラリ」とかで検索すると真っ先にでてくるGDライブラリですが、PHP4.3以降のバージョンではデフォルトの拡張機能になっているそうで、特にライブラリを新規に読み込んだりはしないでも使えます。
この後出てくるコードでガンガン使ってますが、php.net見ていただければ余裕だと思われますので、なんだこれと思ったらご参照ください。

それではコーディング

まえおき

contentっていうviewとImageEditControllerっていうcontrollerを使うシンプル設計です。
加工前画像の保存場所として、storageフォルダ配下にimagesってフォルダと加工後画像の保存場所としてnew_imagesってフォルダを作ってます。
また、文字印字のためのフォントファイルをresourcesフォルダ配下にfontsってフォルダ作っておいてあります。
あと、コードをバーンって晒しても恥ずかしいだけなので、抜粋して解説入れながら記載していきます。
また、すみませんが今回はviewのコードは割愛いたします。。まぁ大したこともしていないので。。

アップロードファイルの確認/処理


まずは、ファイルが入力フォームからアップロードされているかと拡張子に対するチェックを実施して、問題があれば、エラーメッセージを付与して入力フォームのViewへ戻ります。
// 画像のアップロード確認
if(!is_uploaded_file($_FILES['image']['tmp_name']))
{
  $param += [
    'emess' => '画像を選択してください。'
  ];
  return view('content', $param);
}

// 画像の拡張子確認
if(!(mb_substr($_FILES['image']['name'], -3) == 'jpg' ||
     mb_substr($_FILES['image']['name'], -4) == 'jpeg' ||
     mb_substr($_FILES['image']['name'], -3) == 'png'))
{
  $param += [
    'emess' => 'JPEGもしくはPNG形式の画像を使用してください。',
  ];
  return view('content', $param);
}

チェックに通ったら、アップロードしたファイルを指定のフォルダへ移動させます。
名前は被らないように時間と乱数を使って付与しています。

// ファイル名を生成
if(mb_substr($_FILES['image']['name'], -3) == 'jpg' ||   
   mb_substr($_FILES['image']['name'], -3) == 'png')
{
  $ext = mb_substr($_FILES['image']['name'], -3);
}
else
{
  $ext = mb_substr($_FILES['image']['name'], -4);
}
$format = '%s_%s.%s';
$time = time();
$sha1 = sha1(uniqid(mt_rand(),true));
$file_name = sprintf($format,$time,$sha1,$ext);

// アップロードされたファイルを指定のフォルダに移動
move_uploaded_file($_FILES['image']['tmp_name'], dirname(__FILE__) . '/../../../storage/images/' . $file_name);
$image_path = $domain . '/ImageEdit/storage/images/' . $file_name;

// アップロードされた画像の情報を取得
list($w, $h, $type) = getimagesize($image_path);

// pngとjpgで処理関数を分けて画像をインスタンス化
$image = null;
if($ext == 'png')
{
  $image = imagecreatefrompng($image_path);
}
else
{
  $image = imagecreatefromjpeg($image_path);
}

真っ黒?なキャンバスを作成

新しい画像の元となるキャンバス的なものを作成します。
画像が縦向きの時は半分画像にして、残り半分を色で塗りつぶして、画像が横向きの時は、画像を全体に展開して半分を塗りつぶし色で透かせられるようにしています。
縦横の比率を元画像と同じにしないと変に画像が伸びてしまうので、新しい画像の幅を1920pxに固定して、縦方向の高さは横の比率に合わせて動的に設定します。

// 新しい画像のキャンバスを作成
$new_w  = 1920;
if($request->angle == 1) // 縦の場合
{
  $w_per = $new_w / 2 / $w;
  $new_h = $h * $w_per;
}
else  // 横の場合
{
  $w_per = $new_w / $w;
  $new_h = $h * $w_per;
}
$new_image = imagecreatetruecolor($new_w, $new_h);

キャンバスに画像を配置

画像を配置する際に画像が左右どちらに表示されるかも選べるようにしたので、画像の配置位置もフォームの入力値をもとに変えてあげます。

// 入力値から新しい画像領域の幅と高さと位置を決定
if($request->angle == 1) // 縦の場合
{
    // 新しい画像領域の幅と高さ
    $new_image_w = $new_w / 2;
    $new_image_y= $new_h;

    // 画像の位置
  if($request->position == 1) // 左の場合
    {
      $position_x = 0;
      $position_y = 0;
    }
    else // 右の場合
    {
      $position_x = $new_w / 2 + 1;
      $position_y = 0;
    }
}
else // 横の場合
{
  // 新しい画像領域の幅と高さ
  $new_image_w = $new_w ;
  $new_image_y= $new_h;

  // 画像の位置
  $position_x = 0;
  $position_y = 0;          
}

ということで、ここまで作ったパラメーターを持って画像をキャンバスへ展開します。

// 位置を指定して画像をキャンバスに設置
imagecopyresampled($new_image, $image, $position_x, $position_y, 0, 0, $new_image_w , $new_image_y, $w, $h);

単色での塗りつぶし

要領は画像配置と同じです。
というか四角図形を描画するわけなのですが、色は入力値の16進数を10進数に変換して、RGBの値を指定してあげます。

ちなみに、入力フォームでの色指定に最初inputタグのtype="color"を使っていたんですが、スマホのブラウザだとカラーパレットから色選べなくて不便だったので、基本色の選択リストにしました。(よりスマートな方法があればご教示ください。。)

もう1個ちなむと色をインスタンス化させる際に使っている関数は、透過度を指定するもの(imagecolorallocatealpha)と指定しないもの(imagecolorallocate)で2種類あって、透過設定する場合には、前者の最終引数に0~127の値を渡してあげます。(0が不透明で、127が完全透過)

// 塗りつぶし領域を指定
if($request->position == 1) // 縦の場合
{
  // 塗りつぶしの幅と高さ
  $color_w = $new_w;
  $color_h = $new_h;

  // 塗りつぶしの位置
  $color_x = $new_w / 2 + 1;
  $color_y = 0;
}
else // 横の場合
{
  // 塗りつぶしの幅と高さ
  $color_w = $new_w / 2;
  $color_h = $new_h;

  // 塗りつぶしの位置
  $color_x = 0;
  $color_y = 0;
}

// 塗りつぶしの色を取得
$bgcolor_r = hexdec(substr($request->bgcolor, 1, 2));
$bgcolor_g = hexdec(substr($request->bgcolor, 3, 2));
$bgcolor_b = hexdec(substr($request->bgcolor, 5, 2));

// 最後の引数で透過度を指定しながら、色をインスタンス化
$bgcolor = imagecolorallocatealpha($new_image, $bgcolor_r, $bgcolor_g, $bgcolor_b, $request->permeability);

// 塗りつぶしを実施
imagefilledrectangle($new_image, $black_x, $black_y, $e_black_x, $e_black_y, $bgcolor);

文字の印字

入力フォームのメッセージの値を印字領域からはみ出さないように折り返しさせながら印字するために、改行コードで分割して、1行ずつ呼び出して、文字数が閾値を越えたら次の行になるように、配列に値を格納していきます。
ネットで調べた比率を使って、フォントサイズをピクセル変換したりしてます。

// パラメーター
$c_p = 60; // 余白
$c_s = $request->fsize; // 指定フォントサイズ
$c_s_p = $request->fsize / 72 * 96; // フォントサイズをピクセル変換
$c_h = $request->fsize / 72 * 96; // 1行の高さ
$r_c = ($new_w / 2 - $c_p) / $c_s_p; // 1行への印字文字数上限

// メッセージを1行ずつに分割
$mess = $request->message; // メッセージの値を取得
$mess = trim($mess); // 文頭文末の空白を削除
$cr = array("\r\n", "\r"); // 改行コード置換用配列を作成しておく
$mess = str_replace($cr, "\n", $mess); // 改行コードを統一
$mess_array = explode("\n", $mess); // 改行コードで分割

// 1行ずつ取得して加工しながらアウトプット用の配列に格納
$output_array = array(); // アウトプット用の配列
foreach($mess_array as $m) // 1行ずつの処理
{
  if(mb_strlen($m) > $r_c)
  {
     while(mb_strlen($m) > $r_c)
     {
       $outstr = mb_substr($m, 0, $r_c);
       array_push($output_array, $outstr);
       $m = mb_substr($m, $r_c);
     }
     array_push($output_array, $m);
  }
  else
  {
    array_push($output_array, $m);
  }
}

y軸方向の配置は文字の量に応じて丁度良く真ん中になるように高さを調整してます。
x軸方向の配置は画像が右か左かで変更する必要があるので、変えてます。
色の指定は塗りつぶしの透過なしバージョンです。
フォントの指定は、一応フリーフォントファイルいくつか落としてきて、それなり漢字が使えるものから9個ほどに絞って選べるようにしました。
フォント名とフォントファイル名は管理しやすいようにconfigフォルダ下にconstファイル作って定数化させてます。

// 文字のy軸の位置調整
$row_cnt = count($output_array);
$char_y = ($new_h - ($row_cnt + 1) * $c_h) / 2;

// 文字のx軸の位置調整
if($request->position == 1)
{
  $char_x = $new_w / 2 + $c_p / 2;
}
else
{
  $char_x = $c_p / 2;
}

// 塗りつぶしの色指定
$fcolor_r = hexdec(substr($request->fcolor, 1, 2));
$fcolor_g = hexdec(substr($request->fcolor, 3, 2));
$fcolor_b = hexdec(substr($request->fcolor, 5, 2));
$fcolor = imagecolorallocate($new_image, $fcolor_r, $fcolor_g, $fcolor_b);

// フォント指定
$font =  dirname(__FILE__) . '/../../../resources/fonts/' . Config('const.font' . $request->font);

ここまできたら後は印字するだけなのですが、一応、文字の左寄せか中央寄せかを選べるようにしたので、そのための処理が入ってます。

// 文字を印字
foreach($output_array as $om) // 一行ずつ呼び出し
{
  $plp = 0; // 中央寄せした場合の左からの位置
  if($request->asked == 2)  // 中央寄せの場合
  {
    // 中央寄せした場合の左からの位置を算出
    $plp = ($new_w / 2 - $c_p - mb_strlen($om) * $c_s_p) / 2;
  }
  // 各パラメーラーの値を用いて印字
  imagettftext($new_image, $c_s, 0, $char_x + $plp, $char_y, $fcolor, $font, $om);
  // 印字したら、次の行の印字のために1行分印字領域を下へ
  $char_y += $c_h;
}

ここまでで、画像は作成できたので、あとは、保存してそれを表示してあげればOK!
今回はpng形式で書き出してます。

// 画像を保存する
imagepng($new_image, dirname(__FILE__) . '/../../../storage/new_images/' . $file_name);
$new_image_path = $domain . '/ImageEdit/storage/new_images/' . $file_name;

//最後にメモリを開放する
imagedestroy($new_image);

// 戻り値
$param += [
  'imageurl' => $new_image_path
];
return view('content', $param);

まとめ

開発時間はGDライブラリ初心者なので、調べながらで合計で12時間くらいかかってます。
ってかかなりの時間をデプロイにとられました。。

あと、、ぶっちゃけ半角文字対応が上手にできておらず、半角文字を使うと折り返しや中央寄せでの表示が綺麗にできてません。。

とまぁまだまだ要素ばかりの開発でしたが、やっぱり自分が作ったアプリケーションをみんなが使ってくれているのはなんか嬉しいですね。

もっと色々早くクオリティ高くできるようになりたい。
ただただそう思うMashikaなのでした。

いーーーーーーーーーーじょう!!!!!!!!!!