Extraer enlaces de PDF
De siete anotaciones de un PDF salen los cuatro enlaces web reales, cada uno con su número de página; los comentarios, los resaltados y los saltos internos quedan fuera. Un enlace repetido en tres páginas aparece tres veces. Gratis en su navegador.
- Gratis
- Funciona en su navegador
- Sin registro
- Sus datos no salen de su equipo
Ejecutar — gratis
Corre en su navegador. Gratis y sin límite: sus datos no salen de esta página.
El extractor de enlaces de PDF lee la lista de anotaciones por página de un PDF — exactamente lo que una biblioteca lectora como pdf.js devuelve desde getAnnotations — y devuelve cada hipervínculo que encuentra, con su número de página y su URL de destino. Es el paso que faltaba entre «ya tengo las anotaciones» y «sé a dónde apunta este documento»: una pasada determinista, sin renderizado, sin OCR y sin suposiciones. Los enlaces puramente internos y las anotaciones que no son enlaces se descartan, de modo que lo que recibe es una lista limpia y ordenada de las referencias externas que hace el documento, lista para una auditoría, una lista semilla para un rastreador o una comprobación de cumplimiento.
Por qué anotaciones y no texto en bruto
Buscar enlaces recorriendo el texto extraído no es fiable: una URL visible en el texto puede no ser clicable, y un enlace clicable puede mostrar un texto de anclaje que no se parece en nada a su destino. La fuente autorizada para extraer enlaces de un PDF es la capa de anotaciones. Los lectores de PDF guardan los elementos interactivos como anotaciones asociadas a una página, y los hipervínculos viven ahí como entradas de subtipo Link con una URI.
Esta capacidad toma esa lista como entrada — un registro por anotación con su número de página, subtipo y URI opcional — y la filtra hasta quedarse con los hipervínculos salientes reales. Como el número de página viaja con cada anotación, cada resultado le dice exactamente en qué punto del documento está el enlace, que es lo que hace que la salida sirva para revisiones y auditorías y no sea una mera lista de URLs.
Qué conserva el extractor y qué descarta
El filtro es deliberadamente estricto. Solo se consideran las anotaciones cuyo subtipo es Link; los comentarios, los resaltados, los widgets de formulario y cualquier otro tipo de anotación se omiten. Entre las anotaciones Link, solo sobreviven las que llevan una URI no vacía: una anotación de enlace sin URI suele ser un salto interno a otra página, es decir, navegación dentro del documento y no un hipervínculo a un destino externo, así que queda fuera.
Las URIs se recortan de espacios en blanco alrededor, pero por lo demás se devuelven literalmente: sin reescritura, sin resolución, sin deduplicación. Si la misma URL aparece en tres páginas, recibe tres entradas, porque el hecho de que aparezca tres veces es información. La salida se ordena exactamente como llegan las anotaciones, lo que en la práctica significa el orden del documento.
La misma URL puede aparecer legítimamente varias veces en un documento largo — en el pie de página repetido, en una bibliografía y en una página de contacto — y colapsar esas ocurrencias borraría la diferencia entre una cita puntual y una referencia sistemática, así que el extractor las conserva todas y deja que quien llama decida qué agregar después.
Dónde encaja en una canalización
El flujo típico es: abrir el PDF con una biblioteca lectora, recoger las anotaciones página a página, aplanarlas en la lista de registros que esta capacidad espera y enviarlas. Recibe de vuelta un contador y un array de pares {page, url}. Los equipos la usan para inventariar las referencias salientes de contratos e informes antes de publicarlos, para construir la lista semilla de una pasada de comprobación de enlaces, o para confirmar que un documento anonimizado ya no apunta a donde no debería.
Una lista de anotaciones vacía se trata como un error y no como un resultado vacío, porque una lista vacía casi siempre significa que el paso de extracción anterior falló o que el archivo nunca tuvo capa de anotaciones — y devolver cero enlaces en silencio lo ocultaría. El mismo código se ejecuta gratis en su navegador en esta página, de modo que puede validar una carga pegándola y solo pagar cuando automatice la llamada.
Qué puede hacer con ella
Audite las referencias salientes antes de publicar
Inventarie cada hipervínculo que hace un informe o contrato, con números de página, para que los revisores confirmen que cada destino es intencionado y correcto.
Alimente una comprobación de enlaces rotos
Pase las URLs extraídas a un comprobador de enlaces para detectar destinos muertos en un documento que sus clientes están a punto de descargar.
Verifique un documento anonimizado
Confirme que la versión publicada de un PDF ya no enlaza a URLs internas de preproducción o de socios que deberían haberse eliminado.
Preguntas frecuentes
¿Cuánto cuesta?
También es gratis ejecutarlo en su navegador en esta página.
¿Qué entrada espera?
Una lista de anotaciones aplanada: un registro por anotación con su número de página empezando en 1, su subtipo y, para los enlaces, su URI. Bibliotecas lectoras como pdf.js producen esto desde getAnnotations con un pequeño paso de mapeo.
¿Por qué recibo un error en lugar de una lista vacía?
Un array de anotaciones vacío significa que la extracción anterior falló o que el PDF no tiene capa de anotaciones.
¿Se incluyen los saltos internos de página?
No. Las anotaciones de enlace sin URI son destinos internos dentro del documento, no hipervínculos salientes, y se omiten.
¿Se deduplican las URLs repetidas?
No. Si la misma URL está enlazada en tres páginas recibe tres entradas, porque cada ocurrencia lleva su propio número de página y esa repetición es en sí misma información útil.
¿Descarga o valida las URLs?
No. La extracción es puramente local y determinista; no se hace ninguna petición de red contra los destinos.
Herramientas relacionadas
Para desarrolladores — acceso por API
Todo lo de esta página está disponible por programación. Esta sección es para equipos que quieren integrarlo en sus sistemas; el resto puede usar la herramienta de arriba sin más.
Endpoint de API
¿Prefiere automatizarlo? Un POST autenticado crea la tarea; el resultado llega por webhook o enlace firmado. La misma capacidad también se ejecuta aquí en la web, por email y desde Telegram — y pronto también desde nuestra app.
Llámela desde su stack
curl -X POST https://api.kit.forhosting.com/pdf/extract-links \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"annotations":[{"page":1,"subtype":"Link","uri":"https://example.com/docs"},{"page":1,"subtype":"Text","uri":null},{"page":3,"subtype":"Link","uri":"https://example.com/contact"}]}'const res = await fetch("https://api.kit.forhosting.com/pdf/extract-links", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"annotations": [
{
"page": 1,
"subtype": "Link",
"uri": "https://example.com/docs"
},
{
"page": 1,
"subtype": "Text",
"uri": null
},
{
"page": 3,
"subtype": "Link",
"uri": "https://example.com/contact"
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/pdf/extract-links",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"annotations": [
{
"page": 1,
"subtype": "Link",
"uri": "https://example.com/docs"
},
{
"page": 1,
"subtype": "Text",
"uri": null
},
{
"page": 3,
"subtype": "Link",
"uri": "https://example.com/contact"
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/pdf/extract-links", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"annotations":[{"page":1,"subtype":"Link","uri":"https://example.com/docs"},{"page":1,"subtype":"Text","uri":null},{"page":3,"subtype":"Link","uri":"https://example.com/contact"}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"annotations":[{"page":1,"subtype":"Link","uri":"https://example.com/docs"},{"page":1,"subtype":"Text","uri":null},{"page":3,"subtype":"Link","uri":"https://example.com/contact"}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/pdf/extract-links", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Ejemplo de solicitud
{
"annotations": [
{
"page": 1,
"subtype": "Link",
"uri": "https://example.com/docs"
},
{
"page": 1,
"subtype": "Text",
"uri": null
},
{
"page": 3,
"subtype": "Link",
"uri": "https://example.com/contact"
}
]
}Ejemplo de respuesta
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "pdf.extract_links",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}La API es asíncrona: la llamada devuelve un task_id al instante y el resultado llega por webhook. El polling está limitado a 1 req/s por tarea.
Precio
Precio publicado — sin tokens ni créditos inventados. Una tarea fallida no se cobra.
Límites
max_mb | 1 |
max_annotations | 50000 |
Errores
| HTTP | Código | Significado |
|---|---|---|
401 | unauthorized | API key ausente o inválida. |
402 | insufficient_balance | El saldo no cubre el precio de la tarea. |
404 | unknown_type | El tipo de tarea no existe. |
429 | rate_limited | Demasiadas peticiones. Use el webhook en vez de sondear. |
Notas técnicas
Por qué anotaciones y no texto en bruto
El contrato además valida cada registro sobre la marcha: un número de página ausente, una página no entera o un subtipo que falta detiene la llamada con un error invalid_input claro, en lugar de producir en silencio una respuesta parcial que usted se sentiría tentado a creer.