소개
인쇄 작업실은 같은 엽서를 다른 방향으로 돌리고 실제 픽셀 크기도 늘리려고 합니다. 픽셀 하나가 어디로 이동하는지 살펴보고 시계 방향 회전을 구현한 다음, 색을 섞지 않는 정수 배율 확대를 추가합니다.
이 실습은 자체 도안과 소스 파일로 독립적으로 시작합니다. 이전 실습에서 배운 격자, 팔레트, 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은 인수 두 개를 받습니다. 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이 0이면 변경하지 않은 좌표를 반환합니다. 제공된 rotation ? HEIGHT : WIDTH는 “rotation이 0이 아니면 HEIGHT를, 그렇지 않으면 WIDTH를 선택한다”는 뜻입니다.
card.c를 다음 완전한 첫 버전으로 바꿉니다. 첫 실습처럼 구분자를 따옴표로 감싼 here-document가 마지막 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 픽셀인 시계 방향 이미지를 보여 줍니다. 어두운 띠가 맨 위 행으로 이동했으며 회전한 파일과 같은 비율을 유지합니다.
![]()
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을 다시 열어 정상 보기를 복구하세요. 정상 출력 파일 세 개는 그대로 남습니다.
card.c, 실행 파일 card, 제공된 art.h와 cli.h, original.ppm, rotated.ppm, enlarged.ppm을 남겨 둡니다. 생성기는 최대 16열 16행의 제공된 도안에서 두 회전 값과 1부터 4까지의 모든 배율을 처리해야 합니다. 이 이미지의 크기나 색을 고정해서 작성하면 안 됩니다. 같은 결과를 내는 반복문과 좌표식을 사용해도 됩니다. 잘못된 인수에서는 오류 메시지와 함께 실패하고 이미지 헤더를 쓰지 않아야 합니다. 제공된 파서는 이미 이 동작을 지원합니다.
요약
시계 방향 회전에서 픽셀의 이동을 따라가고 정수 나눗셈으로 확대를 되짚은 뒤 두 좌표 대응을 C 함수 두 개에 결합했습니다. 원래 방향, 회전, 확대 이미지를 생성하고 인수 거부와 손상되거나 없는 파일을 구분했습니다.


