from krita import Extension
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton
from PyQt5.QtCore import Qt, QByteArray
from math import ceil
import re
class BookletArranger(Extension):
def __init__(self, parent):
super().__init__(parent)
# Krita.instance() exists, so do any setup work
def setup(self):
pass
# called after setup(self)
def createActions(self, window):
# Create menu item in Tools > Scripts.
action = window.createAction("bookletarranger", "Booklet Arranger")
action.triggered.connect(self.booklet_arranger)
def booklet_arranger(self):
# Dialog creation.
newDialog = QDialog()
newDialog.setWindowTitle("Booklet Arranger")
layout = QVBoxLayout()
newDialog.setLayout(layout)
# Description row.
desc = QLabel('Arranges a set of layers into a booklet.')
desc.setAlignment(Qt.AlignCenter)
row0 = QHBoxLayout()
row0.addWidget(desc)
layout.addLayout(row0)
# Page Information row.
preflight_info = self.preflightInfo()
info_text = preflight_info["InfoText"]
self.page_count = preflight_info["PageCount"]
stats = QLabel(info_text)
stats.setGeometry(200, 250, 100, 50)
stats.setStyleSheet("border: 1px solid white")
stats.setTextFormat(Qt.RichText)
stats.setWordWrap(True)
rown = QHBoxLayout()
rown.addWidget(stats)
layout.addLayout(rown)
# Options row. Bleed, cropmarks, outer-page offset.
self.bleedInput = QLineEdit('24')
self.cropmarksCheck = QCheckBox('Cropmarks')
self.cropmarksCheck.setChecked(True)
self.offsetOuterCheck = QCheckBox('Offset Outer Pages')
self.offsetOuterCheck.setChecked(True)
row2 = QHBoxLayout()
row2.addWidget(QLabel('Bleed size:'))
row2.addWidget(self.bleedInput)
row2.addWidget(self.cropmarksCheck)
row2.addWidget(self.offsetOuterCheck)
layout.addLayout(row2)
# Do It row.
goButton = QPushButton("Arrange Pages")
goButton.setIcon( Krita.instance().icon('animation_play') )
if not preflight_info["Success"]:
goButton.setEnabled(False)
row4 = QHBoxLayout()
row4.addWidget(goButton)
layout.addLayout(row4)
# Hook up the actions.
goButton.clicked.connect( self.arrangeBooklet )
# Show the dialog.
newDialog.exec_()
# Run some checks and generate the preflight message to brief the user.
def preflightInfo(self):
# - Check number of selected layers
# - Check layer names (valid forms are "Page 2" or "2")
# - Check layer dimensions for irregularities.
success = True
info_text = ''
w = Krita.instance().activeWindow()
v = w.activeView()
layers = v.selectedNodes()
# Check layer count. Should be more than 1, and a multiple of 4.
# In the future, will detect missing pages and possibly pad with blank pages (like page2 and second last page inside cover pages).
page_count = len(layers)
message = ''
if page_count < 4:
message = " - not enough selected! Minimum of 4 pages."
success = False
elif page_count % 4 > 0:
message = " - incorrect page count! Must be multiple of 4."
success = False
info_text += 'Pages Selected: ' + str(page_count) + message + '
'
# Check for complete sequence of page numbers. Grabs the first number in a layer name
invalid_names = ''
page_numbers = []
## Collect each layer's first number as a page number. Report on names without numbers.
for layer in layers:
name = layer.name()
match = re.search('[0-9]+', name)
if match:
page_numbers.append(int(match[0]))
else:
invalid_names += name + " "
if invalid_names:
info_text += 'Invalid Layer Names: ' + invalid_names + '
'
success = False
## Check for missing page numbers. Pages must fulfil the range "1 to page_count".
highest_page = max(page_numbers)
last_page = highest_page if highest_page > page_count else page_count
missing_pages = ''
for p in range(1, last_page+1):
if p in page_numbers:
pass
else:
missing_pages += str(p) + " "
if missing_pages:
info_text += 'Missing Page Numbers: ' + missing_pages + '
'
success = False
return {"Success": success, "InfoText": info_text, "PageCount": page_count}
##########
# Slots
##########
def arrangeBooklet(self, e):
# Document dimensions.
self.doc = Krita.instance().activeDocument()
self.doc_width = self.doc.width()
self.doc_height = self.doc.height()
# Get the selected layer(s).
w = Krita.instance().activeWindow()
v = w.activeView()
layers = v.selectedNodes()
for layer in layers:
if layer.type() == 'paintlayer':
self.arrangePage(layer)
didNotRun = False
if didNotRun:
dialog = QDialog()
dialog.setWindowTitle("Paint Layer Required")
layout = QVBoxLayout()
layout.addWidget(QLabel('Booklet Arranger only works on paint layers.'))
dialog.setLayout(layout)
dialog.exec_()
else:
# Refresh the view, or the cropmarks will not be immediately shown.
self.doc.refreshProjection()
# Actually arranges the pages into a booklet!
def arrangePage(self, page):
max_sheets = self.page_count / 4
page_name = page.name()
match = re.search('[0-9]+', page_name)
page_number = int(match[0])
# Is the page in the higher or lower half? e.g. p1-16 or p17-32?
# Calculation differs based on this.
high = page_number > (self.page_count / 2)
# Work out which paper sheet/side a page should be on.
# e.g. for 32 pages: p32 p1/p2 p31 will be on sheet 1 sides A/B.
#
# Side number 1 = sheet 1 side A (front).
# Side number 2 = sheet 1 side B (back).
# Side number 3 = sheet 2 side A (front).
# ...
#
if high:
side_number = self.page_count - page_number + 1
else:
side_number = page_number
current_sheet = ceil(side_number / 2)
# Work out if a page should be on the left or right side of the sheet.
front_side = side_number % 2 # Odds sheet/sides (1, 3, 5...) are front.
right = True
if (high and front_side) or (not high and not front_side):
right = False
# Add page to correct group (which represent paper sheets/sides).
side_name = "p" + str(current_sheet)
side_name += "a" if front_side else "b"
sidegroup = self.doc.nodeByName(side_name)
if not sidegroup:
sidegroup = self.doc.createGroupLayer(side_name)
root = self.doc.rootNode()
root.addChildNode(sidegroup, None)
page.remove()
sidegroup.addChildNode(page, None)
# Offset calculation. First sheet has largest offset, centre sheet has zero offset. 3 pixel step.
if self.offsetOuterCheck.isChecked():
offset = 3 * (max_sheets - current_sheet)
else:
offset = 0
# Position page (with or without offset)
page_pos = page.position()
bds = page.bounds()
x_target = self.doc_width / 2
x_diff = x_target - bds.left()
orig_x = x_diff
y_target = (self.doc_height - (bds.bottom() - bds.top())) / 2
y_diff = y_target - bds.top()
orig_y = y_diff
if right:
x_diff = x_diff + offset
else:
x_diff = x_diff - (bds.right() + 1 - bds.left()) - offset
page.move(int(page_pos.x() + x_diff), int(page_pos.y() + y_diff))
# Keep pre-bleed bounds for crop marks.
bds = page.bounds()
# Bleed by stated amount on outward sides, bleed to middle on inward sides.
bleed_amount = int(self.bleedInput.text())
bleed_amounts = { "top": bleed_amount, "bottom": bleed_amount }
if right:
bleed_amounts["right"] = bleed_amount
bleed_amounts["left"] = offset
else:
bleed_amounts["right"] = offset
bleed_amounts["left"] = bleed_amount
self.bleedPage(page, bleed_amounts)
# Add crop marks. Which crop marks are created depends on which side the page is on.
if self.cropmarksCheck.isChecked():
black_pixel = b'\x00\x00\x00\xff'
crop_mark_width = 2
crop_mark_height = 48
crop_mark = QByteArray(black_pixel * crop_mark_width * crop_mark_height)
## Vertical cropmarks
x = bds.right() if right else bds.left() - 1
ytop = bds.top() - crop_mark_height - bleed_amount
page.setPixelData(crop_mark, x, ytop, crop_mark_width, crop_mark_height)
ybot = bds.bottom() + 1 + bleed_amount
page.setPixelData(crop_mark, x, ybot, crop_mark_width, crop_mark_height)
if page_number == 1:
# Specially mark centre on first page (always p1a right side)
x = int(bds.left() - 1 - offset)
page.setPixelData(crop_mark, x, ytop, crop_mark_width, crop_mark_height)
page.setPixelData(crop_mark, x, ybot, crop_mark_width, crop_mark_height)
## Horizontal cropmarks
crop_mark_width = 48
crop_mark_height = 2
x = bds.right() + 1 + bleed_amount if right else bds.left() - bleed_amount - crop_mark_width
y = bds.top() - 1
page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
y = bds.bottom()
page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
def bleedPage(self, page, amounts):
# Top
bds = page.bounds()
xpos = bds.left()
ypos = bds.top()
width = bds.width()
height = amounts['top']
bleed_line = page.pixelData(xpos, ypos, width, 1)
bleed = bleed_line * height
page.setPixelData(bleed, xpos, ypos - height, width, height)
# Bottom
bds = page.bounds()
xpos = bds.left()
ypos = bds.bottom()
width = bds.width()
height = amounts['bottom']
bleed_line = page.pixelData(xpos, ypos, width, 1)
bleed = bleed_line * height
page.setPixelData(bleed, xpos, ypos + 1, width, height)
# Left
bds = page.bounds()
xpos = bds.left()
ypos = bds.top()
width = int(amounts['left'])
height = bds.height()
bleed_line = page.pixelData(xpos, ypos, 1, height)
for c in range(1, width + 1):
page.setPixelData(bleed_line, (xpos - c), ypos, 1, height)
# Right
bds = page.bounds()
xpos = bds.right()
ypos = bds.top()
width = int(amounts['right'])
height = bds.height()
bleed_line = page.pixelData(xpos, ypos, 1, height)
for c in range(1, width + 1):
page.setPixelData(bleed_line, (xpos + c), ypos, 1, height)