Change link attached to an InlineDrawing object - google-apps-script

This question was cross-posted to Web Applications: Help me find my Element in a Google Doc so I can act on it in script?
Here is my Google Doc:
https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing
You can see a button in it titled ENTER NEW NOTE.
I have been successful at rolling through the elements of the doc to find the table and to replace txt in those areas as needed. But the button here needs to have the URL changed, and I cannot figure out how to do it.
This URL seems to give an idea, but I cannot turn it into my answer since I don't quite understand. Mail merge: can't append images from template
Would someone help me with this to the point of showing the actual code, because I have tried to edit the many examples found about elements, setURL and looking to parent, etc. I just end up with a mess.
I am calling the script from a Google Sheet, to wok on a BUNCH of Google Docs. (I will be running through the spreadsheet to get URL's for the next doc to have it's URL replaced.
Here is as close as I believe I have gotten:
function getDocElements() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
// Look for child elements within the paragraph. Inline Drawings are children.
// if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
var drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
while (drawingRange != null) {
var element = drawingRange.getElement();
var drawingElement = element.asInlineDrawing();
//drawingElement.removeFromParent();
drawingElement.setURL("http://www.google.com");
drawingRange = body.findElement(DocumentApp.ElementType.INLINE_DRAWING);
}
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
//var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
}
Logger.log(i + " : "+type);
}
Here is my newest iteration that shows in the logs the elements, including the inLineDrawing I want to change...
===========
function getDocElement() {
var doc = DocumentApp.openByUrl("https://docs.google.com/document/d/1TgzOIq0g4DyDefmJIEnqycDITIx_2GNQkpdxMGwtqB8/edit?usp=sharing"),
body = doc.getBody(),
numElements = doc.getNumChildren(),
elements = [];
for (var i = 0; i < numElements; ++i){
var element = doc.getChild(i),
type = element.getType();
// daURL = element.getURL();
Logger.log(i + " : " + numElements + " : "+ type + " " + element);
// Search through the page elements. Paragraphs are top-level, which is why I start with those.
if( type == DocumentApp.ElementType.PARAGRAPH ){
// Look for child elements within the paragraph. Inline Drawings are children.
if(element.asParagraph().getNumChildren() !=0 && element.asParagraph().getChild(0).getType() == DocumentApp.ElementType.INLINE_DRAWING) {
//element.getParent().setLinkUrl("http://www.google.com");
Logger.log(element.asParagraph().getChild(0).getType() + " : " + element.getAttributes());
// For whatever reason, drawings don't have their own methods in the InlineDrawing class. This bit copies and adds it to the bottom of the doc.
var drawing = element.asParagraph().copy();
//body.appendParagraph(drawing);
// body.appendParagraph();
if(element.getParent() !=''){
//element.asParagraph().appendHorizontalRule();
//element.editAsText().appendText("text");
// element.getParent().insertHorizontalRule(0);
}
}
}
}
}

I'm not sure why the setLinkUrl() is not available for InlineDrawing 🤔
If you can replace your drawing with an image (You can download your drawing as png or svg and insert it), you will be able to use setLinkUrl
Here is an example:
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
// All inline images as a RangeElement
var images = body.findElement(DocumentApp.ElementType.INLINE_IMAGE);
// select first image, in case your doc has more than one you'll need to loop
var element = images.getElement();
var image = element.asInlineImage();
image.setLinkUrl("www.google.com");
}

Unfortunately the Class InlineDrawing doesn't have methods to access the attached links nor any other to programmatically change it to a InlineImage1. It looks to me that you will have have to make the link changes manually.
Related Feature requests:
Issue 3367: Allow exporting InlineDrawing as an image
Issue 1054: Add ability to create and modify drawings
References
1: Answer by Henrique Abreu to Modifying a drawing using Google Apps Script

Related

Weird problem with appendParagraph method

I have created a form, used by some users at my school, that uploads a document and creates a new formatted document with all the uploads, which can be images or other docs. At this moment I am having problems with one .docx file that contains some lists, paragraphs and 2 images inside paragraphs. The part of the code I am using is:
...
// Insert Google doc
function insertGdoc(vdoc,IDgdoc,asignat)
{
var Gdoc=DocumentApp.openById(IDgdoc).getBody();
var vdoc_id=vdoc.getId();
var docBody=vdoc.getBody();
docBody.appendPageBreak();
docBody.appendParagraph("Documento de "+asignat).setBold(false);
docBody.appendHorizontalRule();
var insertaBody = DocumentApp.openById(IDgdoc).getActiveSection();
var numElements = insertaBody.getNumChildren();
for( var jj = 0; jj < numElements; ++jj )
{
var element = insertaBody.getChild(jj).copy();
var type = element.getType();
try {
if( type == DocumentApp.ElementType.PARAGRAPH )
{
docBody.appendParagraph(element.asParagraph());
}
else if( type == DocumentApp.ElementType.TABLE )
docBody.appendTable(element);
else if( type == DocumentApp.ElementType.HORIZONTAL_RULE)
docBody.appendHorizontalRule();
else if( type == DocumentApp.ElementType.PAGE_BREAK)
docBody.appendPageBreak();
else if( type == DocumentApp.ElementType.LIST_ITEM )
{
docBody.appendListItem(element);
var glyphType = element.getGlyphType();
element.setGlyphType(glyphType);
}
}
catch (e)
{
Logger.log(e);
}
}
}
// Insert MS Word doc
function insertDoc(vdoc,IDdoc,asignat)
{
var docx = DriveApp.getFileById(IDdoc);
var blob=docx.getBlob();
var newDoc = Drive.newFile();
var file=Drive.Files.insert(newDoc,blob,{convert:true});
insertGdoc(vdoc,file.id,asignat);
}
...
After many attempts, I discovered that the elements that are causing the problems are both pictures. By using the try..catch I can avoid the error by not handling the pictures, but the final document is incomplete. I have also tried to use Utilities.sleep to give the server more time to perform actions and even to close and reopen the document, with no change or worse results.
Another option I discovered here in stackoverflow was to try appending child elements from the paragraph, differentiating if they are pictures, which could copy the first of my pictures (although with size and position changed from the original) but not the second one.
Which is most annoying is that this failure happens if I use the form to do all the process, but if I try the operation manually from the console it works and the whole source document with both pictures is copied to the final doc.
Thank you very much in advance if you can give me any advice.
Rafael

Appending template to Google Docs header/footer not working when testing as Add-on

I have built a tool in google scripts that is currently set up as a bound script to a google docs file. The goal of the tool is to pull in templates from other created docs files from a custom form input. The code works as is as a bound script, but I am trying to port that code over to create an Add-on for users within my organization. The issue right now is that when trying to insert the template elements into the header or footer of the clean test documents, the code throws an error when attempting to append the elements to the saying Cannot call method "appendParagraph" of null.
I have tried appending as a header and footer with DocumentApp.getActiveDocument().getHeader().appendParagraph. This method works when I am using the script as a bound script and it works as expected. When I try to append the paragraph (or other element) to the body instead of the header or footer while testing as an Add-on the elements are appended without issue. The only time I am getting this error is when I try to append the elements to either the header or footer.
function examplefunction_( docLink, locPaste ) {
var srcBody = DocumentApp.openById( docLink )
.getBody();
var srcNumChild = srcBody.getNumChildren();
var activeBody = DocumentApp.getActiveDocument()
.getBody();
var activeHead = DocumentApp.getActiveDocument()
.getHeader();
var activeFoot = DocumentApp.getActiveDocument()
.getFooter();
var a =0;
if ( locPaste == 'Body') {
var activeLoc = activeBody;
}
else if (locPaste == 'Header') {
var activeLoc = activeHead;
}
else if (locPaste == 'Footer') {
var activeLoc = activeFoot;
}
for (var i = 0; i<srcNumChild; i++) {
if (srcBody.getChild(i).getType() == DocumentApp.ElementType.PARAGRAPH) {
var srcText = srcBody.getChild(i).copy();
activeLoc.appendParagraph(srcText);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.TABLE) {
var srcTable = srcBody.getTables();
var copiedTable = srcTable[a].copy()
a = a + 1;
activeLoc.appendTable(copiedTable);
}
else if (srcBody.getChild(i).getType() == DocumentApp.ElementType.LIST_ITEM) {
var srcList = srcBody.getChild(i).getText();
var listAtt = srcBody.getChild(i).getAttributes();
var nestLvl = srcBody.getChild(i).getNestingLevel();
activeLoc.appendListItem(srcList).setAttributes(listAtt).setNestingLevel(nestLvl);
}
else {
Logger.log("Could not get element: " + i);
}
}
}
I expected the elements to be appended to the headers and footers from the template without error like when run as a bounded script. The actual result while being run kills the process with error "Cannot call method appendParagraph of null." at the line: activeLoc.appendParagraph(srcText);
You cannot append a paragraph to a header or footer, if your document does not have a header or a footer.
Add to the beginning of your function:
if(!DocumentApp.getActiveDocument().getHeader()){
DocumentApp.openById(docLink).addHeader();
}
Same for the footer.

Can drawings be selected via apps script in Google Docs?

I have a document with Google drawings that for whatever reason are not selectable within the UI. I am not sure how they were ever placed.
I was hoping to write a script to delete them, but I'm not finding a function that applies to drawings specifically.
I'm wondering if anyone knows a trick to accomplish this..
The closest thing I found was their sample for deleting images:
function myFunction() {
var body = DocumentApp.getActiveDocument().getBody();
// Remove all images in the document body.
var imgs = body.getImages();
for (var i = 0; i < imgs.length; i++) {
// Retrieve the paragraph's attributes.
var atts = imgs[i].getAttributes();
// Log the paragraph attributes.
for (var att in atts) {
Logger.log(att + ":" + atts[att]);
}
imgs[i].removeFromParent();
}
}
Never too late (I hope). The trick here is that inline drawings (InlineDrawing) are children of Paragraph or ListItem (source).
If you want to remove some inline drawings, the code below works for me. If you want to find all drawings, please see the TODO comment. It is a simple code, please enhance it if you intend to use it. Just for reference.
Unfortunately, to this time, I didn't find out how to remove drawings that are not inline (drawings that are above or below text). Please forgive my limitation.
function eraseSomeDrawingsFromDoc() {
var body = DocumentApp.getActiveDocument().getBody();
const paragraphs = body.getParagraphs()
paragraphs.forEach(paragraph => {
const childIfAny = paragraph.getNumChildren() > 0 && paragraph.getChild(0) //TODO: analyze all children
const childType = childIfAny && childIfAny.getType()
const iAmADrawing = childType === DocumentApp.ElementType.INLINE_DRAWING
if(iAmADrawing) childIfAny.removeFromParent()
})
}

Manipulate PositionedImage and wrap text around image in Google Docs

I used the following snippet to insert an image into a Google Document:
// Source: http://stackoverflow.com/a/18859986/1536038
var doc = DocumentApp.openById('Google Drive Id');
var img = DriveApp.getFileById('Google Drive Id').getBlob();
doc.getBody().insertImage(0, img);
The result is an In line image:
I want, however, to have a Wrap text image, like so:
Is that possible via Google Apps Script (on the fly)?
Issue 1529 has been fixed. As of December 2015, Google Apps Script can manipulate PositionedImage objects in Google Docs.
They behave a little differently than InlineImage elements, as they need to be anchored to a ListItem or Paragraph element, while InlineImages can be added only to Body, FooterSection, HeaderSection or TableCell elements.
A PositionedImage is an object anchored in an element, while an InlineImage is itself an element of a document. This implies that you cannot convert one type of image directly to the other. (When you switch an image from "Wrap text" to "Inline" using the UI, the PositionedImage is removed from its anchor paragraph, then inserted into the body of the document outside of that paragraph. You could emulate that via script if necessary.)
Insert a PositionedImage
Here's an example of a PositionedImage inserted by the following script:
// http://stackoverflow.com/a/20661113/1677912
function DemoPositionedImage() {
// Get handle on active document
var doc = DocumentApp.getActiveDocument();
// Find desired image file
var matchedFiles = DriveApp.getFilesByName('apple-touch-icon.png');
if (matchedFiles.hasNext()) {
// Get image object from file
var image = matchedFiles.next().getBlob();
// Add image as a PositionedImage.
var positionedImage = doc.getBody().getParagraphs()[0].addPositionedImage(image);
// Adjust layout, etc. here
// Log the ID of the new image
Logger.log( positionedImage.getId() );
}
}
The log shows the ID of the new image, like this:
[15-12-11 20:35:03:706 EST] kix.9dwnzjfapdy8
Be careful - if you add multiple images to the same element (e.g. Paragraph), with default layout, the newest image will overlay existing ones. Therefore, it may look like you have a single image when there are actually a pile of them.
Retrieve existing PositionedImages
Since a PositionedImage is not an element of a document, it does not appear in the element hierarchy with elements like paragraphs, tables, or InlineImages, and cannot be found through the document methods getChild(), getNextSibling(), and so on. Likewise, there is no Body.getPositionedImages() to parallel Body.getImages().
Instead, you can get a PositionedImage using its unique ID, e.g. kix.9dwnzjfapdy8 from the earlier example.
var positionedImage = getPositionedImage(storedId);
Alternatively, you can get all the PositionedImage objects in a containing element as an array.
var positionedImages = getPositionedImages();
for (var i=0; i<positionedImages.length; i++) {
Logger.log( positionedImages[i].getId() );
}
Retrieving all the PositionedImages in a document requires traversing all the possible anchor elements. The following utility does just that.
/**
* Get a list of all PositionedImages in a document.
* See stackoverflow.com/a/20661113/1677912.
*
* #param {String} docId (optional) ID of document to scan
*
* #returns {PositionedImage[]} Array of PositionedImages in document
*/
function getAllPositionedImages( docId ) {
// Open document if given ID, otherwise use active document.
if (docId) {
var doc = DocumentApp.openById(docId);
}
else {
doc = DocumentApp.getActiveDocument();
}
// Get handle on document's body
var body = doc.getBody();
// array to hold all images in document
var allPositionedImages = [];
var numElems = body.getNumChildren();
for (var childIndex=0; childIndex<numElems; childIndex++) {
var child = body.getChild(childIndex);
switch ( child.getType() ) {
case DocumentApp.ElementType.PARAGRAPH:
var container = child.asParagraph();
break;
case DocumentApp.ElementType.LIST_ITEM:
container = child.asListItem();
break;
default:
// Skip elements that can't contain PositionedImages.
continue;
}
// Collect images from current container
var imagesHere = container.getPositionedImages();
allPositionedImages = allPositionedImages.concat(imagesHere);
}
return allPositionedImages;
}
Layout control
Most of the layout controls for PositionedImages are well described in the documentation:
Height: setHeight(), getHeight()
Width: setWidth(), getWidth()
LeftOffset: setLeftOffset(), getLeftOffset()
TopOffset: setTopOffset(), getTopOffset()
Layout: setLayout(), getLayout()
The PositionedLayout enum used with the Layout methods is unique to PositionedImages. At the time of launch of PositionedImage support however, it was not included in editor autocompletion, and the documentation contained no examples of its use. Let's fill that gap.
Here's how you can set the layout of a PositionedImage so that it is wrapped by text:
positionedImage.setLayout( DocumentApp.PositionedLayout.WRAP_TEXT );
The following utility function gets the English equivalent of a PositionedLayout enum.
/**
* Get the string representing the given PositionedLayout enum.
* Ref: https://developers.google.com/apps-script/reference/document/positioned-layout
*
* See stackoverflow.com/a/20661113/1677912.
*
* #param {PositionedLayout} PositionedLayout Enum value.
*
* #returns {String} English text matching enum.
*/
function getLayoutString( PositionedLayout ) {
var layout;
switch ( PositionedLayout ) {
case DocumentApp.PositionedLayout.ABOVE_TEXT:
layout = "ABOVE_TEXT";
break;
case DocumentApp.PositionedLayout.BREAK_BOTH:
layout = "BREAK_BOTH";
break;
case DocumentApp.PositionedLayout.BREAK_LEFT:
layout = "BREAK_LEFT";
break;
case DocumentApp.PositionedLayout.BREAK_RIGHT:
layout = "BREAK_RIGHT";
break;
case DocumentApp.PositionedLayout.WRAP_TEXT:
layout = "WRAP_TEXT";
break;
default:
layout = "";
break;
}
return layout;
}
Note: This has been concurrently posted on my blog.

How to use .findElement(DocumentApp.ElementType.TABLE_OF_CONTENTS) to get and parse a Document's Table of Contents Element

My goal is to parse a TableOfContents element in a Google Document and write it to another one. I want to do this for every document in a folder.
Having gone to the bother of converting each document to the type generated by DocsList just so I can use this method [ which a document generated by DocumentApp does not have. Why, I don't understand, because otherwise the two 'documents' are similar when it comes to finding parts. ], I find that what I get back is a SearchResult. How is this elusive construction used? I've tried converting it into a TableOfContents element [ ele = searchResult.asTableOfContents() ], which does not error out, but nothing I do allows me parse through its child elements to recover their text works. Interestingly enough, if you get a TableOfContents element by parsing through the document's paragraphs to get it, THAT let's you parse the TOC.
Would someone speak to this question. I sure would appreciate a code snippet because I'm getting nowhere, and I have put some hours into this.
The asTableOfContents() method is only there to help the editor's autocomplete function. It has no run-time impact, and cannot be used to cast to a different type. (See ContainerElement documentation.)
To parse the table of contents, start by retrieving the element from the SearchResult. Below is an example that goes through the items in a document's table of contents to produce an array of item information.
Example Document
Parsing results
On a simple document with a few headings and a table of contents, here's what it produced:
[13-08-20 16:31:56:415 EDT]
[
{text=Heading 1.0, linkUrl=#heading=h.50tkhklducwk, indentFirstLine=18.0, indentStart=18.0},
{text=Heading 1.1, linkUrl=#heading=h.ugj69zpoikat, indentFirstLine=36.0, indentStart=36.0},
{text=Heading 1.2, linkUrl=#heading=h.xb0y0mu59rag, indentFirstLine=36.0, indentStart=36.0},
{text=Heading 2.0, linkUrl=#heading=h.gebx44eft4kq, indentFirstLine=18.0, indentStart=18.0}
]
Code
function test_parseTOC() {
var fileId = '--Doc-ID--';
Logger.log( parseTOC( fileId ) );
}
function parseTOC( docId ) {
var contents = [];
var doc = DocumentApp.openById(docId);
// Define the search parameters.
var searchElement = doc.getBody();
var searchType = DocumentApp.ElementType.TABLE_OF_CONTENTS;
// Search for TOC. Assume there's only one.
var searchResult = searchElement.findElement(searchType);
if (searchResult) {
// TOC was found
var toc = searchResult.getElement().asTableOfContents();
// Parse all entries in TOC. The TOC contains child Paragraph elements,
// and each of those has a child Text element. The attributes of both
// the Paragraph and Text combine to make the TOC item functional.
var numChildren = toc.getNumChildren();
for (var i=0; i < numChildren; i++) {
var itemInfo = {}
var tocItem = toc.getChild(i).asParagraph();
var tocItemAttrs = tocItem.getAttributes();
var tocItemText = tocItem.getChild(0).asText();
// Set itemInfo attributes for this TOC item, first from Paragraph
itemInfo.text = tocItem.getText(); // Displayed text
itemInfo.indentStart = tocItem.getIndentStart(); // TOC Indentation
itemInfo.indentFirstLine = tocItem.getIndentFirstLine();
// ... then from child Text
itemInfo.linkUrl = tocItemText.getLinkUrl(); // URL Link in document
contents.push(itemInfo);
}
}
// Return array of objects containing TOC info
return contents;
}
Bad news
The bad news is that you are limited in what you can do to a table of contents from a script. You cannot insert a TOC or add new items to an existing one.
See Issue 2502 in the issue tracker, and star it for updates.
If you can post code or explain your issue with DocsList vs DocumentApp, it could be looked at. The elements of a Google Document can only be manipulated via DocumentApp.
I modified the above code to re-create the TOC in a table only with the desired levels(i.e. h1, h2). The only caveat is that TOC must be present & updated before running this.
function findToc(body, level = 2) {
const indent = 18;
let contents = [];
const tocType = TABLE_OF_CONTENTS;
const tocContainer = body.findElement(tocType);
if (tocContainer) {
// TOC was found
const toc = tocContainer.getElement().asTableOfContents();
const totalLines = toc.getNumChildren();
for (let lineIndex = 0; lineIndex < totalLines; lineIndex++) {
const tocItem = toc.getChild(lineIndex).asParagraph();
const { INDENT_START } = tocItem.getAttributes();
const isDesiredLevel = Number(INDENT_START) <= indent * (level - 1);
if (isDesiredLevel) {
contents.push(tocItem.copy());
}
}
}
return contents;
}
function addToTable(cellText) {
body = DocumentApp.openById(docId).getBody();
const table = body.appendTable();
const tr = table.insertTableRow(0);
const td = tr.insertTableCell(0);
cellText.forEach(text => {
td.appendParagraph(text);
})
}
function parseTOC(docId) {
body = DocumentApp.openById(docId).getBody();
const contents = findToc(body);
addToTable(contents);
}