lohriialo / indesign-scripting-python

Scripting in InDesign is used to automate a wide variety of repetative task or as complex as an entire new feature
145 stars 26 forks source link

Class to call inDesign from Python with test code #15

Open scotth1963 opened 2 years ago

scotth1963 commented 2 years ago

===========================================================

indesign-class.py

Created: 07 JUNE 2022

author = 'Scott H' version = '1.0'

""" This is a class to generate an InDesign document """

""" Instructions are below in the main """

===========================================================

import win32com.client import os from win32com.client import Dispatch from enum import Enum from PIL import Image import pythoncom

class TextWrapOptions(Enum): idBoundingBoxTextWrap = 1651729523 # from enum idTextWrapModes idContour = 1835233134 # from enum idTextWrapModes idJumpObjectTextWrap = 1650552420 # from enum idTextWrapModes idNextColumnTextWrap = 1853384306 # from enum idTextWrapModes idNone = 1852796517 # from enum idTextWrapModes

===========================================================

Values to use when definining text justification in a

textframe

===========================================================

class Justify(Enum): AWAY_FROM_BINDING_SIDE = 1633772147 # from idJustification CENTER_ALIGN = 1667591796 # from idJustification CENTER_JUSTIFIED = 1667920756 # from idJustification FULLY_JUSTIFIED = 1718971500 # from idJustification LEFT_ALIGN = 1818584692 # from idJustification LEFT_JUSTIFIED = 1818915700 # from idJustification RIGHT_ALIGN = 1919379572 # from idJustification RIGHT_JUSTIFIED = 1919578996 # from idJustification TO_BINDING_SIDE = 1630691955 # from idJustification

===========================================================

Values to use when defining an image frame fit options

===========================================================

class FitOptions(Enum): APPLY_FRAME_FITTING_OPTIONS = 1634100847 # from enum idFitOptions CENTER_CONTENT = 1667591779 # from enum idFitOptions CONTENT_AWARE_FIT = 1667327593 # from enum idFitOptions CONTENT_TO_FRAME = 1668575078 # from enum idFitOptions FILL_PROPORTIONALLY = 1718185072 # from enum idFitOptions FRAME_TO_CONTENT = 1718906723 # from enum idFitOptions PROPORTIONALLY = 1668247152 # from enum idFitOptions

===========================================================

Values to use when we vertically justify text

===========================================================

class TextVJust(Enum): BOTTOM_ALIGN = 1651471469 # from enum idVerticalJustification CENTER_ALIGN = 1667591796 # from enum idVerticalJustification JUSTIFY_ALIGN = 1785951334 # from enum idVerticalJustification TOP_ALIGN = 1953460256 # from enum idVerticalJustification

========================================================================

========================================================================

====== InDesign Process ======

====== ======

====== This class will generate an inDesign Document. ======

====== Steps to creating an inDesign Document: ======

====== ======

====== 1 - Create an app cursor by attaching to C library ======

====== 2 - Use app cursor to create a in memory document ======

====== 3 - Set the Page (starting with page 1) ======

====== ======

========================================================================

========================================================================

class inDesign_Process: def init(self): self.in_des_app = self.set_in_des_app() self.in_des_doc = self.set_in_des_doc() self.page_ptr = 1 # defines the page we are on self.lst_flds = [] self.lst_ty = [] self.lst_tx = [] self.lst_by = [] self.lst_bx = []

    self.text_start = 4  # where to start text
    self.text_end = 49  # when to start a new line
    self.in_des_page = self.set_page()
    self.geo_bound_ptr = self.reset_geo_bnds()
    self.file_name = ""
    self.dct_letter_weight = {
        "&": 0.75,
        "-": 0.6,
        "1": 0.6,
        "2": 0.6,
        "3": 0.6,
        "4": 0.65,
        "5": 0.65,
        "6": 0.7,
        "7": 0.7,
        "8": 0.65,
        "9": 0.65,
        "0": 0.65,
        ",": 0.3,
        ".": 0.3,
        ":": 0.3,
        " ": 0.2,
        "a": 0.6,
        "b": 0.6,
        "c": 0.57,
        "d": 0.6,
        "e": 0.65,
        "f": 0.36,
        "g": 0.65,
        "h": 0.65,
        "i": 0.3,
        "j": 0.3,
        "k": 0.6,
        "l": 0.3,
        "m": 0.9,
        "n": 0.65,
        "o": 0.65,
        "p": 0.65,
        "q": 0.65,
        "r": 0.375,
        "s": 0.6,
        "t": 0.375,
        "u": 0.6,
        "v": 0.6,
        "w": 0.85,
        "x": 0.6,
        "y": 0.6,
        "z": 0.6,
        "A": 0.75,
        "B": 0.75,
        "C": 0.75,
        "D": 0.75,
        "E": 0.75,
        "F": 0.6,
        "G": 0.75,
        "H": 0.75,
        "I": 0.375,
        "J": 0.6,
        "K": 0.75,
        "L": 0.6,
        "M": 1,
        "N": 0.75,
        "O": 0.75,
        "P": 0.75,
        "Q": 0.75,
        "R": 0.75,
        "S": 0.75,
        "T": 0.75,
        "U": 0.75,
        "V": 0.75,
        "W": 1,
        "X": 0.75,
        "Y": 0.75,
        "Z": 0.75,
    }

# ===========================================================
# Setup Step 1 Create an App Cursor
# ===========================================================

def set_in_des_app(self):
    return win32com.client.Dispatch(
        "InDesign.Application.2020", pythoncom.CoInitialize()
    )

# ===========================================================
# Setup Step 2 Create Document from App Cursor
# ===========================================================

def set_in_des_doc(self):
    return self.in_des_app.Documents.Add()

# ===========================================================
# Setup Step 3 Create Page in Document
# ===========================================================
def set_page(self):
    self.geo_bound_ptr = self.reset_geo_bnds() # Fixing Image position maybe

    if self.page_ptr != 1:
        self.in_des_doc.Pages.Add()
    return self.in_des_doc.Pages.Item(self.page_ptr)

# ===========================================================
# Get a Text frame to be used above
# ===========================================================
def get_text_frame(self):
    return self.in_des_page.TextFrames.Add()

# ===========================================================
# Get a new image rectangle when adding image (see add_image)
# ===========================================================
def get_image_rectangle(self):
    return self.in_des_page.Rectangles.Add()

# ===========================================================
# Move to the next page by
#    - incrementing the page pointer
#    - set the next inDesign page to write to
#    - reset the Geo Boundary pointer to be at top of page
# ===========================================================
def set_next_page(self, imgs_on_page=2):

    self.page_ptr += 1
    self.in_des_page = self.set_page()
    self.geo_bound_ptr = self.reset_geo_bnds()

# ===========================================================
# Set the name of the file to create (full path)
# ===========================================================
def set_filename(self, file_in):
    self.file_name = file_in

# ===========================================================
# Save the file from memory onto disk at the file_name
# defined above
# ===========================================================

def save_in_des_file(self):
    ret_value = False
    directory = os.path.dirname(self.file_name)
    try:
        # If file path does not exist, create directory
        if not os.path.exists(directory):
            os.makedirs(directory)
        # If file path exist, save the file to the path
        if os.path.exists(directory):
            self.in_des_doc.Save(self.file_name)
        ret_value = True
    except Exception as e:
        print("Export to inDesign failed: " + str(e))
    return ret_value

# ===========================================================
# Close the inDesign Document when done
# ===========================================================
def close_des_file(self):
    self.in_des_doc.Close()

# ===========================================================
# ====                                                  =====
# ====                  IMAGE FUNCTIONS                 =====
# ====                                                  =====
# ===========================================================

# ===========================================================
# get the image size to be used to calculate geo boundaries
# when adding an image. image size is a tuple of
# (width, height) (see add_image)
# ===========================================================
def get_image_size(self, img_in):
    im = Image.open(img_in)
    ret_value = im.size
    im.close()
    return ret_value

# ===========================================================
# get_image_thumb_size will get the size of the image 
# thumbnail. If the image is taller than wider we use
# 400 pixels, if wider than taller we use 900 pixels. This 
# will return the size tuple (width, height) of a thumbnail
# that keeps image perspective.
# ===========================================================
def get_image_thumb_size(self, img_in):
    img = Image.open(img_in)
    img_size = img.size
    img_width = img_size[0]
    img_height = img_size[1]
    if img_width > img_height :
        img.thumbnail((900, 900), Image.ANTIALIAS)
    else:
        img.thumbnail((300, 300), Image.ANTIALIAS)
    ret_value = img.size
    img.close()
    return ret_value

def get_img_per(self,img_size_in,thumb_size_in):
    lst_ret_value = []
    for item in range(len(thumb_size_in)):
        lst_ret_value.append(int(thumb_size_in[item]/img_size_in[item]*100))        
    return tuple(lst_ret_value)

# ===========================================================
# ====                                                  =====
# ====             GROUPING FUNCTIONALITY               =====
# ====                                                  =====
# ===========================================================

# ===========================================================
# start_grouping - resets the list of fields and geoboundaries
# so a new set of textboxes can be grouped together
# ===========================================================
def start_grouping(self):
    self.lst_flds = []
    self.lst_ty = []
    self.lst_tx = []
    self.lst_by = []
    self.lst_bx = []

# ===========================================================
# add_to_group will take the field passed in, save that in 
# the list of fields, get the geoboundary of the textbox and
# save each item in its appropriate list (top y in ty, etc)
# ===========================================================
def add_to_group(self,fld_in):
    self.lst_flds.append(fld_in)
    geo_bnd = fld_in.GeometricBounds
    self.lst_ty.append(geo_bnd[0])
    self.lst_tx.append(geo_bnd[1])
    self.lst_by.append(geo_bnd[2])
    self.lst_bx.append(geo_bnd[3])

# ===========================================================
# end_grouping will take all the textframes stored and group
# them together.
# ===========================================================
def end_grouping(self):
    lst_items = self.lst_flds[:]
    group_ret_value = lst_items[0]
    for item in range(1,len(lst_items)):
        lst_items[item].PreviousTextFrame = lst_items[item-1]
    for item in range(1,len(lst_items)):
        lst_items[item].Delete()

    self.lst_ty.sort()   
    self.lst_tx.sort()   
    self.lst_by.sort(reverse=True)   
    self.lst_bx.sort(reverse=True)   

    top_y = int(self.lst_ty[0])
    top_x = int(self.lst_tx[0])
    bot_y = int(self.lst_by[0])
    bot_x = int(self.lst_bx[0])

    lst_group_bound = ["0","0","0","0"]
    lst_group_bound = self.set_top_y(lst_group_bound,top_y)
    lst_group_bound = self.set_top_x(lst_group_bound,top_x)
    lst_group_bound = self.set_bot_y(lst_group_bound,bot_y)
    lst_group_bound = self.set_bot_x(lst_group_bound,bot_x)
    group_ret_value.NextTextFrame = None
    group_ret_value.Fit(FitOptions.FRAME_TO_CONTENT.value) 
    group_ret_value.GeometricBounds = lst_group_bound
    group_ret_value.TextFramePreferences.VerticalJustification = (
            TextVJust.CENTER_ALIGN.value
        )
    self.geo_bound_ptr = lst_group_bound
    # print(f"after group :{self.geo_bound_ptr}")
    self.geo_bnds_next_line()  # start on new line
    # print(f"after group next line:{self.geo_bound_ptr}")

# ===========================================================
# ====                                                  =====
# ====             GEOBOUND FUNCTIONALITY               =====
# ====                                                  =====
# ===========================================================
# ===========================================================
# Geometric Bounds Functions
#
# Geometric Bounds [ty, tx, by, bx] or (y1, x1, y2, x2)
# maps the top left the bottom right of a box
# NOTE: y is first, by using a set of wrapper functions
# that will be something that the user won't need to remember
# ===========================================================

# ===========================================================
# Reset Geo Boundary - when starting a new page, the
# geo boundary should be reset to the top of the page
# ===========================================================
def reset_geo_bnds(self):
    ret_value = ["1", "2", "3", "4"]  # geo bound placeholders
    ret_value = self.set_top_x(ret_value, self.text_start)
    ret_value = self.set_top_y(ret_value, self.text_start)
    ret_value = self.set_bot_x(ret_value, self.text_start)
    ret_value = self.set_bot_y(ret_value, self.text_start + 1)
    return ret_value

# ===========================================================
# setting ty of [ty, tx, by, bx]
# val_in is an integer (ex 4)
# we make 4p0 as ty
# ===========================================================
def set_top_y(self, geo_bnd_in, val_in):
    ret_value = geo_bnd_in[:]
    top_x_adj = f"{val_in}p0"
    ret_value[0] = top_x_adj
    return ret_value

# ===========================================================
# setting tx of [ty, tx, by, bx]
# val_in is an integer (ex 4)
# we make 4p0 as tx
# ===========================================================
def set_top_x(self, geo_bnd_in, val_in):
    ret_value = geo_bnd_in[:]
    top_y_adj = f"{val_in}p0"
    ret_value[1] = top_y_adj
    return ret_value

# ===========================================================
# setting by of [ty, tx, by, bx]
# val_in is an integer (ex 4)
# we make 4p0 as by
# ===========================================================
def set_bot_y(self, geo_bnd_in, val_in):
    ret_value = geo_bnd_in[:]
    bot_x_adj = f"{val_in}p0"
    ret_value[2] = bot_x_adj
    return ret_value

# ===========================================================
# setting bx of [ty, tx, by, bx]
# val_in is an integer (ex 4)
# we make 4p0 as bx
# ===========================================================
def set_bot_x(self, geo_bnd_in, val_in):
    ret_value = geo_bnd_in[:]
    bot_y_adj = f"{val_in}p0"
    ret_value[3] = bot_y_adj
    return ret_value

# ===========================================================
# getting ty of [ty, tx, by, bx]
# removing the p0 from ty (ex 4p0)
# returning the integer 4
# ===========================================================
def get_top_y(self, geo_bnd_in):
    return int(geo_bnd_in[0].split("p")[0])

# ===========================================================
# getting tx of [ty, tx, by, bx]
# removing the p0 from tx (ex 4p0)
# returning the integer 4
# ===========================================================
def get_top_x(self, geo_bnd_in):
    return int(geo_bnd_in[1].split("p")[0])

# ===========================================================
# getting by of [ty, tx, by, bx]
# removing the p0 from by (ex 4p0)
# returning the integer 4
# ===========================================================
def get_bot_y(self, geo_bnd_in):
    return int(geo_bnd_in[2].split("p")[0])

# ===========================================================
# getting bx of [ty, tx, by, bx]
# removing the p0 from bx (ex 4p0)
# returning the integer 4
# ===========================================================
def get_bot_x(self, geo_bnd_in):
    return int(geo_bnd_in[3].split("p")[0])

# ===========================================================
# Geometric Boundaries Next Line
#
# This function will go through setting the geo boundaries
# to go to the next line.
#   - Move the new top y to bot y
#   - Add 1 to bot y
#   - Set botx and topx to text_start (defined in init)
#   - Return the new Geo Boundary
# ===========================================================

def geo_bnds_next_line(self):
    next_geo = self.geo_bound_ptr[:]
    # set top equal to bottom
    bot_y_val = self.get_bot_y(next_geo)
    next_geo = self.set_top_y(next_geo, bot_y_val)
    bot_y_val += 1  # add 1
    next_geo = self.set_bot_y(next_geo, bot_y_val)
    # set to self.text_start+2 if you want to indent
    next_geo = self.set_top_x(next_geo, self.text_start)
    next_geo = self.set_bot_x(next_geo, self.text_start)
    self.geo_bound_ptr = next_geo

# ===========================================================
# Add Image PUBLIC FACING FUNCTION
#
# This function will go through the process of adding an
# image to the page
#   - Check to see if this is a new page if not move down
#     4 lines
#   - Get a new image rectangle
#   - Get the size of the image (length,height)
#   - Get the image rectange geo boundary size (rec_geo_bnd)
#   - Set the image size to 30%
#   - readjust the geo boundary for text under image
# ===========================================================

def add_image(self, img_in):
    my_frame = self.get_image_rectangle()
    img_size = self.get_image_size(img_in)
    thumb_size = self.get_image_thumb_size(img_in)
    #print(f"thumb_size :{thumb_size}")
    rec_geo_bnd = self.geo_get_image_spacing(thumb_size)
    rec_geo_bnd = self.image_geo_adjust(rec_geo_bnd,thumb_size)
    #print(f"rec_geo_bnd :{rec_geo_bnd}")
    rec_geo_bnd = self.geo_bnd_page_adj(rec_geo_bnd) # adjust for page
    my_frame.GeometricBounds = rec_geo_bnd
    my_graphic_list = my_frame.Place(img_in)
    horiz_per,vert_per = self.get_img_per(img_size,thumb_size)
    #print(f"horiz_per:{horiz_per}  vert_per: {vert_per}")
    my_frame.HorizontalScale = horiz_per
    my_frame.VerticalScale = vert_per

    my_object_style = self.in_des_doc.ObjectStyles.Add()
    my_object_style.EnableStroke = True
    my_object_style.StrokeWeight = 3
    my_object_style.StrokeType = self.in_des_doc.StrokeStyles.Item("Solid")
    my_object_style.StrokeColor = self.in_des_doc.Colors.Item("Black")
    my_frame.ApplyObjectStyle(my_object_style, True)
    my_frame.Fit(FitOptions.FRAME_TO_CONTENT.value)
    # ===================================================
    # if we use the geo bounds for images, they move
    # the text too far down, we need to adjust the
    # geo boundaries to more "text like" before
    # updating geo_bound_ptr
    # ===================================================
    self.geo_bound_ptr = self.text_geo_adjust(rec_geo_bnd, thumb_size)
    self.geo_bnds_next_line()  # start on newline for text

def px_to_geo(self,val_in):
    return int(val_in/35)

def image_geo_adjust(self, geo_bnd_in, thumb_size_in):
    ret_value = geo_bnd_in[:]
    bot_y = self.get_bot_y(ret_value)
    delta_y = self.px_to_geo(thumb_size_in[1])
    bot_y = bot_y + delta_y
    ret_value = self.set_bot_y(ret_value,bot_y)
    return ret_value

def geo_get_image_spacing(self, thumb_size_in):
    ret_value = self.geo_bound_ptr[:]  # start where text left off
    #print(f"1 image_spacing :{ret_value}")
    top_x = self.get_top_x(ret_value)
    bot_x = self.get_bot_x(ret_value)
    top_y = self.get_top_y(ret_value)
    bot_y = self.get_bot_y(ret_value)
    if top_x < 6:
        top_x = 6
    if top_y < 6:
        top_y = 6
    else:
        thumb_height = thumb_size_in[1]
        delta_y = self.px_to_geo(thumb_height)
        top_y = top_y + delta_y
        bot_y = bot_y + delta_y
    thumb_width = thumb_size_in[0]
    geo_wdth = self.px_to_geo(thumb_width)
    delta_x = int((49 - geo_wdth)/2)
    top_x = top_x + delta_x
    bot_x = bot_x + delta_x
    ret_value = self.set_top_x(ret_value,top_x)
    ret_value = self.set_bot_x(ret_value,bot_x)
    ret_value = self.set_top_y(ret_value,top_y)
    ret_value = self.set_bot_y(ret_value,bot_y)
    #print(f"2 image_spacing :{ret_value}")
    return ret_value

def text_geo_adjust(self, geo_bnd_in, thumb_size_in):
    ret_value = geo_bnd_in[:]
    top_y = self.get_top_y(ret_value)
    delta_y = self.px_to_geo(thumb_size_in[1])
    top_y = top_y + delta_y
    ret_value = self.set_top_y(ret_value,top_y)
    return ret_value

# ===========================================================
# Geo Boundary Page Adjustment.
#
# Odd # pages except 1 (ex 3,5,7) need to have the top and
# bottom x moved over to appear on the right page. 55 seems
# to be a good number.
# ===========================================================
def geo_bnd_pg_adj(self, geo_bnd_in):
    ret_value = geo_bnd_in[:]
    if self.page_ptr % 2 == 1 and self.page_ptr > 1:
        top_x = self.get_top_x(ret_value)
        bot_x = self.get_bot_x(ret_value)
        ret_value = self.set_top_x(ret_value, top_x + 45)
        ret_value = self.set_bot_x(ret_value, bot_x + 45)
    return ret_value

# ===========================================================
# Text Functions Public Facing Functions
# ===========================================================

# ===========================================================
# Wrapper to get the next line
# ===========================================================
def set_next_line(self):
    self.geo_bnds_next_line()

# ===========================================================
# Add text defined as a label
# - Text is bolded
# - Text is right aligned
# ===========================================================

def add_label(self, word_in):
    self.geo_bnds_right(word_in)
    label_frame = self.get_text_frame()
    geo_bound_adj = self.geo_bnd_page_adj(self.geo_bound_ptr)
    label_frame.TextFramePreferences.VerticalJustification = (
        TextVJust.CENTER_ALIGN.value
    )
    label_frame.GeometricBounds = geo_bound_adj
    label_frame.texts[0].pointSize = "12pt"
    label_frame.ParentStory.AppliedFont = "Arial"
    label_frame.ParentStory.FontStyle = "Bold"
    label_frame.ParentStory.Justification = Justify.RIGHT_ALIGN.value  # Right Align
    label_frame.Contents = word_in
    self.add_to_group(label_frame)

# ===========================================================
# Add text defined as plain text
# - if the text is going over the end of the line go to the
#   next line
# ===========================================================
def add_text(self, word_in, split_text=False):
    word_weight = int(self.get_word_points(word_in))
    # print(f"word_in :{word_in}")
    self.geo_bnds_right(word_in)
    text_frame = self.get_text_frame()
    geo_bound_adj = self.geo_bound_ptr # don't adjust till afterwards
    if word_weight+self.text_start > self.text_end:
        bot_y = self.get_bot_y(geo_bound_adj)
        geo_bound_adj = self.set_top_x(geo_bound_adj, self.text_start)
        geo_bound_adj = self.set_bot_x(geo_bound_adj, self.text_end - 1)
        if (word_weight / self.text_end).is_integer():
            bot_y_delta = int(word_weight / self.text_end)
        else:
            bot_y_delta = int(word_weight / self.text_end) + 2
        geo_bound_adj = self.set_bot_y(geo_bound_adj, bot_y + bot_y_delta + 1)
        self.geo_bound_ptr = self.set_bot_y(
            self.geo_bound_ptr, bot_y + bot_y_delta + 1
        )  # set geo pointer's bottom y
    geo_bound_adj = self.geo_bnd_page_adj(geo_bound_adj) # see if moving after sizing will put on proper page

    # print(f"geo_bound_adj after :{geo_bound_adj}")
    text_frame.TextFramePreferences.VerticalJustification = (
        TextVJust.CENTER_ALIGN.value
    )
    text_frame.GeometricBounds = geo_bound_adj
    text_frame.texts[0].pointSize = "12pt"
    text_frame.ParentStory.AppliedFont = "Arial"
    text_frame.Contents = word_in + " "
    self.add_to_group(text_frame)

# ===========================================================
# Add text defined as a title
# - text is bolded
# ===========================================================
def add_title(self, word_in):
    word_weight = int(self.get_word_points(word_in))
    self.geo_bnds_right(word_in)
    title_frame = self.get_text_frame()
    geo_bound_adj = self.geo_bnd_page_adj(self.geo_bound_ptr)
    geo_bound_adj = self.geo_bound_ptr # don't adjust till afterwards
    if word_weight+self.text_start > self.text_end:
        bot_y = self.get_bot_y(geo_bound_adj)
        geo_bound_adj = self.set_top_x(geo_bound_adj, self.text_start)
        geo_bound_adj = self.set_bot_x(geo_bound_adj, self.text_end - 1)
        if (word_weight / self.text_end).is_integer():
            bot_y_delta = int(word_weight / self.text_end)
        else:
            bot_y_delta = int(word_weight / self.text_end) + 2
        geo_bound_adj = self.set_bot_y(geo_bound_adj, bot_y + bot_y_delta + 1)
        self.geo_bound_ptr = self.set_bot_y(
            self.geo_bound_ptr, bot_y + bot_y_delta + 1
        )  # set geo pointer's bottom y
    geo_bound_adj = self.geo_bnd_page_adj(geo_bound_adj) # see if moving after sizing will put on proper page
    title_frame.TextFramePreferences.VerticalJustification = (
        TextVJust.CENTER_ALIGN.value
    )
    title_frame.GeometricBounds = geo_bound_adj
    title_frame.texts[0].pointSize = "12pt"
    title_frame.ParentStory.AppliedFont = "Arial"
    title_frame.ParentStory.FontStyle = "Bold"
    title_frame.Contents = word_in + " "
    self.add_to_group(title_frame)

# ===========================================================
# Geo Boundary right
#
# Find out the spacing for the words in the textbox and make
# the textbox the proper size. Set the Geo boundary
# accordingly.
# ===========================================================
def geo_bnds_right(self, word_in):
    new_line_pos = self.geo_get_word_spacing(word_in)
    if new_line_pos >= self.text_end:
        self.geo_bnds_next_line()
        new_line_pos = self.geo_get_word_spacing(word_in)

# ===========================================================
# Get Geo Boundary word spacing
#
# This function will take the current geo boundary and
# add the amount of space needed for the text to store in
# this textbox
# ===========================================================
def geo_get_word_spacing(self, word_in):
    ret_value = self.geo_bound_ptr[:]
    top_x_val = self.get_top_x(ret_value)
    bot_x_val = self.get_bot_x(ret_value)
    ret_value = self.set_top_x(ret_value, bot_x_val)
    right_append = self.get_word_points(word_in)  # the space the word needs
    new_line_pos = int(right_append) + bot_x_val + 1
    ret_value = self.set_bot_x(ret_value, new_line_pos)
    self.geo_bound_ptr = ret_value
    return new_line_pos

# ===========================================================
# get word points
#
# Will get the weighted values of letters defined in
# dct_letter_weight. after a cummulative number is reached
# that's passed back and the integer value is used.
# ===========================================================
def get_word_points(self, word_in):
    ret_value = 0.0
    for letter in word_in:
        if letter in self.dct_letter_weight.keys():
            ret_value += self.dct_letter_weight[letter]
    return ret_value

# ===========================================================
# Geo Boundary Page Adjustment.
#
# Odd # pages except 1 (ex 3,5,7) need to have the top and
# bottom x moved over to appear on the right page. 51 seems
# to be a good number for text
# ===========================================================

def geo_bnd_page_adj(self, geo_bnd_in):
    ret_value = geo_bnd_in[:]
    if self.page_ptr % 2 == 1 and self.page_ptr > 1:
        top_x = self.get_top_x(ret_value)
        bot_x = self.get_bot_x(ret_value)
        ret_value = self.set_top_x(ret_value, top_x + 51)
        ret_value = self.set_bot_x(ret_value, bot_x + 51)
    return ret_value

if name == 'main':

========================================================================

# TEST INDESIGN CLASS
#
# THIS PROGRAM IS FREE SOFTWARE. IT COMES WITHOUT ANY WARRANTY, TO 
# THE EXTENT PERMITTED BY APPLICABLE LAW. YOU CAN REDISTRIBUTE IT
# AND/OR MODIFY IT UNDER SIMILAR TERMS. THIS CODE IS DISTRIBUTED "AS IS".
# THE USER ACCEPTS ALL RESPONSIBILITIES
#
# This class will put an image above, add text below, and group all the 
# text into one textframe at the end. Most of my calculations were eye-
# balling the output. If anyone has a more accurate conversion (like  
# pixels to geo boundary units), please feel free to modify and repost.
#
# I use the general space of the characters in Arial font at 12 point 
# to calculate the weight (width) of a set of text. Again, the weight 
# of each character was what I saw.
# 
#               WHAT YOU NEED TO DO TO RUN THIS EXAMPLE
#
# You need to add any images below. I use a string var
# to define wide and long images. 
# You also need to put in the full path of the output file you want
# to generate. 
# The images I put I use a % by calculating the thumbnail and using 
# that for calculations. 
#
#                      THINGS I FOUND
#
# One weird thing I found is that odd pages over 1, the position 
# is actually a full page over. geo_bnd_page_adj takes care of that
#
# The GeoBoundary Group of coordinates are in the order y,x and not
# x,y the way I've always known coordinates. That's why I added
# gets and sets for top_x,top_y,bot_x and bot_y, to avoid confusing
# myself.
# 
# InDesign comes up with a new COM object to address which seems to 
# corrispond with the year of the InDesign release. I have InDesign
# 2020 so my call in set_in_des_app is to "InDesign.Application.2020".
# Change the year suffix to match the version of InDesign you're working
# with. In the past it seems the same COM, just rebundled. Hopefully,
# they'll keep it similar in future releases.
#
# ========================================================================
lst_words = ["this is an","unscientific","way:","to","see","how:","words","go","together:", "adding more", "stuff", "on", "the",  "end"]
img_long = 'C:\\full\\path\\to\\long\\image"    #<====put your images here
img_wide = 'C:\\full\\path\\to\\wide\\image"    #<====put your images here
img_small = 'C:\\full\\path\\to\\small\\image"  #<====put your images here

# 2 items on a page
# long, wide
#lst_imgs = [img_long,img_wide]

# wide, long
#lst_imgs = [img_wide, img_long]

# long, long
#lst_imgs = [img_long, img_long]

# wide, wide
#lst_imgs = [img_wide,img_wide]

#small, big
#lst_imgs = [img_small,img_wide]

# 3 items on a page 
# long, wide, long
#lst_imgs = [img_long,img_wide,img_long]

# wide, long, wide
#lst_imgs = [img_wide, img_long, img_wide]

# long, long, long
#lst_imgs = [img_long, img_long, img_long]

# wide, wide, wide
#lst_imgs = [img_wide,img_wide, img_wide]

in_des_doc = inDesign_Process()
for img_item in lst_imgs:
    in_des_doc.add_image(img_item)
    in_des_doc.start_grouping() # start grouping text frames together under image
    for word in lst_words:
        if "this is" in word:
            id_test = in_des_doc.add_title(word)
        elif ":" in word:
            in_des_doc.add_label(word)
        else:
            in_des_doc.add_text(word)
    in_des_doc.end_grouping() # merge text frames
#in_des_doc.set_next_page() # Will go to the next page 

in_des_doc.set_filename(r'C:\full\path\to\output\filename.indd')  #<=====put your full path and filename to generate here
if in_des_doc.save_in_des_file():
    print("SUCCESS!!!!")
else:
    print("FAIL!!!!!!")
in_des_doc.close_des_file()