Composición con iconos de documentos y presentaciones de Google.

BAS#005 | Generando un documento de resumen de una presentación con Apps Script

El mes de agosto pasó 🌞, comienza septiembre 🍂 (winter is coming, my friend 🐉). Se acabó lo bueno. O tal vez comience ahora, chi lo sa?

Sea como sea, parece un buen momento para... sí, lo has adivinado, ¡una nueva entrega de tus Básicos Apps Script!

Debo decir que esta píldora BAS llevaba durmiendo el sueño de los justos (y justas, por supuesto) desde hace un montón de tiempo. Según el historial de versiones del documento de Google que usé para montarlo, que no suele equivocarse, desde julio de 2021 😱.

De hecho, se supone que esto tenía que haber sido el BAS#003, pero por aquel entonces se cruzaron en mi camino otras cosas excitantes sobre las que escribir (esta y también esta) que mandaron mis intenciones a tomar viento.

Y es que este BAS es la continuación natural del BAS#002, en el que veíamos cómo exportar a una carpeta de Google Drive todas las diapositivas de una presentación como archivos PNG.

En los comentarios finales de aquel artículo mencionaba ciertas mejoras. Algunas, como [1️⃣] la gestión de excepciones, ya las hemos discutido en el BAS#004, pero otras como [2️⃣] y [3️⃣]... ay, amigo o amiga, a esas otras vamos a hincarles el diente ahora mismo.

Apartado "siguientes pasos" del BAS#003.
BAS#002: Cómo mola tirar de hemeroteca para hacer que todo parezca tener sentido.

En este BAS aprenderás a generar un documento de texto de resumen con las miniaturas y notas obtenidas de las diapositivas a partir de una presentación de Google. Para ello usaremos los servicios Apps Script de Documentos (principalmente) y Presentaciones. Y además, lo harás de manera eficiente gracias al fantástico método UrlFetchApp.fetchAll().

Pero basta de excusas y lamentos y resolvamos este BAS de "fondo de armario" antes de que el tufillo a naftalina sea insoportable.

 

TABLA DE CONTENIDOS

Descripción del problema

Como es costumbre en {BAS}, podremos cuanto antes las cartas sobre la mesa con un lindo diagrama funcional que describa la automatización que pretendemos construir.

Diagrama de la automatización del BAS#005.
Un documento de texto de Google Docs generado automáticamente a partir de una presentación de Google. ¡Qué hermosura!

Partiremos de una presentación de Google corriente y moliente), con sus diapositivas y notas del orador (qué viejuno me suena eso de "notas del orador", pardiez 😅).

Queremos emular la función imprimir páginas de notas de Microsoft Powerpoint, que es capaz de hacerlo disponiendo múltiples diapositivas por página, acompañadas de sus notas, si las hay.

Opciones de impresión de una presentación de Microsoft Powerpoint.
¿Una captura de pantalla de una aplicación de Microsoft en este blog? ¡Deprisa, llamen al padre Damien Karras!

La cruda realidad es que aunque nuestro entrañable editor de presentaciones de Google también puede imprimir, como Powerpoint, varias diapos por hoja, no es capaz en cambio de incluir las notas con las que el autor de la presentación haya tenido a bien acompañarlas.

Pero lo veo y subo (lo de emular). Mejoremos eso, pardiez, que como todo el mundo sabe darle a la manivela para que salgan pedefes está muy demodé!

🎯 Por tanto, nuestro script:

1️⃣ Generará miniaturas a partir de todas las diapositivas de una presentación de Google y las dispondrá, ordenadas y organizadas en grupos de tres, en tantas páginas de un documento de texto (de Google también) como sean necesarias, junto a sus notas. Cada imagen enlazará además con la diapositiva a partir de la cual se ha generado.

2️⃣ Anexará a cada página un espacio para que podamos añadir comentarios adicionales, por ejemplo durante la preparación de la exposición de la presentación.

3️⃣ 
Creará también una sección de encabezado en el documento con el nombre de la presentación original, un 🔗 enlace a ella, así como la fecha y hora de generación

Sin que sirva de precedente en un BAS, esta vez te muestro ya en acción el script que vamos a montar, para que así te hagas inmediatamente una idea clara del problema y de lo que esperamos conseguir al resolverlo. O dicho de otro modo, para ponerte los dientes largos.

Animación que demuestra el funcionamiento de la automatización.
¡BAS#005 en acción!

Ya sé que hoy en día el menos pintado te saca del zurrón una chromecosa en cualquier momento y lugar, que hay móviles plegables con pantallas enormes, que las conexiones de datos son ubicuas y que, en su ausencia, existe algo llamado "modo sin conexión". Pero aún en estos tiempos de incontenible e hiperconectada modernidad, disponer de un clásico resumen anotado de una presentación, en formato digital o incluso tal vez impreso, puede resultar en ocasiones de utilidad a la hora de preparar su exposición pública.

En este BAS no estamos enviando cohetes a la Luna, pero el problema que aspiramos a resolver ya tiene la suficiente enjundia como para permitirnos poner en práctica un montón de cosas vistas en píldoras BAS anteriores, familiarizarnos con el servicio Apps Script de Documentos y, de propina, aprender algún que otro truco nuevo.

Solución GAS

Alojaremos nuestro script en un documento de texto, que podrás utilizar como plantilla posteriormente. Lo encontrarás en esta carpeta, junto con una presentación con la que hacer pruebas inmediatamente. Y como siempre en una píldora BAS, también en el repositorio GitHub de {BAS}.

📂 BAS#005 Generar doc resumen presentación 📂

Preparativos

Al igual que en el BAS#002, vamos a utilizar un menú personalizado para ejecutar el script. Comencemos por ahí.

/**
 * Genera un tabla de miniaturas de diapositivas que incluye los comentarios del presentador
 * a partir de una presentación de Google en un documento de Google Docs.
 *
 * Demo: https://drive.google.com/drive/folders/1Ui5QZRRUb0kkpzTTyNzjQzsiI9SjPWqv
 * 
 * BAS#005 Copyright (C) Pablo Felip (@pfelipm) · Se distribuye bajo licencia MIT.
 */

/**
 * Añade menú personalizado
 */
function onOpen() {
 DocumentApp.getUi().createMenu('BAS#005')
    .addItem('Generar resumen de presentación', 'generarResumen')
    .addToUi();
}

Nos movemos raudos y veloces a la función generarResumen().

/**
 * Genera tabla de miniaturas a partir del ID de la presentación
 */
function generarResumen() {
  
  const ui = DocumentApp.getUi();
  let idPresentacion;
  
  try {

    do {
    
      const respuesta = DocumentApp.getUi().prompt('❔ BAS#005', 'Introduce ID de la presentación:', ui.ButtonSet.OK_CANCEL);
      if (respuesta.getSelectedButton() == ui.Button.CANCEL) throw 'Generación cancelada.';
      // Lo apropiado sería usar una expresión regular para extraer el ID a partir del URL, pero eso lo dejaremos para otro BAS...
      idPresentacion = respuesta.getResponseText();
    
    } while (!idPresentacion);

Lo primero será pedirle al usuario que introduzca el IDentificador del archivo en Drive que contiene la presentación sobre la que el script va a trabajar. Ya sabes, esa enigmática secuencia alfanumérica que forma parte de su URL:

https://docs.google.com/presentation/d/1BKOmQPODK5iONbGtnm4o86-le3-D_Zwr2AVUpDhXMmc/edit#slide=id.gce6d61cc86_0_0

Para ello obtenemos el objeto Ui en el contexto del servicio de Documentos de Apps Script y usamos su método prompt() para abrir un sencillo cuadro de diálogo en el que el usuario podrá escribir el ID (línea 31).

Cuadro de diálogo de solicitud del ID de la presentación.
Un sencillo cuadro de diálogo de entrada de datos.

Este método admite hasta tres parámetros, los usaremos todos para especificar:

  • El título que se mostrará en la parte superior del cuadro de diálogo.
  • Unas breves instrucciones.
  • Los botones que deseamos que aparezcan, en este caso los de ACEPTAR y CANCELAR (ui.ButtonSet.OK_CANCEL).

Esta petición se encuentra dentro de una estructura do...while, que pedirá con insistencia el ID hasta que el usuario introduzca una cadena de texto no vacía...

} while (!idPresentacion);

...o haga clic sobre el botón CANCELAR, en cuyo caso se abortará la ejecución lanzando una excepción con throw, tal y como aprendimos en el BAS#004 (línea 32).

throw 'Generación cancelada.';

Un detalle: fíjate en que aquí declaramos idPresentacion con let. Esto es así porque:

  1. Su valor se debe asignar necesariamente dentro del bucle que interroga al usuario hasta que este cancela o introduce una respuesta válida.
  2. Necesitaremos utilizar el valor almacenado en esta variable más abajo en el código. Si la declaramos con const dentro del bucle, como hemos hecho con respuesta, su ámbito (scope de JavaScript) quedaría limitado a él.

☝ Probablemente nuestro sufrido usuario encontraría más natural que le solicitáramos el URL de la presentación, en lugar de su ID. Eso nos llevaría a utilizar una expresión regular para extraerlo trivialmente a partir de aquel, pero como aún no hemos hablado de expresiones regulares en un BAS lo vamos a dejar para otra ocasión por no liarla aún más parda hoy.

Vale, ya tenemos el ID. ¿Ahora qué? Pues ahora toca inicializar unas cuantas variables que necesitaremos más adelante.

    const contenido = DocumentApp.getActiveDocument().getBody();
    const presentacion = SlidesApp.openById(idPresentacion);
    const diapos = presentacion.getSlides();
    const diaposId = [];
    const anchuraDiapo = presentacion.getPageWidth();
    const alturaDiapo = presentacion.getPageHeight();
    const anchuraMiniatura = 285;
    const alturaMiniatura = alturaDiapo * anchuraMiniatura / anchuraDiapo;

VariableDescripción
contenidoObjeto de tipo Body (cuerpo del documento de texto).
presentacionObjecto de tipo Presentation (la presentación objetivo).
diaposIdVector que contendrá los ID específicos de cada diapositiva de la presentación, se utilizarán para generar la miniatura de cada una de ellas.
anchuraDiapoAnchura de las diapositivas (puntos).
alturaDiapoAltura de las diapositivas (puntos). Junto con la anchura se utiliza para calcular su relación de aspecto.
anchuraMiniaturaAnchura deseada (px) de la anchura de las miniaturas que generará el script.
alturaMiniaturaAltura de las miniaturas (px), calculada usando la anchura establecida y la relación de aspecto de las diapositiva de modo que preserven su proporción original.

Miniaturas, un poco de magia procedente del BAS#002

Te decía antes que no vamos ni siquiera a intentar obtener las miniaturas de las diapositivas usando el servicio Apps Script de Presentaciones, ni muchísimo menos su contrapartida avanzada.

⏸ Lo haremos usando el mismo truco que encontrarás detallado en el apartado 2.4 Generar y guardar las imágenes PNG del BAS#002 basado en la construcción de este URL especial. Asegúrate de entender bien lo que te contaba allí antes de proseguir (yo te espero justo aquí).

Banner sitio web URL mágicos de Google.
¡Más URL mágicos de Google en googleurltricks.tk!

¿Leído? Estupendo, continuemos entonces ▶️...

La novedad radica ahora en que vamos a obtener todas las miniaturas de una vez.

En lugar de lanzar tantas peticiones secuenciales con el método fetch() como diapositivas haya en la presentación, recurriremos ahora a su hermano mayor, fetchAll().

Este método requiere un vector de URL (o de objetos que contengan adicionalmente una serie de parámetros opcionales, que no vamos a usar hoy) y lanza todas las peticiones en paralelo, recibiendo seguidamente un vector análogo que contendrá las respuestas resultantes de cada petición. La diferencia en tiempo de ejecución puede ser dramática, tanto más cuantas más peticiones deban realizarse.

Permíteme un último detalle, extremadamente relevante, relativo a la obtención de las miniaturas antes de espetarte sin contemplaciones el código de esta parte del script.

En el BAS#002 usábamos el servicio de Drive para crear una carpeta en la que almacenar las miniaturas de las diapositivas. Por tanto, el token de autorización OAuth del script, que obteníamos con...

ScriptApp.getOAuthToken()

...ya estaba imbuido de los permisos necesarios para permitirnos leer la presentación, aunque esta no estuviera compartida de manera pública, por medio de su URL (esta era la magia ✨):

https://www.googleapis.com/auth/drive.readonly

Me refiero a esos permisos o scopes granulares que el usuario debe concederle de manera expresa al script la primera vez que se ejecuta para que pueda acceder a la información contenida en determinados servicios de su cuenta de Google.

¡Pero en el script que estamos desarrollando ahora no usamos Drive para nada!

La solución (un poco rocambolesca, debo decir) pasa por invocar de cualquier manera algún método de la clase DriveApp que precise para funcionar del ámbito de seguridad que nos hace falta... ¡dentro de una línea comentada!

Por increíble que parezca, esto basta para que el motor de Apps Script incluya este permiso en el cuadro de diálogo de autorización.

Cuadro de diálogo de autorización con el permiso de lectura de archivos de Drive.
Este permiso es el que hace posible la "magia".

¿Y si no tenemos la precaución de hacer esto? Pues entonces el script fallará al ejecutarse porque no podrá leer la presentación, a menos, claro, que la hayas compartido con cualquier persona con el enlace, algo que obviamente podría no ser siempre aceptable.

Error al tratar de leer la presentación con fetchAll() por falta de permisos.
Error 401: El script falla porque porque carece de las credenciales de seguridad que le permitan leer el recurso solicitado.

Sí, sí, el código (la movida con los permisos, en la línea 48):

    // 👇 Necesario para que access_token adquiera el scope necesario (https://www.googleapis.com/auth/drive.readonly)
    // DriveApp.getFileById(idPresentacion);
    // Sí, increiblemente basta con que esté comentado para que se incluya su scope en el cuadro de diálogo de autorización

    // Construye el URL "mágico" de generación de PNG (ver apartado 2.4 del BAS#002)
    const url = `https://docs.google.com/presentation/d/${idPresentacion}/export/png?access_token=${ScriptApp.getOAuthToken()}`; 

    // Ahora obtendremos cada diapo como imagen PNG... ¡en paralelo!
    const peticionesPng = diapos.map(diapo => {

      // Generamos también un array con todos los ID de las diapositivas
      const diapoId = diapo.getObjectId();
      diaposId.push(diapoId)

      // URL para obtener la imagen de la diapositiva en png
      return {'url': `${url}&pageid=${diapoId}`}
      
    });
    
    const imagenesPng = UrlFetchApp.fetchAll(peticionesPng);
    const numDiapos = imagenesPng.length;

Como puedes apreciar, y dejando de lado los detalles escabrosos que tienen que ver con esos URL de exportación de las diapos en formato PNG tan cuidadosamente construidos, lo que hacemos aquí es:

  1. Recorrer mediante el método Array.prototype.map(), que ya conocemos del BAS#004, la totalidad de las diapositivas que se han hallado en la presentación objetivo (líneas 55 - 64)  para:
    • Enumerar sus identificadores en el vector diaposId, que necesitaremos más tarde para generar los enlaces de cada miniatura hacia su diapositiva correspondiente.
    • Construir un vector (peticionesPng) con esos URL mágicos que permiten obtener las miniaturas PNG de cada diapositiva.
  2. Lanzar todas las peticiones fetch en paralelo (línea 66).
  3. Contabilizar el número de miniaturas que se han generado (línea 67).

Montando el documento

Con las miniaturas en el bolsillo, toca generar la estructura del documento que resumirá la presentación, pero solo seguiremos adelante tras asegurarnos de que realmente haya algo que hacer. 

  // ¿Sabías que una presentación puede existir pero NO tener ninguna diapositiva?
    if (numDiapos == 0) throw 'La presentación seleccionada no contiene dispositivas.'

    // Tenemos diapos que obtener, SOLO ahora ahora borraremos el cuerpo del documento para generar nuevas miniaturas
    contenido.clear();
    
    // Esta vez además mediremos el tiempo de ejecución
    const t1 = new Date();

☝ Esta comprobación la podríamos haber realizado antes de tratar de obtener las miniaturas consultando el tamaño del vector diapos, que ya se ha inicializado en la línea 40, pero dado que parece prudente volver a verificarlo tras generar las miniaturas, he optado por postponerla hasta el último momento. En cualquier caso, invocar a fetchAll() con un vector de URL vacío (si no hubiera diapos) no supone un problema, simplemente obtendríamos un vector de resultados también vacío. 

Pasemos a describir ahora, en orden de inserción, cada elemento que el script añadirá al documento.

1️⃣ Una tabla con dos filas y dos columnas para cada diapositiva:

  • En la segunda fila a la izquierda situaremos la imagen PNG, a modo de miniatura, que enlazará con la diapositiva a partir de la cual se ha generado.
  • A la derecha, las notas de la diapositiva, si las hay.
  • La primera fila se utilizará como encabezado de la tabla, mostrando el número de la diapositiva y la cantidad total en la presentación y el texto "Notas de la diapositiva", usando un formato diferenciado (texto en negrita de color blanco  y fondo de color sólido en las celdas).
Cada una de las tablas utilizadas para mostrar miniatura y notas.

Este bloque de código se encarga de construir la tabla en la que se insertará cada imagen en miniatura de las diapositivas y sus notas:

    // Inserta cada imagen en una tabla:
    // |----------------------------------------------|
    // | Diapositiva nº n de m  |  Notas diapositiva  |
    // |----------------------------------------------|
    // |   Imagen en miniatura  |  Notas presentador  |
    // |----------------------------------------------|
    // Estructura de un DOC https://developers.google.com/apps-script/guides/docs?hl=en#structure_of_a_document

    imagenesPng.forEach((imagen, indiceDiapo) => {
      
      // Si no estamos en la 1ª página añade un párrafo para que todas las tablas comiencen en la misma posición
      // dado que la 1ª página siempre contiene una línea en blanco
      if ((indiceDiapo + 1) % 3 == 1 && indiceDiapo > 2) contenido.appendParagraph('');

      // Construye la tabla para cada diapositiva
      const tabla = contenido.appendTable([[`Diapositiva nº ${indiceDiapo + 1} de ${numDiapos}`, 'Notas de la diapositiva']]);
      const fila = tabla.appendTableRow();
      fila.appendTableCell().appendImage(imagen.getBlob()).setWidth(anchuraMiniatura).setHeight(alturaMiniatura)
        .setLinkUrl(`${presentacion.getUrl()}#slide=id.${diaposId[indiceDiapo]}`);
      fila.appendTableCell().appendParagraph(diapos[indiceDiapo].getNotesPage().getSpeakerNotesShape().getText().asString());
      
      // Formatea celdas de la tabla (encabezado y bordes)
      const atributosEncabezado = {};
      atributosEncabezado[DocumentApp.Attribute.BOLD] = true;
      atributosEncabezado[DocumentApp.Attribute.FOREGROUND_COLOR] = '#FFFFFF';  // Blanco
      tabla.getRow(0).setAttributes(atributosEncabezado);
      tabla.getRow(0).getCell(0).setBackgroundColor('#4E5D6C'); // Carbón
      tabla.getRow(0).getCell(1).setBackgroundColor('#4E5D6C');
      tabla.setBorderColor('#4E5D6C');

Por una parte, gracias a indiceDiapo, el segundo parámetro del forEach(), sabremos en todo momento qué diapositiva se está procesando en cada iteración del bucle. Esto resulta relevante a la hora de hacer ciertas cosillas:

  • Insertar un párrafo en blanco inicial, cuando corresponda (línea 90).
  • Generar el contador de diapositivas en el encabezado de cada tabla (línea 93).
  • Construir el URL que permite enlazar con la diapositiva en la presentación (línea 96).
  • Insertar las notas de cada diapositiva (línea 97).

Como puedes comprobar, en las líneas 86 - 97 se emplean un buen puñado de métodos pertenecientes a los servicios Apps Script de Documentos y Presentaciones, un festival del que ya no nos deberíamos asustar a estas alturas. Te los resumo y enlazo con la documentación oficial, creo que la mayoría de ellos se explican por sí mismos, así que no me detendré a comentarlos pormenorizadamente:

📄 Servicio de Documentos🖼 Servicio de Presentaciones

Los documento de Google Docs están construidos mediante un conjunto de objetos dispuestos en una interesante jerarquía estructurada que aquí puedes apreciar en todo su esplendor. No obstante, no todos los elementos pueden aparecer siempre dentro de otro dado, por lo que existen ciertas limitaciones creativas.

Jerarquía de objetos de un Google Doc.
Debería haber pintado muchas más cajas y flechas, pero no me cabían. Seguro que pillas la idea, ¿verdad?

Lo que estamos haciendo es utilizar las clases Apps Script que representan a estas entidades y sus métodos pertinentes para ir insertando elementos hasta lograr el resultado deseado, ni más ni menos.

En las líneas 95 y 97 nos aprovechamos de que al aplicar un método sobre un objeto se devuelve otro objeto, en ocasiones de la misma clase, para encadenar la invocación de un nuevo método sobre él. Esto constituye una practica habitual al utilizar los distintos servicios Apps Script.

Diagrama que representa el encadenamiento de métodos.
De una fila vacía a una imagen de la anchura y altura deseadas con enlace.

Fíjate en cómo tiramos de nuevo (en un BAS)  de esos espero que ya no tan misteriosos blobs  para colocar las imágenes PNG que hemos obtenido de la presentación en su lugar dentro de la estructura del documento que estamos generando.

Por otra parte, en las líneas 100 - 106, lo que hacemos es aplicar ciertos ajustes de formato sobre las tablas que se van creado. Para ello se usan métodos como setAttributes(), setBackgroundColor() y setBorderColor(), que nuevamente creo que no merecen mayor 

☝ ¡Bien visto! La variable atributosEncabezado no cambia en el transcurso de las iteraciones del bucle forEach. O lo que es lo mismo, aplicaremos del mismo modo negrita y color blanco al texto de la fila de encabezado de todas y cada una de las tablas que crearemos. Por tanto, lo más limpio es situar las instrucciones de las líneas 100 - 102 justo antes del bucle que arranca en la línea 86, pero por una cuestión de claridad me ha parecido razonable dejarlas junto al resto del código que manipula el formato de la tabla.

2️⃣ Cada tres diapositivas insertaremos una zona de notas vacía y un salto de página. La idea es que cada grupo de tres miniaturas quede siempre en la misma página. Además, usaremos negrita en el título de esta sección.

Espacio para añadir notas en la parte inferior de cada página.
      // Inserta una zona de notas y un salto de página cada 3 diapositivas (y al final)
      // para garantizar que las tablas no queden cortadas

      if ((indiceDiapo + 1) % 3 == 0 || indiceDiapo == numDiapos - 1){

        contenido.appendParagraph('Otras notas:').setSpacingAfter(3).setBold(true);
        contenido.appendParagraph('').setBold(false);
        contenido.appendPageBreak();
        
      }

    });

3️⃣ Por último, se añadirá un encabezado con lo siguientes elementos:

  • Primera línea: título de la presentación y enlace a ella.
  • Segunda línea: fecha y hora de generación del documento de resumen, con un tamaño de fuente menor.
Encabezado con información sobre la presentación.
    // Inicializa el encabezado del documento
    let encabezado;
    encabezado = DocumentApp.getActiveDocument().getHeader();
    if (encabezado) encabezado.clear();
    else encabezado = DocumentApp.getActiveDocument().addHeader();

    // Construye el encabezado con enlace a la presentación
    encabezado.appendParagraph('Miniaturas de ').setFontSize(11)
      .appendText(presentacion.getName())
      .setLinkUrl(presentacion.getUrl());

    // Añade marca de tiempo para datar el documento generado
    // Es necesario indicar el "locale" porque Session.getActiveUserLocale() ahora mismo no funciona bien >> https://issuetracker.google.com/issues/179563675
    encabezado.appendParagraph(`Generado el ${t1.toLocaleDateString('es')} a las ${t1.toLocaleTimeString('es')}`)
      .setFontSize(8)
      .setSpacingAfter(6);

Ahora, en lugar de trabajar sobre el cuerpo del documento lo haremos sobre su encabezado

⚠️ Un documento nuevo no tiene sección de encabezado. Esto hace que tengamos que controlar lo que nos devuelve la llamada al método getHeader() y, si es null, crearlo inmediatamente antes de tratar de meter cosas en él, de lo contrario desencadenaríamos un error en tiempo de ejecución.

Esto es justamente de lo que se encargan las líneas 122 - 125. Si eldocumento ya cuenta con un encabezado se borra su contenido (no la propia sección de encabezado) con el método clear(). En caso contrario se crea uno nuevo mediante addHeader().

A continuación:

  • En las líneas 128 - 130 se añade un párrafo que contiene el nombre de la presentación como enlace al propio archivo que la contiene.
  • En 134 - 136  se insertan la fecha y hora actuales, a modo de marca de tiempo, para datar la versión resumida de la presentación que hemos generado.

Ten en cuenta que las miniaturas son instantáneas de las diapositivas tomadas en un momento dado, no se trata de objetos vinculados que se actualizan automáticamente cuando se producen cambios posteriores en la presentación.

Fin del proceso

La últimas líneas de código del script están dedicadas a dos cosas:

1️⃣ Mostrar en pantalla un mensaje de éxito que indique el número de miniaturas generadas y el tiempo aproximado empleado.

    const t2 = new Date();

    // Mensaje de fin del proceso (con éxito)
    DocumentApp.getUi().alert('🟢 BAS#005',
                              `${numDiapos} miniatura(s) generada(s) en aproximadamente ${Math.round((t2 - t1)/1000)}".`,
                              ui.ButtonSet.OK);

En el servicio Apps Script de Documentos no disfrutamos del bendito toast() ,que es exclusivo de las hojas de cálculo, así que nos tendremos que conformar con usar alert(), similar a prompt(), pero con la particulartidad de que este solo muestra un mensaje en pantalla, sin que medie introducción de datos por parte del usuario.

Mensaje informativo del resultado, con éxito, de la ejecución del script emitido mediante el método alert().
Lanzar una alerta cuando corresponde no cuesta dinero y hará a tus usuarios más felices (o al menos más informados).

2️⃣ Declarar el bloque catch que se encargará de cazar las excepciones, tanto las inesperadas como las que lanzamos de manera intencionada cuando el usuario cancela el proceso o la presentación no contiene diapositivas, y mostrar una alerta con un mensaje de error apropiado.

  } catch (e) {
    DocumentApp.getUi().alert('🔴 BAS#005', typeof e == 'string' ? e : `Error interno: ${e.message}.`, ui.ButtonSet.OK);
  }

}

Ya hablamos largo y tendido de este patrón en el BAS#004, así que... ¡lo tenemos ✌!

Comentarios finales y siguientes pasos

Bueno, pues hemos llegado al final de otro Básicos Apps Script. Y ya son ¡cinco!, tras los tres últimos casi del tirón.

Para celebrarlo, no te voy a dejar esta vez muchos deberes. Tan solo me gustaría redondear todo lo que te he contado sobre el control de errores puntualizando algo relacionado con el uso de los métodos fetch() y fetchAll().

Ya sabemos que estos métodos pueden fallar en tiempo de ejecución al tratar de acceder a un URL. Y que justo por esa razón resulta aconsejable utilizar los bloques try...catch para incorporar rutinas de tratamiento que gestionen con salero esas excepciones.

Cuando lanzamos múltiples peticiones de una vez, como hacemos aquí al tratar de obtener las miniaturas de todas las diapositivas de la presentación con fetchAll(), la cosa va igual. El proceso se detendrá también en el acto cuando cualquiera de ellas falle.

Creo que estarás de acuerdo conmigo en que resultaría mucho menos drástico registrar los errores puntuales que pudieran producirse, pero no cortar por lo sano la ejecución, especialmente cuando estas se han lanzado en paralelo.

¿Qué algunas peticiones han fallado? Perfecto, nos lo apuntamos para explicárselo al usuario, pero nos quedamos con los resultados de las peticiones que sí se han resuelto con éxito. ¡Aquí no se tira nada!

Y para eso tenemos el parámetro opcional muteHttpExceptions. Si es true, un error de fetch no desencadenará una excepción.

Lo que haremos entonces es obtener el código de resultado de la operación mediante el método getResponseCode() de la clase HTTPResponse, comprobar si es uno que indica que la cosa ha ido de maravilla (típicamente 200 - 202), y actuar en consecuencia.

Date cuenta de que fetchAll() devolverá, como es de esperar, un array de objetos de esta clase para recoger el resultado de cada petición asíncrona que le hayamos pedido que lance, así que podremos revisarlos uno a uno.

Te dejo pensando en estas cosas, pero con una ayudita...

Encontrarás una implementación funcional de esta estrategia, un tanto más sofisticada que la de cortar por lo sano, en este artículo en el que se explotan los webhooks de Coda desde Apps Script (artículo que, tengo que decir, disfruté muchísimo preparando).

📝 Coda webhooks 💙 Google Apps Script!

Esto ha sido todo por hoy. Como siempre, la caja de comentarios de aquí abajo queda a tu disposición, al igual que el canal #gas-iniciación de Apps Script Ñ.

Ah, y si eres profe, ¡buena entrada de curso 🍀💪!


Créditos: La imagen de portada utiliza un recurso gráfico soñado por DALL-E-2.

Comentarios