내 앱에 MPDF 적용하기

PDF를 여는 앱이라면 MPDF 파일은 이미 열립니다. 음악까지 나오게 하려면 첨부파일 하나를 읽고 몇 가지 규칙만 따르면 됩니다. 페이지는 바뀌지 않으니 렌더링 코드는 그대로 둡니다.

파일 안에 있는 것

보통 PDF에 mpdf.json이라는 첨부파일(선택적으로 음원 파일)이 들어 있습니다. 이 목록에 트랙과 페이지 큐가 적혀 있습니다.

mpdf.jsonJSON
{
  "mpdf": "1.0",
  "title": "Piano Book 2",
  "tracks": [
    { "id": "t1", "title": "Canon in D", "src": "youtube:dQw4w9WgXcQ" },
    { "id": "t2", "title": "Arirang",    "src": "file:track2.m4a" }
  ],
  "cues": [ { "page": 1, "track": "t1" }, { "page": 4, "track": "t2", "at": 32.5 } ]
}

네 단계

  1. mpdf.json이라는 첨부파일을 찾습니다. 없으면 그냥 PDF이니 아무것도 하지 않습니다.
  2. JSON을 읽어 트랙 목록을 보여 줍니다. 모르는 필드와 src 형식은 무시합니다.
  3. 재생: youtube:ID는 공식 유튜브 플레이어로(화면에 보이게), file:이름은 같은 이름의 내장 파일로, https:는 URL로.
  4. 페이지가 바뀌면 그 페이지의 큐가 있을 때 그 트랙의 at초로 전환하고, 없으면 그대로 이어갑니다.

포맷의 신뢰를 지키는 규칙: 페이지 위에 그리지 않기, 플레이어는 페이지 밖에 두거나 옮기고 접을 수 있는 패널로, 사용자 동작 전 자동재생 금지. 자세한 내용은 스펙(세 쪽, CC BY)에 있습니다.

목록 읽기

JavaScriptpdf.js
// Browser or Node, with pdf.js (any version that has getAttachments)
const pdf = await pdfjsLib.getDocument({ data: bytes }).promise
const att = (await pdf.getAttachments()) || {}
const raw = att['mpdf.json']                       // no attachment -> a plain PDF, nothing to do
if (raw) {
  const manifest = JSON.parse(new TextDecoder().decode(raw.content))
  // manifest.tracks: [{ id, title, src }]   src = "youtube:ID" | "file:name" | "https://..."
  // manifest.cues:   [{ page, track, at }]  when page becomes current, play track from `at` seconds
  // embedded audio:  att[name].content       for src "file:name"
}
Pythonpypdf
# Python 3.8+, pip install pypdf
from pypdf import PdfReader
import json

reader = PdfReader("score.pdf")
att = reader.attachments                           # {name: [bytes]}
if "mpdf.json" in att:
    manifest = json.loads(att["mpdf.json"][0])
    tracks = {t["id"]: t for t in manifest["tracks"]}
    for cue in manifest.get("cues", []):
        print(cue["page"], tracks[cue["track"]]["src"], cue.get("at", 0))
SwiftPDFKit
// iOS / macOS with PDFKit (reference; walks the flat /Names form of the EmbeddedFiles tree)
import PDFKit

func mpdfManifest(_ doc: PDFDocument) -> [String: Any]? {
    guard let cg = doc.documentRef, let catalog = cg.catalog else { return nil }
    var names: CGPDFDictionaryRef?, ef: CGPDFDictionaryRef?, arr: CGPDFArrayRef?
    guard CGPDFDictionaryGetDictionary(catalog, "Names", &names), let names,
          CGPDFDictionaryGetDictionary(names, "EmbeddedFiles", &ef), let ef,
          CGPDFDictionaryGetArray(ef, "Names", &arr), let arr else { return nil }
    for i in stride(from: 0, to: CGPDFArrayGetCount(arr), by: 2) {
        var key: CGPDFStringRef?, spec: CGPDFDictionaryRef?, efd: CGPDFDictionaryRef?, stream: CGPDFStreamRef?
        guard CGPDFArrayGetString(arr, i, &key), let key,
              (CGPDFStringCopyTextString(key) as String?) == "mpdf.json",
              CGPDFArrayGetDictionary(arr, i + 1, &spec), let spec,
              CGPDFDictionaryGetDictionary(spec, "EF", &efd), let efd,
              CGPDFDictionaryGetStream(efd, "F", &stream), let stream else { continue }
        var fmt = CGPDFDataFormat.raw
        guard let data = CGPDFStreamCopyData(stream, &fmt) as Data? else { return nil }
        return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
    }
    return nil
}

파일 만들기

Pythonpypdf
# Producing one: attach the manifest (and any audio) to a normal PDF
from pypdf import PdfWriter
w = PdfWriter(clone_from="score.pdf")
w.add_attachment("mpdf.json", manifest_bytes)
w.add_attachment("track2.m4a", audio_bytes)   # optional
w.write("score.pdf")                            # still a PDF; name it .pdf

JavaScriptPython 레퍼런스 라이브러리(각 80줄 안팎, MIT)는 검증까지 포함해 양방향을 다 하고, 샘플 파일이 모든 경우를 담고 있습니다. 지원 등급: Level 0 PDF로 열림(이미 됨), Level 1 트랙 재생, Level 2 페이지 큐 따라가기와 .mpdf 확장자 등록.

질문이 있거나 만든 뷰어를 이 사이트에 올리고 싶으면 GitHub 이슈로 알려 주세요.