내 앱에 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 } ]
}
네 단계
mpdf.json이라는 첨부파일을 찾습니다. 없으면 그냥 PDF이니 아무것도 하지 않습니다.- JSON을 읽어 트랙 목록을 보여 줍니다. 모르는 필드와
src형식은 무시합니다. - 재생:
youtube:ID는 공식 유튜브 플레이어로(화면에 보이게),file:이름은 같은 이름의 내장 파일로,https:는 URL로. - 페이지가 바뀌면 그 페이지의 큐가 있을 때 그 트랙의
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
JavaScript와 Python 레퍼런스 라이브러리(각 80줄 안팎, MIT)는 검증까지 포함해 양방향을 다 하고, 샘플 파일이 모든 경우를 담고 있습니다. 지원 등급: Level 0 PDF로 열림(이미 됨), Level 1 트랙 재생, Level 2 페이지 큐 따라가기와 .mpdf 확장자 등록.
질문이 있거나 만든 뷰어를 이 사이트에 올리고 싶으면 GitHub 이슈로 알려 주세요.