# How It Works - Adobe Photoshop API Text Management ## Overview This project automates **extracting and updating text layers** in Photoshop PSD files. It provides two approaches: 1. **Local workflow** - Python drives Photoshop directly via AppleScript + ExtendScript 2. **Adobe Cloud API workflow** - Files uploaded to Google Cloud Storage, processed via Adobe's Photoshop API Both workflows are fully functional. The API workflow updates text, preserves font size, and supports custom fonts. --- ## System Architecture (ASCII Diagram) ``` +============================================================================+ | ADOBE PSD TEXT MANAGEMENT | +============================================================================+ WORKFLOW A: LOCAL (macOS) WORKFLOW B: ADOBE CLOUD API ======================== ========================== +-------------+ +-------------+ | PSD File | | PSD File | +------+------+ +------+------+ | | v v +-----------------+ +------------------+ | mac_ps_extract | | simplified_ | | .py | | payload.py | +--------+--------+ +--------+---------+ | | | AppleScript +---------+---------+ v | | +-----------------+ +-----+------+ +-------+------+ | Photoshop | | Upload PSD | | Upload fonts | | (ExtendScript) | | to GCS | | from fonts/ | | | +-----+------+ +-------+------+ | ExtractText | | | | WithBreaks.jsx | v v +--------+--------+ +---------------------------+ | | Google Cloud Storage | | JSON output | Bucket: lor-txt-tmp-bkt-26| v +------------+--------------+ +-----------------+ | | *-textonly.json | Signed URLs (input, | {textLayers:[]} | output, fonts) +--------+--------+ | | v | User edits +------------------+ | "updatedText" | adobe_token.py | v | (OAuth 2.0) | +-----------------+ +--------+---------+ | *-textonly.json | | | (edited) | | Bearer Token +--------+--------+ v | +------------------+ v | Adobe Photoshop | +-----------------+ | API Endpoint | | mac_ps_update | | image.adobe.io/ | | .py | | pie/psdService/ | +--------+--------+ | text | | +--------+---------+ | AppleScript | v | 202 Accepted +-----------------+ v | Photoshop | +------------------+ | updateText | | Poll status URL | | Layers.jsx | | until "succeeded"| +--------+--------+ +--------+---------+ | | v v +-----------------+ +------------------+ | Updated PSD | | Download result | | (text changed, | | from GCS bucket | | styles kept) | +--------+---------+ +-----------------+ | v +------------------+ | api_updated_*.psd| | Text changed, | | font + size kept | +------------------+ ``` --- ## API Workflow — How It Actually Works The `simplified_payload.py` script handles the full pipeline: ``` 1. UPLOAD 2. AUTHENTICATE 3. BUILD PAYLOAD ======== =============== =============== PSD file ──> GCS bucket config.py credentials { fonts/ dir ──> GCS bucket ──> Adobe IMS OAuth "inputs": [signed_url], ──> Bearer token "options": { Generate signed URLs "fonts": [font_urls], for input + output "layers": [{ "name": "Layer Name", "text": { 4. API CALL 5. POLL "contents": "NEW TEXT", ========== ===== "characterStyles": [{ "fontPostScriptName": POST payload to GET status URL "FuturaPT-Demi" image.adobe.io/ every 5 seconds }] pie/psdService/text until "succeeded" }}] }, Returns 202 + "outputs": [signed_url] status URL } 6. DOWNLOAD 7. CLEANUP ========== ========= Download processed PSD Delete temporary files from GCS output URL from GCS bucket Save as api_updated_* ``` ### Key API Details - **Endpoint**: `POST https://image.adobe.io/pie/psdService/text` - **Auth headers**: `Authorization: Bearer ` + `x-api-key: ` - **Layer matching**: Uses layer `name` (not numeric ID) - **Text field**: Must be `"contents"` (not `"content"`) - **Paragraph alignment**: Must be `"align"` (not `"alignment"`) - **Font size**: Do NOT include `size` in characterStyles — the API preserves the original size automatically. Only specify `fontPostScriptName` to keep the correct font. - **Custom fonts**: Uploaded to GCS and referenced via `options.fonts` array with signed URLs - **Output type**: `"vnd.adobe.photoshop"` (not `"image/vnd.adobe.photoshop"`) --- ## Custom Fonts Setup The Adobe cloud API does NOT have access to Adobe Fonts (Typekit) or your local fonts. You must provide any non-standard fonts yourself. ### Where to find Adobe Fonts on macOS Adobe Fonts are hidden in: ``` ~/Library/Application Support/Adobe/CoreSync/plugins/livetype/.w/ ``` Files are named with numeric IDs (e.g., `.15586.otf`). To find a specific font: ```bash # Search for a font by PostScript name find ~/Library/Application\ Support/Adobe/CoreSync/plugins/livetype/ \ -name "*.otf" -exec sh -c \ 'strings "$1" | grep -q "FuturaPT-Demi" && echo "FOUND: $1"' _ {} \; ``` ### Adding fonts to the project 1. Find the font file (see above) 2. Copy it to the `fonts/` directory with a descriptive name: ```bash cp ~/.../livetype/.w/.15586.otf fonts/FuturaPT-Demi.otf ``` 3. The script automatically uploads ALL files from `fonts/` to GCS and includes them in the API payload ### Currently included fonts | File | PostScript Name | Used By | |------|----------------|---------| | `FuturaPT-Demi.otf` | FuturaPT-Demi | Vichy product PSDs | | `Futura.ttc` | Futura-Medium, Futura-Bold, etc. | General Futura variants | --- ## Authentication & Credentials Map ``` CREDENTIALS LOCATIONS ===================== +------------------+ +--------------------+ +-------------------+ | config.py | | .adobe_token_ | | gcs_key.json | | | | cache.json | | | | - Client ID |------>| - Bearer token | | - GCS service | | - Client Secret | | - Expiration time | | account key | | - API endpoints | | - Auto-refreshes | | - Project ID | | - GCS bucket name| +--------------------+ | - Private key | | - Token URL | | - Client email | +------------------+ +-------------------+ | | v v +------------------+ +-------------------+ | Adobe IMS OAuth | | Google Cloud | | Token Endpoint | | Storage API | | (ims-na1.adobe | | Bucket: | | login.com) | | lor-txt-tmp-bkt-26| +------------------+ +-------------------+ OAUTH FLOW: config.py credentials --> POST to Adobe IMS --> Bearer token --> cached locally (client_credentials (24hr TTL, grant type) auto-refresh) ``` --- ## File Roles & Connections ### Core Scripts | File | Role | Connects To | |------|------|-------------| | `config.py` | Stores API credentials, endpoints, bucket name | Everything that talks to Adobe/GCS | | `adobe_token.py` | Manages OAuth tokens (generate, cache, refresh) | `config.py` -> Adobe IMS | | `adobe_ps_api.py` | Main API wrapper (upload, request, poll, download) | `adobe_token.py`, `gcs_storage.py` | | `gcs_storage.py` | Google Cloud Storage (upload, download, signed URLs) | `gcs_key.json` -> GCS API | ### Text Extraction (PSD -> JSON) | File | Role | Connects To | |------|------|-------------| | `mac_ps_extract.py` | Python orchestrator for extraction | AppleScript -> Photoshop | | `ExtractTextWithBreaks.jsx` | ExtendScript that reads PSD layers | Runs inside Photoshop | | `batch_extract_text.py` | Batch wrapper for multiple PSDs | `mac_ps_extract.py` | ### Text Update (JSON -> PSD) | File | Role | Connects To | |------|------|-------------| | `mac_ps_update.py` | Python orchestrator for local updates | AppleScript -> Photoshop | | `updateTextLayers.jsx` | ExtendScript that writes to PSD layers | Runs inside Photoshop | | `batch_update_text.py` | Batch wrapper for multiple updates | `mac_ps_update.py` | | `simplified_payload.py` | Sends update via Adobe API (recommended) | `adobe_token.py`, `gcs_storage.py` | | `update_text_with_api.py` | Batch API updates | `adobe_token.py`, `gcs_storage.py` | ### Layer ID Resolution | File | Role | Connects To | |------|------|-------------| | `extract_and_update_json.py` | Gets internal IDs, updates JSON | AppleScript -> Photoshop | | `extract_ids.py` | Wrapper for ID extraction | `test/extract_internal_ids.jsx` | | `test/extract_internal_ids.jsx` | Gets real internal layer IDs | Runs inside Photoshop | --- ## Data Flow Step-by-Step ### Local Workflow ``` STEP 1: EXTRACT STEP 2: EDIT STEP 3: UPDATE ============== =========== ============== $ python mac_ps_extract.py Open the JSON file $ python mac_ps_update.py /path/to/PSDs and edit "updatedText" /path/to/JSONs fields for each layer -p /path/to/PSDs | --save v | Python sends AppleScript | | to open PSD in Photoshop v v | Python sends AppleScript v +-----------+ to open PSD + run JSX ExtractTextWithBreaks.jsx | "textLayers": | runs inside Photoshop | [{ v | | "name":"Title", updateTextLayers.jsx v | "text":"OLD", matches layers by name Reads every text layer: | "updatedText": and replaces content - Layer name & path | "NEW" | - Text content | }] v - Font, size, color, style +-----------+ PSD saved with - Line breaks preserved new text, old styles | v Saves *-textonly.json ``` ### API Workflow ``` STEP 1: PREP JSON STEP 2: RUN API STEP 3: VERIFY ================ =========== ============= Extract text to JSON $ python simplified_payload Open the output (or use existing) .py file.json file.psd api_updated_*.psd in Photoshop Edit "updatedText" Script will: fields in the JSON 1. Upload PSD to GCS Check that: 2. Upload fonts/ to GCS - Text changed Make sure fonts/ 3. Get OAuth token - Font preserved directory has the 4. POST to Adobe API - Size preserved required .otf files 5. Poll until done 6. Download result 7. Clean up GCS ``` --- ## JSON Format Reference ```json { "documentName": "product-label.psd", "psdPath": "/path/to/product-label.psd", "extractedAt": "2024-12-15T10:30:00", "dimensions": { "width": 1200, "height": 800 }, "textLayerCount": 3, "textLayers": [ { "id": "", "name": "Product Title", "path": "Text Group/Product Title", "text": "Original Title Text", "updatedText": "YOUR NEW TEXT HERE", "visible": true, "styleInfo": { "font": "Helvetica-Bold", "size": 24, "color": [0, 0, 0], "alignment": "center", "styles": [ { "start": 0, "end": 19, "text": "Original Title Text", "font": "Helvetica-Bold", "style": "Bold", "size": 24, "color": [0, 0, 0] } ] }, "hasRichTextFormatting": true } ] } ``` --- ## Quick Command Reference ```bash # --- LOCAL WORKFLOW --- # Extract text from PSDs to JSON python mac_ps_extract.py /path/to/psd_folder # Extract recursively with custom output python mac_ps_extract.py /path/to/psd_folder -o /output/dir -r # Update text (dry-run first) python mac_ps_update.py /path/to/json_folder -p /path/to/psd_folder --dry-run # Update text (for real, with save) python mac_ps_update.py /path/to/json_folder -p /path/to/psd_folder --save # --- API WORKFLOW --- # Generate OAuth token python adobe_ps_api.py generate-token # Test API connectivity python adobe_ps_api.py test-api # Update via API (single file) python simplified_payload.py /path/to/file.json /path/to/file.psd # Update via API (batch) python update_text_with_api.py --directory /path/to/directory ``` --- ## GCS Setup (If Starting Fresh) If the GCS bucket or billing expires, set up a new one: 1. **Create/re-enable a GCP project** at https://console.cloud.google.com 2. **Enable Cloud Storage API** at https://console.cloud.google.com/apis/library/storage.googleapis.com 3. **Create a bucket** at https://console.cloud.google.com/storage/browser - Name: e.g. `lor-txt-tmp-bkt-26` - Location: `us-central1`, Standard class, Uniform access 4. **Create a service account** at https://console.cloud.google.com/iam-admin/serviceaccounts - Role: `Storage Admin` - Download JSON key -> save as `gcs_key.json` in project root 5. **Update bucket name** in `config.py` and `simplified_payload.py` if it changed --- ## Bugs Fixed (March 2026) These issues were identified and fixed during API testing: | Bug | Cause | Fix | |-----|-------|-----| | Font size destroyed (0.12pt) | Script divided size by 72 (already in points) | Removed `/72` conversion | | Text not changing | API field was `"content"` | Changed to `"contents"` (per API spec) | | Font replaced with Arial | Adobe cloud doesn't have Adobe Fonts/Typekit | Upload actual `.otf` files via `options.fonts` | | Paragraph alignment ignored | Field was `"alignment"` | Changed to `"align"` (per API spec) | | Output type rejected | Used `"image/vnd.adobe.photoshop"` | Changed to `"vnd.adobe.photoshop"` | | GCS billing expired | Old GCP project billing closed | Created new bucket `lor-txt-tmp-bkt-26` | --- ## Test Results (Verified March 2, 2026) Using `Vichy-Product-Skincare-Liftactiv...Texture.psd` (single text layer): | Property | Original | API Updated | Match? | |----------|----------|-------------|--------| | Text | `QUICK ABSORBTION NON-greasyTEXTURE` | `FAST ABSORPTION LIGHTWEIGHT TEXTURE` | Changed | | Font | `FuturaPT-Demi` | `FuturaPT-Demi` | Exact | | Size | `32.7pt` | `32.7pt` | Exact | --- ## Tech Stack - **Python 3** - Orchestration and API calls - **Adobe ExtendScript (JSX)** - Runs inside Photoshop to read/write layers - **AppleScript** - Bridge between Python and Photoshop on macOS - **Adobe Photoshop API** - Cloud-based PSD manipulation (`image.adobe.io`) - **Adobe IMS OAuth 2.0** - API authentication (`ims-na1.adobelogin.com`) - **Google Cloud Storage** - Temporary file hosting for API workflow (`lor-txt-tmp-bkt-26`) - **requests, google-cloud-storage** - Python libraries (see `requirements.txt`)