コンテンツにスキップ

1. PDFLang 言語

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


// コメントは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 "Ink within limit" tags: ["prepress", "colors"] {
require prepress::validate_tac_limits(300)
}

すべての検証は 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つ失敗しても他は止まりません

Section titled “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 は動き続けます。


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")
}

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

Section titled “真偽値と「真」とみなされる値”
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
}

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 ブロック:各要素に対する繰り返し

Section titled “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 関数:ルールに名前を付ける

Section titled “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:プロファイル間での共有

Section titled “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 で止まることはありません。


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
}

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 受け取る人に役立つメッセージ

Section titled “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
}

メッセージ 原因 対処
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 を同じ行に置くか、中間変数を使う

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

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

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

DigitalOceanこのサイトのホスティングを提供してくださる DigitalOcean に感謝します。