Manul Tech

CloudinaryでOGP Imageを自動生成してみる

当ブログをNext.jsで作り直した事は前回投稿したが、せっかくなのでqiitaやZennのように、投稿タイトルをカードにしたOGP Imageを自動生成してみたくなった。
このブログは技術系のブログによくある、記事のヘッダにその内容に関連する(?)キーボードを打つ手元、みたいなフリー画像を設定していない(そういうのが好きじゃない)ので、記事ごとにOGP Imageを設定するなら上記のようなカードを用意する必要がある。まぁ、このブログにはOGP Imageなんて別に無くたっていいんだけど。

どう実装するか

前回の投稿に書いた通り、このブログのバックエンドは相変わらずWordPressなので、PHPをガリガリ書いて自前で実装することもできるんだろうけども、できないなりにモダンな構成にしたいのでどこかのサービスを利用してみようと。
色々と調べてみた結果、「Cloudinary」に決定。似たようなサービスで「imgix」というのもあるようで、どちらかというとサイトの感じはこちらの方が好みだったけど、Cloudinaryは無料で始められるがimgixは月に最低でも$10(執筆時点)かかるということで。
今回は見送ったけれどもimgixについては以下の投稿がとても参考になった。

WWW WATCH
リアルタイム画像処理機能が充実した CDN、「imgix」 を試してみたらとても簡単で便利だった件

やってみる

CloudinaryのFreeアカウントを作って、ベースとなるカードをアップロードするところまでは問題無し。
やりたい事の要件としては

・ベースのカードにはアクセスされたくない
・URLパラメータによって他者に画像をいじられたくない
・初投稿(公開)時/タイトルを変更した時のみにAPIを叩きたい

ちなみにベースとなるカードの画像はこれ。アクセスされたくないとかいってここに上げちゃってるけど。

OGP Image base

順番は前後するけれど、3つ目の要件はWordPressのaction / filterフックでどうにでもできる。実際に実装したのは以下のようなコード。

//  投稿の保存前に発火するアクション
add_action('pre_post_update', function ($post_id, $data) {
    //  指定の投稿タイプ且つ投稿ステータスが公開時に
    if ($data['post_status'] === 'publish' && get_post_type($post_id) === 'xxx') {
        $before_title = get_the_title($post_id);
        $new_title = $data['post_title'];
        $saved_ogp_image = get_post_meta($post_id, 'ogp_image', true);

        //  OGP Imageがまだ生成されていない/タイトルが変更されている場合
        if (empty($saved_ogp_image) || ($before_title !== $new_title)) {
            //  CloudinaryのAPIを叩いてOGP Imageを生成するコード(後述)
            $ogp_image = *****;

            //  生成したOGP Image URLを保存
            update_post_meta($post_id, 'ogp_image', $ogp_image);
        }
    }
}, 10, 2);

まぁここまでは問題なし。次はいよいよCloudinaryのAPIを叩く部分。

まずはNode.jsで試してみた

最初はAPIがどんなもんか、Node.jsで試してみた。
Cloudinaryはドキュメントが充実していて、あらゆる言語でのサンプルコードもあるのでわかりやすい。

Documentation > Guids > Image transformations

% yarn add cloudinary
const cloudinary = require('cloudinary')

cloudinary.config({
    cloud_name: '(cloud_name)',
    api_key: '(api_key)',
    api_secret: '(api_secret)'
})

const TEXT = 'テスト'

const options = {
    sign_url: true,
    type: 'private',
    secure: true,
    transformation: [
        {
            width: 1200
        },
        {
            width: 1000,
            overlay: {
                font_family: 'NotoSansJP-Medium.otf',
                font_size: 50,
                font_weight: 'bold',
                text: encodeURI(TEXT),
                letter_spacing: 5,
                text_align: 'center'
            },
            crop: 'fit',
            color: '#222222'
        }
    ]
}

const res = cloudinary.image('base_image.png', options)

上記のコードを実行するとresに画像タグ(<img src="xxxx">)が返ってくる。
sign_url: true type: 'private'のオプションはベースとなる画像を非公開としているために必要なものらしい。

DevelopersIO
Cloudinary におけるアクセス制限の設定方法と署名付きURL

これを設定することで、APIでの画像生成はできるけど、URLのパラメータをいじって元画像を変更されるようなことは無くなる。今回の要件としてはばっちり。

また、フォントはGoogleフォントのほとんどが使用できるほか、日本語フォントでも自分でアップロードすることで利用できるようになるんだとか。

Qiita
CloudinaryがテキストオーバーレイでサポートしているGoogle Fontを調べてみた

問題はPHP

Node.jsで簡単にできたからこの調子でPHPも、と思ったらそうは簡単には行かなかった。
まずはcomposerSDKを入れる。v1とv2があるらしいからそこはv2にしておく。

% composer require "cloudinary/cloudinary_php:^2"

ここまではいいんだけど、公式のPHPのサンプルコードが、、

(new ImageTag('front_face.png'))
  ->resize(Resize::thumbnail()->width(150)->height(150)->gravity(Gravity::focusOn(FocusOn::face())))
  ->roundCorners(RoundCorners::byRadius(20))
  ->effect(Effect::sepia())
  ->overlay(
      Overlay::source(Source::image('cloudinary_icon_blue')
        ->transformation((new ImageTransformation())
          ->adjust(Adjust::brightness()->level(100))
          ->adjust(Adjust::opacity(60))
          ->resize(Resize::scale()->width(50))))
      ->position((new Position())
        ->gravity(Gravity::compass(Compass::southEast()))
        ->offsetX(5)->offsetY(5)))
    ->rotate(Rotate::byAngle(10));

オエッ!🤮なにこれ超わかりづらい。Node.jsとの差がエグい。
しかもtypeprivateにする方法がまぁ見当たらない。特にv2での情報が全然ない。
ググったり、SDKのソースコードを眺めたり、それっぽい関数に適当に引数をあててみたり(当然エラー)すること数時間、公式のドキュメントの中に以下のようなコードを発見。

ImageTag::fromParams("actor", ["transformation" => [
  ["effect" =>  "cartoonify"],
  ["radius" => "max"],
  ["effect" => "outline:100", "color" => "lightblue"],
  ["background" => "lightblue"],
  ["height" => 300, "crop" => "scale"]
]]);

このfromParamsという関数を利用すればNode.jsのような書き方ができるのでは!?
この記述があるのはSDK v1の方で、そりゃ気付かない訳だ。。

fromParamsで書いてみた

生成後の画像URLを返してくれるImageクラスで以下のように書いてみた。
引数の構造はほぼNode.jsと一緒。

<?php

namespace Cloudinary;

require_once 'vendor/autoload.php';

Configuration\Configuration::instance([
    'cloud' => [
        'cloud_name' => '(cloud_name)',
        'api_key'    => '(api_key)',
        'api_secret' => '(api_secret)',
    ],

    'url' => [
        'sign_url' => true,
        'secure' => true,
    ]
]);

function create_ogp_image($text = ''){
    if(empty($text)){
        return '';
    }

    $text = urlencode($text);

    return Asset\Image::fromParams('base_image.png', [
        'type' => 'private',
        'transformation' => [
            [
                'width' => 1200
            ],

            [
                'width' => 1000,
                'crop' => 'fit',
                'color' => '#222222',
                'overlay' => [
                    'font_family' => 'NotoSansJP-Medium.otf',
                    'font_size' => 50,
                    'font_weight' => 'bold',
                    'text' => $text,
                    'letter_spacing' => 5,
                    'text_align' => 'center'
                ]
            ]
        ]
    ]);
}

すると見事に成功!🎉

ogp image

上記のWordPressのフックと合わせて今回は完成。
自分の英語力不足のせいなのか、情報は豊富だけど見づらい(?)Cloudinaryのドキュメントのせいなのか、なんかとても疲れた。。

コメント0