Skip to content

Instantly share code, notes, and snippets.

@HelloWorld017
Created December 15, 2025 10:05
Show Gist options
  • Select an option

  • Save HelloWorld017/c61845dccb218ca1ddb9f82d2e97b687 to your computer and use it in GitHub Desktop.

Select an option

Save HelloWorld017/c61845dccb218ca1ddb9f82d2e97b687 to your computer and use it in GitHub Desktop.

concatenate-pymupdf

Concatenate multiple pdf pages using pymupdf I made this to print out lecture notes for my open book exam disclaimer) Gemini wrote this for me

Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "88a174e3-398e-4521-9952-a4f0b96ee123",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1. PDF 파일 읽는 중...\n",
"총 544 페이지를 찾았습니다.\n",
"페이지를 10개 단위로 나누어 총 55개의 새 페이지를 생성합니다.\n",
"✅ 성공! 'output.pdf' 파일이 생성되었습니다.\n"
]
}
],
"source": [
"import fitz # PyMuPDF\n",
"import os\n",
"import math\n",
"\n",
"# --- 설정 변수 (이 부분을 수정하여 그리드 및 여백을 조절할 수 있습니다) ---\n",
"\n",
"# 1. 입력 및 출력 경로 설정\n",
"ASSETS_DIR = \"./assets\" # PDF 파일이 있는 폴더\n",
"OUTPUT_FILENAME = \"output.pdf\" # 결과물 파일 이름\n",
"\n",
"# 2. 그리드 레이아웃 설정\n",
"GRID_W = 2 # 가로에 배치할 페이지 수\n",
"GRID_H = 5 # 세로에 배치할 페이지 수\n",
"\n",
"# 3. 페이지 크기 및 여백 설정 (단위: mm)\n",
"PAGE_WIDTH_MM = 210 # A4 가로\n",
"PAGE_HEIGHT_MM = 297 # A4 세로\n",
"MARGIN_TOP_MM = 20\n",
"MARGIN_BOTTOM_MM = 20\n",
"MARGIN_LEFT_MM = 20\n",
"MARGIN_RIGHT_MM = 20\n",
"\n",
"# --- 스크립트 본문 ---\n",
"\n",
"def mm_to_points(mm):\n",
" \"\"\"밀리미터를 포인트 단위로 변환합니다. (1 inch = 72 points, 1 inch = 25.4 mm)\"\"\"\n",
" return mm / 25.4 * 72\n",
"\n",
"def get_pdf_files(directory):\n",
" \"\"\"지정된 디렉토리에서 PDF 파일 목록을 가져옵니다.\"\"\"\n",
" pdf_files = []\n",
" if not os.path.isdir(directory):\n",
" print(f\"오류: '{directory}' 디렉토리를 찾을 수 없습니다.\")\n",
" return pdf_files\n",
" \n",
" for filename in sorted(os.listdir(directory)):\n",
" if filename.lower().endswith(\".pdf\"):\n",
" pdf_files.append(os.path.join(directory, filename))\n",
" return pdf_files\n",
"\n",
"def main():\n",
" \"\"\"메인 실행 함수\"\"\"\n",
" # 1. ./assets 안의 모든 PDF 페이지를 순서대로 읽어오기\n",
" print(\"1. PDF 파일 읽는 중...\")\n",
" source_docs = []\n",
" all_pages = []\n",
" pdf_files = get_pdf_files(ASSETS_DIR)\n",
"\n",
" if not pdf_files:\n",
" print(f\"'{ASSETS_DIR}' 디렉토리에서 PDF 파일을 찾을 수 없습니다. 작업을 중단합니다.\")\n",
" return\n",
"\n",
" try:\n",
" for filepath in pdf_files:\n",
" doc = fitz.open(filepath)\n",
" source_docs.append(doc) # 나중에 닫아주기 위해 문서 객체 저장\n",
" all_pages.extend(range(doc.page_count)) # 페이지 번호만 저장\n",
" except Exception as e:\n",
" print(f\"PDF 파일을 읽는 중 오류가 발생했습니다: {e}\")\n",
" return\n",
" \n",
" print(f\"총 {len(all_pages)} 페이지를 찾았습니다.\")\n",
"\n",
" # 2. 페이지를 n개 단위로 청킹 (n = GRID_W * GRID_H)\n",
" pages_per_sheet = GRID_W * GRID_H\n",
" page_chunks = [\n",
" all_pages[i : i + pages_per_sheet]\n",
" for i in range(0, len(all_pages), pages_per_sheet)\n",
" ]\n",
" print(f\"페이지를 {pages_per_sheet}개 단위로 나누어 총 {len(page_chunks)}개의 새 페이지를 생성합니다.\")\n",
"\n",
" # 3. 각 청크별로 새 페이지 생성 및 배치\n",
" output_doc = fitz.open() # 최종 결과물 PDF 문서 생성\n",
"\n",
" # mm 단위를 포인트로 변환\n",
" page_width_pt = mm_to_points(PAGE_WIDTH_MM)\n",
" page_height_pt = mm_to_points(PAGE_HEIGHT_MM)\n",
" margin_top_pt = mm_to_points(MARGIN_TOP_MM)\n",
" margin_bottom_pt = mm_to_points(MARGIN_BOTTOM_MM)\n",
" margin_left_pt = mm_to_points(MARGIN_LEFT_MM)\n",
" margin_right_pt = mm_to_points(MARGIN_RIGHT_MM)\n",
"\n",
" # 실제 내용이 들어갈 영역 계산\n",
" content_width = page_width_pt - margin_left_pt - margin_right_pt\n",
" content_height = page_height_pt - margin_top_pt - margin_bottom_pt\n",
" \n",
" # 그리드 한 칸의 크기\n",
" cell_width = content_width / GRID_W\n",
" cell_height = content_height / GRID_H\n",
" \n",
" current_page_index_in_all = 0\n",
" \n",
" for chunk in page_chunks:\n",
" # A4 크기의 새 페이지 생성\n",
" new_page = output_doc.new_page(width=page_width_pt, height=page_height_pt)\n",
"\n",
" for i, page_num_in_doc in enumerate(chunk):\n",
" # 현재 페이지 번호가 어떤 문서에 속하는지 찾아야 함\n",
" temp_index = current_page_index_in_all\n",
" source_doc_for_page = None\n",
" page_index_in_source = -1\n",
"\n",
" # 올바른 문서와 해당 문서 내의 페이지 인덱스 찾기\n",
" doc_page_counter = 0\n",
" for doc in source_docs:\n",
" if temp_index < doc_page_counter + doc.page_count:\n",
" source_doc_for_page = doc\n",
" page_index_in_source = temp_index - doc_page_counter\n",
" break\n",
" doc_page_counter += doc.page_count\n",
"\n",
" if not source_doc_for_page or page_index_in_source == -1:\n",
" continue # 혹시 모를 오류 방지\n",
"\n",
" source_page = source_doc_for_page.load_page(page_index_in_source)\n",
"\n",
" # 그리드 내 위치 계산 (0,0), (0,1), (1,0) ...\n",
" row = i // GRID_W\n",
" col = i % GRID_W\n",
"\n",
" # 페이지를 배치할 사각형(Rect) 영역 계산\n",
" x0 = margin_left_pt + col * cell_width\n",
" y0 = margin_top_pt + row * cell_height\n",
" x1 = x0 + cell_width\n",
" y1 = y0 + cell_height\n",
" target_rect = fitz.Rect(x0, y0, x1, y1)\n",
"\n",
" # 새 페이지에 원본 페이지 내용을 그리기\n",
" # xref=0으로 설정하여 외부 파일 참조 없이 내용을 직접 복사\n",
" new_page.show_pdf_page(target_rect, source_doc_for_page, page_index_in_source)\n",
" current_page_index_in_all +=1\n",
"\n",
" # 4. 최종 PDF 파일로 저장\n",
" try:\n",
" output_doc.save(OUTPUT_FILENAME, garbage=4, deflate=True, clean=True)\n",
" print(f\"✅ 성공! '{OUTPUT_FILENAME}' 파일이 생성되었습니다.\")\n",
" except Exception as e:\n",
" print(f\"PDF 파일을 저장하는 중 오류가 발생했습니다: {e}\")\n",
" finally:\n",
" # 모든 문서 객체 닫기\n",
" output_doc.close()\n",
" for doc in source_docs:\n",
" doc.close()\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5193a6fb-a100-458c-8eb9-2fb0e817ece0",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1758198701,
"narHash": "sha256-7To75JlpekfUmdkUZewnT6MoBANS0XVypW6kjUOXQwc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0147c2f1d54b30b5dd6d4a8c8542e8d7edf93b5d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"systems": "systems"
}
},
"systems": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
{
inputs = {
nixpkgs = {
url = "github:nixos/nixpkgs/nixos-unstable";
};
flake-utils = {
url = "github:numtide/flake-utils";
inputs.systems.follows = "systems";
};
systems = {
url = "github:nix-systems/default-linux";
};
};
outputs = { nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
(python3.withPackages (ps: with ps; [
jupyter
jupyterlab
# pypdf
# pypdfium2
pymupdf
]))
];
};
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment