Cómo mostrar PDFs en Oracle APEX usando <object> e <iframe>

I am a technology professional with about a decade of experience in software development, specialising in Oracle APEX. My career began in telecommunications engineering, but my passion for technology and software development led me to transition into this field. Since then, I have been deeply involved in leading teams and driving digital transformation across various projects. As a part of my daily tasks, I focus on fostering collaboration, efficiency, and technical excellence, ensuring that the projects where I'm involved embrace digital advancements effectively. I take great pride in sharing my knowledge and experience, whether through mentorship, conferences, or engaging with the tech community.
En aplicaciones desarrolladas con Oracle APEX es muy habitual, además de sencillo, gestionar documentación: contratos, informes, facturas o cualquier otro tipo de fichero, normalmente en formato PDF. En muchos casos, estos documentos se almacenan directamente en la base de datos como BLOB y se ofrecen al usuario final mediante un enlace de descarga.
Sin embargo, no siempre es necesario forzar la descarga del documento. Poder previsualizar un PDF directamente en la aplicación, sin abandonar el contexto de la página, mejora la experiencia de usuario y simplifica muchos casos de uso cada vez más frecuentes.
Este artículo es uno de esos que me sirven para no tener que buscar una y otra vez como realizar la misma tarea, y aquí veremos dos formas rápidas de previsualizar documentos PDF en Oracle APEX, aprovechando nuestros navegadores, mediante HTML estándar (object e iframe).
Preparando el escenario: modelo de datos
Antes de entrar en las distintas formas de previsualizar un PDF en Oracle APEX, necesitamos sentar una base común: un modelo de datos mínimo para almacenar documentos en la base de datos.
El objetivo no es construir un gestor documental completo, sino disponer de una estructura sencilla que nos permita:
almacenar un PDF como
BLOB,conservar su nombre y MIME type,
y poder servirlo posteriormente al navegador.
Para los ejemplos que vamos a crear utilizaremos una única tabla, con las columnas estrictamente necesarias:
create table app_docs (
doc_id number generated by default as identity
constraint app_docs_pk primary key,
file_name varchar2(255) not null,
mime_type varchar2(255) not null,
file_blob blob not null,
created_on timestamp default systimestamp not null
);
Nota: este modelo se ha mantenido deliberadamente simple. No se incluyen controles de versión, estados, permisos ni relaciones adicionales, ya que no aportan valor al objetivo de este artículo.

Carga del documento desde Oracle APEX
Cada proyecto es un mundo, por lo que tendríamos más de una fórmula de llegar a tener nuestro documento persistido en la tabla que acabamos de crear. Por sencillez, vamos a crear un pequeño formulario para cargar un documento PDF desde la propia aplicación APEX y persistirlo en la tabla que acabamos de crear.
Para ello, necesitaremos apoyarnos en:
un item de tipo File Browse,
la tabla temporal
APEX_APPLICATION_TEMP_FILES, que APEX utiliza internamente para almacenar los ficheros subidos durante la sesión.
Así, en una página dentro de nuestra aplicación (la creamos desde cero si queremos mantener nuestro ejemplo limpio), añadimos un elemento con las siguientes propiedades:
Tipo: File Upload (File Browse, según versión de APEX)
Storage Type: APEX_APPLICATION_TEMP_FILES Con este atributo, delegamos en APEX la gestión temporal del fichero.
Accept:
application/pdfAl rellenar con este valor esta propiedad, limitamos la carga únicamente a documentos PDF.

No voy a entrar en detalles sobre la apariencia del item, o si debemos realizar el proceso desde una página normal o modal: eso lo dejo a vuestras ganas de explorar opciones y al caso de uso que os ocupe… No obstante, aún nos queda incluir la lógica que nos permita insertar el documento en la tabla APP_DOCS, recuperando la información desde APEX_APPLICATION_TEMP_FILES. Para ello, creamos el siguiente proceso en la sección After Submit o Processing:
insert into app_docs (
file_name,
mime_type,
file_blob
)
select
filename,
mime_type,
blob_content
from apex_application_temp_files
where name = :PXX_FILE;
IMPORTANTE: sustituir PXX_FILE con el nombre del item de tipo File Browse que creamos anteriormente.
Para entender que está ocurriendo, mediante este procedimiento el MIME type se obtiene directamente de la carga que realiza el navegador, por lo que no es necesario realizar ningún tratamiento especial sobre BLOB en este punto. Además, este proceso no tiene nada que ver con si el documento se va a previsualizar, descargar o procesar de algún modo, por lo que es un proceso de carga de archivos bastante estándar.

Por último, y antes de ejecutar nuestra página para comprobar que el proceso funciona correctamente, es importante mencionar una buena práctica: como desarrolladores debemos asegurarnos de optimizar los recursos que utilizamos, por lo que es recomendable eliminar explícitamente el registro de la tabla temporal una vez procesado el fichero; si bien, no sería necesario que lo hagamos, ya que APEX se encarga automáticamente de ello mediante el estado de sesión. Como nota, si queremos incluir esta lógica, solo debemos añadir las siguientes lineas en nuestro proceso, o definirlas en un proceso tras el anterior:
delete from apex_application_temp_files
where name = :PXX_FILE;
Con esto ya podemos tener nuestro documento PDF almacenado en la base de datos, por lo que ya podemos pasar a lo interesante de este articulo: servir el BLOB desde APEX para que el navegador pueda interpretarlo correctamente y permitir la previsualización inline.

Servir el PDF desde APEX (streaming inline)
Ya que tenemos nuestros documentos PDF almacenados como BLOB en una tabla, el siguiente paso es el que ocupa este blog: ahora necesitamos una URL dentro de nuestro contexto APEX que devuelva el BLOB como application/pdf y en modo inline, para que el navegador sea capaz de renderizarlo embebido.
Es importante destacar una idea: tanto object como iframe no “previsualizan” el documento, sino que embeben una URL. Si en lugar de enviar el PDF como inline lo mandamos como attachment, el navegador descargará directamente el fichero.Stre
Para poder continuar, debemos apoyarnos en una página técnica que nos permita servir el BLOB. Para ello, podemos apoyarnos en una página que nunca vamos a renderizar (yo usaré la 999), donde incluiremos un item oculto: P999_DOC_ID.
Por último, en esta página añadimos un proceso Before Header con el siguiente PL/SQL:
declare
l_blob blob;
l_mime varchar2(255);
l_filename varchar2(255);
begin
select file_blob, mime_type, file_name
into l_blob, l_mime, l_filename
from app_docs
where doc_id = :P999_DOC_ID;
sys.htp.init;
owa_util.mime_header(nvl(l_mime,'application/pdf'), false);
-- Important: serve inline so browsers can preview
htp.p('Content-Length: ' || dbms_lob.getlength(l_blob));
htp.p('Content-Disposition: inline; filename="' || replace(l_filename,'"','') || '"');
owa_util.http_header_close;
wpg_docload.download_file(l_blob);
apex_application.stop_apex_engine;
end;
El resultado de esta ejecución será una URL del estilo: f?p=&APP_ID.:999:&SESSION.::&DEBUG.::P999_DOC_ID:<doc_id>, que será la URL que utilizaremos para mostrar nuestro PDF.

Opción 1: Previsualización embebida con <object>
Para el primer ejemplo, vamos a crear una página que contenga el informe con los documentos cargados a la izquierda, y una región para la previsualización a la derecha, repartiendo el espacio de la pantalla:
Región de tipo informe (Classic o Interactive Report):
R_DOCUMENTS.Región Static Content para visualizar el documento:
R_PREVIEW.
Además, para poder realizar la previsualización del documento necesitamos apoyarnos en dos items ocultos, por lo que también crearemos:
PXX_DOC_IDPXX_DOC_URL
En en el informe, presentamos los documentos disponibles para su previsualización, con una consulta sencilla:
select
doc_id,
file_name,
created_on
from app_docs
order by created_on desc
Donde construimos una columna (por ejemplo doc_id) en una columna de tipo enlace, con los siguientes atributos:
Link Type: URL
Link Target:
javascript:void(0);Link Text:
Preview(podemos utilizar un icono, también)Link Attributes:
class="js-doc-preview" data-doc-id="#DOC_ID#"Con esto, tenemos un selector para nuestra acción dinámica, y el id del documento disponible igualmente.
Ahora, utilizamos nuestro elemento <object> html para previsualizar el documento. Para ello, en la región R_PREVIEW incluimos el siguiente contenido (con el item PXX_DOC_URL correctamente definido, según la página en la que estemos):
<object
id="docObj"
title="Document Preview"
type="application/pdf"
data=""
style="width:100%; height:75vh; border:0;">
<p>
No se ha podido previsualizar el Documento.
<a id="docFallback" href="#" target="_blank" rel="noopener">Abrir en pestaña nueva</a>
</p>
</object>
Nota: con este código, si PXX_DOC_URL está vacío, <object> quedará en blanco. En una segunda iteración, podríamos mejorar esta UX para que todo quede más redondo.
Por último, montamos nuestra acción dinámica que muestre el documento al pulsar el enlace Preview. Creamos, por tanto, una acción dinámica con los siguientes atributos:
Event: Click
Selection Type: jQuery Selector
jQuery Selector:
.js-doc-preview
Dentro de nuestra Acción Dinámica, creamos las siguientes True actions:
Set Value: Guardar DOC_ID en un item
Item(s): PXX_DOC_ID
Set Type: JavaScript Expression
JavaScript Expression:
this.triggeringElement.dataset.docId
Execute Server-side Code: Construimos la URL del documento
- PL/SQL:
:PXX_DOC_URL :=
apex_page.get_url(
p_page => 999,
p_items => 'P999_DOC_ID',
p_values => :PXX_DOC_ID
);
Items to Submit:
PXX_DOC_IDItems to Return:
PXX_DOC_URL
Execute JavaScript code: rellenamos el elemento
<object>Code:
const url = apex.item("PXX_DOC_URL").getValue(); const obj = document.getElementById("docObj"); const a = document.getElementById("docFallback"); if (url && obj) { // Force reload in many browsers obj.data = ""; obj.data = url; if (a) a.href = url; }

NOTA: Aunque podríamos refrescar la región del preview (Acción Dinámica declarativa de Refresh), en la práctica muchos navegadores no “reactivan” el visor PDF al hacer partial refresh. Asignar el atributo data del
<object>directamente desde JavaScript es más fiable y evita recargas de página.
Ahora, guardamos y ejecutamos nuestra página para ver si todo funcionó correctamente. 🙂

Opción 2: Previsualización en un dialogo mediante <iframe>
Para esta segunda opción, vamos a usar una aproximación similar al ejemplo anterior, pero aprovecharemos para explorar otras opciones con APEX:
Crearemos una columna de tipo enlace,
que disparará una acción dinámica para calcular la URL del documento
y abrirá un Inline Dialog mediante una Acción Dinámica.
De este modo, podemos mostrar de forma muy ligera ligera y con escaso impacto en la navegación de nuestra app un documento, siguiendo una aproximación parecida a la anterior. Una alternativa similar podría ser crear una nueva página de tipo modal o normal para presentar el documento, pero esta cuestión depende enteramente de la experiencia de usuario que queramos implementar. A efectos del ejemplo, tendría realmente poco impacto.
Siguiendo nuestra secuencia propuesta, creamos una región estática R_PREVIEW_DIALOG, que moveremos a la región a Layout > Slot: Dialogs, Drawers and Popus, con el siguiente HTML Code:
<iframe
title="Document Preview"
src="&PXX_DOC_URL."
style="width:100%; height:80vh; border:0;"
loading="lazy">
</iframe>
Nota: recordar sustituir PXX_DOC_URL por el nombre correcto del item de página.
Además de nuestra región para mostrar el documento, ahora usando un <iframe>, podemos apoyarnos en el item PXX_DOC_URL que creamos para el ejemplo anterior.

Para poder visualizar el documento en el dialog, en mi caso voy a crear una nueva columna de tipo enlace (una columna Virtual, ya que estoy en un informe clásico), donde construir el enlace como en el caso anterior, asignando ahora una nueva clase al mismo (js-doc-preview-inline):
Link Type: URL
Link Target:
javascript:void(0);Link Text:
Preview(podemos utilizar un icono, también)Link Attributes:
class="js-doc-preview-inline" data-doc-id="#DOC_ID#"Con esto, tenemos un selector para nuestra acción dinámica, y el id del documento disponible igualmente.
Para este ejemplo, duplicamos la acción dinámica anterior, ya que comparte gran parte de las acciones necesarias:
En la definición del evento, cambiamos el jQuery Selector a
.js-doc-preview-inline.Mantenemos las dos primeras acciones true (Set Value para rellenar
PXX_DOC_IDy Execute Server-side Code para rellenarPXX_DOC_URL).Eliminamos la tercera acción (la que renderizaba el
object).Creamos una acción Refresh sobre la región
R_PREVIEW_DIALOG.Creamos una acción Open Region sobre la región
R_PREVIEW_DIALOG.

En este punto, podemos ejecutar nuestra aplicación para ver el resultado.

Conclusión
En este artículo hemos visto e implementado dos aproximaciones sencillas y eficaces para previsualizar documentos en Oracle APEX, apoyándonos en capacidades nativas del navegador:
El uso de
<object>, especialmente adecuado para mostrar el documento embebido directamente en la página.El uso de
<iframe>, muy práctico cuando queremos aislar la visualización en un diálogo o región independiente, o cuando trabajamos con URLs de documentos ya disponibles.
Ambas soluciones comparten una ventaja clave: no requieren librerías externas, se integran de forma natural con APEX y cubren la mayoría de casos de uso habituales en aplicaciones de negocio.
Además, esta aproximación no se limita exclusivamente a documentos PDF. Siempre que el navegador sea capaz de renderizar el contenido de forma nativa, podemos reutilizar el mismo patrón para previsualizar otros tipos de archivos, como imágenes (PNG, JPG, SVG) o incluso determinados formatos de texto. Por el contrario, formatos ofimáticos como DOCX o XLSX no suelen poder previsualizarse directamente sin apoyarse en visores externos o servicios adicionales.
Existen alternativas más avanzadas, como PDF.js, que ofrecen una experiencia de usuario más rica (navegación, búsqueda, miniaturas). No obstante, su integración suele implicar consideraciones adicionales en entornos reales - políticas de seguridad, restricciones de origen (same-origin), gestión de recursos estáticos, etc -. Por este motivo, y para mantener una receta rápida y autocontenida, dejaremos este tipo de integraciones para más adelante.
En conjunto, estas técnicas nos permiten resolver de forma eficaz un requisito muy común en aplicaciones APEX, manteniendo la solución simple, mantenible y alineada con arquitecturas modernas.
Si os apetece ver un ejemplo ya construido, podéis descargar el ejemplo que yo fui construyendo durante la redacción de este post en este enlace e instalarlo en vuestro propio workspace.
Como siempre, ¡gracias por llegar hasta aquí!
Happy coding,
Juan L.






