5. Espacio de nombres visual:: — imágenes y comparación visual

struct:: · Índice · Siguiente: prepress::

16 funciones sobre las imágenes del documento y sobre el aspecto renderizado de las páginas.

Las funciones de comparación y de calidad renderizan la página en escala de grises. Cada página se renderiza una sola vez y el resultado queda en caché.


5.1 Inventario de imágenes

visual::detect_images() y visual::count_images()

check "Imágenes del documento" {
  require visual::detect_images()
  print("total de imágenes:", visual::count_images())

  // Un catálogo sin imágenes seguramente esté mal
  assert visual::count_images() >= 10,
    "catálogo con solo #{visual::count_images()} imagen(es)"
}

visual::get_image_resolution(n)

DPI efectivo de la enésima imagen (1-based).

check "Resolución de la imagen de portada" {
  dpi = visual::get_image_resolution(1)
  assert dpi >= 300, "imagen de portada con #{round(dpi)} DPI (mínimo 300)"
}

visual::get_image_size(n)

Dimensiones en píxeles: [anchura, altura].

check "Tamaño de la imagen" {
  tamano = visual::get_image_size(1)
  print("primera imagen:", tamano.first(), "x", tamano.last(), "píxeles")
  require tamano.first() >= 1000
}

visual::detect_image_color_space([n])

Sin argumento: lista de los espacios de color presentes en el documento. Con n: el espacio de la enésima imagen.

check "Espacios de color usados" {
  espacios = visual::detect_image_color_space()
  print("espacios presentes:", espacios.join(", "))

  // Para offset, todo debe ser CMYK
  assert !espacios.contains("DeviceRGB"),
    "hay imágenes RGB — convertirlas a CMYK antes de imprimir"

  // Comprobando una imagen concreta
  require visual::detect_image_color_space(1) == "DeviceCMYK"
}

visual::detect_low_resolution([dpi_minimo])

Verdadero si alguna imagen está por debajo del mínimo (300 por defecto).

check "Resolución general" {
  assert !visual::detect_low_resolution(300),
    "hay imágenes por debajo de 300 DPI"

  // Para impresión de gran formato el límite es otro
  assert !visual::detect_low_resolution(150),
    "hay imágenes por debajo de 150 DPI (mínimo para una lona)"
}

Para saber qué imágenes están mal (y no solo si las hay), recorre doc.images; consulta el capítulo 2:

doc.images.each { |img|
  assert img.dpi >= 300,
    "imagen en la página #{img.page_number}: #{img.dpi} DPI"
}

5.2 Comparación visual entre archivos

Estas funciones comparan una página de este documento con una página de otro archivo. La firma general es:

funcion(pagina_de_aqui, "otro.pdf" [, pagina_de_alli])

Si se omite la página del otro archivo, se usa el mismo número. Las páginas de tamaños distintos se remuestrean antes de la comparación.

visual::measure_ssim(pagina, "otro.pdf" [, pagina_b])

Similitud estructural, de 0.0 a 1.0. Es la medida que más se acerca a la percepción humana de «son la misma página».

check "Prueba aprobada frente al archivo final" {
  aprobado = "aprobados/revista_v1.pdf"

  doc.pages.each { |page|
    ssim = visual::measure_ssim(page.number, aprobado)
    assert ssim > 0.99,
      "la página #{page.number} cambió visualmente (SSIM #{ssim})"
  }
}

visual::compare_images(...) y visual::diff_pages(...)

La misma comparación, en escala de 0 a 100. Son sinónimos: usa el nombre que tenga más sentido en tu script.

check "Similitud en porcentaje" {
  nota = visual::diff_pages(1, "version_anterior.pdf")
  assert nota > 95, "la portada cambió un #{round(100 - nota)}% desde la última versión"
}

visual::pixel_diff(pagina, "otro.pdf" [, pagina_b, tolerancia])

Porcentaje de píxeles que difieren. La tolerancia (10 por defecto, de 0 a 255) ignora las variaciones mínimas de renderizado.

check "Cuánto cambió la página" {
  porcentaje = visual::pixel_diff(4, "anterior.pdf")
  print("píxeles modificados en la página 4:", porcentaje, "%")

  // Tolerancia mayor para ignorar el antialiasing
  suave = visual::pixel_diff(4, "anterior.pdf", 4, 30)
  assert suave < 1.0, "cambio significativo en la página 4"
}

visual::calculate_perceptual_hash([pagina])

Huella visual de la página: 64 bits en hexadecimal. Las páginas parecidas tienen huellas parecidas.

check "Huella de las páginas" {
  doc.pages.each { |page|
    print("página", page.number, "->", visual::calculate_perceptual_hash(page.number))
  }
}

visual::detect_image_replacement(pagina, "otro.pdf" [, pagina_b, distancia])

Verdadero si la página cambió visualmente más de lo tolerado. Compara las huellas perceptuales; distancia es cuántos bits pueden diferir (10 de 64 por defecto).

check "Ninguna imagen cambiada entre versiones" {
  anterior = "aprobados/catalogo_v1.pdf"

  doc.pages.each { |page|
    assert !visual::detect_image_replacement(page.number, anterior),
      "página #{page.number}: contenido visual sustituido"
  }
}

5.3 Calidad de la imagen

visual::detect_image_artifacts([pagina])

Verdadero si la página presenta el bloqueo típico de un JPEG muy comprimido.

check "Sin artefactos de compresión" {
  doc.pages.each { |page|
    assert !visual::detect_image_artifacts(page.number),
      "la página #{page.number} tiene bloqueo de compresión visible"
  }
}

visual::estimate_image_quality([pagina])

Nota de 0 a 100 derivada del bloqueo detectado.

check "Nota de calidad" {
  doc.pages.each { |page|
    nota = visual::estimate_image_quality(page.number)
    assert nota >= 70,
      "la página #{page.number} tiene calidad #{nota}/100 — ¿recomprimida de más?"
  }
}

visual::detect_posterization([pagina])

Verdadero si hay pocos niveles tonales en una página con un rango tonal amplio, señal de una imagen guardada con profundidad de color insuficiente.

check "Degradados íntegros" {
  doc.pages.each { |page|
    assert !visual::detect_posterization(page.number),
      "página #{page.number}: posible posterización (pocos tonos)"
  }
}

visual::detect_banding([pagina])

Verdadero si hay un degradado en escalones visibles en lugar de una transición suave.

La detección exige una progresión monótona con mesetas anchas: las páginas de texto normal, que tienen transiciones abruptas, no disparan la alarma.

check "Degradados sin bandas" {
  // Los fondos con degradado son donde aparece el banding
  doc.pages.each { |page|
    assert !visual::detect_banding(page.number),
      "la página #{page.number} tiene banding en el degradado"
  }
}

5.4 Ejemplo completo

// aprobacion_visual.pdfl — compara el archivo con la versión aprobada
// Uso: pdfl run aprobacion_visual.pdfl nueva_version.pdf
profile "aprobacion-visual" {

  const APROBADO = "aprobados/catalogo_v1.pdf"
  const DPI_MINIMO = 300

  check "Inventario" tags: ["imagenes"] {
    require visual::detect_images()
    print("imágenes:", visual::count_images())
    print("espacios de color:", visual::detect_image_color_space().join(", "))
  }

  check "Resolución" tags: ["imagenes", "prepress"] {
    doc.images.each { |img|
      assert img.dpi >= DPI_MINIMO,
        "imagen en la página #{img.page_number}: #{round(img.dpi)} DPI (mínimo #{DPI_MINIMO})"
    }
  }

  check "Calidad" tags: ["imagenes"] {
    doc.pages.each { |page|
      assert !visual::detect_image_artifacts(page.number),
        "la página #{page.number} tiene artefactos de compresión"
      assert !visual::detect_banding(page.number),
        "la página #{page.number} tiene banding"
    }
  }

  check "Fidelidad a la versión aprobada" tags: ["aprobacion"] {
    doc.pages.each { |page|
      ssim = visual::measure_ssim(page.number, APROBADO)
      assert ssim > 0.99,
        "la página #{page.number} difiere de la aprobada (SSIM #{ssim}, #{visual::pixel_diff(page.number, APROBADO)}% de los píxeles)"
    }
  }
}

struct:: · Índice · Siguiente: prepress::