1. PDFLang 言語

← 目次 · 次:ドキュメント型 →

PDFLang はプログラミングをしない人が読めるように設計されています。クラスも 継承も型宣言もセミコロンもありません。スクリプトは、ほぼ自然な言葉で書かれた チェックのリストです。


1.1 スクリプトの構造

// コメントは2つのスラッシュで始まり、行末まで続きます。

profile "profile-name" {         // profile は任意:セットに名前を付けて
                                 // まとめます。名前はレポートに表示されます。

  const LIMIT = 300%             // 定数:慣例として大文字

  check "Check Name" {           // 各 check はレポートの1セクションになります
    require doc.page_count > 0   // 検証1つ
  }

  check "Another Check" {        // check はいくつでも書けます
    require doc.title != ""
  }
}

profile は省略できます — スクリプトは check の列挙だけでも構いません:

check "Simple" {
  require doc.page_count > 0
}

check のタグ

タグはレポート内で check を分類・絞り込むために使います:

check "Ink within limit" tags: ["prepress", "colors"] {
  require prepress::validate_tac_limits(300)
}

1.2 検証の2つの書き方

すべての検証は requireassert を使います。違いは、失敗したときに レポートへ出るメッセージだけです。

check "Comparing both forms" {

  // require: 式そのものからメッセージが生成されます。
  // 失敗するとレポートには次のように出ます:
  //   "requirement not met: doc.page_count > 0"
  require doc.page_count > 0

  // assert: 読み手に見せたいメッセージを自分で書きます。
  // 失敗するとそのまま表示されます:
  //   "PDF has no title in its metadata"
  assert doc.title != "", "PDF has no title in its metadata"
}

使い分けの目安: 式が自明なときは require、レポートを読む人が スクリプトを知らなくても問題を理解できる必要があるときは assert

1つ失敗しても他は止まりません

check "Three independent validations" {
  assert doc.page_count > 100, "too few pages"    // 失敗
  assert doc.title != "", "no title"              // それでも実行される
  assert doc.author != "", "no author"            // これも実行される
}

レポートにはすべての問題が一度に並びます。これは意図的です。ファイルを 返される側は、修正すべき点の完全なリストを求めているからです。

check どうしでも同じです。ある check が実行時エラー(未定義の変数など)に なっても、それは診断として記録され、残りの check は動き続けます。


1.3 値と型

数値と単位

check "Numbers" {
  x = 42          // 整数
  y = 2.5         // 小数

  // 長さの単位は自動的にポイントへ変換されます(1 pt = 1/72 インチ):
  a = 3mm         // 8.5039... pt
  b = 2.5cm       // 70.866... pt
  c = 1in         // 72 pt
  d = 10pt        // 10 pt

  // パーセントは数値をそのまま保ちます:
  limit = 300%    // 300

  require a < b            // すべてポイントなので直接比較できます
  require c == 72.0
  require limit == 300
}

8.504 ではなく 3mm と書けることが要点です。ミリで考える人にとって自然に 読め、変換ミスも起きません。

文字列

check "Strings" {
  simple = "plain text"

  // 補間:#{...} は任意の式の値を埋め込みます
  name = "document.pdf"
  message = "Analyzing #{name} with #{doc.page_count} pages"

  // エスケープ:\n(改行)、\t(タブ)、\"(引用符)、\\(バックスラッシュ)
  quoted = "he said \"hello\""

  // 未知のバックスラッシュはそのまま通ります — 正規表現を
  // 二重エスケープなしで書けます:
  pattern = "\d{3}\.\d{3}\.\d{3}-\d{2}"

  require message.contains("pages")
}

真偽値と「真」とみなされる値

check "True and false" {
  yes = true
  no = false

  // 偽なのは false と null だけです。それ以外はすべて真 —
  // 0 も、空文字列も、空リストも真です。
  require 0        // 通ります(0 は真)
  require ""       // 通ります(空文字列は真)

  // したがって内容を確認するには明示的に比較します:
  require doc.title != ""              // 正しい
  require doc.pages.length > 0         // 正しい
}

これは、見つからないときに null を返す関数で効いてきます:

check "Taking advantage of null" {
  description = data::lookup_value("batches.csv", "L2026-08")
  // null は偽なので、そのまま書けます:
  assert description, "batch not found in the table"
}

リスト

check "Lists" {
  numbers = [1, 2, 3]
  words = ["a", "b", "c"]
  mixed = [1, "two", true]

  require numbers.length == 3
  require numbers.contains(2)
  require words.join(", ") == "a, b, c"

  // アクセスは1起点:最初の要素は1番です
  require numbers.get(1) == 1
  require numbers.first() == 1
  require numbers.last() == 3
}

1.4 演算子

check "Operators" {
  // 比較
  require 10 > 5
  require 10 >= 10
  require 3 < 4
  require 3 <= 3
  require "a" == "a"
  require "a" != "b"

  // 算術
  require 2 + 3 == 5
  require 10 - 4 == 6
  require 3 * 4 == 12
  require 10 / 4 == 2.5        // 割り切れない除算は小数になります
  require 10 / 5 == 2          // 割り切れる場合は整数のまま

  // 論理(短絡評価:右辺は必要なときだけ評価されます)
  require true && true
  require false || true
  require !false

  // 短絡評価の実例:ページが無ければ右辺は評価されず、
  // 空のドキュメントでもエラーになりません。
  require doc.page_count == 0 || doc.pages.first().width > 0
}

1.5 ブロック:各要素に対する繰り返し

ブロックは波かっこで囲み、縦棒の間に引数を書きます。「各ページについて〜する」 と読めます。

check "Walking through pages" {

  // each: 各要素についてブロックを実行します
  doc.pages.each { |page|
    assert page.width > 0, "page #{page.number} has no width"
  }

  // each_with_index: 位置(0, 1, 2...)も受け取ります
  doc.fonts.each_with_index { |font, i|
    print("font", i, ":", font.name)
  }

  // all: すべての要素が条件を満たせば真
  require doc.fonts.all { |f| f.is_embedded }

  // any: いずれかの要素が条件を満たせば真
  require doc.pages.any { |p| p.extract_text() != "" }

  // filter: 条件を満たす要素だけを残します
  blank = doc.pages.filter { |p| p.extract_text() == "" }
  assert blank.length == 0,
    "#{blank.length} blank page(s)"

  // map: 各要素を変換して新しいリストにします
  names = doc.fonts.map { |f| f.name }
  print("fonts in use:", names.join(", "))
}

ブロックは連結できます — ただし同じ行に書き、ドットの前で改行しないで ください:

check "Chaining" {
  // 埋め込まれていないフォントの名前だけをカンマで連結
  problems = doc.fonts.filter { |f| !f.is_embedded }.map { |f| f.name }
  assert problems.length == 0,
    "fonts not embedded: #{problems.join(", ")}"
}

行が長くなりすぎる場合は、連結を切るのではなく名前付きの段階に分けます。 そのほうが読みやすくもあります:

check "Named steps" {
  loose = doc.fonts.filter { |f| !f.is_embedded }
  names = loose.map { |f| f.name }
  assert names.length == 0, "fonts not embedded: #{names.join(", ")}"
}

1.6 関数:ルールに名前を付ける

同じ検証が何度も出てくるなら、名前を付けましょう:

// 関数の値は「最後の式」の値です — return はありません。
function is_a4(page) {
  abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}

function exceeds_ink(page, limit) {
  page.tac > limit
}

check "Format and ink" {
  // これで check がほとんど文章のように読めます
  require doc.pages.all { |p| is_a4(p) }

  doc.pages.each { |page|
    assert !exceeds_ink(page, 300), "page #{page.number} has too much ink"
  }
}

関数の決まり:

  • 引数は関数の中だけで有効です。
  • 関数から別の関数を呼べます。
  • 再帰は可能ですが200回までです(暴走したスクリプトがプロセスを止めないため)。

1.7 import:プロファイル間での共有

共通のルールを1つのファイルにまとめ、必要な場所で読み込みます。

library.pdfl:

// チーム内で共有する定数と関数
const OFFSET_TAC = 300%
const DEFAULT_BLEED = 3mm

function a4_page(page) {
  abs(page.width - 595.0) < 5.0 && abs(page.height - 842.0) < 5.0
}

magazine.pdfl:

// パスは「このファイル」からの相対です
import "library.pdfl"

check "Format" {
  // OFFSET_TAC と a4_page は import から来ています
  require doc.pages.all { |p| a4_page(p) }
  require prepress::validate_tac_limits(OFFSET_TAC)
}

同じファイルは一度だけ読み込まれます。複数のスクリプトが読み込んでも、 循環 import で止まることはありません。


1.8 rule:ページごとの検証

rule は各ページに対して1回ずつ実行される check です。ページは page 変数に入っています:

// "on" なし:すべてのページで実行されます
rule "Every page has text" {
  assert page.extract_text().trim() != "",
    "page #{page.number} is blank"
}

on を付けると、対象ページを選べます:

rule "Body pages numbered" on doc.pages.filter { |p| p.number > 2 } {
  footer = region(0, 0, page.width, 60)
  assert text::extract_from_region(page.number, footer) != "",
    "page #{page.number} has no page number in the footer"
}

構文上の注意: on の選択式がプロパティで終わる場合(例:on doc.pages) は、かっこで囲んでください。囲まないと本体の { がそのプロパティ呼び出しの ブロックとして解釈されます:

rule "Example" on (doc.pages) {     // かっこが必要
  require page.width > 0
}

1.9 変数とスコープ

const GLOBAL = 100          // ファイル全体で有効

check "Scope" {
  local = 42                // この check の中だけ

  doc.pages.each { |page|
    inner = page.width      // このブロックの中だけ
    require inner > 0
  }

  require local == 42       // まだ有効
  require GLOBAL == 100     // まだ有効
}

慣例として定数は大文字、変数は小文字です。言語が強制するわけではありませんが、 例と配布プロファイルはこれに従っています。


1.10 受け取る人に役立つメッセージ

レポートの質は、あなたが書くメッセージで決まります。比べてみましょう:

check "Poor messages" {
  require doc.pages.all { |p| p.tac <= 300 }
  // レポート: "requirement not met: doc.pages.all() { ... }"
  // — どのページがどれだけ超えたのか受け取る側にはわかりません
}

check "Good messages" {
  doc.pages.each { |page|
    assert page.tac <= 300,
      "Page #{page.number}: ink coverage #{page.tac}% (max 300%)"
  }
  // レポート: "Page 7: ink coverage 324% (max 300%)"
  // — オペレーターは何を直せばよいか正確にわかります
}

エラーではない補足情報には print() を使います。標準エラー出力に出るので、 レポートを汚しません:

check "Context" {
  print("Analyzing", doc.page_count, "pages")
  print("Fonts:", prepress::list_fonts().join(", "))
  require doc.page_count > 0
}

1.11 よくあるエラー

メッセージ 原因 対処
expected end of line after statement 1行に2つの文 1行に1つの文
unknown variable: x 代入前の使用、またはスコープ外 同じ階層で先に宣言する
unknown function: text::xyz 名前の誤りか存在しない関数 該当する名前空間の章を確認
fix:: is only available in the 'pdfl fix' command pdfl runfix:: を使用 pdfl fix input.pdf script.pdfl --output out.pdf を使う
unknown unit: 'kg' 不正な単位 ptmmcmin% を使う
expected '{' with the rule body on の選択式がプロパティで終わっている 選択式をかっこで囲む
unexpected expression: Dot 連結が複数行に分かれている .method を同じ行に置くか、中間変数を使う

実行前には常にこれを行う価値があります:

pdfl lint my_profile.pdfl    # 未使用変数、重複した check など
pdfl fmt my_profile.pdfl     # 書式を統一

← 目次 · 次:ドキュメント型 →