| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- 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 + '<br/>'
- # 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 + '<br>'
- 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 + '<br>'
- 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):
- 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?
- high = page_number > (self.page_count / 2)
- if high:
- side_number = self.page_count - page_number + 1
- else:
- side_number = page_number
- odd_side = side_number % 2 # Detect odd paper sides, as sides have different page arrangements.
- right = True
- if (high and odd_side) or (not high and not odd_side):
- right = False
- max_sheets = self.page_count / 4
- current_sheet = ceil(side_number / 2)
- # Add page to correct group/paper sheet side.
- side_name = "p" + str(current_sheet)
- side_name += "f" if odd_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(page_pos.x() + x_diff, page_pos.y() + y_diff)
- # Keep new 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.
- 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
- y = bds.top() - crop_mark_height - bleed_amount
- page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
- y = bds.bottom() + 1 + bleed_amount
- page.setPixelData(crop_mark, x, y, 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)
|