pdf 파일 한장씩 쪼개서 저장하기
스캔
오늘 회사에서 서류를 여러장 스캔하는데, 각각 페이지마다 다른 곳으로 메일을 발송해야하는 일이었다. 스캔을 하고 보니, 한개의 파일로 바인딩이 되어있어서, acrobat으로 작업을 할까 하다가, 그냥 chatgpt 시켜서 스크립트를 짰더니, 음… 뭐 나쁘지 않다.
pypdf
뭔들 없겠느냐만, python 패키지는 찾는것도 일이다. AI덕분에 이제는 그냥 생각만 해도 되니 꽤나 편하다. 당장 써야하는 관계로 설계랄 것 없이 바로 돌릴 수 있는 형태로 만들었다.
여러 페이지의 pdf를 각 페이지 기준으로 저장
내가 써먹을 스크립트이다. 뭐 향후 사용을 위해 생각을 하자만 한도 끝도 없지만, 일단 필요한 건 바로 쓰기 편한 형태로 간다.
from pypdf import PdfReader, PdfWriter
import os
input_pdf = "1.pdf"
output_dir = "split_pages"
os.makedirs(output_dir, exist_ok=True)
reader = PdfReader(input_pdf)
for i, page in enumerate(reader.pages, start=1):
writer = PdfWriter()
writer.add_page(page)
output_file = os.path.join(
output_dir,
f"page_{i:03d}.pdf"
)
with open(output_file, "wb") as f:
writer.write(f)
print(f"완료: 총 {len(reader.pages)} 페이지 분리")
선택한 페이지만 변환
선택한 페이지만 변환하는 옵션 정도는 있어야하겠다.
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
from pypdf import PdfReader, PdfWriter
def parse_pages(value: str, total_pages: int) -> list[int]:
pages: list[int] = []
for item in value.replace(" ", "").split(","):
if not item:
raise ValueError("페이지 번호를 입력해 주세요.")
try:
if "-" in item:
parts = item.split("-")
if len(parts) != 2:
raise ValueError
start, end = map(int, parts)
if start > end:
raise ValueError(f"범위의 시작이 끝보다 큽니다: {item}")
selected = range(start, end + 1)
else:
selected = [int(item)]
except ValueError as exc:
if str(exc):
raise
raise ValueError(f"잘못된 페이지 형식입니다: {item}") from exc
for page in selected:
if not 1 <= page <= total_pages:
raise ValueError(f"{page}페이지는 범위를 벗어납니다. 1~{total_pages} 사이로 입력하세요.")
if page - 1 not in pages:
pages.append(page - 1)
return pages
def main() -> None:
root = tk.Tk()
root.withdraw()
input_file = filedialog.askopenfilename(
title="원본 PDF 선택", filetypes=[("PDF 파일", "*.pdf")]
)
if not input_file:
root.destroy()
return
try:
reader = PdfReader(input_file)
if reader.is_encrypted and not reader.decrypt(""):
raise ValueError("암호가 설정된 PDF는 처리할 수 없습니다.")
total_pages = len(reader.pages)
page_text = simpledialog.askstring(
"페이지 선택",
f"총 {total_pages}페이지입니다.\n추출할 페이지를 입력하세요.\n\n예: 1,3,5-8",
parent=root,
)
if page_text is None:
return
selected_pages = parse_pages(page_text, total_pages)
source = Path(input_file)
output_file = filedialog.asksaveasfilename(
title="선택한 페이지 저장",
initialdir=str(source.parent),
initialfile=f"{source.stem}_selected.pdf",
defaultextension=".pdf",
filetypes=[("PDF 파일", "*.pdf")],
)
if not output_file:
return
writer = PdfWriter()
for page_index in selected_pages:
writer.add_page(reader.pages[page_index])
with open(output_file, "wb") as output:
writer.write(output)
messagebox.showinfo("완료", f"{len(selected_pages)}개 페이지를 저장했습니다.\n{output_file}", parent=root)
except (ValueError, OSError) as exc:
messagebox.showerror("오류", str(exc), parent=root)
except Exception as exc:
messagebox.showerror("오류", f"PDF 처리 중 오류가 발생했습니다.\n{exc}", parent=root)
finally:
root.destroy()
if __name__ == "__main__":
main()
합치기
분리를 하다보면 또 붙이는게 아쉬우니, 하는김에 같이 해둔다.
from pypdf import PdfWriter
files = [
"01_표지.pdf",
"02_본문.pdf",
"03_첨부.pdf",
]
writer = PdfWriter()
for file in files:
writer.append(file)
with open("merged.pdf", "wb") as f:
writer.write(f)
jpg를 pdf로 변환
그냥 pdf로 출력(print) 하면 되지만, 그마저도 귀찮다.
from PIL import Image
imgname = "계획"
image = Image.open(imgname + ".jpg")
image = image.convert("RGB")
image.save(imgname + ".pdf")
print("완료")
pdf를 jpg로 변환
반대로 pjf를 jpg 파일로 변환하는 기능 정도는 있어야겠지?
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox
import pypdfium2 as pdfium
DPI = 300
JPEG_QUALITY = 100
def pdf_to_jpg(pdf_path: str, output_dir: str, dpi: int = DPI) -> list[Path]:
"""PDF의 모든 페이지를 JPG 파일로 변환하고 저장 경로 목록을 반환한다."""
source = Path(pdf_path)
destination = Path(output_dir)
destination.mkdir(parents=True, exist_ok=True)
document = pdfium.PdfDocument(str(source))
saved_files: list[Path] = []
scale = dpi / 72
try:
page_count = len(document)
if page_count == 0:
raise ValueError("페이지가 없는 PDF입니다.")
digits = max(3, len(str(page_count)))
for page_number in range(page_count):
page = document[page_number]
try:
bitmap = page.render(scale=scale)
image = bitmap.to_pil().convert("RGB")
output_file = destination / (
f"{source.stem}_page_{page_number + 1:0{digits}d}.jpg"
)
image.save(output_file, "JPEG", quality=JPEG_QUALITY, optimize=True)
image.close()
bitmap.close()
saved_files.append(output_file)
finally:
page.close()
finally:
document.close()
return saved_files
def main() -> None:
root = tk.Tk()
root.withdraw()
pdf_path = filedialog.askopenfilename(
title="JPG로 변환할 PDF 선택",
filetypes=[("PDF 파일", "*.pdf")],
)
if not pdf_path:
root.destroy()
return
source = Path(pdf_path)
output_dir = filedialog.askdirectory(
title="JPG를 저장할 폴더 선택",
initialdir=str(source.parent),
)
if not output_dir:
root.destroy()
return
try:
saved_files = pdf_to_jpg(pdf_path, output_dir)
messagebox.showinfo(
"변환 완료",
f"총 {len(saved_files)}페이지를 JPG로 저장했습니다.\n\n{output_dir}",
parent=root,
)
except Exception as exc:
messagebox.showerror(
"변환 실패",
f"PDF를 JPG로 변환하는 중 오류가 발생했습니다.\n\n{exc}",
parent=root,
)
finally:
root.destroy()
if __name__ == "__main__":
main()
결론
프론트 엔드를 붙여서 여러형태의 작업을 자유자재로 하는 것도 생각해봤으나, 그게 무슨 의미인가. 그냥 그때 그때 AI로 만드는게 더 빠를 수 있다. 이걸 활용해서 무엇을 할 수 있을지 생각해봐야한다.
그런데 왜 이 코드를 블로그에 적고 있는 거지?
끝.