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