Indesign JavaScript Creating Text & intra-doc Hyperlinks in Book - Extremely Slow - javascript

First time posting
First time writing in JavaScript, though I have experience in other languages.
I'm working in Adobe InDesign CS5.5. I have multiple files in an ID Book, each containing a varying number of "chapters". The book includes an index file with topic headings that reference the chapters in an abbreviated form (e.g., "CHAPTER 125" becomes "ch 125 no 3" -- note the "no x" part is irrelevant). The goal of my script is to create inter-document links that will add significant functionality when the ID Book is exported to, say, a PDF. The user will be able to jump from index to chapter and vice-versa. I think the script and the issues I'm dealing with would be of use to others but haven't found any posts to address my problem yet.
All refs (like "ch 125 no 1") in the index to a particular chapter ("CHAPTER 125") get a hyperlink to the location of the head of that chapter. This part of the script is working great and runs quickly.
The other half will insert the corresponding topic headings at the end of each chapter text and make those paragraphs link back to the corresponding topic head in the index. (In other words, they are cross references but not true x-refs in ID terms because I wanted more control over them and my reading on the topic told me to steer clear of true x-refs.) This is the part of the script that has me banging my head on the wall. It runs for hours upon hours without finishing a book of 200 chapters. Note that for testing purposes I am simply inserting one paragraph of text in the desired location under each chapter, rather than all topic heads and links. I know from smaller sets of text and from my debugging prints to the console that the script is doing work, not stuck in an infinite loop. Nevertheless, it runs way too long and, if I interrupt it, InDesign is unresponsive and I have to kill it, so cannot even review the partial results.
Based on searching/reading forums: I have disabled preflighting; disabled auto updating of book page numbers; changed the live preview settings to delayed. I still suspect the slowness may have to do with InDesign overhead but I don't know what else to try.
I'm embarrassed at how awful the style of this JS code might be but at the moment I just need it to work, then I can refine it.
var myBookFilePath = File.openDialog("Choose an InDesign Book File", "Indb files: *.indb");
var myOpenBook = app.open(myBookFilePath);
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.neverInteract;
// Open up every file in the currently active Book
app.open(app.activeBook.bookContents.everyItem().fullName)
// TODO: add error handling / user interaction here -- to pick which is Index file
var strIndexFilename = "Index.indd";
var objChapHeadsWeb = {};
var myDoc = app.documents.item(strIndexFilename);
$.writeln("\n\n~~~ " + myDoc.name + " ~~~");
// REMOVED CODE - check for existing hyperlinks, hyperlink sources/destinations
// loop to delete any pre-existing hyperlinks & associated objects
// works w/o any problems
// Ugly GREP to find the Main heading text (all caps entry and nothing beyond) in the index file
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
/// GREP: ^[\u\d \:\;\?\-\'\"\$\%\&\!\#\*\#\,\.\(\)]+[\u\d](?=\.|,)
app.findGrepPreferences.findWhat = "^[\\u\\d \\:\\;\\?\\-\\'\\\"\\$\\%\\&\\!\\#\\*\\#\\,\\.\\(\\)]+[\\u\\d](?=\\.|,)";
app.findGrepPreferences.appliedParagraphStyle = "Main";
var myFound = [];
myFound = myDoc.findGrep();
$.writeln("Found " + myFound.length + " Main headings.");
for (var i = 0; i < myFound.length; i++) {
myDoc.hyperlinkTextDestinations.add(myFound[i], { name: myFound[i].contents });
}
$.writeln("There are now " + myDoc.hyperlinkTextDestinations.count() + " destinations.");
myFound.length = 0;
for (var j = app.documents.count()-1; j >= 0; j--) {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
// set the variable to the document we are working with
myDoc = null;
myDoc = app.documents[j];
myFound.length = 0;
if (myDoc.name === strIndexFilename) {
continue; // we don't want to look for chapter heads in the Index file, so skip it
}
$.writeln("\n\n~~~ " + myDoc.name + " ~~~");
// REMOVED CODE - check for existing hyperlinks, hyperlink sources/destinations
// loop to delete any pre-existing hyperlinks & associated objects
// works w/o any problems
// Clear GREP prefs
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "^CHAPTER \\d+";
app.findGrepPreferences.appliedParagraphStyle = "chapter";
myFound = myDoc.findGrep();
var strTemp = "";
$.writeln("Found " + myFound.length + " chapter headings.");
for (var m = 0; m < myFound.length; m++) {
strTemp = myFound[m].contents;
objChapHeadsWeb[strTemp] = {};
objChapHeadsWeb[strTemp].withinDocName = myDoc.name;
objChapHeadsWeb[strTemp].hltdChHead =
myDoc.hyperlinkTextDestinations.add(myFound[m], {name:strTemp});
objChapHeadsWeb[strTemp].a_strIxMains = [];
objChapHeadsWeb[strTemp].a_hltdIxMains = [];
objChapHeadsWeb[strTemp].nextKeyName = "";
objChapHeadsWeb[strTemp].nextKeyName =
((m < myFound.length-1) ? myFound[m+1].contents : String(""));
}
$.writeln("There are now " + myDoc.hyperlinkTextDestinations.count() + " destinations.");
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Find the "ch" (chapter) references in the index file, link them
// back to the corresponding text anchors for the chapter heads
// in the text.
//
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
myDoc = app.documents.item(strIndexFilename); // work with the Index file
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
// GREP to find the "ch" (chapter) references in the index file
// like ch 151 no 1 OR ch 12 no 3
app.findGrepPreferences.findWhat = "(ch\\s+\\d+\\s+no\\s+\\d+)";
var strExpandedChap = "";
var strWorkingMainHd = "";
var arrFoundChapRefs = [];
var myHyperlinkSource;
var myHyperlinkDest;
for (var x = 0; x < myDoc.hyperlinkTextDestinations.count(); x++) {
strWorkingMainHd = "";
arrFoundChapRefs.length = 0;
// the special case, where we are working with the ultimate hyperlinkTextDestination obj
if (x === myDoc.hyperlinkTextDestinations.count()-1) {
// This is selecting text from the start of one MAIN heading...
myDoc.hyperlinkTextDestinations[x].destinationText.select();
// This next line will extend the selection to the end of the story,
// which should also be the end of the document
myDoc.selection[0].parentStory.insertionPoints[-1].select(SelectionOptions.ADD_TO);
}
// the regular case...
else {
// This is selecting text from the start of one MAIN heading...
myDoc.hyperlinkTextDestinations[x].destinationText.select();
// ... to the start of the next MAIN heading
myDoc.hyperlinkTextDestinations[x+1].destinationText.select(SelectionOptions.ADD_TO);
}
strWorkingMainHd = myDoc.hyperlinkTextDestinations[x].name;
//arrFoundChapRefs = myDoc.selection[0].match(/(ch\s+)(\d+)(\s+no\s+\d+)/g); //NOTE: global flag
arrFoundChapRefs = myDoc.selection[0].findGrep();
for(y = 0; y < arrFoundChapRefs.length; y++) {
myHyperlinkSource = null;
myHyperlinkDest = null;
strExpandedChap = "";
strExpandedChap = arrFoundChapRefs[y].contents.replace(/ch\s+/, "CHAPTER ");
strExpandedChap = strExpandedChap.replace(/\s+no\s+\d+/, "");
// if we found the chapter head corresponding to our chapter ref in the index
// then it is time to create a link
if (strExpandedChap in objChapHeadsWeb) {
objChapHeadsWeb[strExpandedChap].a_strIxMains.push(strWorkingMainHd);
objChapHeadsWeb[strExpandedChap].a_hltdIxMains.push(myDoc.hyperlinkTextDestinations[x]);
myHyperlinkSource = myDoc.hyperlinkTextSources.add(arrFoundChapRefs[y]);
myHyperlinkDest = objChapHeadsWeb[strExpandedChap].hltdChHead;
myDoc.hyperlinks.add(myHyperlinkSource, myHyperlinkDest);
} else {
$.writeln("Couldn't find chapter head " + strExpandedChap);
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// NOW TIME FOR THE HARD PART...
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
myDoc = null;
var strWorkingMainHd = "";
var nextKey = "";
var myParentStory = null;
var myCharIndex = 0;
var myCompareChar = null;
var myLeftmostBound = 0;
var myCurrentPara = null;
for (var key in objChapHeadsWeb) {
myDoc = app.documents.item(objChapHeadsWeb[key].withinDocName);
myCompareChar = null; //recent addition
$.writeln("Working on " + key + "."); //debugging
nextKey = objChapHeadsWeb[key].nextKeyName;
objChapHeadsWeb[key].hltdChHead.destinationText.select();
myLeftmostBound = myDoc.selection[0].index;
myParentStory = myDoc.selection[0].parentStory;
if( (nextKey === "") || (myDoc.name !== objChapHeadsWeb[nextKey].withinDocName) )
{
//// Need to find end of story instead of beginning of next chapter
//myDoc.selection[0].parentStory.insertionPoints[-1].select(SelectionOptions.ADD_TO);
myParentStory.insertionPoints[-1].select();
//myCharIndex = myDoc.selection[0].index; /recently commented out
myCharIndex = myDoc.selection[0].index - 1; //testing new version
myCompareChar = myParentStory.characters.item(myCharIndex); //recenttly added/relocated from below
} else {
/////
//objChapHeadsWeb[nextKey].hltdChHead.destinationText.select(SelectionOptions.ADD_TO);
objChapHeadsWeb[nextKey].hltdChHead.destinationText.select();
//myParentStory.characters.item(myDoc.selection[0].index -1).select();
myParentStory.characters.item(myDoc.selection[0].index -2).select(); //temp test *****
myCharIndex = myDoc.selection[0].index;
myCompareChar = myParentStory.characters.item(myCharIndex);
if (myCompareChar.contents === "\uFEFF") {
$.writeln("Message from inside the \\uFEFF check."); //debugging
myParentStory.characters.item(myDoc.selection[0].index -1).select();
myCharIndex = myDoc.selection[0].index;
myCompareChar = myParentStory.characters.item(myCharIndex);
}
if( (myCompareChar.contents !== SpecialCharacters.PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.ODD_PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.EVEN_PAGE_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.COLUMN_BREAK) &&
(myCompareChar.contents !== SpecialCharacters.FRAME_BREAK))
{
$.writeln("Possible error finding correct insertion point for " + objChapHeadsWeb[key].hltdChHead.name + ".");
}
}
if(myCharIndex <= myLeftmostBound) { // this shouldn't ever happen
alert("Critical error finding IX Marker insertion point for " + objChapHeadsWeb[key].hltdChHead.name + ".");
}
if(myCompareChar.contents !== "\r") {
myDoc.selection[0].insertionPoints[-1].contents = "\r";
}
myDoc.selection[0].insertionPoints[-1].contents = "TESTING text insertion for: " + objChapHeadsWeb[key].hltdChHead.name + "\r";
myDoc.selection[0].insertionPoints.previousItem(myDoc.selection[0].insertionPoints[-1]).select();
//myDoc.selection[0].insertionPoints[-1].contents = "<Now I'm here!>";
myCurrentPara = myDoc.selection[0].paragraphs[0];
myCurrentPara.appliedParagraphStyle = myDoc.paragraphStyles.item("IX Marker");
// TODO:
// need error handling for when style doesn't already exist in the document
} // end big for loop
//TODO: add error handling support to carry on if user cancels
//close each open file; user should be prompted to save changed files by default
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
app.documents.everyItem().close();
// Cleanup
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;

Try open all files of Cross References are linking to them.

May I suggest a few improvements that can probably speed up things a bit.
First of all you have tons of global variables here that you may concentrate in much less scopes using functions. Having many global variables has a big cost in terms of performances.
Once that said, I won't open every single doc of the book at once but process them one by one. Be aware that grep calls are very cost expensive so you may try to look at your patterns.
Another one is the extensive use of the $.writeln command. Avoid it especially within loops. Prefer an easy to set report library.
Finally I tried to rewrite your code in a "better" way but it was hard to construct the whole script with a clear understanding of your needs and no files to process. But I hope the following snippet will help you to start rewriting your code and state significant time improvements.
var debug = true;
var log = function(msg) {
var l = File (Folder.desktop+"/log.txt" );
if ( !debug ) return;
l.open('a');
l.write(msg);
l.close();
};
var main = function() {
var bookFile, uil = app.scriptPreferences.userIntercationLevel;
log("The party has started");
bookFile = File.openDialog("Choose an InDesign Book File", "Indb files: *.indb");
if (!bookFile) return;
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.NEVER_INTERACT;
try {
processBookFile ( bookFile );
}
catch(err) {
alert(err.line+"///"+err.message);
}
app.scriptPreferences.userInteractionLevel = uil;
};
function processBookFile ( bookFile ) {
var book = app.open ( bookFile ),
bks = book.bookContents,
n = bks.length;
while ( n-- ) {
File(bks[n].name)!="Index.indd" && processBookContent ( bks[n] );
}
}
function processBookContent ( bookContent ) {
var bcf = bookContent.fullName,
doc = app.open ( bcf, debug );
//DEAL WITH HEADINGS
processHeadings ( doc );
//DEAL WITH CHAPTERS
processHeadings ( doc );
//add hyperlinks
addHyperlinks( doc);
}
function processHeadings (doc){
var props = {
findWhat : "^[\\u\\d \\:\\;\\?\\-\\'\\\"\\$\\%\\&\\!\\#\\*\\#\\,\\.\\(\\)]+[\\u\\d](?=\\.|,)",
appliedParagraphStyle : "Main"
},
found = findGrep(doc, props),
n = found.length;
while ( n-- ) {
doc.hyperlinkTextDestinations.add(doc, { name: found[i].contents });
}
};
function processChapters (doc ) {
var props = {
findWhat : "^CHAPTER \\d+",
appliedParagraphStyle : "chapter"
},
found = findGrep(doc, props),
n = found.length;
while ( n-- ) {
doc.hyperlinkTextDestinations.add(found[n], found[n].contents);
}
}
function findGrep(doc, props){
app.findGrepPreferences = app.changeGrepPreferences = null;
app.findGrepPreferences.properties = props;
return doc.findGrep();
}
function addHyperlinks (doc){
//a logic of yours
};
main();

Related

Add a paragraph or table etc. at Cursor

I have a function for adding the contents of a separate google document at the cursor point within the active document, but I haven't been able to get it to work. I keep getting the "Element does not contain the specified child element" exception, and then the contents gets pasted to the bottom of the document rather than at the cursor point!
function AddTable() {
//here you need to get document id from url (Example, 1oWyVMa-8fzQ4leCrn2kIk70GT5O9pqsXsT88ZjYE_z8)
var FileTemplateFileId = "1MFG06knf__tcwHWdybaBk124Ia_Mb0gBE0Gk8e0URAM"; //Browser.inputBox("ID der Serienbriefvorlage (aus Dokumentenlink kopieren):");
var doc = DocumentApp.openById(FileTemplateFileId);
var DocName = doc.getName();
//Create copy of the template document and open it
var docCopy = DocumentApp.getActiveDocument();
var totalParagraphs = doc.getBody().getParagraphs(); // get the total number of paragraphs elements
Logger.log(totalParagraphs);
var cursor = docCopy.getCursor();
var totalElements = doc.getNumChildren();
var elements = [];
for (var j = 0; j < totalElements; ++j) {
var body = docCopy.getBody();
var element = doc.getChild(j).copy();
var type = element.getType();
if (type == DocumentApp.ElementType.PARAGRAPH) {
body.appendParagraph(element);
} else if (type == DocumentApp.ElementType.TABLE) {
body.appendTable(element);
} else if (type == DocumentApp.ElementType.LIST_ITEM) {
body.appendListItem(element);
}
// ...add other conditions (headers, footers...
}
Logger.log(element.editAsText().getText());
elements.push(element); // store paragraphs in an array
Logger.log(element.editAsText().getText());
for (var el = 0; el < elements.length; el++) {
var paragraph = elements[el].copy();
var doc = DocumentApp.getActiveDocument();
var bodys = doc.getBody();
var cursor = doc.getCursor();
var element = cursor.getElement();
var container = element.getParent();
try {
var childIndex = body.getChildIndex(container);
bodys.insertParagraph(childIndex, paragraph);
} catch (e) {
DocumentApp.getUi().alert("There was a problem: " + e.message);
}
}
}
You want to copy the objects (paragraphs, tables and lists) from the document of 1MFG06knf__tcwHWdybaBk124Ia_Mb0gBE0Gk8e0URAM to the active Document.
You want to copy the objects to the cursor position on the active Document.
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Modification points:
In your script, appendParagraph, appendTable and appendListItem are used at the 1st for loop. I think that the reason that the copied objects are put to the last of the document is due to this.
var body = docCopy.getBody(); can be put to the out of the for loop.
In your case, I think that when the 1st for loop is modified, 2nd for loop is not required.
When above points are reflected to your script, it becomes as follows.
Modified script:
function AddTable() {
var FileTemplateFileId = "1MFG06knf__tcwHWdybaBk124Ia_Mb0gBE0Gk8e0URAM";
var doc = DocumentApp.openById(FileTemplateFileId);
var docCopy = DocumentApp.getActiveDocument();
var body = docCopy.getBody();
var cursor = docCopy.getCursor();
var cursorPos = docCopy.getBody().getChildIndex(cursor.getElement());
var totalElements = doc.getNumChildren();
for (var j = 0; j < totalElements; ++j) {
var element = doc.getChild(j).copy();
var type = element.getType();
if (type == DocumentApp.ElementType.PARAGRAPH) {
body.insertParagraph(cursorPos + j, element);
} else if (type == DocumentApp.ElementType.TABLE) {
body.insertTable(cursorPos + j, element);
} else if (type == DocumentApp.ElementType.LIST_ITEM) {
body.insertListItem(cursorPos + j, element);
}
}
}
It seems that DocName is not used in your script.
References:
insertParagraph()
insertTable()
insertListItem()
If I misunderstood your question and this was not the result you want, I apologize. At that time, can you provide the sample source Document? By this, I would like to confirm it.

export separate art boards as PNG illustrator script

I've created to a simple illustrator script to loop through the artboards and export each as a PNG back into the folder that the original illustrator file is in. At least I thought I had. I don't need to do anything complicated with layers or different formats this is just a speed up production sort of a thing. I'm a bit rusty on .jsx and appreciate there are similar solutions out there, but I can't get this to run. It seems to fail on the doc.ExportFile line but I really can't see what I'm doing wrong. I'd appreciate anyone taking a look:
var doc = activeDocument;;//Gets the active document
var numArtboards = doc.artboards.length;//returns the number of artboards in the document
var basePath = new File($.fileName).parent.fsName;
$.writeln(doc)
var options;
options = new ExportOptionsPNG24();
options.artBoardClipping = true;
options.matte = false;
options.horizontalScale = 100;
options.verticalScale = 100;
options.transparency = true;
for (var i = 0; i < numArtboards; i++ ) {
doc.artboards.setActiveArtboardIndex( i );
var artboardName = doc.artboards[i].name;
var destFile = new File('/' + artboardName + ".png");
doc.exportFile(destFile, ExportFormat.PNG24 , options);
}
The main problem seemed to be that you hve to have a destination path with the file name. This now seems to work: (You may need to altter the fileNamr generation as code just gets the first nune letters of a filename)
var doc = app.activeDocument;;//Gets the active document
var fleName = doc.name.slice(0, 9)//Get the file code number not the full name;
var numArtboards = doc.artboards.length;//returns the number of artboards in the document
var filePath = (app.activeDocument.fullName.parent.fsName).toString().replace(/\\/g, '/');
$.writeln("fleName= ",fleName)
$.writeln("numArtboards= ",numArtboards)
$.writeln("filePath= ",filePath);
var options = new ExportOptionsPNG24();
for (var i = 0; i < numArtboards; i++ ) {
doc.artboards.setActiveArtboardIndex( i );
options.artBoardClipping = true;
options.matte = false;
options.horizontalScale = 100;
options.verticalScale = 100;
options.transparency = true;
var artboardName = doc.artboards[i].name;
$.writeln("artboardName= ", artboardName);
var destFile = new File(filePath + "/" + fleName + " " + artboardName + ".png");
$.writeln("destFile= ",destFile);
doc.exportFile(destFile,ExportType.PNG24,options);
}

(Scripting) Photoshop removes special characters

I have a script (with a lot of stolen parts you may recognise) that runs through a selected group of images, copies the image and filename and applies to a template in Photoshop. Everything works just fine, except that Photoshop somehow strips umlauts from my strings, ie, Björn becomes Bjorn.
"Logging" through an alert inside of Photoshop (line #30 below) shows that it has the correct string all the way until it's applied as the textItem.contents.
Code provided below, thanks for any help!
#target photoshop
app.displayDialogs = DialogModes.NO;
var templateRef = app.activeDocument;
var templatePath = templateRef.path;
var photo = app.activeDocument.layers.getByName("Photo"); // keycard_template.psd is the active document
// Check if photo layer is SmartObject;
if (photo.kind != "LayerKind.SMARTOBJECT") {
alert("selected layer is not a smart object")
} else {
// Select Files;
if ($.os.search(/windows/i) != -1) {
var photos = File.openDialog("Select photos", "*.png;*.jpg", true)
} else {
var photos = File.openDialog("Select photos", getPhotos, true)
};
if (photos.length) replaceItems();
}
function replaceItems() {
for (var m = 0; m < photos.length; m++) {
if (photos.length > 0) {
// Extract name
var nameStr = photos[m].name;
var nameNoExt = nameStr.split(".");
var name = nameNoExt[0].replace(/\_/g, " ");
// Replace photo and text in template
photo = replacePhoto(photos[m], photo);
// alert(name);
replaceText(templateRef, 'Name', name);
templateRef.saveAs((new File(templatePath + "/keycards/" + name + ".jpg")), jpgOptions, true);
}
}
}
// OS X file picker
function getPhotos(thePhoto) {
if (thePhoto.name.match(/\.(png|jpg)$/i) != null || thePhoto.constructor.name == "Folder") {
return true
};
};
// JPG output options;
var jpgOptions = new JPEGSaveOptions();
jpgOptions.quality = 12; //enter number or create a variable to set quality
jpgOptions.embedColorProfile = true;
jpgOptions.formatOptions = FormatOptions.STANDARDBASELINE;
// Replace SmartObject Contents
function replacePhoto(newFile, theSO) {
app.activeDocument.activeLayer = theSO;
// =======================================================
var idplacedLayerReplaceContents = stringIDToTypeID("placedLayerReplaceContents");
var desc3 = new ActionDescriptor();
var idnull = charIDToTypeID("null");
desc3.putPath(idnull, new File(newFile));
var idPgNm = charIDToTypeID("PgNm");
desc3.putInteger(idPgNm, 1);
executeAction(idplacedLayerReplaceContents, desc3, DialogModes.NO);
return app.activeDocument.activeLayer
};
// Replace text strings
function replaceText(doc, layerName, newTextString) {
for (var i = 0, max = doc.layers.length; i < max; i++) {
var layerRef = doc.layers[i];
if (layerRef.typename === "ArtLayer") {
if (layerRef.name === layerName && layerRef.kind === LayerKind.TEXT) {
layerRef.textItem.contents = decodeURI(newTextString);
}
} else {
replaceText(layerRef, layerName, newTextString);
}
}
}
Had the same problem and tried everything I had in my developer toolbox... for around 3 hours ! without any success and then I found a little hack !
It seems that photoshop is uri encoding the name of the file but don't do it in a way that allow us to do a decodeURI() and get back those special characters.
For exemple instead of "%C3%A9" that should be "é" we get "e%CC%81". So what i do is a replace on the file name to the right UTF-8 code and then a decodeURI. Exemple :
var fileName = file.name
var result = fileName.replace(/e%CC%81/g, '%C3%A9') // allow é in file name
var myTextLayer.contents = decodeURI(result);
Then you can successfully get special chars and in my case accent from filename in your text layer.
You can do a replace for each characters you need for me it was :
'e%CC%81': '%C3%A9', //é
'o%CC%82': '%C3%B4', //ô
'e%CC%80': '%C3%A8', //è
'u%CC%80': '%C3%B9', //ù
'a%CC%80': '%C3%A0', //à
'e%CC%82': '%C3%AA' //ê
I took UTF-8 code from this HTML URL Encoding reference : https://www.w3schools.com/tags/ref_urlencode.asp
Hope it will help somebody one day because nothing existed online on this bug.

Html5 & JavaScript Text to Speech conversion [duplicate]

I am getting a problem when trying to use Speech Synthesis API in Chrome 33. It works perfectly with a shorter text, but if I try longer text, it just stops in the middle. After it has stopped once like that, the Speech Synthesis does not work anywhere within Chrome until the browser is restarted.
Example code (http://jsfiddle.net/Mdm47/1/):
function speak(text) {
var msg = new SpeechSynthesisUtterance();
var voices = speechSynthesis.getVoices();
msg.voice = voices[10];
msg.voiceURI = 'native';
msg.volume = 1;
msg.rate = 1;
msg.pitch = 2;
msg.text = text;
msg.lang = 'en-US';
speechSynthesis.speak(msg);
}
speak('Short text');
speak('Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.');
speak('Another short text');
It stops speaking in the middle of the second text, and I can't get any other page to speak after that.
Is it a browser bug or some kind of security limitation?
I've had this issue for a while now with Google Chrome Speech Synthesis. After some investigation, I discovered the following:
The breaking of the utterances only happens when the voice is not a native voice,
The cutting out usually occurs between 200-300 characters,
When it does break you can un-freeze it by doing speechSynthesis.cancel();
The 'onend' event sometimes decides not to fire. A quirky work-around to this is to console.log() out the utterance object before speaking it. Also I found wrapping the speak invocation in a setTimeout callback helps smooth these issues out.
In response to these problems, I have written a function that overcomes the character limit, by chunking the text up into smaller utterances, and playing them one after another. Obviously you'll get some odd sounds sometimes as sentences might be chunked into two separate utterances with a small time delay in between each, however the code will try and split these points at punctuation marks as to make the breaks in sound less obvious.
Update
I've made this work-around publicly available at https://gist.github.com/woollsta/2d146f13878a301b36d7#file-chunkify-js. Many thanks to Brett Zamir for his contributions.
The function:
var speechUtteranceChunker = function (utt, settings, callback) {
settings = settings || {};
var newUtt;
var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
newUtt = utt;
newUtt.text = txt;
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
}
if (callback !== undefined) {
callback();
}
});
}
else {
var chunkLength = (settings && settings.chunkLength) || 160;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
var chunkArr = txt.match(pattRegex);
if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
//call once all text has been spoken...
if (callback !== undefined) {
callback();
}
return;
}
var chunk = chunkArr[0];
newUtt = new SpeechSynthesisUtterance(chunk);
var x;
for (x in utt) {
if (utt.hasOwnProperty(x) && x !== 'text') {
newUtt[x] = utt[x];
}
}
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
return;
}
settings.offset = settings.offset || 0;
settings.offset += chunk.length - 1;
speechUtteranceChunker(utt, settings, callback);
});
}
if (settings.modifier) {
settings.modifier(newUtt);
}
console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
}, 0);
};
How to use it...
//create an utterance as you normally would...
var myLongText = "This is some long text, oh my goodness look how long I'm getting, wooooohooo!";
var utterance = new SpeechSynthesisUtterance(myLongText);
//modify it as you normally would
var voiceArr = speechSynthesis.getVoices();
utterance.voice = voiceArr[2];
//pass it into the chunking function to have it played out.
//you can set the max number of characters by changing the chunkLength property below.
//a callback function can also be added that will fire once the entire text has been spoken.
speechUtteranceChunker(utterance, {
chunkLength: 120
}, function () {
//some code to execute when done
console.log('done');
});
Hope people find this as useful.
I have solved the probleme while having a timer function which call the pause() and resume() function and callset the timer again. On the onend event I clear the timer.
var myTimeout;
function myTimer() {
window.speechSynthesis.pause();
window.speechSynthesis.resume();
myTimeout = setTimeout(myTimer, 10000);
}
...
window.speechSynthesis.cancel();
myTimeout = setTimeout(myTimer, 10000);
var toSpeak = "some text";
var utt = new SpeechSynthesisUtterance(toSpeak);
...
utt.onend = function() { clearTimeout(myTimeout); }
window.speechSynthesis.speak(utt);
...
This seem to work well.
A simple and effective solution is to resume periodically.
function resumeInfinity() {
window.speechSynthesis.resume();
timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
}
You can associate this with the onend and onstart events, so you will only be invoking the resume if necessary. Something like:
var utterance = new SpeechSynthesisUtterance();
utterance.onstart = function(event) {
resumeInfinity();
};
utterance.onend = function(event) {
clearTimeout(timeoutResumeInfinity);
};
I discovered this by chance!
Hope this help!
The problem with Peter's answer is it doesn't work when you have a queue of speech synthesis set up. The script will put the new chunk at the end of the queue, and thus out of order. Example: https://jsfiddle.net/1gzkja90/
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>
u = new SpeechSynthesisUtterance();
$(document).ready(function () {
$('.t').each(function () {
u = new SpeechSynthesisUtterance($(this).text());
speechUtteranceChunker(u, {
chunkLength: 120
}, function () {
console.log('end');
});
});
});
/**
* Chunkify
* Google Chrome Speech Synthesis Chunking Pattern
* Fixes inconsistencies with speaking long texts in speechUtterance objects
* Licensed under the MIT License
*
* Peter Woolley and Brett Zamir
*/
var speechUtteranceChunker = function (utt, settings, callback) {
settings = settings || {};
var newUtt;
var txt = (settings && settings.offset !== undefined ? utt.text.substring(settings.offset) : utt.text);
if (utt.voice && utt.voice.voiceURI === 'native') { // Not part of the spec
newUtt = utt;
newUtt.text = txt;
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
}
if (callback !== undefined) {
callback();
}
});
}
else {
var chunkLength = (settings && settings.chunkLength) || 160;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
var chunkArr = txt.match(pattRegex);
if (chunkArr[0] === undefined || chunkArr[0].length <= 2) {
//call once all text has been spoken...
if (callback !== undefined) {
callback();
}
return;
}
var chunk = chunkArr[0];
newUtt = new SpeechSynthesisUtterance(chunk);
var x;
for (x in utt) {
if (utt.hasOwnProperty(x) && x !== 'text') {
newUtt[x] = utt[x];
}
}
newUtt.addEventListener('end', function () {
if (speechUtteranceChunker.cancel) {
speechUtteranceChunker.cancel = false;
return;
}
settings.offset = settings.offset || 0;
settings.offset += chunk.length - 1;
speechUtteranceChunker(utt, settings, callback);
});
}
if (settings.modifier) {
settings.modifier(newUtt);
}
console.log(newUtt); //IMPORTANT!! Do not remove: Logging the object out fixes some onend firing issues.
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
}, 0);
};
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>
In my case, the answer was to "chunk" the string before adding them to the queue. See here: http://jsfiddle.net/vqvyjzq4/
Many props to Peter for the idea as well as the regex (which I still have yet to conquer.) I'm sure the javascript can be cleaned up, this is more of a proof of concept.
<script type='text/javascript' src='http://code.jquery.com/jquery-2.1.0.js'></script>
<script type='text/javascript'>
var chunkLength = 120;
var pattRegex = new RegExp('^[\\s\\S]{' + Math.floor(chunkLength / 2) + ',' + chunkLength + '}[.!?,]{1}|^[\\s\\S]{1,' + chunkLength + '}$|^[\\s\\S]{1,' + chunkLength + '} ');
$(document).ready(function () {
var element = this;
var arr = [];
var txt = replaceBlank($(element).text());
while (txt.length > 0) {
arr.push(txt.match(pattRegex)[0]);
txt = txt.substring(arr[arr.length - 1].length);
}
$.each(arr, function () {
var u = new SpeechSynthesisUtterance(this.trim());
window.speechSynthesis.speak(u);
});
});
</script>
<p class="t">MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence.</p>
<p class="t">Joe waited for the train.</p>
<p class="t">The train was late.</p>
<p class="t">Mary and Samantha took the bus.</p>
Here is what i ended up with, it simply splits my sentences on the period "."
var voices = window.speechSynthesis.getVoices();
var sayit = function ()
{
var msg = new SpeechSynthesisUtterance();
msg.voice = voices[10]; // Note: some voices don't support altering params
msg.voiceURI = 'native';
msg.volume = 1; // 0 to 1
msg.rate = 1; // 0.1 to 10
msg.pitch = 2; //0 to 2
msg.lang = 'en-GB';
msg.onstart = function (event) {
console.log("started");
};
msg.onend = function(event) {
console.log('Finished in ' + event.elapsedTime + ' seconds.');
};
msg.onerror = function(event)
{
console.log('Errored ' + event);
}
msg.onpause = function (event)
{
console.log('paused ' + event);
}
msg.onboundary = function (event)
{
console.log('onboundary ' + event);
}
return msg;
}
var speekResponse = function (text)
{
speechSynthesis.cancel(); // if it errors, this clears out the error.
var sentences = text.split(".");
for (var i=0;i< sentences.length;i++)
{
var toSay = sayit();
toSay.text = sentences[i];
speechSynthesis.speak(toSay);
}
}
2017 and this bug is still around. I happen to understand this problem quite well, being the developer of the award-winning Chrome extension Read Aloud. OK, just kidding about the award winning part.
Your speech will get stuck if it's longer than 15 seconds.
I discover that Chrome uses a 15 second idle timer to decide when to deactivate an extension's event/background page. I believe this is the culprit.
The workaround I've used is a fairly complicated chunking algorithm that respects punctuation. For Latin languages, I set max chunk size at 36 words. The code is open-source, if you're inclined: https://github.com/ken107/read-aloud/blob/315f1e1d5be6b28ba47fe0c309961025521de516/js/speech.js#L212
The 36-word limit works well most of the time, staying within 15 seconds. But there'll be cases where it still gets stuck. To recover from that, I use a 16 second timer.
I ended up chunking up the text and having some intelligence around handling of various punctucations like periods, commas, etc. For example, you don't want to break the text up on a comma if it's part of a number (i.e., $10,000).
I have tested it and it seems to work on arbitrarily large sets of input and it also appears to work not just on the desktop but on android phones and iphones.
Set up a github page for the synthesizer at: https://github.com/unk1911/speech
You can see it live at: http://edeliverables.com/tts/
new Vue({
el: "#app",
data: {
text: `Collaboratively administrate empowered markets via plug-and-play networks. Dynamically procrastinate B2C users after installed base benefits. Dramatically visualize customer directed convergence without revolutionary ROI. Efficiently unleash cross-media information without cross-media value. Quickly maximize timely deliverables for real-time schemas. Dramatically maintain clicks-and-mortar solutions without functional solutions.`
},
methods:{
stop_reading() {
const synth = window.speechSynthesis;
synth.cancel();
},
talk() {
const synth = window.speechSynthesis;
const textInput = this.text;
const utterThis = new SpeechSynthesisUtterance(textInput);
utterThis.pitch = 0;
utterThis.rate = 1;
synth.speak(utterThis);
const resumeInfinity = () => {
window.speechSynthesis.resume();
const timeoutResumeInfinity = setTimeout(resumeInfinity, 1000);
}
utterThis.onstart = () => {
resumeInfinity();
};
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="talk">Speak</button>
<button #click="stop_reading">Stop</button>
</div>
As Michael proposed, Peter's solutions is really great except when your text is on different lines. Michael created demo to better illustrate the problem with it. - https://jsfiddle.net/1gzkja90/ and proposed another solution.
To add one maybe simpler way to solve this is to remove line breaks from textarea in Peter's solution and it works just great.
//javascript
var noLineBreaks = document.getElementById('mytextarea').replace(/\n/g,'');
//jquery
var noLineBreaks = $('#mytextarea').val().replace(/\n/g,'');
So in Peter's solution it might look the following way :
utterance.text = $('#mytextarea').val().replace(/\n/g,'');
But still there's problem with canceling the speech. It just goes to another sequence and won't stop.
Other suggestion do weird thing with dot or say DOT and do not respect speech intonnation on sentence end.
var CHARACTER_LIMIT = 200;
var lang = "en";
var text = "MLA format follows the author-page method of in-text citation. This means that the author's last name and the page number(s) from which the quotation or paraphrase is taken must appear in the text, and a complete reference should appear on your Works Cited page. The author's name may appear either in the sentence itself or in parentheses following the quotation or paraphrase, but the page number(s) should always appear in the parentheses, not in the text of your sentence. Joe waited for the train. The train was late. Mary and Samantha took the bus.";
speak(text, lang)
function speak(text, lang) {
//Support for multipart text (there is a limit on characters)
var multipartText = [];
if (text.length > CHARACTER_LIMIT) {
var tmptxt = text;
while (tmptxt.length > CHARACTER_LIMIT) {
//Split by common phrase delimiters
var p = tmptxt.search(/[:!?.;]+/);
var part = '';
//Coludn't split by priority characters, try commas
if (p == -1 || p >= CHARACTER_LIMIT) {
p = tmptxt.search(/[,]+/);
}
//Couldn't split by normal characters, then we use spaces
if (p == -1 || p >= CHARACTER_LIMIT) {
var words = tmptxt.split(' ');
for (var i = 0; i < words.length; i++) {
if (part.length + words[i].length + 1 > CHARACTER_LIMIT)
break;
part += (i != 0 ? ' ' : '') + words[i];
}
} else {
part = tmptxt.substr(0, p + 1);
}
tmptxt = tmptxt.substr(part.length, tmptxt.length - part.length);
multipartText.push(part);
//console.log(part.length + " - " + part);
}
//Add the remaining text
if (tmptxt.length > 0) {
multipartText.push(tmptxt);
}
} else {
//Small text
multipartText.push(text);
}
//Play multipart text
for (var i = 0; i < multipartText.length; i++) {
//Use SpeechSynthesis
//console.log(multipartText[i]);
//Create msg object
var msg = new SpeechSynthesisUtterance();
//msg.voice = profile.systemvoice;
//msg.voiceURI = profile.systemvoice.voiceURI;
msg.volume = 1; // 0 to 1
msg.rate = 1; // 0.1 to 10
// msg.rate = usersetting || 1; // 0.1 to 10
msg.pitch = 1; //0 to 2*/
msg.text = multipartText[i];
msg.speak = multipartText;
msg.lang = lang;
msg.onend = self.OnFinishedPlaying;
msg.onerror = function (e) {
console.log('Error');
console.log(e);
};
/*GC*/
msg.onstart = function (e) {
var curenttxt = e.currentTarget.text;
console.log(curenttxt);
//highlight(e.currentTarget.text);
//$('#showtxt').text(curenttxt);
//console.log(e);
};
//console.log(msg);
speechSynthesis.speak(msg);
}
}
https://jsfiddle.net/onigetoc/9r27Ltqz/
I want to say that through Chrome Extensions and Applications, I solved this quite irritating issue through using chrome.tts, since chrome.tts allows you to speak through the browser, instead of the window which stops the talk when you close the window.
Using the below code, you can fix the above issue with large speakings:
chrome.tts.speak("Abnormally large string, over 250 characters, etc...");
setInterval(() => { chrome.tts.resume(); }, 100);
I'm sure that will work, but I did this just to be safe:
var largeData = "";
var smallChunks = largeData.match(/.{1,250}/g);
for (var chunk of smallChunks) {
chrome.tts.speak(chunk, {'enqueue': true});
}
Hope this helps someone! It helped make my application work more functionally, and epicly.
Yes, the google synthesis api will stop at some point during speaking a long text.
We can see onend event, onpause and onerror event of SpeechSynthesisUtterance won't be fired normally when the sudden stop happens, so does the speechSynthesis onerror event.
After several trials, found speechSynthesis.paused is working, and speechSynthesis.resume() can help resume the speaking.
Hence we just need to have a timer to check the pause status during the speaking, and calling speechSynthesis.resume() to continue.
The interval should be small enough to prevent glitch when continuing the speak.
let timer = null;
let reading = false;
let readText = function(text) {
if (!reading) {
speechSynthesis.cancel();
if (timer) {
clearInterval(timer);
}
let msg = new SpeechSynthesisUtterance();
let voices = window.speechSynthesis.getVoices();
msg.voice = voices[82];
msg.voiceURI = 'native';
msg.volume = 1; // 0 to 1
msg.rate = 1.0; // 0.1 to 10
msg.pitch = 1; //0 to 2
msg.text = text;
msg.lang = 'zh-TW';
msg.onerror = function(e) {
speechSynthesis.cancel();
reading = false;
clearInterval(timer);
};
msg.onpause = function(e) {
console.log('onpause in ' + e.elapsedTime + ' seconds.');
}
msg.onend = function(e) {
console.log('onend in ' + e.elapsedTime + ' seconds.');
reading = false;
clearInterval(timer);
};
speechSynthesis.onerror = function(e) {
console.log('speechSynthesis onerror in ' + e.elapsedTime + ' seconds.');
speechSynthesis.cancel();
reading = false;
clearInterval(timer);
};
speechSynthesis.speak(msg);
timer = setInterval(function(){
if (speechSynthesis.paused) {
console.log("#continue")
speechSynthesis.resume();
}
}, 100);
reading = true;
}
}

Iframe onload seems to not work in time

after dynamicly creating some iframes, setting src and onload, I expect that the contents would be available for putting it to an array and sort the array. Next would be removing the iframes (interesting content already put in array) and create a table on my HTML-page. When no alert is set before sorting, the array is empty as the onload fires just before finishing the script. When an alert is place before sorting,I found the onload-procedure fired, array was populated and everything works fine?!! But I don't want to put an alert, can anyone explain what I did wrong? It's my first script so please help me to understand.
function LoadFile( ) {
var FName, PName, myID, myFrame;
var myBody = document.getElementById("StartList");
for ( var i = 0; i < FileList.length; i++ ) {
// Read input file into frame
FName = FileList[i].FName;
PName = FName + ".html";
myID = "iframe" + i;
// Create frame.
myFrame = document.createElement("iframe");
myFrame.setAttribute("id", myID);
myFrame.setAttribute("width","0");
myFrame.setAttribute("height","0");
myFrame.setAttribute('src', PName);
//Attach onload-event to frame, triggering ReadTableInfo.
if (myFrame.attachEvent){
myFrame.attachEvent("onload", function(){
ReadTableInfo();
});
} else {
myFrame.onload = function(){
ReadTableInfo();
};
}
myBody.appendChild(myFrame);
}
}
function ReadTableInfo() {
var a = document.getElementsByTagName("iframe")[idx];
var p = FileList[idx].FName;
var b = (a.contentWindow || a.contentDocument);
var td;
if ( b.document) {
b = b.document;
// Get and process table with functions and procedures.
var myTable = b.getElementsByTagName("Table")[5];
var myList = myTable.getElementsByTagName("TR");
var Name = "";
var Desc = "";
for ( var j = 0; j < myList.length; j++) {
Name = myTable.getElementsByTagName("TR") [j].getElementsByTagName("A")[0].innerHTML;
if ( myTable.getElementsByTagName("TR")[j].getElementsByTagName("TD")[1] != null) {
td = myTable.getElementsByTagName("TR")[j].getElementsByTagName("TD")[1];
Desc = td.textContent || td.innerText || "";
}
if ( searchval == "" || ( TestVal.test(Name) && searchkey == 1 ) || ( TestVal.test(Desc) && searchkey == 2 ) ) {
ProcList[ProcList.length++] = new AddProcList(Name.toLowerCase(), p.toLowerCase(), Desc);
}
Name = "";
Desc = "";
}
idx++;
}
}
function UpdateList( opt ) {
searchval = document.getElementById("edtSearchVal").value;
TestVal = new RegExp(".", "i");
if ( searchval !== "" ) {
if ( opt == 2 ) {
TestVal = new RegExp(searchval, "i"); // searchpattern for RegExp descriptions
} else {
TestVal = new RegExp(searchval.replace(" ","_"), "i"); // searchpattern for RegExp.
}
}
switch ( opt ) {
case 1: searchkey = 1;
break;
case 2: searchkey = 2;
break;
default:
searchkey = 3;
}
Init();
// Get package names from index.
SetFileList(); // Determines which external files to examine.
LoadFile(); // Loads the external files into iframes to be used later.
alert("Start generating list, this may take a while."); // Necessary to read frames! Don't know why???
var sortkeys = {FName:"a",PName:"a"}; // Sorting order of ProcList, which will be the listorder.
ProcList.keySort(sortkeys); // Sort ProcList.
TableCreate(); // Make new table with all entries in ProcList
}
Thanks for any comments, just here to learn from others :))
your LoadFile(); is loading content to iframe , and alert() is giving just enough time for iframe content to load completely, hence rest of the code works. If you remove alert then rest of the code is executed immediately before iframe content is loaded.
I would suggest to put onload even inside iframe page as well. Then propagate this event to parent window. To access parent container window you can do window.parent inside iframe.

Categories