2. 文档类型

← 语言 · 目录 · 下一章:text::

每个脚本都会自动获得 doc 变量,代表正在分析的 PDF。从它可以访问页面、 字体和图像。


2.1 doc — 文档

属性 类型 含义
doc.page_count 数字 页数
doc.title 文本 元数据中的标题(缺失时为空)
doc.author 文本 元数据中的作者(缺失时为空)
doc.filename 文本 被分析文件的名称
doc.pages 列表 所有页面
doc.fonts 列表 使用的所有字体
doc.images 列表 所有页面上的全部图像

方法:doc.extract_text() — 整个文档的文本,页面之间以换行分隔。

check "Document properties" {
  print("file:", doc.filename)
  print("pages:", doc.page_count)

  // 这些集合就是普通列表 — 所有列表方法都适用
  require doc.pages.length == doc.page_count
  require doc.fonts.length > 0

  text = doc.extract_text()
  assert text.trim() != "", "PDF has no extractable text (images only?)"
  print("total characters:", text.length)
}

2.2 page — 页面

页面来自 doc.pages(在块中)或 page 变量(在 rule 中)。

属性 类型 含义
page.number 数字 页码,从 1 开始
page.index 数字 索引,从 0 开始
page.width / page.height 数字 宽 / 高(点)
page.images 列表 本页的图像
page.tac 数字 估算的最大油墨总量(%)
page.ink_coverage 数字 估算的平均油墨量(%)
page.min_stroke_width 数字/null 最细线宽(pt);没有线条时为 null
page.has_media_box 布尔 has_crop_boxhas_trim_boxhas_bleed_boxhas_art_box

方法:page.extract_text() — 仅本页的文本。

check "Page format" {
  doc.pages.each { |page|
    // number 是人看的页码,index 用于内部计算
    assert page.width > 100mm,
      "page #{page.number} is too narrow: #{page.width}pt"

    // 页面框:印刷必需
    assert page.has_trim_box, "page #{page.number} has no TrimBox (trim area)"
    assert page.has_bleed_box, "page #{page.number} has no BleedBox (bleed area)"

    assert page.tac <= 300,
      "page #{page.number}: #{page.tac}% ink (limit 300%)"

    // min_stroke_width 可能为 null(本页没有线条)。
    // null 为假,所以这样写是安全的:
    assert !page.min_stroke_width || page.min_stroke_width >= 0.25,
      "page #{page.number} has a hairline stroke"
  }
}

check "Blank pages" {
  blank = doc.pages.filter { |p| p.extract_text().trim() == "" }
  assert blank.length == 0,
    "#{blank.length} blank page(s): #{blank.map { |p| p.number }.join(", ")}"
}

2.3 font — 字体

来自 doc.fonts。属性:font.name(名称)、font.is_embedded(是否嵌入)。

check "Embedded fonts" {
  // 未嵌入的字体会被阅读器替换 — 文字外观随之改变
  doc.fonts.each { |font|
    assert font.is_embedded,
      "font '#{font.name}' is not embedded in the PDF"
  }
  print("fonts in use:", doc.fonts.map { |f| f.name }.join(", "))
}

2.4 image — 图像

来自 doc.images(全部)或 page.images(单页)。

属性 含义
image.width / image.height 宽 / 高(像素
image.dpi 有效分辨率(dpi_x 与 dpi_y 中较小者)
image.dpi_x / image.dpi_y 水平 / 垂直有效分辨率
image.color_space DeviceRGBDeviceCMYKIndexed……
image.page_number 所在页码(从 1 开始)
image.bits_per_pixel 位深

DPI 是有效值,按「像素数 ÷ 页面上的印刷尺寸」计算,而不是元数据里的 标称值。这才是影响印刷质量的数字:一张 1000 px 的图被拉伸到 20 cm, 无论元数据怎么写,DPI 都很低。

profile "images-for-offset" {
  const MIN_DPI = 300

  check "Resolution" {
    doc.images.each { |img|
      assert img.dpi >= MIN_DPI,
        "image #{img.width}x#{img.height}px on page #{img.page_number}: #{img.dpi} DPI (minimum #{MIN_DPI})"
    }
  }

  check "Color space" {
    // 胶印使用 CMYK;RGB 需要转换
    doc.images.each { |img|
      assert img.color_space != "DeviceRGB",
        "RGB image on page #{img.page_number} — convert to CMYK"
    }
  }

  check "Images per page" {
    doc.pages.each { |page|
      print("page", page.number, "has", page.images.length, "image(s)")
    }
  }
}

2.5 region — 页面区域

区域用矩形界定页面的一部分,便于校验页脚、页眉、条码区、药品警示带等。

创建:region(x, y, 宽, 高 [, "名称"]),原点 (0,0) 与 PDF 一致,位于左下角。

属性 含义 方法 功能
region.name 创建时给的名称 contains_point(x, y) 点是否在内部
region.x / region.y 左下角坐标 intersects(other) 两个区域是否重叠
region.width / region.height 尺寸 expand(pt) 各边扩大后的新区域
region.right / region.top 右边 / 上边(计算值) inset(pt) 各边缩小后的新区域
region.area 面积(平方点) export_coordinates() [x0, y0, x1, y1]
check "Working with regions" {
  footer = region(0, 0, 595, 60, "footer")

  require footer.name == "footer"
  require footer.top == 60.0
  require footer.right == 595.0
  require footer.area == 35700.0
  require footer.contains_point(300, 30)
  require !footer.contains_point(300, 500)

  // 重叠检测:可用于发现元素侵入保留区
  header = region(0, 780, 595, 62)
  require !footer.intersects(header)

  // expand/inset 返回「新的」区域(原区域不变)
  require footer.expand(5mm).area > footer.area
  require footer.inset(3mm).area < footer.area
}

profile "medicine-label" {
  check "Prescription band" {
    // 警示带必须位于上方并包含法定文字
    band = region(0, 700, 595, 142, "band")
    assert text::extract_from_region(1, band).contains("PRESCRIPTION ONLY"),
      "band is missing the mandatory text"
  }

  check "Ink in the fold area" {
    // 折线处油墨过多会在加工时开裂
    fold = region(290, 0, 15, 842, "center fold")
    measured = prepress::calculate_tac_by_region(1, fold)
    assert measured.first() < 240,
      "too much ink on the fold: #{measured.first()}%"
  }

  check "Barcode in the right place" {
    code_area = region(400, 20, 180, 80, "barcode area")
    assert codes::validate_barcode_position(code_area),
      "barcode outside the reserved area"
  }
}

← 语言 · 目录 · 下一章:text::