스트림과 필터
ISO 32000-2 §7.4 Evidence: Standard-backed
한눈에 보기
섹션 제목: “한눈에 보기”실제 PDF의 바이트 대부분은 스트림 안에 들어 있습니다. 페이지 콘텐츠, 폰트, 이미지, 그리고 상호 참조 스트림 자체가 그렇습니다. 이 바이트 대부분은 원시 상태로 저장되지 않습니다. 먼저 하나 이상의 필터를 거칩니다. 이 페이지에서는 어떤 필터를 마주치게 되는지, 각 필터가 무엇을 위한 것인지, 어디서 문제가 생기는지, 그리고 같은 입력이 항상 같은 바이트를 만들어 내도록 NextPDF가 왜 압축을 고정하는지 다룹니다.
이것이 중요한 이유
섹션 제목: “이것이 중요한 이유”스트림과 그 필터는 하나의 계약입니다. “이 바이트들은 deflate로 압축된 다음 base-85로 인코딩되었으니, 실제 데이터를 얻으려면 그 순서대로 디코딩하라.” /Filter 항목이 바이트의 실제 내용과 일치하지 않거나, /Length가 잘못되었거나, 두 필터가 잘못된 순서로 나열되어 있으면 스트림은 디코딩할 수 없고, 그 안에 있던 객체는 손실됩니다. 리더는 추론으로 맞히려 하지 않습니다. 딕셔너리가 지시하는 대로 따릅니다.
두 번째로, 더 조용한 비용이 있습니다. 라이브러리의 압축기가 비결정적이면(서로 다른 zlib 빌드, 서로 다른 레벨, 서로 다른 내부 블록 경계), 동일한 PDF를 만들어 내야 하는 두 번의 실행이 서로 다른 두 파일을 만들어 냅니다. 이는 바이트 수준의 재현성을 깨뜨립니다. 재현성이 깨지면 이어서 골든 파일 테스트, 서명된 빌드 검증, 그리고 출력을 비교하는 모든 파이프라인이 깨집니다. 필터는 PDF가 올바른지 여부와 PDF가 동일한지 여부를 모두 결정합니다.
- 스트림 객체는 딕셔너리에 바이트 블록을 더한 것으로,
stream…endstream으로 감싸여 있으며,/Length를 가지고 대개/Filter를 가집니다. /Filter항목은 디코드 필터를 지정하거나, 파이프라인으로서 순서대로 적용되는 필터의 배열을 지정합니다.- 필터는 두 계열로 나뉩니다. 압축(FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode)과 ASCII 전송(ASCIIHexDecode, ASCII85Decode), 그리고 암호화를 위한 특수 Crypt 필터입니다.
- 가장 자주 보게 될 것은 FlateDecode, 즉 zlib/deflate입니다. 이것은 콘텐츠, 폰트, 그리고 상호 참조 스트림의 기본값입니다.
- NextPDF는 같은 입력 바이트가 항상 같은 출력 바이트로 압축되도록 Flate 출력의 레벨과 형식을 고정합니다.
NextPDF 의 접근 방식
섹션 제목: “NextPDF 의 접근 방식”NextPDF는 스트림 객체를 단일 버퍼 헬퍼를 통해 내보내고 단일 고정 압축기를 통해 압축합니다. 이는 의도적인 설계입니다.
BinaryBuffer::writeStream()(src/Support/BinaryBuffer.php)은 스트림 콘텐츠를 해당 딕셔너리로 감싸며, 항상 실제 바이트 길이와 같은 /Length를 기록하고 호출자가 제공하는 /Filter 같은 추가 항목을 병합합니다. 선언된 길이는 콘텐츠 문자열 자체에서 가져오므로, 선언된 길이가 기록된 바이트와 어긋날 수 있는 경로는 없습니다.
압축은 PinnedZlibCompressor(src/Writer/PinnedZlibCompressor.php)를 거칩니다. 이 클래스는 한 가지 이유로 존재합니다. 명시적인 레벨이 없는 gzcompress는 zlib 런타임 기본값을 따르는데, 이는 빌드에 따라 역사적으로 달라져 왔습니다. 2 바이트 zlib 헤더는 레벨까지 간접적으로 인코딩하므로, “기본값”은 안정적인 출력이 아닙니다. 이 압축기는 레벨을 RFC 1951 최댓값으로 고정하고 항상 zlib로 감싼 deflate(RFC 1950 헤더 + Adler-32 트레일러)를 내보내며, 이는 정확히 /Filter /FlateDecode가 기대하는 것입니다. zlib의 강제 실패는 비압축 출력으로 조용히 폴백하는 것이 아니라 타입이 지정된 예외가 됩니다. 스트림이 조용히 원시 상태로 내보내지는 일은 결코 없습니다.
상호 참조 스트림 자체가 이 모든 것의 실제 사례입니다. CrossReferenceStream(src/Core/CrossReferenceStream.php)는 이진 테이블을 만들어 압축하고, /Type /XRef, /W 필드 너비 배열, 그리고 /Filter /FlateDecode를 가진 스트림 객체로 내보냅니다. 리더가 모든 객체를 찾을 수 있게 해 주는 인덱스 자체가 필터링된 스트림입니다.
| 필터 | 계열 | 용도 | 어디서 잘못되는가 |
|---|---|---|---|
| FlateDecode | 압축 | zlib/deflate; 콘텐츠, 폰트, xref 스트림의 기본값 | 비결정적인 zlib 빌드는 “동일한” PDF를 바이트 단위로 서로 다르게 만듭니다 |
| LZWDecode | 압축 | 구형 Lempel–Ziv–Welch 압축 | 레거시 형식입니다. Flate로 대체되었으며, 오래된 파일에서 가끔 여전히 보입니다 |
| DCTDecode | 압축 | JPEG로 인코딩된 colour/grayscale 이미지 | 손실 압축 — 이미 DCT로 인코딩된 이미지를 다시 인코딩하면 다시 열화됩니다 |
| JPXDecode | 압축 | JPEG 2000 웨이블릿 이미지 데이터 | 일부 아카이브 프로파일에서는 허용되지 않으며, 폭넓은 지원은 고르지 않습니다 |
| JBIG2Decode | 압축 | 이중값(1 비트) 이미지 압축 | 인라인 이미지에는 사용할 수 없습니다. 손실 모드는 스캔본을 변경할 수 있습니다 |
| RunLengthDecode | 압축 | 바이트 단위 런렝스 | 단일 바이트가 길게 반복되는 데이터에만 도움이 됩니다. 다른 데이터는 늘어날 수 있습니다 |
| ASCIIHexDecode | 전송 | 16 진수 자리로 표현한 이진 데이터 | 크기를 두 배로 늘립니다. 7 비트 안전 채널 전용이며, 크기를 위한 것은 결코 아닙니다 |
| ASCII85Decode | 전송 | base-85 ASCII 로 표현한 이진 데이터 | 약 25% 오버헤드. 압축이 아니라 전송을 위한 편의 수단입니다 |
| Crypt | 보안 | 문서의 보안 핸들러를 적용합니다 | 상호 참조 스트림은 Crypt 필터를 사용해서는 안 됩니다 |
PDF 표준 필터 집합을 계열별로, 각 필터와 연관된 실패 양상과 함께 정리한 표입니다. NextPDF는 콘텐츠, 폰트, 그리고 상호 참조 스트림에 대해 FlateDecode를 기록합니다. ASCII 전송 필터는 7 비트 채널을 위한 것이며, 크기를 줄이기 위한 것은 결코 아닙니다.
증거가 말하는 것
섹션 제목: “증거가 말하는 것”필터 메커니즘은 Spec: ISO 32000-2, §7.4 ISO 32000-2 §7.4 에 의해 정의됩니다. 스트림의 필터는 딕셔너리의 /Filter 항목으로 지정되며, 필터는 연쇄되어 파이프라인을 형성할 수 있어 스트림을 둘 이상의 디코딩 변환을 통해 순차적으로 통과시킵니다. 표준 자체의 예시는 LZW에 이어 ASCII base-85를 적용한 것으로, 그 순서대로 디코딩됩니다. 작성자는 스트림을 압축하거나 7 비트 안전 형식으로 만들기 위해 인코딩합니다. 리더는 원본 데이터를 복원하기 위해 해당 디코드 필터를 호출합니다. Evidence: Standard-backed
표준의 필터 표는 각 필터를 분류합니다. FlateDecode는 zlib/deflate-encoded 데이터를 압축 해제하여 원본 텍스트 또는 이진 데이터를 재현합니다. DCTDecode는 JPEG를 통해 원본을 근사하는 이미지 샘플을 재현합니다. “근사한다”라는 단어는 표준이 이것이 손실 압축임을 알려 주는 것입니다. LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode, 그리고 Crypt 필터도 각각 거기에 정의되어 있으며, JBIG2는 인라인 이미지에서 명시적으로 금지됩니다.
상호 참조 스트림은 형식 자체의 메커니즘을 자신에게 적용합니다. 이는 스트림 객체이며(/Type /XRef,
Spec: ISO 32000-2, §7.5.8 ISO 32000-2 §7.5.8 ), 그 /W 배열은
각 항목 필드의 바이트 너비를 디코딩된 스트림에서 명시합니다. 표준은
해당 스트림이 암호화되지 않아야 하며 Crypt 필터를 사용하지 않아야 한다고 요구합니다.
NextPDF의 CrossReferenceStream은 이를 정확히 따릅니다 — FlateDecode,
명시적 /W, 암호화 없음.
실제 예시
섹션 제목: “실제 예시”Flate로 압축된 페이지 콘텐츠 스트림입니다. 이것은 가장 흔한 형태입니다. /Length와 /Filter를 가진 딕셔너리, 그다음 stream과 endstream 사이의 압축된 바이트입니다.
<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,// fixed zlib-wrapped format. The same $content always yields the// same $compressed bytes, on any supported PHP/zlib build.$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of// $compressed; /Filter names the decode the reader must apply.// N 0 obj// << /Length <strlen($compressed)> /Filter /FlateDecode >>// stream// <$compressed bytes>// endstream// endobj리더는 반대 과정을 수행합니다. /Length 바이트를 읽고, /Filter가 그렇게 지시하므로 FlateDecode를 적용한 다음, 원래의 연산자를 되돌려받습니다. 압축기를 고정하면 그 왕복은 올바를 뿐 아니라 매번 동일하며, 골든 파일과 서명된 빌드 검사는 바로 이 특성에 의존합니다.
흔한 오해
섹션 제목: “흔한 오해”함정은 ASCII 필터를 압축으로 취급하는 것입니다. ASCIIHexDecode와 ASCII85Decode는 스트림을 더 크게 만듭니다 — 각각 대략 두 배와 25% 정도입니다. 이들은 공간을 절약하기 위해서가 아니라, 7 비트 텍스트에만 안전한 채널로 이진 데이터를 옮기기 위해 존재합니다. PDF를 “줄이려고” ASCII85를 선택하면 반대의 결과가 납니다. 같은 부류의 또 다른 오해는 FlateDecode가 이미지에 대해 “공짜로” 무손실이라고 믿는 것입니다. Flate는 무손실이 맞지만, 이미지가 이미 DCT(JPEG)로 인코딩되어 있었다면 이를 다시 감싸거나 손실 필터를 통해 트랜스코딩하는 과정은 Flate가 그 주위에서 무엇을 하든 이미지를 열화시킵니다. 필터 파이프라인은 여러분이 넣은 것을 정확히 보존합니다 — 실수로 넣은 재압축 아티팩트까지 포함해서 말입니다.
한계와 경계
섹션 제목: “한계와 경계”이 페이지는 필터가 어떻게 선언되고 적용되는지를 다루며, 각 필터 내부의 비트 수준 알고리즘은 다루지 않습니다. 결정성 보장은 구체적으로 NextPDF가 작성하는 스트림의 Flate 출력에 관한 것입니다. 이는 PHP 마이너 버전과 표준을 준수하는 zlib 빌드에 걸쳐 유지되지만, 표준은 deflate 인코더가 서로 다른 내부 블록 경계를 선택하는 것을 명시적으로 허용하므로, 진정으로 다른 zlib 구현(예: 표준 zlib 대 zlib-ng) 사이의 바이트 동일 출력은 보장되지 않습니다. 빌드 환경은 그 이유로 고정되어 있습니다.
NextPDF는 자신이 내보내는 데이터에 대해 FlateDecode와 ASCII 전송 필터를 선택합니다. NextPDF는 이미지 트랜스코더가 아닙니다. 임의의 인바운드 JPEG2000 또는 JBIG2 스트림을 다시 패킹한다고 약속하지 않으며, 손실 이미지 트레이드오프는 원본 데이터의 속성이지 작성자가 되돌릴 수 있는 것이 아닙니다.
미니 FAQ
섹션 제목: “미니 FAQ”FlateDecode는 왜 거의 어디에나 쓰입니까? 무손실이고, 범용적이며, 잘 지원되고, 대부분의 PDF 텍스트 및 연산자 콘텐츠에 잘 맞습니다. 콘텐츠 스트림, 임베디드 폰트, 그리고 상호 참조 스트림에 대한 안전한 기본값입니다.
압축을 끌 수 있습니까? /Filter를 생략하고 원시 바이트를 저장할 수 있으며, 리더는 이를 받아들입니다. 파일은 더 커지며, 그 밖에 나아지는 것은 없습니다. 디버깅을 제외하면 그렇게 할 이유는 거의 없습니다.
압축 레벨은 왜 고정합니까? 출력을 재현 가능하게 하기 위해서입니다. 고정되지 않은 레벨(또는 zlib 빌드)은 압축 해제된 콘텐츠를 바꾸지 않으면서 압축된 바이트를 바꿀 수 있습니다 — 올바르지만 동일하지는 않으며, 이는 바이트 수준 검증을 무력화합니다.
관련 문서
섹션 제목: “관련 문서”- PDF란 실제로 무엇인가 — 이 페이지에서 다루는 스트림이 속한 객체 모델을 설명합니다.
- 폰트: 어려운 부분 — 임베디드 폰트 프로그램은 필터링된 스트림이며, 고유한 실패 양상을 가집니다.
- PDF 2.0: 무엇이 바뀌었는가 — 2.0 베이스라인이 스트림과 NextPDF가 기본값으로 삼는 상호 참조 스트림을 어떻게 다루는지 설명합니다.
용어집
섹션 제목: “용어집”- 스트림 객체 —
stream과endstream사이의 바이트 블록에 딕셔너리를 더한 것으로,/Length가 있으며 대개/Filter를 가집니다. - 필터 — 리더가 스트림의 바이트에 적용하는, 이름 있는 디코딩 변환입니다(예:
FlateDecode). - 필터 파이프라인 — 순서대로 적용되는 필터 배열입니다. 배열 순서가 디코드 순서입니다.
- FlateDecode — zlib/deflate 필터입니다. 콘텐츠, 폰트, 상호 참조 스트림의 기본 압축입니다.
- DCTDecode — JPEG 이미지 필터입니다. 손실 압축이므로, 다시 인코딩하면 이미지가 다시 열화됩니다.
- ASCII 전송 필터 — ASCIIHexDecode / ASCII85Decode 입니다. 크기를 늘리는 대가로 데이터를 7 비트 안전 형식으로 만듭니다 — 압축이 아닙니다.
- 결정적 압축 — 동일한 입력에 대해 바이트 단위로 동일한 압축 출력을 생성하는 것으로, 압축기의 레벨과 형식을 고정하여 달성됩니다.