Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Search by External File #7

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# Krita Photobash Plugin

An advanced Krita Plugin, laser-focused on improving productivity for photo-bashing and references!
Want to see this in action? Check out the [video](https://youtu.be/QX9jwhfpB_8)! Tested in Krita 4.4.8, 5.0 and 5.1.
Want to see this in action? Check out the [video](https://youtu.be/QX9jwhfpB_8)! Tested in Krita 4.4.8, 5.0 and 5.1.,5.2.1

## Changes
- Supports search by extra caption file (must be a text file with the same name as the image)
- Supports adding image with transparency layer
- Supports adding image grouped with an erase blending layer
- Supports reloading images in directory without needing to reset to a different directory
- Supports sorting by date (in descending order)

## Installation

Expand All @@ -20,7 +27,9 @@ After setting the references folder, you now have a list of 9 images in the dock
- Clicking on the "next" and "previous" buttons on the bottom row of the docker;
- Scrolling the slider next to the pages indicator;
- Mouse Wheel Up and Down;
- Alt + Drag Left or Right, in case you're using a stylus.
- Alt + Drag Left or Right, in case you're using a stylus.
- Clicking with Middle Mouse Button on an image adds the image with a transparency mask
- Ctrl + Left Click creates a group with the image + a layer with blending mode set to erase

If the images in the folders are of large size, there may be some slowdown when scrolling quickly. However, the plugin is caching the previews, and stores up to 90 images, so you can scroll through them back more easily later.

Expand All @@ -40,5 +49,7 @@ You can also have some extra features by right-clicking on an image. This will o
- **Pin to Beginning / Unpin**: You can add "favourites" to an image, by pinning them to the beginning. This is useful if you have a select few images that you like to re-use, but are on different pages. This way you can have an easy way to access them, which will persist across restarts. It will only forget the favourite images if you decide to change the references folder. You can also unpin the images to send them to their original placement. A favourite will have a triangle in the top-left corner.
- **Open as New Document**: Opens the image as a new document, but keep in mind that this is the original image. If you save it, it will override the one you have on your references folder.
- **Place as Reference**: You can add an image as reference, and place it wherever you want! If you want to remove a reference, you need to press the "Pushpin Icon" on your toolbox, and remove it using that tool.
- **Add with Transparency**: Add the image with a transparency mask. White is to keep the pixels and Black is to erase. The added mask is white filled but the default behavior is to remove pixels if moving the transparency layer.
- **Group with Erase Layer**: Adds an image in a group with an erase blending layer. Easier to move compared to **Add with Transparency** butcan't add any additional images to the group without having the erase layer affect all images in the group.

#### Hope you enjoy this plugin, and feel free to post your artworks over on [Krita Artists](https://krita-artists.org/)!
#### Hope you enjoy this plugin, and feel free to post your artworks over on [Krita Artists](https://krita-artists.org/)!
190 changes: 168 additions & 22 deletions photobash_images/photobash_images_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# along with this
# program. If not, see <http://www.gnu.org/licenses/>.

from krita import *
from enum import Enum
from operator import itemgetter
import copy
import math
from PyQt5 import QtWidgets, QtCore, uic
Expand All @@ -26,7 +28,13 @@
)
import os.path

class TransparencyType(Enum):
NONE = 0
LAYER = 1
BLEND = 2

class PhotobashDocker(DockWidget):

def __init__(self):
super().__init__()

Expand All @@ -50,11 +58,16 @@ def setupVariables(self):
self.imagesButtons = []
self.foundImages = []
self.favouriteImages = []

# maps path to image
self.cachedImages = {}
self.cachedSearchKeywords = {}
self.cachedDatePaths = []
self.order = []
# store order of push
self.cachedPathImages = []
self.maxCachedImages = 90
self.maxCachedSearchKeyword = 2000
self.maxNumPages = 9999

self.currPage = 0
Expand Down Expand Up @@ -93,6 +106,7 @@ def setupInterface(self):
self.layout.imagesButtons8,
]


# Adjust Layouts
self.layout.imageWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
self.layout.middleWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
Expand All @@ -103,10 +117,30 @@ def setupInterface(self):
# setup connections for bottom elements
self.layout.previousButton.clicked.connect(lambda: self.updateCurrentPage(-1))
self.layout.nextButton.clicked.connect(lambda: self.updateCurrentPage(1))
self.layout.refresh.clicked.connect(self.refresh_cache)
self.layout.dateSort.stateChanged.connect(self.sortByDate)
self.layout.scaleSlider.valueChanged.connect(self.updateScale)
self.layout.paginationSlider.setMinimum(0)
self.layout.paginationSlider.valueChanged.connect(self.updatePage)
self.layout.fitCanvasCheckBox.stateChanged.connect(self.changedFitCanvas)
self.sortImagesByDate = False

def refresh_cache(self):
self.favouriteImages = []
self.foundImages = []
self.cachedSearchKeywords = {}
self.getImagesFromDirectory()

def sortByDate(self,state):
if state == Qt.Checked:
self.sortImagesByDate = True
self.order = copy.deepcopy(self.foundImages)
else:
self.sortImagesByDate = False
self.foundImages = copy.deepcopy(self.order)
self.reorganizeImages()
self.updateImages()


def setupModules(self):
# Display Single
Expand All @@ -129,6 +163,10 @@ def setupModules(self):
imageButton.SIGNAL_UN_FAVOURITE.connect(self.unpinFromFavourites)
imageButton.SIGNAL_OPEN_NEW.connect(self.openNewDocument)
imageButton.SIGNAL_REFERENCE.connect(self.placeReference)
imageButton.SIGNAL_ADD_WITH_TRANS_LAYER.connect(self.add_with_layer)
imageButton.SIGNAL_ADD_WITH_ERASE_GROUP.connect(self.add_with_blend)
imageButton.SIGNAL_MMD.connect(self.add_image_with_layer)
imageButton.SIGNAL_CTRL_LEFT.connect(self.add_image_with_group)
self.imagesButtons.append(imageButton)

def setStyle(self):
Expand All @@ -147,6 +185,7 @@ def initialize(self):
self.layout.scaleSliderLabel.setText(f"Image Scale : 100%")

self.updateImages()
self.getImagesFromDirectory()

def reorganizeImages(self):
# organize images, taking into account favourites
Expand All @@ -156,8 +195,22 @@ def reorganizeImages(self):
if image in self.foundImages:
self.foundImages.remove(image)
favouriteFoundImages.append(image)

self.foundImages = favouriteFoundImages + self.foundImages
if not self.sortImagesByDate:
self.foundImages = favouriteFoundImages + self.foundImages
else:
cachedDates = self.cachedDatePaths[:]
for i in self.cachedDatePaths:
if i['value'] not in self.foundImages:
cachedDates.remove(i)
favouriteDateImages = []
for favourite in self.favouriteImages:
for image in self.cachedDatePaths:
if image['value'] == favourite:
cachedDates.remove(image)
favouriteDateImages.append(image)
break
sorted_dates = sorted(favouriteDateImages,key=itemgetter('date'),reverse=True) + sorted(cachedDates,key=itemgetter('date'),reverse=True)
self.foundImages = [image['value'] for image in sorted_dates]

def textFilterChanged(self):
stringsInText = self.layout.filterTextEdit.text().lower().split(" ")
Expand All @@ -173,8 +226,13 @@ def textFilterChanged(self):
# exclude path outside from search
if word in path.replace(self.directoryPath, "").lower() and not path in newImages and word != "" and word != " ":
newImages.append(path)
elif path in self.cachedSearchKeywords:
searchString = ",".join(self.cachedSearchKeywords[path]).lower()
if word in searchString and not path in newImages and word != "" and word != " ":
newImages.append(path)

self.foundImages = newImages
self.order = copy.deepcopy(self.foundImages)
self.reorganizeImages()
self.updateImages()

Expand All @@ -190,19 +248,32 @@ def getImagesFromDirectory(self):

it = QDirIterator(self.directoryPath, QDirIterator.Subdirectories)


cache_date_paths = []
while(it.hasNext()):
if (".webp" in it.filePath() or ".png" in it.filePath() or ".jpg" in it.filePath() or ".jpeg" in it.filePath()) and \
(not ".webp~" in it.filePath() and not ".png~" in it.filePath() and not ".jpg~" in it.filePath() and not ".jpeg~" in it.filePath()):
self.cacheSearchTerms(it.filePath())
newImages.append(it.filePath())

cache_date_paths.append({
'date':os.path.getmtime(it.filePath()),
'value':it.filePath()
})
it.next()

self.cachedDatePaths = sorted(cache_date_paths,key=itemgetter('date'),reverse=True)
self.foundImages = copy.deepcopy(newImages)
self.allImages = copy.deepcopy(newImages)
self.reorganizeImages()
self.updateImages()

def cacheSearchTerms(self,key):
baseName = os.path.basename(key)
fileName = os.path.splitext(baseName)[0]
keywordFile = self.directoryPath +"/"+ fileName +'.txt'
if os.path.exists(keywordFile):
with open(keywordFile,'r') as searchFile:
lines = searchFile.readlines(self.maxCachedSearchKeyword)
self.cachedSearchKeywords[key] = lines

def updateCurrentPage(self, increment):
if (self.currPage == 0 and increment == -1) or \
((self.currPage + 1) * len(self.imagesButtons) > len(self.foundImages) and increment == 1) or \
Expand Down Expand Up @@ -267,12 +338,19 @@ def getImage(self, path):
if len(self.cachedImages) > self.maxCachedImages:
removedPath = self.cachedPathImages.pop()
self.cachedImages.pop(removedPath)

if removedPath in self.cachedSearchKeywords:
self.cachedSearchKeywords.pop(removedPath)
for i in self.cachedDatePaths[:]:
if i['value'] == removedPath:
self.cachedDatePaths.remove(i)
break
self.cachedPathImages = [path] + self.cachedPathImages
self.cachedImages[path] = QImage(path).scaled(200, 200, Qt.KeepAspectRatio, Qt.FastTransformation)

self.cachedImages[path] = QImage(path).scaled(200, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
return self.cachedImages[path]


def splitPathName(self,path):
baseName = os.path.basename(path)
return baseName
# makes sure the first 9 found images exist
def checkValidImages(self):
found = 0
Expand Down Expand Up @@ -323,7 +401,21 @@ def updateImages(self):
self.layout.paginationSlider.setRange(0, maxNumPage - 1)
self.layout.paginationSlider.setSliderPosition(self.currPage)

def addImageLayer(self, photoPath):

def add_image_with_layer(self,position):
if position < len(self.foundImages) - len(self.imagesButtons) * self.currPage:
self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage],TransparencyType.LAYER)

def add_image_with_group(self,position):
if position < len(self.foundImages) - len(self.imagesButtons) * self.currPage:
self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage],TransparencyType.BLEND)
def add_with_layer(self,photoPath):
self.addImageLayer(photoPath,TransparencyType.LAYER)

def add_with_blend(self,photoPath):
self.addImageLayer(photoPath,TransparencyType.BLEND)

def addImageLayer(self, photoPath,transparency_type):
# file no longer exists, remove from all structures
if not self.checkPath(photoPath):
self.updateImages()
Expand Down Expand Up @@ -356,11 +448,55 @@ def addImageLayer(self, photoPath):
mimedata.setUrls([url])
mimedata.setImageData(image)

# Set image in clipboard
QApplication.clipboard().setImage(image)

# Place Image and Refresh Canvas
Krita.instance().action('edit_paste').trigger()
# get doc data
root = doc.rootNode()
active_node = doc.activeNode()

# get base name
file_name = os.path.basename(photoPath)

# create layer with base name
new_layer = doc.createNode(file_name,'paintlayer')

# copy bytes from qImage into the image layer
ptr = image.bits()
ptr.setsize(image.byteCount())
new_layer.setPixelData(QByteArray(ptr.asstring()),0,0,image.width(),image.height())

# center the layer
center_x = int((doc.width() - image.width())/2)
center_y = int((doc.height() - image.height())/2)
new_layer.move(center_x,center_y)
if transparency_type == TransparencyType.NONE or transparency_type == TransparencyType.LAYER:
root.addChildNode(new_layer,None)
if transparency_type == TransparencyType.LAYER:
## create a layer
transparency_layer = doc.createTransparencyMask('transparencymask')

## fill with white
trans_image = image.scaled(doc.width(),doc.height(), Qt.IgnoreAspectRatio, Qt.FastTransformation)
trans_image.fill(QColor('white'))

## copy bytes over
trans_pointer = trans_image.bits()
trans_pointer.setsize(trans_image.byteCount())
transparency_layer.setPixelData(QByteArray(trans_pointer.asstring()),0,0,doc.width(),doc.height())

new_layer.addChildNode(transparency_layer,None)

## move layer so centered image is not cropped.
transparency_layer.move(0,0)

if transparency_type == transparency_type.BLEND:
new_group = doc.createGroupLayer(file_name)
root.addChildNode(new_group,None)
erase = doc.createNode('erase','paintLayer')
erase.setBlendingMode('erase')
new_group.addChildNode(new_layer,None)
new_group.addChildNode(erase,None)


#Refresh Canvas
Krita.instance().activeDocument().refreshProjection()

def checkPath(self, path):
Expand All @@ -371,7 +507,12 @@ def checkPath(self, path):
self.allImages.remove(path)
if path in self.favouriteImages:
self.favouriteImages.remove(path)

if path in self.cachedSearchKeywords:
self.cachedSearchKeywords.remove(path)
for i in self.cachedDatePaths[:]:
if i['value'] == path:
self.cachedDatePaths.remove(i)
break
dlg = QMessageBox(self)
dlg.setWindowTitle("Missing Image!")
dlg.setText("This image you tried to open was not found. Removing from the list.")
Expand Down Expand Up @@ -405,7 +546,8 @@ def placeReference(self, path):
Krita.instance().action('paste_as_reference').trigger()

def openPreview(self, path):
self.imageWidget.setImage(path, self.getImage(path))
image = QImage(path)
self.imageWidget.setImage(path,image)
self.layout.imageWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.layout.middleWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)

Expand Down Expand Up @@ -447,7 +589,7 @@ def canvasChanged(self, canvas):

def buttonClick(self, position):
if position < len(self.foundImages) - len(self.imagesButtons) * self.currPage:
self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage])
self.addImageLayer(self.foundImages[position + len(self.imagesButtons) * self.currPage],TransparencyType.NONE)

def changePath(self):
fileDialog = QFileDialog(QWidget(self))
Expand All @@ -460,12 +602,16 @@ def changePath(self):

title = "Change Directory for Images"
dialogOptions = QFileDialog.ShowDirsOnly | QFileDialog.DontUseNativeDialog
self.directoryPath = fileDialog.getExistingDirectory(self.mainWidget, title, path, dialogOptions)
new_path = fileDialog.getExistingDirectory(self.mainWidget, title, path, dialogOptions)
if self.directoryPath != "" and new_path == "":
return
self.directoryPath = new_path
Application.writeSetting(self.applicationName, self.referencesSetting, self.directoryPath)

self.favouriteImages = []
self.foundImages = []

self.cachedSearchKeywords = {}
self.cachedDatePaths = []
Application.writeSetting(self.applicationName, self.foundFavouritesSetting, "")

if self.directoryPath == "":
Expand Down
Loading