画像処理

【PHP】GDで文字列を縁取り描画する方法

更新日:2023/09/28

PHPのGDを使って文字列を縁取りする方法を紹介します。

このページの目次

 

考え方

GDの文字描画は文字の太さを変更できません。
色々考えたのですが、力業な方法しか思いつきませんでした。

下図のように文字の左上を基準として、縁取り幅×2の枠を想定します。

縁取り幅×2の枠を想定

上下左右斜めの範囲を埋めるように、1ピクセル毎に文字を描画していきます。

文字の上下左右斜めの範囲を埋めるように文字を描画

次に範囲を狭めて、別の色で同じように描画します。
これを繰り返すと、下図のような多色での縁取り文字を生成できます。

多色での縁取り文字

この流れは、外側の縁取りの上に、内側の縁取りを描画しています。
同じ文字を同じ位置に描画しているので、非効率ですね。

プログラムコード上では、内側部分を描画しないようなロジックで処理します。

 

コード

前項の考え方を元にして、GDで文字列を縁取り描画する関数を作成しました。

/*
* GDで文字列を縁取り描画する
* $image:  GdImage オブジェクト
* $fontSize: フォントサイズ
* $left , $top : 左上座標
* $edgingPatterns: 縁取りパターン配列 [['color'=>色,'thick'=>縁取り幅],[]..]
* $fontFile: フォントファイルのパス
* $texts: 描画する文字列
* $characterSpacing: 文字間隔
* $transparency: 不透明度(%) 100% 完全な不透明 
*/
function imageEdgingTTfText($image , $fontSize , $left , $top 
    , $edgingPatterns , $fontFile , $text ,$characterSpacing=0,$transparency=100){

    $len = mb_strlen($text);$patternLen = count($edgingPatterns);
    if( $len === 0 || $patternLen === 0) return;

    $topY = PHP_INT_MAX; $bottomY = PHP_INT_MIN; // 文字列BOXの上下y座標
    $leftX = 0; $fullWidth=0; // 文字列BOXの左x座標、文字列BOXの幅
    $strArray = []; // 文字毎の情報([文字,幅,左座標])格納用配列

       // 文字列BOXの左上下座標と幅と、各文字の幅・左座標を取得
    for( $c = 0 ; $c < $len ; $c ++ ) { // 文字単位でループ
        $t = mb_substr( $text , $c , 1 );
        $tb = imagettfbbox($fontSize ,0 ,$fontFile,$t);
        if( $c === 0 ) {$leftX = $tb[6];}
        $topY = min( $topY , $tb[7] ); $bottomY = max( $bottomY , $tb[1] );
        $w = $tb[2]-$tb[0]+1;$fullWidth += $w;
        $strArray[]=[$t,$w,$tb[0]]; // [文字,幅,左座標]
    }
        // 縁取り幅の総計を算出
    $egWidth = array_reduce( $edgingPatterns , 
        function($carry, $item){return $carry + $item['thick'];}, 0);

    $originalImage = null; // 透明時に使用
    
    if( $transparency >= 100 ){ // 不透明モード
                // ベースライン算出
        $baseY = $top - $topY + $egWidth;$baseX = $left - $leftX;
    }else{  // (1) 透明モード 前処理
        $originalImage = $image;    // 元画像退避
        $image = imagecreatetruecolor( // 新規画像作成
            $fullWidth + $len * $egWidth * 2 + ($len-1) * $characterSpacing,
            $bottomY - $topY + $egWidth * 2);

        $r=0;$g=1;$b=1;$transparentColor=null;

        do{ // 透明色を作成
            for( $c = 0 ; $c < $patternLen ; $c ++ ){ // 縁取り色と重ならない色を探す
                $cl = imagecolorsforindex( $image , $edgingPatterns[$c]['color'] );
                if( $r === $cl['red'] && $g === $cl['green'] && $b === $cl['blue'] ) break;
            }
            if( $c >= $patternLen ) { $transparentColor = imagecolorallocate($image,$r,$g,$b);}
            else { if( $r < 255 ){ $r++; }elseif( $g < 255 ){ $g++; }else{ $b++;} }
        }while( $transparentColor===null && $b <= 255);
        if( $transparentColor!==null ){
            imagefill($image, 0, 0, $transparentColor);
            imagecolortransparent($image,$transparentColor);
        }
            // ベースライン算出
        $baseY = -$topY+$egWidth; $baseX = - $leftX;
    }
        // (2) 描画処理
    for( $count = 0; $count < $len ; $count ++ ){
        [$t,$bw,$l] = $strArray[$count]; // [文字,幅,左座標]

            // 描画基準点の移動範囲算出
        $lx = $baseX - $l; $rx = $baseX + $egWidth * 2 - $l; // 左、右
        $ty = $baseY - $egWidth; $by = $baseY + $egWidth;  // 上、下

        foreach( $edgingPatterns as $ep ){ // 縁取りパターンのループ
 
            $thick = $ep['thick']; $color = $ep['color'];
            if( $thick===0 ) continue;
 
            for( $y = 0 ; $y < $thick ; $y ++ ){  // (3) 次の縁取りと重ならない行
                for( $x = $lx; $x <= $rx; $x ++ ){
                        // 上下同時に描画
                    ImageTTFText($image, $fontSize, 0, $x,$ty ,$color, $fontFile, $t);
                    ImageTTFText($image, $fontSize, 0, $x,$by ,$color, $fontFile, $t);
                }
                $ty ++; $by --;
            }
            for( $y = $ty ; $y <= $by ; $y ++ ){ // (4) 次の縁取りと重なる行
                for( $x = 0 ; $x < $thick ; $x++){
                        // 左右同時に描画
                    ImageTTFText($image, $fontSize, 0, $lx+$x,$y ,$color, $fontFile, $t);
                    ImageTTFText($image, $fontSize, 0, $rx-$x,$y ,$color, $fontFile, $t);
                }
            }
            $lx += $thick; $rx -= $thick;
        }
            // 中心文字を描画 
        ImageTTFText($image, $fontSize, 0, $baseX + $egWidth -$l,$baseY ,$color, $fontFile, $t);
        
            // 次の文字へ基準点(X)を移動
        $baseX += $characterSpacing+ $egWidth * 2 + $bw;
    }

    if( $originalImage !== null ){ // (5) 透明モード 後処理
        imagecopymerge( $originalImage , $image , $left , $top , 0 , 0 
            , imagesx($image) , imagesy($image) ,  $transparency );
    }
}

パラメータの$edgingPatternsは、'color'と'thick'の連想配列を要素とする配列です。

'color'はimagecolorallocate()で生成した色を指定します。

'thick'は縁取りの幅です。
縁取りを行わないときは0を指定します。

外側の縁取りから順番に配列にセットします。

次のように関数を使用します。

$text = 'GDで縁取り描画';
$fontFile = './ZenMaruGothic-Bold.ttf';

$size = 40; // フォントサイズ
$left = 20;$top = 30; // 左上座標

$image = imagecreatefromjpeg('./img.jpg');

$white = imagecolorallocate($image, 255, 255, 255);
$black = imagecolorallocate($image, 0, 0, 0);
$red = imagecolorallocate($image, 255, 0, 0);

$edgingPatterns = [
                    ['color'=>$black,'thick'=>1],
                    ['color'=>$white,'thick'=>0],
                ];

imageEdgingTTfText($image,$size,$left,$top,$edgingPatterns,$fontFile,$text);

header("Content-type: image/jpeg");
imagejpeg($image);

適当な画像を読み込んで、その上に1ピクセルの黒色で縁取りした文字を描画しています。
実行すると、次のようなイメージが生成されます。

1ピクセルの黒色で縁取り

次のようにパターン指定すると、少し太い赤文字を1ピクセルの黒色で縁取りします。

$edgingPatterns = [
                    ['color'=>$black,'thick'=>1],
                    ['color'=>$red,'thick'=>1],
                ];

少し太い赤文字を1ピクセルの黒色で縁取り

縁取りを重ねることもできます。

$edgingPatterns = [
                    ['color'=>$black,'thick'=>1],
                    ['color'=>$white,'thick'=>4],
                    ['color'=>$red,'thick'=>1],
                    ['color'=>$white,'thick'=>2],
                    ['color'=>$black,'thick'=>0]
                ];

縁取りを重ねる

最後のパラメータを指定すると、不透明な縁取り文字を描画できます。

imageEdgingTTfText($image,$size,$left,$top,$edgingPatterns,$fontFile,$text,0,50);

不透明な縁取り文字を描画

不透明な縁取り文字を重ねることで影の効果を表現できます。

$edgingPatterns2 = [
                    ['color'=>$black,'thick'=>8],
                ];
  // 影を描画
imageEdgingTTfText($image,$size,$left+5,$top+5,$edgingPatterns2,$fontFile,$text,0,70);

$edgingPatterns = [
                    ['color'=>$black,'thick'=>1],
                    ['color'=>$white,'thick'=>4],
                    ['color'=>$red,'thick'=>1],
                    ['color'=>$white,'thick'=>2],
                    ['color'=>$black,'thick'=>0]
                ];

imageEdgingTTfText($image,$size,$left,$top,$edgingPatterns,$fontFile,$text);

影の効果を表現

 

解説

縁取り文字を描画するときは、次の文字と重ならないように文字間隔を設定する必要があります。
しかしGDの文字描画関数ImageTTFText()は文字間隔を変更できません。
そのため、一文字ずつ描画します。

次の処理は前処理として、文字の分離、個々の文字の幅、パラメータで与えられた左上座標をベースラインに変換するために必要な情報を取得しています。

    $topY = PHP_INT_MAX; $bottomY = PHP_INT_MIN; // 文字列BOXの上下y座標
    $leftX = 0; $fullWidth=0; // 文字列BOXの左x座標、文字列BOXの幅(不透明時の新規画像作成に使用)
    $strArray = []; // 文字毎の情報([文字,幅,左座標])格納用配列

       // 文字列BOXの左上下座標と幅と、各文字の幅・左座標を取得
    for( $c = 0 ; $c < $len ; $c ++ ) { // 文字単位でループ
        $t = mb_substr( $text , $c , 1 );
        $tb = imagettfbbox($fontSize ,0 ,$fontFile,$t);
        if( $c === 0 ) {$leftX = $tb[6];}
        $topY = min( $topY , $tb[7] ); $bottomY = max( $bottomY , $tb[1] );
        $w = $tb[2]-$tb[0]+1;$fullWidth += $w;
        $strArray[]=[$t,$w,$tb[0]]; // [文字,幅,左座標]
    }

左上座標をベースラインに変換する方法は、次のページを参考にしてください。

コードの(1)と(2)のブロックは、不透明度を100未満にしたときの処理です。
完全に不透明なときは、そのまま描画できます。
しかしimagecolorallocatealpha()で作成した透明色で縁取りを行うと、濃淡がまだらな文字になってしまいます。
そこで(1)で新規画像を作成、(2)で縁取り文字を描画、(5)でimagecopymerge()で透明度を変更して貼り付けています。

透明度を変更して貼り付けるときは、imagecolortransparent()で透明色指定した色で背景を塗りつぶす必要があります。
このとき、縁取りで使用する色と背景色が重複するのを避けるため、次のコードで重複しない色を探しています。

        $r=0;$g=0;$b=1;$transparentColor=null;

        do{ // 透明色を作成
            for( $c = 0 ; $c < $patternLen ; $c ++ ){ // 縁取り色と重ならない色を探す
                $cl = imagecolorsforindex( $image , $edgingPatterns[$c]['color'] );
                if( $r === $cl['red'] && $g === $cl['green'] && $b === $cl['blue'] ) break;
            }
            if( $c >= $patternLen ) { $transparentColor = imagecolorallocate($image,$r,$g,$b);}
            else { if( $r < 255 ){ $r++; }elseif( $g < 255 ){ $g++; }else{ $b++;} }
        }while( $transparentColor===null && $b <= 255);

初期値が完全な黒(赤0緑0青0)でないのは、縁取り色で黒を使う可能性が高いからです。
あまり使わない色を初期値にして、forループが一回で終わることを期待しています。

文字描画は、内側の文字と重ならない部分だけを描画しています。

           for( $y = 0 ; $y < $thick ; $y ++ ){  // (3) 次の縁取りと重ならない行
                for( $x = $lx; $x <= $rx; $x ++ ){
                        // 上下同時に描画
                    ImageTTFText($image, $fontSize, 0, $x,$ty ,$color, $fontFile, $t);
                    ImageTTFText($image, $fontSize, 0, $x,$by ,$color, $fontFile, $t);
                }
                $ty ++; $by --;
            }
            for( $y = $ty ; $y <= $by ; $y ++ ){ // (4) 次の縁取りと重なる行
                for( $x = 0 ; $x < $thick ; $x++){
                        // 左右同時に描画
                    ImageTTFText($image, $fontSize, 0, $lx+$x,$y ,$color, $fontFile, $t);
                    ImageTTFText($image, $fontSize, 0, $rx-$x,$y ,$color, $fontFile, $t);
                }
            }

(3) のループで、上下部分を描画しています。
次のイメージの青で塗りつぶされた部分です。

縁取りの上下部分を描画

(4) で残りの左右部分を描画しています。
少し分かりにくいですが、次のイメージの緑色の部分です。

左右部分を描画

全ての縁取りを描画したら、最後に中心となる文字を描画します。

            // 中心文字を描画 
        ImageTTFText($image, $fontSize, 0, $baseX + $egWidth -$l,$baseY ,$color, $fontFile, $t);

一文字描画できたら、次の文字のベースライン位置を算出して描画します。

全ての文字を描画したら、終了です。

更新日:2023/09/28

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。