콘텐츠로 이동

압축과 서브셋팅으로 PDF 파일 크기 줄이기

목표는 충실도 손실 없이 콘텐츠가 허용하는 가장 작은 PDF를 만드는 것입니다. NextPDF는 두 가지 크기 레버를 제공하며, 둘 다 기본값으로 켜져 있습니다:

  • 스트림 압축. 라이터는 모든 페이지 콘텐츠 스트림과 임베드된 모든 글꼴 프로그램을 FlateDecode(zlib) 스트림으로 감쌉니다. NextPDF\Core\Configcompress 플래그가 이 설정을 담고 있습니다. 스트리밍 문서를 빌드할 때는 withCompress() wither로 이 값을 다시 지정합니다.
  • 글꼴 서브셋팅. TrueType 또는 CFF 글꼴을 임베드하면, 라이터는 문서가 실제로 사용하는 글리프만 담도록 글꼴 프로그램을 다시 빌드한 다음 그 결과를 FlateDecode로 압축합니다. 이 작업은 자동으로 이루어집니다 — 설정할 플래그도, 호출할 메서드도 없습니다. 문서에서 실제로 쓰는 글리프가 수백 개뿐인 20,000개 글리프 규모의 CJK 글꼴은 디스크상의 전체 크기 중 일부만 차지한 채 임베드됩니다.

먼저 분명히 짚고 넘어가면: NextPDF Core는 이미지 리샘플링, 이미지 품질 조절, 객체 스트림 토글, 리소스 중복 제거 설정을 제공하지 않습니다. 존재하는 크기 제어 수단은 위의 두 가지입니다. 이 레시피의 나머지 부분에서는 이 둘을 올바르게 사용하는 방법과 각각이 하지 않는 일을 보여 줍니다.

사전 요구 사항: Core 설치(composer require nextpdf/core:^3) 및 서브셋팅 경로를 위해 임베드 라이선스를 보유한 글꼴 파일.

Terminal window
composer require nextpdf/core:^3

PDF는 객체의 트리입니다. 가장 큰 객체는 보통 콘텐츠 스트림(각 페이지의 그리기 연산자)과 글꼴 프로그램(임베드된 글리프 윤곽선)입니다. 둘 다 압축률이 높은 텍스트와 바이너리 데이터이므로, 가장 효과적인 단일 크기 레버는 이들을 FlateDecode로 압축하는 것입니다. FlateDecode는 zlib로 감싼 DEFLATE 스트림에 대한 PDF 2.0 명칭이며(ISO 32000-2:2020 §7.4.4), NextPDF가 출력하는 필터입니다.

라이터는 NextPDF\Writer\PinnedZlibCompressor를 통해 DEFLATE 압축 레벨을 RFC 1951 최댓값인 9로 고정합니다. 레벨 9는 약간의 CPU를 더 들여 가장 작은 스트림을 얻는 절충안입니다. 또한 레벨을 고정하면 출력이 결정적으로 유지됩니다. zlib 헤더가 레벨을 인코딩하므로 레벨이 흔들리면 바이트가 달라지기 때문입니다. 레벨은 직접 선택하지 않습니다 — 엔진이 이를 고정하여 동일한 입력에 대한 두 번의 실행이 바이트 단위로 동일한 스트림을 생성하도록 합니다.

두 번째 레버는 글꼴 서브셋팅입니다. 디스크상의 글꼴 파일은 서체가 정의하는 모든 글리프를 담고 있지만, “Invoice 2026”을 출력하는 문서에는 그중 십여 개만 필요합니다. NextPDF\Typography\FontSubsetter(TrueType용)와 NextPDF\Typography\CffSubsetter(CFF / OpenType용)는 문서가 실제로 렌더링한 코드포인트를 순회하고, 복합 글리프 의존성을 해소하며, 필요한 글꼴 테이블만 다시 빌드합니다. 이들은 결정적인 6 글자 서브셋 접두사 태그가 붙은 유효한 서브셋 글꼴 바이너리를 출력합니다(ISO 32000-2:2020 §9.9). 라이터는 임베드된 글꼴에서 사용된 글리프 집합을 알 수 있을 때마다 이를 적용한 다음, 서브셋을 FlateDecode로 압축합니다. 특정 글꼴을 서브셋팅해도 10 퍼센트 미만만 절약된다면, 다시 빌드하는 오버헤드가 미미한 이득에 비해 크므로 서브셋터는 대신 원본 프로그램을 반환합니다.

요점은 이렇습니다: PDF를 작게 유지하는 방법은 긴 옵션 목록을 조정하는 것이 아니라, 압축을 켜진 상태(기본값)로 두고 실제 글꼴 파일을 임베드하는 것(그래야 서브셋팅이 줄일 대상을 갖게 됨)입니다.

직접 설정하는 유일한 크기 조절 수단은 구성 객체에 있습니다.

NextPDF\Core\Config는 타입이 지정된 wither 메서드를 갖춘 불변 final readonly 값 객체입니다. 관련 멤버는 다음과 같습니다:

  • compress (bool, 기본값 true) — FlateDecode 압축을 활성화합니다. withCompress(bool $compress): self를 호출하면 해당 플래그만 변경되고 다른 모든 필드는 보존된 새 Config를 반환합니다.

문서 생성 시점에 Config를 문서에 전달합니다:

  • NextPDF\Core\Document::createStandalone(?Config $config = null): self는 CLI 스크립트나 단기 프로세스를 위해 임시 레지스트리로 문서를 빌드하며, 사용자의 Config를 적용합니다.

다음 두 멤버는 크기 레버가 다룰 대상을 형성하지만, 둘 다 그 자체로는 압축 제어 수단이 아닙니다:

  • imageCacheBytes (int, 기본값 52_428_800)는 메모리 내 이미지 캐시의 상한을 정하며, withImageCacheBytes(int $bytes): self가 이를 변경합니다. 이는 빌드 중 최대 메모리를 제한합니다. 임베드하는 이미지를 리샘플링하거나 다시 압축하거나 그 밖의 방법으로 줄이지 않습니다 — 출력 크기 제어 수단이 아니라 메모리 상한입니다.
  • fontsDirectory (string)와 withFontsDirectory(string $dir): self는 글꼴 파일의 기본 검색 경로를 설정하며, 이 경로는 서브셋팅 경로에 입력됩니다.

글꼴 작업은 문서의 타이포그래피 API를 통해 이루어집니다:

  • setFont(string $family, string $style = '', float $size = 12.0): static는 글꼴을 선택합니다. 패밀리가 임베드 가능한 글꼴 파일로 해석되면, 라이터는 저장 시점에 해당 글꼴을 서브셋할 수 있도록 렌더링한 코드포인트를 기록합니다.
  • addFontDirectory(string $directory): static는 글꼴 파일을 검색할 추가 디렉터리를 등록합니다.

출력은 표준 3종 세트입니다: getPdfData(): string은 바이트를 반환하고, save(string $path): void는 이를 원자적으로 기록하며, output(?string $filename, OutputDestination $dest): string는 HTTP 전달을 처리합니다.

서브셋팅에는 공개 메서드도 플래그도 없습니다. 이는 글꼴을 임베드하고 텍스트를 렌더링하는 과정에서 자연스럽게 따라오는 속성입니다 — 라이터가 FontSubsetter / CffSubsetterNextPDF\Writer\PdfFontWriter 내부에서 대신 구동합니다.

이 예제는 압축을 명시적으로 활성화하고 임베드 및 서브셋팅된 글꼴을 갖춘 문서를 빌드한 다음 바이트를 기록합니다. 호출 형태를 보여 주기 위해 오류 처리는 생략했으며, 아래의 프로덕션 예제가 완전한 가드를 추가합니다.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
// compress defaults to true; setting it explicitly documents intent.
$config = (new Config())->withCompress(true);
$doc = Document::createStandalone($config);
$doc->addFontDirectory(__DIR__ . '/fonts');
$doc->addPage();
// Selecting an embeddable face records the glyphs used, so the writer
// subsets this font automatically when the document is built.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026 - subsetted, compressed output.', newLine: true);
$pdf = $doc->getPdfData();
file_put_contents(__DIR__ . '/small.pdf', $pdf);
printf("Wrote %d bytes.\n", strlen($pdf));

이는 자체 완결형 프로그램입니다. 이 프로그램은 압축을 켠 채로 문서를 빌드하고, 사용자가 제어하는 디렉터리에서 글꼴을 임베드하며, 서브셋터가 사용할 글리프 집합을 확보하도록 텍스트를 렌더링한 다음, 결과를 원자적으로 기록합니다. 이 프로그램은 빌드와 저장 경로에서 발생하는 가장 구체적인 NextPDF 예외를 잡은 다음, 각 예외를 삼키지 않고 컨텍스트와 함께 다시 던집니다. NEXTPDF_FONT_DIR를 임베드 라이선스를 보유한 TrueType 또는 CFF 글꼴이 들어 있는 디렉터리로 지정하세요. 프로그램은 임베드하기 전에 경로를 검증합니다.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;
use NextPDF\Core\Document;
use NextPDF\Exception\CompressionException;
use NextPDF\Exception\InvalidConfigException;
/**
* Resolve and validate the font directory from a server-controlled source.
*
* Reading the directory from the environment keeps the path off the request
* surface. The function rejects a missing or unreadable directory so the
* embedding path never runs against untrusted or absent input.
*/
function resolveFontDirectory(): string
{
$configured = getenv('NEXTPDF_FONT_DIR');
$dir = $configured !== false && $configured !== '' ? $configured : __DIR__ . '/fonts';
$real = realpath($dir);
if ($real === false || !is_dir($real) || !is_readable($real)) {
throw new RuntimeException(sprintf('Font directory "%s" is not a readable directory.', $dir));
}
return $real;
}
/**
* Build a compressed, font-subsetted document and return its bytes.
*
* @param non-empty-string $fontDirectory Validated directory of embeddable fonts.
*
* @return string Raw PDF bytes.
*/
function buildCompactPdf(string $fontDirectory): string
{
// compress is true by default; pin it so the intent is explicit and the
// streaming writer path honours it regardless of any wrapper defaults.
$config = (new Config())
->withCompress(true)
->withFontsDirectory($fontDirectory)
// Bound the image cache so a build cannot exhaust memory. This is a
// memory ceiling, not an output-size control.
->withImageCacheBytes(16 * 1024 * 1024);
$doc = Document::createStandalone($config);
$doc->addFontDirectory($fontDirectory);
$doc->addPage();
// Rendering with an embeddable face records the used codepoints, which the
// writer turns into a font subset at build time.
$doc->setFont('LiberationSans', '', 12);
$doc->cell(0, 10, 'Invoice 2026', newLine: true);
$doc->cell(0, 10, 'Compressed streams plus an automatic font subset.', newLine: true);
// getPdfData() triggers the build: page streams and the subset font program
// are FlateDecode-compressed before the bytes are returned.
return $doc->getPdfData();
}
try {
$fontDirectory = resolveFontDirectory();
$pdf = buildCompactPdf($fontDirectory);
} catch (CompressionException $e) {
// Raised if the zlib encoder hard-fails while compressing a stream.
throw new RuntimeException(
sprintf('Compression failed for a %s stream.', $e->getAlgorithm()),
previous: $e,
);
} catch (InvalidConfigException $e) {
// Raised by the output path for an invalid destination configuration.
throw new RuntimeException(
sprintf('Output configuration "%s" was rejected.', $e->getConfigKey()),
previous: $e,
);
}
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/small.pdf';
if (file_put_contents($path, $pdf) === false) {
throw new RuntimeException(sprintf('Could not write PDF to "%s".', $path));
}
printf("Wrote %d bytes to %s.\n", strlen($pdf), $path);

예상 STDOUT(바이트 수는 글꼴과 빌드에 따라 달라집니다):

Wrote <n> bytes to <path>.
  • 압축은 기본값으로 켜져 있습니다.Configcompresstrue로 설정되어 있습니다. withCompress()는 거의 필요하지 않습니다. 의도를 명시적으로 문서화하거나 원시 스트림을 읽고 싶은 디버깅 빌드에서 비활성화할 때만 명시적으로 설정하세요.
  • 압축을 끄면 파일이 작아지는 것이 아니라 커집니다. withCompress(false)는 압축되지 않은 스트림을 검사하기 위한 진단 보조 수단입니다. 이는 결코 크기 최적화 수단이 아닙니다. 압축을 켠 상태로 배포하세요.
  • 서브셋팅에는 실제로 임베드된 글꼴이 필요합니다. Base14 표준 글꼴(Helvetica, Times, Courier 및 그 동류)은 이름으로 참조되며 일반 문서에서는 임베드된 프로그램을 담지 않으므로, 서브셋할 대상이 없습니다. 서브셋팅은 글꼴 파일에서 임베드한 글꼴만 줄입니다.
  • 서브셋팅은 자동이며 별도의 알림은 없습니다. 플래그도, 메서드도, 확인 절차도 없습니다. 글꼴을 임베드하고 그것으로 텍스트를 렌더링했다면, 라이터가 이를 서브셋한 것입니다. 임베드된 프로그램은 6 글자 서브셋 접두사 태그(예: ABCDEF+LiberationSans)를 담고 있어, 리더가 서브셋과 전체 임베드를 구분할 수 있습니다.
  • 절약량이 적으면 전체 글꼴을 그대로 유지합니다. 서브셋으로 줄어드는 크기가 프로그램 크기의 10 퍼센트 미만이라면, 서브셋터는 대신 원본을 반환합니다. 이는 의도적인 하한선입니다: 미미한 이득을 위해 다시 빌드할 가치는 없습니다. 이미 아주 작은 글꼴을 임베드하거나, 그 글리프의 거의 전부를 렌더링하는 경우가 여기에 해당할 수 있습니다.
  • imageCacheBytes는 이미지 크기 조절 수단이 아닙니다. 이는 출력 바이트가 아니라 메모리에 상한을 둡니다. NextPDF Core는 제공한 이미지 데이터를 그대로 임베드하며, 리샘플링, 다운샘플링, 재인코딩 단계가 없습니다. 더 작은 이미지가 필요하다면, 임베드하기 전에 크기를 조정하고 다시 인코딩하세요.
  • 객체 스트림이나 중복 제거 설정은 존재하지 않습니다. NextPDF Core는 PDF 2.0 객체 스트림이나 리소스 중복 제거를 위한 토글을 제공하지 않습니다. 찾으려 하지 마세요 — 크기 레버는 스트림 압축과 글꼴 서브셋팅입니다.

레벨 9 압축은 스트림 기록 시 가장 큰 CPU 비용을 차지합니다. 이는 빌드 시간의 몇 퍼센트를 들여 가장 작은 출력을 얻는 절충안입니다. 비용은 압축 전 바이트 수에 선형적으로 비례하므로, 페이지 수와 임베드된 글꼴 데이터의 양이 예산을 결정합니다. 서브셋팅은 임베드된 글꼴마다 한 번의 패스를 추가하는데, 이 패스는 글꼴의 테이블 디렉터리를 파싱하고, 사용된 글리프의 폐포를 해소하며, 필요한 테이블을 다시 빌드합니다. 대형 CJK 글꼴의 경우 이는 두 레버 중 더 비싼 쪽이지만, 페이지마다가 아니라 글꼴마다 한 번씩만 실행됩니다. 10 퍼센트 절약 하한선이 존재하는 이유 중 하나는, 그 패스가 이득을 내지 못할 때 핫 패스에서 제외하기 위해서입니다. 하나의 임베드된 서브셋을 가진 작은 문서는 1500ms 벽시계 시간과 96MB 최대 예산 안에 여유 있게 들어갑니다. 많은 이미지를 임베드하는 빌드가 스왑에 들어가지 않고 메모리에서 빠르게 실패하도록, imageCacheBytes를 실제 상한에 맞춰 제한하세요.

빌드는 프로세스 내에서 실행됩니다. 어떤 문서 바이트도 호스트를 떠나지 않으며 네트워크 호출도 이루어지지 않습니다. 외부에서 제공된 모든 글꼴이나 이미지는 신뢰할 수 없는 입력으로 취급하세요:

  • 글꼴 디렉터리를 검증하세요. 프로덕션 예제는 서버가 제어하는 환경 변수에서 글꼴 경로를 읽어 들이고, 임베드하기 전에 존재하지 않거나 읽을 수 없는 디렉터리를 거부합니다. 글꼴 경로를 요청 필드에서 도출하지 마세요.
  • 재배포 라이선스를 보유한 글꼴만 임베드하세요. 서브셋도 여전히 임베드된 글꼴 프로그램입니다. 해당 글꼴을 담은 문서를 배포하기 전에, 라이선스가 임베드를 허용하는지 확인하세요.
  • 잘못된 형식의 글꼴은 예외를 던지며, 조용히 손상된 출력으로 이어지지 않습니다. 파싱에 실패하는 글꼴 파일은 NextPDF\Exception\FontParsingException을 던지고, zlib 인코더가 치명적으로 실패하면 NextPDF\Exception\CompressionException을 던집니다. 가장 구체적인 예외를 잡아 그에 맞게 대응하세요. 빌드를 빈 catch로 감싸지 마세요.
  • 사용자 입력을 출력 경로에 끼워 넣지 마세요. 예제는 고정 경로 또는 서버가 제어하는 사이드 채널에 기록하며, save()의 원자적 라이터를 통해 스트림 래퍼와 널 바이트를 거부합니다. 경로 순회를 피하려면 출력 경로를 서버가 제어하는 값에서 도출하세요.
  • 문서에 비밀 정보를 담지 마세요. 클라이언트에 반환하는 생성된 문서에 자격 증명, 토큰, 내부 식별자를 임베드하지 마세요.

이 레시피는 그 자체로 규범적 표준 주장을 하지 않습니다. 이 레시피가 사용하는 메커니즘은 PDF 2.0 사양에 정의되어 있습니다: FlateDecode 스트림 압축(ISO 32000-2:2020 §7.4.4)과 6 글자 서브셋 접두사를 사용하는 글꼴 서브셋 명명(ISO 32000-2:2020 §9.9). NextPDF는 표준 쓰기 경로의 일부로 두 가지를 모두 출력합니다. compress 플래그 외에는 이들을 구성하지 않습니다. 이 페이지가 선언하는 structural 재현성 프로파일은 라이터가 DEFLATE 레벨을 고정하므로 압축된 스트림이 결정적이라는 점을 반영합니다. 다만 결정적 설정을 추가로 구성하지 않는 한, 문서 수준 식별자는 실행마다 여전히 달라질 수 있습니다. 서브셋팅의 기반이 되는 임베딩 메커니즘에 대해서는 아래 링크된 임베드 및 서브셋 레시피를 참고하세요.