Cómo trabajar con anotaciones PDF en Python

Cómo trabajar con anotaciones PDF en Python

Aspose.PDF FOSS para Python expone cada anotación en una página como un objeto Annotation en vivo a través del AnnotationCollection de la página, de modo que puedes añadir anotaciones de marcado, enlace y 3D, inspeccionar sus propiedades específicas de tipo y generar los flujos de apariencia que los visores PDF necesitan para renderizarlos — todo desde Python puro. Esta guía muestra cómo instalar la biblioteca, añadir anotaciones, leerlas de nuevo y generar sus flujos de apariencia.

Guía paso a paso

Paso 1: Instalar el paquete

Instala el paquete Aspose.PDF FOSS:

git clone https://github.com/aspose-pdf-foss/Aspose-PDF-FOSS-for-Python.git
cd Aspose-PDF-FOSS-for-Python
pip install -e .

Verifica la instalación importando el módulo de nivel superior aspose_pdf, que expone la clase Document utilizada en los pasos siguientes. El fragmento imprime aspose_pdf OK cuando la importación tiene éxito; un ModuleNotFoundError suele indicar que el paquete se instaló en un entorno Python diferente al que está ejecutando tu script:

import aspose_pdf
print("aspose_pdf OK")

Paso 2: Importar clases requeridas

Importa Document para contener el PDF, más las clases de anotación que usarás para crear e inspeccionar anotaciones:

from aspose_pdf import Document
from aspose_pdf.annotations import (
    Annotation,
    AnnotationType,
    AnnotationFlags,
    MarkupAnnotation,
    LinkAnnotation,
)

Paso 3: Añadir una anotación a una página

Cada Page expone sus anotaciones a través de la propiedad annotations, un AnnotationCollection. Llama a add(subtype, rect, contents) con un nombre de subtipo, un rectángulo (left, bottom, right, top) y el contenido de texto de la anotación. Los subtipos de marcado estándar, como "Highlight", se devuelven como una instancia de MarkupAnnotation:

from aspose_pdf import Document

document = Document()
document.pages.add()
page = document.pages[0]

annotation = page.annotations.add(
    "Highlight",
    (100, 700, 300, 720),
    "Key clause",
)
print(type(annotation).__name__)   # MarkupAnnotation
print(annotation.contents)         # Key clause

Paso 4: Crear anotaciones a partir de subtipos de enumeración

AnnotationType enumera cada subtipo estándar de anotación PDF (PDF 32000-1:2008, Tabla 169), por lo que puedes pasar un miembro de enumeración en lugar de una cadena cruda. Los datos específicos de tipo — como los vértices de un polígono — se colocan en el diccionario properties y se leen de nuevo con get_property:

from aspose_pdf import Document
from aspose_pdf.annotations import AnnotationType

document = Document()
document.pages.add()
page = document.pages[0]

annotation = page.annotations.add(
    AnnotationType.POLYGON,
    (0, 0, 10, 10),
    "",
    properties={"Vertices": [0, 0, 10, 0, 5, 10]},
)
print(annotation.subtype)                    # Polygon
print(annotation.get_property("Vertices"))   # [0, 0, 10, 0, 5, 10]

Paso 5: Leer e iterar anotaciones existentes

AnnotationCollection es iterable, por lo que puedes recorrer cada anotación ya presente en una página — incluidas las cargadas desde un PDF existente — y leer las propiedades comunes que expone cada Annotation (subtype, contents, rect, title, author, color):

from aspose_pdf import Document

document = Document()
document.pages.add()
page = document.pages[0]
page.annotations.add("Text", (50, 50, 70, 70), "First note")
page.annotations.add("Text", (80, 80, 100, 100), "Second note")

for annotation in page.annotations:
    print(annotation.subtype, "-", annotation.contents)

Paso 6: Generar flujos de apariencia de anotación

Una anotación creada sin un appearance_normal explícito no tiene representación visible (/AP /N) hasta que se genera una. Llama a generate_appearance(force) en un solo Annotation, o generate_appearances(force) en todo el AnnotationCollection para sintetizar cada flujo de apariencia faltante en la página de una vez:

from aspose_pdf import Document
from aspose_pdf.engine.cos import AnnotationName

document = Document()
document.pages.add()
page = document.pages[0]

stamp = page.annotations.add(
    "Stamp",
    (100, 100, 260, 150),
    "",
    properties={"Name": AnnotationName("Approved")},
)
if stamp.generate_appearance():
    print(f"Appearance stream: {len(stamp.appearance_normal)} byte(s)")

# Regenerate every missing appearance stream on the page in one call:
count = page.annotations.generate_appearances(force=True)
print(f"Regenerated {count} appearance stream(s)")

Paso 7: Distinguir subclases de anotación

AnnotationCollection.add() despacha a una subclase específica según el subtipo: los subtipos de estilo markup (Highlight, Square, Stamp, y similares) se devuelven como MarkupAnnotation, y "Link" se devuelve como LinkAnnotation. Ambos heredan todos los métodos y propiedades de Annotation, por lo que las comprobaciones de isinstance te permiten ramificar según el tipo de anotación sin inspeccionar directamente las cadenas subtype:

from aspose_pdf import Document
from aspose_pdf.annotations import LinkAnnotation, MarkupAnnotation

document = Document()
document.pages.add()
page = document.pages[0]

link = page.annotations.add("Link", (50, 750, 200, 770), "")
highlight = page.annotations.add("Highlight", (50, 700, 200, 720), "")

print(isinstance(link, LinkAnnotation))         # True
print(isinstance(highlight, MarkupAnnotation))  # True

Problemas comunes y soluciones

get_property returns None para una propiedad que acabo de establecer

Las claves properties son nombres de campos PDF exactos, sensibles a mayúsculas y minúsculas ("Vertices", "Name", y similares) — un error tipográfico o una mayúscula incorrecta se ignoran silenciosamente en lugar de generar una excepción. Pasa un argumento default a get_property(name, default) y compruébalo explícitamente mientras depuras un nuevo subtipo.

generate_appearance() returns False

No todas las combinaciones de subtipo y propiedad pueden ser sintetizadas en un flujo de apariencia por el generador incorporado. Verifique has_appearance antes de asumir que la llamada tuvo éxito, y proporcione un appearance_normal pre-renderizado (bytes) directamente en add() para los subtipos que el generador no cubre.

Subclase de anotación inesperada después de add()

La cadena de subtipo (o el miembro AnnotationType) que pasa determina la clase devuelta: los subtipos de marcado regresan como MarkupAnnotation, "Link" regresa como LinkAnnotation, y cualquier otro subtipo reconocido regresa como el Annotation base. Use isinstance() contra MarkupAnnotation/LinkAnnotation en lugar de asumir una cadena de subtipo específica.

AnnotationFlags los valores no parecen cambiar el renderizado

AnnotationFlags (PRINT, HIDDEN, NO_ZOOM, READ_ONLY y similares) es una enumeración estándar de Python IntFlag para componer e interpretar los bits de comportamiento de una anotación con el operador | — es un tipo de valor, no una propiedad que Annotation.add() escribe automáticamente. Combine los indicadores que necesite y páselos a través del mismo mecanismo properties específico del tipo usado para otros datos específicos de subtipo.

Trabajando con una página que aún no tiene anotaciones

page.annotations siempre es un AnnotationCollection válido (posiblemente vacío) — nunca es necesario comprobar None antes de llamar a add(), iterar o llamar a clear().

Preguntas Frecuentes

¿Qué subtipos de anotación admite Aspose.PDF FOSS para Python?

AnnotationType enumera los 25 subtipos estándar de la Tabla 169 del PDF 32000-1:2008, incluidos TEXT, LINK, FREE_TEXT, LINE, SQUARE, CIRCLE, POLYGON, HIGHLIGHT, STAMP, INK, FILE_ATTACHMENT, REDACT y más.

¿Esta biblioteca admite anotaciones 3D?

Sí. PDF3DAnnotation, junto con PDF3DArtwork, PDF3DContent, PDF3DView, PDF3DLightingScheme, y PDF3DRenderMode, modela una superficie de anotación PDF 3D mínima (un rectángulo, arte incrustado y vistas nombradas) para flujos de trabajo PDF 3D prerelease. Consulte el referencia PDF3DAnnotation para su conjunto completo de propiedades.

¿Cómo elimino una anotación de una página?

Llame a page.annotations.delete(index) para eliminar una anotación por posición, o a page.annotations.clear() para eliminar todas las anotaciones de la página.

¿Puedo insertar una anotación en una posición específica en lugar de añadirla al final?

Sí — AnnotationCollection.insert(index, subtype, rect, contents, title, appearance_normal, properties) toma los mismos argumentos que add() más el objetivo index.

¿Cómo se representa el color de una anotación?

La propiedad color en Annotation (y sus subclases) es un tuple[float, ...] que coincide con el recuento de componentes de la entrada /C del PDF (vacío cuando el color no está establecido) — no es un objeto de color dedicado.

Ver también

 Español