Image crop, resize and color conversion GUI

From Scribus Wiki
Revision as of 18:31, 20 July 2012 by Meira (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, 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)