PNG 파일은 왜 89 50 4E 47로 시작할까: 시그니처와 청크 구조

PNG 파일이 늘 89 50 4E 47 0D 0A 1A 0A로 시작하는 이유, 길이·타입·데이터·CRC로 된 청크 구조, 대소문자로 속성을 나타내는 청크 이름 규칙과 필수 청크 순서를 W3C PNG 3판과 RFC 2083으로 정리했습니다.

개발파일 형식

PNG 파일을 16진수로 열어 보면 어떤 이미지든 처음 8바이트가 같습니다.

$ xxd red1x1.png | head -2
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753  ..............wS

앞의 89 50 4E 47 0D 0A 1A 0A가 PNG 시그니처이고, 그 뒤에는 청크(chunk)라는 같은 모양의 블록이 이어집니다.

시그니처 8바이트

현재 규격인 W3C PNG 3판은 이 8바이트가 PNG를 다른 데이터와 구별하고 일부 전송 오류를 일찍 찾게 해 준다고 설명합니다. 각 바이트를 선택한 이유는 PNG 1.0을 담은 RFC 2083에 나옵니다.

바이트 RFC 2083이 밝힌 이유
89 ASCII가 아닌 값이라 텍스트 파일이 PNG로 오인될 가능성을 줄이고, 8번째 비트를 지우는 전송을 잡아냄
50 4E 47 ASCII로 PNG
0D 0A CR-LF. 줄바꿈을 바꾸는 전송을 잡아냄
1A Control-Z. MS-DOS에서 파일 표시를 멈춤
0A LF. CR-LF 변환의 반대 방향 문제를 잡아냄

텍스트 모드 전송으로 CR이나 LF 바이트가 더해지거나 바뀐 파일은 이미지를 풀기 전에 이 8바이트에서 어긋납니다. 시그니처에는 버전 번호가 없습니다. RFC 2083은 이것이 의도된 설계이며, 형식 확장은 청크를 통해 이뤄진다고 설명합니다.

청크의 구조

필드 크기 내용
Length 4바이트 데이터 필드의 바이트 수만 셈. 최대 2³¹−1
Chunk Type 4바이트 IHDR, IDAT 같은 영문자 4개
Chunk Data Length 바이트 0바이트도 가능
CRC 4바이트 타입과 데이터로 계산. Length는 제외

여러 바이트로 된 정수는 큰 자리 바이트가 먼저 오는 네트워크 바이트 순서입니다. 위 덤프에서 00 00 00 0D는 길이 13, 49 48 44 52는 IHDR입니다. 이어지는 13바이트에는 가로·세로, 비트 깊이, 색 형식 같은 기본 정보가 들어 있습니다.

길이 상한이 2³¹−1인 것은 부호 없는 4바이트 정수를 다루기 어려운 언어를 배려한 제한입니다.

이름의 대소문자

청크 타입 네 글자는 각 바이트의 5번 비트, 즉 대문자냐 소문자냐로 속성을 나타냅니다.

  • 첫 글자: 대문자면 필수(critical), 소문자면 보조(ancillary)
  • 둘째 글자: 대문자면 공개(public, W3C가 정의하도록 예약됨), 소문자면 비공개(private)
  • 셋째 글자: 예약 비트. 현재 규격에서는 반드시 대문자
  • 넷째 글자: 소문자면 복사 가능(safe-to-copy)

예를 들어 pHYs는 보조·공개·복사 가능 청크입니다. 현재 규격에서 이미지를 추출하는 디코더가 모르는 필수 청크를 만나면 치명적인 상황입니다. 모르는 보조 청크는 오류가 아니며 무시할 수 있습니다.

필수 청크의 순서

PNG의 공통 필수 뼈대는 시그니처 다음 IHDR, 하나 이상의 IDAT, IEND입니다. IHDR은 맨 앞, IEND는 맨 끝에 하나씩만 옵니다.

다만 인덱스 컬러 이미지, 즉 색 형식 3에는 PLTE도 반드시 있어야 하며 첫 IDAT보다 앞에 와야 합니다. IDAT가 여러 개면 다른 청크를 사이에 두지 않고 연달아 배치합니다. 모든 IDAT의 데이터를 이어 붙인 것이 하나의 zlib 압축 데이터입니다.

CRC 확인해 보기

import struct, sys, zlib

data = open(sys.argv[1], "rb").read()
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"

pos = 8
while pos < len(data):
    length, ctype = struct.unpack(">I4s", data[pos:pos + 8])
    body = data[pos + 8:pos + 8 + length]
    crc, = struct.unpack(">I", data[pos + 8 + length:pos + 12 + length])
    ok = zlib.crc32(ctype + body) == crc
    print(ctype.decode("ascii"), length, "CRC ok" if ok else "CRC mismatch")
    pos += 12 + length

1×1 빨간 점 PNG로 실행한 결과입니다. 환경은 Python 3.14.7, macOS 15.6.1입니다.

IHDR 13 CRC ok
IDAT 12 CRC ok
IEND 0 CRC ok

PNG의 CRC는 gzip과 같은 다항식을 씁니다. 이 실행에서는 세 청크 모두 zlib.crc32 값이 파일에 적힌 CRC와 일치했습니다. 복사본에서 IDAT 데이터의 첫 바이트인 오프셋 41을 바꾸자 그 청크만 IDAT 12 CRC mismatch가 됐습니다. 규격은 CRC 불일치를 손상된 데이터의 신호로 보고, 치명적 오류로 다뤄도 된다고 합니다.

이 코드는 완전하고 올바른 입력을 가정한 확인용입니다. 길이 범위, 청크 순서, IEND 존재 여부, IEND 뒤에 남은 데이터는 검사하지 않습니다. 잘렸거나 형식이 틀린 파일을 넣으면 예외가 날 수 있습니다.

블로그로