- Fix API payload: "contents" not "content", "align" not "alignment", output type "vnd.adobe.photoshop" not "image/vnd.adobe.photoshop" - Remove broken font size /72 conversion (values already in points) - Add automatic font upload from fonts/ directory to GCS - Add FuturaPT-Demi.otf extracted from Adobe CoreSync - Update GCS bucket to lor-txt-tmp-bkt-26 (old billing expired) - Update HOW-IT-WORKS.md with working API docs, font setup guide, bug fixes, and verified test results Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
18 KiB
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:
- Local workflow - Python drives Photoshop directly via AppleScript + ExtendScript
- 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 <token>+x-api-key: <client_id> - 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
sizein characterStyles — the API preserves the original size automatically. Only specifyfontPostScriptNameto keep the correct font. - Custom fonts: Uploaded to GCS and referenced via
options.fontsarray 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:
# 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
- Find the font file (see above)
- Copy it to the
fonts/directory with a descriptive name:cp ~/.../livetype/.w/.15586.otf fonts/FuturaPT-Demi.otf - 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
{
"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
# --- 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:
- Create/re-enable a GCP project at https://console.cloud.google.com
- Enable Cloud Storage API at https://console.cloud.google.com/apis/library/storage.googleapis.com
- 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
- Name: e.g.
- Create a service account at https://console.cloud.google.com/iam-admin/serviceaccounts
- Role:
Storage Admin - Download JSON key -> save as
gcs_key.jsonin project root
- Role:
- Update bucket name in
config.pyandsimplified_payload.pyif 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)