브라우저에서 PDF를 스캔한 것처럼 보이게 만드는 방법
요약
브라우저 환경에서 디지털 PDF를 실제 스캔한 문서처럼 보이게 만드는 기술적 구현 방법을 설명합니다. PDF.js와 Canvas API를 활용하여 로컬에서 모든 처리를 수행하는 파이프라인을 구축하는 과정을 다룹니다.
핵심 포인트
- PDF.js를 사용하여 PDF 페이지를 Canvas에 렌더링
- 서버 비용 절감 및 보안을 위한 브라우저 측 로컬 처리 방식 채택
- Canvas API를 통한 스캐너 스타일의 시각적 효과 적용
- DPI 조절을 통한 최종 결과물의 해상도 제어
PDF를 스캔한 것처럼 보이게 만드는 것은 놀라울 정도로 실용적인 문서 처리 문제입니다. 사용자들은 종종 물리적인 프린터나 스캐너를 사용하지 않고도 디지털 PDF가 인쇄되고, 서명된 후 다시 스캔된 문서처럼 보이기를 원합니다.
이 기사는 Make PDF Look Scanned 컴포넌트를 구축하기 위한 브라우저 기반의 기술적 접근 방식을 설명합니다. 이 컴포넌트는 PDF 파일을 받아 각 페이지를 Canvas에 렌더링하고, 스캐너와 같은 시각적 효과를 적용한 뒤, 새로운 이미지 기반 PDF로 내보냅니다.
컴포넌트 목표
컴포넌트의 임무는 단 하나입니다:
깨끗한 디지털 PDF를 브라우저에서 직접 현실적인 스캔 느낌의 PDF로 변환하는 것입니다.
훌륭한 구현체는 다음을 지원해야 합니다:
PDF 업로드
페이지별 렌더링
조절 가능한 스캐너 효과
로컬 전용 처리 (Local-only processing)
미리보기
다운로드 가능한 스캔 느낌의 PDF
상위 수준의 파이프라인(Pipeline)은 다음과 같습니다:
PDF 파일
-> PDF.js로 페이지 렌더링
-> 각 페이지를 Canvas에 그리기
-> 스캐너 스타일의 Canvas 효과 적용
-> 페이지를 JPEG로 인코딩
-> 이미지 기반 PDF로 조립
-> 결과물 다운로드
브라우저 측 처리(Browser-Side Processing)를 사용하는 이유?
스캔 느낌의 PDF 도구는 입력 파일에 민감한 내용이 포함될 수 있기 때문에 브라우저 측 처리에 적합합니다.
변환을 로컬에서 유지하면 다음과 같은 몇 가지 장점이 있습니다:
원본 PDF를 업로드할 필요가 없습니다.
서버가 PDF 렌더링 또는 이미지 처리 비용을 부담하지 않습니다.
사용자가 더 빠른 피드백을 받습니다.
컴포넌트가 독립적인 프론트엔드 도구로 작동할 수 있습니다.
브라우저는 이미 필요한 대부분의 기능을 갖추고 있습니다: 파일 입력(file input), Canvas, Blob URL, JPEG 인코딩, 그리고 PDF.js를 통한 Web Workers.
PDF 페이지 렌더링
브라우저는 PDF 페이지를 픽셀로 네이티브하게 노출할 수 없으므로, 컴포넌트는 PDF.js를 사용하여 각 페이지를 Canvas에 렌더링합니다.
컴포넌트는 업로드된 파일을 ArrayBuffer로 읽어 PDF.js에 전달한 다음, 모든 페이지를 루프(loop)로 처리합니다:
const source = new Uint8Array(await file.arrayBuffer());
const pdf = await pdfjs.getDocument({ data: source.slice() }).promise;
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({
scale: dpi / 72,
});
canvas.width = Math.floor(viewport.width);
canvas.height = Math.floor(viewport.height);
await page.render({
canvasContext: ctx,
viewport,
}).promise;
}
dpi / 72 스케일은 중요합니다. PDF 페이지는 포인트 (points) 단위로 측정되며, 72포인트는 1인치와 같습니다. DPI를 조정함으로써, 컴포넌트는 최종 스캔된 페이지의 래스터 해상도 (raster resolution)를 제어합니다.
스캐너 효과 적용하기 (Applying Scanner Effects)
PDF 페이지가 캔버스 (Canvas)에 렌더링되면, 컴포넌트는 이를 스캔한 것처럼 보이는 이미지로 변환합니다.
목표는 노골적인 필터를 만드는 것이 아닙니다. 실제 스캔된 문서는 보통 다음과 같은 미세한 불완전함을 포함합니다:
- 약간의 회전 (slight rotation)
- 따뜻한 종이 톤 (warm paper tone)
- 그레이스케일 (grayscale) 또는 감소된 대비 (reduced contrast)
- 작은 블러 (small blur)
- 미세한 입자감 (fine grain)
- 가장자리 그림자 (edge shadow)
- JPEG 압축 (JPEG compression)
컴포넌트는 표준 Canvas API를 사용하여 이러한 효과를 적용합니다.
약간의 페이지 회전 (Slight Page Rotation)
완벽하게 정렬된 문서는 디지털 문서처럼 보입니다. 실제 스캔된 페이지는 종종 약간 어긋나 있습니다.
컴포넌트는 렌더링된 페이지를 중심을 기준으로 회전시킵니다:
const angle = ((rng() - 0.5) * 2 * skew * Math.PI) / 180;
ctx.translate(width / 2, height / 2);
ctx.rotate(angle);
ctx.drawImage(sourceCanvas, -width / 2, -height / 2);
기본 회전 값은 작아야 합니다. 보통 0.5°에서 1° 사이의 값이면 충분합니다.
그레이스케일, 블러, 그리고 대비 (Grayscale, Blur, and Contrast)
Canvas는 ctx.filter를 지원하며, 이를 통해 그리기 전에 일반적인 이미지 효과들을 결합할 수 있습니다:
ctx.filter = [
grayscale ? 'grayscale(1)' : '',
blur > 0 ? blur(${blur}px) : '',
paperTone > 0 ? contrast(${1 - paperTone * 0.08}) : '',
]
.filter(Boolean)
.join(' ');
사실적인 스캔 느낌은 절제에 달려 있습니다. 블러가 너무 심하면 텍스트를 읽기 어려워집니다. 대비 감소가 너무 과하면 결과물이 인위적으로 처리된 것처럼 보입니다.
종이 톤 (Paper Tone)
깨끗한 디지털 PDF는 보통 순백색 배경을 가집니다. 스캔된 페이지는 종종 더 따뜻하고 부드러운 종이 색상을 띱니다.
해당 컴포넌트는 먼저 출력 캔버스(canvas)를 가벼운 종이 느낌의 색상으로 채웁니다:
ctx.fillStyle = paperFill(paperTone);
ctx.fillRect(0, 0, width, height);
그 다음 미세하고 따뜻한 색조(tint)를 덧씌웁니다:
ctx.globalCompositeOperation = 'multiply';
ctx.globalAlpha = Math.min(0.14, paperTone * 0.14);
ctx.fillStyle = 'rgb(255, 248, 226)';
ctx.fillRect(0, 0, width, height);
불투명도(opacity)는 낮게 유지해야 합니다. 목표는 페이지를 노랗게 만드는 것이 아니라, 살균된 듯한 디지털 화이트(digital-white) 느낌을 제거하는 것입니다.
미세한 입자 (Fine Grain)
노이즈(Noise)는 가장 중요한 효과 중 하나이지만, 잘못 구현하기 매우 쉽습니다.
노이즈가 큰 블록 단위로 적용되면 페이지 배경이 픽셀화(pixelated)되거나 모자이크처럼 보일 수 있습니다. 더 나은 접근 방식은 매우 낮은 강도를 가진 미세한 픽셀당 입자(per-pixel grain)를 사용하는 것입니다.
const image = ctx.getImageData(0, 0, width, height);
const data = image.data;
for (let y = 0; y < height; y++) {
let rowBias = 0;
if (y % 3 === 0) {
rowBias = (rng() - 0.5) * amount * 6;
}
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const luminance =
0.299 * data[idx] +
0.587 * data[idx + 1] +
...
}
}
ctx.putImageData(image, 0, 0);
이 방식은 더 미세한 질감을 만들어내며 명백한 사각형 아티팩트(artifacts)를 방지합니다.
행 편향(row bias)은 문서를 줄무늬 이미지로 만들지 않으면서도 매우 미세한 스캐너 라인(scanner-line) 느낌을 더해줍니다.
가장자리 그림자 (Edge Shadow)
실제 종이는 스캐너 유리 위에 완벽하게 평평하게 놓이는 경우가 드뭅니다. 약간의 가장자리 어두워짐(edge darkening)은 환상을 심어주는 데 도움이 됩니다.
컴포넌트는 선형 그라데이션(linear gradients)을 사용하여 가장자리 그림자를 적용합니다:
ctx.globalCompositeOperation = 'multiply';
drawEdgeGradient(ctx, 0, 0, band, height, 'left', alpha);
drawEdgeGradient(ctx, width - band, 0, band, height, 'right', alpha);
drawEdgeGradient(ctx, 0, 0, width, band, 'top', alpha);
drawEdgeGradient(ctx, 0, height - band, width, band, 'bottom', alpha);
문서는 직사각형이기 때문에 강한 비네팅(vignette)보다는 방향성이 있는 그라데이션이 더 낫습니다. 효과는 사진 필터가 아니라 유리 위의 종이처럼 보여야 합니다.
페이지를 JPEG로 인코딩하기 (Encoding Pages as JPEG)
효과가 적용된 후, 처리된 각 Canvas는 JPEG로 인코딩됩니다:
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(
(nextBlob) =>
nextBlob
? resolve(nextBlob)
: reject(new Error('Failed to encode JPEG.')),
'image/jpeg',
quality
);
});
const jpegBytes = new Uint8Array(await blob.arrayBuffer());
JPEG 압축 (JPEG compression)은 스캔된 느낌을 구현하는 요소 중 하나입니다. 실제 스캔된 PDF는 종종 이미지 압축 아티팩트 (compression artifacts)를 포함합니다. 하지만 품질이 너무 낮으면 텍스트 가독성이 떨어지므로 주의해야 합니다.
보통 0.7에서 0.8 사이의 기본 품질이 적절하게 작동합니다.
PDF 재구성하기 (Rebuilding the PDF)
최종 출력물은 이미지 기반의 PDF입니다. 처리된 각 JPEG 페이지는 원래 페이지 크기에 맞춰 새로운 PDF 페이지에 임베드 (embed) 됩니다.
개념적으로는 다음과 같습니다:
각 처리된 페이지에 대하여:
- 원래 너비/높이를 가진 PDF 페이지 생성
- JPEG를 전체 페이지 이미지로 임베드
- 0,0 위치에 페이지 전체를 덮도록 이미지 그리기
이렇게 하면 각 페이지가 선택 가능한 텍스트가 아닌 이미지로 구성되어, 스캔된 문서처럼 동작하는 PDF가 생성됩니다.
스캔된 문서는 대개 래스터화 (rasterized) 되어 있기 때문에 이 방식이 유용합니다. 또한 결과물이 단순히 일반적인 디지털 PDF 위에 필터를 씌운 것처럼 보이는 것을 방지하는 데 도움이 됩니다.
미리보기 및 다운로드 (Preview and Download)
새로운 PDF 바이트 (bytes)가 생성되면, 컴포넌트는 이를 Blob URL로 변환합니다:
const blob = new Blob([pdfBuffer], {
type: 'application/pdf',
});
const url = URL.createObjectURL(blob);
동일한 URL은 다음 용도로 사용될 수 있습니다:
- iframe 미리보기
- 다운로드 링크
- document.scanned.pdf와 같이 생성된 파일 이름
성능과 메모리 안전성을 위해, 컴포넌트는 새로운 파일이 선택되거나 컴포넌트가 언마운트 (unmount) 될 때 이전의 Blob URL을 해제 (revoke) 해야 합니다.
URL.revokeObjectURL(previousUrl);
권장 기본 설정 (Recommended Default Settings)
좋은 스캔 효과는 미묘해야 합니다. 실용적인 기본 설정 세트는 다음과 같습니다:
const defaultParams = {
dpi: 144,
skew: 0.8,
grayscale: true,
paperTone: 0.52,
noise: 0.035,
blur: 0.35,
edgeShadow: 0.18,
jpegQuality: 74,
};
이 값들은 가독성을 우선시하면서도, 문서가 디지털적으로 너무 완벽해 보이지 않도록 조절합니다.
흔한 실수
과도한 노이즈 (Too Much Noise)
노이즈가 너무 강하면 페이지가 지저분해 보이고 시선을 분산시킵니다. 블록 기반의 노이즈는 모자이크 아티팩트 (Mosaic artifacts)를 생성할 수 있습니다. 대신 미세하고 강도가 낮은 입자 (Grain)를 사용하세요.
과도한 회전 (Too Much Rotation)
스캔된 페이지는 약간 불완전해 보여야지, 망가져 보여서는 안 됩니다. 회전 값이 너무 크면 금방 가짜처럼 보입니다.
과도한 블러 (Excessive Blur)
블러 (Blur)는 스캐너의 광학 특성을 시뮬레이션하는 데 도움이 되지만, 너무 과하면 텍스트의 명확성을 해칩니다.
지나치게 노란 종이 (Overly Yellow Paper)
종이 톤 (Paper tone)은 거의 눈에 띄지 않아야 합니다. 강한 노란색 오버레이는 문서를 스캔한 것이 아니라 인위적으로 오래된 것처럼 보이게 만듭니다.
텍스트를 텍스트로 유지하기 (Keeping Text as Text)
스캔된 문서를 시뮬레이션하는 것이 목표라면, 벡터 텍스트 (Vector text)를 내보내는 것은 이상적이지 않습니다. 각 페이지를 이미지 기반의 PDF로 래스터화 (Rasterizing)하면 더 설득력 있는 결과를 얻을 수 있습니다.
최종 아키텍처 (Final Architecture)
깔끔한 컴포넌트 아키텍처는 단순하게 유지할 수 있습니다:
MakeLookScanned 컴포넌트
- 파일 입력 처리
- PDF.js 로드
- PDF 페이지 렌더링
- Canvas 효과 적용
- JPEG 페이지 인코딩
- 최종 PDF 조립
- 미리보기/다운로드 URL 관리
이 컴포넌트는 백엔드 서비스가 필요하지 않습니다. 브라우저 내에서 완전히 실행될 수 있습니다.
결론
“PDF를 스캔된 것처럼 만들기” 컴포넌트는 표준 브라우저 기술로 구축할 수 있습니다:
- PDF 페이지 렌더링을 위한 PDF.js
- 시각적 스캐너 효과를 위한 Canvas
- 압축 아티팩트 (Compression artifacts)를 위한 JPEG 인코딩
- 미리보기 및 다운로드를 위한 Blob URL
- 최종 출력을 위한 이미지 기반 PDF 조립
사실성의 핵심은 미묘함입니다. 약간의 회전, 미세한 입자, 부드러운 블러, 따뜻한 종이 톤, 그리고 가벼운 가장자리 그림자 (Edge shadows)를 사용하면, 결과물을 읽기 어렵게 만들거나 명백한 필터 효과를 주지 않으면서도 깨끗한 디지털 PDF를 실제 물리적 스캐너에서 나온 것처럼 보이게 할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기