adobe-ps-scripts-loreal/HOW-IT-WORKS.md
DJP d7dd117dab Fix Adobe API text updates: correct field names, font handling, and GCS bucket
- 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>
2026-03-02 17:03:07 -05:00

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`)