diff --git a/SampleScripts/ExtendScript/balanceRaggedLines/README.md b/SampleScripts/ExtendScript/balanceRaggedLines/README.md new file mode 100644 index 0000000..79b96fd --- /dev/null +++ b/SampleScripts/ExtendScript/balanceRaggedLines/README.md @@ -0,0 +1,125 @@ +# Balance Ragged Lines Capability + +This directory contains a script for managing ragged line balancing in Adobe InDesign documents using ExtendScript. The script is designed to work with InDesign versions 16.0.1 and above. + +## Directory Contents + +``` +balanceRaggedLines/ +├── manifest.json # Capability manifest file +├── balanceRaggedLines.jsx # Main script implementation +├── utils.jsx # Utility functions +├── errors.jsx # Error handling definitions +└── json2.jsx # JSON parsing utilities +``` + +## Files Description + +### 1. `manifest.json` +The manifest file defines the capability configuration: +- Supports InDesign versions 16.0.1 and above. +- Implements an ExtendScript capability entry point +- Version: 1.0.0 + +### 2. `balanceRaggedLines.jsx` +The main script file that implements the ragged line balancing functionality. Features include: +- Text frame identification by labels +- Ragged line balancing control for specified paragraphs +- Document link management +- Performance monitoring +- Comprehensive error handling + +### 3. Helper Files +- `utils.jsx`: Contains utility functions for document processing and logging +- `errors.jsx`: Defines error types and messages for consistent error handling +- `json2.jsx`: Provides JSON parsing and manipulation utilities + +## Prerequisites + +- Adobe InDesign (version 16.0.1 or higher) +- ExtendScript environment +- Access to InDesign Services +- Appropriate file system permissions + +## Installation + +1. Create a capability bundle (ZIP file) containing all JSX files: + ``` + Archive.zip + ├── manifest.json + ├── balanceRaggedLines.jsx + ├── utils.jsx + ├── errors.jsx + └── json2.jsx + ``` + Note: All files must be zipped directly without a parent folder. + +2. Register your script following the [InDesign Capabilities API guide](https://developer.adobe.com/firefly-services/docs/indesign-apis/how-tos/working-with-capabilities-api/) + +## Usage + +### Required Parameters + +Provide the following JSON parameters to run the script: + +```json +{ + "assets": [ + { + "source": { + "url": "" + }, + "destination": "sample.indd" + } + ], + "params": { + "targetDocument": "sample.indd", + "outputPath": "balanced.indd", + "labels": ["MainPara", "Heading"], // Optional: If not provided, affects all text frames + "balance": "true" // Can be "true" or "false" + } +} +``` + +### Parameter Details +- `labels`: Array of text frame labels to process (optional) +- `balance`: Boolean value to enable/disable ragged line balancing +- `targetDocument`: Input InDesign document path +- `outputPath`: Output document path + +## Logging and Monitoring + +### Log File +The script generates detailed logs in `LogFile.txt` including: +- Document processing steps +- Text frame identification +- Ragged line balancing operations +- Performance metrics +- Error messages and warnings + +### Performance Metrics +- Document opening time +- Link update time +- Ragged line balancing processing time +- Total execution duration + +## Error Handling + +The script includes comprehensive error handling for various scenarios: +- Parameter validation errors +- Document processing errors +- Text frame identification issues +- Link management problems +- File system operations + +Each error includes: +- Error code +- Detailed error message +- Context information + +## Support + +For additional information and support: +- Refer to the [Adobe InDesign APIs documentation](https://developer.adobe.com/firefly-services/docs/indesign-apis/) +- Check the error messages in the log file for troubleshooting +- Ensure all prerequisites are met before running the script diff --git a/SampleScripts/ExtendScript/balanceRaggedLines/balanceRaggedLines.jsx b/SampleScripts/ExtendScript/balanceRaggedLines/balanceRaggedLines.jsx new file mode 100644 index 0000000..c740bd9 --- /dev/null +++ b/SampleScripts/ExtendScript/balanceRaggedLines/balanceRaggedLines.jsx @@ -0,0 +1,324 @@ +/************************************************************************* + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2025 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + **************************************************************************/ + + +// Following are the file inclusions and not comments. +// @include "errors.jsx" +// @include "json2.jsx" +// @include "utils.jsx" +// File inclusion ends. + +// Globals: define any globals here. +var timingObject = {} +var warnings = {} +var document +// Globals end. + +// Constant strings for key references +var KeyStrings = { + OutputPath: 'outputPath', + ProcessedData: 'processedData', + TargetDocument: 'targetDocument', + TimeDocumentClose: 'DocumentClose', + TimeDocumentOpen: 'DocumentOpen', + TimeBalancing: 'TimeBalanceRagged', + TimeOverall: 'Overall', + TimeRelinkAssets: 'RelinkAssets', + Timings: 'timings', + WorkingFolder: 'workingFolder' +} + +/* Handles document warnings such as missing links or fonts */ +function handleWarnings (document) { + UTILS.Log('Handling warnings') + var errorlog = app.errorListErrors + var warningsObject = {} + + var missingLinkArray = [] + var missingFontArray = [] + var failedErrorsArray = [] + var otherWarningsArray = [] + var missingLinkStr = '(Link missing.; ' + var missingLinkStrLen = missingLinkStr.length + + // Iterate through error list and categorize warnings + for (var i = 0; i < errorlog.count(); i++) { + var error = errorlog[i] + UTILS.Log('Warning No. ' + (i + 1) + ': ' + error.listErrorCode + '(' + error.listErrorMessage + ')') + if (error.listErrorCode === 35842) { // Missing link error + var missingLink = error.listErrorMessage + missingLink = missingLink.substring(missingLink.indexOf(missingLinkStr) + missingLinkStrLen) + if (missingLinkArray.indexOf(missingLink) === -1) { + missingLinkArray.push(missingLink) + } + } else if (error.listErrorCode === 1 && error.listErrorMessage.search(/missing font/i) !== -1) { + var missingFont = error.listErrorMessage + missingFont = missingFont.substring(missingFont.search(/missing font/i) + 13) + if (missingFontArray.indexOf(missingFont) === -1) { + missingFontArray.push(missingFont) + } + } else if (error.listErrorCode === 1) { + otherWarningsArray.push(error.listErrorMessage) + } + } + + // Store collected warnings in an object + if (missingLinkArray.length > 0) { + warningsObject.missingLinks = missingLinkArray + } + if (missingFontArray.length > 0) { + warningsObject.missingFonts = missingFontArray + } + if (failedErrorsArray.length > 0) { + warningsObject.exportErrors = failedErrorsArray + } + if (otherWarningsArray.length > 0) { + warningsObject.otherWarnings = otherWarningsArray + } + return warningsObject +}; + + +/* Balances ragged lines for text frames within a document */ +function balanceRaggedLines (document,labels,balance) { + + var allFrames = document.allPageItems; + var labelledFrames = [] + + UTILS.Log('Inside Balanced Ragged Lines') + + // Convert labels to an object for faster lookups (Set is not available in ExtendScript) + var labelMap = {}; + if (labels && labels.length > 0) { + if (!(labels instanceof Array)) { + labels = [labels]; // Convert single label to an array + } + for (var i = 0; i < labels.length; i++) { + labelMap[labels[i]] = true; + } + + // Iterate over frames and find those matching the labels + for (var i = 0; i < allFrames.length; i++) { + var frame = allFrames[i]; + if (frame instanceof TextFrame && labelMap[frame.label]) { + labelledFrames.push(frame); + } + } + } else { + // If no labels provided, select all text frames + for (var i = 0; i < allFrames.length; i++) { + if (allFrames[i] instanceof TextFrame) { + labelledFrames.push(allFrames[i]); + } + } + } + UTILS.Log('Labels Extracted ' + labelledFrames.length) + + + // Balance ragged lines for all paragraphs in the labelled frames + if(labelledFrames.length == 0){ + UTILS.Log('No frames found') + return + } + for (var i = 0; i < labelledFrames.length; i++) { + var textFrame = labelledFrames[i]; + var paragraphs = textFrame.paragraphs; + for (var j = 0; j < paragraphs.length; j++) { + var para = paragraphs[j]; + if(balance===true){ + para.balanceRaggedLines = true; + } + else{ + para.balanceRaggedLines = false; + } + } + } + document.save(); +}; + + +/* Processes parameters and performs document operations */ +function ProcessParams (parameters) { + UTILS.Log('Processing parameters internal') + var returnVal = {} + + // Open the target document + var documentPath = UTILS.GetStringFromObject(parameters, KeyStrings.TargetDocument) + documentPath = UTILS.GetFullPath(documentPath) + + UTILS.Log('Opening document') + var tempTime = new Date().getTime() + document = app.open(File(documentPath)) + timingObject[KeyStrings.TimeDocumentOpen] = (new Date()).getTime() - tempTime + UTILS.Log('Opened document') + + // Relink assets and handle warnings + tempTime = new Date().getTime() + UTILS.UpdateDocumentLinks(document) + timingObject[KeyStrings.TimeRelinkAssets] = (new Date()).getTime() - tempTime + UTILS.Log('Updated links in the document') + + // Capturing top level errors and then clear errorList. + UTILS.Log('Number of errors: ' + app.errorListErrors.count()) + if (app.errorListErrors.count() > 0) { + warnings = handleWarnings(document) + } + app.clearAllErrors() + UTILS.Log('Got the warnings from the document.') + + var outputPath = UTILS.GetStringFromObject(parameters, KeyStrings.OutputPath) + outputPath = UTILS.GetFullPath(outputPath) + + tempTime = new Date().getTime() + UTILS.UpdateDocumentLinks(document) + var labels = parameters.labels + var balance = parameters.balance + + // Calling the function to balance ragged lines + balanceRaggedLines(document,labels,balance); + + // Save the document as a new file. + document.save(File(outputPath)) + + timingObject[KeyStrings.TimeBalancing] = (new Date()).getTime() - tempTime + + // Add file to be uploaded as an output file. + var relativePath = UTILS.GetRelativeReturnPath(outputPath) + UTILS.AddAssetToBeUploaded(relativePath) + + returnVal.OutputPath = documentPath + // Processing ends. + + return returnVal +} + + +/* Main function to execute the script */ +function main () { + + var startTime = new Date().getTime() // Capture the script start time + var returnObj = {} // Object to store return values + var parameters = {} // Stores input parameters + var tempTime + var errorOccurred = false // Flag to track errors + var data = {} // Stores processed data + + // Set application preferences + app.clearAllErrors() + app.generalPreferences.pageNumbering = PageNumberingOptions.ABSOLUTE + app.linkingPreferences.checkLinksAtOpen = true + app.serverSettings.useErrorList = true + var previousUnit = app.scriptPreferences.measurementUnit + app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS + + try { + UTILS.InitiateLogging() + UTILS.Log('Initiating logging.') + + // As a safe practice close any open documents before processing + UTILS.CloseAllOpenDocuments() + + // Parse input parameters + var input = app.scriptArgs.get('parameters') + var allParameters = JSON.parse(input) + + UTILS.Log('Parsed job input : ' + input) + parameters = allParameters.params + if (parameters === undefined) { + parameters = allParameters.input.params + } + + // Check if parameters are valid + if (parameters === undefined || typeof parameters !== 'object' || Array.isArray(parameters)) { + UTILS.Log('No params found') + UTILS.RaiseException(Errors.MissingParams) + } + + // Set the working folder first. This is the directory within which all the input and output assets are to be managed. + UTILS.SetWorkingFolder(UTILS.GetStringFromObject(allParameters, KeyStrings.WorkingFolder)) + + var result + UTILS.Log('Processing Params') + tempTime = new Date().getTime() + + // Process parameters and document + result = ProcessParams(parameters) + data[KeyStrings.ProcessedData] = result + + // Also add the log file + UTILS.AddAssetToBeUploaded(UTILS.logFilePath) + + data.warnings = warnings + // Processing ends. + + tempTime = new Date().getTime() + if (document && document.isValid){ + document.close() + } + + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + UTILS.Log('End of try') + } catch (e) { + var tempObj = { + name: e.name, + message: e.message, + errorCode: e.number, + isCustom: e.isCustom, + line: e.line, + fileName: e.fileName + } + + UTILS.Log('Exception occurred', tempObj) + errorOccurred = true + + // Failure, prepare the object to be returned. + returnObj = UTILS.HandleError(tempObj) + } finally { + app.scriptPreferences.measurementUnit = previousUnit + UTILS.Log('In finally') + if (document && document.isValid) { + + // If Document is still open. Close it. + UTILS.Log('Closing document') + tempTime = new Date().getTime() + document.close() + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + } + + var elapsedTime = (new Date()).getTime() - startTime + UTILS.Log('Time taken: ' + elapsedTime) + timingObject[KeyStrings.TimeOverall] = elapsedTime + UTILS.Log('Timing: ' + JSON.stringify(timingObject)) + data[KeyStrings.Timings] = timingObject + + if (!errorOccurred) { + // Success, prepare the object to be returned. + UTILS.Log('Finally: No error') + returnObj = UTILS.GetSuccessReturnObj(data) + } + UTILS.Log('Final Result', JSON.stringify(returnObj)) + + // Cleanup and Return + UTILS.TerminateLogging() + app.clearAllErrors() + + return UTILS.GetFinalReturnPackage(returnObj) + } +} + +// Execute the main function +main() diff --git a/SampleScripts/ExtendScript/balanceRaggedLines/errors.jsx b/SampleScripts/ExtendScript/balanceRaggedLines/errors.jsx new file mode 100644 index 0000000..b5cd221 --- /dev/null +++ b/SampleScripts/ExtendScript/balanceRaggedLines/errors.jsx @@ -0,0 +1,160 @@ +/************************************************************************* + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2025 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + **************************************************************************/ +// errors.jsx + +// Error constants are defined here. +/* This document list all the error which are possible from the scripts. The error object is built such that it has an error code and some error strings. + The error strings are constructed in a way such that the first string is default and a string literal. It is to be returned anyhow. The subsequent strings can be + strings having '^1' as placeholder replacement string. This replacement string can be replaced with relevant information. Based on the information + available the final error message can be created. +*/ +/* eslint-disable no-unused-vars */ + +var ErrorReplacementString = '^1' + +var Errors = { + // Errors during processing: 1001-1999 + InternalScriptError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal script error. This should not had happened.' + ] + }, + ProcessingErrorOccurred: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Error during processing.' + ] + }, + PDFPresetNotSet: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: No PDF Preset could be set.' + ] + }, + OutputDirError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Unable to create specified output directory.' + ] + }, + RelinkError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to relink.', + 'Relink failed for ^1.' + ] + }, + PlaceError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to place.', + 'Place failed for ^1.' + ] + }, + + // Errors in input: 2001-2999 + ArrayExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected array ', + 'for property ^1.' + ] + }, + ParsingBoolError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected boolean ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingIntError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected integer ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingFloatError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected number ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingStringError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected string ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + OutOfBound: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Incorrect range provided. ', + 'Culprit Value is ^1.' + ] + }, + MissingKey: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Key not found in object. ', + 'Missing key is ^1.' + ] + }, + EnumError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: No matching enum value found. ', + '^1 is the provided value', + 'for property ^1.' + ] + }, + MissingParams: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Either the \'params\' is not found in the request or it is not in the correct format.' + ] + }, + ObjectExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected object ', + 'for property ^1.' + ] + }, + AtLeastOneInternalParamShouldBePresent: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: At least one of the parameters should be present. ', + 'Missing parameters are ^1' + ] + }, + + // Errors coming from IDS and sent under ProcessingErrorOccurred. These are specific errors which need string change while returning to the user. + CannotOpenFileError: { + errorCode: 'capability_error', // kCannotOpenFileError + errorStrings: [ + 'Capability error: Cannot open file.' + ] + } +} diff --git a/Example/json2.jsx b/SampleScripts/ExtendScript/balanceRaggedLines/json2.jsx similarity index 100% rename from Example/json2.jsx rename to SampleScripts/ExtendScript/balanceRaggedLines/json2.jsx diff --git a/SampleScripts/ExtendScript/balanceRaggedLines/manifest.json b/SampleScripts/ExtendScript/balanceRaggedLines/manifest.json new file mode 100644 index 0000000..b11480b --- /dev/null +++ b/SampleScripts/ExtendScript/balanceRaggedLines/manifest.json @@ -0,0 +1,17 @@ +{ + "manifestVersion": "1.0.0", + "name": "balanceRaggedLines", + "host": { + "app": "indesign", + "maxVersion": "99.9.9", + "minVersion": "16.0.1" + }, + "version": "1.0.0", + "apiEntryPoints": [ + { + "path": "balanceRaggedLines.jsx", + "type": "capability", + "language": "extendscript" + } + ] +} \ No newline at end of file diff --git a/SampleScripts/ExtendScript/balanceRaggedLines/utils.jsx b/SampleScripts/ExtendScript/balanceRaggedLines/utils.jsx new file mode 100644 index 0000000..2974cda --- /dev/null +++ b/SampleScripts/ExtendScript/balanceRaggedLines/utils.jsx @@ -0,0 +1,702 @@ +/************************************************************************* + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2025 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + **************************************************************************/ +// utils.jsx + +// Utils functions which can be used across. +/* globals app, Errors, File, Folder, LinkStatus, SaveOptions */ + +// Globals. +var logFileObject + +// eslint-disable-next-line no-extend-native +Array.prototype.indexOf = function (item) { + var index = 0 + var length = this.length + for (; index < length; index++) { + if (this[index] === item) { + return index + } + } + return -1 +} + +// eslint-disable-next-line no-extend-native +if (typeof Array.isArray === 'undefined') { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]' + } +} + +// Create a UTILS object to store utility functions and variables. +var UTILS = {} + +// Initialize various properties of the UTILS object. +UTILS.Logs = [] // Stores log messages. +UTILS.workingFolder = '' // Path of the working folder. +UTILS.outputFolder = '' // Path of the output folder. +UTILS.assetsToBeUploaded = [] // List of assets that need to be uploaded. +UTILS.createSeparateLogFile = true // Flag to determine if a separate log file should be created. +UTILS.logFilePath = 'LogFile.txt' // File name for logging. + +// Converts the package in appropriate format, which can be returned from the capability. +UTILS.GetFinalReturnPackage = function (obj) { + UTILS.Log('Final package created') + return JSON.stringify(obj) +} + +// Creates and returns the package to be returned in case the job is successful. +UTILS.GetSuccessReturnObj = function (data) { + var obj = {} + obj.status = 'SUCCESS' + obj.assetsToBeUploaded = UTILS.assetsToBeUploaded // Attach the list of assets to be uploaded. + var dataURL = UTILS.WriteToFile(data, 'outputData') // Save data to file. + if (dataURL) { + obj.dataURL = dataURL + } + return obj +} + +// Creates and returns the package to be returned in case the job has failed. +UTILS.GetFailureReturnObj = function (errorCode, errorString, data) { + var obj = {} + obj.status = 'FAILURE' + obj.errorCode = errorCode // Attach the error code. + obj.errorString = errorString // Attach the error message. + obj.data = data // Include additional data if available. + return obj +} + +// Add an asset which is to be uploaded and sent back to the caller. +UTILS.AddAssetToBeUploaded = function (assetPath, data) { + var assetToBeUploaded = {} + assetToBeUploaded.path = assetPath + if (data !== undefined) { + assetToBeUploaded.data = data // Include additional asset data if provided. + } + UTILS.assetsToBeUploaded.push(assetToBeUploaded) +} + +// This handles errors in case an exception is raised. This results the capability returning error to the caller. +UTILS.HandleError = function (exception) { + var errorCode, errorString + UTILS.Log('Exception occurred: ' + JSON.stringify(exception)) + + // Handle specific exception cases + if (exception.message === 'open') { + exception.message = Errors.CannotOpenFileError.errorStrings[0] + exception.errorCode = Errors.CannotOpenFileError.errorCode + } + + errorString = 'Script error: ' + if (exception.isCustom === true) { + if (exception.message) { + errorString = errorString + exception.message + } + errorCode = exception.errorCode + } else { + errorCode = Errors.ProcessingErrorOccurred.errorCode + if (exception.message) { + errorString = errorString + Errors.ProcessingErrorOccurred.errorStrings[0] + ' ' + exception.message + } + if (exception.errorCode !== undefined) { + errorString = errorString + '\nInternal error code: ' + exception.errorCode + '.' + } + if (exception.line !== undefined) { + errorString = errorString + ' Line: ' + exception.line + '.' + } + if (exception.fileName !== undefined) { + errorString = errorString + ' FileName: ' + exception.fileName + '.' + } + UTILS.Log('Processing error occurred. ' + errorString) + } + + return UTILS.GetFailureReturnObj(errorCode, errorString) +} + +// This is used to raise an exception with all the required details. +// NOTE: This takes variable list of argument and then try to fill in the details in errorObj.errorStrings +UTILS.RaiseException = function (errorObj) { + UTILS.Log('RaiseException()') + var numMessageParameters = arguments.length + UTILS.Log('', 'numMessageParameters: ' + numMessageParameters) + var numErrorStrings = errorObj.errorStrings.length + UTILS.Log('', 'numErrorStrings: ' + numErrorStrings) + var numIterations = (numMessageParameters > numErrorStrings) ? numErrorStrings : numMessageParameters + var errorMessage = errorObj.errorStrings[0] + UTILS.Log('', 'Default errorMessage: ' + errorMessage) + + // Construct the detailed error message using available parameters. + for (var itr = 1; itr < numIterations; itr++) { + var parameter = arguments[itr] + if (parameter !== undefined && parameter !== '') { + var parameterMessage = errorObj.errorStrings[itr] + errorMessage = errorMessage + ' ' + parameterMessage.replace('^1', parameter) + UTILS.Log('', 'Appended errorMessage: ' + errorMessage) + } + } + + // Throw an exception object with custom details. + var exceptionObj = { + number: errorObj.errorCode, + isCustom: true, + message: errorMessage + } + + throw exceptionObj +} + +// This will update out-of-date links in a document. +UTILS.UpdateDocumentLinks = function (document) { + var links = document.links + var numLinks = links.length + var linkItr, link, uri + var outOfDateLinks = [] + + UTILS.Log('Number of links: ' + numLinks) + for (linkItr = 0; linkItr < numLinks; linkItr++) { + try { + link = links[linkItr] + uri = link.linkResourceURI + UTILS.Log(linkItr + ': URI: ' + uri) + if (link.status === LinkStatus.LINK_OUT_OF_DATE) { + outOfDateLinks.push(link.id) + } + } catch (err) { + UTILS.Log('Link status unknown : ' + err) + } + } + + // Update all out-of-date links. + numLinks = outOfDateLinks.length + for (linkItr = 0; linkItr < numLinks; linkItr++) { + link = document.links.itemByID(outOfDateLinks[linkItr]) + if (link.isValid) { + link.update() + } + } + + // Recompose the document after updating links. + var composeStartTime = new Date().getTime() + document.recompose() + var composedTime = (new Date()).getTime() - composeStartTime + UTILS.Log('document.recompose Time: ' + composedTime) +} + +// This Embeds all the links in the document. +UTILS.EmbedDocumentLinks = function (document) { + var links = document.links + var numLinks = links.length + var linkItr, link + + for (linkItr = 0; linkItr < numLinks; linkItr++) { + try { + link = links[linkItr] + if (link.status === LinkStatus.NORMAL) { + link.unlink() + } + } catch (err) { + UTILS.Log('Unable to embed link: status is unknown : ' + err) + } + } +} + +// Trims leading and trailing spaces from a string. +UTILS.Trim = function (val) { + return val.replace(/^\s+|\s+$/gm, '') +} + +// Removes all the spaces from a string. +UTILS.RemoveAllSpaces = function (val) { + return val.replace(/\s/g, '') +} + +// Converts a value to integer. Exception is thrown if the conversion fails. +UTILS.GetBoolean = function (val) { + if (val === 'true' || val === true) { + return true + } else if (val === 'false' || val === false) { + return false + } + + // throw exception. + UTILS.Log('GetBoolean() val: ' + val) + UTILS.RaiseException(Errors.ParsingBoolError, val) +} + +// Gets a boolean from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetBooleanFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetBooleanFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (val === 'true' || val === true) { + return true + } else if (val === 'false' || val === false) { + return false + } + + UTILS.Log('GetBooleanFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingBoolError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Converts a value to integer. Exception is thrown if the conversion fails. +UTILS.GetInteger = function (val) { + var returnVal = parseInt(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetInteger() val: ' + val) + UTILS.RaiseException(Errors.ParsingIntError, val) +} + +// Gets a integer from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetIntegerFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetIntegerFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + var returnVal = parseInt(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetIntegerFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingIntError, val, key) + } else if (defaultVal === undefined) { + UTILS.Log('Missing key: ' + key) + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Converts a value to float. Exception is thrown if the conversion fails. +UTILS.GetFloat = function (val) { + var returnVal = parseFloat(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetFloat() val: ' + val) + UTILS.RaiseException(Errors.ParsingFloatError, val) +} + +// Gets a float from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetFloatFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetFloatFromObject ' + key) + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + var returnVal = parseFloat(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetFloatFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingFloatError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Validates a value to be one of the enum definitions. +UTILS.GetEnum = function (enumDef, val) { + try { + val = UTILS.Trim(val) + return UTILS.GetValueFromObject(enumDef, val) + } catch (e) { + UTILS.RaiseException(Errors.EnumError, val) + } +} + +// Gets a enum value from an 'object' defined with 'key' and after comparing against an enum definition. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value is not present in enum definition. +UTILS.GetEnumFromObject = function (enumDef, object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetEnumFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + try { + val = UTILS.Trim(val) + if (Array.isArray(enumDef) === false) { + return UTILS.GetValueFromObject(enumDef, val, ignoreCase) + } else { + if (enumDef.indexOf(val) >= 0) { + return val + } + } + } catch (e) { + UTILS.Log('GetEnumFromObject() val: ' + val) + UTILS.RaiseException(Errors.EnumError, val, key) + } + + UTILS.Log('GetEnumFromObject() val: ' + val) + UTILS.RaiseException(Errors.EnumError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets a string from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not string. +UTILS.GetStringFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetStringFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (typeof val !== 'string') { + UTILS.RaiseException(Errors.ParsingStringError, val, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets an array from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not an array. +UTILS.GetArrayFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetArrayFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (Array.isArray(val) === false) { + UTILS.RaiseException(Errors.ArrayExpected, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets an object from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not an object. +UTILS.GetObjectFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetObjectFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (typeof val !== 'object' || Array.isArray(val)) { + UTILS.RaiseException(Errors.ObjectExpected, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets the value from an 'object' defined with 'key'. +// The case of key can be ignored via 'ignoreCase'. In case, key is missing an exception will be thrown. +UTILS.GetValueFromObject = function (object, key, ignoreCase) { + UTILS.Log('GetValueFromObject ' + key) + + if (ignoreCase === true) { + var objectKey + for (objectKey in object) { + if (Object.prototype.hasOwnProperty.call(object, objectKey)) { + if (objectKey.toLowerCase() === key.toLowerCase()) { + return object[objectKey] + } + } + } + } else if (Object.prototype.hasOwnProperty.call(object, key)) { + return object[key] + } + + UTILS.RaiseException(Errors.MissingKey, key) +} + +// Checks whether a key is present in the object or not. +UTILS.IsKeyPresent = function (object, key) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + return true + } + return false +} + +// Sets the working folder to be used across the capability. +UTILS.SetWorkingFolder = function (workingFolder) { + workingFolder = workingFolder.replace(/\//g, '\\') + if (workingFolder.charAt(workingFolder.length - 1) !== '\\') { + workingFolder = workingFolder + '\\' + } + UTILS.workingFolder = workingFolder + UTILS.OpenLogFileHandle() +} + +// Gets the path of the directory for a given path. +UTILS.GetDirPath = function (path) { + path = path.replace(/\//g, '\\') + var indexOfSlash = path && path.lastIndexOf('\\') + if (indexOfSlash >= 0) { + return path.substr(0, indexOfSlash) + } + return '' +} + +// Gets the path of the directory for a given path. If 'withExtension is true the file name consists of the file extension and not otherwise. +UTILS.GetFileName = function (path, withExtension) { + var fileName + var indexOfSlash = path.lastIndexOf('\\') + 1 + if (indexOfSlash >= 1) { + fileName = path.substr(indexOfSlash) + if (withExtension === false) { + var indexOfDot = path.lastIndexOf('.') + fileName = path.substr(indexOfSlash, indexOfDot - indexOfSlash) + } + } else { + fileName = path + } + return fileName +} + +// Get relative path to the working directory for a path. +UTILS.GetRelativePath = function (path) { + var index = path.indexOf(UTILS.workingFolder) + if (index === 0) { + return path.substr(UTILS.workingFolder.length) + } + return path +} + +// Get relative path to the working directory for a path. This returns in unix notation. +UTILS.GetRelativeReturnPath = function (path) { + var index = path.indexOf(UTILS.workingFolder) + if (index === 0) { + var tempPath = path.substr(UTILS.workingFolder.length) + tempPath = tempPath.replace(/\\/g, '/') + return tempPath + } +} + +// Get the full path from a relative and a base path. In absence of base path, working directory is used. +UTILS.GetFullPath = function (relativePath, basePath) { + var origRelativePath = relativePath + if (relativePath.indexOf('..') !== -1 || relativePath.indexOf('/') === 0) { + this.RaiseException(Errors.InCorrectRelativePath, origRelativePath) + } + relativePath = relativePath.replace(/\//g, '\\') + if (relativePath.indexOf('\\.\\') !== -1) { + this.RaiseException(Errors.InCorrectRelativePath, origRelativePath) + } + + if (basePath === undefined) { + basePath = UTILS.workingFolder + } else { + basePath = basePath.replace(/\//g, '\\') + if (basePath.charAt(basePath.length - 1) !== '\\') { + basePath = basePath + '\\' + } + } + + if (relativePath.charAt(0) === '.') { + relativePath = relativePath.slice(1) + } + if (relativePath.charAt(0) === '\\') { + relativePath = relativePath.slice(1) + } + + var fullPath = basePath + relativePath + return fullPath +} + +// Get a unique name. +UTILS.GetUniqueName = function () { + return Math.random().toString().substr(2, 6) +} + +// Creates a directory for the path provided. +UTILS.CreateDirectory = function (path) { + var outputFolder = Folder(path) + return outputFolder.create() +} + +// Creates a directory for the outputs on the basis of relative path passed. In case nothing is passed a random directory is created in working folder. +UTILS.GetOutputDirectory = function (outputFolderPath) { + var outputPath = '' + var created = false + if (outputFolderPath) { + outputPath = UTILS.GetFullPath(outputFolderPath) + created = UTILS.CreateDirectory(outputPath) + var outputFolder = Folder(outputPath) + if (outputFolder.exists === false) { + UTILS.RaiseException(Errors.OutputDirError) + } + } else { + var tempName = '' + do { + tempName = 'tmp' + UTILS.GetUniqueName() + outputPath = UTILS.GetFullPath(tempName) + created = UTILS.CreateDirectory(outputPath) + } while (!created) + } + UTILS.outputFolder = outputPath + return outputPath +} + +// Setup to do the logging. +UTILS.InitiateLogging = function () { + UTILS.Logs = [] +} + + +// Opens a log file handle if separate logging is enabled. +UTILS.OpenLogFileHandle = function () { + if (UTILS.createSeparateLogFile === true) { + logFileObject = new File(UTILS.GetFullPath(UTILS.logFilePath)) + var exists = false + + // Check if the log file already exists + if (logFileObject.open('read')) { + exists = true + } + UTILS.Log('Creating log file at ' + UTILS.GetFullPath(UTILS.logFilePath)) + + // Open the log file in append mode if it exists, otherwise create a new file + if (exists) { + logFileObject.close() + logFileObject.open('append') + } else { + logFileObject.open('write') + } + } +} + +// Ends logging by closing the log file handle if separate logging is enabled +UTILS.TerminateLogging = function () { + if (UTILS.createSeparateLogFile === true) { + logFileObject.close() + } +} + +// This logs any provided information +UTILS.Log = function (log) { + var logText + + // Convert non-string logs to JSON format + if (log === undefined) { + logText = '' + } else if (typeof primaryLog !== 'string') { + logText = JSON.stringify(log) + } else { + logText = log + } + + if (UTILS.createSeparateLogFile === true) { + + // Write to file if log file object exists + if (logFileObject) { + if (UTILS.Logs.length > 0) { + for (var itr = 0; itr < UTILS.Logs.length; itr++) { + logFileObject.writeln(UTILS.Logs[itr]) + } + UTILS.Logs = [] + } + logFileObject.writeln(logText) + } else { + UTILS.Logs.push(logText) + } + } else { + UTILS.Logs.push(logText) + } +} + +// Writes data to a uniquely named JSON file +UTILS.WriteToFile = function (data, fileName) { + var fileURL, newFile + var exists = false + var suffix = '' + var counter = 1 + + // Generate a unique filename if none is provided + if (fileName === undefined) { + fileName = UTILS.GetUniqueName() + } + + // Ensure the file does not already exist + do { + fileURL = UTILS.GetFullPath(fileName + suffix + '.json') + newFile = File(fileURL) + if (newFile.open('read')) { + exists = true + suffix = counter++ + newFile.close() + } else { + exists = false + } + } while (exists) + + // Write data to the file + newFile.encoding = 'UTF8' + newFile.open('write') + if (newFile.write(JSON.stringify(data))) { + UTILS.Log('Data was successfully written') + newFile.close() + return (fileName + suffix + '.json') + } else { + UTILS.Log('Data write failed') + newFile.close() + } +} + +// Closes all open documents in the application. +UTILS.CloseAllOpenDocuments = function () { + UTILS.Log('Closing all the documents') + app.documents.everyItem().close(SaveOptions.NO) +} diff --git a/SampleScripts/ExtendScript/hyphenation/README.md b/SampleScripts/ExtendScript/hyphenation/README.md new file mode 100644 index 0000000..c9a3ec1 --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/README.md @@ -0,0 +1,125 @@ +# Hyphenate Capability + +This directory contains a script for managing hyphenation in Adobe InDesign documents using ExtendScript. The script is designed to work with InDesign versions 16.0.1 and above. + +## Directory Contents + +``` +hyphenation/ +├── manifest.json # Capability manifest file +├── hyphenate.jsx # Main script implementation +├── utils.jsx # Utility functions +├── errors.jsx # Error handling definitions +└── json2.jsx # JSON parsing utilities +``` + +## Files Description + +### 1. `manifest.json` +The manifest file defines the capability configuration: +- Supports InDesign versions 16.0.1 and above +- Implements an ExtendScript capability entry point +- Version: 1.0.0 + +### 2. `hyphenate.jsx` +The main script file that implements the hyphenation functionality. Features include: +- Text frame identification by labels +- Hyphenation control for specified paragraphs +- Document link management +- Performance monitoring +- Comprehensive error handling + +### 3. Helper Files +- `utils.jsx`: Contains utility functions for document processing and logging +- `errors.jsx`: Defines error types and messages for consistent error handling +- `json2.jsx`: Provides JSON parsing and manipulation utilities + +## Prerequisites + +- Adobe InDesign (version 16.0.1 or higher) +- ExtendScript environment +- Access to InDesign Services +- Appropriate file system permissions + +## Installation + +1. Create a capability bundle (ZIP file) containing all JSX files: + ``` + Archive.zip + ├── manifest.json + ├── hyphenate.jsx + ├── utils.jsx + ├── errors.jsx + └── json2.jsx + ``` + Note: All files must be zipped directly without a parent folder. + +2. Register your script following the [InDesign Capabilities API guide](https://developer.adobe.com/firefly-services/docs/indesign-apis/how-tos/working-with-capabilities-api/) + +## Usage + +### Required Parameters + +Provide the following JSON parameters to run the script: + +```json +{ + "assets": [ + { + "source": { + "url": "" + }, + "destination": "sample.indd" + } + ], + "params": { + "targetDocument": "sample.indd", + "outputPath": "hyphenate.indd", + "labels": ["MainPara", "Heading"], // Optional: If not provided, affects all text frames + "hyphenate": "true" // Can be "true" or "false" + } +} +``` + +### Parameter Details +- `labels`: Array of text frame labels to process (optional) +- `hyphenate`: Boolean value to enable/disable hyphenation +- `targetDocument`: Input InDesign document path +- `outputPath`: Output document path + +## Logging and Monitoring + +### Log File +The script generates detailed logs in `LogFile.txt` including: +- Document processing steps +- Text frame identification +- Hyphenation operations +- Performance metrics +- Error messages and warnings + +### Performance Metrics +- Document opening time +- Link update time +- Hyphenation processing time +- Total execution duration + +## Error Handling + +The script includes comprehensive error handling for various scenarios: +- Parameter validation errors +- Document processing errors +- Text frame identification issues +- Link management problems +- File system operations + +Each error includes: +- Error code +- Detailed error message +- Context information + +## Support + +For additional information and support: +- Refer to the [Adobe InDesign APIs documentation](https://developer.adobe.com/firefly-services/docs/indesign-apis/) +- Check the error messages in the log file for troubleshooting +- Ensure all prerequisites are met before running the script diff --git a/SampleScripts/ExtendScript/hyphenation/errors.jsx b/SampleScripts/ExtendScript/hyphenation/errors.jsx new file mode 100644 index 0000000..e90052c --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/errors.jsx @@ -0,0 +1,160 @@ +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ +// errors.jsx + +// Error constants are defined here. +/* This document list all the error which are possible from the scripts. The error object is built such that it has an error code and some error strings. + The error strings are constructed in a way such that the first string is default and a string literal. It is to be returned anyhow. The subsequent strings can be + strings having '^1' as placeholder replacement string. This replacement string can be replaced with relevant information. Based on the information + available the final error message can be created. +*/ +/* eslint-disable no-unused-vars */ + +var ErrorReplacementString = '^1' + +var Errors = { + // Errors during processing: 1001-1999 + InternalScriptError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal script error. This should not had happened.' + ] + }, + ProcessingErrorOccurred: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Error during processing.' + ] + }, + PDFPresetNotSet: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: No PDF Preset could be set.' + ] + }, + OutputDirError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Unable to create specified output directory.' + ] + }, + RelinkError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to relink.', + 'Relink failed for ^1.' + ] + }, + PlaceError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to place.', + 'Place failed for ^1.' + ] + }, + + // Errors in input: 2001-2999 + ArrayExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected array ', + 'for property ^1.' + ] + }, + ParsingBoolError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected boolean ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingIntError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected integer ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingFloatError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected number ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingStringError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected string ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + OutOfBound: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Incorrect range provided. ', + 'Culprit Value is ^1.' + ] + }, + MissingKey: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Key not found in object. ', + 'Missing key is ^1.' + ] + }, + EnumError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: No matching enum value found. ', + '^1 is the provided value', + 'for property ^1.' + ] + }, + MissingParams: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Either the \'params\' is not found in the request or it is not in the correct format.' + ] + }, + ObjectExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected object ', + 'for property ^1.' + ] + }, + AtLeastOneInternalParamShouldBePresent: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: At least one of the parameters should be present. ', + 'Missing parameters are ^1' + ] + }, + + // Errors coming from IDS and sent under ProcessingErrorOccurred. These are specific errors which need string change while returning to the user. + CannotOpenFileError: { + errorCode: 'capability_error', // kCannotOpenFileError + errorStrings: [ + 'Capability error: Cannot open file.' + ] + } +} diff --git a/SampleScripts/ExtendScript/hyphenation/hyphenate.jsx b/SampleScripts/ExtendScript/hyphenation/hyphenate.jsx new file mode 100644 index 0000000..8e643ac --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/hyphenate.jsx @@ -0,0 +1,325 @@ +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ + + +// Following are the file inclusions and not comments. +// @include "errors.jsx" +// @include "json2.jsx" +// @include "utils.jsx" +// File inclusion ends. + +// Globals: define any globals here. +var timingObject = {} +var warnings = {} +var document +// Globals end. + +// Constant strings for key references +var KeyStrings = { + OutputPath: 'outputPath', + ProcessedData: 'processedData', + TargetDocument: 'targetDocument', + TimeDocumentClose: 'DocumentClose', + TimeDocumentOpen: 'DocumentOpen', + TimeBalancing: 'TimeBalanceRagged', + TimeOverall: 'Overall', + TimeRelinkAssets: 'RelinkAssets', + Timings: 'timings', + WorkingFolder: 'workingFolder' +} + +/* Handles document warnings such as missing links or fonts */ +function handleWarnings (document) { + UTILS.Log('Handling warnings') + var errorlog = app.errorListErrors + var warningsObject = {} + + var missingLinkArray = [] + var missingFontArray = [] + var failedErrorsArray = [] + var otherWarningsArray = [] + var missingLinkStr = '(Link missing.; ' + var missingLinkStrLen = missingLinkStr.length + + // Iterate through error list and categorize warnings + for (var i = 0; i < errorlog.count(); i++) { + var error = errorlog[i] + UTILS.Log('Warning No. ' + (i + 1) + ': ' + error.listErrorCode + '(' + error.listErrorMessage + ')') + if (error.listErrorCode === 35842) { // Missing link error + var missingLink = error.listErrorMessage + missingLink = missingLink.substring(missingLink.indexOf(missingLinkStr) + missingLinkStrLen) + if (missingLinkArray.indexOf(missingLink) === -1) { + missingLinkArray.push(missingLink) + } + } else if (error.listErrorCode === 1 && error.listErrorMessage.search(/missing font/i) !== -1) { + var missingFont = error.listErrorMessage + missingFont = missingFont.substring(missingFont.search(/missing font/i) + 13) + if (missingFontArray.indexOf(missingFont) === -1) { + missingFontArray.push(missingFont) + } + } else if (error.listErrorCode === 1) { + otherWarningsArray.push(error.listErrorMessage) + } + } + + // Store collected warnings in an object + if (missingLinkArray.length > 0) { + warningsObject.missingLinks = missingLinkArray + } + if (missingFontArray.length > 0) { + warningsObject.missingFonts = missingFontArray + } + if (failedErrorsArray.length > 0) { + warningsObject.exportErrors = failedErrorsArray + } + if (otherWarningsArray.length > 0) { + warningsObject.otherWarnings = otherWarningsArray + } + return warningsObject +}; + +/* Hypenation for text frames within a document */ +function hyphenation (document,labels,hyphenate) { + + var allFrames = document.allPageItems; + var labelledFrames = [] + + UTILS.Log('Inside Hyphenate') + + // Convert labels to an object for faster lookups (Set is not available in ExtendScript) + var labelMap = {}; + if (labels && labels.length > 0) { + if (!(labels instanceof Array)) { + labels = [labels]; // Convert single label to an array + } + for (var i = 0; i < labels.length; i++) { + labelMap[labels[i]] = true; + } + + // Iterate over frames and find those matching the labels + for (var i = 0; i < allFrames.length; i++) { + var frame = allFrames[i]; + if (frame instanceof TextFrame && labelMap[frame.label]) { + labelledFrames.push(frame); + } + } + } else { + // If no labels provided, select all text frames + for (var i = 0; i < allFrames.length; i++) { + if (allFrames[i] instanceof TextFrame) { + labelledFrames.push(allFrames[i]); + } + } + } + UTILS.Log('Labels Extracted ' + labelledFrames.length) + + + // Hyphenation for all paragraphs in the labelled frames + if(labelledFrames.length == 0){ + UTILS.Log('No frames found') + return + } + // Hyphenation for all paragraphs in the labelled frames + for (var i = 0; i < labelledFrames.length; i++) { + var textFrame = labelledFrames[i]; + var paragraphs = textFrame.paragraphs; + for (var j = 0; j < paragraphs.length; j++) { + var para = paragraphs[j]; + if(hyphenate===true){ + para.hyphenation = true; + } + else{ + para.hyphenation = false + } + + } + } + document.save(); +}; + +/* Processes parameters and performs document operations */ +function ProcessParams (parameters) { + UTILS.Log('Processing parameters internal') + var returnVal = {} + + // Open the target document + var documentPath = UTILS.GetStringFromObject(parameters, KeyStrings.TargetDocument) + documentPath = UTILS.GetFullPath(documentPath) + + UTILS.Log('Opening document') + var tempTime = new Date().getTime() + document = app.open(File(documentPath)) + timingObject[KeyStrings.TimeDocumentOpen] = (new Date()).getTime() - tempTime + UTILS.Log('Opened document') + + // Relink assets and handle warnings + tempTime = new Date().getTime() + UTILS.UpdateDocumentLinks(document) + timingObject[KeyStrings.TimeRelinkAssets] = (new Date()).getTime() - tempTime + UTILS.Log('Updated links in the document') + + // Capturing top level errors and then clear errorList. + UTILS.Log('Number of errors: ' + app.errorListErrors.count()) + if (app.errorListErrors.count() > 0) { + warnings = handleWarnings(document) + } + app.clearAllErrors() + UTILS.Log('Got the warnings from the document.') + + var outputPath = UTILS.GetStringFromObject(parameters, KeyStrings.OutputPath) + outputPath = UTILS.GetFullPath(outputPath) + + tempTime = new Date().getTime() + UTILS.UpdateDocumentLinks(document) + + var labels = parameters.labels + var hyphenate = parameters.hyphenate + + // Calling the function to hyphenate the text frames + hyphenation(document,labels,hyphenate); + + // Save the document as a new file. + document.save(File(outputPath)) + + timingObject[KeyStrings.TimeBalancing] = (new Date()).getTime() - tempTime + + // Add file to be uploaded as an output file. + var relativePath = UTILS.GetRelativeReturnPath(outputPath) + UTILS.AddAssetToBeUploaded(relativePath) + + returnVal.OutputPath = documentPath + // Processing ends. + + return returnVal +} + + +/* Main function to execute the script */ +function main () { + + var startTime = new Date().getTime() // Capture the script start time + var returnObj = {} // Object to store return values + var parameters = {} // Stores input parameters + var tempTime + var errorOccurred = false // Flag to track errors + var data = {} // Stores processed data + + // Set application preferences + app.clearAllErrors() + app.generalPreferences.pageNumbering = PageNumberingOptions.ABSOLUTE + app.linkingPreferences.checkLinksAtOpen = true + app.serverSettings.useErrorList = true + var previousUnit = app.scriptPreferences.measurementUnit + app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS + + try { + UTILS.InitiateLogging() + UTILS.Log('Initiating logging.') + + // As a safe practice close any open documents before processing + UTILS.CloseAllOpenDocuments() + + // Parse input parameters + var input = app.scriptArgs.get('parameters') + var allParameters = JSON.parse(input) + + UTILS.Log('Parsed job input : ' + input) + parameters = allParameters.params + if (parameters === undefined) { + parameters = allParameters.input.params + } + + // Check if parameters are valid + if (parameters === undefined || typeof parameters !== 'object' || Array.isArray(parameters)) { + UTILS.Log('No params found') + UTILS.RaiseException(Errors.MissingParams) + } + + // Set the working folder first. This is the directory within which all the input and output assets are to be managed. + UTILS.SetWorkingFolder(UTILS.GetStringFromObject(allParameters, KeyStrings.WorkingFolder)) + + var result + UTILS.Log('Processing Params') + tempTime = new Date().getTime() + + // Process parameters and document + result = ProcessParams(parameters) + data[KeyStrings.ProcessedData] = result + + // Also add the log file + UTILS.AddAssetToBeUploaded(UTILS.logFilePath) + + data.warnings = warnings + // Processing ends. + + tempTime = new Date().getTime() + if (document && document.isValid){ + document.close() + } + + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + UTILS.Log('End of try') + } catch (e) { + var tempObj = { + name: e.name, + message: e.message, + errorCode: e.number, + isCustom: e.isCustom, + line: e.line, + fileName: e.fileName + } + + UTILS.Log('Exception occurred', tempObj) + errorOccurred = true + + // Failure, prepare the object to be returned. + returnObj = UTILS.HandleError(tempObj) + } finally { + app.scriptPreferences.measurementUnit = previousUnit + UTILS.Log('In finally') + if (document && document.isValid) { + + // If Document is still open. Close it. + UTILS.Log('Closing document') + tempTime = new Date().getTime() + document.close() + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + } + + var elapsedTime = (new Date()).getTime() - startTime + UTILS.Log('Time taken: ' + elapsedTime) + timingObject[KeyStrings.TimeOverall] = elapsedTime + UTILS.Log('Timing: ' + JSON.stringify(timingObject)) + data[KeyStrings.Timings] = timingObject + + if (!errorOccurred) { + // Success, prepare the object to be returned. + UTILS.Log('Finally: No error') + returnObj = UTILS.GetSuccessReturnObj(data) + } + UTILS.Log('Final Result', JSON.stringify(returnObj)) + + // Cleanup and Return + UTILS.TerminateLogging() + app.clearAllErrors() + + return UTILS.GetFinalReturnPackage(returnObj) + } +} + +// Execute the main function +main() diff --git a/SampleScripts/ExtendScript/hyphenation/json2.jsx b/SampleScripts/ExtendScript/hyphenation/json2.jsx new file mode 100644 index 0000000..397349b --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/json2.jsx @@ -0,0 +1,530 @@ +// json2.js +// 2017-06-12 +// Public Domain. +// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO +// NOT CONTROL. + +// This file creates a global JSON object containing two methods: stringify +// and parse. This file provides the ES5 JSON capability to ES3 systems. +// If a project might run on IE8 or earlier, then this file should be included. +// This file does nothing on ES5 systems. + +// JSON.stringify(value, replacer, space) +// value any JavaScript value, usually an object or array. +// replacer an optional parameter that determines how object +// values are stringified for objects. It can be a +// function or an array of strings. +// space an optional parameter that specifies the indentation +// of nested structures. If it is omitted, the text will +// be packed without extra whitespace. If it is a number, +// it will specify the number of spaces to indent at each +// level. If it is a string (such as "\t" or " "), +// it contains the characters used to indent at each level. +// This method produces a JSON text from a JavaScript value. +// When an object value is found, if the object contains a toJSON +// method, its toJSON method will be called and the result will be +// stringified. A toJSON method does not serialize: it returns the +// value represented by the name/value pair that should be serialized, +// or undefined if nothing should be serialized. The toJSON method +// will be passed the key associated with the value, and this will be +// bound to the value. + +// For example, this would serialize Dates as ISO strings. + +// Date.prototype.toJSON = function (key) { +// function f(n) { +// // Format integers to have at least two digits. +// return (n < 10) +// ? "0" + n +// : n; +// } +// return this.getUTCFullYear() + "-" + +// f(this.getUTCMonth() + 1) + "-" + +// f(this.getUTCDate()) + "T" + +// f(this.getUTCHours()) + ":" + +// f(this.getUTCMinutes()) + ":" + +// f(this.getUTCSeconds()) + "Z"; +// }; + +// You can provide an optional replacer method. It will be passed the +// key and value of each member, with this bound to the containing +// object. The value that is returned from your method will be +// serialized. If your method returns undefined, then the member will +// be excluded from the serialization. + +// If the replacer parameter is an array of strings, then it will be +// used to select the members to be serialized. It filters the results +// such that only members with keys listed in the replacer array are +// stringified. + +// Values that do not have JSON representations, such as undefined or +// functions, will not be serialized. Such values in objects will be +// dropped; in arrays they will be replaced with null. You can use +// a replacer function to replace those with JSON values. + +// JSON.stringify(undefined) returns undefined. + +// The optional space parameter produces a stringification of the +// value that is filled with line breaks and indentation to make it +// easier to read. + +// If the space parameter is a non-empty string, then that string will +// be used for indentation. If the space parameter is a number, then +// the indentation will be that many spaces. + +// Example: + +// text = JSON.stringify(["e", {pluribus: "unum"}]); +// // text is '["e",{"pluribus":"unum"}]' + +// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); +// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + +// text = JSON.stringify([new Date()], function (key, value) { +// return this[key] instanceof Date +// ? "Date(" + this[key] + ")" +// : value; +// }); +// // text is '["Date(---current time---)"]' + +// JSON.parse(text, reviver) +// This method parses a JSON text to produce an object or array. +// It can throw a SyntaxError exception. + +// The optional reviver parameter is a function that can filter and +// transform the results. It receives each of the keys and values, +// and its return value is used instead of the original value. +// If it returns what it received, then the structure is not modified. +// If it returns undefined then the member is deleted. + +// Example: + +// // Parse the text. Values that look like ISO date strings will +// // be converted to Date objects. + +// myData = JSON.parse(text, function (key, value) { +// var a; +// if (typeof value === "string") { +// a = +// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); +// if (a) { +// return new Date(Date.UTC( +// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] +// )); +// } +// return value; +// } +// }); + +// myData = JSON.parse( +// "[\"Date(09/09/2001)\"]", +// function (key, value) { +// var d; +// if ( +// typeof value === "string" +// && value.slice(0, 5) === "Date(" +// && value.slice(-1) === ")" +// ) { +// d = new Date(value.slice(5, -1)); +// if (d) { +// return d; +// } +// } +// return value; +// } +// ); + +// This is a reference implementation. You are free to copy, modify, or +// redistribute. + +/*jslint + eval, for, this +*/ + +/*property + JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== "object") { + JSON = {}; +} + +(function () { + "use strict"; + + var rx_one = /^[\],:{}\s]*$/; + var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; + var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var rx_four = /(?:^|:|,)(?:\s*\[)+/g; + var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + function f(n) { + // Format integers to have at least two digits. + return (n < 10) + ? "0" + n + : n; + } + + function this_value() { + return this.valueOf(); + } + + if (typeof Date.prototype.toJSON !== "function") { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? ( + this.getUTCFullYear() + + "-" + + f(this.getUTCMonth() + 1) + + "-" + + f(this.getUTCDate()) + + "T" + + f(this.getUTCHours()) + + ":" + + f(this.getUTCMinutes()) + + ":" + + f(this.getUTCSeconds()) + + "Z" + ) + : null; + }; + + Boolean.prototype.toJSON = this_value; + Number.prototype.toJSON = this_value; + String.prototype.toJSON = this_value; + } + + var gap; + var indent; + var meta; + var rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + rx_escapable.lastIndex = 0; + return rx_escapable.test(string) + ? "\"" + string.replace(rx_escapable, function (a) { + var c = meta[a]; + return typeof c === "string" + ? c + : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); + }) + "\"" + : "\"" + string + "\""; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i; // The loop counter. + var k; // The member key. + var v; // The member value. + var length; + var mind = gap; + var partial; + var value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if ( + value + && typeof value === "object" + && typeof value.toJSON === "function" + ) { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === "function") { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case "string": + return quote(value); + + case "number": + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return (isFinite(value)) + ? String(value) + : "null"; + + case "boolean": + case "null": + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce "null". The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is "object", we might be dealing with an object or an array or +// null. + + case "object": + +// Due to a specification blunder in ECMAScript, typeof null is "object", +// so watch out for that case. + + if (!value) { + return "null"; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === "[object Array]") { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || "null"; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? "[]" + : gap + ? ( + "[\n" + + gap + + partial.join(",\n" + gap) + + "\n" + + mind + + "]" + ) + : "[" + partial.join(",") + "]"; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === "object") { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === "string") { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? "{}" + : gap + ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" + : "{" + partial.join(",") + "}"; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== "function") { + meta = { // table of character substitutions + "\b": "\\b", + "\t": "\\t", + "\n": "\\n", + "\f": "\\f", + "\r": "\\r", + "\"": "\\\"", + "\\": "\\\\" + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ""; + indent = ""; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === "number") { + for (i = 0; i < space; i += 1) { + indent += " "; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === "string") { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== "function" && ( + typeof replacer !== "object" + || typeof replacer.length !== "number" + )) { + throw new Error("JSON.stringify"); + } + +// Make a fake root object containing our value under the key of "". +// Return the result of stringifying the value. + + return str("", {"": value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== "function") { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k; + var v; + var value = holder[key]; + if (value && typeof value === "object") { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with "()" and "new" +// because they can cause invocation, and "=" because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we +// replace all simple value tokens with "]" characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or "]" or +// "," or ":" or "{" or "}". If that is so, then the text is safe for eval. + + if ( + rx_one.test( + text + .replace(rx_two, "@") + .replace(rx_three, "]") + .replace(rx_four, "") + ) + ) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The "{" operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval("(" + text + ")"); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return (typeof reviver === "function") + ? walk({"": j}, "") + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError("JSON.parse"); + }; + } +}()); \ No newline at end of file diff --git a/SampleScripts/ExtendScript/hyphenation/manifest.json b/SampleScripts/ExtendScript/hyphenation/manifest.json new file mode 100644 index 0000000..46bf820 --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/manifest.json @@ -0,0 +1,17 @@ +{ + "manifestVersion": "1.0.0", + "name": "hyphenate", + "host": { + "app": "indesign", + "maxVersion": "99.9.9", + "minVersion": "16.0.1" + }, + "version": "1.0.0", + "apiEntryPoints": [ + { + "path": "hyphenate.jsx", + "type": "capability", + "language": "extendscript" + } + ] +} \ No newline at end of file diff --git a/SampleScripts/ExtendScript/hyphenation/utils.jsx b/SampleScripts/ExtendScript/hyphenation/utils.jsx new file mode 100644 index 0000000..817c332 --- /dev/null +++ b/SampleScripts/ExtendScript/hyphenation/utils.jsx @@ -0,0 +1,702 @@ +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ +// utils.jsx + +// Utils functions which can be used across. +/* globals app, Errors, File, Folder, LinkStatus, SaveOptions */ + +// Globals. +var logFileObject + +// eslint-disable-next-line no-extend-native +Array.prototype.indexOf = function (item) { + var index = 0 + var length = this.length + for (; index < length; index++) { + if (this[index] === item) { + return index + } + } + return -1 +} + +// eslint-disable-next-line no-extend-native +if (typeof Array.isArray === 'undefined') { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]' + } +} + +// Create a UTILS object to store utility functions and variables. +var UTILS = {} + +// Initialize various properties of the UTILS object. +UTILS.Logs = [] // Stores log messages. +UTILS.workingFolder = '' // Path of the working folder. +UTILS.outputFolder = '' // Path of the output folder. +UTILS.assetsToBeUploaded = [] // List of assets that need to be uploaded. +UTILS.createSeparateLogFile = true // Flag to determine if a separate log file should be created. +UTILS.logFilePath = 'LogFile.txt' // File name for logging. + +// Converts the package in appropriate format, which can be returned from the capability. +UTILS.GetFinalReturnPackage = function (obj) { + UTILS.Log('Final package created') + return JSON.stringify(obj) +} + +// Creates and returns the package to be returned in case the job is successful. +UTILS.GetSuccessReturnObj = function (data) { + var obj = {} + obj.status = 'SUCCESS' + obj.assetsToBeUploaded = UTILS.assetsToBeUploaded // Attach the list of assets to be uploaded. + var dataURL = UTILS.WriteToFile(data, 'outputData') // Save data to file. + if (dataURL) { + obj.dataURL = dataURL + } + return obj +} + +// Creates and returns the package to be returned in case the job has failed. +UTILS.GetFailureReturnObj = function (errorCode, errorString, data) { + var obj = {} + obj.status = 'FAILURE' + obj.errorCode = errorCode // Attach the error code. + obj.errorString = errorString // Attach the error message. + obj.data = data // Include additional data if available. + return obj +} + +// Add an asset which is to be uploaded and sent back to the caller. +UTILS.AddAssetToBeUploaded = function (assetPath, data) { + var assetToBeUploaded = {} + assetToBeUploaded.path = assetPath + if (data !== undefined) { + assetToBeUploaded.data = data // Include additional asset data if provided. + } + UTILS.assetsToBeUploaded.push(assetToBeUploaded) +} + +// This handles errors in case an exception is raised. This results the capability returning error to the caller. +UTILS.HandleError = function (exception) { + var errorCode, errorString + UTILS.Log('Exception occurred: ' + JSON.stringify(exception)) + + // Handle specific exception cases + if (exception.message === 'open') { + exception.message = Errors.CannotOpenFileError.errorStrings[0] + exception.errorCode = Errors.CannotOpenFileError.errorCode + } + + errorString = 'Script error: ' + if (exception.isCustom === true) { + if (exception.message) { + errorString = errorString + exception.message + } + errorCode = exception.errorCode + } else { + errorCode = Errors.ProcessingErrorOccurred.errorCode + if (exception.message) { + errorString = errorString + Errors.ProcessingErrorOccurred.errorStrings[0] + ' ' + exception.message + } + if (exception.errorCode !== undefined) { + errorString = errorString + '\nInternal error code: ' + exception.errorCode + '.' + } + if (exception.line !== undefined) { + errorString = errorString + ' Line: ' + exception.line + '.' + } + if (exception.fileName !== undefined) { + errorString = errorString + ' FileName: ' + exception.fileName + '.' + } + UTILS.Log('Processing error occurred. ' + errorString) + } + + return UTILS.GetFailureReturnObj(errorCode, errorString) +} + +// This is used to raise an exception with all the required details. +// NOTE: This takes variable list of argument and then try to fill in the details in errorObj.errorStrings +UTILS.RaiseException = function (errorObj) { + UTILS.Log('RaiseException()') + var numMessageParameters = arguments.length + UTILS.Log('', 'numMessageParameters: ' + numMessageParameters) + var numErrorStrings = errorObj.errorStrings.length + UTILS.Log('', 'numErrorStrings: ' + numErrorStrings) + var numIterations = (numMessageParameters > numErrorStrings) ? numErrorStrings : numMessageParameters + var errorMessage = errorObj.errorStrings[0] + UTILS.Log('', 'Default errorMessage: ' + errorMessage) + + // Construct the detailed error message using available parameters. + for (var itr = 1; itr < numIterations; itr++) { + var parameter = arguments[itr] + if (parameter !== undefined && parameter !== '') { + var parameterMessage = errorObj.errorStrings[itr] + errorMessage = errorMessage + ' ' + parameterMessage.replace('^1', parameter) + UTILS.Log('', 'Appended errorMessage: ' + errorMessage) + } + } + + // Throw an exception object with custom details. + var exceptionObj = { + number: errorObj.errorCode, + isCustom: true, + message: errorMessage + } + + throw exceptionObj +} + +// This will update out-of-date links in a document. +UTILS.UpdateDocumentLinks = function (document) { + var links = document.links + var numLinks = links.length + var linkItr, link, uri + var outOfDateLinks = [] + + UTILS.Log('Number of links: ' + numLinks) + for (linkItr = 0; linkItr < numLinks; linkItr++) { + try { + link = links[linkItr] + uri = link.linkResourceURI + UTILS.Log(linkItr + ': URI: ' + uri) + if (link.status === LinkStatus.LINK_OUT_OF_DATE) { + outOfDateLinks.push(link.id) + } + } catch (err) { + UTILS.Log('Link status unknown : ' + err) + } + } + + // Update all out-of-date links. + numLinks = outOfDateLinks.length + for (linkItr = 0; linkItr < numLinks; linkItr++) { + link = document.links.itemByID(outOfDateLinks[linkItr]) + if (link.isValid) { + link.update() + } + } + + // Recompose the document after updating links. + var composeStartTime = new Date().getTime() + document.recompose() + var composedTime = (new Date()).getTime() - composeStartTime + UTILS.Log('document.recompose Time: ' + composedTime) +} + +// This Embeds all the links in the document. +UTILS.EmbedDocumentLinks = function (document) { + var links = document.links + var numLinks = links.length + var linkItr, link + + for (linkItr = 0; linkItr < numLinks; linkItr++) { + try { + link = links[linkItr] + if (link.status === LinkStatus.NORMAL) { + link.unlink() + } + } catch (err) { + UTILS.Log('Unable to embed link: status is unknown : ' + err) + } + } +} + +// Trims leading and trailing spaces from a string. +UTILS.Trim = function (val) { + return val.replace(/^\s+|\s+$/gm, '') +} + +// Removes all the spaces from a string. +UTILS.RemoveAllSpaces = function (val) { + return val.replace(/\s/g, '') +} + +// Converts a value to integer. Exception is thrown if the conversion fails. +UTILS.GetBoolean = function (val) { + if (val === 'true' || val === true) { + return true + } else if (val === 'false' || val === false) { + return false + } + + // throw exception. + UTILS.Log('GetBoolean() val: ' + val) + UTILS.RaiseException(Errors.ParsingBoolError, val) +} + +// Gets a boolean from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetBooleanFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetBooleanFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (val === 'true' || val === true) { + return true + } else if (val === 'false' || val === false) { + return false + } + + UTILS.Log('GetBooleanFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingBoolError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Converts a value to integer. Exception is thrown if the conversion fails. +UTILS.GetInteger = function (val) { + var returnVal = parseInt(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetInteger() val: ' + val) + UTILS.RaiseException(Errors.ParsingIntError, val) +} + +// Gets a integer from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetIntegerFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetIntegerFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + var returnVal = parseInt(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetIntegerFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingIntError, val, key) + } else if (defaultVal === undefined) { + UTILS.Log('Missing key: ' + key) + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Converts a value to float. Exception is thrown if the conversion fails. +UTILS.GetFloat = function (val) { + var returnVal = parseFloat(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetFloat() val: ' + val) + UTILS.RaiseException(Errors.ParsingFloatError, val) +} + +// Gets a float from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the conversion of the provided value fails. +UTILS.GetFloatFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetFloatFromObject ' + key) + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + var returnVal = parseFloat(val) + if (!isNaN(returnVal)) { + return returnVal + } + + UTILS.Log('GetFloatFromObject() val: ' + val) + UTILS.RaiseException(Errors.ParsingFloatError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Validates a value to be one of the enum definitions. +UTILS.GetEnum = function (enumDef, val) { + try { + val = UTILS.Trim(val) + return UTILS.GetValueFromObject(enumDef, val) + } catch (e) { + UTILS.RaiseException(Errors.EnumError, val) + } +} + +// Gets a enum value from an 'object' defined with 'key' and after comparing against an enum definition. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value is not present in enum definition. +UTILS.GetEnumFromObject = function (enumDef, object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetEnumFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + try { + val = UTILS.Trim(val) + if (Array.isArray(enumDef) === false) { + return UTILS.GetValueFromObject(enumDef, val, ignoreCase) + } else { + if (enumDef.indexOf(val) >= 0) { + return val + } + } + } catch (e) { + UTILS.Log('GetEnumFromObject() val: ' + val) + UTILS.RaiseException(Errors.EnumError, val, key) + } + + UTILS.Log('GetEnumFromObject() val: ' + val) + UTILS.RaiseException(Errors.EnumError, val, key) + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets a string from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not string. +UTILS.GetStringFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetStringFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (typeof val !== 'string') { + UTILS.RaiseException(Errors.ParsingStringError, val, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets an array from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not an array. +UTILS.GetArrayFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetArrayFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (Array.isArray(val) === false) { + UTILS.RaiseException(Errors.ArrayExpected, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets an object from an 'object' defined with 'key'. +// A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. +// In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not an object. +UTILS.GetObjectFromObject = function (object, key, defaultVal, ignoreCase) { + var val + UTILS.Log('GetObjectFromObject ' + key) + + if (UTILS.IsKeyPresent(object, key)) { + val = UTILS.GetValueFromObject(object, key, ignoreCase) + if (typeof val !== 'object' || Array.isArray(val)) { + UTILS.RaiseException(Errors.ObjectExpected, key) + } else { + return val + } + } else if (defaultVal === undefined) { + UTILS.RaiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } +} + +// Gets the value from an 'object' defined with 'key'. +// The case of key can be ignored via 'ignoreCase'. In case, key is missing an exception will be thrown. +UTILS.GetValueFromObject = function (object, key, ignoreCase) { + UTILS.Log('GetValueFromObject ' + key) + + if (ignoreCase === true) { + var objectKey + for (objectKey in object) { + if (Object.prototype.hasOwnProperty.call(object, objectKey)) { + if (objectKey.toLowerCase() === key.toLowerCase()) { + return object[objectKey] + } + } + } + } else if (Object.prototype.hasOwnProperty.call(object, key)) { + return object[key] + } + + UTILS.RaiseException(Errors.MissingKey, key) +} + +// Checks whether a key is present in the object or not. +UTILS.IsKeyPresent = function (object, key) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + return true + } + return false +} + +// Sets the working folder to be used across the capability. +UTILS.SetWorkingFolder = function (workingFolder) { + workingFolder = workingFolder.replace(/\//g, '\\') + if (workingFolder.charAt(workingFolder.length - 1) !== '\\') { + workingFolder = workingFolder + '\\' + } + UTILS.workingFolder = workingFolder + UTILS.OpenLogFileHandle() +} + +// Gets the path of the directory for a given path. +UTILS.GetDirPath = function (path) { + path = path.replace(/\//g, '\\') + var indexOfSlash = path && path.lastIndexOf('\\') + if (indexOfSlash >= 0) { + return path.substr(0, indexOfSlash) + } + return '' +} + +// Gets the path of the directory for a given path. If 'withExtension is true the file name consists of the file extension and not otherwise. +UTILS.GetFileName = function (path, withExtension) { + var fileName + var indexOfSlash = path.lastIndexOf('\\') + 1 + if (indexOfSlash >= 1) { + fileName = path.substr(indexOfSlash) + if (withExtension === false) { + var indexOfDot = path.lastIndexOf('.') + fileName = path.substr(indexOfSlash, indexOfDot - indexOfSlash) + } + } else { + fileName = path + } + return fileName +} + +// Get relative path to the working directory for a path. +UTILS.GetRelativePath = function (path) { + var index = path.indexOf(UTILS.workingFolder) + if (index === 0) { + return path.substr(UTILS.workingFolder.length) + } + return path +} + +// Get relative path to the working directory for a path. This returns in unix notation. +UTILS.GetRelativeReturnPath = function (path) { + var index = path.indexOf(UTILS.workingFolder) + if (index === 0) { + var tempPath = path.substr(UTILS.workingFolder.length) + tempPath = tempPath.replace(/\\/g, '/') + return tempPath + } +} + +// Get the full path from a relative and a base path. In absence of base path, working directory is used. +UTILS.GetFullPath = function (relativePath, basePath) { + var origRelativePath = relativePath + if (relativePath.indexOf('..') !== -1 || relativePath.indexOf('/') === 0) { + this.RaiseException(Errors.InCorrectRelativePath, origRelativePath) + } + relativePath = relativePath.replace(/\//g, '\\') + if (relativePath.indexOf('\\.\\') !== -1) { + this.RaiseException(Errors.InCorrectRelativePath, origRelativePath) + } + + if (basePath === undefined) { + basePath = UTILS.workingFolder + } else { + basePath = basePath.replace(/\//g, '\\') + if (basePath.charAt(basePath.length - 1) !== '\\') { + basePath = basePath + '\\' + } + } + + if (relativePath.charAt(0) === '.') { + relativePath = relativePath.slice(1) + } + if (relativePath.charAt(0) === '\\') { + relativePath = relativePath.slice(1) + } + + var fullPath = basePath + relativePath + return fullPath +} + +// Get a unique name. +UTILS.GetUniqueName = function () { + return Math.random().toString().substr(2, 6) +} + +// Creates a directory for the path provided. +UTILS.CreateDirectory = function (path) { + var outputFolder = Folder(path) + return outputFolder.create() +} + +// Creates a directory for the outputs on the basis of relative path passed. In case nothing is passed a random directory is created in working folder. +UTILS.GetOutputDirectory = function (outputFolderPath) { + var outputPath = '' + var created = false + if (outputFolderPath) { + outputPath = UTILS.GetFullPath(outputFolderPath) + created = UTILS.CreateDirectory(outputPath) + var outputFolder = Folder(outputPath) + if (outputFolder.exists === false) { + UTILS.RaiseException(Errors.OutputDirError) + } + } else { + var tempName = '' + do { + tempName = 'tmp' + UTILS.GetUniqueName() + outputPath = UTILS.GetFullPath(tempName) + created = UTILS.CreateDirectory(outputPath) + } while (!created) + } + UTILS.outputFolder = outputPath + return outputPath +} + +// Setup to do the logging. +UTILS.InitiateLogging = function () { + UTILS.Logs = [] +} + + +// Opens a log file handle if separate logging is enabled. +UTILS.OpenLogFileHandle = function () { + if (UTILS.createSeparateLogFile === true) { + logFileObject = new File(UTILS.GetFullPath(UTILS.logFilePath)) + var exists = false + + // Check if the log file already exists + if (logFileObject.open('read')) { + exists = true + } + UTILS.Log('Creating log file at ' + UTILS.GetFullPath(UTILS.logFilePath)) + + // Open the log file in append mode if it exists, otherwise create a new file + if (exists) { + logFileObject.close() + logFileObject.open('append') + } else { + logFileObject.open('write') + } + } +} + +// Ends logging by closing the log file handle if separate logging is enabled +UTILS.TerminateLogging = function () { + if (UTILS.createSeparateLogFile === true) { + logFileObject.close() + } +} + +// This logs any provided information +UTILS.Log = function (log) { + var logText + + // Convert non-string logs to JSON format + if (log === undefined) { + logText = '' + } else if (typeof primaryLog !== 'string') { + logText = JSON.stringify(log) + } else { + logText = log + } + + if (UTILS.createSeparateLogFile === true) { + + // Write to file if log file object exists + if (logFileObject) { + if (UTILS.Logs.length > 0) { + for (var itr = 0; itr < UTILS.Logs.length; itr++) { + logFileObject.writeln(UTILS.Logs[itr]) + } + UTILS.Logs = [] + } + logFileObject.writeln(logText) + } else { + UTILS.Logs.push(logText) + } + } else { + UTILS.Logs.push(logText) + } +} + +// Writes data to a uniquely named JSON file +UTILS.WriteToFile = function (data, fileName) { + var fileURL, newFile + var exists = false + var suffix = '' + var counter = 1 + + // Generate a unique filename if none is provided + if (fileName === undefined) { + fileName = UTILS.GetUniqueName() + } + + // Ensure the file does not already exist + do { + fileURL = UTILS.GetFullPath(fileName + suffix + '.json') + newFile = File(fileURL) + if (newFile.open('read')) { + exists = true + suffix = counter++ + newFile.close() + } else { + exists = false + } + } while (exists) + + // Write data to the file + newFile.encoding = 'UTF8' + newFile.open('write') + if (newFile.write(JSON.stringify(data))) { + UTILS.Log('Data was successfully written') + newFile.close() + return (fileName + suffix + '.json') + } else { + UTILS.Log('Data write failed') + newFile.close() + } +} + +// Closes all open documents in the application. +UTILS.CloseAllOpenDocuments = function () { + UTILS.Log('Closing all the documents') + app.documents.everyItem().close(SaveOptions.NO) +} diff --git a/SampleScripts/ExtendScript/idmlConversion/README.md b/SampleScripts/ExtendScript/idmlConversion/README.md new file mode 100644 index 0000000..f56d1b3 --- /dev/null +++ b/SampleScripts/ExtendScript/idmlConversion/README.md @@ -0,0 +1,122 @@ +# IDML Conversion Capability (ExtendScript) + +This directory contains an ExtendScript implementation for converting InDesign documents to IDML format. This is the ExtendScript version of the IDML conversion capability, designed to work with InDesign versions 16.0.1 and above. + +## Directory Contents + +``` +Example/ +├── manifest.json # Capability manifest file +├── sample.jsx # Main script implementation +├── utils.jsx # Utility functions +├── errors.jsx # Error handling definitions +└── json2.jsx # JSON parsing utilities +``` + +## Files Description + +### 1. `manifest.json` +The manifest file defines the capability configuration: +- Supports InDesign versions 16.0.1 and above +- Implements an ExtendScript capability entry point +- Version: 1.0.0 + +### 2. `sample.jsx` +The main script file that implements the IDML conversion functionality. Features include: +- InDesign document to IDML conversion +- Document link management +- Asset handling and processing +- Performance monitoring +- Comprehensive error handling +- Logging capabilities + +### 3. Helper Files +- `utils.jsx`: Contains utility functions for document processing and logging +- `errors.jsx`: Defines error types and messages for consistent error handling +- `json2.jsx`: Provides JSON parsing and manipulation utilities + +## Prerequisites + +- Adobe InDesign (version 16.0.1 or higher) +- ExtendScript environment +- Access to InDesign Services +- Appropriate file system permissions + +## Installation + +1. Create a capability bundle (ZIP file) containing all JSX files: + ``` + Archive.zip + ├── manifest.json + ├── sample.jsx + ├── utils.jsx + ├── errors.jsx + └── json2.jsx + ``` + Note: All files must be zipped directly without a parent folder. + +2. Register your script following the [InDesign Capabilities API guide](https://developer.adobe.com/firefly-services/docs/indesign-apis/how-tos/working-with-capabilities-api/) + +## Usage + +### Required Parameters + +Provide the following JSON parameters to run the script: + +```json +{ + "assets": [ + { + "source": { + "url": "" + }, + "destination": "sample.indd" + } + ], + "params": { + "targetDocument": "sample.indd", + "outputPath": "converted.idml" + } +} +``` + +### Parameter Details +- `targetDocument`: Input InDesign document path +- `outputPath`: Output IDML file path + +## Logging and Monitoring + +### Log File +The script generates detailed logs in `LogFile.txt` including: +- Document processing steps +- IDML conversion progress +- Asset processing status +- Performance metrics +- Error messages and warnings + +### Performance Metrics +- Document opening time +- IDML conversion duration +- Asset processing time +- Total execution time + +## Error Handling + +The script includes comprehensive error handling for various scenarios: +- Parameter validation errors +- Document processing errors +- IDML conversion failures +- Asset handling issues +- File system operations + +Each error includes: +- Error code +- Detailed error message +- Context information + +## Support + +For additional information and support: +- Refer to the [Adobe InDesign APIs documentation](https://developer.adobe.com/firefly-services/docs/indesign-apis/) +- Check the error messages in the log file for troubleshooting +- Ensure all prerequisites are met before running the script diff --git a/Example/errors.jsx b/SampleScripts/ExtendScript/idmlConversion/errors.jsx similarity index 82% rename from Example/errors.jsx rename to SampleScripts/ExtendScript/idmlConversion/errors.jsx index 2bf324f..459bb79 100644 --- a/Example/errors.jsx +++ b/SampleScripts/ExtendScript/idmlConversion/errors.jsx @@ -1,11 +1,28 @@ -// Error constants are defined here. -/* eslint-disable no-unused-vars */ +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ +// errors.jsx +// Error constants are defined here. /* This document list all the error which are possible from the scripts. The error object is built such that it has an error code and some error strings. The error strings are constructed in a way such that the first string is default and a string literal. It is to be returned anyhow. The subsequent strings can be strings having '^1' as placeholder replacement string. This replacement string can be replaced with relevant information. Based on the information available the final error message can be created. */ +/* eslint-disable no-unused-vars */ var ErrorReplacementString = '^1' diff --git a/SampleScripts/ExtendScript/idmlConversion/json2.jsx b/SampleScripts/ExtendScript/idmlConversion/json2.jsx new file mode 100644 index 0000000..397349b --- /dev/null +++ b/SampleScripts/ExtendScript/idmlConversion/json2.jsx @@ -0,0 +1,530 @@ +// json2.js +// 2017-06-12 +// Public Domain. +// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO +// NOT CONTROL. + +// This file creates a global JSON object containing two methods: stringify +// and parse. This file provides the ES5 JSON capability to ES3 systems. +// If a project might run on IE8 or earlier, then this file should be included. +// This file does nothing on ES5 systems. + +// JSON.stringify(value, replacer, space) +// value any JavaScript value, usually an object or array. +// replacer an optional parameter that determines how object +// values are stringified for objects. It can be a +// function or an array of strings. +// space an optional parameter that specifies the indentation +// of nested structures. If it is omitted, the text will +// be packed without extra whitespace. If it is a number, +// it will specify the number of spaces to indent at each +// level. If it is a string (such as "\t" or " "), +// it contains the characters used to indent at each level. +// This method produces a JSON text from a JavaScript value. +// When an object value is found, if the object contains a toJSON +// method, its toJSON method will be called and the result will be +// stringified. A toJSON method does not serialize: it returns the +// value represented by the name/value pair that should be serialized, +// or undefined if nothing should be serialized. The toJSON method +// will be passed the key associated with the value, and this will be +// bound to the value. + +// For example, this would serialize Dates as ISO strings. + +// Date.prototype.toJSON = function (key) { +// function f(n) { +// // Format integers to have at least two digits. +// return (n < 10) +// ? "0" + n +// : n; +// } +// return this.getUTCFullYear() + "-" + +// f(this.getUTCMonth() + 1) + "-" + +// f(this.getUTCDate()) + "T" + +// f(this.getUTCHours()) + ":" + +// f(this.getUTCMinutes()) + ":" + +// f(this.getUTCSeconds()) + "Z"; +// }; + +// You can provide an optional replacer method. It will be passed the +// key and value of each member, with this bound to the containing +// object. The value that is returned from your method will be +// serialized. If your method returns undefined, then the member will +// be excluded from the serialization. + +// If the replacer parameter is an array of strings, then it will be +// used to select the members to be serialized. It filters the results +// such that only members with keys listed in the replacer array are +// stringified. + +// Values that do not have JSON representations, such as undefined or +// functions, will not be serialized. Such values in objects will be +// dropped; in arrays they will be replaced with null. You can use +// a replacer function to replace those with JSON values. + +// JSON.stringify(undefined) returns undefined. + +// The optional space parameter produces a stringification of the +// value that is filled with line breaks and indentation to make it +// easier to read. + +// If the space parameter is a non-empty string, then that string will +// be used for indentation. If the space parameter is a number, then +// the indentation will be that many spaces. + +// Example: + +// text = JSON.stringify(["e", {pluribus: "unum"}]); +// // text is '["e",{"pluribus":"unum"}]' + +// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); +// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + +// text = JSON.stringify([new Date()], function (key, value) { +// return this[key] instanceof Date +// ? "Date(" + this[key] + ")" +// : value; +// }); +// // text is '["Date(---current time---)"]' + +// JSON.parse(text, reviver) +// This method parses a JSON text to produce an object or array. +// It can throw a SyntaxError exception. + +// The optional reviver parameter is a function that can filter and +// transform the results. It receives each of the keys and values, +// and its return value is used instead of the original value. +// If it returns what it received, then the structure is not modified. +// If it returns undefined then the member is deleted. + +// Example: + +// // Parse the text. Values that look like ISO date strings will +// // be converted to Date objects. + +// myData = JSON.parse(text, function (key, value) { +// var a; +// if (typeof value === "string") { +// a = +// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); +// if (a) { +// return new Date(Date.UTC( +// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] +// )); +// } +// return value; +// } +// }); + +// myData = JSON.parse( +// "[\"Date(09/09/2001)\"]", +// function (key, value) { +// var d; +// if ( +// typeof value === "string" +// && value.slice(0, 5) === "Date(" +// && value.slice(-1) === ")" +// ) { +// d = new Date(value.slice(5, -1)); +// if (d) { +// return d; +// } +// } +// return value; +// } +// ); + +// This is a reference implementation. You are free to copy, modify, or +// redistribute. + +/*jslint + eval, for, this +*/ + +/*property + JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== "object") { + JSON = {}; +} + +(function () { + "use strict"; + + var rx_one = /^[\],:{}\s]*$/; + var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; + var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var rx_four = /(?:^|:|,)(?:\s*\[)+/g; + var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + function f(n) { + // Format integers to have at least two digits. + return (n < 10) + ? "0" + n + : n; + } + + function this_value() { + return this.valueOf(); + } + + if (typeof Date.prototype.toJSON !== "function") { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? ( + this.getUTCFullYear() + + "-" + + f(this.getUTCMonth() + 1) + + "-" + + f(this.getUTCDate()) + + "T" + + f(this.getUTCHours()) + + ":" + + f(this.getUTCMinutes()) + + ":" + + f(this.getUTCSeconds()) + + "Z" + ) + : null; + }; + + Boolean.prototype.toJSON = this_value; + Number.prototype.toJSON = this_value; + String.prototype.toJSON = this_value; + } + + var gap; + var indent; + var meta; + var rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + rx_escapable.lastIndex = 0; + return rx_escapable.test(string) + ? "\"" + string.replace(rx_escapable, function (a) { + var c = meta[a]; + return typeof c === "string" + ? c + : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); + }) + "\"" + : "\"" + string + "\""; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i; // The loop counter. + var k; // The member key. + var v; // The member value. + var length; + var mind = gap; + var partial; + var value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if ( + value + && typeof value === "object" + && typeof value.toJSON === "function" + ) { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === "function") { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case "string": + return quote(value); + + case "number": + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return (isFinite(value)) + ? String(value) + : "null"; + + case "boolean": + case "null": + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce "null". The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is "object", we might be dealing with an object or an array or +// null. + + case "object": + +// Due to a specification blunder in ECMAScript, typeof null is "object", +// so watch out for that case. + + if (!value) { + return "null"; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === "[object Array]") { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || "null"; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? "[]" + : gap + ? ( + "[\n" + + gap + + partial.join(",\n" + gap) + + "\n" + + mind + + "]" + ) + : "[" + partial.join(",") + "]"; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === "object") { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === "string") { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? "{}" + : gap + ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" + : "{" + partial.join(",") + "}"; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== "function") { + meta = { // table of character substitutions + "\b": "\\b", + "\t": "\\t", + "\n": "\\n", + "\f": "\\f", + "\r": "\\r", + "\"": "\\\"", + "\\": "\\\\" + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ""; + indent = ""; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === "number") { + for (i = 0; i < space; i += 1) { + indent += " "; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === "string") { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== "function" && ( + typeof replacer !== "object" + || typeof replacer.length !== "number" + )) { + throw new Error("JSON.stringify"); + } + +// Make a fake root object containing our value under the key of "". +// Return the result of stringifying the value. + + return str("", {"": value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== "function") { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k; + var v; + var value = holder[key]; + if (value && typeof value === "object") { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with "()" and "new" +// because they can cause invocation, and "=" because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we +// replace all simple value tokens with "]" characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or "]" or +// "," or ":" or "{" or "}". If that is so, then the text is safe for eval. + + if ( + rx_one.test( + text + .replace(rx_two, "@") + .replace(rx_three, "]") + .replace(rx_four, "") + ) + ) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The "{" operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval("(" + text + ")"); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return (typeof reviver === "function") + ? walk({"": j}, "") + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError("JSON.parse"); + }; + } +}()); \ No newline at end of file diff --git a/Example/manifest.json b/SampleScripts/ExtendScript/idmlConversion/manifest.json similarity index 88% rename from Example/manifest.json rename to SampleScripts/ExtendScript/idmlConversion/manifest.json index d5f53e8..a64daac 100644 --- a/Example/manifest.json +++ b/SampleScripts/ExtendScript/idmlConversion/manifest.json @@ -2,10 +2,10 @@ "manifestVersion": "1.0.0", "id": "Unique id for the capability", "name": "Name of the capability", - "version": "0.0.1", + "version": "1.0.0", "host": { "app": "indesign", - "minVersion": "17.0.0", + "minVersion": "16.0.1", "maxVersion": "99.9.9" }, "apiEntryPoints": [ diff --git a/Example/sample.jsx b/SampleScripts/ExtendScript/idmlConversion/sample.jsx similarity index 91% rename from Example/sample.jsx rename to SampleScripts/ExtendScript/idmlConversion/sample.jsx index e876e5d..cde1062 100644 --- a/Example/sample.jsx +++ b/SampleScripts/ExtendScript/idmlConversion/sample.jsx @@ -1,4 +1,21 @@ -// Example.jsx +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ + +// Example.jsx // This is a sample script which exports idml from an InDesign document. The expected input is as follows: // "params": { // "targetDocument": "doc.indd", diff --git a/Example/utils.jsx b/SampleScripts/ExtendScript/idmlConversion/utils.jsx similarity index 96% rename from Example/utils.jsx rename to SampleScripts/ExtendScript/idmlConversion/utils.jsx index 6179587..1518a73 100644 --- a/Example/utils.jsx +++ b/SampleScripts/ExtendScript/idmlConversion/utils.jsx @@ -1,3 +1,24 @@ +/************************************************************************* + * ADOBE CONFIDENTIAL + * ___________________ + * + * Copyright 2025 Adobe + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of Adobe and its suppliers, if any. The intellectual + * and technical concepts contained herein are proprietary to Adobe + * and its suppliers and are protected by all applicable intellectual + * property laws, including trade secret and copyright laws. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Adobe. + **************************************************************************/ +// utils.jsx + +// Utils functions which can be used across. +/* globals app, Errors, File, Folder, LinkStatus, SaveOptions */ + // Utils functions which can be used across. /* globals app, Errors, File, Folder, LinkStatus, SaveOptions */ diff --git a/SampleScripts/UXP/idmlConversion/README.md b/SampleScripts/UXP/idmlConversion/README.md new file mode 100644 index 0000000..ff5c3fe --- /dev/null +++ b/SampleScripts/UXP/idmlConversion/README.md @@ -0,0 +1,107 @@ +# IDML Export Capability + +This directory contains a UXP script for exporting InDesign documents to IDML format using Adobe InDesign's scripting capabilities. The script is designed to work with InDesign versions 18.4.0 and above. + +## Directory Contents + +``` +idmlConversion/ +├── manifest.json # Capability manifest file +└── idml.idjs # Main script implementation +``` + +## Files Description + +### 1. `manifest.json` +The manifest file defines the capability configuration: +- Supports InDesign versions 18.4.0 and above +- Implements a UXP capability entry point +- Version: 1.0.0 + +### 2. `idml.idjs` +The main script file that implements the IDML export functionality. Features include: +- Robust error handling with detailed error messages +- Comprehensive logging system +- Document link management +- Asset handling and processing +- Performance monitoring +- File system operations using UXP storage API + +## Prerequisites + +- Adobe InDesign (version 18.4.0 or higher) +- UXP scripting environment +- Access to InDesign Services +- Appropriate file system permissions + +## Installation + +1. Create a capability bundle (ZIP file) with the following structure: + ``` + Archive.zip + ├── manifest.json + └── idml.idjs + ``` + Note: Files should be zipped directly without a parent folder. + +2. Register your script following the [InDesign Capabilities API guide](https://developer.adobe.com/firefly-services/docs/indesign-apis/how-tos/working-with-capabilities-api/) + +## Usage + +### Required Parameters + +Provide the following JSON parameters to run the script: + +```json +{ + "assets": [ + { + "source": { + "url": "" + }, + "destination": "sample.indd" + } + ], + "params": { + "targetDocument": "sample.indd", + "outputPath": "converted.idml" + } +} +``` + +### Error Handling + +The script includes comprehensive error handling for various scenarios: +- Parameter validation errors +- File system operations +- Document processing errors +- Link management issues +- Asset handling problems + +Each error includes: +- Error code +- Detailed error message +- Context information + +## Logging and Monitoring + +### Log File +The script generates detailed logs in `LogFile.txt` including: +- Document processing steps +- Performance metrics +- Error messages and warnings +- Asset processing status +- Link management operations + +### Performance Metrics +- Document opening time +- Link update time +- Processing duration +- Asset handling time + +## Support + +For additional information and support: +- Refer to the [Adobe InDesign APIs documentation](https://developer.adobe.com/firefly-services/docs/indesign-apis/) +- Check the error messages in the log file for troubleshooting +- Ensure all prerequisites are met before running the script diff --git a/SampleScripts/UXP/idmlConversion/idml.idjs b/SampleScripts/UXP/idmlConversion/idml.idjs new file mode 100644 index 0000000..fc35416 --- /dev/null +++ b/SampleScripts/UXP/idmlConversion/idml.idjs @@ -0,0 +1,745 @@ +/************************************************************************* +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2025 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +**************************************************************************/ + +// Define a replacement string for error messages where dynamic values will be inserted +const ErrorReplacementString = '^1'; + +// Define an object containing various error types and their corresponding messages +const Errors = { + InternalScriptError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal script error. This should not have happened.' + ] + }, + ProcessingErrorOccurred: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Error during processing.' + ] + }, + PDFPresetNotSet: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: No PDF Preset could be set.' + ] + }, + OutputDirError: { + errorCode: 'internal_error', + errorStrings: [ + 'Internal error: Unable to create specified output directory.' + ] + }, + RelinkError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to relink.', + 'Relink failed for ^1.' + ] + }, + PlaceError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Unable to place.', + 'Place failed for ^1.' + ] + }, + ArrayExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected array ', + 'for property ^1.' + ] + }, + ParsingBoolError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected boolean ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingIntError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected integer ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingFloatError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected number ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + ParsingStringError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected string ', + '^1 is the provided value ', + 'for property ^1.' + ] + }, + OutOfBound: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Incorrect range provided. ', + 'Culprit Value is ^1.' + ] + }, + MissingKey: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Key not found in object. ', + 'Missing key is ^1.' + ] + }, + EnumError: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: No matching enum value found. ', + '^1 is the provided value', + 'for property ^1.' + ] + }, + MissingParams: { + errorCode: 'parameter_error', + errorStrings: [ + "Parameter error: Either the 'params' is not found in the request or it is not in the correct format." + ] + }, + ObjectExpected: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: Expected object ', + 'for property ^1.' + ] + }, + AtLeastOneInternalParamShouldBePresent: { + errorCode: 'parameter_error', + errorStrings: [ + 'Parameter error: At least one of the parameters should be present. ', + 'Missing parameters are ^1' + ] + }, + CannotOpenFileError: { + errorCode: 'capability_error', + errorStrings: [ + 'Capability error: Cannot open file.' + ] + } +}; + +// DOM related variables + +// Load InDesign's scripting environment +const myInDesign = require("indesign"); +const app = myInDesign.app; // Access the InDesign application instance + +// Import the local file system module from UXP for file handling +const fs = require("uxp").storage.localFileSystem; + +// Import UXP scripting module for getting script arguments and setting script output. +const script = require("uxp").script; + + +// Utility class containing helper functions for logging, error handling, file operations, and InDesign document management. +class Utils { + constructor() { + // Array to store log messages + this.logs = []; + + // Paths for working and output directories + this.workingFolder = ""; + this.outputFolder = ""; + + // List of assets to be uploaded + this.assetsToBeUploaded = []; + + // Flag to determine whether a separate log file should be created + this.createSeparateLogFile = true; + + // Default log file name + this.logFilePath = "LogFile.txt"; + } + + + // Initialize logging by clearing the logs array + initiateLogging() { + this.logs = []; + } + + // Log a message to the console and store it in the logs array + log(message) { + console.log(message); + this.logs.push(message); + + } + + // Converts the package in appropriate format, which can be returned from the capability. + getFinalReturnPackage(obj) { + this.log("Final package created"); + return JSON.stringify(obj); + } + + // Creates and returns the package to be returned in case the job is successful. + async getSuccessReturnObj(data) { + let obj = { + status: "SUCCESS", + assetsToBeUploaded: this.assetsToBeUploaded + }; + + try { + obj.dataURL = await this.writeToFile(data, "outputData"); + } catch (err) { + this.log(`Error writing outputData: ${err.message}`); + } + + return obj; + } + + // Creates and returns the package to be returned in case the job has failed. + getFailureReturnObj(errorCode, errorString, data = null) { + return { + status: "FAILURE", + errorCode, // Attach the error code. + errorString, // Attach the error message. + data // Include additional data if available. + }; + } + + // Add an asset which is to be uploaded and sent back to the caller. + addAssetToBeUploaded(assetPath, data = null) { + this.assetsToBeUploaded.push({ path: assetPath, data: data }); + } + + // This handles errors in case an exception is raised. This results the capability returning error to the caller. + handleError(exception) { + let errorCode, errorString; + console.log('Exception occurred: ' + JSON.stringify(exception)); + + if (exception.message === 'open') { + exception.message = Errors.CannotOpenFileError.errorStrings[0]; + exception.errorCode = Errors.CannotOpenFileError.errorCode; + } + + errorString = 'Script error: '; + if (exception.isCustom === true) { + if (exception.message) { + errorString += exception.message; + } + errorCode = exception.errorCode; + } else { + errorCode = Errors.ProcessingErrorOccurred.errorCode; + if (exception.message) { + errorString += Errors.ProcessingErrorOccurred.errorStrings[0] + ' ' + exception.message; + } + if (exception.errorCode !== undefined) { + errorString += '\nInternal error code: ' + exception.errorCode + '.'; + } + if (exception.line !== undefined) { + errorString += ' Line: ' + exception.line + '.'; + } + if (exception.fileName !== undefined) { + errorString += ' FileName: ' + exception.fileName + '.'; + } + console.log('Processing error occurred. ' + errorString); + } + + return this.getFailureReturnObj(errorCode, errorString); + } + + // This will update out-of-date links in a document. + async updateDocumentLinks(document) { + try { + let links = document.links; + let numLinks = links.length; + let outOfDateLinks = []; + + this.log('Number of links: ' + numLinks); + + for (let i = 0; i < numLinks; i++) { + let link = links.item(i); + try { + let uri = link.linkResourceURI; + this.log('URI: ' + uri); + if (link.status === myInDesign.LinkStatus.LINK_OUT_OF_DATE) { + outOfDateLinks.push(link); + } + } catch (err) { + this.log('Link status unknown: ' + err); + } + } + + // Update all out-of-date links. + for (let i = 0; i < outOfDateLinks.length; i++) { + let link = outOfDateLinks[i]; + if (link.isValid) { + await link.update(); + } + } + + let composeStartTime = Date.now(); + + // Recompose the document after updating links. + await document.recompose(); + let composedTime = Date.now() - composeStartTime; + this.log('Recompose Time: ' + composedTime); + } catch (error) { + this.error("Error updating document links:", error); + } + } + + // Writes data to the given named JSON file + async writeToFile(data, filename) { + try { + let folder = await fs.getEntryWithUrl("file://" + this.workingFolder); + let file = await folder.createFile(filename + '.json', { overwrite: true }); + await file.write(" "); + if (await file.write(JSON.stringify(data))) { + this.log('Data was successfully written') + } + else { + this.log('Data write failed') + } + return file.nativePath; + } + catch (err) { + this.log('Error writing to file: ' + err.message) + } + + } + + // Sets the working folder to be used across the capability. + async setWorkingFolder(workingFolder) { + // workingFolder = workingFolder.replace(/\//g, '\\') + // if (workingFolder.charAt(workingFolder.length - 1) !== '\\') { + // workingFolder = workingFolder + '\\' + // } + this.workingFolder = workingFolder + await this.openLogFileHandle(); + } + + // Opens a log file handle if separate logging is enabled. + async openLogFileHandle() { + try { + var folder = await fs.getEntryWithUrl("file://" + this.workingFolder); + var logFile = await folder.createFile(this.logFilePath, { overwrite: true }); + this.logFileHandle = logFile; + } catch (err) { + console.error("Failed to create log file:", err); + this.log(`Failed to create log file: ${err.message}`); + } + } + + // Append a log message to the log file + async logToFile(message) { + if (!this.logFileHandle) { + await this.openLogFileHandle(); + } + + try { + await this.logFileHandle.write(message + "\n"); + } catch (err) { + console.error("Failed to write to log file:", err); + + } + } + + // Closes all open documents in the application. + async closeAllOpenDocuments() { + try { + console.log("Closing all open documents..."); + this.log("Closing all open documents..."); + const documents = app.documents; + let SaveOptions = myInDesign.SaveOptions; + + // Close each document without saving + for (let i = documents.length - 1; i >= 0; i--) { + let doc = documents.item(i) + await doc.close(SaveOptions.NO); + } + + console.log("All documents closed."); + } catch (error) { + console.error("Error closing documents:", error); + this.log(`Error closing documents: ${error.message}`); + } + } + + // This is used to raise an exception with all the required details. + // NOTE: This takes variable list of argument and then try to fill in the details in errorObj.errorStrings + raiseException(errorObj) { + UTILS.log('RaiseException()') + var numMessageParameters = arguments.length + UTILS.log('numMessageParameters: ' + numMessageParameters) + var numErrorStrings = errorObj.errorStrings.length + UTILS.log('numErrorStrings: ' + numErrorStrings) + var numIterations = (numMessageParameters > numErrorStrings) ? numErrorStrings : numMessageParameters + var errorMessage = errorObj.errorStrings[0] + UTILS.log('Default errorMessage: ' + errorMessage) + + // Construct the detailed error message using available parameters. + for (var itr = 1; itr < numIterations; itr++) { + var parameter = arguments[itr] + if (parameter !== undefined && parameter !== '') { + var parameterMessage = errorObj.errorStrings[itr] + errorMessage = errorMessage + ' ' + parameterMessage.replace('^1', parameter) + UTILS.log('Appended errorMessage: ' + errorMessage) + } + } + var exceptionObj = { + number: errorObj.errorCode, + isCustom: true, + message: errorMessage + } + + throw exceptionObj + } + + // Gets a string from an 'object' defined with 'key'. + // A default value can be passed via 'defaultVal' which will be used if the key is missing. The case of key can be ignored via 'ignoreCase'. + // In case, key is missing and a default value is not provided, an exception will be thrown. Exception will also be thrown if the value found is not string. + getStringFromObject(object, key, defaultVal, ignoreCase) { + var val + this.log('GetStringFromObject ' + key) + + if (this.isKeyPresent(object, key)) { + val = this.getValueFromObject(object, key, ignoreCase) + if (typeof val !== 'string') { + this.raiseException(Errors.ParsingStringError, val, key) + } else { + return val + } + } else if (defaultVal === undefined) { + this.raiseException(Errors.MissingKey, key) + } else { + // default value provided. + return defaultVal + } + } + + // Checks whether a key is present in the object or not. + isKeyPresent(object, key) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + return true + } + return false + } + + // Gets the value from an 'object' defined with 'key'. + // The case of key can be ignored via 'ignoreCase'. In case, key is missing an exception will be thrown. + getValueFromObject(object, key, ignoreCase) { + UTILS.log('GetValueFromObject ' + key) + + if (ignoreCase === true) { + var objectKey + for (objectKey in object) { + if (Object.prototype.hasOwnProperty.call(object, objectKey)) { + if (objectKey.toLowerCase() === key.toLowerCase()) { + return object[objectKey] + } + } + } + } else if (Object.prototype.hasOwnProperty.call(object, key)) { + return object[key] + } + + this.raiseException(Errors.MissingKey, key) + } + + // Get the full path from a relative and a base path. In absence of base path, working directory is used. + getFullPath(relativePath, basePath) { + var origRelativePath = relativePath + if (relativePath.indexOf('..') !== -1 || relativePath.indexOf('/') === 0) { + this.raiseException(Errors.InCorrectRelativePath, origRelativePath) + } + relativePath = relativePath.replace(/\//g, '\\') + if (relativePath.indexOf('\\.\\') !== -1) { + this.raiseException(Errors.InCorrectRelativePath, origRelativePath) + } + + if (basePath === undefined) { + basePath = this.workingFolder + } else { + basePath = basePath.replace(/\//g, '\\') + if (basePath.charAt(basePath.length - 1) !== '\\') { + basePath = basePath + '\\' + } + } + + if (relativePath.charAt(0) === '.') { + relativePath = relativePath.slice(1) + } + if (relativePath.charAt(0) === '\\') { + relativePath = relativePath.slice(1) + } + + var fullPath = basePath + "/"+relativePath + return fullPath + } + + // Get relative path to the working directory for a path. This returns in unix notation. + getRelativeReturnPath(path) { + var index = path.indexOf(this.workingFolder) + if (index === 0) { + var tempPath = path.substr(UTILS.workingFolder.length) + tempPath = tempPath.replace(/\\/g, '/') + return tempPath + } + } + + // Terminate logging and write logs to file + async terminateLogging() { + this.log('Terminating logging.') + await this.logToFile(this.logs.join('\n')); + } +} + +// Create an instance of the Utils class +const UTILS = new Utils(); + + +// Constant strings +var KeyStrings = { + OutputPath: 'outputPath', + ProcessedData: 'processedData', + TargetDocument: 'targetDocument', + TimeDocumentClose: 'DocumentClose', + TimeDocumentOpen: 'DocumentOpen', + TimeExportIDML: 'ExportIDML', + TimeOverall: 'Overall', + TimeRelinkAssets: 'RelinkAssets', + Timings: 'timings', + WorkingFolder: 'workingFolder' +} + +// Object to store timing metrics for various processing steps. +var timingObject = {}; + +// Object to store warnings encountered during execution. +var warnings = {}; + +// Variable to hold the InDesign document. +var document; + + +/** + * Processes the provided parameters and executes the main operations. + * This includes opening the document, updating links, exporting to IDML, and logging performance metrics. + */ +async function ProcessParams(parameters) { + + // This is where the actual processing will take place. + UTILS.log('Processing parameters internal') + var returnVal = {} + + // Retrieve the target document path from parameters. + var documentPath = UTILS.getStringFromObject(parameters, KeyStrings.TargetDocument) + documentPath = UTILS.getFullPath(documentPath) + + UTILS.log('Opening document') + var tempTime = new Date().getTime() + + // Get file entry from the file system. + let file = await fs.getEntryWithUrl(`file://${documentPath}`); + + // Check if the path points to a valid file. + if (!file.isFile) { + UTILS.log("Not a valid file."); + return; + } + + // Open the document in InDesign Server. + document = await app.open(file) + + // Log the time taken to open the document. + timingObject[KeyStrings.TimeDocumentOpen] = (new Date()).getTime() - tempTime + UTILS.log('Opened document') + tempTime = new Date().getTime() + + // Update document links. + await UTILS.updateDocumentLinks(document) + timingObject[KeyStrings.TimeRelinkAssets] = (new Date()).getTime() - tempTime + UTILS.log('Updated links in the document') + + + // Retrieve the output file path from parameters. + var outputPath = UTILS.getStringFromObject(parameters, KeyStrings.OutputPath) + outputPath = UTILS.getFullPath(outputPath) + tempTime = new Date().getTime() + + // Export the document as an IDML file. + document.exportFile(myInDesign.ExportFormat.INDESIGN_MARKUP, outputPath) + timingObject[KeyStrings.TimeExportIDML] = (new Date()).getTime() - tempTime + + // Convert the absolute output path to a relative path for return. + var relativePath = UTILS.getRelativeReturnPath(outputPath) + + // Register the exported file as an asset to be uploaded. + UTILS.addAssetToBeUploaded(relativePath) + + returnVal.idmlPath = relativePath + // Processing ends. + + return returnVal +}; + +/** + * Main function that controls the script execution. + * It initializes logging, processes parameters, handles exceptions, and returns the result. + */ +async function main() { + + var startTime = new Date().getTime() // Capture the script start time + var returnObj = {} // Object to store return values + var parameters = {} // Stores input parameters + var tempTime + var errorOccurred = false // Flag to track errors + var data = {} // Stores processed data + + try { + + // Initialize logging. + await UTILS.initiateLogging() + UTILS.log('Initiating logging.') + UTILS.log('This way of logging can be used for debugging and generic logging.') + UTILS.log('This has been done after setting of working directory because it is relative to the working directory') + UTILS.log('Each log will be a separate line in the log file.') + UTILS.log('The file generated can be sent back') + + // Close all open InDesign documents to prevent conflicts. + await UTILS.closeAllOpenDocuments() + + // Retrieve input parameters from the script arguments. + let input = script.args // Right now out input will look like ["parameters={...}"]. So, will first need input[0] + + input=input[0] // Now out input looks like "parameters={...}". We will have to slice it down so that we only have the JSON Object {...}. + // if this step is skipped, parsing it using JSON.parse() will give us error. + // Remove "parameters=" prefix to extract JSON. + + input=input.slice(11) + + // Now we have the parameters as string. Parse the input JSON string into an object. + var allParameters = JSON.parse(input) + UTILS.log('Parsed job input : ' + input) + + // Extract parameters from the parsed JSON. + parameters = allParameters.params + if (parameters === undefined) { + parameters = allParameters.input.params + } + + // Validate the extracted parameters. + if (parameters === undefined || typeof parameters !== 'object' || Array.isArray(parameters)) { + UTILS.log('No params found') + UTILS.raiseException(Errors.MissingParams) + } + + // Set the working folder based on parameters. + await UTILS.setWorkingFolder(UTILS.getStringFromObject(allParameters, KeyStrings.WorkingFolder)) + + var result + UTILS.log('Processing Params') + tempTime = new Date().getTime() + + // Process the parameters and perform document-related tasks. + result = await ProcessParams(parameters) + data[KeyStrings.ProcessedData] = result + + // Include the log file in the list of assets to be uploaded. + UTILS.addAssetToBeUploaded(UTILS.logFilePath) + + data.warnings = warnings + // processing ends. + + tempTime = new Date().getTime() + + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + UTILS.log('End of try') + } + catch (e) { + + // Handle exceptions and prepare failure response. + var tempObj = { + name: e.name, + message: e.message, + errorCode: e.number, + isCustom: e.isCustom, + line: e.line, + fileName: e.fileName + } + + UTILS.log('Exception occurred', tempObj) + errorOccurred = true + + // Failure, prepare the object to be returned. + returnObj = UTILS.handleError(tempObj) + + } + finally { + + if (document && document.isValid) { + // If Document is still open. Close it. + UTILS.log('Closing document') + tempTime = new Date().getTime() + document.close() + timingObject[KeyStrings.TimeDocumentClose] = (new Date()).getTime() - tempTime + } + + // Log execution time. + var elapsedTime = (new Date()).getTime() - startTime + UTILS.log('Time taken: ' + elapsedTime) + timingObject[KeyStrings.TimeOverall] = elapsedTime + UTILS.log('Timing: ' + JSON.stringify(timingObject)) + data[KeyStrings.Timings] = timingObject + + // If no errors occurred, return a success response. + if (!errorOccurred) { + UTILS.log('Finally: No error') + returnObj = await UTILS.getSuccessReturnObj(data) + } + + // Log the final result. + UTILS.log('Final Result' + JSON.stringify(returnObj)) + + // Terminate logging and return the final response. + await UTILS.terminateLogging() + return UTILS.getFinalReturnPackage(returnObj) + } +} + +/** + * Executes the main function and sets the script result. + * If an error occurs, it logs the error and sets the result accordingly. + */ +try { + var out=await main(); + + //In UXP Script, we have to set the return Result using setResult() function. + script.setResult(out); +} +catch (error) { + console.log(error); + script.setResult(error); +} + + + diff --git a/SampleScripts/UXP/idmlConversion/manifest.json b/SampleScripts/UXP/idmlConversion/manifest.json new file mode 100644 index 0000000..3426410 --- /dev/null +++ b/SampleScripts/UXP/idmlConversion/manifest.json @@ -0,0 +1,17 @@ +{ + "manifestVersion": "1.0.0", + "name": "idml", + "host": { + "app": "indesign", + "maxVersion": "99.9.9", + "minVersion": "18.4.0" + }, + "version": "1.0.0", + "apiEntryPoints": [ + { + "path": "idml.idjs", + "type": "capability", + "language": "uxpscript" + } + ] +} \ No newline at end of file diff --git a/samples.md b/samples.md index 0f263cf..e51e687 100644 --- a/samples.md +++ b/samples.md @@ -3,7 +3,7 @@ The below code snippets are for demonstration purpose. Please feel free to downl Just remember you will need to have the assets stored in one of the accepted external storage. ## Sample capability bundle -A sample capability bundle can be found at [Example](Example/). +Sample capability bundles can be found at [SampleScripts](SampleScripts/). A manifest.json must be bundled inside the capability zip. A typical manifest would look like: ```json {