AI 해결 노트 · 2026-09-18 · 실측 2026-09-18
PDF에 워터마크 넣기 — 대각선 글자, 도장 그림, 모든 쪽에 한 번에
한 줄로
5쪽짜리 더미 보고서에 워터마크를 네 가지 방법으로 넣어 봤습니다. 대각선 글자는 0.15초에 끝나고 파일은 78,230바이트에서 137,355바이트가 됐습니다. 다만 글꼴 처리를 한 줄 빠뜨리면 같은 파일이 7,568,897바이트, 원본의 96.75배가 됩니다. 글자로 넣은 워터마크는 글자 뽑기에 그대로 걸려 나오고, 그림으로 넣은 워터마크는 걸리지 않습니다.
이런 분께
- 초안을 돌리기 전에 「대외비」나 「초안」 표시를 박아 두어야 하는 분
- 사진이나 도면 PDF에 회사 도장 이미지를 얹고 싶은 분
- 쪽이 수십 장인데 한 장씩 손대기 싫은 분
이 글에서 다루지 않는 것
워터마크를 넣는 방법만 씁니다. 남이 넣은 워터마크를 지우는 방법은 다루지 않습니다.
실측 환경
| 항목 | 값 |
|---|---|
| OS | Windows 11 Home (10.0.26200) |
| 셸 | PowerShell 7 / Git Bash |
| Python | 3.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. 대각선으로 눕힌 반투명 글자
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번을 찍어도 크기는 오히려 조금 작습니다.
본문을 얼마나 가리는지 보시면 이렇습니다.

이 예제는 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%로 늘려 봤습니다.

위 그림에서 글자 워터마크는 획이 매끈하고, 도장 쪽은 원 테두리에 계단이 보입니다. 숫자로 따지면 이렇습니다. 도장 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.632 | 252.632 | 0.000 |
| 도장 그림 | 251.205 | 251.205 | 0.000 |
| 바둑판 글자 | 252.688 | 252.688 | 0.000 |
| 주석으로 넣은 판 | 252.781 | 252.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만 바이트 늘었습니다. 늘어난 몫이 전부 글꼴인지까지는 뜯어보지 않았습니다.
실패한 것
- 도장 PNG의 배경을 투명하게 만들려고 PyMuPDF의
Pixmap.set_alpha로 흰색만 골라 투명 처리했는데, 검은 네모가 찍혔습니다. 그 PNG의 왼쪽 위 모서리 픽셀을 찍어 보니(0, 0, 0, 255), 곧 불투명한 검정이었습니다. Pillow로 그린 도장의 같은 자리는(0, 0, 0, 0)이었습니다. 실패판을 넣은 PDF의 1쪽 평균 밝기는 236.95로, 제대로 된 도장을 넣은 251.21이나 원본 252.89보다 한참 어둡습니다. 이 글의 도장은 전부 Pillow로 그린 것입니다. - 엣지의 인쇄 기능(
msedge --headless --print-to-pdf)으로 뽑아서 워터마크가 인쇄에 나오는지 보려 했습니다. 5쪽짜리 595.0x842.0pt 문서를 넣었는데 1쪽짜리 612.0x792.0pt 파일(28,747바이트)이 나왔고, 1쪽 글자가26. 9. 18. 오후 5:15 file:///C:/...로 시작했습니다. 문서 대신 PDF 뷰어 화면이 인쇄된 것으로 보입니다. 그래서 인쇄 확인은 주석 여부를 견주는 방법으로 바꿨습니다. insert_image가 받는 인자를 뽑아 보니rect, alpha, filename, height, keep_proportion, mask, oc, overlay, pixmap, rotate, stream, width, xref로, 투명도 자리가 없었습니다. 그래서 PNG 쪽에서 알파를 미리 깎았습니다.get_images()가 한 쪽에서 그림을 5개로 셉니다. 뜯어 보니 전부 같은 xref 41이었습니다. 실제로는 그림 덩어리가 파일에 하나 들어가고 다섯 쪽이 같이 쓰는 것인데, 이 함수가 같은 것을 거듭 세는 모양입니다. 쪽마다 그림이 다섯 벌 들어간 것으로 읽으시면 안 됩니다.
확인하지 않은 것
- 종이에 실제로 뽑아 보지는 않았습니다. 인쇄 이야기는 파일 구조(주석인지 쪽 내용인지)를 보고 판단한 것입니다.
- 아크로뱃 리더나 한글 뷰어, 스마트폰 뷰어에서 이 워터마크가 어떻게 보이는지는 보지 않았습니다.
- 쪽 크기가 서로 다른 PDF나 가로 쪽이 섞인 PDF에 같은 코드를 걸면 어떻게 되는지는 시험하지 않았습니다. 이 글의 더미는 다섯 쪽 모두 595.0 x 842.0pt 세로입니다.
- 이미 그림이 많은 PDF나 스캔본에 워터마크를 넣으면 크기가 얼마나 느는지는 재지 않았습니다. 시험한 원본에는 그림이 0개입니다.
- 워드나 한글의 메뉴로 워터마크를 넣은 뒤 PDF로 내보내는 방법은 다루지 않았습니다. 이미 PDF인 파일에 넣는 경우만 봤습니다.
- 워터마크를 본문 아래(밑바탕)에 까는 방법은 시험하지 않았습니다. 그림 워터마크는
overlay=True로 본문 위에 얹었고, 나머지는 인자를 따로 주지 않은 기본 동작 그대로입니다. - 쪽 수가 수백 장인 문서에서 걸리는 시간은 재지 않았습니다. 다섯 쪽 기준 숫자만 있습니다.