├── .editorconfig ├── LICENSE ├── README.md ├── full ├── BookUpdateTOCs.jsx ├── DocPreventOpening.jsx ├── DocShowClosestByName.jsx ├── LinkAutoUpdate.jsx ├── ReframeAllPages.jsx ├── SelApplyStyleNone.jsx ├── SelPlacePDFPage.jsx ├── SelSetFrameHeightByLineCount.jsx ├── SelTransformDimensions.jsx ├── StyleListGotoSample.jsx └── TypeActuallyShowHidden.jsx ├── snip ├── DocBackupSave.jsx ├── DocGrepTextFindAll.jsx ├── FileParseCSV.jsx ├── FontGlyphCount.jsx ├── GroupAddItems.jsx ├── SelFirstImage.jsx ├── SelFlattenTransform.jsx ├── TableCellBox.jsx ├── TextSortParagraphs.jsx └── TransformationMatrixInfo.jsx └── tests └── CustomTextSortParagraphs.jsx /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | max_line_length = 80 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Marc Autret - http://indiscripts.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IdGoodies 2 | Cool InDesign scripts and snippets 3 | -------------------------------------------------------------------------------- /full/BookUpdateTOCs.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: BookUpdateTOCs [1.0] 4 | Desc: Update all Tables of Contents in the active Book. 5 | Path: /full/BookUpdateTOCs.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5/CS4 [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220726 (YYMMDD) 14 | Modified: 220726 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | You may find convenient to batch-update all TOCs present in a book, since 25 | InDesign doesn't seem to provide such feature. The present script inspects 26 | every chapter and runs the UpdateTableOfContents command if available. 27 | 28 | Note: chapters that aren't presently visible are temporarily opened, saved, 29 | and closed. 30 | 31 | Then the script displays a basic report at the end of the process. 32 | 33 | */ 34 | 35 | function updateBookTocs( ret,msg,nme,mna,bk,ff,closeBook,K,a,i,j,pp,x,closeDoc,uri,doc,S,t) 36 | //---------------------------------- 37 | // Update TOCs in Book. 38 | // => str[] | false 39 | { 40 | mna = app.menuActions.itemByName('$ID/UpdateTableOfContentsCmd'); 41 | if( !mna.isValid ){ return ["Update TOC action unavailable."]; } 42 | 43 | // Some constants. 44 | // --- 45 | const BCS_USE = +BookContentStatus.DOCUMENT_IN_USE; 46 | const BCS_MISS = +BookContentStatus.MISSING_DOCUMENT; 47 | const BCS_OPEN = +BookContentStatus.DOCUMENT_IS_OPEN; 48 | const TOC_TYPE = +StoryTypes.TOC_STORY; 49 | const SAVE_YES = +SaveOptions.YES; 50 | 51 | // Open the target Book (if not already active.) 52 | // --- 53 | closeBook = 0; 54 | if( !(bk=app.properties.activeBook) ) 55 | { 56 | ff = File.openDialog("Choose the BOOK File", "INDB:*.indb", false); 57 | if( !ff ) return false; 58 | ff = File(ff); 59 | if( !ff.exists ) return false; 60 | bk = app.open(ff); 61 | closeBook = 1; 62 | } 63 | 64 | // Loop in chapters. 65 | // --- 66 | ret = Array(); 67 | a = bk.bookContents.everyItem().getElements(); 68 | for( K=app.documents, i=a.length ; i-- ; (msg&&ret[ret.length]=msg), (closeDoc&&doc.close(SAVE_YES)) ) 69 | { 70 | closeDoc = 0; 71 | msg = ''; 72 | pp = a[i].properties; 73 | x = +pp.status; 74 | 75 | // Filter out missing/locked/error chapters. 76 | // --- 77 | if( (!x) || BCS_USE===x || BCS_MISS===x ) continue; 78 | ff = File(pp.fullName); 79 | if( !ff.exists ) continue; 80 | 81 | // Set the chapter as target document --> doc 82 | // --- 83 | if( BCS_OPEN===x ) 84 | { 85 | uri = ff.absoluteURI; 86 | for( j=K.length ; j-- && uri !== (K[j].properties.fullName||0).absoluteURI ; ); 87 | if( 0 > j ) continue; 88 | doc = K[j].getElements()[0]; 89 | } 90 | else 91 | { 92 | doc = app.open(ff); 93 | closeDoc = 1; 94 | } 95 | 96 | // Is there a TOC story here? 97 | // --- 98 | nme = doc.properties.name; 99 | msg = "No TOC found in " + nme; 100 | S = doc.stories; 101 | for( t=S.everyItem().storyType, j=t.length ; j-- && TOC_TYPE !== +t[j] ; ); 102 | if( 0 > j ) continue; 103 | if( !(t=S[j]).isValid ) continue; // t :: Story 104 | t = (t.textContainers||0)[0]||0; // t :: TextFrame | falsy 105 | if( !t.isValid ) continue; 106 | 107 | // Select the frame and invoke the menu action. 108 | // --- 109 | doc===app.properties.activeDocument || (app.activeDocument = doc); 110 | 111 | msg = "Couldn't update TOC in " + nme; 112 | try 113 | { 114 | app.select(t); 115 | if( mna.enabled ) 116 | { 117 | mna.invoke(); 118 | msg = "TOC updated in " + nme; 119 | } 120 | } 121 | catch(e){ msg = nme + ': ' + e } 122 | } 123 | 124 | // Terminate. 125 | // --- 126 | if( closeBook ) bk.close(SAVE_YES); 127 | return ret; 128 | }; 129 | 130 | // Run me. 131 | // --- 132 | (function( prf,bkp,r) 133 | { 134 | const UI_NEVER = +UserInteractionLevels.NEVER_INTERACT; 135 | 136 | prf = app.scriptPreferences; 137 | bkp = +prf.userInteractionLevel; 138 | prf.userInteractionLevel = UI_NEVER; 139 | 140 | r = updateBookTocs(); 141 | 142 | prf.userInteractionLevel = bkp; 143 | 144 | if( bkp !== UI_NEVER && r && r.length ) 145 | { 146 | alert( r.slice(0,25).join('\r') ); 147 | } 148 | })(); 149 | -------------------------------------------------------------------------------- /full/DocPreventOpening.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: DocPreventOpening [1.0] 4 | Desc: Prohibits the opening of certain InDesign documents based on their filename. 5 | Path: /full/DocPreventOpening.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5 [Mac/Win] 8 | L10N: --- 9 | Kind: Script / Startup script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220221 (YYMMDD) 14 | Modified: 220221 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This script prevents the opening of documents having a certain filename pattern. 25 | Change the `RE_EXCLUDE` constant (see below) to make it fit your needs. 26 | 27 | The present implementation relies on the event `beforeOpen` available in CS5/CS6/CC. 28 | Its target is the Application instance (which may not be obvious at first!) but it 29 | still provides, as any DocumentEvent, a `fullName` property (File) that indicates 30 | which document is about to be opened. Unlike `afterOpen`, `beforeOpen` is cancelable, 31 | so `ev.preventDefault()` actually cancels document opening if you decide so. 32 | 33 | [REM] No `#targetengine` directive is used in this script as we don't really need 34 | to store persistent data for such a basic process. Instead, the script file itself 35 | is registered as "the event handler" and we use `$.global.evt` to determine whether 36 | the script is invoked for either installing the listener or managing the event. 37 | 38 | - To install the listener just execute the script manually (once). You can still 39 | change and refine your `RE_EXCLUDE` pattern dynamically (since there is no persis- 40 | tent engine.) 41 | 42 | - To uninstall the listener, close your InDesign session. (Of course, if you have 43 | installed the present file as a startup script, remove it from its folder!) 44 | 45 | DISCLAIMER. - DO NOT INSTALL THIS SCRIPT AS A STARTUP SCRIPT UNLESS YOU REALLY 46 | KNOW WHAT YOU ARE DOING. USERS WILL BE UNABLE TO OPEN DOCUMENTS HAVING A CERTAIN 47 | FILE NAME PATTERN AND WON'T BE NOTIFIED. IN CASE YOU DEPLOY THIS SOLUTION, MAKE 48 | SURE YOUR CLIENTS ARE INFORMED. ALWAYS PROVIDE INSTRUCTIONS FOR UNINSTALLING! 49 | 50 | */ 51 | 52 | // ADJUST THIS REGEX TO YOUR NEEDS. 53 | // --- 54 | const RE_EXCLUDE = /\.skip\.indd$/; // Exclude filenames ending with ".skip.indd" 55 | 56 | (function(/*any*/ev, ff,t) 57 | { 58 | if( (ev||0).isValid ) 59 | { 60 | // Run the event handler. 61 | // --- 62 | 'beforeOpen' == ev.eventType // Safer (make sure you're managing the right event!) 63 | && (t=ev.properties.fullName) // Get the `fullName` (File object.) 64 | && RE_EXCLUDE.test(t.fsName) // Should be excluded? 65 | && ev.preventDefault(); // Cancel the opening. 66 | } 67 | else 68 | { 69 | // Install the event listener (if not yet installed!) 70 | // --- 71 | const UID = 'myBeforeOpenKiller'; 72 | (ff=File($.fileName)).exists 73 | && !((t=app.eventListeners).itemByName(UID)).isValid 74 | && ((t.add('beforeOpen',ff)).name=UID); 75 | } 76 | })($.global.evt); 77 | -------------------------------------------------------------------------------- /full/DocShowClosestByName.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: DocShowClosestByName [1.0] 4 | Desc: Makes the 'closest' document active (w.r.t name.) 5 | Path: /full/DocShowClosestByName.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5 [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220623 (YYMMDD) 14 | Modified: 220623 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Based on a discussion of the InDesign Scripting forum, "Javascript to find a 25 | similar filename among all open documents" (https://adobe.ly/3HJfSS9), 26 | this script automatically selects the document having the most similar 27 | filename based on the Levenshtein distance. 28 | 29 | A custom RegExp can be supplied to the showClosestDocByName function in 30 | order to filter out specific name parts that must be ignored anyway 31 | (typically, shared suffixes among your set of documents.) 32 | 33 | */ 34 | 35 | // ADJUST THIS REGEX TO YOUR NEEDS -- OR COMMENT THE LINE! 36 | // (It defines a shared suffix that will be ignored and fits 37 | // the particular example discussed in the InDesign forum.) 38 | // --- 39 | const MY_DROP_REGEX = /_spec-sheet-[a-z0-9]+_v\d+\.indd$/i; 40 | 41 | 42 | function dist(/*str*/a,/*str*/b, x,y,t,p,Q,i,j,c,r,q) 43 | //---------------------------------- 44 | // Measures the difference between two strings `a` and `b` 45 | // using the Levenshtein distance algorithm. 46 | // [REF] en.wikipedia.org/wiki/Levenshtein_distance 47 | // Implementation from IdExtenso: 48 | // github.com/indiscripts/IdExtenso/blob/master/core/Ext/$$.string.jsxinc 49 | // => uint 50 | { 51 | if( a === b ) return 0; 52 | x = a.length||0; 53 | y = b.length||0; 54 | if( !(x&&y) ) return x||y; 55 | 56 | while( a.charAt(--x)===b.charAt(--y) ); // R-trim => (x,y)::maxIds 57 | (t=x) <= y || (x=y,y=t, t=a,a=b,b=t); // Symm. => x <= y 58 | for( p=-1 ; ++p <= x && a.charAt(p)===b.charAt(p) ; ); // L-trim => p::startId 59 | if( p >= x ) return 1+y-p; 60 | 61 | // Loop. 62 | // --- 63 | for( Q=Array(1+x), j=p ; j <= y ; ++j ) 64 | for( c=b.charAt(j), r=1+(t=j-p), i=p ; i <= x ; t=q, Q[i++]=r ) 65 | { 66 | c!==a.charAt(i) && ++t; 67 | q = Q[i]||(Q[i]=1+i-p); 68 | r = (q=Q[i]) > r 69 | ? ( t > r ? (1+r) : t) 70 | : ( t > q ? (1+q) : t); 71 | } 72 | return r; 73 | } 74 | 75 | function showClosestDocByName(/*?RegExp*/dropRegex,/*?Document*/doc, re,ini,D,a,i,t) 76 | //---------------------------------- 77 | // Makes active the 'closest' document based on Levenshtein 78 | // distance AND ignoring name part(s) captured by `dropRegex`. 79 | // - If set, `dropRegex` tells wich part of a doc name should 80 | // be entirely ignored by the algo. Default is /\.indd$/i 81 | // - If supplied, `doc` is the input document. Default is the 82 | // active one. 83 | // --- 84 | // => undef 85 | { 86 | const CHR = String.fromCharCode; 87 | 88 | // Checkpoint. 89 | // --- 90 | doc || doc=app.properties.activeDocument; 91 | if( !doc ){ alert("No document available."); return; } 92 | 93 | re = 'function'==typeof dropRegex && 'RegExp'==dropRegex.__class__ 94 | ? dropRegex 95 | : /\.indd$/i; 96 | 97 | // Create a reference key for the input doc name. 98 | // --- 99 | i = doc.properties.index; // 0 if active 100 | ini = CHR(i) + doc.properties.name.replace(re,''); 101 | 102 | // Get a reference keys for all available documents. 103 | // --- 104 | D = app.documents; 105 | a = D.length && D.everyItem().name; 106 | if( 2 > (0|a.length) ) return; 107 | for( i=a.length ; i-- ; a[i]=CHR(i)+a[i].replace(re,'') ); 108 | 109 | // Sort the keys relative to `ini`, based on `dist()`. 110 | // --- 111 | a.sort( function(x,y){ return dist(ini,x)-dist(ini,y)} ); 112 | 113 | // [REM] a[0] is ini ; the closest key is a[1]. 114 | // --- 115 | t = D[a[1].charCodeAt(0)]; 116 | if( !t.visible ) 117 | { 118 | alert("The closest document is " + t.name + " but it is not visible."); 119 | } 120 | else 121 | { 122 | app.activeDocument = t; 123 | } 124 | } 125 | 126 | showClosestDocByName(MY_DROP_REGEX); 127 | -------------------------------------------------------------------------------- /full/LinkAutoUpdate.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: LinkAutoUpdate [4.3] 4 | Desc: Automatically updates 'out-of-date' links. 5 | Path: /full/LinkAutoUpdate.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CS5/CS6/CC [Mac/Win] 8 | L10N: --- 9 | Kind: Script / Startup script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 181604 (YYMMDD) 14 | Modified: 210221 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This script automatically updates 'out-of-date' links in InDesign. It is a 25 | listener. You can either run it manually during your session, or load it 26 | as a 'startup script'. 27 | 28 | An alternate code exists based on a script from あるふぁ(仮) : 29 | twitter.com/peprintenpa/status/1361132929678667787?ref_src=twsrc%5Etfw 30 | which listens to the event `afterAttributeChanged` and delegates the task 31 | to BridgeTalk. 32 | 33 | The present implementation relies on the event `afterLinksChanged` 34 | available in CS5/CS6/CC. Its target is the document itself, so we access 35 | the links from `ev.target.links.everyItem()`. (For the record, the 36 | alternate implementation is kept in the event handler.) 37 | 38 | */ 39 | 40 | #targetengine 'LinkAutoUpdate' 41 | 42 | 'string' == typeof $.global.EVENT_TYPE 43 | || ( $.global.EVENT_TYPE = 'afterLinksChanged'); // Alt. 'afterAttributeChanged' 44 | 45 | 46 | 'function' == typeof $.global.onEvent || ($.global.onEvent= function onEvent(/*{eventType,target}*/ev, tg,s,i) 47 | //---------------------------------------------------------- 48 | // ev.eventType :: 'afterLinksChanged' | 'afterAttributeChanged' 49 | // ev.target :: Document | any 50 | { 51 | const OUTDATED = LinkStatus.LINK_OUT_OF_DATE.toString(); 52 | 53 | if( !(tg=(ev||0).target) ) return; 54 | 55 | if( 'afterLinksChanged' == ev.eventType ) 56 | { 57 | if( (!(tg instanceof Document)) || !(tg=tg.links.everyItem()).isValid ) return; 58 | 59 | try 60 | { 61 | s = tg.status; // LinkStatus[] 62 | if( -1 == (s=s.join('|')).indexOf(OUTDATED) ) return; // Aborts faster. 63 | for 64 | ( 65 | s=s.split('|'), tg=tg.getElements(), i=tg.length ; 66 | i-- ; 67 | OUTDATED===s[i] && tg[i].update() 68 | ); 69 | } 70 | catch(e) 71 | { 72 | alert($.engineName + " Error:\r" + e); 73 | } 74 | 75 | return; 76 | } 77 | 78 | // Alternate code. 79 | // --- 80 | if( 'afterAttributeChanged' == ev.eventType ) 81 | { 82 | if( 'Link' != tg.constructor.name || tg.status != LinkStatus.LINK_OUT_OF_DATE ) return; 83 | 84 | // --- 85 | // We cannot simply invoke `tg.update()` from within the event handler 86 | // as InDesign would consider it 'removing' the event target and fire a 87 | // runtime error. The solution is to delegate the task to BridgeTalk. 88 | // --- 89 | 90 | with( new BridgeTalk ) 91 | { 92 | target = BridgeTalk.appSpecifier + '#' + $.engineName; 93 | body = $.global.localize('try{resolve(%1).update()}catch(e){alert("%2 Error:\r"+e)}' 94 | , tg.toSpecifier().toSource() 95 | , $.engineName 96 | ); 97 | send(); 98 | } 99 | } 100 | 101 | }) 102 | .help = $.global.localize("%1\r\r%2\r\r%1" 103 | , '-----------------------' 104 | , [ 105 | "> Out-of-date links will refresh automatically.", 106 | "> Re-run this script to turn it off.", 107 | "[Tip] Put the JSX file in your `startup scripts` folder to make it automatically active." 108 | ].join('\r\r') 109 | ); 110 | 111 | (function(/*str*/evType,/*str*/name,/*fct*/callback, t,evls,MX_SAVE,doc) 112 | //---------------------------------------------------------- 113 | // Install/uninstall the listener. 114 | { 115 | evls = app.eventListeners; 116 | if( (t=evls.itemByName(name)).isValid && t.properties.handler===callback ) 117 | { 118 | t.remove(); 119 | alert(name + " is turned OFF."); 120 | } 121 | else 122 | { 123 | MX_SAVE = app.performanceMetric(+PerformanceMetricOptions.MINISAVE_COUNT); 124 | 125 | // Autofix the active document, if relevant. 126 | // --- 127 | if( (doc=app.properties.activeDocument) && 'afterLinksChanged'==evType ) 128 | { 129 | callback({target:doc, eventType:evType}); 130 | } 131 | 132 | // Prevent alert on automatic startup. 133 | // --- 134 | if( 0 < MX_SAVE ) alert(name + " is turned ON.\r\r" + callback.help); 135 | 136 | evls.add(evType,callback,void 0,{name:name}); 137 | } 138 | 139 | })(EVENT_TYPE, $.engineName, onEvent); 140 | -------------------------------------------------------------------------------- /full/ReframeAllPages.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: ReframeAllPages [1.0] 4 | Desc: Reframe the pages of the active document w.r.t item bounds 5 | Path: /full/ReframeAllPages.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5 [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220429 (YYMMDD) 14 | Modified: 220429 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Considering the active document, this script scans the page items of each 25 | page, determines the overall bounding box and 'reframe' the page so it 26 | exactly fits the area. Page margins are reset to zero too. 27 | 28 | The script is undo-able and displays a short report. 29 | 30 | */ 31 | 32 | function reframePages( doc,CS_IN,MG,GB,errs,pages,pg,K,a,gb,t) 33 | //---------------------------------- 34 | // Reframe all pages w.r.t. page items bounds. 35 | { 36 | // Checkpoint. 37 | // --- 38 | if( !(doc=app.properties.activeDocument) ) 39 | { 40 | alert( "Open a document." ); 41 | return; 42 | } 43 | 44 | // Set up the document metrics. 45 | // --- 46 | app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS; 47 | doc.zeroPoint = [0,0]; 48 | doc.viewPreferences.rulerOrigin = RulerOrigin.PAGE_ORIGIN; 49 | 50 | // Misc. 51 | // --- 52 | CS_IN = +CoordinateSpaces.INNER_COORDINATES; 53 | MG = { top:0, left:0, bottom:0, right:0 }; 54 | GB = Array(4); 55 | errs = []; 56 | 57 | // Loop. 58 | // --- 59 | pages = doc.pages.everyItem().getElements().slice(); 60 | for each( pg in pages ) 61 | { 62 | K = pg.pageItems; 63 | if( !K.length ) continue; // Empty page -> noop 64 | 65 | // Calculate the overall [T,L,B,R] thru min/max. 66 | // --- 67 | GB[0]=GB[1]=1/0 ; GB[2]=GB[3]=-1/0; 68 | a = K.everyItem().geometricBounds.slice(); 69 | for each( gb in a ) 70 | { 71 | (t=gb[0]) < GB[0] && (GB[0]=t); 72 | (t=gb[1]) < GB[1] && (GB[1]=t); 73 | (t=gb[2]) > GB[2] && (GB[2]=t); 74 | (t=gb[3]) > GB[3] && (GB[3]=t); 75 | } 76 | 77 | // Reframe the page. 78 | // --- 79 | pg.marginPreferences.properties = MG; 80 | t = [ [GB[1],GB[0]], [GB[3],GB[2]] ]; 81 | try 82 | { 83 | pg.reframe(CS_IN, t); 84 | } 85 | catch(e) 86 | { 87 | errs.push("[" + pg.name + "] " + e); 88 | } 89 | } 90 | 91 | // Report. 92 | // --- 93 | if( errs.length ) 94 | { 95 | alert( "Couldn't reframe the following page(s):\r\r" 96 | + errs.slice(0,20).join('\r') ); 97 | } 98 | else 99 | { 100 | alert( pages.length + " pages were reframed." ); 101 | } 102 | }; 103 | 104 | app.doScript 105 | ( 106 | reframePages, 107 | void 0, 108 | void 0, 109 | +UndoModes.ENTIRE_SCRIPT, 110 | 'Reframe Pages', 111 | ); 112 | -------------------------------------------------------------------------------- /full/SelApplyStyleNone.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelApplyStyleNone (1.2) 4 | Desc: Reset the selection to the [None] character style. 5 | Path: /full/SelApplyStyleNone.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 100102 (YYMMDD) 14 | Modified: 221006 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Forcibly apply the [None] character style to the selected text or insertion point 25 | (if any), making sure that all specific character attributes are removed. 26 | 27 | This script has the same effect than clicking [None] in the Character Style panel 28 | [which unfortunately cannot be easily associated to a KB shortcut despite 29 | countless InDesign user requests.] 30 | 31 | So, you can attach a KB shortcut to this script and have a manual control of the 32 | Apply None action. All InDesign versions should be supported from CS4 to CC. 33 | Also, the script is undo-able (Cmd Z.) 34 | 35 | */ 36 | 37 | ;app.doScript 38 | ( 39 | function( doc,t,s) 40 | { 41 | (doc=app.properties.activeDocument) // Is there an active document? 42 | && (t=doc.properties.selection||0).length // Is there a selection? 43 | && (t=t[0]).isValid // Is this a valid DOM component? 44 | && (t.hasOwnProperty('endBaseline')) // Is this a Text instance? 45 | && (s=doc.characterStyles.itemByName('$ID/[None]')).isValid // Clean identification of the [None] style. 46 | && 47 | ( 48 | t.applyCharacterStyle(s), // Apply [None] (attributes remain as overrides) 49 | t.clearOverrides(OverrideType.CHARACTER_ONLY) // then remove character-scoped overrides. 50 | ); 51 | }, 52 | void 0, 53 | void 0, 54 | UndoModes.ENTIRE_SCRIPT, 55 | 'Apply None' 56 | ); 57 | -------------------------------------------------------------------------------- /full/SelPlacePDFPage.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelPlacePDFPage (1.0) 4 | Desc: Simple interface for changing placed PDF page. 5 | Path: /full/SelPlacePDFPage.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 230116 (YYMMDD) 14 | Modified: 230122 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Need to easily change the page of a placed PDF? 25 | Just select the graphic container and run this script. 26 | Source: https://adobe.ly/3HoxrZ2 27 | 28 | */ 29 | 30 | ;(function selPlacePDFPage( dt,doc,sel,t,ok,ff) 31 | //---------------------------------- 32 | // Simple interface for changing page in placed PDF. 33 | // (Just select the PDF container and run.) 34 | { 35 | dt = +new Date; 36 | try 37 | { 38 | doc = app.properties.activeDocument; 39 | if( !doc ) throw "No active document."; 40 | 41 | sel = doc.properties.selection; 42 | if( !(sel && sel.length) ) throw "No selection."; 43 | if( !(sel=sel[0]).isValid ) throw "Invalid selection."; 44 | 45 | for( t=sel ; 'PDF' != t.constructor.name ; t=t[0] ) 46 | { 47 | ok = t.hasOwnProperty('contentType') 48 | && ContentType.graphicType == t.properties.contentType 49 | && 1 == (t=t.properties.allGraphics||[]).length; 50 | if( !ok ) throw "The selection has no direct PDF content."; 51 | } 52 | 53 | ff = (t=t.properties.itemLink||0).isValid && (t.properties.filePath||0); 54 | if( !(ff && File(ff).exists) ) throw "PDF File is missing. You probably should relink."; 55 | 56 | // Runtime error if canceled by the user (hence the `dt` timer.) 57 | // --- 58 | sel.place(ff, true); 59 | 60 | alert( "PDF page sucessfully placed!" ); 61 | } 62 | catch( e ) 63 | { 64 | dt = +new Date - dt; // full delay, in ms. 65 | if( 500 > dt ) alert( e ); // Only display instant error. 66 | } 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /full/SelSetFrameHeightByLineCount.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelSetFrameHeightByLineCount (1.2) 4 | Desc: Change frame(s) height to match some line count. 5 | Path: /full/SelSetFrameHeightByLineCount.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6 [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: Test clinical cases (infinite loop?) 13 | Created: 250216 (YYMMDD) 14 | Modified: 250217 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | 1. DESCRIPTION 25 | ____________________________________________________________________________ 26 | 27 | Instead of resizing a text frame to a certain defined height, you may need 28 | to specify a *number of lines*, e.g. “All frames in the selection should 29 | have 25 lines available”, whatever the particular text, text size, leading, 30 | paragraph spacing, inset... they undergo.” 31 | 32 | By ‘available lines’, we mean the number of lines the text frame actually 33 | displays or would display if it were full, regardless of whether it is 34 | empty, partially filled, threaded or overloaded (overset text). In some 35 | ways, this number of available lines indirectly specifies a height. 36 | 37 | The present script offers you a tool to resize the height of one or more 38 | text frames in this way. A prompt box allows you to enter the “Desired 39 | Line Count” from 1 to 80. 40 | 41 | Changes are undo-able (Cmd Z). 42 | 43 | 2. LIMITATIONS & TECH NOTES 44 | ____________________________________________________________________________ 45 | 46 | - This program will SKIP *multicolumn* and/or *autosizing* text frames. 47 | 48 | - When a frame is resized, the transformation is processed w.r.t its 49 | top edge. You can change the AP_TOP anchor point to get a different 50 | behaviour (bottom or center origin.) 51 | 52 | - The maximum Line Count (80) can be easily increased or reduced. 53 | 54 | [REM] Idea taken from the discussion "How to resize a text frame to 55 | a given value" -> community.adobe.com/t5/indesign-discussions/ 56 | how-to-resize-a-text-frame-to-a-given-value/td-p/15155617 57 | 58 | */ 59 | 60 | ;(function setFrameHeightByLineCount( ask,NL,tf,dup,sto,mul,a,i,t,n,k,L) 61 | //-------------------------------------------------------------------- 62 | // Select one or more textframes (or have the insertion pt in a frame), 63 | // enter the desired line number when prompted. 64 | { 65 | const UID_ENV = 'SetFrameHeightByLineCount'; 66 | const LN_MIN = 1; 67 | const LN_MAX = 80; 68 | // --- 69 | const SZOF = +AutoSizingTypeEnum.OFF; 70 | const FITC = +FitOptions.FRAME_TO_CONTENT; 71 | // --- 72 | const CS_IN = +CoordinateSpaces.INNER_COORDINATES; 73 | const AP_TOP = +AnchorPoint.TOP_CENTER_ANCHOR; 74 | const RM_MUL = +ResizeMethods.MULTIPLYING_CURRENT_DIMENSIONS_BY; 75 | // --- 76 | const WARN = '\u26A0'; 77 | const __ = $.global.localize; 78 | 79 | a = app.properties.selection||0; 80 | if( !a ) 81 | { 82 | alert("No selection.") 83 | return; 84 | } 85 | 86 | // Inner text -> parent frame. 87 | 1==a.length 88 | && (t=a[0]).hasOwnProperty('horizontalOffset') 89 | && (a=t.parentTextFrames||[]); 90 | 91 | // Preparatory loop: compute mult factors. 92 | // --- 93 | ask = __("Desired Line Count?\r(%1 to %2)", LN_MIN, LN_MAX); 94 | for( NL=1/0, i=a.length ; i-- ; 1==mult ? a.splice(i,1) : a[i]=[a[i],mult] ) 95 | { 96 | tf = a[i]; 97 | mult = 1; 98 | 99 | if( !(tf||0).isValid ) continue; 100 | if( tf.constructor.name.slice(-9) != 'TextFrame' ) continue; 101 | t = tf.textFramePreferences.properties; 102 | if( +t.autoSizingType != SZOF ) continue; 103 | if( t.textColumnCount != 1 ) continue; 104 | 105 | while( ask ) 106 | { 107 | // [REM] Values managed through $.setenv/getenv are 108 | // session persistent, even in the 'main' engine. 109 | t = $.getenv(UID_ENV) || String(tf.lines.length); 110 | NL = prompt(ask, t, "Set Height by Line Count"); 111 | 112 | if( null===NL ) return; 113 | NL = parseInt(NL, 10); 114 | 115 | isFinite(NL) && NL >= LN_MIN && NL <= LN_MAX 116 | ? ( ask=false, $.setenv(UID_ENV,String(NL)) ) 117 | : ( -1==ask.indexOf(WARN) && ask=ask.split('\r').join('\r'+WARN+' ') ); 118 | } 119 | 120 | (dup = tf.duplicate()).properties = { ignoreWrap:true }; 121 | 122 | // Make sure we have enough 'potential' lines in the story. 123 | sto = dup.parentStory; 124 | t = Array(2+NL).join('\r.'); 125 | do{ sto.texts[0].contents += t; } while( !sto.overflows ); 126 | 127 | for 128 | ( 129 | t=-1, L=dup.lines ; 130 | t != (n=L.length) && NL != n ; 131 | dup.resize( CS_IN, AP_TOP, RM_MUL, [1,k=(NL/(t=n))] ), 132 | mult *= k 133 | ); 134 | 135 | dup.remove(); 136 | } 137 | 138 | if( !a.length ) 139 | { 140 | alert( "No change done." ); 141 | return; 142 | } 143 | 144 | // Makes hot process undoable. 145 | // --- 146 | app.doScript 147 | ( 148 | function(){ while(t=a.pop())(tf=t[0]).isValid&&tf.resize(CS_IN,AP_TOP,RM_MUL,[1,t[1]]) }, 149 | void 0, void 0, UndoModes.ENTIRE_SCRIPT, 'Set Frame Height' 150 | ); 151 | 152 | })(); 153 | 154 | -------------------------------------------------------------------------------- /full/SelTransformDimensions.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelTransformDimensions (1.1) 4 | Desc: Displays the dimensions of the selection as in the Transform panel. 5 | Path: /full/SelTransformDimensions.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 240108 (YYMMDD) 14 | Modified: 240109 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Given a *single* selected object (frame, group, image, etc), the Transform 25 | panel displays its Width (W) and Height (H) with respect to its current 26 | transform state, using user's ruler units and taking into account the pre- 27 | ference `Dimensions Include Stroke Weight`. 28 | 29 | Those W and H attributes are not available in the DOM but can be computed. 30 | The present script determines and shows them. 31 | 32 | [REM] Idea taken from the discussion "Transform properties width" 33 | -> community.adobe.com/t5/indesign-discussions/transform-properties-width/td-p/14333981 34 | 35 | [REF] Coordinate Spaces & Trasnformations in InDesign 36 | -> indiscripts.com/tag/CST 37 | 38 | */ 39 | 40 | ;(function selTransformDimensions( sel,VP,BBL,CS_IN,CS_PB,tl,br,mx,rw,rh,wu,hu,a) 41 | //---------------------------------- 42 | // Display the dimensions of the selection (W,H) as shown in the Transform panel. 43 | // (If multiple objects are selected, the 1st one is considered.) 44 | // [FIX240109] Now supporting Shear X angle. 45 | { 46 | const PRECISION = 1e4; 47 | 48 | sel = (app.selection||0)[0]||0; 49 | if( !sel.hasOwnProperty('resolve') ) return; 50 | 51 | VP = app.properties.activeDocument.viewPreferences; 52 | 53 | // Boring enums. 54 | BBL = +BoundingBoxLimits 55 | [ 56 | app.transformPreferences.dimensionsIncludeStrokeWeight 57 | ? 'OUTER_STROKE_BOUNDS' 58 | : 'GEOMETRIC_PATH_BOUNDS' 59 | ]; 60 | CS_IN = +CoordinateSpaces.innerCoordinates; 61 | CS_PB = +CoordinateSpaces.pasteboardCoordinates; 62 | 63 | // Inner bounding box corners -> inner dims (in pt) 64 | tl = sel.resolve([ [0,0], BBL, CS_IN ], CS_IN)[0]; 65 | br = sel.resolve([ [1,1], BBL, CS_IN ], CS_IN)[0]; 66 | w = br[0] - tl[0]; 67 | h = br[1] - tl[1]; 68 | 69 | // Apply the scale factors (relative to PB). 70 | mx = sel.transformValuesOf(CS_PB)[0]; 71 | w *= mx.horizontalScaleFactor; 72 | h *= mx.verticalScaleFactor; 73 | 74 | // [FIX240109] Apply the shear X angle (relative to PB). 75 | (a=mx.clockwiseShearAngle) && (h/=Math.cos(a*Math.PI/180)); 76 | 77 | // Use horiz./vert. ruler units (instead of pt) 78 | tl = sel.resolve( [[0,0],0], CS_PB, true)[0]; 79 | br = sel.resolve( [[1,1],0], CS_PB, true)[0]; 80 | rw = br[0]-tl[0]; 81 | rh = br[1]-tl[1]; 82 | 83 | // Final message. 84 | wu = 'W: ' + Math.round((PRECISION*w)/rw)/PRECISION 85 | + ' ' + VP.horizontalMeasurementUnits.toString(); 86 | hu = 'H: ' + Math.round((PRECISION*h)/rh)/PRECISION 87 | + ' ' + VP.verticalMeasurementUnits.toString(); 88 | alert( [wu,hu].join('\r') ); 89 | 90 | })(); 91 | -------------------------------------------------------------------------------- /full/StyleListGotoSample.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: StyleListGotoSample (1.0) 4 | Desc: Add a "Goto Sample" menu item to the paragraph/character style lists 5 | Path: /full/StyleListGotoSample.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5 [Mac/Win] 8 | L10N: --- 9 | Kind: Startup Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 230321 (YYMMDD) 14 | Modified: 230321 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This startup script creates an extra item "Goto Sample" in 25 | the right-click menu of the Paragraph/Character Styles panel. 26 | When the action is invoked, a text sample having the desired 27 | style is selected and shown in the active document. 28 | 29 | INSTALLATION. - Go into the `Scripts` folder of your InDesign 30 | install (i.e. right-click the User item from the Scripts 31 | panel and click Reveal in...). If not already here, create 32 | a `startup scripts` folder and put the present file 33 | `StyleListGotoSample.jsx` in that folder. Restart InDesign. 34 | 35 | If you no longer use the "Goto Sample" feature, quit InDesign 36 | and simply remove the file `StyleListGotoSample.jsx` from the 37 | `startup scripts` folder. 38 | 39 | */ 40 | 41 | const MENU_KEY = "$ID/RtMenuStyleListItem"; 42 | 43 | const ACTION_NAME = (function(/*uint*/loc, i) 44 | { 45 | i = 1*(loc==+Locale.FRENCH_LOCALE) || 46 | 2*(loc==+Locale.GERMAN_LOCALE) || 47 | 3*(loc==+Locale.SPANISH_LOCALE); 48 | return [ "Goto Sample", "Voir \xE9chantillon", "Gehe zu Beispiel", "Ir a ejemplo" ][i]; 49 | })(+app.locale); 50 | 51 | const PTN = app.translateKeyString('$ID/Edit "^1"...').split('^1'); 52 | 53 | (function(/*any*/ev, ff,pp,MA,sma,MI,mni,doc,sty,FTP,a,t,tt,i,z,s) 54 | { 55 | MI = (t=app.menus.itemByName(MENU_KEY)).isValid && t.menuItems; 56 | if( !MI.length ) return; 57 | 58 | // Event manager. 59 | // --- 60 | if( (ev||0).isValid ) 61 | { 62 | if( !(doc=app.properties.activeDocument||0).isValid ) return; 63 | 64 | // Get the style name under consideration. 65 | // --- 66 | a = MI.everyItem().name; 67 | z = a.length; 68 | for 69 | ( 70 | t=PTN[0], tt=PTN[1], i=-1 ; 71 | ++i < z && !( 0===a[i].indexOf(t) && tt===a[i].slice(-tt.length) ) ; 72 | ); 73 | if( i >= z ) return; 74 | 75 | s = a[i].slice(t.length,-tt.length); 76 | if( !s.length ) return; 77 | 78 | // Identify the style. 79 | // [REM] So far I don't know how to decide which style panel is used, 80 | // therefore if the *same* style name exists in both Para & Char styles 81 | // we cannot be sure. At best, panel visibility may allow us to restrict 82 | // the search area. 83 | // --- 84 | a = []; 85 | (t=app.panels.itemByName("$ID/Character Styles")).isValid && t.visible && (a=doc.allCharacterStyles); 86 | (t=app.panels.itemByName("$ID/Paragraph Styles")).isValid && t.visible && (a.push.apply(a,doc.allParagraphStyles)); 87 | a.length || (a=doc.allCharacterStyles.concat(doc.allParagraphStyles)); // Fallback 88 | 89 | for( i=a.length ; i-- && a[i].name !== s ; ); 90 | sty = 0 <= i && a[i]; 91 | if( !sty ) return; 92 | 93 | // Find it. 94 | // --- 95 | FTP = app.findTextPreferences; 96 | bkp = FTP.properties; 97 | app.findTextPreferences = null; 98 | FTP['applied'+sty.constructor.name] = sty; 99 | a=doc.findText(); 100 | FTP.properties = bkp; 101 | 102 | if( a.length ) 103 | { 104 | // [REM] Text.showText() is available since CS5. 105 | // --- 106 | try{ a[0].showText(); }catch(_){} 107 | } 108 | else 109 | { 110 | alert( "No result." ); 111 | } 112 | 113 | return; 114 | } 115 | 116 | // Installer. 117 | // --- 118 | if( (ff=new File($.fileName)).exists ) 119 | { 120 | // 1. Check or create the script menu action (session-persistent). 121 | // --- 122 | MA = app.scriptMenuActions; 123 | sma = false; 124 | pp = 125 | { 126 | title: ACTION_NAME, 127 | name: ACTION_NAME, 128 | label: ff.absoluteURI, 129 | }; 130 | for 131 | ( 132 | a = (t=MA.itemByName(pp.title)).isValid ? t.getElements() : [] ; 133 | 134 | t=a.pop() ; 135 | 136 | false===sma && t.properties.label===pp.label 137 | // Already available? 138 | ? ( sma=t ) 139 | // Remove any undesired duplicate. 140 | : ( (tt=t.eventListeners).length && tt.everyItem().remove(), t.remove() ) 141 | ); 142 | 143 | // Need to create the menu action? 144 | // --- 145 | false===sma && (sma=MA.add(pp.title)); 146 | 147 | // Always assign valid properties. 148 | // --- 149 | sma.properties = pp; 150 | 151 | // Re-attach event listeners if necessary. 152 | // --- 153 | i = (t=sma.eventListeners).length; 154 | if( 1 != i || ( i && t[0].isValid && t[0].handler.absoluteURI !=ff.absoluteURI ) ) 155 | { 156 | if( 0 < i ){ t.everyItem().remove() } 157 | sma.addEventListener('onInvoke', ff); 158 | } 159 | 160 | // 2. Check or create the contextual menu item (app-persistent). 161 | // --- 162 | mni = false; 163 | for 164 | ( 165 | a = (t=MI.itemByName(pp.title)).isValid ? t.getElements() : [] ; 166 | t=a.pop() ; 167 | // Remove any undesired duplicate. 168 | t.associatedMenuAction===sma && (mni ? t.remove() : (mni=t)) 169 | ); 170 | 171 | // Need to create the menu item? 172 | // Try..catch block in case `MI.add()` wouldn't work for some reason. 173 | // --- 174 | try{ mni||(mni=MI.add(sma, LocationOptions.AT_END)) }catch(_){} 175 | } 176 | 177 | })($.global.evt); 178 | 179 | -------------------------------------------------------------------------------- /full/TypeActuallyShowHidden.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: TypeActuallyShowHidden [2.0] 4 | Desc: Makes 'Show Hidden Characters' always work! 5 | Path: /full/TypeActuallyShowHidden.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC/CS6/CS5/CS4 [Mac/Win] 8 | L10N: --- 9 | Kind: Startup script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 250213 (YYMMDD) 14 | Modified: 250215 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | 1. DESCRIPTION 25 | ____________________________________________________________________________ 26 | 27 | This startup script changes the behaviour of Type > Show Hidden Characters 28 | so that Screen Mode is automatically changed into 'Normal' (aka preview off) 29 | if necessary. 30 | 31 | A SMART_MODE option (0 or 1) is available (see 'YOUR SETTINGS'): 32 | 33 | - SMART_MODE = 0 34 | ---------------------------------------------------------------- 35 | With SMART_MODE turned off, the regular Show/Hide switch works 36 | as usual. The script only disables preview effects if you ex- 37 | pressly go to 'Show Hidden Characters'. Hence, if the special 38 | characters are already in 'visible' state according to InDesign, 39 | i.e. the menu command is labelled 'Hide Hidden Characters', then 40 | the script does not interfere with that command and lets the 41 | toggle operate without extra steps regarding screen modes. 42 | 43 | - SMART_MODE = 1 44 | ---------------------------------------------------------------- 45 | With SMART_MODE turned on, the command 'Show Hidden Characters' 46 | works as already specified, but the command 'Hide Hidden Cha- 47 | racters' gets a special behaviour: it does not let the toggle 48 | operate if the active window is in some preview mode (meaning 49 | that hidden characters aren't actually noticeable to the user.) 50 | In that particular case, special characters are kept 'visible' 51 | (despite the 'Hide...' command) and preview modes are disabled. 52 | 53 | WARNING. Enabling SMART_MODE fulfills what the user intuitively expects from 54 | the Cmd Alt I shortcut. Special characters are shown when you don't see them, 55 | and hidden when you already see them. However, be aware that this special 56 | mode alters the semantics of the 'Hide Hidden Characters' menu item in that 57 | the command won't be honored in the case we've described above. 58 | 59 | [REM] No `#targetengine` directive is used in this script as we don't really 60 | need to store persistent data for such a basic process. Instead, the script 61 | file itself is registered as "the event handler" and we use `$.global.evt` 62 | to determine whether the script is invoked for either installing the listener 63 | or managing the event. 64 | 65 | 66 | 2. INSTALL / UNINSTALL 67 | ____________________________________________________________________________ 68 | 69 | - To install the listener, drop it (or an alias) in the 70 | Scripts/[startup scripts] folder. 71 | 72 | - To uninstall the listener, remove it (or its alias) from the 73 | Scripts/[startup scripts] folder. 74 | 75 | NOTE. If you don't know where is the parent [Scripts] folder, run InDesign 76 | and show the Scripts panel (Window > Utility > Scripts). Then right-click 77 | the [User] branch and click “Reveal in Finder/Explorer”. 78 | 79 | 80 | 3. VERSION HISTORY 81 | ____________________________________________________________________________ 82 | 83 | [FIX250215] Version 2.0 fix various bugs regarding the event listener. 84 | It also activates SMART_MODE by default (you can change 85 | that behavior in YOUR SETTINGS below.) 86 | Added CS4 compatibility. 87 | 88 | [FIX250214] Deals with overprint preview too [thx Branislav]. 89 | 90 | More info: 91 | → indiscripts.com/post/2025/02/finally-fixing-show-hidden-characters-menu-action 92 | 93 | */ 94 | 95 | // YOUR SETTINGS (see DESCRIPTION) 96 | //---------------------------------- 97 | SMART_MODE = 1; 98 | 99 | 100 | (function(/*any*/ev, doc,win,shw,off,pvw,ff,mna,evs,t,a) 101 | //---------------------------------- 102 | { 103 | if( (ev||0).isValid && 'afterInvoke'==ev.eventType ) 104 | { 105 | // Checkpoints. 106 | if( !(doc=app.properties.activeDocument||0).isValid ) return; 107 | if( !(win=app.properties.activeWindow ||0).isValid ) return; 108 | if( !('LayoutWindow'==win.constructor.name && doc===win.parent) ) return; 109 | 110 | // ShowHidden state (shw) 111 | shw = doc.textPreferences.showInvisibles; 112 | if( !shw && !$.global.SMART_MODE ) return; 113 | 114 | // Preview state (pvw) 115 | off = +ScreenModeOptions.PREVIEW_OFF; 116 | t = win.properties; 117 | pvw = t.overprintPreview || (+t.screenMode != off); 118 | if( !pvw ) return; 119 | 120 | // Hot process. 121 | win.properties = 122 | { 123 | screenMode: off, // Go back to Normal screen mode. 124 | overprintPreview: false, // Deactivate overprint preview. 125 | }; 126 | 127 | !shw && (doc.textPreferences.showInvisibles=true); // Smart mode! 128 | 129 | // [REM250215] It is crucial that switching back to 130 | // `showInvisibles=true` (against the command being 131 | // processed!) does not trigger a new 'afterInvoke' 132 | // MenuAction event. 133 | } 134 | else 135 | { 136 | // This block will install the event listener 137 | // iff it is not installed yet! 138 | 139 | const UID = 'TypeActuallyShowHidden'; 140 | const ID_SHOW = '$ID/Show Hidden Characters'; 141 | const ID_HIDE = '$ID/Hide Hidden Characters'; 142 | 143 | if( !(ff=new File($.fileName)).exists ) return; 144 | 145 | // [FIX250215] Get the MenuAction from its *current* name. 146 | shw = app.textPreferences.showInvisibles; 147 | mna = app.menuActions.itemByName(shw?ID_HIDE:ID_SHOW); 148 | if( !mna.isValid ) 149 | { 150 | // This shouldn't happen, but let's try the other way. 151 | mna = app.menuActions.itemByName(shw?ID_SHOW:ID_HIDE); 152 | if( !mna.isValid ) return; 153 | } 154 | 155 | // [REM250215] Whatever its *dynamic* name, the MenuAction 156 | // remains the same: ID=0x1D301 in all tested ID versions. 157 | 158 | mna = mna.getElements()[0]; // Not needed but let's be careful. 159 | 160 | // [FIX250215] In CS4, an EventListener has no 'name' prop 161 | // although EventListeners.itemByName() is available :-/ 162 | // Let's find a trick that works anywhere. Our purpose here 163 | // is to prevent the user from duplicating the listener by 164 | // manually running the JSX (again). 165 | evs = mna.eventListeners; 166 | if( evs.length ) 167 | for( a=evs.everyItem().properties ; t=a.pop() ; ) 168 | { 169 | if( t.name===UID ) return; // Found by name (> CS4) 170 | 171 | t = t.handler; 172 | if( t && t.absoluteURI==ff.absoluteURI ) return; // Found by URI (CS4) 173 | } 174 | 175 | // Hot process. 176 | evs.add('afterInvoke',ff).properties = { name:UID }; 177 | 178 | // [REM250215] Assigning a name thru properties is 179 | // harmless (although ineffective) in CS4. 180 | } 181 | 182 | })($.global.evt); 183 | -------------------------------------------------------------------------------- /snip/DocBackupSave.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: DocBackupSave [1.0] 4 | Desc: Backup before save. 5 | Path: /snip/DocBackupSave.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: backupSave() 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 200518 (YYMMDD) 14 | Modified: 200519 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Based on an original idea from @billgreenwood, this function creates a 25 | backup of the target document before it saves it, so the consecutive 26 | versions are memorized, timestamped, and remain safe in a subfolder. 27 | 28 | The first time you call the function on a fresh, not-saved document, 29 | it simply invokes the native `save` method. Then, any subsequent call 30 | copies the existing document file in a backup folder before saving the 31 | new version. Each backup file is named as follows, 32 | 33 | --
__ 34 | 35 | where the timestamp `--
_` reflects the 36 | modified date/time of the document version. 37 | 38 | Using `backupSave(myDoc)` rather than `myDoc.save()` may protect you 39 | against doomsday scenarios where a huge, in-progress document gets 40 | suddenly corrupted and cannot be recovered from the regular auto- 41 | recovery feature. (Besides that, having consistent backups is still 42 | useful for 'historizing' your projects...) 43 | 44 | Tip: Assign the `backupSave();` script a kb shortcut, e.g Cmd+Alt+S, 45 | going into Edit > Keyboard Shortcuts... 46 | 47 | Sample codes: 48 | 49 | // 1. Backup-save the active document in a '/backup' subfolder. 50 | backupSave(); 51 | 52 | // 2. Backup-save a document, using some existing backup folder. 53 | backupSave(myDocument, myFolder); 54 | 55 | */ 56 | 57 | ;const backupSave = function backupSave(/*Document=active*/doc,/*Folder|str=backup*/bkp, pp,ff,t) 58 | //---------------------------------- 59 | // `doc` :: [opt.] A Document instance. By default, take the active document. 60 | // `bkp` :: [opt.] Backup folder, passed either as a string (subpath relative to 61 | // the document folder) or a full Folder instance. By default, a 62 | // subfolder 'backup' is used. If you supply an empty string (bkp==='') 63 | // then the backup files will be saved in the document folder. 64 | // --- 65 | // => undef 66 | { 67 | doc || (doc=app.properties.activeDocument); 68 | if( (!doc) || !doc.isValid || !(doc instanceof Document) ) return; 69 | 70 | // Normalize `bkp` -> string | Folder 71 | // --- 72 | 'string' == typeof bkp 73 | || (bkp && (bkp instanceof Folder)) 74 | || (bkp='backup'); 75 | 76 | pp = doc.properties; 77 | if( pp.saved && pp.modified && (ff=File(pp.fullName)).exists ) 78 | { 79 | // Format the date/timestamp. 80 | // --- 81 | t = ff.modified || new Date; 82 | t = $.global.localize("%1-%2-%3_%4%5%6_" 83 | , t.getFullYear() 84 | , ('0'+(1+t.getMonth())).slice(-2) 85 | , ('0'+t.getDate()).slice(-2) 86 | , ('0'+t.getHours()).slice(-2) 87 | , ('0'+t.getMinutes()).slice(-2) 88 | , ('0'+t.getSeconds()).slice(-2) 89 | ); 90 | 91 | // Backup folder. (Fallback -> document folder.) 92 | // --- 93 | bkp instanceof Folder || (bkp=Folder(ff.parent+'/'+bkp)); 94 | bkp.exists || bkp.create() || (bkp=ff.parent); 95 | 96 | // Backup file. 97 | // --- 98 | bkp = File(bkp + '/' + t + pp.name); 99 | if( !ff.copy(bkp) ) alert( "Unable to backup the document." ); 100 | } 101 | 102 | // Regular save ; try..catch used to deal with Cancel, etc 103 | // --- 104 | try{ doc.save() }catch(_){} 105 | }; 106 | -------------------------------------------------------------------------------- /snip/DocGrepTextFindAll.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: DocGrepTextFindAll [1.1] 4 | Desc: Show all Grep/Text found items in a temp file. 5 | Path: /snip/DocGrepTextFindAll.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: showFindAll() 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 200828 (YYMMDD) 14 | Modified: 200904 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Based on a suggestion from @leo_narre, this function lists all text items 25 | found by the active GREP query (or TEXT query if no GREP available.) 26 | Results are saved in a UTF8-encoded file (typically `findGrep.txt`) then 27 | shown to the user. The function itself returns true if all went fine. 28 | 29 | Tip: Assign the `showFindAll();` script a kb shortcut, e.g Cmd+Alt+F, 30 | going into Edit > Keyboard Shortcuts... 31 | 32 | Sample codes: 33 | 34 | // 1. Show items found in the active document. 35 | showFindAll(); 36 | 37 | // 2. Supplying a particular document and the output file. 38 | showFindAll( myDocument, File("path/to/results.txt") ); 39 | 40 | [REF] 41 | Discussion: twitter.com/leo_narre/status/1299277914744201216 42 | Draft: gist.github.com/indiscripts/7f6669c291451cc833041afb38fd4a0c 43 | 44 | */ 45 | 46 | ;const showFindAll = function showFindAll(/*Document=active*/doc,/*File=auto*/ff, q,k,w,a,i) 47 | //---------------------------------- 48 | // Run the current findGrep (or findText) query on a document, 49 | // and show the results (found text contents) in a `txt` file. 50 | // [REM] GREP search is processed by default, as long as a GREP query is available; 51 | // TEXT search is processed otherwise. If no query is available at all, or if 52 | // anything goes wrong, the function prompts an error message and returns false. 53 | // --- 54 | // `doc` :: [opt.] A Document instance. By default, take the active document. 55 | // `ff` :: [opt.] Output File. By default, create a txt file in the temp folder. 56 | // --- 57 | // => true [OK] | false [KO] 58 | { 59 | doc || (doc=app.properties.activeDocument); 60 | if( (!doc) || !doc.isValid || !(doc instanceof Document) ){ alert("No document."); return false; } 61 | 62 | // Is there a findGrep or findText query? 63 | // k :: 'findGrep' | 'findText' | 0 64 | // --- 65 | ('string'==typeof(q=app[(k='findGrep')+'Preferences'].findWhat) && q.length) 66 | || ('string'==typeof(q=app[(k='findText')+'Preferences'].findWhat) && q.length) 67 | || (k=0); 68 | if( !k ){ alert("Neither findGrep nor findText query is defined."); return false; } 69 | 70 | // Prepare the output file. 71 | // --- 72 | (ff && (ff instanceof File)) || (ff=File(Folder.temp + '/' + k + '.txt')); 73 | const NL = 'W'==ff.lineFeed[0] ? '\r\n' : '\n'; 74 | 75 | // Basic running bar. (Because DOM processing can be time consuming.) 76 | // --- 77 | w = new Window('palette', k.charAt(0).toUpperCase()+k.slice(1)); 78 | w.margins = 25; 79 | (w.add('staticText',void 0,"Running " + k + "...")).preferredSize=[250,22]; 80 | w.show(); 81 | w.update(); 82 | 83 | // Run the find query at the document level. 84 | // --- 85 | try{ a=0; a=doc[k]()||0; } catch(_){} 86 | if( !(i=a.length) ){ w.hide(); alert("No result."); return false; } 87 | 88 | // Grab text contents. 89 | // --- 90 | w.children[0].text = "Processing " + i + " found items..."; 91 | for( ; i-- ; a[i]=a[i].contents ); 92 | 93 | // Results -> file. 94 | // --- 95 | w.children[0].text = "Writing the results in a file..."; 96 | if( ff.open('w') ) 97 | { 98 | ff.encoding='UTF8'; 99 | q = $.global.localize("# %1: `%2` (%3 result%4.)" 100 | , k 101 | , q 102 | , a.length 103 | , 1 < a.length ? 's' : '' 104 | ); 105 | ff.write(q + NL+NL + a.join(NL)); 106 | ff.close(); 107 | } 108 | else 109 | { 110 | w.hide(); 111 | alert("Cannot open/create the file " + ff); 112 | return false; 113 | } 114 | 115 | // Show the results. 116 | // --- 117 | w.hide(); 118 | ff.execute(); 119 | return true; 120 | }; 121 | -------------------------------------------------------------------------------- /snip/FileParseCSV.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: FileParseCSV [draft] 4 | Desc: Parse a CSV file or stream. 5 | Path: /snip/FileParseCSV.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: ExtendScript (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: --- 11 | DOM-access: NO 12 | Todo: --- 13 | Created: 230519 (YYMMDD) 14 | Modified: 230519 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | A simple CSV parser. Supports comma-separated data of various forms including 25 | items enclosed in double quotes, e.g "xxx, yyy". Escape sequences like `""`, 26 | `\"`, etc, are parsed as well. 27 | 28 | Example with three columns: 29 | 30 | itemA1,itemB1,itemC1 31 | itemA2,,itemC2 32 | "itemA3,etc","item""B3","item\"C3" 33 | "item,A4","item\,B4",itemC4 34 | 35 | The function `parseCSV` takes a File (or the full CSV content as a string) 36 | and returns an array of rows. By default, each row is an array of strings, 37 | e.g `["itemA1", "itemB1", "itemC1"]`. Set the `header` argument to TRUE to 38 | get every row parsed as an object (based on the labels of the first row). 39 | 40 | Sample code: 41 | 42 | var uri = "C:/Documents/test.csv"; 43 | var data = parseCSV( new File(uri) ); 44 | alert( data.join('\r') ); 45 | 46 | [REF] community.adobe.com/t5/indesign/ 47 | what-s-the-least-inelegant-way-to-ingest-csv-w-js/td-p/13799042 48 | 49 | */ 50 | 51 | ;function parseCSV(/*str|File*/input,/*bool=0*/header, r,i,s,a,m,n,o,j) 52 | //---------------------------------- 53 | // Pass in a string or a `File(path/to/file.csv)` argument. 54 | // If `header` is truthy, parses the 1st line as providing field names 55 | // and returns an array of objects {:, :, ...} 56 | // Otherwise, returns an array of arrays. If no data can be found, 57 | // returns an empty array. 58 | // => str[][] | obj[] 59 | { 60 | // Input. 61 | if( !input ) return []; 62 | if( input instanceof File ) 63 | { 64 | if( !(input.exists && input.length) ) return []; 65 | input = input.open('r','UTF8') && [input.read(),input.close()][0]; 66 | } 67 | if( !('string' == typeof input && input.length) ) return []; 68 | 69 | // Get lines. 70 | r = input.split(/(?:\r\n|\r|\n)/g); 71 | 72 | // Get fields. 73 | const reFld = /(,|^)(?:"((?:\\.|""|[^\\"])*)"|([^,"]*))/g; // $1 :: `,`|undef ; $2 :: ``|undef ; $3 :: ``|undef 74 | const reEsc = /[\\"](.)/g; // $1 :: `` 75 | for( i=r.length ; i-- ; a.length ? (r[i]=a) : r.splice(i,1) ) 76 | { 77 | s = r[i]; 78 | if( -1 == s.indexOf('"') ) 79 | { 80 | a = s.length ? s.split(',') : []; 81 | continue; 82 | } 83 | 84 | for 85 | ( 86 | a = 0x2C==s.charCodeAt(0) ? [""] : [] ; 87 | m=reFld.exec(s) ; 88 | a[a.length] = 'undefined' != typeof m[2] ? m[2].replace(reEsc,'$1') : m[3] 89 | ); 90 | } 91 | if( !header ) return r; 92 | 93 | // Header -> convert rows to objects. 94 | m = r.shift(); 95 | n = m.length; 96 | for( i=-1 ; ++i < r.length ; r[i]=o ) 97 | for( o={}, a=r[i], j=-1 ; ++j < n ; o[m[j]]=a[j]||'' ); 98 | return r; 99 | } 100 | -------------------------------------------------------------------------------- /snip/FontGlyphCount.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: FontGlyphCount [1.0] 4 | Desc: Fast Glyph Counter for InDesign Font 5 | Path: /snip/FontGlyphCount.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Method (extends Font.prototype) 10 | API: Font.prototype.glyphCount() 11 | DOM-access: Font 12 | Todo: --- 13 | Created: 200421 (YYMMDD) 14 | Modified: 200422 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This snippet adds a `glyphCount` method to Font instances. You can use it 25 | in scripts that need to know how many glyphs are available in a font, in 26 | particular those which handle GID. (Plural specifier are supported.) 27 | 28 | Sample codes: 29 | 30 | // 1. Get the max glyphID in a font. 31 | var n = myText.appliedFont.glyphCount(); 32 | alert( "Highest GlyphID: " + (n-1) ); 33 | 34 | // 2. Running on a plural spec. 35 | // --- 36 | alert( app.fonts.itemByRange(200,300).glyphCount() ); 37 | 38 | */ 39 | 40 | ;Font.prototype.glyphCount = function glyphCount( a,B,K,i,n,t,p,x,s) 41 | //---------------------------------- 42 | // Return the number of glyphs contained in this Font. If the 43 | // underlying file cannot be parsed, return 0. This method 44 | // support plural specifiers: in such case an array is returned, 45 | // possibly with zero value(s) whenever the process fails. 46 | // Each returned count is a uint16: 0 <= N <= 65535. (If N!=0, 47 | // the highest GID is very likely N-1.) 48 | // --- 49 | // => uint | uint[] | 0 50 | { 51 | if( !this.isValid ) return 0; 52 | 53 | // Plural specifier support. 54 | // --- 55 | a = 1 < (a=this.getElements()).length 56 | ? this.location 57 | : [a[0].properties.location]; 58 | 59 | if( !(i=a.length) ) return 0; 60 | for( B=0xFF, K='charCodeAt' ; i-- ; a[i]=n ) 61 | { 62 | // Get the font file as a binary stream (str). 63 | // --- 64 | n = 'string' == typeof(t=a[i]) && (t=File(t)).exists 65 | && (t.encoding='BINARY') && t.open('r') 66 | && (t=[t.read(),t.close()][0]).length; 67 | if( !(n>>>=0) ) continue; 68 | 69 | // Raw parser: locate the `maxp` table and read numGlyph. 70 | // --- 71 | for( p=-1 ; 0 <= (p=t.indexOf('maxp',1+p)) ; ) 72 | { 73 | x = ((B&t[K](8+p))<<24) | ((B&t[K](9+p))<<16) 74 | | ((B&t[K](10+p))<<8) | (B&t[K](11+p)); 75 | if( 6+(x>>>=0) > n ){ p=-1; break; } 76 | 77 | s = t.slice(x,4+x); 78 | if( '\0\0\x50\0' != s && '\0\x01\0\0' !=s ) continue; 79 | n = ((B&t[K](4+x))<<8) | (B&t[K](5+x)); 80 | break; 81 | } 82 | 0 > p && (n=0); 83 | } 84 | 85 | return 1 < a.length ? a : a[0]; 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /snip/GroupAddItems.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: GroupAddItems [1.0] 4 | Desc: Add one or several PageItems to a Group. 5 | Path: /snip/GroupAddItems.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Method (extends Group.prototype) 10 | API: Group.prototype.addItems() 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 200513 (YYMMDD) 14 | Modified: 200513 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This snippet provides a `addItems` method to any singular Group instance. 25 | 26 | Such feature is not available in InDesign's scripting API and it is 27 | hard to implement in a way that does not temporarily destroy the target 28 | group. The present solution relies on a brilliant idea proposed by Uwe 29 | Laubender -- originally discussed in the InDesign Scripting Forum. 30 | 31 | The trick is to create a temporary MultiStateObject (MSO) which can 32 | then handle the target group into a State. New items are added to that 33 | state. When the state is finally released, the original group is restored 34 | with its new components. 35 | 36 | Issues may still arise in edge cases. The present code should deal with 37 | the target group G being part of an AnchoredObject (AO), or inside a 38 | 'nested item' (i.e pasted into.) However, no solution has been found in 39 | the particular case of G being *directly* nested in a SplineItem. 40 | 41 | Sample codes: 42 | 43 | // 1. Add an Oval to the target Group. 44 | myGroup.addItems(myOval); 45 | 46 | // 2. Add multiples page items. 47 | // --- 48 | myGroup.addItems([myItem1, myItem2...]); 49 | 50 | */ 51 | 52 | ;Group.prototype.addItems = function addItems(/*PageItem|PageItem[]*/items, gp,gpn,ao,t,p,mso,sta) 53 | //---------------------------------- 54 | // Add the incoming item(s) to this Group instance. 55 | // [REM] If `this` is a plural specifier, the 1st 56 | // element is considered as the target. 57 | // --- 58 | // => Group [OK] | false [KO] 59 | { 60 | // Checkpoint and target Group -> gp. 61 | // --- 62 | if( !this.isValid ) return false; 63 | gp = this.getElements()[0]; 64 | 65 | // Coerce items into an Array (if needed.) 66 | // [REM] `items` can be a plural specifier too :-) 67 | // --- 68 | (items instanceof Array) || (items=items.getElements()); 69 | if( !items.length ) return false; 70 | 71 | // Backup the name of the Group. 72 | // --- 73 | gpn = gp.properties.name; 74 | 75 | // If necessary, temporarily release the containing AO. 76 | // --- 77 | if( ao=callee.UPAO(gp) ) 78 | { 79 | t = ao.anchoredObjectSettings; 80 | ao = { 81 | obj: ao, 82 | ancPos: t.anchoredPosition, 83 | ipOffset: ao.parent.insertionPoints[0].getElements()[0], 84 | }; 85 | t.releaseAnchoredObject(); 86 | } 87 | 88 | // --- 89 | // Possible parents of a Group: 90 | // Snippet | PlaceGun | ComboBox | ListBox | TextBox | SignatureField 91 | // Spread | MasterSpread | Group | State | Character 92 | // SplineItem | Polygon | GraphicLine | Rectangle | Oval 93 | // --- 94 | 95 | p = gp.parent; 96 | 97 | // [TODO] 'Unnest' gp (temporarily) if it is the direct child of a SplineItem. 98 | // --- 99 | if( p.hasOwnProperty('textPaths') ) return false; 100 | 101 | // Make sure we can create a MSO from `p`. 102 | // --- 103 | if( !p.hasOwnProperty('multiStateObjects') ) return false; 104 | 105 | // Create a MSO and convert `gp` into a new state. 106 | // --- 107 | mso = p.multiStateObjects.add(); 108 | mso.addItemsAsState( [gp] ); 109 | sta = mso.states.lastItem(); 110 | 111 | // Inject `items` in the new state. 112 | // --- 113 | sta.addItemsToState( items ); 114 | 115 | // Release the state "as object". 116 | // The `gp` specifier is fully restored. 117 | // --- 118 | sta.releaseAsObject(); 119 | mso.remove(); 120 | gp.properties = {name: gpn}; 121 | 122 | // Restore the containing AO. 123 | // --- 124 | if( ao ) 125 | { 126 | t = ao.obj.anchoredObjectSettings; 127 | t.insertAnchoredObject(ao.ipOffset,ao.ancPos); 128 | } 129 | 130 | return gp; 131 | }; 132 | 133 | Group.prototype.addItems.UPAO = function(/*DOM*/obj, t) 134 | // ----------------------------------------------- 135 | // (Up-AnchoredObject-Utility.) Return the first AO (if any) 136 | // containing the input object. If `obj` is itself an AO, return it. 137 | // If obj doesn't belong to any AO, return false. 138 | // --- 139 | // [REM] The fact that any AO is parented by a Character is not 140 | // a sufficient condition to identify it as an AO, since Cells, 141 | // Footnotes, or HiddenTexts also belong to a Character. 142 | // [REM] The returned AO might in turn belong to a higher AO. 143 | // --- 144 | // => SplineItem | Polygon | GraphicLine | Rectangle | Oval | Group | 145 | // TextFrame | EndnoteTextFrame | MultiStateObject | Button | 146 | // FormField | SignatureField | TextBox | RadioButton | ListBox | 147 | // ComboBox | CheckBox | EPSText [OK] | false [KO] 148 | { 149 | if( obj.hasOwnProperty('anchoredObjectSettings') && (t=obj.anchoredObjectSettings.properties) && t.anchorPoint ) 150 | { 151 | t = obj.constructor.name; 152 | return 'Application'!=t && 'Document'!=t && 'ObjectStyle'!=t && obj; 153 | } 154 | 155 | // Any Text -> callee(TextFrame|TextPath) 156 | // --- 157 | if( obj.hasOwnProperty('parentTextFrames') ) 158 | { 159 | obj = obj.parentTextFrames; 160 | return ( obj && obj.length ) ? callee(obj[0]) : false; 161 | } 162 | 163 | t = (obj=obj.parent).constructor.name; 164 | return 'Document'!=t && 'Spread'!=t && 'MasterSpread'!=t && 'Page'!=t && callee(obj); 165 | }; 166 | -------------------------------------------------------------------------------- /snip/SelFirstImage.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelFirstImage [1.0] 4 | Desc: Select and return the (top+left)most image container on a page. 5 | Path: /snip/SelFirstImage.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 210618 (YYMMDD) 14 | Modified: 210618 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Given a target page (and optionally a target layer) this snippet identifies 25 | and returns the 'top-left-most' SplineItem containing a Graphic object. 26 | By 'top-left-most', we mean the object whose (top+left) coordinate is minimal. 27 | On average, this determines what the user perceives as the 'first' geometric 28 | container of the page. 29 | 30 | [REF] community.adobe.com/ 31 | t5/indesign/script-how-to-select-most-top-left-object/td-p/12116280 32 | 33 | */ 34 | 35 | ;function selFirstImage(/*?Layer|str*/ly,/*?Document*/doc,/*?Page*/pg, t,a,b,c,i,iBest,xyMin) 36 | //---------------------------------- 37 | // Select and return the (top+left)most image container on a page. 38 | // `ly` : Target layer or layer name (opt.) 39 | // `doc` : Target document (default=active.) 40 | // `pg` : Target page (default=active.) 41 | // --- 42 | // E.g. `selectFirstImage('Layer 1');` 43 | // `selectFirstImage('Layer 1', myDoc, myDoc.pages[123]);` 44 | // --- 45 | // => SplineItem [OK] | false [KO] 46 | { 47 | // Checkpoint -> doc, pg 48 | // --- 49 | doc || (doc=app.properties.activeDocument||0); 50 | if( !doc.isValid || 'Document' != doc.constructor.name ) return false; 51 | 52 | pg || ((pg=app.properties.activeWindow)&&(pg=pg.properties.activePage)) || (pg=doc.pages[0]); 53 | if( !pg.isValid || 'Page' != pg.constructor.name ) return false; 54 | 55 | // Layer filter (optional.) 56 | // --- 57 | 'string'==typeof ly && ly.length && (ly=doc.layers.itemByName(ly)); 58 | ( ly && 'Layer'==ly.constructor.name && ly.isValid ) || (ly=void 0); 59 | 60 | // Browse the SplineItems collection. 61 | // --- 62 | t = pg.splineItems.everyItem(); 63 | if( !t.isValid ) return false; 64 | a = t.getElements(); 65 | b = ly ? t.itemLayer : []; 66 | 67 | // Identify the object having the minimal (x+y) sum. 68 | // --- 69 | for 70 | ( 71 | i=a.length, iBest=false, xyMin=1/0 ; 72 | i-- ; 73 | ly===b[i] && (t=a[i]).graphics.length && (t=t.geometricBounds) 74 | && xyMin > (t=t[0]+t[1]) && (xyMin=t, iBest=i) 75 | ); 76 | 77 | return false !== iBest && (app.select(t=a[iBest]), t); 78 | } 79 | -------------------------------------------------------------------------------- /snip/SelFlattenTransform.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: SelFlattenTransform [1.0] 4 | Desc: Flatten the transformations of the selection. 5 | Path: /snip/SelFlattenTransform.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function + Undoable Script 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 210211 (YYMMDD) 14 | Modified: 210211 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This snippet operates on the selection. It clears the transformation 25 | attributes of every SplineItem while keeping path points in place. At 26 | the end of the process the objects look the same but they no longer 27 | undergo any scaling/rotation/shear component. 28 | 29 | [REM] The present implementation does not scan inner items from groups, 30 | MSOs, etc. 31 | 32 | */ 33 | 34 | ;function flattenTransform(/*PageItem[]=auto*/sel,/*bool=0*/MUTE, z,i,t,PS,eps,j) 35 | //---------------------------------- 36 | { 37 | sel || (sel=app.properties.selection); 38 | if( (!sel) || !sel.length ) 39 | { 40 | if( !MUTE ) alert("No selection."); 41 | return 0; 42 | } 43 | 44 | for( z=0, i=sel.length ; i-- ; ) 45 | { 46 | if( !(t=sel[i]).hasOwnProperty('paths') ) continue; 47 | eps = (PS=t.paths).everyItem().entirePath; 48 | t.clearTransformations(); 49 | for( j=eps.length ; j-- ; PS[j].entirePath=eps[j] ); 50 | ++z; 51 | } 52 | 53 | if( MUTE ) return z; 54 | switch( z ) 55 | { 56 | case 0: alert("No path available."); break; 57 | case 1: break; 58 | default: alert(z + " items processed."); 59 | } 60 | } 61 | 62 | app.doScript(flattenTransform, void 0, void 0, UndoModes.ENTIRE_SCRIPT, "Flatten Transform"); 63 | -------------------------------------------------------------------------------- /snip/TableCellBox.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: TableCellBox (v.4) 4 | Desc: Create a box enclosing a cell (or cell range). 5 | Path: /snip/TableCellBox.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign CC [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220731 (YYMMDD) 14 | Modified: 230620 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | Takes a table cell (single or plural specifier) and creates a Rectangle object 25 | that encloses the area, with respect to cell stroke(s) and existing transformations. 26 | 27 | This snippet is a 'proof of concept' following this technical discussion: 28 | https://www.hilfdirselbst.ch/gforum/gforum.cgi?post=584008#584008 29 | 30 | The code illustrates and extends an idea from Uwe Laubender. Although 31 | of no particular interest to the end-user, the method might help scripters 32 | to access cell or cell-range coordinates for further processing... 33 | 34 | Update (230620): 35 | - v4: ability to skip box creation: pass in `false` as 2nd argument. The function 36 | now returns `topLeft`, `topRight`, `bottomLeft`, `bottomRight` properties in 37 | *spread* coordinate space (instead of inner space anchors). 38 | - v3 circumvents the bug reported in https://t.co/qhHLtR7w5F (& https://t.co/9mgv6wh4L6) 39 | related to anchored objects. This is an InDesign bug and we only have a half-hearted 40 | solution so far: when the target cell contains an anchored objet, the inner contents 41 | is restored thru `duplicate()`, not `move()`, to prevent ID crash. Consequence: 42 | internal IDs of the underlying objects are then modified. 43 | - The present implementation deals with various InDesign issues, in particular 44 | the `obj.duplicate()` bug that affects GraphicCell's inner object. The 45 | `duplicate` method is no longer invoked at all and the obj/spread matrix 46 | relationship is now purely computed 'from scratch'. 47 | - The code supports the case of MSO or Button in the graphic cell. 48 | - It can generate multiple cell boxes if the incoming cell range runs 49 | across different spreads (threaded frames.) 50 | 51 | Note. For a good understanding of the transformations, matrices, and coordinate 52 | spaces involved here, see https://indiscripts.com/tag/CST 53 | 54 | */ 55 | 56 | ;function cellBox(/*Cell|PageItem*/target,/*?str|false*/fillColor,/*?Document*/doc, bkStrokeMu,wrk,K,cell,pp,t,i,q,m,r) 57 | //---------------------------------- 58 | // By default, create a rectangle that exactly (?) matches the `target` 59 | // (single or plural specifier) w.r.t to transform states and stroke 60 | // weight. The function then returns a { box, topLeft, topRight, bottomLeft, 61 | // bottomRight, innerWidth, innerHeight } structure, `box` being the new Rectangle. 62 | // [CHG230620] If fillColor is explicitly set to `false`, no actual Rectangle is 63 | // created and the returned structure only contains coordinates (the `box` property 64 | // being set to false). 65 | // [CHG230620] While `innerWidth`, `innerHeight` are the intrinsic dimensions of the 66 | // box in points and relative to the inner space, the properties `topLeft`, `topRight`, 67 | // `bottomLeft`, and `bottomRight` give the [x,y] coordinates of the corresponding 68 | // anchors in SPREAD coordinate space. (Those anchors describe a parallelogram.) 69 | // [ADD220801] Added `innerWidth`, `innerHeight` props. 70 | // [ADD220803] `target` can be the cell's pageitem as well; the client 71 | // code can provide `doc` if the host document is already known. 72 | // [REM230620] If target is a plural cell that involves multiple pages, 73 | // the function returns an array of objects. 74 | // Note: Master items are not supported! 75 | // --- 76 | // Based on: 77 | // 1. Discussed suggestion at HDS: 78 | // Koordinaten einer Tabellenzelle (25. Jul 2022, 12:23) 79 | // www.hilfdirselbst.ch/gforum/gforum.cgi?post=584008#584008 80 | // 2. Uwe Laubender's code (25. Jul 2022, 17:57) 81 | // --- 82 | // => | [] [OK] | false [KO] 83 | // where :: { box:new Rectangle|false, innerWidth:num, innerHeight:num, 84 | // topLeft:num[2], topRight:num[2], bottomLeft:num[2], bottomRight:num[2] } 85 | { 86 | // Boring enums. 87 | // --- 88 | const MX = callee.MX || (callee.MX= 89 | { 90 | muPT : +MeasurementUnits.POINTS, 91 | ctGC : +CellTypeEnum.GRAPHIC_TYPE_CELL, 92 | ctTX : +CellTypeEnum.TEXT_TYPE_CELL, 93 | loBEG : +LocationOptions.AT_BEGINNING, 94 | // --- 95 | apTL : +AnchorPoint.TOP_LEFT_ANCHOR, 96 | apBR : +AnchorPoint.BOTTOM_RIGHT_ANCHOR, 97 | apCC : +AnchorPoint.CENTER_ANCHOR, 98 | // --- 99 | bbVIS : +BoundingBoxLimits.OUTER_STROKE_BOUNDS, 100 | // --- 101 | csPB : +CoordinateSpaces.PASTEBOARD_COORDINATES, 102 | csSP : +CoordinateSpaces.SPREAD_COORDINATES, 103 | csPR : +CoordinateSpaces.PARENT_COORDINATES, 104 | csIN : +CoordinateSpaces.INNER_COORDINATES, 105 | // --- 106 | mcSHR : [+(t=MatrixContent).scaleValues, +t.shearValue, +t.rotationValue], 107 | }); 108 | 109 | // Checkpoint and settings. 110 | // --- 111 | if( !(target||0).isValid ) 112 | { 113 | return false; 114 | } 115 | if( 'Document' != (doc||0).constructor.name ) 116 | { 117 | t = 'function' == typeof(target.toSpecifier) && target.toSpecifier(); 118 | if( 'string' != typeof t ) return false; 119 | t = t.split('//')[0]; 120 | try{ doc = resolve( 0===t.indexOf('(') ? t.slice(1) : t ) } 121 | catch(_){ doc=0 } 122 | if( !doc.isValid ) return false; 123 | } 124 | if( 'Cell' != target.constructor.name && 'Cell' != (target=target.parent||0).constructor.name ) 125 | { 126 | return false; 127 | } 128 | 129 | if( false !== fillColor ) 130 | { 131 | ( 'string' == typeof fillColor && (doc.colors.itemByName(fillColor).isValid||doc.swatches.itemByName(fillColor).isValid) ) 132 | || (fillColor='Black'); 133 | callee.BOX_PROPS = 134 | { 135 | strokeColor: 'None', 136 | fillColor: fillColor, 137 | // You may add safety attributes: 138 | // corner options, etc 139 | }; 140 | } 141 | else 142 | { 143 | // [ADD230620] 'No box' option. 144 | callee.BOX_PROPS = false; 145 | } 146 | 147 | // Temporarily force stroke weights in PT. 148 | // --- 149 | MX.muPT == (bkStrokeMu=+doc.viewPreferences.strokeMeasurementUnits) 150 | ? ( bkStrokeMu = false ) 151 | : ( doc.viewPreferences.strokeMeasurementUnits=MX.muPT ); 152 | 153 | // Supports multiple cells. 154 | // --- 155 | for( wrk={}, K=target.cells, i=K.length ; i-- ; ) 156 | { 157 | pp = (cell=K[i]).properties; 158 | t = callee[MX.ctGC == +pp.cellType ? 'GRAC' : 'TEXC']; 159 | t.call(callee,wrk,doc,cell,pp,MX); 160 | } 161 | 162 | // Apply final reframe and format result. 163 | // --- 164 | r = []; 165 | for( t in wrk ) 166 | { 167 | if( !wrk.hasOwnProperty(t) ) continue; 168 | q = wrk[t]; // { box, tsf, insp, T, L, B, R } 169 | 170 | // [CHG230619] Q.box remains FALSE if 'No box' 171 | q.box && q.box.reframe(MX.csIN, [ [q.L,q.T] , [q.R,q.B] ]); 172 | 173 | m = q.insp; 174 | r[r.length] = 175 | { 176 | box: q.box, // Rectangle|false 177 | // --- [DEL230620] 178 | // top: q.T, // Top coordinate (pt) in inner space 179 | // left: q.L, // Left coordinate (pt) in inner space 180 | // bottom: q.B, // Bottom coordinate (pt) in inner space 181 | // right: q.R, // Right coordinate (pt) in inner space 182 | innerWidth: q.R-q.L, // Inner width (pt) 183 | innerHeight: q.B-q.T, // Inner height (pt) 184 | // --- [ADD230620] 185 | topLeft: m.changeCoordinates([q.L,q.T]), // Translate the topLeft anchor in SPREAD coords. 186 | topRight: m.changeCoordinates([q.R,q.T]), // Translate the topRight anchor in SPREAD coords. 187 | bottomLeft: m.changeCoordinates([q.L,q.B]), // Translate the bottomLeft anchor in SPREAD coords. 188 | bottomRight: m.changeCoordinates([q.R,q.B]), // Translate the bottomRight anchor in SPREAD coords. 189 | }; 190 | } 191 | 192 | // Restore stroke unit if necessary. 193 | // --- 194 | bkStrokeMu && (doc.viewPreferences.strokeMeasurementUnits=bkStrokeMu); 195 | 196 | return 0 < (t=r.length) && (1 < t ? r : r[0]); 197 | }; 198 | 199 | cellBox.TEXC = function(/*Work&*/wrk,/*Document*/doc,/*Cell&*/cell,/*CellProp*/pp,/*Enums*/MX, reGrow,sto,tf,bx,k) 200 | //---------------------------------- 201 | // (Process-Text-Cell.) `cell` is a regular Cell. 202 | // this :: cellBox (fct) 203 | // => {} [OK] | false [KO] 204 | { 205 | (reGrow=pp.autoGrow)&&(cell.autoGrow=false); 206 | 207 | sto=(tf=doc.textFrames.add()).parentStory; // Dummy frame/story. 208 | cell.texts[0].move(MX.loBEG, sto); // Assert: Cell is now empty. 209 | cell.convertCellType(MX.ctGC); // Assert: Cell.pageItems[0] is a Rectangle. 210 | bx = this.GRAC(wrk,doc,cell,pp,MX); 211 | 212 | cell.convertCellType(MX.ctTX); // Restore Text cell. 213 | k = -1 == sto.texts[0].contents.indexOf('\uFFFC') // [FIX230608] Prevents InDesign Bug: 214 | ? 'move' // `move` is allowed if `sto` has no anchor 215 | : 'duplicate'; // otherwise, use `duplicate` (fallback) 216 | sto[k](MX.loBEG,cell.texts[0].insertionPoints[0]); // Restore contents. 217 | tf.remove(); // Remove dummy frame. 218 | 219 | reGrow && (cell.autoGrow=true); // Restore autoGrow if necessary. 220 | return bx; 221 | }; 222 | 223 | cellBox.GRAC = function(/*Work&*/wrk,/*Document*/doc,/*Cell*/cell,/*CellProp*/pp,/*Enums*/MX, gco,spd,bx,q,k,t,m,lt,rb) 224 | //---------------------------------- 225 | // (Process-Graphic-Cell.) `cell` is a GC. 226 | // this :: cellBox (fct) 227 | // => {} [OK] | false [KO] 228 | { 229 | const myTL = callee.LOC_TL||(callee.LOC_TL=[MX.apTL,MX.bbVIS,MX.csPR]); 230 | const myBR = callee.LOC_BR||(callee.LOC_BR=[MX.apBR,MX.bbVIS,MX.csPR]); 231 | 232 | // 1. Determine the destination SPREAD. 233 | // --- 234 | gco = cell.pageItems[0]; // Could be any kind of PageItem (incl. Button, MSO etc.) 235 | if( !gco.properties.visibleBounds ) return false; // Make sure `gco` is not a 'ghost'. 236 | t = gco.resolve(MX.apCC,MX.csPB)[0][1]; // Y-coord of the center point *in PASTEBOARD space*. 237 | spd = this.Y2SP(t,doc,MX); // Host spread. 238 | 239 | // 2. Get/create the box (SPREAD item). 240 | // --- 241 | if( wrk.hasOwnProperty(k='_'+spd.id) ) 242 | { 243 | q = wrk[k]; 244 | bx = q.box; // Recover existing box. 245 | m = q.tsf; // Recover PB->boxInner matrix. 246 | } 247 | else 248 | { 249 | if( this.BOX_PROPS ) 250 | { 251 | bx = this.IBOX(spd,gco,MX); // New box. 252 | m = bx.transformValuesOf(MX.csPB)[0].invertMatrix(); // PB->boxInner 253 | } 254 | else 255 | { 256 | // [ADD230619] No box: use gcoParent as virtual dest space; 257 | // boxInner is not available so inner->parent is [1,0,0,1,0,0]. 258 | bx = false; // No box. 259 | m = this.PBPR(gco,MX); // PB->boxInner(=gcoParent) 260 | } 261 | 262 | // Save. 263 | q = wrk[k] = 264 | { 265 | box:bx, tsf:m, 266 | insp: this.INSP(spd, bx||gco, MX), // [ADD230620] Inner->Spread matrix 267 | L:1/0, T:1/0, R:-1/0, B:-1/0 268 | }; 269 | } 270 | 271 | // 3. The whole cellBox trick is here: get the opposite 272 | // corners of the *VISIBLE IN-PARENT* box of `gco`. 273 | // --- 274 | lt = m.changeCoordinates(gco.resolve(myTL,MX.csPB)[0]); // Translate the resolved (L,T) from PB to boxInner 275 | (t=pp.leftEdgeStrokeWeight||0) && (lt[0]-=t/2); // Left edge shift. 276 | (t=pp.topEdgeStrokeWeight||0) && (lt[1]-=t/2); // Top edge shift. 277 | // --- 278 | rb = m.changeCoordinates(gco.resolve(myBR,MX.csPB)[0]); // Translate the resolved (R,B) from PB to boxInner 279 | (t=pp.rightEdgeStrokeWeight||0) && (rb[0]+=t/2); // Right edge shift. 280 | (t=pp.bottomEdgeStrokeWeight||0) && (rb[1]+=t/2); // Rottom edge shift. 281 | 282 | // 4. Basically, all we have to do is reframing the box 283 | // in its inner space along [lt,rb]. But since we may 284 | // address multiple cells, just update the metrics. 285 | // --- 286 | (t=lt[0]) < q.L && (q.L=t); 287 | (t=lt[1]) < q.T && (q.T=t); 288 | (t=rb[0]) > q.R && (q.R=t); 289 | (t=rb[1]) > q.B && (q.B=t); 290 | 291 | return bx; 292 | }; 293 | 294 | cellBox.IBOX = function(/*Spread*/spd,/*PageItem*/gco,/*Enums*/MX, r,t) 295 | //---------------------------------- 296 | // (Initialize-Box.) Create a new box in appropriate transform state. 297 | // this :: cellBox (fct) 298 | // => Rectangle. 299 | { 300 | // 1. Create a fresh rectangle in `spd`. 301 | // --- 302 | r = spd.rectangles.add(gco.itemLayer); 303 | r.properties = this.BOX_PROPS; 304 | 305 | // 2. Adjust the transform state (diregarding translation) 306 | // so that: recInner->Spread fits gcoParent->Spread. 307 | t = gco.transformValuesOf(MX.csPR)[0].invertMatrix(). // Parent->Inner 308 | catenateMatrix( this.INSP(spd,gco,MX) ); // x Inner->Spread 309 | r.transform(MX.csSP, MX.apCC, t, MX.mcSHR); // Replace the existing S•H•R components. 310 | 311 | return r; 312 | }; 313 | 314 | cellBox.INSP = function(/*Spread*/spd,/*PageItem*/item,/*Enums*/MX) 315 | //---------------------------------- 316 | // (Inner-to-Spread-Matrix) [ADD230619] Get the *safe* inner-to-spread 317 | // matrix of the object `item`. 318 | // this :: cellBox (fct) 319 | // => TranformationMatrix 320 | { 321 | const TVO = 'transformValuesOf'; 322 | const INV = 'invertMatrix'; 323 | 324 | // [REM] Since item.transformValuesOf() may be unsafe, rely 325 | // on Spread->PB matrix and use Inner->Spread = Inner->PB × PB->Spread 326 | // --- 327 | return item[TVO](MX.csPB)[0]. // Inner->PB 328 | catenateMatrix( spd[TVO](MX.csPB)[0][INV]() ); // × PB->Spread 329 | }, 330 | 331 | cellBox.PBPR = function(/*PageItem*/item,/*Enums*/MX) 332 | //---------------------------------- 333 | // (Pasteboard-to-Parent-Matrix) [ADD230619] Get the PB-to-parent 334 | // matrix of the object `item`. 335 | // this :: cellBox (fct) 336 | // => TranformationMatrix 337 | { 338 | const TVO = 'transformValuesOf'; 339 | const INV = 'invertMatrix'; 340 | 341 | // PB->Parent = (Parent->Inner x Inner->PB).inv() 342 | // --- 343 | return item[TVO](MX.csPR)[0][INV](). // ( Parent->Inner 344 | catenateMatrix( item[TVO](MX.csPB)[0] )[INV](); // × Inner->PB ).invert() 345 | }, 346 | 347 | cellBox.Y2SP = function(/*num*/Y,/*Document*/doc,/*Enums*/MX, K,a,t,k,i,z,b) 348 | //---------------------------------- 349 | // Get the Spread that contains the absolute Y coordinate (in Pasteboard space.) 350 | // [REM] Master spreads not supported!! 351 | // => Spread. 352 | { 353 | // Spread y-positions. (Cached.) 354 | // --- 355 | K = doc.spreads; 356 | a = (t=callee.Q||(callee.Q={})).hasOwnProperty(k=doc.toSpecifier()) && t[k]; 357 | if( !a ) 358 | { 359 | a = K.everyItem().resolve([MX.apTL,MX.bbVIS,MX.csPB],MX.csPB)[0]; 360 | for( i=z=a.length ; i-- ; a[i]=i?a[i][1]:-1/0 ); 361 | a[z] = 1/0; 362 | t[k] = a; 363 | } 364 | else 365 | { 366 | z = -1 + a.length; 367 | } 368 | 369 | // Binary search. Looks for the unique `i` s.t. 370 | // `a[i] <= Y < a[i+1]` (i is then the spread index.) 371 | // --- 372 | for 373 | ( 374 | t=[0,z] ; 375 | Y < a[i=(t[b=0]+t[1])>>1] || Y >= a[(b=1)+i] ; 376 | t[1-b]=b+i 377 | ); 378 | 379 | return K[i]; 380 | }; 381 | 382 | 383 | 384 | // Test Me. 385 | // --- 386 | var cell = app.selection[0]; 387 | var ret = cellBox(cell, 'Yellow'); // Use `false` rather than 'Yellow' to only get coordinates. 388 | /* 389 | if( ret ) 390 | { 391 | var i, msg; 392 | if( ret instanceof Array ) 393 | { 394 | for 395 | ( 396 | msg=["Intrinsic dimensions (multiple spreads):"], i=-1 ; 397 | ++i < ret.length ; 398 | msg[msg.length] = ret[i].innerWidth + ' \xD7 ' + ret[i].innerHeight + ' pt' 399 | ); 400 | msg = msg.join('\r'); 401 | } 402 | else 403 | { 404 | msg = "Intrinsic dimensions: " + ret.innerWidth + ' \xD7 ' + ret.innerHeight + ' pt'; 405 | } 406 | 407 | alert( msg ); 408 | } 409 | else 410 | { 411 | alert("Select a Table cell or cell range."); 412 | } 413 | */ 414 | 415 | 416 | -------------------------------------------------------------------------------- /snip/TextSortParagraphs.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: TextSortParagraphs [1.3] 4 | Desc: Fast sort of InDesign paragraphs. 5 | Path: /snip/TextSortParagraphs.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Function 10 | API: sortParagraphs() 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 200515 (YYMMDD) 14 | Modified: 220726 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This snippet re-orders a range of paragraphs according to a sort function. 25 | By default `Array.prototype.sort` is applied to the contents, but the client 26 | code can supply a custom function as 2nd argument. 27 | 28 | In InDesign, sorting and reordering text elements is a complex task, as this 29 | involves time-consuming DOM commands and must be done with respect to the 30 | particular character styles and text attributes that the range may contain. 31 | Usually, it is not possible to sort the strings in JS and then simply re- 32 | assign the paragraphs: all text attributes would be puzzled. 33 | 34 | The present implementation provides a mechanism that actually reorders the 35 | paragraphs once sorted and preserves individual character attributes. This 36 | is done by 'moving' text ranges at the DOM level (no extra text container is 37 | created during the process.) The algorithm has been optimized to reduce the 38 | necessay moves to the strict minimum, so this function should run fast on 39 | average cases. 40 | 41 | Sample codes: 42 | 43 | // 1. Sort the paragraphs of the selection. 44 | sortParagraphs(app.selection[0]); 45 | 46 | // 2. Sort an entire story. 47 | sortParagraphs(myStory); 48 | 49 | [UPD220726] Version 1.3 introduces a 3rd argument, extraOrderFunc, that 50 | allows you to create custom order keys (instead of paragraph contents) 51 | based on the plural Paragraph specifier. Cf. https://adobe.ly/3Oz8LNQ 52 | 53 | */ 54 | 55 | ;const sortParagraphs = function sortParagraphs(/*Text|TextContainer*/input,/*fct=auto*/sortFunc,/*fct=auto*/extraOrderFunc, F,T,A,pev,c,host,n,X,Z,order,i,a,k,x,dx,xi,t,j) 56 | //---------------------------------- 57 | // `input` :: a Text object -- for example a Paragraphs.itemByRange(...) specifier -- or a text container 58 | // (Story, TextFrame, Cell...) In case a TextFrame is supplied, the whole parent story is 59 | // targeted. If the input Text is a partial range, only the containing paragraphs are treated. 60 | // `sortFunc` :: [opt.] Custom function taking an array of strings and returning that very array sorted. 61 | // If not supplied, strings are sorted based on toLowerCase() and Array.sort(). 62 | // WARNING: if you supply a custom sort function, make sure it doesn't alter the strings 63 | // passed through the array (sortParagraphs adds a special suffix for indexing purpose.) 64 | // `extraOrderFunc` :: [opt.] Custom function taking a plural Paragraph specifier `pev` and returning 65 | // an array of N sortable string keys (excluding \0.), where N is the paragraph count in pev. 66 | // By default, `extraOrderFunc` is not implemented; `pev.contents` is used instead. 67 | // WARNING: THIS IS AN ADVANCED PARAMETER, USE IT ONLY IF YOU KNOW WHAT YOU'RE DOING!!! 68 | // [REM] At most 65,535 paragraphs can be treated by this function. 69 | // --- 70 | // => undef 71 | { 72 | // Checkpoints. 73 | // --- 74 | if( 'function' != typeof(F=callee.RUN) ) throw "The RUN routine is missing." 75 | const CHR = String.fromCharCode; 76 | const MTH = 'function' == typeof sortFunc ? 'toString' : (sortFunc=0, 'toLowerCase'); 77 | const EXT = 'function' == typeof extraOrderFunc; 78 | // --- 79 | if( (!input) || !input.hasOwnProperty('texts') ) return; 80 | input instanceof TextFrame && (input=input.parentStory); 81 | 82 | // Split `input` into independent areas (useful in plural Cell context.) 83 | // --- 84 | input = input.texts.everyItem().getElements(); 85 | 86 | for( T=[], (A=[]).size=0 ; pev=input.pop() ; T.length=A.size=0 ) 87 | { 88 | if( 2 > (pev=pev.paragraphs).count() ) continue; 89 | 90 | // Add a temporary newline if the last para reaches the end of story/container. 91 | // --- 92 | c = pev[-1].characters[-1].getElements()[0]; 93 | host = c.parent.insertionPoints; 94 | c = '\r' != c.texts[0].contents && (host[-1].contents='\r', pev[-1].characters[-1].getElements()[0]); 95 | 96 | // Paragraphs metrics. 97 | // --- 98 | pev = pev.everyItem(); 99 | X = pev.index; // positions 100 | Z = pev.length; // lengths 101 | n = X.length; // count 102 | 103 | if( 0xFFFF < n ){ alert( "Too many paragraphs." ); continue; } 104 | 105 | // Sort the paragraph contents. 106 | // --- 107 | order = EXT ? extraOrderFunc(pev) : pev.contents; // [ADD220726] Supports extraOrderFunc. 108 | for( i=n ; i-- ; T[i]=i, order[i]=order[i][MTH]()+'\0'+CHR(i) ); 109 | sortFunc ? (order=sortFunc(order)) : order.sort(); 110 | for( i=n ; i-- ; order[i]=order[i].slice(-1).charCodeAt(0) ); 111 | 112 | // Optimize reordering steps. 113 | // --- 114 | for( --n, a=false, k=-1, x=X[0] ; ++k < n ; x+=dx ) 115 | { 116 | i = order[k]; 117 | dx = Z[i]; 118 | 119 | if( x != (xi=X[i]) ) 120 | { 121 | // OPTIM NEW MOVE 122 | // --- 123 | a[1]===xi ? (a[1]+=dx) : (a=A[A.size++]=[xi,xi+dx,x]); 124 | } 125 | else 126 | { 127 | // [FIX200619] It is necessary to restore the `false` 128 | // state to prevent irrelevant optimizations. 129 | // --- 130 | a = false; 131 | } 132 | 133 | for( t=-1 ; i > (j=T[++t]) ; X[j]+=dx ); 134 | T.splice(t,1); 135 | } 136 | 137 | // Run the DOM commands. 138 | // --- 139 | (t=app.scriptPreferences).enableRedraw ? (t.enableRedraw=false) : (t=0); 140 | F.ACTIONS = A; 141 | F.HOST = host; 142 | F.CHAR = c; 143 | app.doScript(F,void 0,void 0, +UndoModes.FAST_ENTIRE_SCRIPT, "SortParagraphs"); 144 | t && (t.enableRedraw=true); 145 | } 146 | }; 147 | 148 | sortParagraphs.RUN = function( A,host,n,w,i,a,c) 149 | //---------------------------------- 150 | // Utility of sortParagraphs. 151 | // => undef 152 | { 153 | const BEF = +LocationOptions.BEFORE; 154 | A = callee.ACTIONS; 155 | host = callee.HOST; 156 | n = A.size; 157 | 158 | if( w=300 < n ) 159 | { 160 | w = new Window('palette','SortParagraphs'); 161 | w.margins = 30; 162 | w.orientation = 'column'; 163 | w.alignChildren = ['center','top']; 164 | w.add('statictext', void 0, "Sorting the paragraphs requires "+n+" moves..."); 165 | (w.pb = w.add('progressbar', void 0, 0, n)).minimumSize = [200,20]; 166 | w.show(); 167 | } 168 | 169 | for( i=-1 ; ++i < n ; (w&&(i%50||(w.pb.value=i,w.update()))), (a=A[i]), host.itemByRange(a[0],a[1]).move(BEF,host[a[2]]) ); 170 | (c=callee.CHAR) && c.isValid && c.remove(); 171 | }; -------------------------------------------------------------------------------- /snip/TransformationMatrixInfo.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: TransformationMatrixInfo [1.0] 4 | Desc: Display clean attributes of a TransformationMatrix 5 | Path: /snip/TransformationMatrixInfo.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Method (extends TransformationMatrix.prototype) 10 | API: TransformationMatrix.prototype.info() 11 | DOM-access: TransformationMatrix 12 | Todo: --- 13 | Created: 231203 (YYMMDD) 14 | Modified: 231204 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | //========================================================================== 19 | // PURPOSE 20 | //========================================================================== 21 | 22 | /* 23 | 24 | This snippet adds a `info` method to TransformationMatrix instances. Calling 25 | myMatrix.info("Some title") prompts useful data about the matrix, e.g 26 | 27 | Matrix 28 | ≈ [ 0.97 | -0.26 | 0.26 | 0.97 ] + [ 33.85 | 51.94 ] 29 | 30 | (Sx,Sy) ≈ 1.00 | 1.00 31 | (H) ≈ 0.00 CW 32 | (R) ≈ 15.00 CCW 33 | (Tx,Ty) ≈ 33.85 | 51.94 34 | 35 | [REM] The '≈' symbol indicates that values are rounded for readability. 36 | 37 | Sample code: 38 | 39 | // Show the transform matrix of the selection 40 | // relative to the parent spread. 41 | // --- 42 | var myObj = app.selection[0]; // asumming a PageItem is selected 43 | var mx = myObj.transformValuesOf(CoordinateSpaces.spreadCoordinates)[0]; 44 | mx.info("Spread Matrix of " + myObj); 45 | 46 | [RES] See also: https://indiscripts.com/tag/CST 47 | 48 | */ 49 | 50 | ;TransformationMatrix.prototype.info = function(/*?str*/title, pp,mx,sx,sy,rot,shr,tx,ty,i) 51 | //---------------------------------- 52 | // Call `alert(...)` with a human-readable message reporting matrix attributes, 53 | // all rounded to 2 decimal digits. 54 | // ================================== 55 | // Matrix 56 | // ≈ [ a | b | c | d ] + [ tx | ty ] ; all six components in matrixValues order 57 | // 58 | // (Sx,Sy) ≈ , ; scaling factors 59 | // (H) ≈ CW ; shear angle (clockwise) 60 | // (R) ≈ CCW ; rotation angle (counterclockwise) 61 | // (Tx,Ty) ≈ | ; translation attributes 62 | // ================================== 63 | // If supplied, the `title` argument is used as alert's 2nd arg. 64 | // --- 65 | // => undef 66 | { 67 | const MR = Math.round; 68 | const DG = 2; // Rounding parameter (decimal digits) 69 | const EP = Math.pow(10,DG); 70 | 71 | pp = this.properties; 72 | mx = pp.matrixValues; 73 | for( i=mx.length ; i-- ; mx[i]=(MR(EP*mx[i])/EP).toFixed(DG) ); 74 | 75 | sx = (MR(EP*pp.horizontalScaleFactor)/EP).toFixed(DG); 76 | sy = (MR(EP*pp.verticalScaleFactor)/EP).toFixed(DG); 77 | 78 | tx = mx[4]; 79 | ty = mx[5]; 80 | 81 | rot = (MR(EP*pp.counterclockwiseRotationAngle)/EP).toFixed(DG); 82 | shr = (MR(EP*pp.clockwiseShearAngle)/EP).toFixed(DG); 83 | 84 | alert( 85 | [ 86 | 'Matrix\r\u2248 [ ' + mx.slice(0,4).join(' | ') + ' ] + [ ' +mx.slice(4,6).join(' | ') + ' ]\r' 87 | , 88 | '(Sx,Sy) \u2248 ' + [sx,sy].join(' | ') 89 | , 90 | '(H) \u2248 ' + shr + ' CW' 91 | , 92 | '(R) \u2248 ' + rot + ' CCW' 93 | , 94 | '(Tx,Ty) \u2248 ' + [tx,ty].join(' | ') 95 | ].join('\r'), title||void 0); 96 | }; 97 | -------------------------------------------------------------------------------- /tests/CustomTextSortParagraphs.jsx: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | 3 | Name: CustomTextSortParagraphs 4 | Desc: Custom sort of InDesign paragraphs. 5 | Path: /tests/CustomTextSortParagraphs.jsx 6 | Encoding: ÛȚF8 7 | Compatibility: InDesign (all versions) [Mac/Win] 8 | L10N: --- 9 | Kind: Script ; using ../snip/TextSortParagraphs.jsx 10 | API: --- 11 | DOM-access: YES 12 | Todo: --- 13 | Created: 220726 (YYMMDD) 14 | Modified: 220726 (YYMMDD) 15 | 16 | *******************************************************************************/ 17 | 18 | // For a standalone script, copy the contents of `snip/TextSortParagraphs.jsx` 19 | // and replace the #include directive below by the clipboard: 20 | #include '../snip/TextSortParagraphs.jsx' 21 | 22 | //========================================================================== 23 | // Testing the snippet with a custom order function - cf https://adobe.ly/3Oz8LNQ 24 | //========================================================================== 25 | 26 | const myCustomOrder = function(/*PluralParagraph*/pev, q,r,n,i,z,k) 27 | //---------------------------------- 28 | // WARNING: Make sure the returned strings do not contain \0 29 | // => str[] 30 | { 31 | // => final order 32 | q = callee.Q||(callee.Q= 33 | { 34 | 'Item Number' : 1, 35 | 'Name' : 2, 36 | 'Name Chinese' : 3, 37 | 'Details' : 4, 38 | 'Details Chinese' : 5, 39 | 'Size' : 6, 40 | 'Size Chinese' : 7, 41 | 'Price' : 8, 42 | }); 43 | 44 | const CHR = String.fromCharCode; 45 | 46 | // r :: ParagraphStyle[] --> str[] 47 | // --- 48 | r = pev.appliedParagraphStyle; 49 | for 50 | ( 51 | z=1, n=r.length, i=-1 ; 52 | ++i < n ; 53 | r[i]=q.hasOwnProperty(k=r[i].name) ? CHR(z,q[k]) : CHR(++z) 54 | ); 55 | 56 | return r; 57 | }; 58 | 59 | sortParagraphs(app.selection[0].parentStory, void 0, myCustomOrder); 60 | 61 | --------------------------------------------------------------------------------