Skip to content

fix(core): fall back to CLSID when Photoshop publishes no versioned ProgID - #435

Merged
loonghao merged 2 commits into
mainfrom
feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd
Oct 1, 2026
Merged

loonghao merged 2 commits into
mainfrom
feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd

Conversation

@loonghao

@loonghao loonghao commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Problem

On some Photoshop installations the automation classes are registered under a bare CLSID and the versioned Photoshop.<Class>.<version> ProgIDs are never published. Every wrapper class then failed with:

PhotoshopPythonAPIError: Please check if you have Photoshop installed correctly.

while photoshop.api.Application() worked fine, because Photoshop.Application is one of the few ProgIDs that are published.

Concretely, on such an install only 6 ProgIDs exist (Photoshop.Application, Photoshop.Application.190, Photoshop.Application.190.1, Photoshop.Image, Photoshop.Image.26, Photoshop.PlugIn), while 50 CLSIDs are registered as LocalServer32 entries pointing at Photoshop.exe and 48 of them are creatable. Session() and the 21 helper wrappers were all unusable.

Fix

Photoshop._get_application_object now falls back to the class ID once every ProgID lookup has failed:

  1. Photoshop's own type library is read and indexed. It ships with Photoshop, declares every automation coclass by name, and requires instantiating nothing. This is the route that resolves real installs.
  2. If no type library is available, each CLSID registered as a Photoshop local server is instantiated and asked for its coclass name through IDispatch::GetTypeInfo. This depends only on Photoshop itself, but costs roughly a second per candidate, so it is strictly a last resort.

Both routes are cached for the lifetime of the process. A working ProgID still wins, so nothing changes on installations that publish ProgIDs.

The failure message now reports what was actually tried instead of always blaming a missing installation:

Unable to resolve the Photoshop COM object 'ActionDescriptor'.
  - Program ID 'Photoshop.ActionDescriptor.190' could not be created.
  - Program ID 'Photoshop.ActionDescriptor' could not be created.
  - No CLSID is registered for 'ActionDescriptor': no Photoshop type library or
    automation class declares 'ActionDescriptor'.
Please check if you have Photoshop installed correctly.
Class IDs are cached for the lifetime of the process; call
photoshop.api._clsid.reset_cache() if Photoshop was started or upgraded after
this process began.

Changes

  • photoshop/api/_clsid.py (new): class ID resolution from Photoshop's type library, with the IDispatch probe as a fallback.
  • photoshop/api/_core.py: CLSID fallback in _get_application_object, plus the diagnostic error message.
  • test/test_clsid_fallback.py (new): unit tests for how the fallback is wired into Photoshop, with the resolver stubbed out.
  • test/test_clsid_resolution.py (new): tests that run the resolver itself against an in-memory registry and a fake type library.

Why a second test file

The first suite stubs the resolver, so it verifies the wiring but not the resolution. A review pointed out that this left the type library path — the one that carries the whole feature, and the one CI cannot exercise because it runs without Photoshop — with no automated coverage at all: breaking the index entirely still passed all 13 tests.

The second suite runs the real code and fails if the type library index returns nothing, if the richest-library sort is dropped, or if a sibling install such as Photoshop 2024 Beta is mistaken for Photoshop 2024.

Three failure modes surfaced while writing it:

  • _is_within matched on a bare name prefix, so a sibling directory sharing a prefix counted as being inside the install directory and could feed another version's type library to LoadTypeLibEx. The comparison is now anchored on a path separator.
  • winreg.OpenKey was called bare while walking the TypeLib hive, so a key that disappeared mid-walk raised out of the resolver instead of degrading to the normal "could not resolve" error. The walk now tolerates unreadable keys, and _core records the real reason when the resolver itself raises.
  • A miss was cached for the life of the process even when no Photoshop had been seen, so a lookup that ran before Photoshop started kept failing afterwards. Misses are now only cached once Photoshop has actually been found, and the error distinguishes a genuinely unknown class from a failed inspection.

Also skip the TypeLib walk entirely when there is no install directory, since every candidate would be discarded anyway.

Verification

  • pytest — 51 passed.
  • black --check, isort --check-only, flake8 clean.
  • Verified against a Photoshop install that publishes no versioned ProgIDs:
    • Session(action="new_document") no longer raises; the README example runs end to end and writes a JPEG.
    • JPEGSaveOptions(quality=5), SolidColor(), ActionDescriptor(), ActionReference() and the other 16 wrapper classes all construct.
    • Application() still resolves through its ProgID in a single CreateObject call, so the existing path is untouched.
    • The resolver returns the real install directory, orders the type libraries [48, 1] richest-first, and resolves all 22 coclass names the library uses with none missing.
    • Both resolution routes were cross-checked and return identical CLSIDs.
  • Mutation-checked the new tests: breaking the type library index, dropping the richest-first sort, reverting _is_within to a bare prefix match, removing the install-dir short circuit, and dropping quote handling all make the suite fail.

…rogID

Some Photoshop installations, notably portable or relocated copies, register
their automation classes under a bare CLSID and never publish the versioned
`Photoshop.<Class>.<version>` ProgIDs that this library resolves classes
through. Those classes are perfectly creatable, but every wrapper raised
"Please check if you have Photoshop installed correctly.", so only
`Application()` worked.

`_get_application_object` now resolves the class ID once every ProgID lookup
has failed. The ID comes from Photoshop's own type library, which declares
every automation coclass by name, and falls back to asking each registered
Photoshop CLSID for its name over IDispatch when no type library is
available. Both routes are cached for the lifetime of the process, and a
working ProgID still wins, so existing behaviour is unchanged.

The failure message now lists each program ID that was attempted and the
outcome of the class ID fallback instead of always pointing at a missing
installation.
The previous change resolved Photoshop classes through their type library when
no versioned ProgID is published, but none of that code was exercised by the
test suite: every case stubbed the resolver out, so a completely broken type
library index still passed all 13 tests. Since CI runs without Photoshop, the
main path was the one thing CI could not catch.

test/test_clsid_resolution.py runs the resolver itself against an in-memory
registry and a fake type library, covering the type library index, the registry
walk, the install directory lookup, the LocalServer32 command parsing, and the
IDispatch probe. The suite now fails if the index returns nothing, if the
richest-library sort is dropped, or if a sibling install such as
"Photoshop 2024 Beta" is mistaken for "Photoshop 2024".

Three failure modes found while writing those tests:

- `_is_within` matched on a bare name prefix, so a sibling directory sharing a
  prefix counted as being inside the install directory and could feed another
  version's type library to LoadTypeLibEx. The comparison is now anchored on a
  path separator.
- `winreg.OpenKey` was called bare while walking the TypeLib hive, so a key that
  disappeared mid-walk raised out of the resolver instead of degrading to the
  normal "could not resolve" error. The walk now tolerates unreadable keys, and
  `_core` records the real reason when the resolver itself raises.
- A miss was cached for the life of the process even when no Photoshop had been
  seen, so a lookup that ran before Photoshop started kept failing afterwards.
  Misses are now only cached once Photoshop has actually been found, and the
  error reports whether the class is genuinely unknown or whether the registered
  classes simply could not be inspected.

Also skip the TypeLib walk entirely when there is no install directory, since
every candidate would be discarded anyway.
@loonghao
loonghao force-pushed the feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd branch from 8bdb12e to e6be49d Compare September 30, 2026 23:40
@loonghao
loonghao merged commit 0bf7d04 into main Oct 1, 2026
9 checks passed
@loonghao
loonghao deleted the feat/photoshop-python-api-photoshop-com-clsid-a0ff335153cd branch October 1, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant