| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- from krita import DockWidget
- from PyQt5.QtWidgets import QWidget, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QButtonGroup, QRadioButton, QComboBox, QPushButton, QCheckBox
- from PyQt5.QtCore import QPointF
- from math import ceil, floor
- DOCKER_TITLE = 'Image Fitter'
- class ImageFitter(DockWidget):
- def __init__(self):
- super().__init__()
- self.setWindowTitle(DOCKER_TITLE)
- widget = QWidget()
- layout = QVBoxLayout()
- widget.setLayout(layout)
- layout.addSpacing(10)
- layout.addWidget(QLabel('Fit an image to a rectangle defined by guides.'))
- # Perform scaling or just centering? Many options below should be in the
- # same button group to be disabled en-masse.
- self.scalingCheck = QCheckBox('Scaling')
- self.scalingCheck.setChecked(False)
- layout.addWidget(self.scalingCheck)
- self.aspectCheck = QCheckBox('Keep Aspect Ratio')
- self.aspectCheck.setChecked(True)
- layout.addWidget(self.aspectCheck)
- # Scale image to fit horizontal or vertical rectangle bounds.
- self.matchMode = QComboBox()
- modes = ['Horizontal', 'Vertical']
- self.matchMode.addItems(modes)
- row0 = QHBoxLayout()
- row0.addWidget(QLabel('Scaling Match Mode:'))
- row0.addWidget(self.matchMode)
- layout.addLayout(row0)
- # Scaling strategy.
- # 'Box' is 'Nearest Neighbor'.
- # https://api.kde.org/appscomplete-api/krita-apidocs/libs/image/html/kis__filter__strategy_8h_source.html#l00083
- self.stratInput = QComboBox()
- strategies = ['Bicubic', 'Box', 'Bilinear', 'Lancoz3', 'Bell', 'BSpline', 'Mitchell', 'Hermite']
- self.stratInput.addItems(strategies)
- row1 = QHBoxLayout()
- row1.addWidget(QLabel('Scaling Strategy:'))
- row1.addWidget(self.stratInput)
- layout.addLayout(row1)
- # Button!
- layout.addSpacing(20)
- goButton = QPushButton("Fit")
- goButton.setIcon( Krita.instance().icon('animation_play') )
- layout.addWidget(goButton)
- # Add a stretch to prevent the rest of the content from stretching.
- layout.addStretch()
- # Add widget to the docker.
- self.setWidget(widget)
- # Hook up the action to the button.
- goButton.clicked.connect( self.fitImageLoop )
- # notifies when views are added or removed
- # 'pass' means do not do anything
- def canvasChanged(self, canvas):
- pass
- ##########
- # Slots
- ##########
- def fitImageLoop(self, e):
- self.doc = Krita.instance().activeDocument()
- # Get the selected layer(s).
- w = Krita.instance().activeWindow()
- v = w.activeView()
- layers = v.selectedNodes()
- didNotRun = True
- for layer in layers:
- if layer.type() == 'paintlayer':
- self.fitImage(layer)
- didNotRun = False
- if layer.type() == 'vectorlayer' and not self.scalingCheck.isChecked():
- self.fitImage(layer)
- didNotRun = False
- if didNotRun:
- dialog = QDialog()
- dialog.setWindowTitle("Paint Layer Required")
- layout = QVBoxLayout()
- layout.addWidget(QLabel('Image Fitter only works on paint layers. Please select one.'))
- dialog.setLayout(layout)
- dialog.exec_()
- else:
- # Refresh the view, or changes will not be immediately reflected.
- self.doc.refreshProjection()
- # Actually fits the image.
- def fitImage(self, layer):
- # Get centres and bounds for the layer and the target rectangle.
- bds = layer.bounds()
- layerCentre = self.getCentre(bds.left(), bds.right() + 1, bds.top(), bds.bottom() + 1)
- tbds = self.getTargetBounds(layerCentre)
- targetCentre = self.getCentre(tbds['left'], tbds['right'], tbds['top'], tbds['bottom'])
- # Handle Scaling. Don't scale if not needed.
- # Repeated scaling does weird things to images.
- if self.scalingCheck.isChecked():
- doScaling = False
- targetWidth = tbds['right'] - tbds['left']
- targetHeight = tbds['bottom'] - tbds['top']
- layerWidth = bds.right() - bds.left()
- layerHeight = bds.bottom() - bds.top()
- # Deal with aspect ratio. Simply adjust target dimensions.
- if self.aspectCheck.isChecked():
- aspectRatio = layerWidth / layerHeight
- mode = self.matchMode.currentIndex()
- if mode == 0: # fit horizontally
- targetHeight = int(targetWidth / aspectRatio)
- if layerHeight != targetHeight:
- doScaling = True
- else: # fit vertically
- targetWidth = int(targetHeight * aspectRatio)
- if layerWidth != targetWidth:
- doScaling = True
- else:
- if layerWidth != targetWidth or layerHeight != targetHeight:
- doScaling = True
- # Actual scaling.
- if doScaling:
- origin = QPointF(bds.left(), bds.top())
- layer.scaleNode(origin, targetWidth, targetHeight, self.stratInput.currentText())
- # Refresh bounds and centre, which are now changed.
- bds = layer.bounds()
- layerCentre = self.getCentre(bds.left(), bds.right() + 1, bds.top(), bds.bottom() + 1)
- # Round centre coordinates so that odd centres tend towards top-left.
- # Rule that was worked out:
- # - target values are always rounded down.
- # - layer values are rounded up if target is even, down if odd.
- for axis in ['x', 'y']:
- if layerCentre[axis].is_integer() == False and targetCentre[axis].is_integer():
- layerCentre[axis] = ceil(layerCentre[axis])
- else:
- layerCentre[axis] = floor(layerCentre[axis])
- targetCentre[axis] = floor(targetCentre[axis])
- # Reposition.
- # The layer move() function works according to the "actual" position of
- # the layer, which starts off as the top corner of the image (0, 0).
- # So, instead of giving move() an absolute position, we have to
- # translate the layer's position by applying the difference between the
- # actual layer bounds and the target destination.
- # mid = getLayerCentre(layer) # Bounds may have changed, if scaling.
- diffX = layerCentre['x'] - targetCentre['x']
- diffY = layerCentre['y'] - targetCentre['y']
- layerPos = layer.position()
- layer.move(layerPos.x() - diffX, layerPos.y() - diffY)
- def getCentre(self, left, right, top, bottom):
- return {'x': (left + right)/2, 'y': (top + bottom)/2}
- def getTargetBounds(self, layerCentre):
- # Horizontal guides provide the Vertical y-axis points and vice versa.
- # Also include the edges of the image for the rectangle.
- vPoints = self.doc.horizontalGuides()
- hPoints = self.doc.verticalGuides()
- vPoints.insert(0, 0)
- vPoints.append(self.doc.height())
- hPoints.insert(0, 0)
- hPoints.append(self.doc.width())
- # Sanitize points. Guide values are floats, and may contain unexpected
- # extra fractional values.
- vPoints = [round(v) for v in vPoints]
- vPoints.sort()
- hPoints = [round(h) for h in hPoints]
- hPoints.sort()
- # Determine the boundaries most relevant to the layer
- bounds = {}
- prevV = 0
- for v in vPoints:
- if v >= layerCentre['y']:
- bounds['top'] = prevV
- bounds['bottom'] = v
- break
- prevV = v
- prevH = 0
- for h in hPoints:
- if h >= layerCentre['x']:
- bounds['left'] = prevH
- bounds['right'] = h
- break
- prevH = h
- return bounds
|