- 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>
423 lines
18 KiB
Markdown
423 lines
18 KiB
Markdown
# 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 <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 `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`)
|