Illustrator COM Python

From Noah.org
Jump to: navigation, search


Notes on Scripting Adobe Illustrator with Windows COM and Python

These are some very old notes I had on scripting Adobe Illustrator with COM using ActiveState Python. This stuff is probably not that useful anymore, but at the very least it demonstrates some COM programming. Python COM programming was about the best I found on Windows. It was a little tricky to get started because of poor documentation, but once I got into it I found it was actually easier than using VB.

Python speaks COM. Illustrator speaks COM. Combine the two. Unfortunately, I think Adobe is not going to be pushing the COM interface in the future. I don't know how they plan to support scripting. Last I saw they has some unusable and ugly XML "interface".

If you keep getting an "internal error" exception like:

 com_error: (-2147352567, 'Exception occurred.', (0, None, 'an internal error occurred: PARM', None, 0, -2147352577), None)

This means that Illustrator is in a bad state. You should shut down Illustrator. When you restart Illustrator the problem should go away. I suspect that Illustrator is running out of some internal resource because once you start getting these errors they will occur intermittently even if you close all documents and reopen them. Shutting down Illustrator seems to get rid of the problem for a long time.

Generally, scripting Illustrator sucks. Adobe didn't finish the interface. Not that you can blame them since Microsoft seems to be dropping support for COM in favor of SOAP. This is a shame since after years of pushing COM it had just begun to get ubiquitous and easy to use. SOAP just sucks. It's like starting all over again with something that is slower and harder to use. Maybe it 10 years it will all make sense. This is why I like UNIX. It's just easier to glue components together with scripts.

 
"""
1. Confirm that text layers are named as follows:
    Text Layer 1
    Text Layer 2
    Text Later 3
2. Add a new layer called:
    Main Text
3. Add bounded text fields to "Main Text" that hold lines from Step 1.
   Name each text field as follows:
        txt1
        txt2
        txt3
"""

import os, time
from win32com.client import Dispatch
from win32com.client import constants
import win32com

def rgb_to_cmyk (color):
    R = color.Red
    G = color.Green
    B = color.Blue
    C = 255 - R
    M = 255 - G
    Y = 255 - B
    K = min(C, M, Y)
    return (C,M,Y,K)

def cmyk_to_rgb (color):
    Cyan = color.Cyan
    Magenta = color.Magenta
    Yellow = color.Yellow
    Black = color.Black
    Cyan = min(1, Cyan * (1 - Black) + Black)
    Magenta = min(1, Magenta * (1 - Black) + Black)
    Yellow = min(1, Yellow * (1 - Black) + Black)
    return (1 - Cyan, 1 - Magenta, 1 - Yellow)

class AI:
    def __init__(self):
        #from win32com.client import gencache
        #self.tlb = gencache.EnsureModule('{14937A4C-9A34-48a9-853A-DA92323E3587}', 0, 1, 0)
        #self.ai = Dispatch("Illustrator.Application")
        # This is the best way to start Illustrator because it forces you to run the COM Makepy utility.
		# This also initializes "constants" which otherwise is not initialized even if you did run makepy.
        self.ai = win32com.client.gencache.EnsureDispatch("Illustrator.Application")
    def open (self, filename):
        self.ai.Open (filename)
    def close_all (self):
        while self.ai.Application.Documents.Count > 0:
            self.ai.Application.Documents.item(1).Close(constants.aiDoNotSaveChanges)
    def close(self):
        self.ai.Application.ActiveDocument.Close(constants.aiDoNotSaveChanges)
    def save(self):
        self.ai.Application.ActiveDocument.Save()
    def get_justification (self, paragraph):
        if paragraph.Justification == constants.aiCenter:
            return "center"
        if paragraph.Justification == constants.aiLeft:
            return "left"
        if paragraph.Justification == constants.aiRight:
            return "right"
        return None
    def get_font_info_for_layer (self, layer):
        """This returns (font, size, rgb color)"""
        for textArt in layer.TextArtItems:
            textArtRange = textArt.TextRange()
            for textCharacter in textArtRange.Characters:
                return (textCharacter.Font, textCharacter.Size, cmyk_to_rgb(textCharacter.FillColor.CMYK))
    def get_paragraph_info_for_layer (self, layer):
        """This returns (left, top, width, height, justification, content)."""
        coord_dict = {}
        for textArt in layer.TextArtItems:
            textArtRange = textArt.TextRange()
            justify = self.get_justification(textArtRange.Paragraphs[0])
            content = textArtRange.Contents
            coord_dict[textArt.Name] = (textArt.Left, textArt.Top, textArt.Width, textArt.Height, justify, content)
        return coord_dict

    def get_Main_Text (self):
        """This gets the paragraph info for the Main Text layer."""
        for layer in self.ai.Application.ActiveDocument.Layers:
            if layer.Name == 'Main Text':
                info = self.get_paragraph_info_for_layer (layer)
                return {'txt1':info['txt1'], 'txt2':info['txt2'], 'txt3':info['txt3']}
    def get_Text_Layer_fonts (self):
        """This gets the fonts of the Text Layers."""
        font_dict = {}
        for layer in self.ai.Application.ActiveDocument.Layers:
            if layer.Name == 'Text Layer 1':
                fi = self.get_font_info_for_layer (layer)
                font_dict[layer.Name] = fi[0]
            if layer.Name == 'Text Layer 2':
                fi = self.get_font_info_for_layer (layer)
                font_dict[layer.Name] = fi[0]
            if layer.Name == 'Text Layer 3':
                fi = self.get_font_info_for_layer (layer)
                font_dict[layer.Name] = fi[0]
        return  font_dict

    def select_all (self, unlock=0, unhide=0):
        """This ignores locked, hiiden layers. Unlock/unhide them if you want them.
		"""
        for l in self.ai.ActiveDocument.Layers:
            ##print l.Name, l.Locked
            if l.Locked:
                if unlock:
                    l.Locked = 0
                else:
                    continue
            if not l.Visible:
                if unhide:
                    l.Visible = 1
                else:
                    continue
            self.select_all_in_layer(l)
            
    def select_all_in_layer (self, layer):
        """ It is unclear if this actually selects everything. It selects everything that has
        subelements with a Count properly.
        """
        prop_map = layer._prop_map_get_
        for prop_name in prop_map:
            prop = getattr(layer, prop_name)
            if hasattr (prop, 'Count'):
                for i in prop:
                    i.Selected = 1

    def xml_label_designer (self):
        """This returns the active Illustrator file as Label Designer XML."""
        print '' % (self.ai.Application.ActiveDocument.Name[:-3],self.ai.Application.ActiveDocument.Name[:-3])

        text_fields = self.get_Main_Text()
        print '  '
        for key in text_fields:
            val = text_fields[key]
            x = abs(val[0])
            y = abs(val[1])
            w = abs(val[2])
            h = abs(val[3])
            justify = val[4]
            content = val[5]
            print '    ' % (key,key, x, y, w, h, justify)
            print '      %s' % (content)
            print '    '
        print '  '
        print '  '
        print '    '
        print '      0xFFFFFF'
        print '      0xFFFFFF'
        print '      0xFFFFFF'
        print '    '
        print '  '
        print '  '
        fonts = self.get_Text_Layer_fonts()
        for key in fonts:
            print '    %s   ' % (fonts[key],key)
        print '  '
        print ''
    def crop_box (self, left, top, width, height):
        self.ai.ActiveDocument.CropBox = (left, top, width, height)
    def crop_visible_unlocked (self):
        self.select_all()
        (x,y,w,h) = self.ai.ActiveDocument.GeometricBounds
        self.ai.ActiveDocument.CropBox = (x,y,w,h)
    def crop_visible_unlocked_top_left_corner (self, width, height):
        self.select_all()
        (x,y,w,h) = self.ai.ActiveDocument.GeometricBounds
        self.ai.ActiveDocument.CropBox = (x,y,x+width,y-height)
        
    def get_text_by_layer_name_and_text_name (self, layer_name, text_name):
        for l in self.ai.ActiveDocument.Layers:
            if l.Name == layer_name:
                for t in l.TextArtItems:
                    if t.Name == text_name:
                        return t
    def get_layer_by_name (self, layer_name):
        for l in self.ai.ActiveDocument.Layers:
            if l.Name == layer_name:
                return l
    def iterate_directory (self, path, ignore, func, foo = None):
        files = os.listdir(path)
        for f in files:
            if  f[-3:] != ".ai" or f in ignore:
                continue
            print f
            self.open(os.path.join(path, f))
            func(self, foo)
            self.save()
            self.close()
    def add_crappy_text (self):
        src_txt_1 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt1")
        src_txt_2 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt2")
        src_txt_3 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt3")

        copy_doc_ref = self.ai.ActiveDocument
        filename = self.ai.ActiveDocument.FullName
        path = os.path.dirname(filename)
        files = os.listdir(path)
        count = 0
        for f in files:
            if f[-3:] == ".ai" and not f == os.path.basename(filename):
                count = count + 1
 #               if count >4:
 #                   continue
                print f
                self.open (os.path.join(path, f))
                dest_doc_ref = self.ai.ActiveDocument
                dest_txt1 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt1")
                dest_txt2 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt2")
                dest_txt3 = self.get_text_by_layer_name_and_text_name ("Main Text", "txt3")
                dest_txt1.Top = src_txt_1.Top
                dest_txt1.Left = src_txt_1.Left
                dest_txt2.Top = src_txt_2.Top
                dest_txt2.Left = src_txt_2.Left
                dest_txt3.Top = src_txt_3.Top
                dest_txt3.Left = src_txt_3.Left
##                copy_doc_ref.Activate()
##                reference_3.Copy()
##                dest_doc_ref.Activate()
##                newlayer.Paste()
                self.save()
                self.close()

#            print str(layer.name)
#a.close_all()
#a.open (r"C:\Documents and Settings\x\Desktop\CIPHIX\capricorn_mod_03.ai")
#a.xml_label_designer()
#a.add_crappy_text()
    def iterate_everything (self, object, list=[]):
        if object in list:
            return list
        list.append(object)
        
        pm = object._prop_map_get_
        for prop_name in pm:
            #if prop_name in ['GroupItems','GraphItems']:
            if prop_name in ['Parent',]:
                continue
            #om = getattr(pm, prop_name)
#            print prop_name
            try:
                om = getattr(object, prop_name)
                if om in list:
                    continue
                if hasattr(om, 'Count'):
    #                print prop_name, 'is iterable'
                    self.iterate_everything(om,list)
            except:
                pass
#                print prop_name

        if hasattr(object, 'Count'):
#            print 'Object is iterable.'
            for so in object:
                self.iterate_everything (so, list)

        return list

def normalize_crop_and_art_location(a, foo):
    print a.ai.ActiveDocument.FullName
    l = a.get_layer_by_name("Layer 1")
    print l.Name
    #for l in munch.Layers:
    g = l.GroupItems[0]
    g.Left = 200
    g.Top = 500
    a.crop_box(200,500,200+288,500-252)
#    for t in l.TextArtItems:
#        print t.Contents
#        t.Hidden = 1

def normalize_crop_and_art_location2(a, foo):
    print a.ai.ActiveDocument.FullName
    l = a.get_layer_by_name("Layer 1")
    print l.Name
    #for l in munch.Layers:
    g = l.GroupItems[0]
    g.Left = 200
    g.Top = 500
    a.crop_box(200,500,200+288,500-252)
#    for t in l.TextArtItems:
#        print t.Contents
#        t.Hidden = 1


def LabelWineInfoLayerText (a, foo):
    l = a.get_layer_by_name("Wine Info")
    for t in l.TextArtItems:
        if "20"==t.Contents[0:2]:
            print "Yup"
            t.Name = "wi1"

def StampXMLTemplate (a, home):
    basename = os.path.basename(a.ai.ActiveDocument.FullName[0:-3])
    path = os.path.join (home, basename + '.xml')
    fout = file (path, "w")
    output = """
      
        
          Happy Birthday Markus Arielus
        
        
          Love Always Melisa
        
        
          January 7, 2003
        
      
      
        
          0x000000
          0x000000
          0x000000
        
      
      
        AGaramond
        Copperplate30bc
        Shelley Allegro Script
      
      """
    fout.write (output % (basename, basename))
    fout.close ()
    
def MakeThumbnails (a, home):
    basename = os.path.basename(a.ai.ActiveDocument.FullName[0:-3])
    eo = win32com.client.gencache.EnsureDispatch("Illustrator.ExportOptionsJPEG")
    eo.QualitySetting = 70
    eo.AntiAliasing = 1

    l = a.get_layer_by_name ("Main Text")
    l.Visible = 1

    eo.HorizontalScale = 121.0 / 288.0  * 100.0
    eo.VerticalScale = 108.0 / 252.0 * 100.0
    path = os.path.join (home, 'browse')
    path = os.path.join (path, basename + '_lib')
    print path
    a.ai.ActiveDocument.Export (path, constants.aiJPEG, eo)

##    eo.HorizontalScale = 130.0 / 288.0  * 100.0
##    eo.VerticalScale = 114.0 / 252.0 * 100.0
##    path = os.path.join (home, 'thumbs')
##    path = os.path.join (path, basename + '_lib')
##    print path
##    a.ai.ActiveDocument.Export (path, constants.aiJPEG, eo)
##
##    eo.HorizontalScale = 375.0 / 288.0  * 100.0
##    eo.VerticalScale = 329.0 / 252.0 * 100.0
##    path = os.path.join (home, 'color_themes')
##    path = os.path.join (path, basename + '_lib')
##    print path
##    l = a.get_layer_by_name ("Main Text")
##    l.Visible = 0
##    a.ai.ActiveDocument.Export (path, constants.aiJPEG, eo)

    l = a.get_layer_by_name ("Main Text")
    l.Visible = 1

def crop_top_left_corner (a, foo):
    a.crop_visible_unlocked_top_left_corner(288,252)
def hide_crappy_layers (a, foo):
    try:
        for l in a.ai.ActiveDocument.Layers:
            if l.Name[0:len("text option")] == "text option":
                l.Locked = 1
                l.Visible = 0
    except:
        print l.Name
def copy_text_options (a, model):            
    dest = a.ai.ActiveDocument
    if model.FullName == dest.FullName:
        print "skipping", model.FullName
        return
    model.Activate()
    src_txt_1 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt1")
    src_txt_2 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt2")
    src_txt_3 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt3")
    dest.Activate()
    destnewlayer = dest.Layers.Add()
    destnewlayer.Name = "Main Text"
    model.Activate()
    src_txt_1.Copy()
    dest.Activate()
    destnewlayer.Paste()
    model.Activate()
    src_txt_2.Copy()
    dest.Activate()
    destnewlayer.Paste()
    model.Activate()
    src_txt_3.Copy()
    dest.Activate()
    destnewlayer.Paste()

    dest.Activate()    
    dest_txt1 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt1")
    dest_txt2 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt2")
    dest_txt3 = a.get_text_by_layer_name_and_text_name ("Main Text", "txt3")
    dest_txt1.Top = src_txt_1.Top
    dest_txt1.Left = src_txt_1.Left
    dest_txt2.Top = src_txt_2.Top
    dest_txt2.Left = src_txt_2.Left
    dest_txt3.Top = src_txt_3.Top
    dest_txt3.Left = src_txt_3.Left

#    l = a.get_layer_by_name("Layer 1")
#    g = l.GroupItems[0]
#    for t in g.TextArtItems:
#        if "Happy"==t.Contents[0:5]:
#            t.Hidden=1
    
a = AI()
#a.close_all()
#model = a.ai.ActiveDocument
#l = a.ai.ActiveDocument.ActiveLayer
#a.select_all(unlock=0,unhide=0)
#a.crop_visible_unlocked_top_left_corner(288,252)

#a.iterate_directory(r'C:\Documents and Settings\x\Desktop\label_repository\astrology\elements.test', [], crop_top_left_corner)
a.iterate_directory(r'C:\Documents and Settings\x\Desktop\label_repository\astrology\elements.test', [], hide_crappy_layers)

#a.iterate_directory(r'C:\Documents and Settings\x\Desktop\label_repository\astrology\constellation', [], StampXMLTemplate, r'C:\Documents and Settings\x\Desktop\label_repository\astrology\constellation')
#a.iterate_directory(r'C:\Documents and Settings\x\Desktop\label_repository\astrology\constellation', [model.Name], copy_text_options, model)
#a.iterate_directory(r'C:\Documents and Settings\x\Desktop\label_repository\astrology\constellation', normalize_crop_and_art_location)

#l = a.iterate_everything(a.ai.ActiveDocument)
#pprint (l)

print "Done."

##filename = a.ai.ActiveDocument.FullName
##path = os.path.dirname(filename)
##files = os.listdir(path)
##for f in files:
##    if f[-3:] == ".ai":
##        print f
##        a.open (os.path.join(path, f))
##        a.standard_crop_box()
##        a.save()
##        a.close()
##
#a.open(filename)