Automating repetitive tasks with Python scripts in Scribus

Getting started with Python scripting in Scribus

Automate repetitive tasks in Scribus with Python.

Real python in the graphic jungle
Image credits : 
Photo by Jen Wike Huger, CC BY-SA; Original photo by Torkild Retvedt
x

Subscribe now

Get the highlights in your inbox every week.

In my previous article, Learn Perl with this temperature conversion script, I walked readers through a Perl script. When I first began using Scribus—a desktop publishing (DTP) application used to create PDFs for publishing in print or online—Perl was the scripting language I was most familiar with, and I even tried using it with Scribus. At one point I made a Perl script that would generate a file in the Scribus format (SLA) from scratch. Although certainly feasible, it really wasn't of much practical use.

Included with Scribus is a plugin called Scripter, which allows the use of a number of internal Python commands to perform a variety of analytical, creative, and editing operations in Scribus, as well as using all the other functionality that Python provides. As I advocated in my article about getting started with Perl, I began by looking at the various scripts included with Python, went back and forth with various references, and made various scripts for use outside of Scribus. If you can't make a basic algorithm work in some way outside of Scribus, it's certainly not going to work inside it.

Let's start with a basic utility that we might envision using in a DTP setting later on. This is a simple script called date.py:

#!/usr/bin/env python
# -*- coding: utf-8  -*-

from datetime import date

today = date.today()
d = today.strftime("%A, %d %B %Y")

print d

At the top we have our she-bang line (#, as with Perl), but here is a bit different style, using /usr/bin/env to find the current system Python interpreter. The next isn't absolutely necessary because UTF-8 is the default character encoding on most Linux systems, but it does highlight an issue I first had when I modified this script from one I copied from a Windows machine. When I was trying to run this, I kept getting an error that it couldn't find /usr/bin/env python^M. Eventually I figured out the encoding of the file was (as in Windows) with carriage returns at the ends of lines, in spite of this line being in the script. All I needed to do was to run dos2unix on the file.

As we get to executable lines, note that in Python, these don't end in a semicolon—they just end. Also, variable names do not begin with a special character. from and import are Python commands; datetime and date are variable names. In this particular case, datetime refers to the Python module of that name, and date refers to a particular function in that module. The basic Python interpreter would not know about the command without this statement. Later we'll see how this applies to Scribus.

Our next statement sets up a new variable, today, with the collected information of date.today(). Python is particular about syntax here, too. today is an operation being performed on date, with its results now saved in the variable today. In a way, this is unfortunate variable naming on my part, but at the same time it shows that Python sees these as two separate entities, the syntactic structure clarifying this.

Next, just as we did with the sprintf command in Python, we set up a particular format for the output for today's date: A being the day of the week, d the day in the month, B the month, and Y the year. Not surprisingly then, the output of this script when I ran it was:

Thursday, 13 October 2016

See how the comma in the formatting comes through to the output? You can look up the datetime module to see what options you have for the formatting.

As a standalone script, this a pretty weak one considering I can run date from a command-line whenever I want. Now let's think about DTP and Scribus—for example, imagine we are making a newsletter or other periodical document on which we want to have the publication date, perhaps in the header. Conceptually, you just run this little script, and you're done. Except that there are a number of design issues in DTP that we care about.

Desktop publishing and page design

In a DTP program like Scribus, you don't have anything to work with by default. You don't have a document until you create one or open one you've saved. When you open a new document, by default it has a single page, and won't automatically make any more until you tell it to. Furthermore, the page is just a space in which to work. You don't start typing on the page or inserting pictures; rather, you must work in a frames environment. Frames are spaces in which you might enter text, images, or other graphics (such as tables or charts), but these are unique—you can't enter text in an image frame or vice versa.

With scripting, or more specifically Scripter in Scribus, you also must work with this sort of environment. A general rule I try to promote with scripts in Scribus is that you should use the graphical environment when that is the most efficient way of working, but use scripting when you are doing repetitive, tedious, or perhaps difficult tasks that you expect to do with some regularity.

Example repetitive task

Let's invent a repetitive task so we can make use of our Python logic about datetime. Imagine we have a newsletter for which we have a template, and in the header we have this:

Structurally, this is a text frame having certain dimensions, and a particular X,Y location on the page, with the embellishment of a black border two points wide. The text is centered, with the top line being 20-point Fontin Bold and the second line 14-point Fontin Regular, with automatic line spacing, and an added space at the top (called “Distance" in Scribus) of 9 points. For our scripting task, we only want to update the date, so we will work with a pre-existing document with this frame in it.

We create our script, open the document, run the script, and we're done—but wait a minute. How does Scripter know which page element to work on? As with Scribus outside of Scripter, you must select an item on the current page to begin work (if you're not making a new element).

Let's come up with a list of operations:

  1. Identify selected frame. (Is one selected? Is it a text frame?)
  2. Either clear all the text, or only clear the date.
  3. Create our new date to enter, and enter into the frame at the right place.
  4. Make sure the correct font, font size, and justification are used.

Here is what we finally come up with (don't let its size scare you):

#!/usr/bin/env python
# -*- coding: utf-8  -*-
# gazette_date.py

try:
     import scribus
except ImportError:
     print "Unable to import the 'scribus' module. This script will only run within"
     print "the Python interpreter embedded in Scribus. Try Script->Execute Script."
     sys.exit(1)

from datetime import date

if not scribus.haveDoc():
     scribus.messageBox('Scribus - Script Error', "No document open", scribus.ICON_WARNING, scribus.BUTTON_OK)
     sys.exit(1)

if scribus.selectionCount() == 0:
     scribus.messageBox('Scribus - Script Error',
             "There is no object selected.\nPlease select a text frame and try again.",
             scribus.ICON_WARNING, scribus.BUTTON_OK)
     sys.exit(2)
if scribus.selectionCount() > 1:
     scribus.messageBox('Scribus - Script Error',
             "You have more than one object selected.\nPlease select one text frame and try again.",
             scribus.ICON_WARNING, scribus.BUTTON_OK)
     sys.exit(2)
textbox = scribus.getSelectedObject()
ftype = scribus.getObjectType(textbox)

if (ftype != "TextFrame"):
     scribus.messageBox('Scribus - Script Error', "This is not a textframe. Try again.", scribus.ICON_WARNING, scribus.BUTTON_OK)
     sys.exit(2)

today = date.today()
d = today.strftime("%A, %B %d, %Y")
length = scribus.getTextLength()
scribus.selectText(19, length-19, textbox)
scribus.deleteText(textbox)
scribus.insertText(d, -1, textbox)

length = scribus.getTextLength()
scribus.selectText(19, length-19, textbox)
scribus.setFontSize(14.0, textbox)

Script explanation

Everything from try: to the last sys.exit(2) is more or less error-checking boilerplate that you can easily copy from one of your scripts to another (or from someone else's script). You don't have to put all of these error checks in there if you don't mind your script crashing (until you figure out you didn't select an object, or it wasn't a text frame, and so on). At a minimum, you do need to have import scribus and from datetime import date for this script. What you're doing with these checks is giving feedback about why the script failed, which is always a kind thing to do, even for yourself.

Beginning at today = date.today() should look familiar. Because we've already checked that this is a text frame selected, we can getTextLength() on it. We must prepend scribus so that Python knows this is a Scripter command. We're going to use that text length to delete the date selectively from the frame. We don't know how many characters there are going to be in the date, but we do know that it's the total number minus the number in the first line. In Scribus, counting characters for the selectText() command begins with 0, and you must count the \n that is implicitly there at the end of the first line—that's how I came up with 19 as the first character of the second line.

I thought I would get lucky and be finished after the insertText() command because the old text was gone and the new text was all there, had the correct font, and was centered—but the font size was 16pt. I have no good explanation for this, because I tried retyping this several times in the original; somehow in the transition with the newline to the next, the point size changed. The simplest answer was to reselect the date after rechecking textLength() and setFontSize() to fix. Not too bad, really.

After saving your script, select Scripter | Execute Script in the Scribus menu to find your script and run it. If you've run the script recently, it will appear in the Scripter | Recent Scripts list.

Troubleshooting

This script admittedly doesn't do much, but should give you a handle on the transition to using Python inside of Scribus. Typically I will slowly build a script, gradually working toward my objective until I get the final result I want.

Sometimes when you've done a series of operations, you can't figure out what is going on. For example, what if patiently counting characters doesn't work? I have used the messageBox() command often as a troubleshooting tool. Following either of the lines beginning with length, we could have inserted the following (all on one line):

scribus.messageBox('Length Value',
             "Text length is " + str(length), scribus.ICON_NONE, scribus.BUTTON_OK)

When the script reaches this point, it pauses to show this message, then resumes after you click the OK button. Notice that, in contrast to Perl, you must convert the integer length to a string to combine it with the preceding text. The reverse situation happens if you request input from the user in a valueDialog() command. This is always considered a string, so if you want to use this as a number, you must convert with int() or float().

Documentation about Scripter commands is included in Scribus, and you'll also find a number of scripts to review and borrow from on the wiki.

About the author

Greg Pittman - Greg is a retired neurologist in Louisville, Kentucky, with a long-standing interest in computers and programming, beginning with Fortran IV in the 1960s. When Linux and open source software came along, it kindled a commitment to learning more, and eventually contributing. He is a member of the Scribus Team.