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
Created
December 15, 2025 10:05
-
-
Save HelloWorld017/c61845dccb218ca1ddb9f82d2e97b687 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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 | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| 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