BAS#002 | Exportar diapositivas de una presentación como PNG
📌 Este artículo fue publicado originalmente el 11/04/21 en la plataforma social utilizada para dar soporte a la comunidad de usuarios de GEG Spain entre septiembre de 2019 y diciembre de 2022, plataforma que lamentablemente ya no está disponible. He reproducido aquí el contenido de la publicación prácticamente sin cambios, así que es posible que encuentres en el texto alguna referencia descontextualizada.
TABLA DE CONTENIDOS
Descripción del problema
Todos sabemos que lo único que es indiscutiblemente mejor que una hoja de cálculo son... ¡dos hojas de cálculo!
Pero también es cierto que hay vida más allá de ellas. O al menos eso dicen. Por eso, en esta 2ª 💊 píldora BAS les decimos un hasta luego para tratar de resolver un problema relacionado con las presentaciones de Google.
Como probablemente sepas, podemos exportar como imágenes las diapositivas de estas presentaciones fácilmente utilizando el comando Archivo → Descargar Imagen.
Pero como ves, hay que hacerlo de una en una.
¿Y por qué razón querríamos hacer esto? Pues por ejemplo para utilizar estas imágenes como miniaturas en un documento de texto a modo de guión de una charla o conferencia. Y lo usaremos como excusa para aprender un poco más sobre Apps Script.
Evidentemente, este es un trabajo para GAS. Por tanto, vamos a producir unas líneas de código que:
- Convertirán todas las diapositivas de una presentación de Google a formato PNG.
- Las guardarán en una carpeta junto a la propia presentación, gestionando su creación o actualización, según sea necesario.
- Añadirán un menú a la interfaz de usuario del editor de presentaciones para facilitar la ejecución de este proceso.
El esquema de nuestra automatización:
Como no, en esta carpeta encontrarás los archivos auxiliares necesarios para seguir esta 💊 píldora BAS.
Y aquí todo el script, del tirón. Aunque he salpicado el resto del artículo con numerosos fragmentos de código, igual te apetece mantenerlo en la ventana de al lado mientras lees para que los árboles no te impidan ver el bosque.
Igual te apetece mantenerlo en la ventana de al lado mientras lees esto
Pero antes de comenzar este apasionante BAS quizás quieras echarle un vistazo a alguno de los anteriores. Bueno, alguno, que solo estamos en el segundo 😅. Los tienes todos en el repositorio GitHub de {BAS}.
Aunque cada BAS es independiente del resto (bueno, más o menos), generalmente asumiré que te has leído los anteriores a la hora de explicar con mayor o menor intensidad determinadas cosas. Si paso volando por encima de algún concepto probablemente será porque ya he hablado sobre él con más detalle en algún BAS anterior.
Solución GAS
En esta ocasión utilizaremos nuevamente un script GAS vinculado a una presentación. De este modo, si creas tus presentaciones a partir de ella, en plan plantilla, podrás exportar sus imágenes utilizando este procedimiento. Lo sé, no es el modo más sofisticado de reutilizar nuestra linda creación GAS, pero por el momento no tenemos otro.
Encontrarás el código del script que vamos a crear dentro de la presentación Exportar diapositivas como PNG. Se descompone en varias secciones:
- Añadir un menú personalizado.
- Inicializar algunas cosillas que vamos a necesitar.
- Crear la carpeta de destino de las imágenes exportadas (o borrarla, si ya existe, antes de una nueva exportación.
- Generar las imágenes PNG correspondientes a cada diapositiva y guardarlas en la carpeta.
Planazo, ¿verdad? Pues vamos a divertirnos.
Añadir un menú personalizado
Comenzamos con la pequeña pero matona función onOpen(), que es especial. Pero mucho. Y es que el código en su interior se va a ejecutar cada vez que un usuario con permisos de edición abra la presentación dentro de la que se encuentra.
De hecho, onOpen()
forma parte de lo que en el universo GAS se denominan activadores simples (simple triggers). Hay mucho que decir sobre ellos (y aún más de sus primos más avanzados, los installable triggers), pero por el momento me conformo con que te acuerdes de esto:
- Estas funciones son desencadenadas automágicamente, de modo reactivo, cuando se producen determinados eventos asociados, en general, a los distintos editores de Google. Como te puedes imaginar, esto lleva GAS a otro nivel.
- La función que contiene su código debe tener cierto nombre, a saber,
onOpen()
,onInstall()
,onEdit()
,onSelectionChange()
,doGet()
odoPost()
, de acuerdo con el evento que cause la activación. No le toques ni una letra si quieres que sea invocada correctamente y pueda hacer lo que tienen que hacer. - Los activadores simples dentro de un documento solo se ejecutan cuando quien los abre tiene permisos de edición sobre él. Ya sé que lo he dicho hace un momento, pero lo repito porque es importante no olvidarlo.
- El código dentro del activador no puede hacer lo que le dé la gana. Técnicamente, no puede utilizar servicios de Google Apps Script que requieran autorización. Para que nos entendamos, la autorización es necesaria para acceder a los datos privados del usuario. Con matices, si no hay datos privados de por medio, no es necesaria esa autorización.
- Ah, y una cosita de nada, también tienen que ser capaces de ejecutarse, cada vez que son invocados, en un máximo de 30 segundos.
☝ Sí, lo de traducir trigger por activador es cosa mía. Si lo prefieres, utiliza otros términos como gatillo o desencadenador. Personalmente el primero no me gusta y el segundo me parece farragoso, así que con tu permiso yo seguiré hablando de activadores. Lo de usar avanzado por installable ya es más discutible, si eso.
Por tanto, lo que hacen estas líneas es añadir un nuevo menú en el extremo derecho de la barra de menú estándar del editor de presentaciones cada vez que un usuario con permisos de edición abre la presentación.
/**
* Exporta todas las diapositivas de la presentación como imágenes PNG
* en una carpeta junto a la propia presentación.
* No se realiza ningún control de errores.
* Demo: https://drive.google.com/drive/u/0/folders/1dahbbyFE-3ixc_rHOA8--ufx3idgSGBM
*
* BAS#002 Copyright (C) Pablo Felip (@pfelipm) · Se distribuye bajo licencia MIT.
*/
/**
* Añadir menú personalizado
*/
function onOpen() {
SlidesApp.getUi().createMenu('Slides2PNG')
.addItem('Exportar diapositivas como PNG', 'exportarDiaposPngUrl')
.addToUi();
}
Para ello se usa la clase “padre” del servicio de presentaciones (SlidesApp), a partir de la cual se aplican otros métodos de manera secuencial (en lugar de utilizar una lista de elementos monda y lironda para enumerarlos voy a hacerla esta vez multi nivel para que se vea más claro):
- getUi(): Devuelve una instancia (objeto) de la clase Ui, que representa la interfaz de usuario del editor de presentaciones.
- createMenu(): Nos proporciona como resultado un objeto de la clase Menu, sobre el que utilizaremos dos métodos adicionales para construir nuestro menú:
- addItem(): Le indicamos el texto para la opción del menú y la función que debe invocar en el código al seleccionarse. En nuestro caso solo tenemos una opción, pero podríamos crear otras simplemente añadiendo más líneas como esta. También podríamos crear submenús con el método addSubMenu() e incluso separadores visuales, para delimitar conjuntos de opciones, con addSeparator(), pero eso lo vamos a dejar para otra ocasión.
- addToUi(): Esto es lo que finalmente modifica la interfaz de usuario y hace que el menú que hemos creado se manifieste en pantalla.
Estas modificaciones no son permanentes, por esa razón hay que efectuarlas cada vez que se abre el editor. Y, permíteme que insista, si quien lo hace solo tiene permisos de lectura, nasti de plasti. El menú no será visible para él o ella.
Inicializar cosillas
Vamos con la función principal, exportarDiaposPngUrl()
.
Primeramente obtenemos una referencia al objeto Presentation (nuestra presentación) y su Id) utilizando SlidesApp, la clase padre del servicio de presentaciones.
/* Exporta todas las diapos como png en carpeta de Drive junto a la presentación */
function exportarDiaposPngUrl() {
// Presentación sobre la que estamos trabajando
const presentacion = SlidesApp.getActivePresentation();
const idPresentacion = presentacion.getId();
A continuación, localizamos el archivo de la presentación en Drive mediante el método getFileById() de la clase DriveApp por medio del ID obtenido hace un momento, así como la carpeta en la que se encuentra, que no será otra que el primer elemento del iterador de carpetas que nos devuelve getParents() cuando lo aplicamos sobre el objeto File devuelto por el primero. Recuerda que ya hablamos de esos útiles engendros denominados iteradores en el BAS#001.
// Presentación en Drive
const presentacionDrive = DriveApp.getFileById(idPresentacion);
// Carpeta donde se encuentra la presentación
const carpeta = presentacionDrive.getParents().next();
Ahora ya sabemos dónde debemos crear la carpeta con las imágenes obtenidas al exportar las diapositivas de nuestra presentación, solo nos falta determinar su nombre, que estará formado por:
- El prefijo “Miniaturas”.
- Un espacio.
- El ID de la presentación, entre corchetes, para diferenciar del mejor modo posible la carpeta de cualquier otra que tengamos en nuestra unidad.
// Nombre de la carpeta de exportación para los PNG
const nombreCarpetaExp = `Miniaturas {${idPresentacion}}`;
Esta expresión tan raruna, encerrada entre tildes invertidas, (`...`
) es una plantilla literal ES6. Las plantillas literales resultan muy cómodas a la hora de construir cadenas de texto compuestas por secuencias de caracteres prefijadas y expresiones JavaScript.
Como ves, la cosa no tiene mayor misterio, aparentemente, que utilizar el marcador ${...} para encerrar la o las expresiones a evaluar dentro de la expresión de cadena, evitando de este modo el uso del operador de concatenación de cadenas (+) convencional. Lo anterior es equivalente a esto, que tal vez te resulte más familiar.
// Nombre de la carpeta de exportación para los PNG
const nombreCarpetaExp = `Miniaturas {${idPresentacion}}`;
☝ Aunque estos marcadores ${...}
pueden limitarse, como en nuestro caso, a contener un simple identificador de constante o variable, también podrían alojar elementos más complejos, por ejemplo estructuras de decisión, incluso anidadas. Por tanto, hay situaciones en las que el uso de plantillas literales resultará muy conveniente, pero no abriremos hoy ese melón. No obstante y precisamente por eso, te aconsejo que te familiarices con su uso cuanto antes.
Crear carpeta de exportación
Nos preparamos ahora para el meollo del script.
Primero comprobamos si la carpeta de exportación ya existe como consecuencia seguramente de una exportación previa. En ese caso la fulminaremos, junto con todo su contenido, para evitar duplicados ¡con el mismo nombre! (esto es Drive, amigo o amiga).
// Si la carpeta de exportación ya existe la eliminamos para evitar duplicados
if (carpeta.getFoldersByName(nombreCarpetaExp).hasNext()) {
carpeta.getFoldersByName(nombreCarpetaExp).next().setTrashed(true);
}
Simple como el mecanismo de un botijo:
carpeta.getFoldersByName(nombreCarpetaExp).hasNext()
devuelveTRUE
si dentro de la carpeta que contiene nuestra presentación existe otra con el nombre que hemos determinado usar para la carpeta de exportación. ¿Te acuerdas del método hasNext()? ¿No? Pues a repasar el BAS#001.carpeta.getFoldersByName(nombreCarpetaExp).next()
obtiene a continuación, si lo de arriba es TRUE, la carpeta que hemos encontrado y la elimina por medio del método setTrashed().
Ahora ya podemos crear tranquilamente la carpeta de exportación sin miedo a duplicados con createFolder().
// Crear carpeta de exportación
const carpetaExp = carpeta.createFolder(nombreCarpetaExp);
Generar y guardar las imágenes PNG
Para nombrar las imágenes que vamos a generar usaremos:
- El prefijo “Diapositiva”.
- Un espacio.
- Un número que representa el orden de la diapositiva dentro de la presentación, rellenado con ceros por la izquierda para que al mostrarlas en Drive queden correctamente ordenadas.
Primero, obtenemos las diapositivas a exportar usando el método getSlides() sobre nuestra presentación:
// Lista de diapositivas en la presentación
const diapos = presentacion.getSlides();
A continuación, determinamos cuántos dígitos deben utilizarse para numerar las diapositivas. Usaremos posteriormente esta información para rellenar con ceros por la izquierda. Para obtenerlo miramos cuántas diapositivas hay (diapos.length), transformamos el resultado en una cadena de texto con el método toString() y obtenemos nuevamente su longitud.
// ¿Cuántos dígitos necesitamos para representar el nº de la imagen exportada?
const nDigitos = parseInt(diapos.length.toString().length);
Por ejemplo, si tuviéramos 115 diapositivas (no le hagas eso a tu audiencia 😅), el resultado de este apaño sería 3.
Y ahora viene la movida, truco del almendruco o como quieras llamarlo.
Usaremos uno de esos URL especiales que hacen cosillas sorprendentes con los archivos almacenados Drive.
☝ Tienes una lista más o menos rigurosa de URL mágicos en esta recopilación que he publicado en googleurltricks.tk.
Concretamente invocaremos este, que es capaz de exportar a diferentes formatos las diapositivas de nuestra presentación:
https://docs.google.com/presentation/d/ID_PRESENTACIÓN/export/png?pageid=ID_DIAPO
Si le echas un vistazo al URL de cualquier diapositiva identificarás sin problemas tanto ID_PRESENTACIÓN
como ID_DIAPO
.
Por ejemplo, si haces clic en el URL que te muestro a continuación obtendrás de manera inmediata una versión en formato PNG de la 1ª diapositiva de la presentación de ejemplo que usamos en este BAS:
https://docs.google.com/presentation/d/144pPxBeABfBo8V2OS9h2gfuKMFTDr8XLyKoHtOrxc60/export/png?pageid=p
¿Magia? Aún no, eso viene después 😏.
GAS dispone de un mecanismo para acceder a recursos HTTP / HTTPS por medio de sus URL. Con todos ustedes, el servicio URL Fetch.
URL Fetch no parece gran cosa. Pero nada más alejado de la realidad. Gracias a este servicio podemos conectar nuestros scripts con el mundo exterior, o lo que es lo mismo, con cualquier servicio que disponga de algo llamado API REST. Y hoy en día, muchísimas aplicaciones y plataformas disponen de esta capacidad.
¿Te gustaría que tus scripts GAS pudieran interactuar con plataformas de terceros como Moodle, GitHub, Twitter, Notion (en beta privada en el momento de escribir esto), Asana, evolCampus, Tribe...? ¿O acceder a repositorios de datos públicos como por ejemplo los que encontramos en el Portal de Dades Obertes de la Generalitat Valenciana? ¡Concedido!
Pero me estoy desviando de lo que teníamos entre manos. Seguro que tenemos tiempo en algún BAS más avanzado de meterle mano a estas fascinantes APIs REST. No lo dudes.
Estábamos hablando de cómo acceder a esos URL mágicos que convierten al vuelo las diapositivas de una presentación a formato PNG, ¿verdad? Pues eso lo vamos a conseguir con el método fetch() de la clase UrlFetchApp.
Solo tendremos que pasarle como parámetro el URL mágico que exporta cada diapositiva en PNG y esperar a que recoja los datos devueltos como un blob mediante el método getBlob() de la clase HTTPresponse. Sí, como ese que te facilitaba hace un momento para descargar la 1ª diapositiva de la presentación como PNG.
Sí, lo repito: b-l-o-b. ¿Y qué es eso? Por ahora nos basta con saber que se trata de un objeto utilizado para el intercambio de información entre los distintos servicios Apps Script. Dentro puede haber de todo, incluyendo nuestras preciosas imágenes en formato PNG. Y, cómo no, los blobs disponen de su propia clase, que hoy solo tocaremos (muy) tangencialmente.
Muchos conceptos nuevos en este BAS, me temo. Nadie dijo que esto fuera a ser siempre un paseo campestre. Pues aún nos queda algo más. La “magia” que mencionaba hace unas líneas, para disponer por fin de todos los ingredientes que nos permitan culminar nuestro objetivo.
Y es que hay un detalle fundamental que debes conocer cuanto antes (la información es poder).
⚠️ Cuando usas el método fetch() para acceder a los datos contenidos en un URL resulta que no eres tú, persona humana, quien lo hace, sino los servidores de Google. Y esto quiere decir que si estos datos no son un recurso público la cosa acabará en error.
Si tratamos de descargar por tanto del modo que ya sabemos nuestra diapositiva número 1 tal que así...
const diapo = UrlFetchApp.fetch('https://docs.google.com/presentation/d/144pPxBeABfBo8V2OS9h2gfuKMFTDr8XLyKoHtOrxc60/export/png?pageid=p').getBlob();
...el resultado será este:
¿No te suena de algo el texto en rojo que se vislumbra en el registro de ejecución? Es un fragmento inicial del código HTML de la típica página de inicio de sesión en Google. Nuestro impetuoso fetch se ha dado de morros con ella.
Espera un momento, pero esta presentación es mía y solo mía y ¡soy yo quien está ejecutando esta función GAS! Irrelevante. Quien realmente accede al URL indicado es una entidad robótica de género ambiguo que habita en las entrañas de cualquier de los centros de datos de Google. Y que no te conoce de nada, para que nos entendamos. Toma castaña.
Soluciones: Pues dos, la que funciona y la mágica (y correcta):
- La que funciona: Ajustamos los permisos de la presentación para que cualquiera con el enlace pueda acceder. Si eso te vale, a mí también. Bueno, miento, a mí no me vale.
- La correcta: Introducimos en el URL de acceso una clave secreta que solo el propietario de la presentación conoce. Esa clave está imbuida de su presencia y autoridad en el ámbito de los servicios de Google. Con ella en ristre, esa entidad robótica que accede al URL lo hará en nombre del propietario. Que alguien llame a John Carpenter inmediatamente, por favor.
Calma, esa clave secreta no es la contraseña de tu cuenta de Google, sino algo denominado token de acceso.
Los tokens de acceso son moneda de cambio frecuente cuando se usan APIs REST que requieren autentificación. No pensarías que eso de acceder a los datos almacenados con otros servicios iba a ser en barra libre para cualquiera, ¿no? ¡Menuda locura!
Inyectaremos el token de acceso en nuestro URL usando esta secuencia (no documentada, para variar):
?access_token=RISTRA_DE LETRAS_Y_NÚMEROS
En consecuencia, el URL que le permitirá a nuestro script obtener una copia en PNG de cada diapositiva será una marcianada como esta:
https://docs.google.com/presentation/d/ID_PRESENTACIÓN/export/png?access_token=TOKEN_DE_ACCESO&pageid=ID_DIAPO
Fíjate en que separamos access_token
de pageid
con un muy expresivo signo et (&
). El orden en el que los utilicemos no importa, pero el primero debe ir precedido de un interrogante (?
) que, para que nos entendamos, indica que lo que va detrás son algo así como los parámetros que pasamos al servidor en nuestra petición HTTPS.
Creo que llegados a este punto el cuerpo nos pide un esquema, para que los árboles no nos impidan ver el bosque.
Y por fin, vamos a colocar la última pieza del puzzle antes de retomar la revisión del código de nuestra función. Pues bien, ¿de dónde vamos a sacar ese maldito token de acceso que necesitamos para hacer las cosas como es debido?
Revisemos primero ciertos conceptos técnicos importantes.
Ya sabes que cuando ejecutas por primera vez un script GAS tienes que concederle permisos. Si posteriormente modificas tu script de modo que use otros servicios adicionales a los necesarios en el momento de la autorización inicial, el cuadro de diálogo de autorización surgirá por sí solo de la nada para que manifiestes (o no) tu conformidad nuevamente.
Y es que realmente, por debajo de esa utilísima jerarquía de clases, propiedades y métodos que usamos en nuestros scripts GAS no hay otra cosa que una serie de APIs REST dispuestas por Google. Sí, como esas de las que te hablaba hace unas líneas para acceder a servicios de terceros.
Estas APIs solo pueden utilizarse tras un proceso de identificación y autorización OAuth en el que se genera un token de acceso sintonizado con los scopes (permisos) que necesita nuestro código para acceder a la información personal de los usuarios que interaccionan con él de manera segura.
Todo esta fachada de servicios GAS, por tanto, está ahí para ocultar la complejidad de lo que hay por debajo, que ciertamente resulta inicialmente aterradora, y facilitarnos la vida a la hora de crear scripts que hagan cosas útiles sin preocuparnos por todo esos mecanismo de autorización y acceso a las APIs de Google subyacentes.
Seguramente, en algún momento no tendremos más remedio que encender las luces dentro de la caverna, romper las cadenas que apresan el conocimiento y dejar de ver las sombras de los servicios GAS para conocer la realidad y realizar ciertos procedimientos avanzados… aunque para eso aún nos faltan muchos BAS (perdón por la idea de olla pseudo platónica).
Bajando de nuevo a la tierra, el caso es que vamos a conseguir nuestro ansiado token por medio de otra clase del catálogo de servicios GAS, ScriptApp, concretamente utilizando el método de sugerente nombre getOAuthToken().
Y ya lo tenemos todo. Vamos a ver de una vez cómo queda finalmente la cosa, que ya era hora, así de un trago.
// URL "mágico" para la exportación PNG
const url = `https://docs.google.com/presentation/d/${idPresentacion}/export/png?access_token=${ScriptApp.getOAuthToken()}`;
// Enumerar diapositivas y exportar en formato PNG
diapos.forEach((diapo, num) => {
// Obtener blob de la diapositiva exportada en png
const url = `https://docs.google.com/presentation/d/${idPresentacion}/export/png?access_token=${ScriptApp.getOAuthToken()}`;
// Por fin, creamos imágenes a partir de los blobs obtenidos para cada diapo,
// nombres precedidos por nº de diapositiva con relleno de 0s por la izquierda
carpetaExp.createFile(blobDiapo.setName(`Diapositiva ${String(num + 1).padStart(nDigitos, '0')}`));
});
Se utiliza un bucle forEach()
para recorrer todas las diapositivas. En esta ocasión utilizo su 2º parámetro opcional (num
) que indica la posición dentro del vector de diapositivas en cada iteración. Este valor se utiliza más abajo, cuando se crea el archivo correspondiente a cada imagen PGN en la carpeta de exportación de Drive por medio del método createFile(), que recibe como parámetro único el blob obtenido a partir de nuestros URL mágicos.
Como seguramente ya habrás adivinado, el nombre del archivo que se guarda en Drive queda establecido por medio del método setName() de la clase Blob. Rellenaremos el cardinal del archivo PNG tirando del método JavaScript padStart().
⚠️ Una advertencia para lo que no tengo suficientes negritas ⚠️
Muchísimo cuidado con ese token de acceso con el que hemos estado jugando. Te / nos representa. Es la llave de las puertas de Mordor, digo Drive 🗝️. No lo compartas, te sirvas de él para posturear en redes sociales, barras de bar o similares (cuando nos dejen volver a ellas 😷, por supuesto) ni nada por el estilo. Úsalo y destrúyelo. Advertido quedas, querido amigo o amiga.
Siguientes pasos
Bueno, pues yo diría que este ha sido un BAS intenso, más que nada porque hemos hablado de APIs, tokens y ciertas características no documentadas de los URL de Google. Pero ahora que ya estás un poco más familiarizado con estas rarezas seguro que te parecen menos temibles, ¿a que sí?
No obstante este BAS aún puede dar más de sí.
1️⃣ Una vez más, no hemos realizado ningún tipo de control de errores. Cuando se accede a recursos externos mediante el servicio URL Fetch esto adquiere aún más importancia si cabe. Este control de errores lo podemos realizar de manera genérica, usando bloques try...catch de los que ya hemos hablado en BAS anteriores, o interrogando al objeto HTTPResponse
por medio de su método getResponseCode(). Ten en cuenta que estamos accediendo a recursos HTTP(S) ajenos a nuestro script, pueden pasar infinidad de cosas.
2️⃣ El proceso de exportación puede resultar largo cuando tenemos muchas diapositivas. ¿Podríamos acelerarlo? La respuesta es un sí rotundo. Solo diré una cosa: fetchAll(). Bueno, como no sé callarme añadiré otra: este método permite lanzar varias operaciones fetch simultáneas, logrando una paralelización efectiva del proceso. Puedes ver un ejemplo de uso en el 2º script que presento en este repositorio GitHub. Comprobarás que se utiliza el servicio avanzado de presentaciones, lo que evita hacer malabarismos con tokens de acceso, pero también tira de fetch()
, bueno, realmente de fetchAll()
. Seguro que en un plis eres capaz de adaptar la idea a este BAS.
3️⃣ ¿No sería guay construir un documento de texto con todas estas estupendas diapos exportadas como PNG? Así en plan guión, con espacio para notas y esas cosas. Yo me apunto. Es más, creo que el próximo BAS irá justo de eso, y así tocamos otro servicio distinto a los de hojas de cálculo y presentaciones con los que hemos jugado hasta ahora 😏.
Como siempre, a tu disposición para cualquier consulta, duda o rasgadura de vestiduras. ¡Haz GAS-comunidad! 👋
Comentarios