
고객을 위해 문서 목록을 만들었는데, 고객이 이렇게 묻습니다. "다운로드하기 전에 페이지를 볼 수 있나요?" 그렇게 해서 계획에 없던 렌더링 기능을 하나 떠안게 됩니다. 같은 요청은 어디에서나 나타납니다. 받은편지함 아래의 썸네일 스트립, 채팅에 붙여 넣는 미리보기 카드, 슬라이드 덱에서 뽑아 소셜에 게시하는 한 장의 슬라이드 등이 그렇습니다. 어느 경우든 PDF는 인라인으로 표시할 수 없지만, 그 페이지를 담은 그림은 표시할 수 있습니다.
이렇게 전환이 이루어지면 네 가지 결정이 이 작업의 실용성을 좌우합니다. 어떤 페이지, 어떤 형식, 얼마나 선명하게, 그리고 파일이 사용자에게 어떻게 전달되는지입니다. 대부분의 PDF-이미지 변환 예제는 첫 번째 질문에만 답하고, 나머지는 프로덕션에서 직접 알아내도록 남겨 둡니다.
Spire.PDF for JavaScript는 WebAssembly를 통해 브라우저에서 페이지를 렌더링하고 각 페이지를 이미지 스트림으로 돌려줍니다. 이 네 가지 결정은 모두 여러분의 몫이며, 각각은 인수 하나를 바꾸거나 파일 확장자를 바꾸는 것으로 끝납니다. 파일은 가상 파일 시스템(VFS)을 통해 드나들기 때문에 어디에도 업로드되지 않습니다.
이 글에서는 다음 내용을 배웁니다.
- PDF의 모든 페이지를 PNG로 렌더링하고 그 결과를 ZIP으로 다운로드하기
- 단일 페이지 또는 페이지 범위로 변환 제한하기
- 이미지의 용도에 따라 PNG, JPEG, BMP 사이를 전환하기
- DPI를 사용해 선명도와 파일 크기를 맞바꾸기
- 결과를 다운로드, 개별 파일, 또는 React UI에 바로 전달하기
- 글꼴 누락으로 인한 깨진 텍스트나 빈 텍스트 해결하기
사전 준비 사항
이 안내는 Spire.PDF for JavaScript가 설치되고 WASM 모듈이 초기화된 React 프로젝트를 전제로 합니다. 설정 방법은 React 프로젝트에서 Spire.PDF for JavaScript 연동하기를 참고하세요.
다음이 필요합니다.
- VFS에 로드된 PDF
-
window.wasmModule.spirepdf에서 접근 가능한 WASM 모듈 - 여러 페이지를 하나의 다운로드로 묶으려면
jszip패키지(npm install jszip)
모든 페이지를 PNG로 변환하기
전체 문서를 다루는 경우가 가장 자주 쓰게 되는 방식입니다. PDF를 열고, 모든 페이지를 순회하며 각각을 스트림으로 렌더링하고, 각 스트림을 VFS의 폴더에 기록한 다음, 그 폴더를 압축해 브라우저에 다운로드로 전달합니다.
import JSZip from "jszip";
function App() {
const convertToImage = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check if the WASM module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file and fonts into VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'Flowers.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Create an output directory to hold the converted images
let outputDirectoryName = "ImagesFolders/";
window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// Loop through each page and save it as an image
for (let i = 0; i < doc.Pages.Count; i++) {
const outputFileName = outputDirectoryName + "ConvertedImages_" + i + ".png";
let stream = doc.SaveAsImage({ pageIndex: i });
stream.Save(outputFileName);
stream.Dispose();
}
doc.Dispose();
// Read the converted files from VFS and trigger download
const zip = new JSZip();
let items = await window.dotnetRuntime.Module.FS.readdir(outputDirectoryName);
items = items.filter((item) => item !== "." && item !== "..");
for (const item of items) {
const itemPath = `${outputDirectoryName}/${item}`;
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zip.file(item, fileData);
}
// Convert the ZIP to a Blob and trigger the browser download
const zipBlob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'ImagesFolders';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert PDF To Image</h1>
<button onClick={convertToImage}>
Generate
</button>
</div>
);
}
export default App;
PDF의 각 페이지가 SaveAsImage를 통해 PNG 이미지로 내보내지고, 다운로드용 ZIP 파일로 묶입니다

코드가 하는 일:
-
doc.SaveAsImage({ pageIndex: i })는 페이지 하나를 렌더링하고 이미지 스트림을 반환합니다. 페이지 인덱스는 0부터 시작하므로pageIndex: 0이 첫 페이지입니다. -
stream.Save(outputFileName)는 그 스트림을 VFS에 기록합니다. 여기서 전달하는 확장자가 형식을 결정합니다. 자세한 내용은 아래에서 다룹니다. -
stream.Dispose()는 다음 페이지를 렌더링하기 전에 스트림을 해제합니다. 긴 문서에서 이를 생략하면 아무 이득 없이 메모리만 낭비됩니다. -
FS.mkdirTree→FS.readdir→FS.readFile순서가 필요한 이유는 WASM 모듈이 브라우저의 다운로드 폴더가 아니라 자체 파일 시스템에 기록하기 때문입니다. 이 폴더를 사용자가 저장할 수 있는 형태로 바꿔 주는 것이 JSZip입니다. - 렌더링은 읽기 전용입니다. 원본 PDF는 열리고, 읽히고, 해제될 뿐 결코 수정되지 않습니다.
참고: 이 방식은 각 페이지의 그림을 만들어 내며, 이는 미리보기, 썸네일, 공유에 적합합니다. 그 대신 PDF 안에 포함된 원본 이미지 파일, 즉 차트나 사진 자체를 원래 해상도로 얻고 싶다면(그것이 놓여 있는 페이지의 스크린샷이 아니라) 이는 다른 작업입니다. JavaScript(React)에서 PDF의 이미지 추출하기를 참고하세요.
특정 페이지만 변환하기
썸네일 세 장을 만들기 위해 200페이지 문서를 렌더링하는 것은 낭비입니다. 페이지 순회는 doc.Pages.Count에 대한 for 루프일 뿐이므로, 범위를 좁히는 것은 경계값을 바꾸는 문제입니다.
단일 페이지 — 표지나 특정 자료 페이지:
// Render only the first page
const outputFileName = outputDirectoryName + "cover.png";
let stream = doc.SaveAsImage({ pageIndex: 0 });
stream.Save(outputFileName);
stream.Dispose();
연속된 범위 — 3페이지부터 5페이지까지, 0부터 시작하는 인덱스 2부터 4를 사용:
const startPage = 2;
const endPage = 4;
for (let i = startPage; i <= endPage && i < doc.Pages.Count; i++) {
let stream = doc.SaveAsImage({ pageIndex: i });
stream.Save(outputDirectoryName + "page_" + (i + 1) + ".png");
stream.Dispose();
}
직접 고른 집합 — 서로 인접하지 않은 페이지들:
const pagesToRender = [0, 7, 12];
for (const pageIndex of pagesToRender) {
if (pageIndex >= doc.Pages.Count) continue;
let stream = doc.SaveAsImage({ pageIndex });
stream.Save(outputDirectoryName + "page_" + (pageIndex + 1) + ".png");
stream.Dispose();
}
i < doc.Pages.Count 검사는 페이지 목록이 하드코딩된 상수가 아니라 사용자 입력에서 올 때 중요합니다. 범위를 벗어난 인덱스는 출시하고 싶지 않은 런타임 오류입니다.
파일 이름에 있는 (i + 1)에 주목하세요. 0부터 시작하는 인덱스는 API에는 맞지만 사람에게는 맞지 않습니다. 사용자가 보는 페이지 번호로 파일 이름을 지정하면 나중에 생길 지원 문의 한 종류를 피할 수 있습니다.
추천 글: 렌더링된 페이지가 아니라 PDF에서 원본 이미지 개체를 꺼내야 할 때, 즉 포함된 차트나 사진을 원래 해상도로 얻고 싶을 때는 JavaScript(React)에서 PDF의 이미지 추출하기를 참고하세요.
출력 형식 선택하기
형식은 전적으로 stream.Save에 전달하는 확장자로 결정됩니다. 파이프라인의 다른 부분은 아무것도 바뀌지 않습니다.
stream.Save(outputDirectoryName + "page_1.png"); // PNG
stream.Save(outputDirectoryName + "page_1.jpg"); // JPEG
stream.Save(outputDirectoryName + "page_1.bmp"); // BMP
| 형식 | 확장자 | 결과물 | 다음 경우에 사용하세요 |
|---|---|---|---|
| PNG | .png |
무손실. 텍스트 가장자리와 가는 선이 선명하게 유지됩니다. 파일 크기가 더 큽니다. | 문서 페이지, UI 미리보기, 텍스트나 도표가 있는 모든 것 |
| JPEG | .jpg |
손실 압축. 훨씬 작습니다. 투명도를 지원하지 않으며, 작은 텍스트 주변에 압축 아티팩트가 나타납니다. | 사진이나 스캔 페이지, 이메일 첨부, 크기 제약이 있는 전달 |
| BMP | .bmp |
비압축. 매우 큽니다. | 비압축 비트맵을 특별히 요구하는 레거시 파이프라인 |
실용적인 규칙은 이렇습니다. 페이지가 대부분 텍스트라면 PNG를 사용하세요. JPEG의 압축은 사진 콘텐츠에 맞춰져 있어, 텍스트가 많은 페이지에서는 글리프 가장자리 주변에 눈에 띄는 번짐이 생기며, 어떤 품질 설정으로도 이를 완전히 없앨 수 없습니다. 페이지가 스캔한 사진이라면 JPEG는 눈에 띄는 손실 없이 파일 크기를 크게 줄여 줍니다.
해상도(DPI) 설정하기
SaveAsImage는 기본적으로 96 DPI로 렌더링하며, 이는 일반적인 화면과 일치합니다. 썸네일과 화면 미리보기에는 이 설정이 적절합니다. 하지만 누군가 확대하거나, 페이지를 인쇄하거나, 이미지를 OCR에 넣을 때는 충분하지 않습니다.
dpiX와 dpiY를 전달해 변경하세요:
// Export each page as an image at 150 DPI
for (let i = 0; i < doc.Pages.Count; i++) {
let stream = doc.SaveAsImage({ pageIndex: i, dpiX: 150, dpiY: 150 });
stream.Save(outputDirectoryName + "highres_" + i + ".png");
stream.Dispose();
}
해상도는 공짜 업그레이드가 아닙니다. 픽셀 수는 DPI의 제곱에 비례해 늘어나므로 비용도 빠르게 치솟습니다:
| DPI | 96 DPI 대비 상대적 픽셀 수 | 비교 대상 | 적합한 용도 |
|---|---|---|---|
| 96 (기본값) | 1× | 일반 화면 | 썸네일, 인라인 미리보기, 갤러리 스트립 |
| 150 | ~2.4× | 레티나 디스플레이, 가벼운 확대 | 이메일 첨부, 사용자가 확대할 수 있는 미리보기, 초안 인쇄 |
| 300 | ~9.8× | 인쇄 품질 | 인쇄 출력, OCR 입력, 보관용 원본 |
이 표에서 두 가지가 따라옵니다. 첫째, 96에서 300으로 올리면 작업량과 저장 공간이 대략 열 배로 늘어나므로, 실제로 인쇄할 사람이 없다면 200페이지 문서에 300을 선택하지 마세요. 둘째, dpiX와 dpiY가 별도 인수인 데에는 이유가 있습니다. 보통은 같은 값으로 설정하며, 다르게 설정하면 페이지가 늘어나게 됩니다.
이미지 패키징 또는 미리보기
페이지를 렌더링하는 것이 절반이고, 나머지 절반은 그것을 사용자에게 전달하는 것입니다. 세 가지 패턴이면 거의 모든 경우를 다룰 수 있습니다.
모두 하나의 ZIP으로 묶기 — 첫 번째 예제의 방식입니다. 사용자가 파일 폴더를 원할 때 가장 좋으며, 수십 페이지로 확장해도 다운로드 프롬프트로 브라우저를 괴롭히지 않는 유일한 방법입니다.
단일 이미지를 바로 다운로드하기 — 파일이 하나뿐이라면 JSZip이 필요 없습니다:
const outputFileName = 'cover.png';
let stream = doc.SaveAsImage({ pageIndex: 0 });
stream.Save(outputFileName);
stream.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'image/png' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
이미지를 React UI 안에 유지하기 — 다운로드가 전혀 없습니다. 이미지가 임시적이고 page_1.png 파일로 가득 찬 다운로드 폴더가 오히려 어수선하기만 할 미리보기 창과 썸네일 스트립에 적합한 선택입니다:
const renderPreviewStrip = async () => {
const pdfModule = window.wasmModule?.spirepdf;
if (!pdfModule) return;
await window.spire.FetchFileToVFS('Flowers.pdf', "", `${process.env.PUBLIC_URL}/data/`);
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile('Flowers.pdf');
const previewUrls = [];
for (let i = 0; i < doc.Pages.Count; i++) {
const tempName = `preview_${i}.png`;
let stream = doc.SaveAsImage({ pageIndex: i, dpiX: 72, dpiY: 72 });
stream.Save(tempName);
stream.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(tempName);
const blob = new Blob([fileArray], { type: 'image/png' });
previewUrls.push(URL.createObjectURL(blob));
}
doc.Dispose();
// previewUrls can now be rendered as <img src={url} /> in a thumbnail strip
return previewUrls;
};
미리보기 사례에서 dpiX: 72를 사용한 점에 주목하세요. 썸네일에는 전체 해상도가 필요하지 않으며, 더 작게 렌더링하면 스트립의 반응성이 유지됩니다. 컴포넌트가 언마운트될 때 각 URL에 대해 URL.revokeObjectURL(url)를 호출하는 것을 잊지 마세요. 그렇지 않으면 다시 렌더링할 때마다 메모리가 누수됩니다.
이런 글도 좋아하실 수 있습니다: PDF가 아니라 이미지 집합으로 작업하며 스캔본이나 사진을 하나의 문서로 묶는 경우라면 JavaScript(React)에서 이미지를 PDF로 변환하기를 참고하세요.
깨진 텍스트 해결하기: 글꼴 로드
PDF는 보통 사용하는 글꼴을 포함하지만 항상 그런 것은 아니며, 포함하지 않을 때는 렌더러가 무언가로 대체해야 합니다. 브라우저에는 대체할 운영 체제 글꼴 라이브러리가 없으므로, 글꼴이 없으면 빈 공간이나 네모 상자로 나타납니다.
해결책은 렌더링하기 전에 WASM 런타임이 글꼴을 찾을 것으로 기대하는 위치에 글꼴 파일을 넣는 것입니다:
// Load a TrueType font into the VFS font directory before converting
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
ARIALUNI.TTF는 알아 둘 가치가 있습니다. 일반적인 CJK 문자는 물론 라틴 문자도 포함하므로, 사용자가 어떤 언어의 문서를 업로드할지 미리 알 수 없을 때 더 안전한 기본값이 됩니다. 영어 문서만 다룬다면 더 작은 글꼴을 쓰면 다운로드가 가벼워집니다.
이는 어떤 페이지를 렌더링하기 전에 실행되므로, 변환 함수 시작 부분에서 글꼴 누락 검사를 한 번만 하면 충분합니다. 페이지마다 글꼴을 다시 로드할 필요는 없습니다.
자주 묻는 질문
PDF를 이미지로 변환하면 원본 PDF가 바뀌나요?
아니요. SaveAsImage는 페이지를 렌더링하고 새로운 이미지 데이터를 반환합니다. 원본 문서는 읽기 전용으로 열리고 이후 해제됩니다. 원본 PDF는 그대로입니다.
첫 페이지만 변환하려면 어떻게 하나요?
루프를 돌리는 대신 pageIndex: 0으로 SaveAsImage를 한 번 호출하세요. 특정 페이지만 변환하기를 참고하세요.
출력이 원본 PDF보다 큰 이유는 무엇인가요?
렌더링된 페이지는 픽셀을 저장하는 반면 PDF는 그리기 명령을 저장하기 때문입니다. 텍스트가 많은 PDF 페이지는 흔히 몇 킬로바이트에 불과하지만, 같은 페이지의 300 DPI PNG는 몇 메가바이트가 될 수 있습니다. 충실도보다 크기가 중요하다면 96~150 DPI로 낮추거나 JPEG로 전환하세요.
다운로드하는 대신 앱 안에 이미지를 표시할 수 있나요?
가능합니다. 이미지 데이터로 객체 URL을 만들어 <img> 소스로 사용하세요. 이미지 패키징 또는 미리보기를 참고하세요. 다만 작업이 끝나면 URL을 해제하는 것을 잊지 마세요.
아주 긴 문서는 문제를 일으키나요?
페이지는 루프 안에서 한 번에 하나씩 렌더링되므로, WASM 모듈은 문서 전체가 아니라 대략 페이지 이미지 하나만 유지합니다. 매우 긴 파일의 경우에는 모든 이미지를 배열에 보관하는 대신 청크 단위로 변환하고 진행하면서 아카이브에 추가하세요.
페이지를 이미지로 변환하는 것과 PDF에서 이미지를 추출하는 것의 차이는 무엇인가요?
변환은 텍스트, 레이아웃, 이미지, 배경을 포함한 페이지 전체를 하나의 평면 그림으로 렌더링합니다. 추출은 문서에 포함된 원본 이미지 개체를 원래 해상도로 꺼냅니다. 미리보기와 공유에는 변환을 사용하고, 원본 아트워크 자체가 필요할 때는 추출을 사용하세요.
변환된 파일은 실제로 어디로 가나요?
서버도 사용자 디스크도 아닌 WASM 가상 파일 시스템으로 갑니다. 브라우저 밖으로 나가는 것은 아무것도 없으며, 파일이 사용자에게 도달하는 유일한 방법은 직접 작성하는 다운로드 또는 객체 URL 단계입니다.
함께 보기
반대 방향, 즉 이미지를 다시 PDF로 바꾸는 방법을 찾고 계신가요? 그것은 JavaScript(React)에서 이미지를 PDF로 변환하기입니다. React에서의 나머지 PDF 페이지 파이프라인은 다음과 같습니다: