/** * MiCompress Add-on — Google Slides Image Handler * * Handles scanning and replacing images in Google Slides presentations. * Google Slides has a more straightforward image replacement API * compared to Docs, using the Image.replace() method. */ /** * Scan all images in the active Google Slides presentation * * @returns {Array} Array of image objects with blob, size, name, dimensions */ function scanSlidesImages() { const presentation = SlidesApp.getActivePresentation(); if (!presentation) { throw new Error('No active presentation found'); } const slides = presentation.getSlides(); const images = []; let imageIndex = 0; for (let s = 0; s < slides.length; s++) { const slide = slides[s]; const slideNumber = s + 1; const pageElements = slide.getPageElements(); for (let p = 0; p < pageElements.length; p++) { const element = pageElements[p]; const pageElementType = element.getPageElementType(); if (pageElementType === SlidesApp.PageElementType.IMAGE) { const image = element.asImage(); try { const blob = image.getBlob(); const bytes = blob.getBytes(); imageIndex++; images.push({ name: 'Slide ' + slideNumber + ' — Image ' + imageIndex, // Batch-resume key for compressAllImages (see Code.gs). Slides keeps // every image it can read, so the running index doubles as position. position: imageIndex - 1, blob: blob, size: bytes.length, contentType: blob.getContentType() || 'image/png', width: Math.round(image.getWidth()), height: Math.round(image.getHeight()), element: image, slideIndex: s, slideNumber: slideNumber, type: 'slides', }); } catch (imgErr) { console.error('Could not extract image on slide ' + slideNumber + ':', imgErr); // Skip images that can't be extracted (linked images, etc.) } } else if (pageElementType === SlidesApp.PageElementType.GROUP) { // Scan for images inside groups. The helper returns the updated index // so grouped images keep numbering on from the ungrouped ones instead // of repeating numbers already used on this slide. const group = element.asGroup(); imageIndex = scanSlidesGroup_(group, images, slideNumber, imageIndex); } } } return images; } /** * Recursively scan a group element for images * * @param {Group} group - The group element to scan * @param {Array} images - Array to push found images into * @param {number} slideNumber - Which slide this group is on * @param {number} index - Running image index * @returns {number} Updated image index */ function scanSlidesGroup_(group, images, slideNumber, index) { const children = group.getChildren(); for (let i = 0; i < children.length; i++) { const child = children[i]; const childType = child.getPageElementType(); if (childType === SlidesApp.PageElementType.IMAGE) { const image = child.asImage(); try { const blob = image.getBlob(); const bytes = blob.getBytes(); index++; images.push({ name: 'Slide ' + slideNumber + ' — Group Image ' + index, position: index - 1, blob: blob, size: bytes.length, contentType: blob.getContentType() || 'image/png', width: Math.round(image.getWidth()), height: Math.round(image.getHeight()), element: image, slideIndex: slideNumber - 1, slideNumber: slideNumber, type: 'slides', }); } catch (imgErr) { console.error('Could not extract group image:', imgErr); } } else if (childType === SlidesApp.PageElementType.GROUP) { // Nested groups index = scanSlidesGroup_(child.asGroup(), images, slideNumber, index); } } return index; } /** * Replace an image in Google Slides with a compressed version * * Google Slides provides a direct replace() method on Image objects, * making this simpler than Google Docs replacement. * * @param {Image} originalImage - The original Image page element * @param {Blob} compressedBlob - The compressed image blob * @returns {Image} The image now on the slide */ function replaceSlidesImage(originalImage, compressedBlob) { try { // Save original transform (position, size, rotation) const left = originalImage.getLeft(); const top = originalImage.getTop(); const width = originalImage.getWidth(); const height = originalImage.getHeight(); const rotation = originalImage.getRotation(); // Alt text lives on the element, and replace() does not guarantee it // survives — read it before swapping the content. let altTitle = null; let altDescription = null; try { altTitle = originalImage.getTitle(); altDescription = originalImage.getDescription(); } catch (e) { /* older element types may not expose these */ } // replace() RETURNS the resulting Image. Keep working with that return // value: the original reference can be stale afterwards, so setting the // geometry on it may silently do nothing. const newImage = originalImage.replace(compressedBlob) || originalImage; // Restore original geometry — replace() re-fits the element to the new // image's intrinsic dimensions. newImage.setLeft(left); newImage.setTop(top); newImage.setWidth(width); newImage.setHeight(height); newImage.setRotation(rotation); try { if (altTitle) newImage.setTitle(altTitle); if (altDescription) newImage.setDescription(altDescription); } catch (e) { /* non-fatal: geometry already restored */ } return newImage; } catch (err) { console.error('replaceSlidesImage error:', err); throw new Error('Failed to replace image in slide: ' + err.message); } }