2. Tipos del documento

← El lenguaje · Índice · Siguiente: text::

Todo script recibe automáticamente la variable doc, que representa el PDF en análisis. A partir de ella llegas a las páginas, las fuentes y las imágenes.


2.1 doc — el documento

Propiedades

Propiedad Tipo Qué es
doc.page_count número Cantidad de páginas
doc.title texto Título de los metadatos (vacío si no existe)
doc.author texto Autor de los metadatos (vacío si no existe)
doc.filename texto Nombre del archivo analizado
doc.pages lista Todas las páginas
doc.fonts lista Todas las fuentes usadas
doc.images lista Todas las imágenes de todas las páginas
check "Propiedades del documento" {
  print("archivo:", doc.filename)
  print("páginas:", doc.page_count)
  print("título:", doc.title)

  // Las colecciones son listas normales: aceptan todos los métodos de lista
  require doc.pages.length == doc.page_count
  require doc.fonts.length > 0
  print("imágenes en todo el documento:", doc.images.length)
}

Métodos

doc.extract_text()

Todo el texto del documento, con las páginas separadas por saltos de línea.

check "Texto del documento" {
  texto = doc.extract_text()
  assert texto.trim() != "", "PDF sin texto extraíble (¿solo imágenes?)"
  require texto.contains("Contrato")
  print("caracteres en total:", texto.length)
}

2.2 page — la página

Las páginas vienen de doc.pages (dentro de bloques) o de la variable page (dentro de una rule).

Propiedades

Propiedad Tipo Qué es
page.number número Número de página, empezando en 1
page.index número Índice de la página, empezando en 0
page.width número Anchura en puntos
page.height número Altura en puntos
page.images lista Imágenes de esta página
page.tac número Cobertura de tinta máxima estimada (%)
page.ink_coverage número Cobertura media de tinta estimada (%)
page.min_stroke_width número/null Grosor de filete mínimo (pt); null si no hay filetes
page.has_media_box booleano Tiene MediaBox definida
page.has_crop_box booleano Tiene CropBox definida
page.has_trim_box booleano Tiene TrimBox definida
page.has_bleed_box booleano Tiene BleedBox definida
page.has_art_box booleano Tiene ArtBox definida
check "Formato de las páginas" {
  doc.pages.each { |page|
    // number es lo que ve el usuario; index sirve para cálculos internos
    assert page.width > 100mm,
      "la página #{page.number} es demasiado estrecha: #{page.width}pt"

    // Cajas: imprescindibles para impresión
    assert page.has_trim_box,
      "la página #{page.number} no tiene TrimBox (área de corte)"
    assert page.has_bleed_box,
      "la página #{page.number} no tiene BleedBox (sangrado)"
  }
}

check "Tinta y filetes" {
  doc.pages.each { |page|
    assert page.tac <= 300,
      "página #{page.number}: #{page.tac}% de tinta (límite 300%)"

    // min_stroke_width puede ser null (página sin filetes);
    // null es falso, así que esta comprobación es segura:
    assert !page.min_stroke_width || page.min_stroke_width >= 0.25,
      "la página #{page.number} tiene un filete demasiado fino"
  }
}

Métodos

page.extract_text()

El texto solo de esta página.

check "Páginas en blanco" {
  blancas = doc.pages.filter { |p| p.extract_text().trim() == "" }
  assert blancas.length == 0,
    "#{blancas.length} página(s) en blanco: #{blancas.map { |p| p.number }.join(", ")}"
}

2.3 font — la fuente

Las fuentes vienen de doc.fonts.

Propiedad Tipo Qué es
font.name texto Nombre de la fuente
font.is_embedded booleano Está incrustada en el archivo
check "Fuentes incrustadas" {
  // Una fuente no incrustada la sustituye el lector: el texto cambia de aspecto
  doc.fonts.each { |font|
    assert font.is_embedded,
      "la fuente '#{font.name}' no está incrustada en el PDF"
  }
}

check "Informe de fuentes" {
  print("fuentes usadas:", doc.fonts.map { |f| f.name }.join(", "))
  faltan = doc.fonts.filter { |f| !f.is_embedded }
  print("no incrustadas:", faltan.length)
}

2.4 image — la imagen

Las imágenes vienen de doc.images (todas) o de page.images (las de una página).

Propiedad Tipo Qué es
image.width número Anchura en píxeles
image.height número Altura en píxeles
image.dpi número Resolución efectiva (la menor entre dpi_x y dpi_y)
image.dpi_x número Resolución horizontal efectiva
image.dpi_y número Resolución vertical efectiva
image.color_space texto DeviceRGB, DeviceCMYK, Indexed...
image.page_number número Página donde aparece (1-based)
image.bits_per_pixel número Bits por píxel

El DPI es el efectivo, calculado como píxeles ÷ tamaño impreso en la página, no el valor nominal grabado en los metadatos. Es el número que importa para la calidad de impresión: una imagen de 1000 px estirada hasta ocupar 20 cm tiene un DPI bajo, aunque los metadatos digan otra cosa.

profile "imagenes-para-offset" {
  const DPI_MINIMO = 300

  check "Resolución" {
    doc.images.each { |img|
      assert img.dpi >= DPI_MINIMO,
        "imagen #{img.width}x#{img.height}px en la página #{img.page_number}: #{img.dpi} DPI (mínimo #{DPI_MINIMO})"
    }
  }

  check "Espacio de color" {
    // La impresión offset trabaja en CMYK; el RGB necesita conversión
    doc.images.each { |img|
      assert img.color_space != "DeviceRGB",
        "imagen RGB en la página #{img.page_number} — convertir a CMYK"
    }
  }

  check "Imágenes por página" {
    doc.pages.each { |page|
      // page.images trae solo las imágenes de esa página
      print("la página", page.number, "tiene", page.images.length, "imagen(es)")
    }
  }
}

2.5 region — área de la página

Las regiones delimitan áreas rectangulares para validar partes concretas de la página: pie, cabecera, área del código de barras, franja de un medicamento.

Creación

// region(x, y, anchura, altura [, "nombre"])
// El origen (0,0) es la esquina INFERIOR izquierda, como en el PDF.
cabecera = region(0, 742, 595, 100, "cabecera")
pie = region(0, 0, 595, 60, "pie")
franja = region(20mm, 250mm, 60mm, 15mm, "franja roja")

Propiedades

Propiedad Qué es
region.name Nombre dado al crearla (vacío si se omite)
region.x / region.y Esquina inferior izquierda
region.width / region.height Dimensiones
region.right / region.top Bordes derecho y superior (calculados)
region.area Área en puntos cuadrados

Métodos

Método Qué hace
region.contains_point(x, y) ¿El punto está dentro?
region.intersects(otra) ¿Las dos regiones se solapan?
region.expand(pt) Nueva región mayor por todos los lados
region.inset(pt) Nueva región menor por todos los lados
region.export_coordinates() [x0, y0, x1, y1]
check "Trabajando con regiones" {
  pie = region(0, 0, 595, 60, "pie")

  require pie.name == "pie"
  require pie.top == 60.0
  require pie.right == 595.0
  require pie.area == 35700.0

  // ¿Un punto en el pie?
  require pie.contains_point(300, 30)
  require !pie.contains_point(300, 500)

  // Solapamiento: útil para detectar elementos que invaden áreas
  cabecera = region(0, 780, 595, 62)
  require !pie.intersects(cabecera)

  // expand/inset devuelven regiones NUEVAS (la original no cambia)
  holgura = pie.expand(5mm)     // 5mm mayor por cada lado
  seguro = pie.inset(3mm)       // 3mm menor por cada lado
  require holgura.area > pie.area
  require seguro.area < pie.area
}

Usar regiones en las validaciones

profile "prospecto-farmaceutico" {

  check "Franja roja" {
    // La franja debe estar arriba, con el texto legal
    franja = region(0, 700, 595, 142, "franja")
    contenido = text::extract_from_region(1, franja)
    assert contenido.contains("VENDA SOB PRESCRIÇÃO"),
      "la franja no lleva el texto obligatorio"
  }

  check "Tinta en la zona del plegado" {
    // El exceso de tinta en el plegado da problemas de acabado
    plegado = region(290, 0, 15, 842, "plegado central")
    medida = prepress::calculate_tac_by_region(1, plegado)
    assert medida.first() < 240,
      "demasiada tinta en el plegado: #{medida.first()}%"
  }

  check "Código de barras en su sitio" {
    area_codigo = region(400, 20, 180, 80, "área del código")
    assert codes::validate_barcode_position(area_codigo),
      "código de barras fuera del área reservada"
  }
}

← El lenguaje · Índice · Siguiente: text::