Line numbering for text frames

From Scribus Wiki
Jump to: navigation, search
This article is part of the Scripts series.

Here is an idea that comes from a feature request, specifically http://bugs.scribus.net/view.php?id=1975

As you can see this was filed back in 2005, so it's mostly been gathering dust, or whatever bug reports do over the years. Kunda wondered if this might be scriptable, so I began to see what I could do. Consider this a work in progress.

What the filer wanted was the ability to have numbering of lines present on a proof of some document, so that this numbering scheme could be used when one person is recommending an edit to be done by someone else in some particular frame on some particular page. Later, the numbering would eliminated or turned off for the final product. Kunda dug up this bug, and thought that it seemed an interesting concept, and might be useful for other purposes, purposes unknown as of yet.

linenumber.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: linenumber.py
# © 2015.11.25 Gregory Pittman
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""
USAGE



"""
import scribus

if scribus.haveDoc():
    user_units = scribus.getUnit()
    scribus.setUnit(scribus.UNIT_POINTS)
    linecount = 1
    textnumbers = ""
else:
    scribus.messageBox('Usage Error', 'You need a Document open', icon=scribus.ICON_NONE, button1=scribus.BUTTON_OK)
    sys.exit(2)

if scribus.selectionCount() == 0:
    scribus.messageBox('Scribus - Usage 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 - Usage 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()
ftype1 = scribus.getObjectType(textbox)
if (ftype1 != "TextFrame"):
    scribus.messageBox('Object Type', "Selected object must be a text frame",
                       scribus.ICON_WARNING, scribus.BUTTON_OK)
    sys.exit(2)
    
frameW, frameH = scribus.getSize(textbox)
textlines = scribus.getTextLines(textbox)
linespace = scribus.getLineSpacing(textbox)
xframe, yframe = scribus.getPosition(textbox)
font_size = scribus.getFontSize(textbox)
#scribus.messageBox("Font Size",str(font_size),icon=scribus.ICON_NONE,button1=scribus.BUTTON_OK) # here to debug getFontSize()

while (linecount <= textlines):          # here we create a customized column of numbers, just enough for the lines of text
    textnumbers = textnumbers + str(linecount) + "\n"
    linecount = linecount + 1
newframe = scribus.createText(xframe - font_size * 1.5, yframe, font_size * 1.5, frameH)
scribus.selectObject(newframe)
scribus.insertText(textnumbers, 0, newframe)
scribus.setFontSize(font_size, newframe)
scribus.setLineSpacing(linespace, newframe)
if (scribus.scribus_version_info[:3] >= (1,5,0)):
    scribus.setLineSpacingMode(1, newframe)

scribus.setUnit(user_units)
scribus.setRedraw(1)
scribus.docChanged(1)
endmessage = 'Successfully ran script!' # Change this message to your liking
scribus.messageBox("Finished", endmessage,icon=scribus.ICON_NONE,button1=scribus.BUTTON_OK)

Notes

At this point, it only works on a single selected text frame, by getting information about the frame size and position, font size, linespacing, number of lines of text, then creates a small text frame just to the left of the original, which it fills with a column of numbers, created with a loop based on the number of lines of text. In case you wondered, the getTextLines() command only gets the number of lines you see in the frame. If the text overflows, it only counts those you see. One potential error is that the linespacing mode is set to Automatic. This comes from the problem that even though there is a command to set the mode, you cannot get the mode from the original frame. If the original text is in fixed linespacing, you may need to switch for the line numbering frame.

One thing I found as I began this was that there was a bug in the getFontSize() command, in that it would not return a decimal value. A font 14.4 points returned 14.0. I looked this up in the C++ code, found it was being assigned a long integer value instead of a double precision float. Fixed that, and with Craig Bradney's blessing committed it to fix this bug and the script's behavior.

Getting back to the original usage envisioned, next I'd like to create a new layer, and put this text frame on that instead of the same layer as the original text. This way, the new layer could be made invisible and/or nonprintable (and therefore not exporting to PDF).

Finally, the last element would be to run this on all the text frames in a document. This has pros and cons, but at least a starting point for variants. After that, perhaps other bells and whistles, like adjusting the font or font color, transparency, who knows.

The script will work in Scribus versions ≥ 1.5.0 as noted above. If you are using lower versions, there is no setLineSpacingMode() command. The script tests for your version, and skips that command so that it works on 1.4.x also.

linenumber.py Version 2

Now we add the code to create our numbering frame on its own layer, "numberinglayer". Since we may have already run the script, we need to check first if it already exists, and if not create it. In either case, we set this as the active layer.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: linenumber.py
# © 2015.11.28 Gregory Pittman
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""
USAGE



"""
import scribus

if scribus.haveDoc():
    user_units = scribus.getUnit()
    scribus.setUnit(scribus.UNIT_POINTS)
    linecount = 1
    textnumbers = ""
else:
    scribus.messageBox('Usage Error', 'You need a Document open', icon=scribus.ICON_NONE, button1=scribus.BUTTON_OK)
    sys.exit(2)

if scribus.selectionCount() == 0:
    scribus.messageBox('Scribus - Usage 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 - Usage 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()
ftype1 = scribus.getObjectType(textbox)
if (ftype1 != "TextFrame"):
    scribus.messageBox('Object Type', "Selected object must be a text frame",
                       scribus.ICON_WARNING, scribus.BUTTON_OK)
    sys.exit(2)
    
frameW, frameH = scribus.getSize(textbox)
textlines = scribus.getTextLines(textbox)
linespace = scribus.getLineSpacing(textbox)
xframe, yframe = scribus.getPosition(textbox)
font_size = scribus.getFontSize(textbox)

# next, we find out if there already is a layer for our numbering, but if not
# we create it and set it as the active layer
layerNames = []
layerNames = scribus.getLayers()
for layer in layerNames:
    if (layer == "numberinglayer"):
	scribus.setActiveLayer("numberinglayer")
A_layer = scribus.getActiveLayer()
if (A_layer != "numberinglayer"):
    scribus.createLayer("numberinglayer")
    scribus.setActiveLayer("numberinglayer")
    
while (linecount <= textlines):
    textnumbers = textnumbers + str(linecount) + "\n"
    linecount = linecount + 1
newframe = scribus.createText(xframe - font_size * 1.5, yframe, font_size * 1.5, frameH)
scribus.selectObject(newframe)
scribus.insertText(textnumbers, 0, newframe)
scribus.setFontSize(font_size, newframe)
scribus.setLineSpacing(linespace, newframe)
if (scribus.scribus_version_info[:3] >= (1,5,0)):
    scribus.setLineSpacingMode(1, newframe)

scribus.setUnit(user_units)
scribus.setRedraw(1)
scribus.docChanged(1)
endmessage = 'Successfully ran script!' # Change this message to your liking
scribus.messageBox("Finished", endmessage,icon=scribus.ICON_NONE,button1=scribus.BUTTON_OK)

Notes

So what are the next steps? Obviously we need to set up a loop to go through all text frames, but first we'll need to find our way back to the original layer. We may be able to dispense with checking for a selected frame, since we want to find all the text frames. Do we need to jump from one page to the next? We'll see.

linenumber_v3.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# File: linenumber_v3.py
# © 2015.11.28 Gregory Pittman
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""
USAGE
You must have a document open. This script goes through your document, page by page,
identifying text frames, then making a linenumbering frame adjacent to each, and these 
frames are placed on their own layer.
One shortcoming is that there is no way to detect whether a frame has fixed or automatic
linespacing, so by default all of these numbering frames are set to automatic.
If you have various fonts, font size and linespacing inside your frame, you can expect results
to be problematic.
Also, all of these numbering frames are placed adjacent to each frame to the left.


"""
import scribus

if scribus.haveDoc():
    user_units = scribus.getUnit()
    scribus.setUnit(scribus.UNIT_POINTS)
    NLayer = ""
    layerNames = []
    layerNames = scribus.getLayers()
    for layer in layerNames:
	if (layer == "numberinglayer"):
	    NLayer = "exists"
    if (NLayer != "exists"):
	scribus.createLayer("numberinglayer")
    OrigLayer = scribus.getActiveLayer()  
else:
    scribus.messageBox('Usage Error', 'You need a Document open', icon=scribus.ICON_NONE, button1=scribus.BUTTON_OK)
    sys.exit(2)

pages = scribus.pageCount()
page = 1
while page <= pages:
    scribus.gotoPage(page)
    AllObjects = scribus.getAllObjects()

    for textbox in AllObjects:
	ftype1 = scribus.getObjectType(textbox)
	if (ftype1 == "TextFrame"):
	    frameW, frameH = scribus.getSize(textbox)
	    textlines = scribus.getTextLines(textbox)
	    linespace = scribus.getLineSpacing(textbox)
	    xframe, yframe = scribus.getPosition(textbox)
            fontmatch = scribus.getFont(textbox)
	    font_size = scribus.getFontSize(textbox)
	    scribus.setActiveLayer("numberinglayer")

	    linecount = 1
	    textnumbers = ""
	    while (linecount <= textlines):
		textnumbers = textnumbers + str(linecount) + "\n"
		linecount = linecount + 1
	    newframe = scribus.createText(xframe - font_size * 1.5, yframe, font_size * 1.5, frameH)
	    scribus.selectObject(newframe)
	    scribus.insertText(textnumbers, 0, newframe)
            scribus.setFont(fontmatch, newframe)
	    scribus.setFontSize(font_size, newframe)
	    scribus.setTextAlignment(scribus.ALIGN_RIGHT, newframe)
	    scribus.setLineSpacing(linespace, newframe)
	    if (scribus.scribus_version_info[:3] >= (1,5,0)):
		scribus.setLineSpacingMode(1, newframe) # here we set the linespacing mode; 0 = fixed, 1 = automatic
	scribus.setActiveLayer(OrigLayer)
    page = page + 1
scribus.setUnit(user_units)
scribus.setRedraw(1)
scribus.docChanged(1)
endmessage = 'Successfully ran script!' # Change this message to your liking
scribus.messageBox("Finished", endmessage,icon=scribus.ICON_NONE,button1=scribus.BUTTON_OK)

Notes

Whew! I've made some rearrangements here as you can see. The business about seeing whether there was a layer for the numbering was moved up to the top.

It turns out we DO need to jump page to page, since getAllObjects() only gets all the objects from the current page. We don't care whether any objects are selected, so we do away with that error-checking. This script doesn't care if you don't have any text frames, it will just crank away and the only thing that will happen is that the extra layer will be created.

It should be self-evident, but if you run the script again while having the numbering layer active, you'll create a new set of numbering frames based on the existing ones.

One possible addition might be to add a valueDialog() to choose whether you want the linespacing mode set to fixed instead.

I found that the script seemed to fail if you are using page units other than points – it doesn't actually fail, it just puts the numbering frames WAY off the page. The fix is to switch to points, then back. This is corrected now in the above older versions also.

Another tweak is to match the font in the numbering frame to its reference text frame. This is because, especially with automatic linespacing, different fonts will not necessarily line up. Also, I have set the numbering frames to right alignment. If you would like a little separation between the numbering frames and their reference, change the first parameter in the createText() function, maybe from xframe - font_size * 1.5 to xframe - font_size * 1.5 - 3 — remember, you're working with points.

I have fixed in all 3 versions an error in the messageBox(). You can no longer use numerical equivalents for the icon and button values. If you try, your script and Scribus absolutely freeze up.

Example

Linenumbers1.png Here is an example of some problematic results, and not surprising ones because of the mixed fonts. When the script initially ran, the number column was shorter than the lines of text on the right. Here we have switched the numbering frame to Fixed linespacing, and it's a bit too long, even though both frames use 15 point fixed spacing.
The answer comes from holding down Shift while using the spinbox to reduce the number frame spacing to 14.60. Maybe not perfect, but the numbers line up well enough for the intended purpose.

These examples show the numbering frame placed with a 3-point gap.

Linenumbers2.png