NaN/NaN/NaN

PDFを「生成する」だけでは終わらないケースは多くあります。生成した複数のPDFを1つにまとめたい、特定ページだけ抜き出したい、メールに添付するのでファイルサイズを小さくしたい――こうしたPDF後処理の自動化が、業務システムの完成度を左右します。

この記事では、PDF操作の主要な6つのシナリオ(結合・分割・圧縮・透かし・ページ番号・ヘッダー/フッター)を取り上げ、APIアプローチとライブラリアプローチを比較しながら実装方法を解説します。

基本的なPDF生成についてはHTML to PDF変換 完全ガイドを、請求書の自動化については請求書PDF自動生成ガイドを参照してください。

PDF操作の全体像

PDF後処理のニーズは、大きく次のカテゴリに分類できます。

PDF後処理の種類
├── 構造操作
│   ├── 結合(複数PDFを1つに)
│   └── 分割(1つのPDFを複数に)
├── 品質・サイズ最適化
│   └── 圧縮(ファイルサイズ削減)
├── コンテンツ追加
│   ├── 透かし(ウォーターマーク)
│   ├── ページ番号
│   └── ヘッダー/フッター
└── セキュリティ
    ├── パスワード保護
    └── 権限制御

各操作をサーバー側で実装する方法には、ライブラリを使う方法APIを呼ぶ方法の2択があります。ライブラリは細かい制御が可能ですが、依存関係の管理・メモリ消費・言語縛りという課題があります。APIアプローチはインフラ不要でどの言語からも同じように呼び出せます。

FUNBREW PDFはHTMLからのPDF生成と後処理をワンストップで提供します。プレイグラウンドでブラウザから試せます。


1. PDF結合

ユースケース

  • 請求書 + 利用規約 + 支払い確認書を1つのPDFにまとめる
  • 月次レポートの各章(別々に生成)を結合して配信
  • 申込書類のセット(申込書・同意書・案内書)を結合

HTMLテンプレートで「最初から1つのPDF」として生成する

PDFを後から結合するより、最初から1つのHTMLとして設計する方がシンプルです。CSS の page-break-before を使うと、各セクションを別ページに分けられます。

<!DOCTYPE html>
<html>
<head>
<style>
  .page { page-break-before: always; }
  .invoice-section { ... }
  .terms-section { ... }
</style>
</head>
<body>
  <!-- 請求書(1ページ目) -->
  <div class="invoice-section">
    <h1>請求書</h1>
    <p>請求書番号: INV-2026-001</p>
    <!-- ... -->
  </div>

  <!-- 利用規約(2ページ目以降) -->
  <div class="page terms-section">
    <h1>利用規約</h1>
    <p>第1条...</p>
  </div>
</body>
</html>
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<html>...(上記HTML)...</html>",
    "options": { "format": "A4" }
  }' \
  -o combined-document.pdf

Python での実装例

import requests

API_KEY = "YOUR_API_KEY"
API_URL = "https://pdf.funbrew.cloud/api/v1/generate"

# 請求書HTMLと利用規約HTMLを結合して1リクエストで生成
invoice_html = """
<div class="invoice">
  <h1>請求書</h1>
  <table>
    <tr><th>品目</th><th>金額</th></tr>
    <tr><td>API利用料</td><td>¥10,000</td></tr>
  </table>
</div>
"""

terms_html = """
<div style="page-break-before: always;">
  <h1>利用規約</h1>
  <p>第1条(利用規約の適用)...</p>
</div>
"""

combined_html = f"""<!DOCTYPE html>
<html>
<head>
<style>
  body {{ font-family: sans-serif; margin: 40px; }}
  h1 {{ color: #333; }}
  table {{ width: 100%; border-collapse: collapse; }}
  th, td {{ border: 1px solid #ccc; padding: 8px; }}
</style>
</head>
<body>
{invoice_html}
{terms_html}
</body>
</html>"""

response = requests.post(
    API_URL,
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"html": combined_html, "options": {"format": "A4"}},
)

with open("invoice-with-terms.pdf", "wb") as f:
    f.write(response.content)

print("PDF生成完了: invoice-with-terms.pdf")

Node.js での実装例

const fs = require("fs");

const API_KEY = "YOUR_API_KEY";
const API_URL = "https://pdf.funbrew.cloud/api/v1/generate";

async function combinePDFs(sections) {
  // 各セクションのHTMLを結合(最初以外にpage-break-beforeを追加)
  const combinedHtml = sections
    .map((section, index) => {
      const style =
        index > 0 ? 'style="page-break-before: always;"' : "";
      return `<div ${style}>${section.html}</div>`;
    })
    .join("\n");

  const fullHtml = `<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: sans-serif; margin: 40px; }
</style>
</head>
<body>${combinedHtml}</body>
</html>`;

  const response = await fetch(API_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html: fullHtml, options: { format: "A4" } }),
  });

  return Buffer.from(await response.arrayBuffer());
}

// 使用例
const sections = [
  { html: "<h1>請求書</h1><p>金額: ¥10,000</p>" },
  { html: "<h1>利用規約</h1><p>第1条...</p>" },
  { html: "<h1>支払い確認</h1><p>受領しました。</p>" },
];

combinePDFs(sections).then((pdfBuffer) => {
  fs.writeFileSync("combined.pdf", pdfBuffer);
  console.log("結合PDF生成完了");
});

2. PDF分割

ユースケース

  • 月次まとめレポートから特定月のページだけ抽出
  • 一括生成した申請書セットを個人ごとのPDFに分割して送付
  • 大容量PDFをページ範囲で分割してダウンロードさせる

HTMLテンプレートで「最初からページ単位」に設計する

後から分割するより、最初から1ページ単位のHTMLとして生成する方が確実です。

import requests

API_KEY = "YOUR_API_KEY"
API_URL = "https://pdf.funbrew.cloud/api/v1/generate"

def generate_single_page_pdf(data, template_fn):
    """1件分のデータから1ページのPDFを生成"""
    html = template_fn(data)
    response = requests.post(
        API_URL,
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "html": html,
            "options": {
                "format": "A4",
                "margin": {"top": "20mm", "bottom": "20mm",
                           "left": "15mm", "right": "15mm"}
            }
        }
    )
    return response.content

def invoice_template(data):
    return f"""<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; margin: 40px;">
  <h1>請求書</h1>
  <p>宛先: {data['customer_name']}</p>
  <p>請求番号: {data['invoice_no']}</p>
  <p>金額: ¥{data['amount']:,}</p>
</body>
</html>"""

# 顧客リストから個別PDFを生成(並行処理版は pdf-api-batch-processing を参照)
customers = [
    {"customer_name": "株式会社A", "invoice_no": "INV-001", "amount": 50000},
    {"customer_name": "株式会社B", "invoice_no": "INV-002", "amount": 80000},
    {"customer_name": "株式会社C", "invoice_no": "INV-003", "amount": 30000},
]

for customer in customers:
    pdf = generate_single_page_pdf(customer, invoice_template)
    filename = f"invoice_{customer['invoice_no']}.pdf"
    with open(filename, "wb") as f:
        f.write(pdf)
    print(f"生成: {filename}")

大量の個別PDF生成にはバッチ処理ガイドも参照してください。


3. PDF圧縮

ファイルサイズが大きくなる原因

PDFのファイルサイズが大きくなる主な原因は以下の3つです。

原因 対策
高解像度画像の埋め込み 画像を事前に圧縮・リサイズ
フォントの完全埋め込み サブセット化(使用文字のみ埋め込む)
未圧縮のコンテンツストリーム ストリーム圧縮を有効化

HTMLレベルでの最適化

FUNBREW PDF API に渡すHTML自体を最適化することで、生成されるPDFのサイズを大幅に削減できます。

<!DOCTYPE html>
<html>
<head>
<style>
  /* フォントのサブセット化: Google Fonts は display=swap で軽量に */
  @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap');

  body {
    font-family: 'Noto Sans JP', sans-serif;
    /* 印刷向けの軽量スタイル */
    background: white;
    color: black;
  }

  /* 不要なシャドウ・グラデーションを避ける */
  .card {
    border: 1px solid #ccc;  /* box-shadow の代わりにborderを使用 */
    /* background: linear-gradient(...) は避ける */
  }
</style>
</head>
<body>
  <!-- 画像は適切なサイズに圧縮してから埋め込む -->
  <!-- NG: <img src="original-4k-photo.jpg"> -->
  <!-- OK: <img src="compressed-800px.jpg" width="400"> -->
  <img src="logo-compressed.png" width="200" height="60" alt="ロゴ">
</body>
</html>

画像の事前圧縮(Python)

from PIL import Image
import io
import base64

def compress_image_to_base64(image_path, max_width=800, quality=85):
    """画像を圧縮してBase64文字列に変換(HTML埋め込み用)"""
    with Image.open(image_path) as img:
        # リサイズ
        if img.width > max_width:
            ratio = max_width / img.width
            new_height = int(img.height * ratio)
            img = img.resize((max_width, new_height), Image.LANCZOS)

        # JPEG圧縮
        buffer = io.BytesIO()
        img.convert("RGB").save(buffer, format="JPEG", quality=quality, optimize=True)
        buffer.seek(0)

        # Base64エンコード
        b64 = base64.b64encode(buffer.read()).decode("utf-8")
        return f"data:image/jpeg;base64,{b64}"

# 使用例
compressed_logo = compress_image_to_base64("logo.png", max_width=400, quality=90)
html = f"""
<html>
<body>
  <img src="{compressed_logo}" width="200" alt="ロゴ">
  <h1>レポート</h1>
</body>
</html>
"""

Node.js での画像最適化

const sharp = require("sharp");
const fs = require("fs");

async function compressImageToBase64(imagePath, maxWidth = 800, quality = 85) {
  const buffer = await sharp(imagePath)
    .resize({ width: maxWidth, withoutEnlargement: true })
    .jpeg({ quality, progressive: true })
    .toBuffer();

  return `data:image/jpeg;base64,${buffer.toString("base64")}`;
}

async function generateCompactPDF(data) {
  const logoBase64 = await compressImageToBase64("logo.png", 400, 90);

  const html = `<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; margin: 40px;">
  <img src="${logoBase64}" width="200" alt="ロゴ">
  <h1>${data.title}</h1>
  <p>${data.content}</p>
</body>
</html>`;

  const response = await fetch("https://pdf.funbrew.cloud/api/v1/generate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4" } }),
  });

  return Buffer.from(await response.arrayBuffer());
}

4. 透かし(ウォーターマーク)の追加

透かしはCSSで実装できます。position: fixedz-index を組み合わせることで、全ページに透かしを表示できます。

対角線テキスト透かし

<!DOCTYPE html>
<html>
<head>
<style>
  /* 全ページに表示される透かし */
  .watermark {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(-45deg);
    font-size: 80px;
    font-weight: bold;
    color: rgba(200, 200, 200, 0.3);
    z-index: 1000;
    pointer-events: none;
    white-space: nowrap;
    user-select: none;
  }

  /* コンテンツ */
  .content {
    position: relative;
    z-index: 1;
    margin: 40px;
  }
</style>
</head>
<body>
  <div class="watermark">DRAFT / 下書き</div>

  <div class="content">
    <h1>契約書</h1>
    <p>この文書は下書きです。最終版ではありません。</p>
    <!-- 本文コンテンツ -->
  </div>
</body>
</html>

機密情報の透かし(ユーザー名入り)

import requests
from datetime import datetime

API_KEY = "YOUR_API_KEY"

def generate_confidential_pdf(content_html, user_name, document_id):
    """機密文書にユーザー名と日時の透かしを入れて生成"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")

    watermark_html = f"""
    <div style="
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 9999;
      pointer-events: none;
      overflow: hidden;
    ">
      <!-- 対角線透かし(繰り返し) -->
      {''.join([
        f'''<div style="
          position: absolute;
          top: {i * 200}px;
          left: -100px;
          width: 150%;
          transform: rotate(-30deg);
          font-size: 14px;
          color: rgba(150, 150, 150, 0.2);
          white-space: nowrap;
          letter-spacing: 10px;
        ">{user_name} • {timestamp} • {document_id}</div>'''
        for i in range(-2, 10)
      ])}
    </div>
    """

    full_html = f"""<!DOCTYPE html>
<html>
<head>
<style>
  body {{ font-family: sans-serif; margin: 40px; }}
  .page-break {{ page-break-before: always; }}
</style>
</head>
<body>
  {watermark_html}
  <div style="position: relative; z-index: 1;">
    {content_html}
  </div>
</body>
</html>"""

    response = requests.post(
        "https://pdf.funbrew.cloud/api/v1/generate",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"html": full_html, "options": {"format": "A4"}},
    )
    return response.content

# 使用例
pdf = generate_confidential_pdf(
    content_html="<h1>機密レポート</h1><p>...</p>",
    user_name="田中太郎",
    document_id="DOC-2026-001"
)
with open("confidential.pdf", "wb") as f:
    f.write(pdf)

画像透かし(ロゴ)

<style>
  .logo-watermark {
    position: fixed;
    bottom: 30px;
    right: 30px;
    opacity: 0.15;
    z-index: 1000;
  }
</style>

<img class="logo-watermark" src="data:image/png;base64,..." width="100" alt="">

5. ページ番号・ヘッダー/フッター

CSS @page ルールによる実装

CSSの @page ルールと content プロパティを使うと、各ページに自動でページ番号を追加できます。

<!DOCTYPE html>
<html>
<head>
<style>
  /* ページ設定 */
  @page {
    size: A4;
    margin: 25mm 20mm 30mm 20mm;

    /* ヘッダー */
    @top-center {
      content: "月次レポート 2026年4月";
      font-size: 10px;
      color: #666;
    }

    /* フッター(ページ番号付き) */
    @bottom-center {
      content: counter(page) " / " counter(pages);
      font-size: 10px;
      color: #666;
    }

    /* フッター左(会社名) */
    @bottom-left {
      content: "株式会社FUNBREW";
      font-size: 10px;
      color: #999;
    }

    /* フッター右(日付) */
    @bottom-right {
      content: "2026-04-06";
      font-size: 10px;
      color: #999;
    }
  }

  /* 最初のページのみ異なるヘッダー */
  @page :first {
    @top-center {
      content: "";
    }
  }

  body {
    font-family: sans-serif;
    font-size: 11pt;
    line-height: 1.6;
  }
</style>
</head>
<body>
  <h1>月次レポート</h1>
  <p>レポートの本文...</p>

  <div style="page-break-before: always;">
    <h2>第2章: データ分析</h2>
    <p>...</p>
  </div>
</body>
</html>

JavaScript でのヘッダー/フッター動的生成

async function generateReport(reportData) {
  const { title, author, date, sections } = reportData;

  // セクションのHTMLを生成
  const sectionsHtml = sections
    .map(
      (section, i) => `
    <div ${i > 0 ? 'style="page-break-before: always;"' : ""}>
      <h2>${section.title}</h2>
      ${section.content}
    </div>
  `
    )
    .join("");

  const html = `<!DOCTYPE html>
<html>
<head>
<style>
  @page {
    size: A4;
    margin: 25mm 20mm 30mm 20mm;
    @top-left { content: "${title}"; font-size: 9px; color: #666; }
    @top-right { content: "${author}"; font-size: 9px; color: #666; }
    @bottom-center {
      content: "- " counter(page) " -";
      font-size: 9px;
      color: #999;
    }
    @bottom-right { content: "${date}"; font-size: 9px; color: #999; }
  }
  @page :first {
    @top-left { content: ""; }
    @top-right { content: ""; }
  }
  body { font-family: sans-serif; font-size: 11pt; line-height: 1.7; }
  h1 { text-align: center; margin-bottom: 50px; }
  h2 { color: #333; border-bottom: 2px solid #333; padding-bottom: 5px; }
</style>
</head>
<body>
  <!-- 表紙(最初のページ) -->
  <div style="text-align: center; padding-top: 100px;">
    <h1>${title}</h1>
    <p style="color: #666;">${author} | ${date}</p>
  </div>

  <!-- 各セクション -->
  ${sectionsHtml}
</body>
</html>`;

  const response = await fetch("https://pdf.funbrew.cloud/api/v1/generate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4" } }),
  });

  return Buffer.from(await response.arrayBuffer());
}

// 使用例
const report = await generateReport({
  title: "2026年第1四半期 事業レポート",
  author: "事業企画部",
  date: "2026年4月6日",
  sections: [
    { title: "エグゼクティブサマリー", content: "<p>...</p>" },
    { title: "売上実績", content: "<table>...</table>" },
    { title: "来期の計画", content: "<p>...</p>" },
  ],
});

6. FUNBREW PDF APIでの実装まとめ

curl による基本呼び出し

# 基本的なPDF生成
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!DOCTYPE html><html><body><h1>テスト</h1></body></html>",
    "options": {
      "format": "A4",
      "margin": {
        "top": "20mm",
        "bottom": "20mm",
        "left": "15mm",
        "right": "15mm"
      }
    }
  }' \
  --output output.pdf

# 横向き(ランドスケープ)
curl -X POST https://pdf.funbrew.cloud/api/v1/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "...",
    "options": { "format": "A4", "landscape": true }
  }' \
  --output landscape.pdf

エラーハンドリング付き Python クライアント

import requests
import time
from typing import Optional

class FunbrewPDFClient:
    """FUNBREW PDF APIクライアント(リトライ・エラーハンドリング付き)"""

    BASE_URL = "https://pdf.funbrew.cloud/api/v1"

    def __init__(self, api_key: str, max_retries: int = 3):
        self.api_key = api_key
        self.max_retries = max_retries
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})

    def generate(
        self,
        html: str,
        format: str = "A4",
        landscape: bool = False,
        margin: Optional[dict] = None,
    ) -> bytes:
        """PDFを生成して返す"""
        payload = {
            "html": html,
            "options": {
                "format": format,
                "landscape": landscape,
                "margin": margin or {
                    "top": "20mm", "bottom": "20mm",
                    "left": "15mm", "right": "15mm"
                },
            },
        }

        for attempt in range(self.max_retries):
            try:
                response = self.session.post(
                    f"{self.BASE_URL}/generate",
                    json=payload,
                    timeout=60,
                )
                response.raise_for_status()
                return response.content

            except requests.HTTPError as e:
                if e.response.status_code == 429:  # Rate limit
                    wait = 2 ** attempt
                    print(f"レート制限。{wait}秒後にリトライ...")
                    time.sleep(wait)
                    continue
                raise
            except requests.Timeout:
                if attempt < self.max_retries - 1:
                    print(f"タイムアウト。リトライ中... ({attempt + 1}/{self.max_retries})")
                    continue
                raise

        raise RuntimeError("最大リトライ回数を超えました")

# 使用例
client = FunbrewPDFClient(api_key="YOUR_API_KEY")

# 透かし付き請求書
html = """<!DOCTYPE html>
<html>
<head>
<style>
  .watermark {
    position: fixed; top: 50%; left: 50%;
    transform: translate(-50%, -50%) rotate(-45deg);
    font-size: 60px; color: rgba(255,0,0,0.15);
    z-index: 1000; pointer-events: none;
  }
</style>
</head>
<body>
  <div class="watermark">DRAFT</div>
  <h1>請求書(下書き)</h1>
  <p>金額: ¥100,000</p>
</body>
</html>"""

pdf = client.generate(html)
with open("invoice-draft.pdf", "wb") as f:
    f.write(pdf)

エラーハンドリングの詳細についてはAPIエラーハンドリングガイドを、本番環境での運用については本番環境運用ガイドを参照してください。


7. ライブラリ比較:PyPDF2・pdf-lib・Ghostscript vs API

主要ライブラリの特徴

ライブラリ 言語 得意な操作 課題
PyPDF2 / pypdf Python 結合・分割・メタデータ HTML→PDF は非対応
pdf-lib JavaScript 結合・テキスト追加・フォーム フォント埋め込みが複雑
Ghostscript CLI 圧縮・変換・高品質出力 インストールが必要、ライセンス注意
pdfkit Python/Node HTML→PDF wkhtmltopdfが必要
FUNBREW PDF API Any 全操作(HTML→PDF + 後処理) 外部API依存

PyPDF2 での結合・分割

# PyPDF2での結合(既存PDFファイルの操作)
from pypdf import PdfWriter, PdfReader

def merge_pdfs(pdf_paths: list[str], output_path: str):
    writer = PdfWriter()
    for path in pdf_paths:
        reader = PdfReader(path)
        for page in reader.pages:
            writer.add_page(page)
    with open(output_path, "wb") as f:
        writer.write(f)

# 分割
def split_pdf(input_path: str, output_dir: str):
    reader = PdfReader(input_path)
    for i, page in enumerate(reader.pages):
        writer = PdfWriter()
        writer.add_page(page)
        with open(f"{output_dir}/page_{i+1}.pdf", "wb") as f:
            writer.write(f)

pdf-lib での透かし追加(JavaScript)

import { PDFDocument, rgb, degrees } from "pdf-lib";
import fontkit from "@pdf-lib/fontkit";

async function addWatermark(pdfBytes, watermarkText) {
  const pdfDoc = await PDFDocument.load(pdfBytes);
  const pages = pdfDoc.getPages();

  for (const page of pages) {
    const { width, height } = page.getSize();
    page.drawText(watermarkText, {
      x: width / 2 - 100,
      y: height / 2,
      size: 50,
      color: rgb(0.8, 0.8, 0.8),
      opacity: 0.3,
      rotate: degrees(-45),
    });
  }

  return pdfDoc.save();
}

Ghostscript での圧縮(CLI)

# Ghostscriptで高圧縮(プリンター品質)
gs -sDEVICE=pdfwrite \
   -dCompatibilityLevel=1.4 \
   -dPDFSETTINGS=/printer \
   -dNOPAUSE -dQUIET -dBATCH \
   -sOutputFile=compressed.pdf \
   input.pdf

# 設定オプション:
# /screen   - 最小サイズ(スクリーン表示向け、72dpi)
# /ebook    - バランス型(150dpi)
# /printer  - 印刷品質(300dpi)
# /prepress - 高品質(300dpi + カラープロファイル保持)

どちらを選ぶか

ライブラリアプローチが向いているケース:

  • 既存PDFファイルの後処理(結合・分割・暗号化)
  • オフライン環境での処理
  • 大量の単純操作(1ページ分割など)

APIアプローチが向いているケース:

  • HTMLから直接生成 + 後処理を一括で行いたい
  • サーバーへの依存ライブラリをなくしたい(DockerイメージをSlimにしたい)
  • 複数言語・マイクロサービスから統一したインターフェースで呼び出したい
  • フォント・CSSの複雑な制御が必要

本番環境でのパフォーマンス最適化については本番環境運用ガイドを、セキュリティ設計についてはセキュリティガイドを参照してください。


まとめ

PDF後処理の6つの主要操作と実装方法をまとめます。

操作 推奨アプローチ ポイント
結合 HTMLで最初から設計 page-break-before: always
分割 最初から1件1ファイルで生成 バッチAPIで並行処理
圧縮 画像の事前最適化 sharp/Pillowで前処理
透かし CSS position: fixed 半透明 + rotate(-45deg)
ページ番号 CSS @page ルール counter(page)
ヘッダー/フッター CSS @page ルール @top-center, @bottom-center

PDF後処理を効率的に自動化するには、「後から操作する」より「最初から目的のPDFを生成する」設計が重要です。FUNBREW PDF APIではプレイグラウンドでCSS効果をその場で確認しながら開発できます。

関連記事

まずは無料プランでAPI操作を試してみてください。ドキュメントにはオプションの全リファレンスが揃っています。

Powered by FUNBREW PDF