旋转并放大明信片

C 语言Beginner
立即练习

介绍

印刷工作室希望同一张明信片换个方向,并增加实际像素尺寸。你将先跟踪一个像素的移动,再实现顺时针旋转,最后增加整数倍放大,且不混合颜色。

本实验独立提供自己的图案和源文件。你需要具备上一个实验教授的网格、调色板、P3 和终端操作知识。提供的小型命令行读取器负责解析选项,由你实现坐标函数。保持提供的图案和选项读取器不变。练习时长为作者估计。

顺时针旋转矩形明信片

在这一步中,你将把五乘三的图像转成三乘五,同时保持颜色不变。

打开 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 接受两个参数: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,并返回一个整数索引。当 rotation 为零时,函数返回未改变的坐标。提供的 rotation ? HEIGHT : WIDTH 表达式表示:“如果 rotation 非零就选 HEIGHT,否则选 WIDTH。”

用下面的完整第一版替换 card.c。与第一个实验相同,带引号的 here-document 会将 C 源码按原样复制到最后的 EOF 行。这一版处理放大倍数 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

重新编译,用这个可执行文件更新原方向、旋转后的输出,再生成旋转后放大两倍的图像:

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。倍数为零无法产生有用图像。下一条命令会有意失败。使用新文件名,因为 > 会在可执行文件拒绝参数之前打开并截断目标文件。紧接着执行的 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。文件中没有可解码的像素。这不同于文件不存在:shell 在 C 程序运行前就创建了空文件。在 Postcard Preview 中刷新文件列表并打开 rejected.ppm,应看到错误,而不是上一张图片。再次打开 enlarged.ppm,恢复正常视图。三个正常输出文件仍然完整。

保留 card.c、可执行文件 card、提供的 art.h 和 cli.h,以及 original.ppm、rotated.ppm 和 enlarged.ppm。生成器必须支持两个旋转值和 1 到 4 的全部倍数,并适用于不超过 16 列、16 行的提供图案,不能把当前图像的尺寸或颜色写死。等效循环和坐标表达式都可以。无效参数必须报错退出,且不写图像头;提供的解析器已经实现这种行为。

总结

你跟踪了像素顺时针旋转的过程,用整数除法撤销放大,并把两种映射组合进两个 C 函数。你生成了原方向、旋转和放大后的图片,还区分了参数拒绝、格式损坏与文件缺失。