martes, 31 de octubre de 2023


La librería pdf-lib es una de las librerías más personalizables y fáciles de utilizar a la hora de crear documentos PDF, que puedan ser descargados para su posterior visualización. Pero si hay algo que complica el escenario al momento de configurarla, es justamente la parte de cargar y leer una fuente personalizada, porque si bien EXPO está creado para ser más seguro y facilitarnos muchas cosas, a la vez nos complica el acceso a los archivos del dispositivo.

En esta guía veremos un ejemplo de cómo cargar y embeber una fuente personalizada a un documento PDF de la manera correcta.


Requisitos previos: Instalar las siguientes dependencias:

-   expo-file-system

- pdf-lib

- base-64

- expo-asset




Directorio: Colocar los archivos de fuentes personalizadas en el
directorio assets/fonts



Para este ejemplo utilizaremos la fuente:
AlexBrush-Regular.ttf







En el archivo principal de nuestro proyecto (App.js) procederemos a importar las librerías previamente
instaladas

import * as FileSystem from 'expo-file-system';
import * as Sharing from 'expo-sharing';
import { PDFDocument, StandardFonts } from 'pdf-lib';
import React, { useEffect, useState } from 'react';
import { Button, Text, View } from 'react-native';
import fontkit from '@pdf-lib/fontkit';
import { decode, encode } from 'base-64';
import { Asset } from 'expo-asset';

Procedemos a configurar las funciones globales de codificación y decodificación: btoa y
atob para poder codificar la fuente personalizada



// Comprobar si la función btoa (Base64 encode) no está disponible en el entorno global
if (!global.btoa) {
  // Asignar la función de codificación Base64 (encode) al objeto global (global)
  global.btoa = encode;
}

// Comprobar si la función atob (Base64 decode) no está disponible en el entorno global
if (!global.atob) {
  // Asignar la función de decodificación Base64 (decode) al objeto global (global)
  global.atob = decode;
}


Dentro de la función principal App definiremos un estado para almacenar la ruta
donde se guardará el doc. pdf, veremos también el esquema

export default function App() {

  const [pdfUri, setPdfUri] = useState(null);

// .....

 async function createAndSavePDF() {
//...
}

const downloadPDF = async () => {
//...
}
useEffect (() => {
createAndSavePDF()
}, [])

return (
<></>
)
}


A continuación crearemos una función llamada createAndSavePDF que contendrá toda
la lógica de creación y guardado de un archivo PDF



async function createAndSavePDF() {

    try {

      console.log('Generando el PDF...');

      // Accediendo al archivo de fuente personalizada AlexBrush-Regular.ttf

      // Comprobando si el directorio 'Fonts' en el sistema de archivos
// local existe.
      if (!(await FileSystem.getInfoAsync(FileSystem.documentDirectory +
        'Fonts')).exists) {
        // Si no existe, crea el directorio 'Fonts' en el sistema de archivos local.
        await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory +
          'Fonts');
      }
      // Comprobando si el archivo 'AlexBrush-Regular.ttf' en el directorio
// 'Fonts' existe.
      if (!(await FileSystem.getInfoAsync(FileSystem.documentDirectory +
        'Fonts/AlexBrush-Regular.ttf')).exists) {
        // Si no existe, descarga el archivo 'AlexBrush-Regular.ttf'
//desde la ubicación
        //de activos ('assets/fonts/CoconutOil.otf')
        await FileSystem.downloadAsync(
          Asset.fromModule(require('./assets/fonts/CoconutOil.otf')).uri,
          FileSystem.documentDirectory + 'Fonts/AlexBrush-Regular.ttf'
        );
      }
      // Accediendo a los datos binarios del archivo 'AlexBrush-Regular.ttf'.
      const ttfBinary = await FileSystem.readAsStringAsync(FileSystem
        .documentDirectory +
        'Fonts/AlexBrush-Regular.ttf', {
        encoding: FileSystem.EncodingType.Base64,
      });

      // Crea un nuevo documento PDF
      const pdfDoc = await PDFDocument.create();

      // Registro de fontkit
      pdfDoc.registerFontkit(fontkit)

      // Agregando una página al documento
      const pageWidth = 8.5 * 72; // 8.5 pulgadas convertidas a puntos
      const pageHeight = 11 * 72; // 11 pulgadas convertidas a puntos

      const page = pdfDoc.addPage([pageWidth, pageHeight]);

      // Registrando la fuente personalizada estandar
      const font = await pdfDoc.embedFont(StandardFonts.Helvetica);

      // Registrando la fuente personalizada externa
      const customFont = await pdfDoc.embedFont(ttfBinary, {
        custom: true,
      });

      // Agregando contenido al PDF
      page.drawText('Hola, Mundo', {
        x: 270,
        y: 600,
        size: 12,
        font: font,
        colorRgb: [0, 0, 0],
      });

      page.drawText('Cómo estás hoy', {
        x: 250,
        y: 560,
        size: 24,
        font: customFont, // Fuente personalizada
        colorRgb: [0, 0, 0],
      });

      // Convertir el PDF a bytes
      const pdfBytes = await pdfDoc.save();

      // Guardar el archivo PDF en el almacenamiento local
      const fileUri = `${FileSystem.documentDirectory}mi_archivo.pdf`;

      // Convertir pdfBytes a una cadena Base64 utilizando btoa
      const pdfBase64 = btoa(String.fromCharCode(...pdfBytes));

      await FileSystem.writeAsStringAsync(fileUri, pdfBase64, {
        encoding: FileSystem.EncodingType.Base64,
        uri: fileUri,
      });

      console.log('El archivo PDF se ha generado y guardado.');
      setPdfUri(fileUri);

    } catch (error) {
      console.error('Error al generar y guardar el PDF:', error);
    }
  };

Con la función anterior hemos logrado crear un nuevo documento, cargar la fuente
personalizada, y embeberla en el documento pdf el cual se guarda en una ubicación
por default dentro de los archivos del dispositivo.

Ahora crearemos la función downloadPDF para poder seleccionar una ruta personalizada
a través de algún explorador de archivos, y guardar ahí el documento PDF, o bien
compartirlo por medio de las redes sociales:
const downloadPDF = async () => {
    if (pdfUri) {

      console.log('Ruta del archivo:', pdfUri);

      const result = await Sharing.shareAsync(pdfUri);
      // const { status } = await Sharing.shareAsync(pdfUri);

      if (result) {
        const { status } = result;
 
        if (status === 'done') {
          console.log('El archivo PDF se descargó exitosamente.', status);
        } else {
          console.error('No se pudo completar la descarga del archivo PDF.', status);
        }
      } else {
        console.error('No se pudo compartir el archivo PDF.');
        console.error('Result: ', result);
      }
    } else {
      console.error('El archivo PDF no existe.');
    }
  };
Con esto podremos asegurarnos de colocar el archivo en el lugar que queramos. Si elegimos guardarlo con
el explorador de archivos, luego podríamos ir manualmente al lugar donde lo guardamos y elegir abrirlo
con algún gestor de archivos PDF

Finalmente configuramos la vista con un simple botón que nos permitirá guardar el archivo:

 return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>
        Ejemplo de generación y descarga de PDF en React Native
      </Text>
      <Button title="Guardar PDF"
      onPress={downloadPDF}
      // onPress={()=> saveToDocumentPicker(pdfUri)}
      />
    </View>
  );

Código completo:

 
import * as FileSystem from 'expo-file-system';                        
  import * as Sharing from 'expo-sharing';
  import { PDFDocument, StandardFonts } from 'pdf-lib';
  import React, { useEffect, useState } from 'react';
  import { Button, Text, View } from 'react-native';
  import fontkit from '@pdf-lib/fontkit';
  import { decode, encode } from 'base-64';
  import { Asset } from 'expo-asset';

  // Comprobar si la función btoa (Base64 encode) no está disponible
  // en el entorno global
  if (!global.btoa) {
    // Asignar la función de codificación Base64 (encode) al objeto
    // global (global)
    global.btoa = encode;
  }

  // Comprobar si la función atob (Base64 decode) no está disponible
  //  en el entorno global
  if (!global.atob) {
    // Asignar la función de decodificación Base64 (decode) al objeto
    // global (global)
    global.atob = decode;
  }


  export default function App() {

    const [pdfUri, setPdfUri] = useState(null);

    async function createAndSavePDF() {

      try {

        console.log('Generando el PDF...');

        // Accediendo al archivo de fuente personalizada AlexBrush-Regular.ttf

        // Comprobando si el directorio 'Fonts' en el sistema de
        // archivos local existe.
        if (!(await FileSystem.getInfoAsync(FileSystem.documentDirectory +
          'Fonts')).exists) {
          // Si no existe, crea el directorio 'Fonts' en el
          // sistema de archivos local.
          await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory +
            'Fonts');
        }
        // Comprobando si el archivo 'AlexBrush-Regular.ttf'
        // en el directorio 'Fonts' existe.
        if (!(await FileSystem.getInfoAsync(FileSystem.documentDirectory +
          'Fonts/AlexBrush-Regular.ttf')).exists) {
          // Si no existe, descarga el archivo 'AlexBrush-Regular.ttf'
          // desde la ubicación
          //de activos ('assets/fonts/CoconutOil.otf')
          await FileSystem.downloadAsync(
            Asset.fromModule(require('./assets/fonts/CoconutOil.otf')).uri,
            FileSystem.documentDirectory + 'Fonts/AlexBrush-Regular.ttf'
          );
        }
        // Accediendo a los datos binarios del archivo 'AlexBrush-Regular.ttf'.
        const ttfBinary = await FileSystem.readAsStringAsync(FileSystem
          .documentDirectory +
          'Fonts/AlexBrush-Regular.ttf', {
          encoding: FileSystem.EncodingType.Base64,
        });

        // Crea un nuevo documento PDF
        const pdfDoc = await PDFDocument.create();

        // Registro de fontkit
        pdfDoc.registerFontkit(fontkit)

        // Agregando una página al documento
        const pageWidth = 8.5 * 72; // 8.5 pulgadas convertidas a puntos
        const pageHeight = 11 * 72; // 11 pulgadas convertidas a puntos

        const page = pdfDoc.addPage([pageWidth, pageHeight]);

        // Registrando la fuente personalizada estandar
        const font = await pdfDoc.embedFont(StandardFonts.Helvetica);

        // Registrando la fuente personalizada externa
        const customFont = await pdfDoc.embedFont(ttfBinary, {
          custom: true,
        });

        // Agregando contenido al PDF
        page.drawText('Hola, Mundo', {
          x: 270,
          y: 600,
          size: 12,
          font: font,
          colorRgb: [0, 0, 0],
        });

        page.drawText('Cómo estás hoy', {
          x: 250,
          y: 560,
          size: 24,
          font: customFont, // Fuente personalizada
          colorRgb: [0, 0, 0],
        });

        // Convertir el PDF a bytes
        const pdfBytes = await pdfDoc.save();

        // Guardar el archivo PDF en el almacenamiento local
        const fileUri = `${FileSystem.documentDirectory}mi_archivo.pdf`;

        // Convertir pdfBytes a una cadena Base64 utilizando btoa
        const pdfBase64 = btoa(String.fromCharCode(...pdfBytes));

        await FileSystem.writeAsStringAsync(fileUri, pdfBase64, {
          encoding: FileSystem.EncodingType.Base64,
          uri: fileUri,
        });

        console.log('El archivo PDF se ha generado y guardado.');
        setPdfUri(fileUri);

      } catch (error) {
        console.error('Error al generar y guardar el PDF:', error);
      }
    };

    const downloadPDF = async () => {
      if (pdfUri) {

        console.log('Ruta del archivo:', pdfUri);

        const result = await Sharing.shareAsync(pdfUri);

        if (result) {
          const { status } = result;

          if (status === 'done') {
            console.log('El archivo PDF se descargó exitosamente.', status);
          } else {
            console.error('No se pudo completar la descarga del archivo PDF.', status);
          }
        } else {
          console.error('No se pudo compartir el archivo PDF.');
          console.error('Result: ', result);
        }
      } else {
        console.error('El archivo PDF no existe.');
      }
    };


    useEffect(() => {
      createAndSavePDF();
    }, []);

    return (
      <View style={{ flex: 1, justifyContent: 'center',
alignItems: 'center' }}>
        <Text>Ejemplo de generación y descarga de PDF en React Native</Text>
        <Button title="Guardar PDF"
          onPress={downloadPDF}
        />
      </View>
    );
  }


La aplicación se vería así: