カードを回転して拡大する

C 言語Beginner
オンラインで実践に進む

はじめに

印刷工房は、同じカードを別の向きにし、実際のピクセル数も増やしたいと考えています。1 つのピクセルがどこに移るかを追い、時計回りの回転を実装してから、色を混ぜずに整数倍の拡大を加えます。

このラボは独立して始まり、独自の図案とソースファイルが用意されています。前のラボで学んだ格子、パレット、P3、端末操作の知識が必要です。小さなコマンドライン読み取りコードがオプションを解析し、座標関数は自分で実装します。提供された図案と読み取りコードは変更しません。練習時間は作者の見積もりです。

長方形のカードを時計回りに回転する

このステップでは、色を変えずに 5 列 3 行の画像を 3 列 5 行に回転します。

Workspace を開きます。このラボには新しく準備された専用の /home/labex/project/postcards があり、前のラボのファイルは使いません。base.c は元の向きを出力する完成済みのサンプルです。最初に、この正常なサンプルをコンパイルして実行します。

cd /home/labex/project/postcards
gcc -std=c11 -Wall -Wextra base.c -o base
./base > original.ppm

Postcard Preview に切り替え、Refresh files をクリックし、original.ppm を選んで Open postcard をクリックします。5 × 3 の状態表示と各行の色が、最初の向きを示します。Workspace に戻って新しい枠組みを読みます。

cat art.h
cat card.c

art.h、base.c、cli.h はそのままにし、card.c を編集します。提供された main は 2 つの引数を受け取ります。ROTATE は厳密に 0(元の向き)または 1(時計回り)で、SCALE は 1 から 4 の整数です。cli.h の補助コードはこれらを整数変数 rotation と scale に保存します。parse_options は対応する引数のときだけ真を返し、それ以外では main が画像を書く前にステータス 2 で終了します。argc と argv の宣言は提供済みの C のエントリーポイントの一部です。ここでポインターを使う引数解析を実装する必要はありません。

時計回りの回転では幅と高さが入れ替わります。元画像の左上のピクセル (0,0) を追うと、回転後の右上 (HEIGHT - 1,0) に移ります。出力ループは逆の質問をします。出力先 (x,y) に対し、元のどのピクセルを読むのでしょうか。拡大しない場合、答えは source_x = y と source_y = HEIGHT - 1 - x です。HEIGHT - 1 から出力先の列を引くことで、元の行を逆順に読みます。ここで WIDTH - 1 を使うと、長方形では誤りになります。

各座標関数は整数の引数 x と y を受け取り、整数の添字を 1 つ返します。rotation が 0 のときは、そのままの座標を返します。提供された rotation ? HEIGHT : WIDTH は、「rotation が 0 でなければ HEIGHT、そうでなければ WIDTH を選ぶ」という意味です。

card.c を次の完成した最初の版で置き換えます。最初のラボと同じく、引用符付きの区切りを使うヒアドキュメントは、最後の EOF 行まで C ソースをそのままコピーします。この版は倍率 1 に対応し、次のステップで倍率 2 から 4 を追加します。

cat > card.c <<'EOF'
#include <stdio.h>
#include "art.h"
#include "cli.h"

int source_x(int x, int y) {
    if (rotation) return y;
    return x;
}
int source_y(int x, int y) {
    if (rotation) return HEIGHT - 1 - x;
    return y;
}
int main(int argc, char **argv) {
    if (!parse_options(argc, argv)) return 2;
    int out_width = (rotation ? HEIGHT : WIDTH) * scale;
    int out_height = (rotation ? WIDTH : HEIGHT) * scale;
    printf("P3\n%d %d\n255\n", out_width, out_height);
    for (int y = 0; y < out_height; y++) {
        for (int x = 0; x < out_width; x++) {
            int c = pattern[source_y(x, y)][source_x(x, y)] - '0';
            printf("%d %d %d\n", palette[c][0], palette[c][1], palette[c][2]);
        }
    }
    return 0;
}
EOF

コンパイルします。元の向きと時計回りの回転を倍率 1 で出力し、それぞれ別のファイル名を使います。

gcc -std=c11 -Wall -Wextra card.c -o card
./card 0 1 > original.ppm
./card 1 1 > rotated.ppm

回転したファイルを調べます。

python3 /home/labex/.local/share/pixel-postcards/inspect.py rotated.ppm
3 x 5 pixels; 15 RGB triples; max value 255

Postcard Preview に切り替え、Refresh files をクリックし、rotated.ppm を選んで Open postcard をクリックします。以前は左にあった暗い帯が、上の行になるはずです。寸法だけでは回転の正しさを証明できないため、色の付いたピクセルも比べてください。鏡に映したような結果になったら、HEIGHT - 1 - x による反転を見直し、コンパイルしてファイルを作り直します。

各ピクセルを拡大して無効なオプションを拒否する

このステップでは、各元ピクセルを同じ大きさのブロックにコピーして、回転後のカードを拡大します。

Workspace から始めます。倍率 2 では、出力先の列 0 と 1 はどちらも元の列 0 を読み、列 2 と 3 は元の列 1 を読みます。整数除算でちょうどこのグループ化ができます。0 / 2 と 1 / 2 はどちらも 0 です。最初に出力先の座標を scale で割ると、拡大を逆算できます。その後、同じ逆向きの回転式で元の位置を選びます。これにより、時計回りに回転して正の整数倍に拡大した画像ができ、色の混合や補間は行いません。

回転する場合、source_x は y / scale、source_y は HEIGHT - 1 - x / scale を返します。元の向きの場合は x / scale と y / scale を返します。提供されたメインのループでは、すでに出力寸法に scale を掛けています。ファイルを完成版に置き換えます。

cat > card.c <<'EOF'
#include <stdio.h>
#include "art.h"
#include "cli.h"

int source_x(int x, int y) {
    if (rotation) return y / scale;
    return x / scale;
}
int source_y(int x, int y) {
    if (rotation) return HEIGHT - 1 - x / scale;
    return y / scale;
}
int main(int argc, char **argv) {
    if (!parse_options(argc, argv)) return 2;
    int out_width = (rotation ? HEIGHT : WIDTH) * scale;
    int out_height = (rotation ? WIDTH : HEIGHT) * scale;
    printf("P3\n%d %d\n255\n", out_width, out_height);
    for (int y = 0; y < out_height; y++) {
        for (int x = 0; x < out_width; x++) {
            int c = pattern[source_y(x, y)][source_x(x, y)] - '0';
            printf("%d %d %d\n", palette[c][0], palette[c][1], palette[c][2]);
        }
    }
    return 0;
}
EOF

コンパイルし、この実行ファイルで元の向きと回転後の出力を更新してから、回転後に 2 倍拡大した画像を作ります。

gcc -std=c11 -Wall -Wextra card.c -o card
./card 0 1 > original.ppm
./card 1 1 > rotated.ppm
./card 1 2 > enlarged.ppm
python3 /home/labex/.local/share/pixel-postcards/inspect.py enlarged.ppm
6 x 10 pixels; 60 RGB triples; max value 255

元の 15 ピクセルがそれぞれ 2 行 2 列のブロックになり、合計 60 ピクセルになります。Postcard Preview に切り替えてファイル一覧を更新し、enlarged.ppm を選び、Open postcard をクリックします。向きと縦横比は rotated.ppm と同じです。プレビューは両方を画面に合わせて表示するため、実際のファイルの寸法は寸法ラベルで区別します。Download PPM で拡大したファイルをダウンロードしてください。

スクリーンショットは時計回りの向きで、保存されたピクセル数は 6 × 10 です。暗い帯は一番上の行になり、回転したファイルと同じ縦横比を保っています。

Postcard Preview に表示した時計回りに拡大したカード

Workspace に戻ります。倍率 0 では有用な画像はできません。次のコマンドは意図的に失敗させるものです。> は実行ファイルがオプションを拒否する前にファイルを開いて内容を切り詰めるため、新しい名前を使います。直後の echo $? は前のコマンドの終了ステータスを表示します。0 は成功、2 はこのプログラムの引数エラーです。

./card 1 0 > rejected.ppm
echo $?

Scale must be an integer from 1 to 4 に続いて 2 と表示されるはずです。学んだデコーダーで拒否されたファイルを調べます。

python3 /home/labex/.local/share/pixel-postcards/inspect.py rejected.ppm

このコマンドも意図どおり失敗し、Cannot read postcard: Expected a P3 header と表示されます。デコードするピクセルがありません。ファイルが存在しない場合とは異なり、C プログラムが動く前にシェルが空のファイルを作っています。Postcard Preview でファイル一覧を更新して rejected.ppm を開くと、前の画像ではなくエラーが表示されます。もう一度 enlarged.ppm を開き、正常な表示に戻してください。正常な 3 つの出力ファイルはそのまま残ります。

card.c、実行ファイル card、提供された art.h と cli.h、original.ppm、rotated.ppm、enlarged.ppm を残します。生成プログラムは、最大 16 列 16 行の提供済み図案で、両方の回転値と 1 から 4 のすべての倍率に対応する必要があります。この画像の寸法や色を固定値として埋め込んではいけません。同等のループや座標式を使って構いません。無効な引数ではエラーメッセージを出して失敗し、画像ヘッダーを書いてはいけません。提供された解析コードはすでにこの動作を実装しています。

まとめ

時計回りに回転するピクセルの移動を追い、整数除算で拡大を逆算し、その 2 つの対応を C の 2 つの関数に組み合わせました。元の向き、回転後、拡大後の画像を生成し、引数の拒否とファイルの破損・欠落を区別しました。