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

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:

  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:

# 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:
    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

{
  "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:

  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)