Image crop, resize and color conversion GUI

From Scribus Wiki
Jump to navigation Jump to search
This article is part of the Scripts series.

This script cuts, converts the color space and saves an image in chosen file format. Needs Scribus >1.3.8 and uses Python Image Library (PIL).

Use: select a frame with image and run the script. Choose DPI, color space, file format and resize algorithm.

Caveats: CMYK colorspace is not recomended, as PIL CMYK generate a blank black plate. Let Scribus do the job.

PIL freezes Scribus in Ubuntu >10.04 and <12.04, if run as menu script, due to an Ubuntu bug. Runs OK in Scripter console (F9 keystroke) and in Windows XP or Wine. The script run without problems in Ubuntu >=12.04.

#! /usr/bin/env python
#-*- coding: utf-8 -*-
'''
Crop and resample selected photo in Scribus
Use: select a frame with image and run the script. 
It will create and load a new cropped TIFF image.

By prof. MS. José Antonio Meira da Rocha
mailto:joseantoniorocha@gmail.com
http://meiradarocha.jor.br
License GPL
2011-01-05a
PIL docs: http://infohost.nmt.edu/tcc/help/pubs/pil/image-constructors.html
http://www.pythonware.com/library/pil/handbook/image.htm
'''

import sys
sys.path.insert(0, "%1")
sys.path.insert(1, "/usr/lib/python2.7")
#sys.path.append("/usr/lib/python2.7")
#sys.path.append("")


py2 = py30 = py31 = False
version = sys.hexversion
if version >= 0x020504F0 and version < 0x03000000 :
    py2 = True    # Python 2.6 or 2.7
    from Tkinter import *
    import ttk
elif version >= 0x03000000 and version < 0x03010000 :
    py30 = True
    from tkinter import *
    import ttk
elif version >= 0x03010000:
    py31 = True
    from tkinter import *
    import tkinter.ttk as ttk
else:
    print ("""
    You do not have a version of python supporting ttk widgets..
    You need a version >= 2.6 to execute PAGE modules.
    """)
    sys.exit()

if sys.platform != 'win32':
    sys.path.append("/usr/lib/python2.6") # prevent 'import re' PIL bug in Scribus 1.3.8, Ubuntu 10.04
try:
    import scribus
except ImportError,err:
    print "This Python script is written for the Scribus scripting interface."
    print "It can only be run from within Scribus."
    sys.exit(1)

from scribus import UNIT_POINTS, UNIT_MILLIMETERS, UNIT_INCHES, LINE_SOLID, JOIN_MITTER, BUTTON_OK, BUTTON_NO, BUTTON_YES, ICON_WARNING, ICON_INFORMATION, pt, mm

import os
from string import Template

#############
# PIL library
#############
# 'import Image' cause Scribus freeze after script run, in Ubuntu 10.04-10.10. 
#  OK in Ubuntu 12.04, Windows XP, Python 2.5.4, PIL 1.1.7
try:
    import Image
except ImportError,err:
    print "This Python script is written for the PIL graphic interface."
    print "It should be instaled in Scribus Python tree."
    sys.exit(1)
#############################################
# OS string encoding for PIL handle filenames
# (too much time to discover this)
#############################################
encoding = sys.getfilesystemencoding()


statusText = 'Running script...'
resolutionValues = ['72','75','96','144','150','200','288','300','600','1200']
resolutionDefault = '200'
imageResolution = 200
colorValues = ['RGB','B&W','Grey scale','CMYK']
colorDefault = 'RGB'
filterDefault = 'ANTIALIAS'
fileTypeValues = ['.TIF','.JPG','.PNG','.GIF','.BMP']
fileTypeDefault = '.TIF'
imageFrame = ''
cropsufix = '_cropped'  # '_cropped'

###################
# Scribus Constants
UNDER_FRAME  = 0
AROUND_FRAME = 1
BOUNDING_BOX = 2
CONTOUR_LINE = 3
SCALETOFRAME = 1
FREESCALE = 0
PROPORTIONAL = 1
DISTORTED = 0

################
# Locale strings
################
openImageErrorLabel = 'Open error'
openImageErrorMsg = '<h2>'+openImageErrorLabel+'</h2>' \
    + '<p>There was an error trying to open the photo file.<br>Maybe the file has been lost.</p>'
#    
openDocLabel="Open an document"
askOpenDoc = '<h2>'+openDocLabel+'</h2>' \
    + '<p>Please, open a document and select a frame before using this script.</p>'
# Strings to open frame
imageFrameLabel = "Select a image frame"
askSelectImageFrame = '<h2>'+imageFrameLabel+'</h2>' \
    + "Select <b> a image frame </b>\n" \
    + "to crop the photo inside." 
# Out of range image strings
outOfRangeLabel = 'Rescale the picture'
msgOutOfRange = '<h2>'+outOfRangeLabel+'</h2><p>The photo is not well framed. Make a better framming.'
# Error on save image
errorOnSaveLabel = 'Saving error'
errorOnSaveMsg = '<h2>'+errorOnSaveLabel+'</h2><p>There was an error writing the image file.'
# Error on cropping
cropErrorLabel = 'Error cropping the image'
cropErrorMsg = '<h2>'+cropErrorLabel+'</h2><p>There was an error while cropping the image. Maybe this type of file is not supported.'
# Error on loading image
errorOnLoadLabel = 'Importing error'
errorOnLoadMsg = '<h2>'+errorOnLoadLabel+'</h2>There was an error when importing the image. Make sure the file exists.'

def set_Tk_var():
    # These are Tk variables used passed to Tkinter and must be
    # defined before the widgets using them are created.
    global resolution
    resolution = StringVar()
    resolution.set(resolutionDefault)
    global color
    color = StringVar()
    color.set(colorDefault)
    global filter
    filter = StringVar()
    filter.set(filterDefault)
    global fileType
    fileType = StringVar()
    fileType.set(fileTypeDefault)

######################
# Dialog definitons
######################
class cropImageDialog:
    def __init__(self, master=None):
        # Set background of toplevel window to match
        # current style
        style = ttk.Style()
        theme = style.theme_use()
        default = style.lookup(theme, 'background')
        master.configure(background=default)

        self.tLa34 = ttk.Label (master)
        self.tLa34.place(relx=0.04,rely=0.05)
        self.tLa34.configure(text="Resolution")

        self.tCo35 = ttk.Combobox (master)
        self.tCo35.place(relx=0.04,rely=0.15,relheight=0.11,relwidth=0.41)
        self.tCo35.configure(values=resolutionValues)
        self.tCo35.configure(justify="center")
        self.tCo35.configure(textvariable=resolution)

        self.tLa36 = ttk.Label (master)
        self.tLa36.place(relx=0.04,rely=0.3)
        self.tLa36.configure(text="Color")

        self.tCo37 = ttk.Combobox (master)
        self.tCo37.place(relx=0.04,rely=0.4,relheight=0.11,relwidth=0.41)
        self.tCo37.configure(justify="center")
        self.tCo37.configure(values=colorValues)
        self.tCo37.configure(textvariable=color)

        self.tLa32 = ttk.Label (master)
        self.tLa32.place(relx=0.04,rely=0.55)
        self.tLa32.configure(text="File type")

        self.tCo33 = ttk.Combobox (master)
        self.tCo33.place(relx=0.04,rely=0.65,relheight=0.11,relwidth=0.41)
        self.tCo33.configure(justify="center")
        self.tCo33.configure(values=fileTypeValues)
        self.tCo33.configure(textvariable=fileType)

        self.tLa38 = ttk.Labelframe (master)
        self.tLa38.place(relx=0.52,rely=0.05,relheight=0.72,relwidth=0.45)
        self.tLa38.configure(text="Filters")
        self.tLa38.configure(width="105")
        self.tLa38.configure(height="145")

        self.tLa38_tRa40 = ttk.Radiobutton (self.tLa38)
        self.tLa38_tRa40.place(relx=0.1,rely=0.10,relheight=0.2,relwidth=0.6)
        self.tLa38_tRa40.configure(variable=filter)
        self.tLa38_tRa40.configure(value="BICUBIC")
        self.tLa38_tRa40.configure(text="Bicubic")

        self.tLa38_tRa41 = ttk.Radiobutton (self.tLa38)
        self.tLa38_tRa41.place(relx=0.1,rely=0.30,relheight=0.2,relwidth=0.6)
        self.tLa38_tRa41.configure(variable=filter)
        self.tLa38_tRa41.configure(value="NEAREST")
        self.tLa38_tRa41.configure(text="Nearest")

        self.tLa38_tRa42 = ttk.Radiobutton (self.tLa38)
        self.tLa38_tRa42.place(relx=0.1,rely=0.50,relheight=0.2,relwidth=0.6)
        self.tLa38_tRa42.configure(variable=filter)
        self.tLa38_tRa42.configure(value="BILINEAR")
        self.tLa38_tRa42.configure(text="Bilinear")

        self.tLa38_tRa43 = ttk.Radiobutton (self.tLa38)
        self.tLa38_tRa43.place(relx=0.1,rely=0.70,relheight=0.2,relwidth=0.6)
        self.tLa38_tRa43.configure(variable=filter)
        self.tLa38_tRa43.configure(value="ANTIALIAS")
        self.tLa38_tRa43.configure(text="Antialias")

        #Buttons
        self.tBu44 = ttk.Button (master)
        self.tBu44.place(relx=0.13,rely=0.81)
        self.tBu44.configure(command=earlyExit)
        self.tBu44.configure(text="Cancel")

        self.tBu45 = ttk.Button (master)
        self.tBu45.place(relx=0.52,rely=0.81)
        self.tBu45.configure(command=ok)
        self.tBu45.configure(text="OK")

##################
# Test object type
##################

def isGroup(obj):
    """Verify if object is group."""
    type = scribus.getObjectType(obj)
    if (type == 'Polygon') and (scribus.selectionCount() > 1) and (obj.find(groupString) != -1):
        return 1
    else:
        return 0

def isTextFrame(obj):
    """Verify if object is text frame."""
    type = scribus.getObjectType(obj)
    if (type == 'TextFrame'):
        return 1
    else:
        return 0

def isGraphicFrame(obj):
    """Verify if object is image frame."""
    type = scribus.getObjectType(obj)
    if (type == 'ImageFrame'):
        return 1
    else:
        return 0

######################
# Open image
######################
def openImage(imageFrame):
    imf = scribus.getImageFile(imageFrame)
    try:
        image = Image.open(imf.encode(encoding)) # encode to let PIL handle accented filenames
    except:
        scribus.messageBox(openImageErrorLabel,openImageErrorMsg,ICON_WARNING,BUTTON_OK)
        sys.exit(1)
    return image,imf

##################
# Crop Image
##################
def cropImage(image,imageFrame):
    ''''''
    global unit,imageResolution
    
    ###########################
    # Calculate image resample
    # to press resolution (DPI)
    unit = scribus.getUnit()
    scribus.setUnit(UNIT_INCHES)
    if resolutionValues.count(resolution.get()) == 0:
        imageResolution = int(resolutionDefault)
    else:
        imageResolution = int(resolution.get())
    frameSizeX,frameSizeY = scribus.getSize(imageFrame)
    newWidth = int(frameSizeX * imageResolution)
    newHeight = int(frameSizeY * imageResolution)
    scribus.setUnit(unit)
    
    ####################
    # Calculate cropping
    imageXOffset = scribus.getProperty(imageFrame,'imageXOffset')
    imageYOffset = scribus.getProperty(imageFrame,'imageYOffset')
    frameSizeX,frameSizeY = scribus.getSize(imageFrame)
    imageXScale = scribus.getProperty(imageFrame,'imageXScale')
    imageYScale = scribus.getProperty(imageFrame,'imageYScale')
    imageSizeX, imageSizeY = image.size
    # calculate crop box
    left = int(imageXOffset * -1)
    top = int(imageYOffset * -1)
    right = int(left + (frameSizeX / imageXScale))
    bottom = int(top + (frameSizeY / imageYScale))
    # Limit crop to image area 
    # (avoid black areas due crop bigger than image)
    if right > imageSizeX: 
        right = imageSizeX
    if bottom > imageSizeY: 
        bottom = imageSizeY
    if imageXOffset > 0: 
        left = 0
    if imageYOffset > 0: 
        top = 0
    # Recalculate new image dimensions
    # to fit proportionaly
    proportionX = newWidth / (right-left)
    proportionY = newHeight / (bottom-top)
    if proportionX > proportionY:
        newHeight = newWidth * (bottom-top) /(right-left)
    else:
        newWidth = newHeight * (right-left) / (bottom-top)
    print left,top,right,bottom
    print imageSizeX, imageSizeY
    
    try:
        # Crop
        im = image.crop((left,top,right,bottom))
        
        if fileTypeValues.count(filter.get()) == 0:
            filterType = filterDefault
        else:
            filterType = filter.get()
        # Resize    
        if filterType == 'BICUBIC':
            im = im.resize((newWidth,newHeight),Image.BICUBIC)
        elif filterType == 'NEAREST':
            im = im.resize((newWidth,newHeight),Image.NEAREST)
        elif filterType == 'BILINEAR':
            im = im.resize((newWidth,newHeight),Image.BILINEAR)
        else:  # filterType == 'ANTIALIAS'
            im = im.resize((newWidth,newHeight),Image.ANTIALIAS)
    except:
        scribus.messageBox(cropErrorLabel,cropErrorMsg,ICON_WARNING,BUTTON_OK)
        earlyExit()
        
    #########################
    # color space conversion.
    # 
    try:
        if colorValues.count(color.get()) == 0:
            colorType = colorDefault
        else:
            colorType = color.get()
        # 'B&W','Grey scale','RGB','CMYK'
        if colorType == 'CMYK':
            if im.mode != 'CMYK':
                im = im.convert('CMYK')
        elif colorType == 'RGB':
            if im.mode != 'RGB':
                im = im.convert('RGB')
        elif colorType == 'B&W':
            if im.mode != '1':
                im = im.convert('1')
        elif colorType == 'Grey scale':
            if im.mode != 'L':
                im = im.convert('L')       
    except: print 'WARNING: Error in color conversion.' 
    return im

####################
# Test if file exist
####################
def fileExist(fileName):
    try:
        f = open(fileName,'r')
        return 1
    except:
        return 0
    
#################
# Save Image
#################    
def saveImage(newImage,imageFile):
    global fileType,filterDefault,imageResolution
    name,ext = os.path.splitext(imageFile)
    # choose file type
    if fileTypeValues.count(fileType.get()) == 0:
        fileType  = fileTypeDefault
    else:
        fileType = fileType.get()
    fileType = fileType.lower()
    # increment file version to avoid overwriting old files
    f = Template(name+cropsufix+'($version)'+fileType)
    count = 1
    newImageFile = f.substitute(version=str(count))
    while fileExist(newImageFile):
        count += 1
        newImageFile = f.substitute(version=str(count))
    try:
        #'encode' let PIL handle accented filenames
        newImage.save(newImageFile.encode(encoding),dpi=(imageResolution,imageResolution))
    except:
        scribus.messageBox(errorOnSaveLabel,errorOnSaveMsg,ICON_WARNING,BUTTON_OK)
        earlyExit()
    return newImageFile

#######################
# Reload image to frame
#######################
def reloadImage(newImageFile,imageFrame):
    try:
        scribus.loadImage(newImageFile,imageFrame)
    except:
        scribus.messageBox(errorOnLoadLabel,errorOnLoadMsg,ICON_WARNING,BUTTON_OK)
        earlyExit()
    scribus.setScaleImageToFrame(SCALETOFRAME,PROPORTIONAL,imageFrame)

##################
# Handle Image
##################
def handleImage():
    global image,imageFile,imageFrame
    image,imageFile = openImage(imageFrame)
    newImage = cropImage(image,imageFrame)
    newImageFile = saveImage(newImage,imageFile)
    reloadImage(newImageFile,imageFrame)
#
##################
# Do all the works
##################
def ok():
    try: root.destroy()
    except: pass
    try: handleImage()
    except: pass    
    quit()

def quit():
    earlyExit()

def earlyExit():
    if scribus.haveDoc():
        global unit
        scribus.setUnit(unit)
        scribus.setRedraw(True)
    scribus.statusMessage("")
    scribus.progressReset()
    try: root.destroy()
    except: pass
    scribus.setUnit(unit)
    if 'Image' in sys.modules:  
        del(sys.modules["Image"])
    sys.exit()

def init():
    pass

##################
# Handle selection
##################
def handleSelection():
    """Handle selected objects."""
    global imageFrame
    if (scribus.selectionCount() == 1) and isGraphicFrame(scribus.getSelectedObject()):
        imageFrame = scribus.getSelectedObject()
    else:
        scribus.messageBox(imageFrameLabel,askSelectImageFrame,ICON_WARNING,BUTTON_OK)
        earlyExit()


#####################
# Handle document
#####################
def handleDocument(): 
    """Handle documents """
    # If there are open document
    global unit
    if scribus.haveDoc():
        # turn off redraw
        scribus.setRedraw(False)
        # Save unit
        unit = scribus.getUnit() 
        # Define unit in points
        scribus.setUnit(UNIT_POINTS) # pre-press mesure system
        handleSelection()
    else:  # If there not open document.
        scribus.messageBox(openDocLabel,askOpenDoc,ICON_WARNING,BUTTON_OK)
        if 'Image' in sys.modules:  
            del(sys.modules["Image"])
        sys.exit()


###################
def vp_start_gui():
    global w, root
    root = Tk()
    root.title('Crop Image')
    root.geometry('235x180+263+103')
    set_Tk_var()
    handleDocument()
    w = cropImageDialog(root)
    init()
    root.mainloop()
    
###############
def main(argv):
    """Main entry point"""
    vp_start_gui()

def main_wrapper(argv):
    try:
        scribus.statusMessage(statusText)
        scribus.progressReset()
        main(argv)
    finally:
        if scribus.haveDoc():
            scribus.setRedraw(True)
        scribus.statusMessage("")
        scribus.progressReset()

if __name__ == '__main__':
    main_wrapper(sys.argv)