AI 해결 노트 · 2026-09-18 · 실측 2026-09-18

PDF에 워터마크 넣기 — 대각선 글자, 도장 그림, 모든 쪽에 한 번에

한 줄로

5쪽짜리 더미 보고서에 워터마크를 네 가지 방법으로 넣어 봤습니다. 대각선 글자는 0.15초에 끝나고 파일은 78,230바이트에서 137,355바이트가 됐습니다. 다만 글꼴 처리를 한 줄 빠뜨리면 같은 파일이 7,568,897바이트, 원본의 96.75배가 됩니다. 글자로 넣은 워터마크는 글자 뽑기에 그대로 걸려 나오고, 그림으로 넣은 워터마크는 걸리지 않습니다.

이런 분께

이 글에서 다루지 않는 것

워터마크를 넣는 방법만 씁니다. 남이 넣은 워터마크를 지우는 방법은 다루지 않습니다.

실측 환경

항목
OSWindows 11 Home (10.0.26200)
PowerShell 7 / Git Bash
Python3.12.10
파이썬 꾸러미PyMuPDF 1.28.0 (MuPDF 1.29.0), pypdf 6.14.2, Pillow
글꼴맑은 고딕 C:\Windows\Fonts\malgun.ttf (13,459,196바이트)
시험 파일직접 만든 더미 보고서. 이름·번호는 지어낸 값(홍길동, 010-0000-0000)
온라인 변환 사이트쓰지 않았습니다

원본은 5쪽, 78,230바이트, 뽑히는 글자 237자, 그림 0개입니다. 다섯 쪽 모두 595.0 x 842.0pt(A4 세로), 회전 0도입니다.

먼저 보실 것

세 가지를 나란히 놓으면 이렇습니다.

대각선 글자, 도장 그림, 바둑판 글자 워터마크를 각각 넣은 1쪽
대각선 글자, 도장 그림, 바둑판 글자 워터마크를 각각 넣은 1쪽

1. 대각선으로 눕힌 반투명 글자

PyMuPDF의 TextWriter를 씁니다. 글자를 담아 두었다가 write_text에서 투명도와 회전을 한꺼번에 주는 구조입니다.

import fitz

FONT = r"C:\Windows\Fonts\malgun.ttf"
doc = fitz.open(r"C:\경로\원본.pdf")
글꼴 = fitz.Font(fontfile=FONT)

for page in doc:
    가운데 = fitz.Point(page.rect.width / 2, page.rect.height / 2)
    글 = "대외비 · 홍길동"
    폭 = 글꼴.text_length(글, fontsize=54)
    시작 = fitz.Point(가운데.x - 폭 / 2, 가운데.y + 18)

    tw = fitz.TextWriter(page.rect, color=(0.85, 0.1, 0.1))
    tw.append(시작, 글, font=글꼴, fontsize=54)
    tw.write_text(page, opacity=0.18, morph=(가운데, fitz.Matrix(45)))

doc.subset_fonts()
doc.save(r"C:\경로\워터마크.pdf", garbage=4, deflate=True)
doc.close()

opacity=0.18은 불투명도 18%, fitz.Matrix(45)는 45도 회전입니다. morph의 첫 값이 회전의 기준점이라 쪽 한가운데를 줬고, text_length로 잰 글 폭의 절반만큼 시작점을 왼쪽으로 옮겨 가운데에 맞췄습니다.

다섯 쪽 전부 처리하는 데 0.15초가 걸렸습니다(subset_fonts() 포함).

subset_fonts()를 빼면 96배가 넘습니다

위 코드에서 doc.subset_fonts() 한 줄을 빼고 돌려 봤습니다.

저장 방법결과 크기원본(78,230바이트) 대비
subset_fonts() 없이7,568,897바이트9,675%
subset_fonts() 넣고137,355바이트176%

찍은 글자는 「대외비 · 홍길동」 아홉 자뿐인데 파일이 7.5MB가 됩니다. 쓴 글꼴 파일 malgun.ttf 자체가 13,459,196바이트인 것을 보면 글꼴 쪽이 무게를 지고 있는 것으로 보입니다. 저장 직전에 subset_fonts()를 한 줄 부르면 7,568,897바이트가 137,355바이트로 내려갑니다.

2. 도장 그림 얹기

그림은 insert_image 한 줄입니다. 쪽 어디에 얼마만 하게 놓을지 사각형으로 정해 줍니다.

import fitz

doc = fitz.open(r"C:\경로\원본.pdf")
for page in doc:
    가운데 = fitz.Point(page.rect.width / 2, page.rect.height / 2)
    칸 = fitz.Rect(가운데.x - 90, 가운데.y - 90, 가운데.x + 90, 가운데.y + 90)
    page.insert_image(칸, filename=r"C:\경로\도장.png",
                      overlay=True, keep_proportion=True)
doc.save(r"C:\경로\도장찍음.pdf", garbage=4, deflate=True)
doc.close()

도장이 본문을 네모나게 덮지 않게 하려면 배경이 비치는 PNG를 쓰셔야 합니다. 배경이 불투명한 PNG를 본문 위에 얹으면 그 네모만큼 본문이 가려집니다. 배경 처리를 잘못해 검은 네모가 찍혔던 일은 아래 「실패한 것」에 숫자와 함께 적었습니다.

투명 배경 도장은 Pillow로 그릴 수 있습니다.

from PIL import Image, ImageDraw, ImageFont

FONT = r"C:\Windows\Fonts\malgun.ttf"
크 = 420
im = Image.new("RGBA", (크, 크), (0, 0, 0, 0))    # 알파 0 = 완전히 투명
dr = ImageDraw.Draw(im)
빨강 = (217, 26, 26, 255)
dr.ellipse([16, 16, 크 - 16, 크 - 16], outline=빨강, width=15)

def 가운데글(글, f, y):
    w = dr.textbbox((0, 0), 글, font=f)[2]
    dr.text(((크 - w) / 2, y), 글, font=f, fill=빨강)

가운데글("대외비", ImageFont.truetype(FONT, 92), 120)
가운데글("DUMMY", ImageFont.truetype(FONT, 62), 230)
im.save(r"C:\경로\도장.png")

420x420픽셀짜리 이 PNG는 8,323바이트입니다. 넣은 결과는 93,946바이트(원본의 120.1%)가 됐습니다.

insert_image가 받는 인자에는 투명도가 없습니다(rect, alpha, filename, height, keep_proportion, mask, oc, overlay, pixmap, rotate, stream, width, xref). 흐리게 하려면 PNG의 알파 값을 미리 깎아 두면 됩니다.

from PIL import Image

im = Image.open(r"C:\경로\도장.png").convert("RGBA")
im.putalpha(im.getchannel("A").point(lambda v: int(v * 0.25)))   # 25%로
im.save(r"C:\경로\도장_연하게.png")

이 연한 도장으로 만든 파일은 0.03초에 93,990바이트로 나왔습니다. 1쪽을 200dpi로 그려 평균 밝기를 재 보니 원본 252.89, 진한 도장 251.21, 연한 도장 252.47이었습니다(0~255, 낮을수록 잉크가 많다는 뜻입니다).

그림은 파일 안에 하나만 들어가고 다섯 쪽이 그것을 같이 썼습니다. 쪽마다 따로 넣었는데도 다섯 쪽 모두 같은 xref 41을 가리킵니다. 쪽 수를 더 늘려 보지는 않았습니다.

3. 쪽 전체에 바둑판으로

한 쪽에 여러 번 찍으려면 TextWriter 하나에 여러 번 append하고 마지막에 한 번 write_text 하면 됩니다.

for page in doc:
    tw = fitz.TextWriter(page.rect, color=(0.1, 0.1, 0.1))
    for y in range(120, int(page.rect.height), 180):
        for x in range(40, int(page.rect.width), 200):
            tw.append(fitz.Point(x, y), "대외비", font=글꼴, fontsize=22)
    가운데 = fitz.Point(page.rect.width / 2, page.rect.height / 2)
    tw.write_text(page, opacity=0.12, morph=(가운데, fitz.Matrix(30)))
doc.subset_fonts()
doc.save(...)

한 쪽에 15개씩, 다섯 쪽에 75개를 찍었습니다. 0.23초가 걸렸고 파일은 136,381바이트입니다. 쪽마다 한 번만 찍은 대각선 글자 쪽이 137,355바이트였으니, 75번을 찍어도 크기는 오히려 조금 작습니다.

본문을 얼마나 가리는지 보시면 이렇습니다.

원본과 바둑판 워터마크를 넣은 1쪽 윗부분 비교
원본과 바둑판 워터마크를 넣은 1쪽 윗부분 비교

이 예제는 opacity=0.12로 찍은 것입니다. 표나 도면처럼 선이 많은 쪽에서 어떻게 보이는지는 시험하지 않았으니, 쓰실 문서로 한 번 뽑아 보시는 편이 좋습니다.

4. pypdf로 도장 PDF를 겹치기

PyMuPDF 없이 pypdf만 쓸 수도 있습니다. 이쪽은 워터마크를 미리 1쪽짜리 PDF로 만들어 두고 본문 위에 겹칩니다.

from pypdf import PdfReader, PdfWriter, Transformation

스탬프 = PdfReader(r"C:\경로\도장.pdf").pages[0]
w = PdfWriter(clone_from=r"C:\경로\원본.pdf")
for page in w.pages:
    page.merge_transformed_page(스탬프, Transformation())
with open(r"C:\경로\결과.pdf", "wb") as f:
    w.write(f)

겹칠 도장 PDF(60,439바이트, 1쪽)를 만들어 붙이니 0.06초에 139,664바이트가 나왔습니다. 결과 파일도 다섯 쪽 모두 「대외비」가 글자로 뽑혔고 주석은 0개였습니다. 이미 만들어 둔 워터마크 PDF가 있다면 이 길이 편합니다.

워터마크가 글자 선택에 잡히나

여기가 갈리는 지점입니다. 결과 파일에서 글자를 뽑아 봤습니다.

파일뽑히는 글자 수「대외비」가 글자로 잡히는 쪽
원본237자0/5
대각선 글자287자5/5
도장 그림237자0/5
바둑판 글자477자5/5
pypdf 겹치기257자5/5

1쪽에서 뽑은 글자를 그대로 보면 이렇습니다.

원본        : 사업 추진 계획 (더미) / 작성 홍길동 / 연락처 010-0000-0000 / 이 문서는 시험용으로 지어낸 내용입니다. / - 1 -
대각선 글자  : ... / - 1 - / 대외비 · 홍길동
도장 그림    : ... / - 1 -
바둑판      : ... / - 1 - / 대외비 / 대외비 / 대외비 / 대외비 / 대외비 / 대외 ...

글자로 넣은 워터마크는 본문 글자와 같은 자리에 들어갑니다. 바둑판은 원본 237자에 240자가 더 붙어 477자가 됐습니다. 본문만 떼어 쓰려는 사람에게는 「대외비」가 섞여 나온다는 뜻입니다.

도장 그림으로 넣으면 글자 수가 237자 그대로였고 「대외비」는 다섯 쪽 어디에서도 뽑히지 않았습니다. 뷰어의 글자 찾기가 이 워터마크를 잡는지까지는 시험하지 않았습니다.

확대하면 어떻게 보이나

같은 자리를 400%로 늘려 봤습니다.

원본·글자 워터마크·도장·연한 도장을 400%로 확대한 네 장
원본·글자 워터마크·도장·연한 도장을 400%로 확대한 네 장

위 그림에서 글자 워터마크는 획이 매끈하고, 도장 쪽은 원 테두리에 계단이 보입니다. 숫자로 따지면 이렇습니다. 도장 PNG는 420x420픽셀인데 쪽에서 차지하는 자리가 180 x 180pt(6.35 x 6.35cm)이므로 쪽 위 해상도가 168dpi입니다.

크게 찍거나 확대해 볼 문서라면 도장 PNG를 더 큰 픽셀로 만들어 두시는 편이 낫습니다. 다른 뷰어에서도 같게 보이는지는 확인하지 않았습니다.

인쇄에도 나오나

종이에 뽑아 보지는 못해서, 대신 워터마크가 쪽 내용으로 들어갔는지 주석(annotation)으로 붙었는지를 확인했습니다. 주석은 뷰어 설정에 따라 인쇄에서 빠질 수 있어 자리를 가려 둘 필요가 있습니다.

파일쪽 안 글꼴쪽 안 그림주석
원본1개0개0개
대각선 글자6개0개0개
도장 그림1개있음0개
바둑판 글자6개0개0개
pypdf 겹치기6개0개0개
(견주려고 만든) 주석판1개0개1개

주석은 전부 0개입니다. 워터마크가 쪽 내용 안에 들어갔다는 뜻입니다. 확인 삼아 「주석을 빼고 그리기」로 1쪽을 200dpi로 그려 평균 밝기를 견줘 봤습니다.

파일주석 포함주석 빼고차이
대각선 글자252.632252.6320.000
도장 그림251.205251.2050.000
바둑판 글자252.688252.6880.000
주석으로 넣은 판252.781252.885-0.105

주석으로 넣은 판만 값이 달라지고, 주석을 빼면 원본과 같은 252.885로 돌아갑니다. 위 네 가지는 주석을 빼도 값이 그대로입니다. 다만 종이에 뽑아 보지는 않았습니다.

크기 변화 정리

방법크기원본 대비늘어난 몫걸린 시간
원본78,230바이트100.0%
대각선 글자 (subset_fonts 없이)7,568,897바이트9,675.2%+7,490,667바이트0.99초
대각선 글자 (subset_fonts 넣고)137,355바이트175.6%+59,125바이트0.15초
도장 그림93,946바이트120.1%+15,716바이트0.04초
도장 그림 (25%로 연하게)93,990바이트120.1%+15,760바이트0.03초
바둑판 글자136,381바이트174.3%+58,151바이트0.23초
pypdf 겹치기139,664바이트178.5%+61,434바이트0.06초

글자로 넣은 세 가지는 원본보다 5.8만~6.1만 바이트 커졌고, 그림으로 넣은 쪽은 1.6만 바이트 늘었습니다. 늘어난 몫이 전부 글꼴인지까지는 뜯어보지 않았습니다.

실패한 것

확인하지 않은 것

함께 보기