Automating Adobe InDesign 2018


Single Page Calendar-AppleScript

Tuesday, December 18, 2018

Seasons Greetings

Need a little something to give to friends and family? How about a personalized calendar? You could create a simple one page calendar printed similar to the one shown below. This one is letter size but you could do it legal size or larger. (Smaller won't work). Interested? Our sample script for this week will create the calendar for you--well, most of it.

calendar image  Calendar suggestion

Start with a table style to define the paragraph styles for the table regions. For the script below you will need to name it "Calendar. "You will also want a paragraph style for the header and month titles and whatever you add to your creation.

The styles used in the example are:

Paragraph Styles:

Name Font Size and Leading Alignment Color
Label Din Condensed Bold 14 pt on 14 lead flush left black
Header Din Condensed Bold 10 pt on 10 lead center paper
Body Myriad Pro Normal 10 pt on 10 lead flush left black
Red Based on Body red

Cell Styles:

Name Applied Paragraph Style
Body Body
Red Red

Table Style

Name Region Cell Style
Calendar
Body Row Body
Left and Right Column Red
Header and Footer None

The script to create the calendar is fairly long but not difficult. To keep it as short as possible we will assume that the table style named Calendar exists, and paragraph styles Header and Label are part of styles for the document. The script uses fixed values for variables. To make the script flexible, you would want to add a dialog for entering these values. The sample script below covers the basics. We will go through the code step by step. Test each step as you go.

Step 1: To define the area of the page for the calendar, a text frame needs to be selected. We will also have the script check to make sure it will be large enough for the calendar (minimum width 7.5 inches, minimum height:8 inches).

For the top portion of the script

    property docRef: missing value
    try
        set frameParams to getFrameSelected()
        set frameRef to item 1 of frameParams
        set areaBounds to item 2 of frameParams
    on error errStr
        activate
        display alert "Error: " & errStr
    end try

The handler

(*Acts to initialize the script.
Returns reference to text frame or insertion point in text frame selected.
checks to make sure frame is at least 7.5x8.*)
on getFrameSelected()
    set errMsg to "Requires a text frame 7.5x8 or larger to be selected"
    tell application "Adobe InDesign CC 2018"
	set measurement unit of script preferences to points
	set selList to selection
	if selList is {} then error errMsg
	set selItem to item 1 of selList
	if class of selItem is insertion point then
            set frameRef to item 1 of parent text frames of selItem
	else if class of selItem is text frame then
	    set frameRef to selItem
	else
	    error errMsg
	end if
	set gBounds to geometric bounds of frameRef
	copy gBounds to {y0, x0, y1, x1}
	if y1 - y0 < 8 * 72 or x1 - x0 < 7.5 * 72 then
	    error errMsg
	end if
	set docRef to active document
    end tell
    return {frameRef, gBounds}
end getFrameSelected

Notice how the try/on error/end try statement block in the top portion of the script is designed to catch any errors most notably the error statements in the handlers designed to notify the user.

Step 2: Next we will have the script create a grid for the calendar months. After the call to getFrameSelected, add a call to a handler named calculateGrid. The arguments being passed in the call are the bounds of the selected frame, number of rows, number of columns, gutter (horizontal space between columns), and gap (vertical space between individual calendars).

In the top portion of the script, just above the on error statement, add:

set boundsList to calculateGrid (areaBounds, 6, 2, 18, 6) 

Then at the bottom of the script, add the following handler. This is a handy routine that you can use for any number or instances where you need to create a grid.

(*Returns list of bounds for each item in grid defined by values for rows, columns, gutter and gap passed.*)
on calculateGrid(areaBounds, rCount, cCount, gut, gap)
    set boundsList to {}
    set fullWid to (item 4 of areaBounds) - (item 2 of areaBounds)
    set fullHgt to (item 3 of areaBounds) - (item 1 of areaBounds)
    set colWid to (fullWid - ((cCount - 1) * gut)) / cCount
    set rowHgt to (fullHgt - ((rCount - 1) * gap)) / rCount
    if class of (rowHgt / 6) = real then
	set cellHgt to (round (rowHgt / 6))
	set rowHgt to cellHgt * 6
	set gap to gap - 1
    end if
    set x0 to item 2 of areaBounds
    set y0 to item 1 of areaBounds
    repeat with j from 1 to rCount
	repeat with i from 1 to cCount
	    set x1 to x0 + colWid
	    set y1 to y0 + rowHgt
	    copy {y0, x0, y1, x1} to end of boundsList
	    set x0 to x1 + gut
	end repeat
	set x0 to item 2 of areaBounds
	set y0 to y1 + gap
    end repeat
    return boundsList
end calculateGrid

Step 3: In this step the script creates text frames with labels. For the labels we need a list of the month names to use for the title. Add the following property to the top of the script:

property monthList : {"December", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "January"}

With this we now have the script create the text frames for the individual calendars with a title frame to its left. The arguments for the makeTextFrames handler are: the boundsList (created above), the width of the title frame (60), the base name of the title frame "month" and the name of the title paragraph style ("Label"). Add the following call just after the call to calculateGrid.

    set calFrameSiz to makeTextFrames (boundsList, 60, "month", "Label")

The handler

--create text frames for titles and tables
on makeTextFrames(boundsList, titleWid, baseName, titleStyle)
    tell application "Adobe InDesign CC 2018"
	tell docRef
            set properties of text frame preferences to {text column count:1, inset spacing:0}
            set titleStyleRef to paragraph style titleStyle
        end tell
	tell spread 1 of docRef
	    repeat with i from 1 to length of boundsList
		set thisBounds to item i of boundsList
		copy thisBounds to {y0, x0, y1, x1}
		set titleLabel to item (i + 1) of monthList
		set titleFrame to make text frame with properties ¬{geometric bounds:{y0, x0, y1, x0 + titleWid}, name:"title" & i, label:titleLabel, contents:titleLabel, text frame preferences:{inset spacing:3}}
		set applied paragraph style of paragraph 1 of titleFrame to titleStyleRef
		set calFrame to make text frame with properties {stroke color:"None", stroke weight:0.0, fill color:"None", geometric bounds:{y0, x0 + (titleWid - 1), y1, x1}, name:baseName & i}
            end repeat
	    set calFrameWid to x1 - (x0 + (titleWid - 1))
	    set calFrameHgt to y1 - y0
	end tell
    end tell
    return {calFrameWid, calFrameHgt}
end makeTextFrames

Notice that our list of month names begins with December. This is because our calendar routine is designed to support a 14 month calendar (incuding the previous month of December and the next year's month of January. These are not used in this example.

Step 4: To create the headers at the top of the calendar, the call to our next handler is placed below the call to makeTextFrames. Arguments passed are the first two items from the boundsList (creatrd in Step 2 above), the height of the header frame, the width of the label frames, and the name of the paragraph style to use for the text.

    set bounds1 to item 1 of boundsList
    set bounds2 to item 2 of boundsList
    makeHeaderFrames(bounds1, bounds2, 18, 60, "Header")

And for the handler:

on makeHeaderFrames(bounds1, bounds2, headerHgt, labelWid, parastyleName)
    set headerList to {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
    copy bounds1 to {y0, x0, y1, x1}
    tell application "Adobe InDesign CC 2018"
	set blackColor to swatch "Black" of docRef
	set parastyleRef to paragraph style parastyleName of docRef
	repeat 2 times
	  tell spread 1 of docRef
	      set hx0 to x0 + labelWid
	      set headFrame to make text frame with properties {geometric bounds:{y0 - headerHgt, hx0, y0, x1}}
	   end tell
	   tell headFrame
	      set tableRef to make table with properties {body row count:1, column count:7, width:(x1 - hx0)}
	   end tell
	   tell tableRef
	      set contents to headerList
	      tell row 1
		 set fill color of cells to blackColor
		 set applied paragraph style of paragraph 1 of cells to parastyleRef
	      end tell
	   end tell --table
	   copy bounds2 to {y0, x0, y1, x1}
	end repeat
    end tell --app
end makeHeaderFrames

Step 5: There are others who will come up with a better method for creating a calendar, but this works good for our purpose. For our calendar routine this step creates a control list that is used for creating the calendars.

The control list is a list of lists. Each list within the list indicates (1) the number of days in the month, (2) the number of table cells that need to be empty at the beginning of the month table, and (3) the number of rows required for the table. Two dates involved are: February 1 and March 1 of the calendar year (to take care of the leap year issue). Add the following after the call to makeHeaderFrames:

set calList to getCalList(2019)

This requires a second handler to calculates number of days for month, and blank day offsets for each month. To accommodate a 14 month calendar the first day is December 1 of the previous year. This example of our one page calendar does not use 14 months, so just adds 1 to the month number.

on getCalList(theYear)
   set calList to {}
   set dayList to {31, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31}
   set theDay to date ("February 1 " & theYear)
   set nextDay to date ("March 1 " & theYear)
   set item 3 of dayList to (nextDay - theDay) div days
   set firstDay to date ("December 1 " & (theYear - 1))
   set thefill to (weekday of firstDay as number) - 1
   set numDays to (item 1 of dayList)
   set {numRows, overFill} to getnumRows(thefill, numDays)
   copy {numDays, thefill, numRows} to the end of calList
   copy overFill to thefill
   repeat with i from 2 to count of dayList
	set numDays to (item i of dayList)
	set {numRows, overFill} to getnumRows(thefill, numDays)
	copy {numDays, thefill, numRows} to the end of calList
	copy overFill to thefill
    end repeat
    return calList
end getCalList
--helper handler calculates number of rows for each calendar month
on getnumRows(thefill, numDays)
    set totalDays to numDays + thefill
    set numRows to totalDays div 7
    if totalDays mod 7 > 0 then
	set numRows to numRows + 1
	set overFill to 7 - ((numRows * 7) - totalDays)
     else
	set overFill to 0
	end if
    return {numRows, overFill}
end getnumRows

Step 6. Now the piece de resistance. Using the calList returned from Step 5, this step actually creates the calendars. The last handler call to add to the top of the script passes the following arguments: a list of the calendar width and height, the name of the table style, and the calList created above. Add the call to the handler just above the on error statement.

    createCalendarTables(calFrameSiz, "Calendar", calList)

And the handler:

(*Creates tables for calendars, populates, and styles using the table style*)
on createCalendarTables(frameSiz, tableStyleName, calList)
   set blankList to {"", "", "", "", "", "", ""}
   set numList to {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"}
   copy frameSiz to {frameWid, frameHgt}
   tell application "Adobe InDesign CC 2018"
	set tableStyleRef to table style tableStyleName of docRef
	repeat with i from 1 to 12
	   set monthNumber to i
	   set tableframeName to "month" & monthNumber
	   copy item (monthNumber + 1) of calList to {numDays, spaces, numRows}
	   set tableFrame to text frame tableframeName of spread 1 of docRef
	   tell tableFrame
		set tableRef to make table with properties {width:frameWid, column count:7, header row count:0, body row count:numRows, applied table style:tableStyleRef}
	   end tell
	   tell tableRef
		--subtract 1.5 to account for calculation round off errors
		set height of rows 1 thru -1 to ((frameHgt - 1.5) / numRows)
		try
		   set applied table style to tableStyleRef
		end try
	   end tell
	   if spaces > 0 then
		set spaceList to items 1 thru spaces of blankList
		set tableList to spaceList & items 1 thru numDays of numList
	   else
		set tableList to items 1 thru numDays of numList
	   end if
	   set contents of tableRef to tableList
	   tell tableRef
		tell cells to clear cell style overrides
		set vertical justification of cells to top align
	   end tell
	end repeat
   end tell
end createCalendarTables

That's it! For clarification, the top of the script should read as follows:

property docRef : missing value
property monthList : {"December", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "January"}
try
	set frameParams to getFrameSelected()
	set frameRef to item 1 of frameParams
	set areaBounds to item 2 of frameParams
	set boundsList to calculateGrid(areaBounds, 6, 2, 18, 6)
	set calFrameSiz to makeTextFrames(boundsList, 60, "month", "Label")
	set bounds1 to item 1 of boundsList
	set bounds2 to item 2 of boundsList
	makeHeaderFrames(bounds1, bounds2, 18, 60, "Header")
	set calList to getCalList(2019)
	createCalendarTables(calFrameSiz, "Calendar", calList)
on error errStr
	activate
	display alert "Error: " & errStr
end try

Finishing Up

Add an image and the year at the top. For a legal size document you might want to add a Season's Greeting at the bottom. Or--Well, the rest is up to you.

Onward and Upward

To make the script bullet proof, you will need to add code to make sure the document has the required table and paragraph styles. You can change these styles to alter the style of your table as desired. You may also want to replace fixed values with values returned from a custom dialog. We will leave this up to you.

Disclaimer:
Scripts provided are for demonstration and educational purposes. No representation is made as to their accuracy or completeness. Readers are advised to use the code at their own risk.

Trackback Link
http://www.yourscriptdoctor.com/BlogRetrieve.aspx?BlogID=18391&PostID=1528133&A=Trackback
Trackbacks
Post has no trackbacks.