C言語でアルファチャンネル付きPNGを画像処理

このページにはプロモーションが含まれています

このページではアルファチャンネル付きのPNGを用いたC言語での画像処理について解説します。具体的なイメージがつきやすいようにソースコードも多く載せているため、ページのボリュームが大きいです。興味あるところが絞られているのであれば、下記の目次のリンクを利用してください。

アルファチャンネルとは?

まずアルファチャンネルとは何か理解していきましょう。アルファチャンネルの説明をWikipediaから引用します。

αチャンネル(アルファチャンネル、英: alpha channel)とは画像処理分野において、各ピクセルに対し色表現のデータとは別にもたせた補助データのこと。一般に画素の不透明度 (opacity) を表現する。

下のページで画像データは画素の集まりであり、各画素はRGBの3つの輝度情報を持つと説明しています。

画像データの解説ページアイキャッチ 画像データの構造・画素・ビットマップデータについて解説

しかし、アルファチャンネル付きの画像データでは各画素はRGB+αの4つの情報を持ちます。そしてこのアルファチャンネルは一般にその画素の不透明度を表現するものです。「一般に」とわざわざ説明している通り、不透明度とは違う使い方もありますが、このページではアルファチャンネルは不透明度を表現するものとして扱います。

アルファチャンネル付きのPNG画像

PNG画像では画素にRGBだけでなくアルファチャンネル(不透明度)を付加することができます。

例えば下のPNG画像は格子状にアルファチャンネルを255(不透明)・128(半透明)に設定した画像です。

私がMACのプレビューでこのPNGを開くと下の図のようになりました。プレビューソフトの背景が黒色なので、アルファチャンネルが128(半透明)の画素はその背景の黒色が透けた感じに見えています。

スポンサーリンク

PNGファイルの読み込み・書き込み

さて、ここからアルファチャンネル付きPNGファイルを用いたプログラムを用いて解説していきたいと思います。が、その前にPNGファイルの読み込みと書き込み(ファイル保存)するプログラムについて触れておきます。下記のページでlibpngを用いたPNG画像の読み込み・書き込みについて解説しています。

libpngの使い方解説ページアイキャッチ 【C言語】libpngのインストールと使用方法・使用例

このページに記載するプログラムにおいても、画像データの情報を格納する構造体として上ページに記載しているBITMAPDATA_tを、PNGファイルの読み込みはpngFileReadDecode関数を、PNGファイルの書き込みはpngFileEncodeWrite関数を使用していますので、よろしければ上のページも見てみてください。

pngFileReadDecode関数ではPNGファイルを読み込んでBITMAP形式の画像データを生成します。またpngFileEncodeWrite関数ではBITMAP形式の画像データをPNGファイルとして書き出しします。どちらの関数もアルファチャンネル付き・アルファチャンネルなしの両方のPNGファイルに対応しています。

上のページで紹介した通りにlibpngをインストールし、myPng.cとmyPng.hを用意しておけば下記のソースコードの通りにプログラムを実行できると思います。

RGBとアルファチャンネルを分離するプログラム

もっとアルファチャンネルのイメージが付きやすいように、まずアルファチャンネル付きPNGファイルをRGB画像とアルファチャンネル画像に分離してみましょう。

スポンサーリンク

ソースコード

分離するプログラムのソースコードは下記のようになります。

separate.c
#include "myPng.h"

#define ALPHA_CH 3

/* inのRGB部分だけをコピーした画像データoutを生成 */
int getRgb(BITMAPDATA_t *out, BITMAPDATA_t *in){
  int i, j, c;

  out->height = in->height;
  out->width = in ->width;
  out->ch = in->ch - 1;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      for(c = 0; c < out->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + c];
      }
    }
  }
  return 0;
}

/* inのα部分だけをコピーした画像データoutを生成 */
int getAlpha(BITMAPDATA_t *out, BITMAPDATA_t *in){
  int i, j, c;

  out->height = in->height;
  out->width = in ->width;
  out->ch = in->ch - 1;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      for(c = 0; c < out->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + ALPHA_CH];
      }
    }
  }
  return 0;
}

int main(int argc, char *argv[]){

  BITMAPDATA_t in, rgb, alpha;
  char outname[256];

  if(argc != 2){
    printf("ファイル名が指定されていません\n");
    return -1;
  }

  if(pngFileReadDecode(&in, argv[1]) == -1){
    printf("pngFileReadDecode error\n");
    return -1;
  }
  if(in.ch != 4){
    printf("chnnel num is invalid\n");
    freeBitmapData(&in);
    return -1;
  }

  if(getRgb(&rgb, &in) == -1) {
    printf("rgba2Rgb error\n");
    freeBitmapData(&in);
    return -1;
  }

  sprintf(outname, "%s", "rgb.PNG");

  if(pngFileEncodeWrite(&rgb, outname) == -1){
    printf("pngFileEncodeWrite error\n");
    freeBitmapData(&rgb);
    freeBitmapData(&in);
    return -1;
  }

  freeBitmapData(&rgb);

  if(getAlpha(&alpha, &in) == -1) {
    printf("rgba2Rgb error\n");
    freeBitmapData(&in);
    return -1;
  }

  sprintf(outname, "%s", "alpha.PNG");

  if(pngFileEncodeWrite(&alpha, outname) == -1){
    printf("pngFileEncodeWrite error\n");
    freeBitmapData(&alpha);
    freeBitmapData(&in);
    return -1;
  }

  freeBitmapData(&in);

  return 0;
}

コンパイルと実行ファイル生成は下記コマンドで作成可能です。

gcc myPng.c -c
gcc separate.c -c
gcc separate.o myPng.o -lpng -o separate.exe
./separate.exe addalpha.PNG 

入力ファイルはアルファ付きのPNG画像であることを想定しています。

スポンサーリンク

スポンサーリンク

実行結果

実行すると、rgb.PNGとalpha.PNGが出力されると思います。ちなみにaddalpha.PNGはアルファチャンネル付きのPNG画像で紹介したPNG画像です。

・rgb.PNG

・alphaPNG

alpha.PNGは格子状に白色と灰色が交互に現れる画像になっています。アルファチャンネルは不透明度を表す情報ですので、白色(画素値255)の部分は不透明であり、灰色(画素値128)の部分は半透明であることを示しています。rgb.PNGに対してalpha.PNGの灰色部分を半透明にした画像が入力ファイルのaddalpha.PNGになることが想像できると思います。つまり、アルファチャンネル付き画像は、元のRGB画像に対してアルファチャンネルに基づいて画素ごとに不透明度を設定した画像となります。

スポンサーリンク

ソースコードの説明

RGB部分だけを取得するgetRgb関数では出力画像の値の格納を下記のように行なっています。cが0の時は、出力画像の(i, j)座標の画素のRに、入力画像の同じ座標の画素のRをコピーしています。同様のコピー処理をcが1の時は画素のGに対して、cが2の時は画素のBに対して行なっています。

RGBのコピー
      for(c = 0; c < out->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + c];
      }

それに対しアルファチャンネルだけを取得するgetAlpha関数では下記のように値の格納を行なっています。座標等の考え方はgetRgb関数と同じですが、出力画像のRGBそれぞれに対して入力画像のアルファチャンネルをコピーしています。これによりアルファチャンネルのみを取得、それをRGB画像と見立てて画像データを生成しています。

アルファチャンネルのコピー
      for(c = 0; c < out->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + ALPHA_CH];
      }

もし画素値取得についてまだ理解できていない方は下記の記事を先に読むと良いかもしれません。

C言語での画像の画素値取得

アルファチャンネルを追加するプログラム

続いてアルファチャンネルなしの PNG画像からアルファチャンネル付きPNG画像を生成するプログラムを紹介します。

スポンサーリンク

ソースコード

addalpha.c
#include "myPng.h"

/* inにアルファチャンネルを付加した画像データoutを生成 */
int addAlpha(BITMAPDATA_t *out, BITMAPDATA_t *in){
  int i, j, c;

  out->height = in->height;
  out->width = in ->width;
  out->ch = in->ch + 1;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      for(c = 0; c < in->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + c];
      }
      if(j % 64 < 32 && i % 64 < 32) {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] =  255;
      } else if(j % 64 >= 32 && i % 64 >= 32) {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] =  255;
      } else {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] = 128;
      }
    }
  }
  return 0;
}

int main(int argc, char *argv[]){

  BITMAPDATA_t in, out;
  char outname[256];

  if(argc != 2){
    printf("ファイル名が指定されていません\n");
    return -1;
  }

  if(pngFileReadDecode(&in, argv[1]) == -1){
    printf("pngFileReadDecode error\n");
    return -1;
  }

  if(addAlpha(&out, &in) == -1) {
    printf("addAlpha error\n");
    freeBitmapData(&in);
    return -1;
  }

  sprintf(outname, "%s", "addalpha.PNG");

  if(pngFileEncodeWrite(&out, outname) == -1){
    printf("pngFileEncodeWrite error\n");
    freeBitmapData(&out);
    freeBitmapData(&in);
    return -1;
  }

  freeBitmapData(&out);
  freeBitmapData(&in);

  return 0;
}

コンパイルと実行ファイルの生成・実行は下記コマンドで行えます。

gcc myPng.c -c
gcc addalpha.c -c
gcc addalpha.o myPng.o -lpng -o addalpha.exe
./addalpha.exe rgb.PNG 

入力ファイルはRGBのPNG画像であることを想定しています。ちなみにrgb.PNGはRGBとアルファチャンネルを分離するプログラムの出力結果です。

スポンサーリンク

スポンサーリンク

実行結果

rgb.PNGを入力すると、下のようなアルファチャンネル付き画像が出力されるはずです。

・addalpha.PNG

まさに最初に紹介したアルファチャンネル付きのPNG画像ですね。

スポンサーリンク

ソースコードの説明

アルファチャンネルの値を設定しているのは下記です。

アルファチャンネル値設定
      if(j % 64 < 32 && i % 64 < 32) {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] =  255;
      } else if(j % 64 >= 32 && i % 64 >= 32) {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] =  255;
      } else {
        out->data[out->ch * (i + j * out->width) + (out->ch - 1)] = 128;
      }

ですので、この部分を変更すれば様々なアルファチャンネルを設定することも可能です。

スポンサーリンク

アルファチャンネルを除去するプログラム

今度はアルファチャンネルを除去するプログラムについて紹介します。アルファチャンネルに対応していないプレビューソフトや画像フォーマットもありますので、そういったファイルでも開けるように、RGBA画像をRGB画像に変換します。

スポンサーリンク

ソースコード

rgba2rgb.c
#include "myPng.h"

#define ALPHA_CH 3

/* inのアルファチャンネルを考慮してRGB画像データoutを生成 */
int rgba2Rgb(BITMAPDATA_t *out, BITMAPDATA_t *in){
  int i, j, c;
  unsigned int tmp;
  double a;

  out->height = in->height;
  out->width = in ->width;
  out->ch = in->ch - 1;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      a = (double)in->data[in->ch * (i + j * in->width) + ALPHA_CH] / 256;
      for(c = 0; c < out->ch; c++) {
        tmp = (unsigned int)(double)in->data[in->ch * (i + j * in->width) + c] * a + 255 * (1 - a);
        if(tmp >= 256) {
          tmp = 255;
        }
        out->data[out->ch * (i + j * out->width) + c] = (unsigned char)tmp;
      }
    }
  }
  return 0;
}

int main(int argc, char *argv[]){

  BITMAPDATA_t in, out;
  char outname[256];

  if(argc != 2){
    printf("ファイル名が指定されていません\n");
    return -1;
  }

  if(pngFileReadDecode(&in, argv[1]) == -1){
    printf("pngFileReadDecode error\n");
    return -1;
  }
  if(in.ch != 4){
    printf("chnnel num is invalie\n");
    freeBitmapData(&in);
    return -1;
  }

  if(rgba2Rgb(&out, &in) == -1) {
    printf("rgba2Rgb error\n");
    freeBitmapData(&in);
    return -1;
  }

  sprintf(outname, "%s", "rgba2rgb.PNG");

  if(pngFileEncodeWrite(&out, outname) == -1){
    printf("pngFileEncodeWrite error\n");
    freeBitmapData(&out);
    freeBitmapData(&in);
    return -1;
  }

  freeBitmapData(&out);
  freeBitmapData(&in);

  return 0;
}

コンパイルと実行ファイルの生成・実行は下記コマンドで行えます。

gcc myPng.c -c
gcc rgba2rgb.c -c
gcc rgba2rgb.o myPng.o -lpng -o rgba2rgb.exe
./rgba2rgb.exe addalpha.PNG 

入力ファイルはアルファチャンネル付きRGBAのPNG画像であることを想定しています。addalpha.PNGはアルファチャンネル付きのPNG画像で紹介したPNG画像です。

スポンサーリンク

スポンサーリンク

実行結果

addalpha.PNGを入力した場合、下のようなrgba2rgb.PNGが出力されるはずです。

入力画像と変わらないじゃん!と思う人も多いかもしれません。

が、画像の情報やファイルのプロパティを見てみると違いがまず分かると思います。

例えば Mac であれば、ファイルを右クリックして画像の情報を確認してみると、入力のaddalpha.PNGだと「アルファチャンネル:はい」に対して、出力のrgba2rgb.PNGだと「アルファチャンネル:いいえ」になっており、rgba2rgb.PNGはアルファチャンネルが無いにも関わらず、アルファチャンネルのあるaddalpha.PNGと同じ見た目になっています。

これは、上のプログラムでaddalpha.PNGのアルファチャンネルに従って、RGB画素を生成した結果だと言えます。

また、MACのプレビューで画像を開くと差は歴然です。

・入力画像(addalpha.PNG)

・出力画像(rgba2rgb.PNG)

入力画像ではプレビューソフトの背景が黒なので、アルファチャンネルが半透明になっている部分はその黒色が透けて見えます。しかし、出力画像では黒色で透けなくなっています。つまり、RGB変換後の画像にはもはや不透明度の情報は残っていないのです。

スポンサーリンク

ソースコードの説明

下記の部分でアルファチャンネルを考慮しつつ各画素のRGB値の計算を行なっています。

RGBへのアルファチャンネル値の反映
      a = (double)in->data[in->ch * (i + j * in->width) + ALPHA_CH] / 256;
      for(c = 0; c < out->ch; c++) {
        tmp = (unsigned int)(double)in->data[in->ch * (i + j * in->width) + c] * a + 255 * (1 - a);
        if(tmp >= 256) {
          tmp = 255;
        }
        out->data[out->ch * (i + j * out->width) + c] = (unsigned char)tmp;
      }

この計算式についてはWikipediaを参考にしています。

参考 アルファブレンドWikipedia

なかなか理解するのが難しかったですが・・・。分からない点があればコメントか問い合わせをいただければ回答できる部分は回答します。

アルファチャンネル付き画像を合成するプログラム

アルファチャンネルを除去するプログラムではアルファチャンネルを除去したRGB画像を生成しました。実はこれは「真っ白な画像」と「アルファチャンネル付き画像」を合成する処理によってRGB画像を生成しているのです。

真っ白な画像だけでなく、他の画像でも画像を合成し、合成画像を取得することが可能です。

スポンサーリンク

ソースコード

composit.c
#include "myPng.h"

#define ALPHA_CH 3

/* inにアルファチャンネルを付加した画像データoutを生成 */
int addAlpha(BITMAPDATA_t *out, BITMAPDATA_t *in){
  int i, j, c;

  out->height = in->height;
  out->width = in ->width;
  out->ch = in->ch + 1;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      for(c = 0; c < in->ch; c++) {
        out->data[out->ch * (i + j * out->width) + c] =
          in->data[in->ch * (i + j * in->width) + c];
      }
      /* 追加するアルファチャンネルは全て0(不透明) */
      out->data[out->ch * (i + j * out->width) + (out->ch - 1)] =  0;
    }
  }
  return 0;
}

/* inのアルファチャンネルを考慮してRGB画像データoutを生成 */
int compoist(BITMAPDATA_t *out, BITMAPDATA_t *in1, BITMAPDATA_t *in2){
  int i, j, c;
  unsigned int tmpRgb, tmpAlpha;
  double a1, a2;

  out->height = in1->height;
  out->width = in2->width;
  out->ch = in2->ch;

  out->data = (unsigned char*)malloc(sizeof(unsigned char) * out->height * out->width * out->ch);
  if(out->data == NULL) {
    printf("malloc error\n");
    return -1;
  }

  for(j = 0; j < out->height; j++) {
    for(i = 0; i < out->width; i++) {
      a1 = (double)in1->data[in1->ch * (i + j * in1->width) + ALPHA_CH] / 256;
      a2 = (double)in2->data[in2->ch * (i + j * in2->width) + ALPHA_CH] / 256;
      for(c = 0; c < out->ch; c++) {
        if(a1 + a2 * (1 - a1) == 0 ) {
          tmpRgb = 0;
        } else {
          tmpRgb =
            (unsigned int)(
              (double)in1->data[in1->ch * (i + j * in1->width) + c] * a1
               + (double)in2->data[in2->ch * (i + j * in2->width) + c] * a2
               * (1 - a1)
             ) / (a1 + a2 * (1- a1));
        }
        if(tmpRgb >= 256) {
          tmpRgb = 255;
        }
        out->data[out->ch * (i + j * out->width) + c] = (unsigned char)tmpRgb;
      }
      tmpAlpha = (unsigned int)((a1 + a2 * (1 - a1)) * 256);
      if(tmpAlpha >= 256){
        out->data[out->ch * (i + j * out->width) + ALPHA_CH] = 255;
      } else {
        out->data[out->ch * (i + j * out->width) + ALPHA_CH] = (unsigned char)tmpAlpha;
      }
    }
  }
  return 0;
}

int main(int argc, char *argv[]){

  BITMAPDATA_t in1, in2, out, tmp;
  char outname[256];

  if(argc != 3){
    printf("ファイル名が2つ指定されていません\n");
    return -1;
  }

  if(pngFileReadDecode(&in1, argv[1]) == -1){
    printf("pngFileReadDecode error\n");
    return -1;
  }
  if(pngFileReadDecode(&in2, argv[2]) == -1){
    printf("pngFileReadDecode error\n");
    freeBitmapData(&in1);
    return -1;
  }

  if(in1.height != in2.height || in1.width != in2.width){
    printf("size error\n");
    freeBitmapData(&in1);
    freeBitmapData(&in2);
    return -1;
  }

  if(in1.ch == 3){
    addAlpha(&tmp, &in1);
    freeBitmapData(&in1);
    memcpy(&in1, &tmp, sizeof(BITMAPDATA_t));
  }
  if(in2.ch == 3){
    addAlpha(&tmp, &in2);
    freeBitmapData(&in2);
    memcpy(&in2, &tmp, sizeof(BITMAPDATA_t));
  }

  if(compoist(&out, &in1, &in2) == -1) {
    printf("compoist error\n");
    freeBitmapData(&in1);
    freeBitmapData(&in2);
    return -1;
  }

  sprintf(outname, "%s", "composit.PNG");

  if(pngFileEncodeWrite(&out, outname) == -1){
    printf("pngFileEncodeWrite error\n");
    freeBitmapData(&out);
    freeBitmapData(&in1);
    freeBitmapData(&in2);
    return -1;
  }

  freeBitmapData(&out);
  freeBitmapData(&in1);
  freeBitmapData(&in2);

  return 0;
}

コンパイルと実行ファイルの生成・実行は下記コマンドで行えます。

gcc myPng.c -c
gcc composit.c -c
gcc composit.o myPng.o -lpng -o composit.exe
./composit.exe addalpha.PNG red.PNG 

入力ファイルはPNG画像2つを想定しています。1つはRGBAでもう一つはRGBでもRGBAでも問題ないはずです。ただし両方の画像の縦横サイズは同じにしてください。addalpha.PNGはアルファチャンネル付きのPNG画像で紹介したPNG画像で、red.PNGは下の真っ赤なRGBAのPNG画像です(全画素のアルファチャンネルは全て255で不透明)。

スポンサーリンク

スポンサーリンク

実行結果

addalpha.PNGとred.PNGを入力した場合、下のようなcomposit.PNGが出力されるはずです。

addalpha.PNGでアルファチャンネルが半透明の画素が透け、red.PNGの赤色が見えていることが分かると思います。またアルファチャンネルが不透明の画素はそのまま残っています。これはアルファチャンネルを考慮しながら2つの画像を合成した結果です。

例えば2つ目の画像に右に行くほど不透明度が高くなるRGBA画像を入力した場合は、

合成語の結果は下のようになります。

背景が黒のプレビューソフトで開くと下のように見えます。まだ左側が透明であることが分かりますね。

スポンサーリンク

ソースコードの説明

2つの画像を合成している部分は下記です。やたらややこしいですが、アルファチャンネルを除去するプログラムで紹介したWikipediaのページ載っている計算式をそのままC言語の実装に落とし込んだだけになります。

アルファチャンネル考慮した画素値の合成
      a1 = (double)in1->data[in1->ch * (i + j * in1->width) + ALPHA_CH] / 256;
      a2 = (double)in2->data[in2->ch * (i + j * in2->width) + ALPHA_CH] / 256;
      for(c = 0; c < out->ch; c++) {
        if(a1 + a2 * (1 - a1) == 0 ) {
          tmpRgb = 0;
        } else {
          tmpRgb =
            (unsigned int)(
              (double)in1->data[in1->ch * (i + j * in1->width) + c] * a1
               + (double)in2->data[in2->ch * (i + j * in2->width) + c] * a2
               * (1 - a1)
             ) / (a1 + a2 * (1- a1));
        }
        if(tmpRgb >= 256) {
          tmpRgb = 255;
        }
        out->data[out->ch * (i + j * out->width) + c] = (unsigned char)tmpRgb;
      }
      tmpAlpha = (unsigned int)((a1 + a2 * (1 - a1)) * 256);
      if(tmpAlpha >= 256){
        out->data[out->ch * (i + j * out->width) + ALPHA_CH] = 255;
      } else {
        out->data[out->ch * (i + j * out->width) + ALPHA_CH] = (unsigned char)tmpAlpha;
      }

このソースコードでは2つの画像を合成していますが、1つの画像を真っ白かつアルファチャンネルが全て不透明(255)であると決めつけて処理を行うのがアルファチャンネルを除去するプログラムで紹介したプログラムになります。

まとめ

このページではアルファチャンネルについて解説およびC言語でアルファチャンネルの付いた画像を加工する方法について解説しました。アルファチャンネルを用いることで透明度を持たせた画像を作成することができますので、今後画像を扱うのであれば、アルファチャンネルについてはしっかり覚えておくと良いと思います。

MEMO

この記事で作成したプログラムは下記環境で動作を確認しています

macOS Mojave バージョン10.14

同じカテゴリのページ一覧を表示