npm package: @fge-bo-vue/pdf-viewer
Description: Componente Vue 3 para visualización de PDFs con soporte para HTTP Range Requests (Formerly pdf-fge-vue3)
Author: DNTIC - Ministerio Público de Bolivia
Latest version: 1.0.2

# @fge-bo-vue/pdf-viewer

![Vue 3](https://img.shields.io/badge/Vue-3-green?style=flat-square&logo=vue.js)
![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?style=flat-square&logo=typescript)
![pdfjs-dist](https://img.shields.io/badge/PDF.js-5.x-red?style=flat-square)

> 📦 **Formerly**: [`pdf-fge-vue3`](https://www.npmjs.com/package/pdf-fge-vue3) (deprecated)

Componente Vue 3 para visualización de PDFs con soporte para HTTP Range Requests, caché inteligente y múltiples estrategias de carga.

## 🔄 Migrating from pdf-fge-vue3

If you're migrating from the old package:

```bash
# Uninstall old package
npm uninstall pdf-fge-vue3

# Install new package
npm install @fge-bo-vue/pdf-viewer
```

**your imports:**
```vue
import { PdfViewer, PdfViewerRange } from '@fge-bo-vue/pdf-viewer';

import '@fge-bo-vue/pdf-viewer/styles.css';
```

✅ **No breaking changes** - All APIs remain the same!

---

## 🎯 Características

- ✅ **HTTP Range Requests**: Descarga solo las partes necesarias del PDF
- ✅ **3 Estrategias de Carga**: on-demand, progressive, complete
- ✅ **Caché Inteligente**: Persiste entre recargas (F5) con expiración configurable
- ✅ **Pre-validación de URLs**: Captura errores HTTP (403, 404, etc.) antes de cargar
- ✅ **Arquitectura Modular**: Composables y componentes reutilizables
- ✅ **TypeScript**: Tipado completo
- ✅ **Optimización de Ancho de Banda**: Modo on-demand carga solo páginas visibles

## 📦 Instalación

```bash
npm install @fge-bo-vue/pdf-viewer
# o
yarn add @fge-bo-vue/pdf-viewer
```

## ⚠️ Importación de Estilos (OBLIGATORIO)

**IMPORTANTE**: Los estilos CSS son obligatorios para el correcto funcionamiento del componente. Debes importarlos en tu aplicación:

```typescript
import '@fge-bo-vue/pdf-viewer/styles.css';
```

## 📋 Componentes Disponibles

La librería incluye dos componentes principales:

### 1. **PdfViewerRange** (Recomendado)
- Soporta **HTTP Range Requests**
- 3 estrategias de carga: `on-demand`, `progressive`, `complete`
- Caché inteligente con expiración configurable
- Ideal para archivos grandes o conexiones lentas

### 2. **PdfViewer** (Clásico)
- Carga completa del PDF desde URL, File o Base64
- Soporte para integración con AGETIC
- Marcas de agua configurables
- Ideal para archivos pequeños o cuando no hay soporte de Range Requests

### ¿Cuál usar?

| Característica | PdfViewerRange | PdfViewer |
|----------------|----------------|-----------|
| **Range Requests** | ✅ Sí | ❌ No |
| **Caché inteligente** | ✅ Sí | ❌ No |
| **Integración AGETIC** | ❌ No | ✅ Sí |
| **Marcas de agua** | ❌ No | ✅ Sí |
| **Base64 support** | ❌ No | ✅ Sí |
| **File objects** | ❌ No | ✅ Sí |
| **Optimizado para** | Archivos grandes | Archivos pequeños |
| **Ancho de banda** | Mínimo | Completo |

**Recomendación:**
- Usa **PdfViewerRange** si trabajas con archivos grandes (>5MB) y tu backend soporta Range Requests
- Usa **PdfViewer** si necesitas integración con AGETIC, marcas de agua, o trabajas con archivos pequeños

---

## 🚀 Uso de PdfViewerRange

### Importación con defineAsyncComponent (Recomendado)

```js
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const PdfViewerRange = defineAsyncComponent(() => 
  import('@fge-bo-vue/pdf-viewer').then((module) => module.PdfViewerRange)
);

const pdfUrl = ref('https://api.example.com/files/document.pdf');
const headers = ref({ Authorization: 'Bearer token123' });
</script>

<template>
  <PdfViewerRange
    :range-url="pdfUrl"
    :http-headers="headers"
    load-strategy="on-demand"
    :cache-enabled="true"
    :cache-expiration-minutes="5"
  />
</template>
```

### Importación Directa

```ts
<script setup lang="ts">
import { ref } from 'vue';
import { PdfViewerRange } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const pdfUrl = ref('https://api.example.com/files/document.pdf');
</script>

<template>
  <PdfViewerRange :range-url="pdfUrl" />
</template>
```

### Ejemplo Completo con PdfViewerRange

```ts
<script setup lang="ts">
import { ref } from 'vue';
import { PdfViewerRange } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const pdfUrl = ref('https://api.example.com/files/document.pdf?token=abc123');
const headers = ref({
  Authorization: 'Bearer token123',
});

const handleError = (message: string) => {
  console.error('Error al cargar PDF:', message);
};

const handleLoaded = (pages: number) => {
  console.log(`PDF cargado con ${pages} páginas`);
};
</script>

<template>
  <PdfViewerRange
    :range-url="pdfUrl"
    :http-headers="headers"
    :load-strategy="'on-demand'"
    :cache-enabled="true"
    :cache-expiration-minutes="5"
    :download-name="'documento.pdf'"
    :show-progress="true"
    @error="handleError"
    @loaded="handleLoaded"
  />
</template>
```

### Props de PdfViewerRange

| Prop | Tipo | Default | Descripción |
|------|------|---------|-------------|
| `rangeUrl` | `string` | **requerido** | URL del endpoint que soporta Range Requests |
| `httpHeaders` | `Record<string, string>` | `{}` | Headers HTTP adicionales (ej: Authorization) |
| `loadStrategy` | `'on-demand' \| 'progressive' \| 'complete'` | `'on-demand'` | Estrategia de carga del PDF |
| `cacheEnabled` | `boolean` | `true` | Habilitar caché del PDF |
| `cacheExpirationMinutes` | `number` | `1` | Tiempo de expiración del caché en minutos |
| `rangeChunkSize` | `number` | `65536` | Tamaño del chunk en bytes (64KB) |
| `downloadName` | `string` | `'documento.pdf'` | Nombre del archivo al descargar |
| `showProgress` | `boolean` | `true` | Mostrar barra de progreso |
| `pageScale` | `PageScale` | `'auto'` | Escala inicial de la página |
| `pageNumber` | `number` | `1` | Número de página inicial |
| `toolbar` | `Partial<Toolbar>` | Ver abajo | Configuración de la barra de herramientas |

### Toolbar por Defecto

```typescript
{
  search: true,
  paginator: true,
  zoom: true,
  fullscreen: true,
  download: true,
  print: true,
}
```

## 📊 Estrategias de Carga

### 1. **On-Demand** (Recomendado para archivos grandes)

Carga **solo las páginas visibles** cuando el usuario navega a ellas.

**Ventajas:**
- Mínimo ancho de banda
- Carga inicial rápida (~100KB-1MB de metadatos)
- Ideal para documentos grandes (>10MB)

**Uso:**
```ts
<PdfViewerRange
  :range-url="url"
  :load-strategy="'on-demand'"
/>
```

### 2. **Progressive** (Balance)

Precarga páginas cercanas anticipadamente mientras el usuario navega.

**Ventajas:**
- Balance entre velocidad y ancho de banda
- Experiencia de usuario fluida
- Bueno para documentos medianos (5-10MB)

**Uso:**
```ts
<PdfViewerRange
  :range-url="url"
  :load-strategy="'progressive'"
/>
```

### 3. **Complete** (Máximo rendimiento local)

Descarga todo el PDF de una vez.

**Ventajas:**
- Navegación instantánea después de cargar
- Sin esperas al cambiar de página
- Ideal para documentos pequeños (<5MB)

**Uso:**
```ts
<PdfViewerRange
  :range-url="url"
  :load-strategy="'complete'"
/>
```

---

## 🎨 Uso de PdfViewer

El componente **PdfViewer** es ideal para cuando:
- No necesitas HTTP Range Requests
- Trabajas con archivos pequeños
- Necesitas integración con AGETIC
- Requieres marcas de agua
- El PDF viene de File, Base64 o URL simple

### Importación con defineAsyncComponent (Recomendado)

```ts
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const PdfViewer = defineAsyncComponent(() => 
  import('@fge-bo-vue/pdf-viewer').then((module) => module.PdfViewer)
);

const pdfFile = ref({
  src: 'https://api.example.com/files/document.pdf',
  downloadName: 'documento.pdf',
});
</script>

<template>
  <PdfViewer
    :file="pdfFile"
    :token="'your-token'"
    @error="(msg) => console.error(msg)"
  />
</template>
```

### Importación Directa

```ts
<script setup lang="ts">
import { ref } from 'vue';
import { PdfViewer } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const pdfFile = ref({
  src: { url: 'https://api.example.com/files/document.pdf' },
  downloadName: 'documento.pdf',
});
</script>

<template>
  <PdfViewer :file="pdfFile" :token="'your-token'" />
</template>
```

### Ejemplo Completo con PdfViewer

```ts
<script setup lang="ts">
import { ref } from 'vue';
import { PdfViewer } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

const pdfFile = ref({
  src: {
    url: 'https://api.example.com/files/document.pdf',
    httpHeaders: {
      authorization: 'Bearer token123',
    },
  },
  downloadName: 'documento.pdf',
  msFileId: 'file-id-123', // Para integración con AGETIC
});

const token = ref('your-session-token');

const handleError = (message: string) => {
  alert(`Error: ${message}`);
};

const handleFileApproved = (data: any) => {
  console.log('Archivo aprobado:', data);
};
</script>

<template>
  <PdfViewer
    :file="pdfFile"
    :token="token"
    :toolbar="{
      search: true,
      paginator: true,
      zoom: true,
      fullscreen: true,
      download: true,
      print: true,
      agetic: {
        enableSign: true
      }
    }"
    @error="handleError"
    @fileApprovedCD="handleFileApproved"
  />
</template>
```

### Props de PdfViewer

| Prop | Tipo | Default | Descripción |
|------|------|---------|-------------|
| `file` | `FileSrcPdf` | `undefined` | Objeto con la fuente del PDF |
| `token` | `string` | **requerido** | Token de sesión |
| `pageScale` | `PageScale` | `'auto'` | Escala inicial de la página |
| `pageNumber` | `number` | `1` | Número de página inicial |
| `toolbar` | `Partial<Toolbar>` | Ver abajo | Configuración de la barra de herramientas |
| `tokenAgetic` | `string` | `undefined` | Token para integración AGETIC |
| `modeAgetic` | `MODE_AGETIC` | `MODE_AGETIC.DEV` | Modo de AGETIC (DEV/PROD) |
| `verifyFgeEmployee` | `boolean` | `false` | Verificar empleado FGE |
| `watermark` | `WatermarkType` | `undefined` | Configuración de marca de agua |
| `isFetchV2` | `boolean` | `false` | Usar API v2 para obtener PDF |
| `isBase64` | `boolean` | `false` | El PDF viene en formato Base64 |
| `v2ParamsType` | `V2ParamsType` | `undefined` | Parámetros para API v2 |

### Tipo FileSrcPdf

```typescript
interface FileSrcPdf {
  src: PDFSrc; // URL string, File object, or { url: string, httpHeaders?: {} }
  downloadName?: string;
  msFileId?: string; // Para integración AGETIC
}
```

### Toolbar de PdfViewer

```typescript
{
  search: true,
  paginator: true,
  zoom: true,
  fullscreen: true,
  download: true,
  print: true,
  agetic: {
    enableSign: boolean; // Habilitar firma digital AGETIC
  }
}
```

### Eventos de PdfViewer

```typescript
interface PdfViewerEvents {
  error: (message: string) => void;
  fileApprovedCD: (data: ApprobationData) => void; // Cuando se aprueba con firma AGETIC
}
```

---

## 💾 Sistema de Caché (PdfViewerRange)

- ✅ Persiste entre recargas (F5) y navegación
- ✅ Usa Cache Storage API + localStorage
- ✅ Expiración configurable por tiempo
- ✅ Limpieza automática de caché antiguo

### Configuración de Caché

```ts
<PdfViewerRange
  :cache-enabled="true"
  :cache-expiration-minutes="5"
/>
```

### Limpiar Caché Manualmente

```ts
<script setup>
const pdfViewer = ref();

// Limpiar caché del PDF actual
pdfViewer.value?.clearPdfCache();

// Limpiar todo el caché de PDFs
pdfViewer.value?.clearAllPdfCache();
</script>

<template>
  <PdfViewerRange ref="pdfViewer" :range-url="url" />
</template>
```

## 🔒 Manejo de Errores HTTP

El componente captura y muestra errores HTTP del backend automáticamente:

- **403 Forbidden**: URL firmada expirada, sin permisos
- **404 Not Found**: Documento no encontrado
- **401 Unauthorized**: Sesión expirada

### Ejemplo de Respuesta del Backend

```json
{
  "error": true,
  "message": "URL firmada expirada.",
  "response": null,
  "status": 403
}
```

El componente mostrará una pantalla de error elegante con el mensaje del backend.

## 🎨 Métodos Expuestos

Ambos componentes exponen métodos para controlar el visor programáticamente.

### Métodos Comunes (PdfViewer y PdfViewerRange)

```typescript
interface CommonMethods {
  setZoom(zoom: PageScale | number): void;
  setPage(page: number): void;
  rotatePDF(): void;
  downloadClick(): void;
  printPDF(): void;
  search(query?: string, findPrevious?: boolean): void;
}
```

### Métodos Exclusivos de PdfViewerRange

```typescript
interface PdfViewerRangeMethods extends CommonMethods {
  clearPdfCache(): Promise<void>;      // Limpiar caché del PDF actual
  clearAllPdfCache(): Promise<void>;   // Limpiar todo el caché
}
```

### Uso de Métodos

```ts
<script setup>
import { ref } from 'vue';
import { PdfViewerRange, PdfViewer } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css'; // ⚠️ OBLIGATORIO

// Funciona con ambos componentes
const pdfViewerRange = ref();
const pdfViewer = ref();

// Métodos comunes (disponibles en ambos)
const zoomIn = () => {
  pdfViewerRange.value?.setZoom(1.5);
  pdfViewer.value?.setZoom(1.5);
};

const goToPage = (page: number) => {
  pdfViewerRange.value?.setPage(page);
  pdfViewer.value?.setPage(page);
};

const rotate = () => {
  pdfViewerRange.value?.rotatePDF();
  pdfViewer.value?.rotatePDF();
};

const searchText = () => {
  pdfViewerRange.value?.search('texto a buscar');
  pdfViewer.value?.search('texto a buscar');
};

// Métodos exclusivos de PdfViewerRange
const clearCache = () => {
  pdfViewerRange.value?.clearPdfCache();
};

const clearAllCache = () => {
  pdfViewerRange.value?.clearAllPdfCache();
};
</script>

<template>
  <!-- Con PdfViewerRange -->
  <PdfViewerRange ref="pdfViewerRange" :range-url="url" />
  
  <!-- Con PdfViewer -->
  <PdfViewer ref="pdfViewer" :file="file" :token="token" />
  
  <button @click="zoomIn">Zoom In</button>
  <button @click="goToPage(5)">Ir a página 5</button>
  <button @click="rotate">Rotar</button>
  <button @click="searchText">Buscar</button>
  <button @click="clearCache">Limpiar caché (solo Range)</button>
</template>
```

## 📡 Eventos

### Eventos de PdfViewerRange

```typescript
interface PdfViewerRangeEvents {
  error: (message: string) => void;
  loaded: (pages: number) => void;
  progress: (loaded: number, total: number) => void;
}
```

**Ejemplo:**
```ts
<template>
  <PdfViewerRange
    :range-url="url"
    @error="(msg) => console.error('Error:', msg)"
    @loaded="(pages) => console.log('Cargado:', pages, 'páginas')"
    @progress="(loaded, total) => console.log('Progreso:', loaded, '/', total)"
  />
</template>
```

### Eventos de PdfViewer

```typescript
interface PdfViewerEvents {
  error: (message: string) => void;
  fileApprovedCD: (data: ApprobationData) => void; // Solo cuando se usa AGETIC
}
```

**Ejemplo:**
```ts
<template>
  <PdfViewer
    :file="file"
    :token="token"
    @error="(msg) => alert(`Error: ${msg}`)"
    @fileApprovedCD="(data) => console.log('Archivo aprobado:', data)"
  />
</template>
```

## 🏗️ Arquitectura Modular

### Estructura de Archivos

```
src/
├── components/
│   ├── PdfViewerRange.vue           # Componente con Range Requests
│   ├── PdfViewer.vue                # Componente clásico con AGETIC
│   ├── AgeticSidebar.vue            # Integración con firma AGETIC
│   ├── PdfErrorDisplay.vue          # Pantalla de errores HTTP
│   ├── PdfLoadingProgress.vue       # Indicador de progreso
│   ├── LoadingComponent.vue         # Componente de carga
│   ├── ValidationItem.vue           # Item de validación
│   └── PDFToolbar.vue               # Barra de herramientas
├── composables/
│   ├── usePdfCache.ts               # Manejo de caché
│   ├── usePdfLoader.ts              # Carga de PDFs con Range Requests
│   ├── usePdfValidation.ts          # Pre-validación de URLs
│   ├── usePdfViewer.ts              # Lógica del visor PDF
│   ├── usePdfSearch.ts              # Búsqueda en PDF
│   └── usePdfActions.ts             # Acciones (descargar, imprimir, etc.)
├── hooks/
│   └── useAgetic.ts                 # Hook para integración AGETIC
└── types/
    └── index.ts                     # Tipos TypeScript
```

### Composables

#### `usePdfCache` (PdfViewerRange)

```typescript
const { getCachedPdf, cachePdf, clearCache, clearAllCache } = usePdfCache();
```

Funciones:
- `getCachedPdf(url, expirationMinutes)`: Buscar PDF en caché
- `cachePdf(url, data, strategy)`: Guardar PDF en caché
- `clearCache(url)`: Limpiar caché de un PDF
- `clearAllCache()`: Limpiar todo el caché

#### `usePdfLoader`

```typescript
const { isLoading, loadProgress, loadPdf, getPdfData } = usePdfLoader();
```

Funciones:
- `loadPdf(options, onProgress)`: Cargar PDF con Range Requests
- `getPdfData(pdfDoc)`: Obtener datos completos del PDF

#### `usePdfValidation`

```typescript
const { validateUrl } = usePdfValidation();
```

Funciones:
- `validateUrl(url, headers)`: Pre-validar URL antes de cargar

## 🔧 Requisitos del Backend

El backend debe soportar **HTTP Range Requests (RFC 7233)**:

### Headers Requeridos

```
Accept-Ranges: bytes
Content-Range: bytes 0-1023/1234567
Content-Length: 1024
```

### Respuestas HTTP

- **206 Partial Content**: Para requests con Range header
- **416 Range Not Satisfiable**: Si el rango solicitado es inválido
- **200 OK**: Para requests sin Range header (fallback)

### Ejemplo de Implementación NestJS

Ver el backend de ejemplo en: `/ms-files-v2/src/modules/file/file-range.service.ts`

## 🐛 Troubleshooting

### Los estilos no se aplican correctamente

**Solución:** Verifica que hayas importado los estilos:
```typescript
import '@fge-bo-vue/pdf-viewer/styles.css';
```
Esta importación es **obligatoria** y debe estar en el componente donde uses PdfViewer o PdfViewerRange.

### El PDF no carga en modo on-demand (PdfViewerRange)

Verifica en DevTools → Network:
- Los requests deben tener header `Range: bytes=X-Y`
- Las respuestas deben ser `206 Partial Content`
- Backend debe enviar header `Accept-Ranges: bytes`

### El caché no funciona (PdfViewerRange)

- Verifica que `cacheEnabled={true}`
- Revisa la consola del navegador
- El caché solo funciona en HTTPS o localhost
- Solo aplica para `PdfViewerRange`, no para `PdfViewer`

### El componente no se importa correctamente

Si usas `defineAsyncComponent`:
```typescript
import '@fge-bo-vue/pdf-viewer/styles.css';
const PdfViewerRange = defineAsyncComponent(() => 
  import('@fge-bo-vue/pdf-viewer').then((module) => module.PdfViewerRange)
);
```

Si usas importación directa:
```typescript
import { PdfViewerRange, PdfViewer } from '@fge-bo-vue/pdf-viewer';
import '@fge-bo-vue/pdf-viewer/styles.css';
```

### Errores CORS (PdfViewerRange)

El backend debe permitir estos headers:
```
Access-Control-Allow-Headers: Range
Access-Control-Expose-Headers: Content-Range, Accept-Ranges
```

## 📄 Licencia

UNLICENSED - FGE Internal Use Only