Pour la fonctionalité d'Autolock sur Nuxeo Drive, nous avons besoin de savoir si un fichier est ouvert dans tel ou tel logiciel.
Certains sont assez facile à détecter, comme LibreOffice ou Microsoft Office, qui créent des fichiers temporaires reconnaissables.

Mais ce n'est ni le cas pour Photoshop, ni Illustrator, de la suite Adobe Creative. Ils utilisent un seul gros fichier temporaire protégé en lecture et on ne peut rien en tirer (Photoshop Temp\d{10}).
J'ai écrit un rapport assez complet sur l'analyse de Photoshop, si cela vous tente : Adobe Creative Suite: Photoshop (en anglais).

En fin de compte, j'ai trouvé une solution élégante : Photoshop Scripting (idem pour Illustrator).
La documentation technique et les exemples sur internet sont orientés manipulation de documents, mais rien n'indique clairement comment avoir une liste des fichiers ouverts dans le logiciel ; de manière passive.

Notez que les morceaux de code suivants sont tout aussi valables pour Photoshop qu'Illustrator.

Windows

L'idée est d'utiliser COM pour communiquer avec une instance en cours de Photoshop.
Pour ce faire, nous avons besoin du module pywin32 et ces ces quelques lignes :

from contextlib import suppress
from typing import Iterator

from win32com.client import GetActiveObject


def get_opened_files_adobe_cc_(obj: str) -> Iterator[str]:
    """ Retrieve documents path of opened files of the given *obj* (application). """
    with suppress(Exception):
        app = GetActiveObject(obj)
        for doc in app.Application.Documents:
            yield doc.fullName


def get_opened_files() -> Iterator[str]:
    """ Find opened files in Photoshop or Illustrator. """
    yield from get_opened_files_adobe_cc_("Photoshop.Application")
    yield from get_opened_files_adobe_cc_("Illustrator.Application")

macOS

Nous utiliserons AppleScript, aurons besoin des modules PyObjC Cocoa et ScriptingBridge, ainsi que de ce code :

from typing import Iterator

from AppKit import NSWorkspace
from ScriptingBridge import SBApplication


def is_running_(identifier: str) -> bool:
    """
    Check if a given application bundle identifier is found in
    the list of opened applications, meaning it is running.
    """
    shared_workspace = NSWorkspace.sharedWorkspace()
    if not shared_workspace:
        return False

    running_apps = shared_workspace.runningApplications()
    if not running_apps:
        return False

    return any(str(app.bundleIdentifier()) == identifier for app in running_apps)


def get_opened_files_adobe_cc_(identifier: str) -> Iterator[str]:
    """ Retrieve documents path of opened files of the given bundle *identifier* (application). """
    if not is_running_(identifier):
        return

    app = SBApplication.applicationWithBundleIdentifier_(identifier)
    if not (app and app.isRunning()):
        return

    try:
        documents = list(app.documents())
    except AttributeError:
        return
    if not documents:
        return

    for doc in documents:
        file_path = doc.filePath()
        if not file_path:
            # The document is not yet saved and so has no path
            continue

        yield file_path.path()


def get_opened_files() -> Iterator[str]:
    """ Find opened files in Photoshop or Illustrator. """
    yield from get_opened_files_adobe_cc_("com.adobe.Photoshop")
    yield from get_opened_files_adobe_cc_("com.adobe.Illustrator")

Utilisation

Maintenant que nous avons nos jolies fonctions, nous pouvons faire ce genre de chose :

for file in get_opened_files():
    print(file)

De rien !


Sources

Historique

  • 2020-03-20 : Amélioration du code grâce à Sourcery.
  • 2019-11-25 : Fix pour macOS.
  • 2019-10-11 : Fix pour AttributeError: 'SBApplication' object has no attribute 'documents' sur macOS.
  • 2019-05-10 : Fix pour IndexError: NSRangeException - *** -[NSArray getObjects:range:]: range {0, 2} extends beyond bounds for empty array sur macOS.
  • 2019-04-11 : Ignorance des fichiers ouverts mais non enregistrés sur macOS.
  • 2019-04-02 : Correction de is_running_() quand il n'y a pas de NSWorkspace et utilisation de runningApplication au lieu de launchedApplications (dépréciée).
  • 2019-03-16 : Refactorisation du code, ajout des sources.
  • 2019-03-07 : Correction de l'application démarrée automatiquement sur macOS.