booklet_arranger_extension.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. from krita import Extension
  2. from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton
  3. from PyQt5.QtCore import Qt, QByteArray
  4. from math import ceil
  5. import re
  6. class BookletArranger(Extension):
  7. def __init__(self, parent):
  8. super().__init__(parent)
  9. # Krita.instance() exists, so do any setup work
  10. def setup(self):
  11. pass
  12. # called after setup(self)
  13. def createActions(self, window):
  14. # Create menu item in Tools > Scripts.
  15. action = window.createAction("bookletarranger", "Booklet Arranger")
  16. action.triggered.connect(self.booklet_arranger)
  17. def booklet_arranger(self):
  18. # Dialog creation.
  19. newDialog = QDialog()
  20. newDialog.setWindowTitle("Booklet Arranger")
  21. layout = QVBoxLayout()
  22. newDialog.setLayout(layout)
  23. # Description row.
  24. desc = QLabel('Arranges a set of layers into a booklet.')
  25. desc.setAlignment(Qt.AlignCenter)
  26. row0 = QHBoxLayout()
  27. row0.addWidget(desc)
  28. layout.addLayout(row0)
  29. # Page Information row.
  30. preflight_info = self.preflightInfo()
  31. info_text = preflight_info["InfoText"]
  32. self.page_count = preflight_info["PageCount"]
  33. stats = QLabel(info_text)
  34. stats.setGeometry(200, 250, 100, 50)
  35. stats.setStyleSheet("border: 1px solid white")
  36. stats.setTextFormat(Qt.RichText)
  37. stats.setWordWrap(True)
  38. rown = QHBoxLayout()
  39. rown.addWidget(stats)
  40. layout.addLayout(rown)
  41. # Options row. Bleed, cropmarks, outer-page offset.
  42. self.bleedInput = QLineEdit('24')
  43. self.cropmarksCheck = QCheckBox('Cropmarks')
  44. self.cropmarksCheck.setChecked(True)
  45. self.offsetOuterCheck = QCheckBox('Offset Outer Pages')
  46. self.offsetOuterCheck.setChecked(True)
  47. row2 = QHBoxLayout()
  48. row2.addWidget(QLabel('Bleed size:'))
  49. row2.addWidget(self.bleedInput)
  50. row2.addWidget(self.cropmarksCheck)
  51. row2.addWidget(self.offsetOuterCheck)
  52. layout.addLayout(row2)
  53. # Do It row.
  54. goButton = QPushButton("Arrange Pages")
  55. goButton.setIcon( Krita.instance().icon('animation_play') )
  56. if not preflight_info["Success"]:
  57. goButton.setEnabled(False)
  58. row4 = QHBoxLayout()
  59. row4.addWidget(goButton)
  60. layout.addLayout(row4)
  61. # Hook up the actions.
  62. goButton.clicked.connect( self.arrangeBooklet )
  63. # Show the dialog.
  64. newDialog.exec_()
  65. # Run some checks and generate the preflight message to brief the user.
  66. def preflightInfo(self):
  67. # - Check number of selected layers
  68. # - Check layer names (valid forms are "Page 2" or "2")
  69. # - Check layer dimensions for irregularities.
  70. success = True
  71. info_text = ''
  72. w = Krita.instance().activeWindow()
  73. v = w.activeView()
  74. layers = v.selectedNodes()
  75. # Check layer count. Should be more than 1, and a multiple of 4.
  76. # In the future, will detect missing pages and possibly pad with blank pages (like page2 and second last page inside cover pages).
  77. page_count = len(layers)
  78. message = ''
  79. if page_count < 4:
  80. message = " - not enough selected! Minimum of 4 pages."
  81. success = False
  82. elif page_count % 4 > 0:
  83. message = " - incorrect page count! Must be multiple of 4."
  84. success = False
  85. info_text += 'Pages Selected: ' + str(page_count) + message + '<br/>'
  86. # Check for complete sequence of page numbers. Grabs the first number in a layer name
  87. invalid_names = ''
  88. page_numbers = []
  89. ## Collect each layer's first number as a page number. Report on names without numbers.
  90. for layer in layers:
  91. name = layer.name()
  92. match = re.search('[0-9]+', name)
  93. if match:
  94. page_numbers.append(int(match[0]))
  95. else:
  96. invalid_names += name + " "
  97. if invalid_names:
  98. info_text += 'Invalid Layer Names: ' + invalid_names + '<br>'
  99. success = False
  100. ## Check for missing page numbers. Pages must fulfil the range "1 to page_count".
  101. highest_page = max(page_numbers)
  102. last_page = highest_page if highest_page > page_count else page_count
  103. missing_pages = ''
  104. for p in range(1, last_page+1):
  105. if p in page_numbers:
  106. pass
  107. else:
  108. missing_pages += str(p) + " "
  109. if missing_pages:
  110. info_text += 'Missing Page Numbers: ' + missing_pages + '<br>'
  111. success = False
  112. return {"Success": success, "InfoText": info_text, "PageCount": page_count}
  113. ##########
  114. # Slots
  115. ##########
  116. def arrangeBooklet(self, e):
  117. # Document dimensions.
  118. self.doc = Krita.instance().activeDocument()
  119. self.doc_width = self.doc.width()
  120. self.doc_height = self.doc.height()
  121. # Get the selected layer(s).
  122. w = Krita.instance().activeWindow()
  123. v = w.activeView()
  124. layers = v.selectedNodes()
  125. for layer in layers:
  126. if layer.type() == 'paintlayer':
  127. self.arrangePage(layer)
  128. didNotRun = False
  129. if didNotRun:
  130. dialog = QDialog()
  131. dialog.setWindowTitle("Paint Layer Required")
  132. layout = QVBoxLayout()
  133. layout.addWidget(QLabel('Booklet Arranger only works on paint layers.'))
  134. dialog.setLayout(layout)
  135. dialog.exec_()
  136. else:
  137. # Refresh the view, or the cropmarks will not be immediately shown.
  138. self.doc.refreshProjection()
  139. # Actually arranges the pages into a booklet!
  140. def arrangePage(self, page):
  141. page_name = page.name()
  142. match = re.search('[0-9]+', page_name)
  143. page_number = int(match[0])
  144. # Is the page in the higher or lower half? e.g. p1-16 or p17-32?
  145. high = page_number > (self.page_count / 2)
  146. if high:
  147. side_number = self.page_count - page_number + 1
  148. else:
  149. side_number = page_number
  150. odd_side = side_number % 2 # Detect odd paper sides, as sides have different page arrangements.
  151. right = True
  152. if (high and odd_side) or (not high and not odd_side):
  153. right = False
  154. max_sheets = self.page_count / 4
  155. current_sheet = ceil(side_number / 2)
  156. # Add page to correct group/paper sheet side.
  157. side_name = "p" + str(current_sheet)
  158. side_name += "f" if odd_side else "b"
  159. sidegroup = self.doc.nodeByName(side_name)
  160. if not sidegroup:
  161. sidegroup = self.doc.createGroupLayer(side_name)
  162. root = self.doc.rootNode()
  163. root.addChildNode(sidegroup, None)
  164. page.remove()
  165. sidegroup.addChildNode(page, None)
  166. # Offset calculation. First sheet has largest offset, centre sheet has zero offset. 3 pixel step.
  167. if self.offsetOuterCheck.isChecked():
  168. offset = 3 * (max_sheets - current_sheet)
  169. else:
  170. offset = 0
  171. # Position page (with or without offset)
  172. page_pos = page.position()
  173. bds = page.bounds()
  174. x_target = self.doc_width / 2
  175. x_diff = x_target - bds.left()
  176. orig_x = x_diff
  177. y_target = (self.doc_height - (bds.bottom() - bds.top())) / 2
  178. y_diff = y_target - bds.top()
  179. orig_y = y_diff
  180. if right:
  181. x_diff = x_diff + offset
  182. else:
  183. x_diff = x_diff - (bds.right() + 1 - bds.left()) - offset
  184. page.move(page_pos.x() + x_diff, page_pos.y() + y_diff)
  185. # Keep new bounds for crop marks.
  186. bds = page.bounds()
  187. # Bleed by stated amount on outward sides, bleed to middle on inward sides.
  188. bleed_amount = int(self.bleedInput.text())
  189. bleed_amounts = { "top": bleed_amount, "bottom": bleed_amount }
  190. if right:
  191. bleed_amounts["right"] = bleed_amount
  192. bleed_amounts["left"] = offset
  193. else:
  194. bleed_amounts["right"] = offset
  195. bleed_amounts["left"] = bleed_amount
  196. self.bleedPage(page, bleed_amounts)
  197. # Add crop marks. Which crop marks are created depends on which side the page is on.
  198. black_pixel = b'\x00\x00\x00\xff'
  199. crop_mark_width = 2
  200. crop_mark_height = 48
  201. crop_mark = QByteArray(black_pixel * crop_mark_width * crop_mark_height)
  202. ## Vertical cropmarks
  203. x = bds.right() if right else bds.left() - 1
  204. y = bds.top() - crop_mark_height - bleed_amount
  205. page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
  206. y = bds.bottom() + 1 + bleed_amount
  207. page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
  208. ## Horizontal cropmarks
  209. crop_mark_width = 48
  210. crop_mark_height = 2
  211. x = bds.right() + 1 + bleed_amount if right else bds.left() - bleed_amount - crop_mark_width
  212. y = bds.top() - 1
  213. page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
  214. y = bds.bottom()
  215. page.setPixelData(crop_mark, x, y, crop_mark_width, crop_mark_height)
  216. def bleedPage(self, page, amounts):
  217. # Top
  218. bds = page.bounds()
  219. xpos = bds.left()
  220. ypos = bds.top()
  221. width = bds.width()
  222. height = amounts['top']
  223. bleed_line = page.pixelData(xpos, ypos, width, 1)
  224. bleed = bleed_line * height
  225. page.setPixelData(bleed, xpos, ypos - height, width, height)
  226. # Bottom
  227. bds = page.bounds()
  228. xpos = bds.left()
  229. ypos = bds.bottom()
  230. width = bds.width()
  231. height = amounts['bottom']
  232. bleed_line = page.pixelData(xpos, ypos, width, 1)
  233. bleed = bleed_line * height
  234. page.setPixelData(bleed, xpos, ypos + 1, width, height)
  235. # Left
  236. bds = page.bounds()
  237. xpos = bds.left()
  238. ypos = bds.top()
  239. width = int(amounts['left'])
  240. height = bds.height()
  241. bleed_line = page.pixelData(xpos, ypos, 1, height)
  242. for c in range(1, width + 1):
  243. page.setPixelData(bleed_line, (xpos - c), ypos, 1, height)
  244. # Right
  245. bds = page.bounds()
  246. xpos = bds.right()
  247. ypos = bds.top()
  248. width = int(amounts['right'])
  249. height = bds.height()
  250. bleed_line = page.pixelData(xpos, ypos, 1, height)
  251. for c in range(1, width + 1):
  252. page.setPixelData(bleed_line, (xpos + c), ypos, 1, height)