Friday, August 29, 2008

Read-only Windows files with Python

How do you use Python to get or change read-only/writeable access on files in Windows? The Python docs don't answer this in a direct manner. Here's one option using only the standard library.

import os, stat
myFile = r'C:\stuff\grail.txt'

fileAtt = os.stat(myFile)[0]
if (not fileAtt & stat.S_IWRITE):
   # File is read-only, so make it writeable
   os.chmod(myFile, stat.S_IWRITE)
else:
   # File is writeable, so make it read-only
   os.chmod(myFile, stat.S_IREAD)
You may prefer the pywin32 extensions for this sort of thing...
import win32api, win32con
myFile = r'C:\stuff\grail.txt'

fileAtt = win32api.GetFileAttributes(myFile)
if (fileAtt & win32con.FILE_ATTRIBUTE_READONLY):
   # File is read-only, so make it writeable
   win32api.SetFileAttributes(myFile, ~win32con.FILE_ATTRIBUTE_READONLY)
else:
   # File is writeable, so make it read-only
   win32api.SetFileAttributes(myFile, win32con.FILE_ATTRIBUTE_READONLY)
Or, more concisely with win32:
roAtt = win32api.GetFileAttributes(myFile) & win32con.FILE_ATTRIBUTE_READONLY
win32api.SetFileAttributes(myFile, ~roAtt)
Using win32 you can also set other Windows file attributes (unlike os.chmod), but read/write is usually all I care about.

Friday, August 22, 2008

ColladaMax for 3ds Max 2009 64-bit

I was unable to find the ColladaMax importer/exporter plugin for 3ds Max 2009 64-bit, so I built one. It's from the 3.05b source.

Update 02/03/09 - I made a new build that depends on an older version of the DirectX SDK. It should fix the "failed to initialize" error some of you were getting, without the need to install anything else.

Feel free to grab it if you like:
ColladaMax_2009x64.rar (709 KB)

Tuesday, August 5, 2008

Photoshop scripting with Python

Photoshop natively supports scripting with AppleScript, JavaScript and VBScript. While Python is notably absent from that list, it can still be used to automate nearly anything in Photoshop. This is thanks to the extensive COM interface Photoshop provides.

The methods here are similar to those used in my GDC 2008 Python lecture, about driving 3ds Max via Python. You start by dispatching the Photoshop COM server, using Python as the client:

import win32com.client
psApp = win32com.client.Dispatch("Photoshop.Application")
This connects to your already-opened Photoshop session, or opens one if none are running. The root COM object is then assigned to psApp, and you're ready to do some cool stuff. Here's a quick example:
psApp.Open(r"D:\temp\blah.psd")         # Opens a PSD file
doc = psApp.Application.ActiveDocument  # Get active document object
layer = doc.ArtLayers[0]                # Get the bottom-most layer
layer.AdjustBrightnessContrast(20,-15)  # Bright +20, Contrast -15
doc.Save()                              # Save the modified PSD
Here's a more complex example. This script recursively scans a folder for PSD files, exporting various textures contained inside. One PSD can have specifically-named Layer Groups, each of which is written to a separate PNG file with a specific suffix. If a Group contains several layers, they're flattened when exported, allowing you to keep all your layered effects intact in the PSD.

In the example below, a group named "diffuse" is exported as "psdname_D.png", the "normal" group as "psdname_N.png", and so on. The exportType dictionary determines the name/suffix pairs.
# Recursively scans a folder (psdRoot) for Photoshop PSD files.
# For each, exports various 24-bit PNG textures based on layer
# groups found in the PSD.
# Requires the Win32 Extensions:
# http://python.net/crew/mhammond/win32/

import win32com.client
import os

# Change to match your root folder
psdRoot = r'C:\ArtFiles\PSD'

# Map of layer group names and the suffixes to use when exporting
exportTypes = {'diffuse':'_D', 'normal':'_N', 'specular':'_S'}

if (__name__ == '__main__'):
   # COM dispatch for Photoshop
   psApp = win32com.client.Dispatch('Photoshop.Application')

   # Photoshop actually exposes several different COM interfaces,
   # including one specifically for classes defining export options.
   options = win32com.client.Dispatch('Photoshop.ExportOptionsSaveForWeb')
   options.Format = 13   # PNG
   options.PNG8 = False  # Sets it to PNG-24 bit

   # Get all PSDs under root dir
   psdFiles = []

   for root, dir, files in os.walk(psdRoot):
      for thisFile in files:
         if (thisFile.lower().endswith('.psd')):
            fullFilename = os.path.join(root, thisFile)
            psdFiles.append(fullFilename)

   # Loop through PSDs we found
   for psdFile in psdFiles:
      doc = psApp.Open(psdFile)
      layerSets = doc.LayerSets

      if (len(layerSets) > 0):
         # First hide all root-level layers
         for layer in doc.Layers:
            layer.Visible = False
         # ... and layerSets
         for layerSet in layerSets:
            layerSet.Visible = False
           
         # Loop through each LayerSet (aka Group)
         for layerSet in layerSets:
            lsName = layerSet.Name.lower()

            if (lsName in exportTypes):
               layerSet.Visible = True  # make visible again

               # Make our export filename
               pngFile = os.path.splitext(psdFile)[0] + exportTypes[lsName] + '.png'

               # If PNG exists but older than PSD, delete it.
               if (os.path.exists(pngFile)):
                  psdTime = os.stat(psdFile)[8]
                  pngTime = os.stat(pngFile)[8]
        
                  if (psdTime > pngTime):
                     os.remove(pngFile)

               # Export PNG for this layer Group
               if (not os.path.exists(pngFile)):
                  doc = psApp.Open(psdFile)
                  doc.Export(ExportIn=pngFile, ExportAs=2, Options=options)
                  print 'exporting:', pngFile
               else:
                  print 'skipping newer file:', psdFile
                 
               # Make LayerSet invisible again
               layerSet.Visible = False

         # Close PSD without saving
         doc.Close(2)
It only exports when the PNG is missing or older than the PSD. This makes it good for running a batch texture export on your project's entire texture tree.

Here is a ZIP containing the above script and a sample PSD file to try it on: exportTextureLayers.zip (143 KB)

I imagine you can do all of the above with the native Photoshop scripting. I just think it's cool being able to use Python instead of rooting through a language I'm less familiar with. Dinosaurs were roaming the earth the last time I tried anything in VB.

If you dig this, I'd recommend reading the Photoshop CS5 Scripting Guide and Photoshop CS5 VBScript Reference found in the Adobe Photoshop Developer Center. While the above wasn't VBScript, the COM interface we used is nearly identical.