diff --git a/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5SafeImage_Selenium.py b/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5SafeImage_Selenium.py new file mode 100644 index 0000000000000000000000000000000000000000..1fcd8a01b04b85fa06fbf3828f0e67ab45f2d60d --- /dev/null +++ b/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5SafeImage_Selenium.py @@ -0,0 +1,51 @@ +import Image as PIL_Image +import os +import transaction +from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase + + +class FileUpload(file): + """Act as an uploaded file. + """ + __allow_access_to_unprotected_subobjects__ = 1 + def __init__(self, path, name): + self.filename = name + file.__init__(self, path) + self.headers = {} + +def makeFilePath(name): + # return os.path.join(os.path.dirname(__file__), 'tmp', name) + return name + +def makeFileUpload(name, as_name=None): + if as_name is None: + as_name = name + path = makeFilePath(name) + return FileUpload(path, as_name) + + +def uploadImage(self): + portal = self.getPortalObject() + image = portal.restrictedTraverse('portal_skins/erp5_safeimage/img/image_test.jpg') + path_image = "tmp/selenium_image_test.jpg" + fd = os.open(path_image, os.O_CREAT | os.O_RDWR) + os.write(fd,str(image.data)) + os.close(fd) + tile_image_transformed = makeFileUpload(path_image) + tile_transformed = self.image_module.newContent(portal_type='Image Tile Transformed', + title='testTileTransformed', id='testTileTransformed', + file=tile_image_transformed, filename='testTileTransformed') + if tile_transformed: + return True + else: + return False + +def cleanUp(self): + portal = self.getPortalObject() + print "exists path: %r" %os.path.exists("tmp/selenium_image_test.jpg") + if os.path.exists("tmp/selenium_image_test.jpg"): + print "REMOVE IMAGE: %s" %(os.remove("tmp/selenium_image_test.jpg")) + portal.image_module.manage_delObjects(ids=['testTileTransformed']) + return True + else: + return False diff --git a/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5ZoomifyImage.py b/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5ZoomifyImage.py new file mode 100644 index 0000000000000000000000000000000000000000..64117892a6d4df4a741515d090b5081606edb659 --- /dev/null +++ b/bt5/erp5_safeimage/ExtensionTemplateItem/ERP5ZoomifyImage.py @@ -0,0 +1,576 @@ +############################################################################## +# Copyright (C) 2005 Adam Smith asmith@agile-software.com +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +############################################################################## + +import os, sys, shutil, tempfile +import Image +from cStringIO import StringIO +from zLOG import LOG,ERROR,INFO,WARNING +from OFS.Image import File, Image +import os, transaction +from AccessControl import getSecurityManager, ClassSecurityInfo +from Globals import package_home +import Image as PIL_Image +import thread +import random +import base64 +from OFS.Folder import Folder + +class ZoomifyBase: + + _v_imageFilename = '' + format = '' + originalWidth = 0 + originalHeight = 0 + _v_scaleInfo = [] + numberOfTiles = 0 + _v_tileGroupMappings = {} + qualitySetting = 80 + tileSize = 256 + my_file = StringIO() + + def openImage(self): + """ load the image data """ + + pass + return + + def getImageInfo(self): + """ """ + + image = self.openImage() + self.format = image.format + self.originalWidth, self.originalHeight = image.size + image = None + width, height = (self.originalWidth, self.originalHeight) + self._v_scaleInfo = [(width, height)] + while (width > self.tileSize) or (height > self.tileSize): + width, height = (width / 2, height / 2) + self._v_scaleInfo.insert(0, (width, height)) + totalTiles=0 + tier, rows, columns = (0,0,0) + for tierInfo in self._v_scaleInfo: + rows = height/self.tileSize + if height % self.tileSize > 0: + rows +=1 + columns = width/self.tileSize + if width%self.tileSize > 0: + columns += 1 + totalTiles += rows * columns + tier += 1 + + def getImageMetadata(self): + """ given an image name, load it and extract metadata """ + + image = self.openImage() + self.format = image.format + self.originalWidth, self.originalHeight = image.size + image = None + # get scaling information + width, height = (self.originalWidth, self.originalHeight) + self._v_scaleInfo = [(width, height)] + while (width > self.tileSize) or (height > self.tileSize): + width, height = (width / 2, height / 2) + self._v_scaleInfo.insert(0, (width, height)) + # tile and tile group information + self.preProcess() + return + + def createDataContainer(self, imageName): + """ create a container for tile groups and tile metadata """ + + pass + return + + def getAssignedTileContainerName(self, tileFileName=None): + """ return the name of the tile group for the indicated tile """ + if tileFileName: + if hasattr(self, '_v_tileGroupMappings') and self._v_tileGroupMappings: + containerName = self._v_tileGroupMappings.get(tileFileName, None) + if containerName: + return containerName + x = self.getNewTileContainerName() + return x + + def getNewTileContainerName(self, tileGroupNumber=0): + """ return the name of the next tile group container """ + + return 'TileGroup' + str(tileGroupNumber) + + def createTileContainer(self, tileContainerName=None): + """ create a container for the next group of tiles within the data container """ + + pass + return + + def getTileFileName(self, scaleNumber, columnNumber, rowNumber): + """ get the name of the file the tile will be saved as """ + + return '%s-%s-%s.jpg' % (str(scaleNumber), str(columnNumber), str(rowNumber)) + + def getFileReference(self, scaleNumber, columnNumber, rowNumber): + """ get the full path of the file the tile will be saved as """ + + pass + return + + def getNumberOfTiles(self): + """ get the number of tiles generated + Should this be implemented as a safeguard, or just use the count of + tiles gotten from processing? (This would make subclassing a little + easier.) """ + + return self.numberOfTiles + + def getXMLOutput(self): + """ create xml metadata about the tiles """ + + numberOfTiles = self.getNumberOfTiles() + xmlOutput = '<IMAGE_PROPERTIES WIDTH="%s" HEIGHT="%s" NUMTILES="%s" NUMIMAGES="1" VERSION="1.8" TILESIZE="%s" />' + xmlOutput = xmlOutput % (str(self.originalWidth), + str(self.originalHeight), str(numberOfTiles), str(self.tileSize)) + return xmlOutput + + def saveXMLOutput(self): + """ save xml metadata about the tiles """ + + pass + return + + def saveTile(self, image, scaleNumber, column, row): + """ save the cropped region """ + + pass + return + + def processImage(self): + """ starting with the original image, start processing each row """ + tier=(len(self._v_scaleInfo) -1) + row = 0 + ul_y, lr_y = (0,0) + root, ext = os.path.splitext(self._v_imageFilename) + if not root: + root = self._v_imageFilename + ext = '.jpg' + image = self.openImage() + while row * self.tileSize < self.originalHeight: + ul_y = row * self.tileSize + if (ul_y + self.tileSize) < self.originalHeight: + lr_y = ul_y + self.tileSize + else: + lr_y = self.originalHeight + print "Going to open image" + imageRow = image.crop([0, ul_y, self.originalWidth, lr_y]) + saveFilename = root + str(tier) + '-' + str(row) + ext + if imageRow.mode != 'RGB': + imageRow = imageRow.convert('RGB') + imageRow.save(os.path.join(tempfile.gettempdir(), saveFilename), + 'JPEG', quality=100) + print "os path exist : %r" % os.path.exists(os.path.join( + tempfile.gettempdir(), saveFilename)) + if os.path.exists(os.path.join(tempfile.gettempdir(), saveFilename)): + self.processRowImage(tier=tier, row=row) + row += 1 + + def processRowImage(self, tier=0, row=0): + """ for an image, create and save tiles """ + + print '*** processing tier: ' + str(tier) + ' row: ' + str(row) + tierWidth, tierHeight = self._v_scaleInfo[tier] + rowsForTier = tierHeight/self.tileSize + if tierHeight % self.tileSize > 0: + rowsForTier +=1 + root, ext = os.path.splitext(self._v_imageFilename) + if not root: + root = self._v_imageFilename + ext = '.jpg' + imageRow = None + if tier == (len(self._v_scaleInfo) -1): + firstTierRowFile = root + str(tier) + '-' + str(row) + ext + if os.path.exists(os.path.join(tempfile.gettempdir(),firstTierRowFile)): + imageRow = PIL_Image.open(os.path.join(tempfile.gettempdir(), + firstTierRowFile)) + else: + # create this row from previous tier's rows + imageRow = PIL_Image.new('RGB', (tierWidth, self.tileSize)) + firstRowFile = root + str(tier+1) + '-' + str(row + row) + ext + firstRowWidth, firstRowHeight = (0,0) + secondRowWidth, secondRowHeight = (0,0) + if os.path.exists(os.path.join(tempfile.gettempdir(),firstRowFile)): + firstRowImage = PIL_Image.open(os.path.join(tempfile.gettempdir(), + firstRowFile)) + firstRowWidth, firstRowHeight = firstRowImage.size + imageRow.paste(firstRowImage, (0,0)) + os.remove(os.path.join(tempfile.gettempdir(), firstRowFile)) + secondRowFile = root + str(tier+1) + '-' + str(row + row +1) + ext + # there may not be a second row at the bottom of the image... + if os.path.exists(os.path.join(tempfile.gettempdir(), secondRowFile)): + secondRowImage = PIL_Image.open(os.path.join(tempfile.gettempdir(), + secondRowFile)) + secondRowWidth, secondRowHeight = secondRowImage.size + imageRow.paste(secondRowImage, (0, firstRowHeight)) + os.remove(os.path.join(tempfile.gettempdir(), secondRowFile)) + # the last row may be less than self.tileSize... + if (firstRowHeight + secondRowHeight) < (self.tileSize*2): + imageRow = imageRow.crop((0, 0, tierWidth, + (firstRowHeight+secondRowHeight))) + if imageRow: + # cycle through columns, then rows + column = 0 + imageWidth, imageHeight = imageRow.size + ul_x, ul_y, lr_x, lr_y = (0,0,0,0) # final crop coordinates + while not ((lr_x == imageWidth) and (lr_y == imageHeight)): + # set lower right cropping point + if (ul_x + self.tileSize) < imageWidth: + lr_x = ul_x + self.tileSize + else: + lr_x = imageWidth + if (ul_y + self.tileSize) < imageHeight: + lr_y = ul_y + self.tileSize + else: + lr_y = imageHeight + self.saveTile(imageRow.crop([ul_x, ul_y, lr_x, lr_y]), tier, + column, row) + self.numberOfTiles += 1 + # set upper left cropping point + if (lr_x == imageWidth): + ul_x=0 + ul_y = lr_y + column = 0 + else: + ul_x = lr_x + column += 1 + if tier > 0: + # a bug was discovered when a row was exactly 1 pixel in height + # this extra checking accounts for that + if imageHeight > 1: + tempImage = imageRow.resize((imageWidth/2, imageHeight/2), + PIL_Image.ANTIALIAS) + tempImage.save(os.path.join(tempfile.gettempdir(), root + str(tier) + + '-' + str(row) + ext)) + tempImage = None + rowImage = None + if tier > 0: + if row % 2 != 0: + self.processRowImage(tier=(tier-1), row=((row-1)/2)) + elif row == rowsForTier-1: + self.processRowImage(tier=(tier-1), row=(row/2)) + + def ZoomifyProcess(self, imageNames): + """ the method the client calls to generate zoomify metadata """ + + pass + return + + def preProcess(self): + """ plan for the arrangement of the tile groups """ + + tier = 0 + tileGroupNumber = 0 + numberOfTiles = 0 + for width, height in self._v_scaleInfo: + #cycle through columns, then rows + row, column = (0,0) + ul_x, ul_y, lr_x, lr_y = (0,0,0,0) #final crop coordinates + while not ((lr_x == width) and (lr_y == height)): + tileFileName = self.getTileFileName(tier, column, row) + tileContainerName = self.getNewTileContainerName( + tileGroupNumber=tileGroupNumber) + if numberOfTiles ==0: + self.createTileContainer(tileContainerName=tileContainerName) + elif (numberOfTiles % self.tileSize) == 0: + tileGroupNumber += 1 + tileContainerName = self.getNewTileContainerName( + tileGroupNumber=tileGroupNumber) + self.createTileContainer(tileContainerName=tileContainerName) + self._v_tileGroupMappings[tileFileName] = tileContainerName + numberOfTiles += 1 + # for the next tile, set lower right cropping point + if (ul_x + self.tileSize) < width: + lr_x = ul_x + self.tileSize + else: + lr_x = width + if (ul_y + self.tileSize) < height: + lr_y = ul_y + self.tileSize + else: + lr_y = height + # for the next tile, set upper left cropping point + if (lr_x == width): + ul_x=0 + ul_y = lr_y + column = 0 + row += 1 + else: + ul_x = lr_x + column += 1 + tier += 1 + + +class ZoomifyZopeProcessor(ZoomifyBase): + """ basic functionality to provide Zoomify functionality inside Zope """ + + _v_imageObject = None + _v_saveFolderObject = None + _v_transactionCount = 0 + security = ClassSecurityInfo() + security.declareObjectProtected('Add Documents, Images, and Files') + + def openImage(self): + """ load the image data """ + + return PIL_Image.open(self._v_imageObject.name) + + def createDefaultViewer(self): + """ add the default Zoomify viewer to the Zoomify metadata """ + + # also, add the default zoomifyViewer here if a zoomify viewer is acquirable + # (could this be done a better way, like using the 'web methods' + # approach that points to ZMI screens that are DTML or ZPT files + # in the product package)? + if not hasattr(self._v_saveFolderObject, 'default_ZoomifyViewer'): + defaultViewerPath = os.path.join(package_home(globals()), 'www', + 'zoomifyViewer.swf') + if os.path.isfile(defaultViewerPath): + fileContent = open(defaultViewerPath).read() + self._v_saveFolderObject._setObject('default_ZoomifyViewer', + File('default_ZoomifyViewer', '', fileContent, + 'application/x-shockwave-flash', '')) + transaction.savepoint() + return + + def createDataContainer(self): + """ create a folder that contains all the tiles of the image """ + + self._v_saveToLocation = str(self._v_imageObject.getId()) + '_data' + parent = self._v_imageObject.aq_parent + if hasattr(parent, self._v_saveToLocation): + # allow for tiles to be updated from a changed image + parent._delObject(self._v_saveToLocation) + if not hasattr(parent, self._v_saveToLocation): + newFolder = Folder() + newFolder.id = self._v_saveToLocation + parent._setObject(self._v_saveToLocation, newFolder) + self._v_saveFolderObject = parent[self._v_saveToLocation] + transaction.savepoint() + return + + def createTileContainer(self, tileContainerName=None): + """ create a container for the next group of tiles within the data container """ + + if hasattr(self._v_saveFolderObject, tileContainerName): + # allow for tiles to be updated from a changed image + self._v_saveFolderObject._delObject(tileContainerName) + if not hasattr(self._v_saveFolderObject, tileContainerName): + newFolder = Folder() + newFolder.id = tileContainerName + self._v_saveFolderObject._setObject(tileContainerName, newFolder) + transaction.savepoint() + return + + def getNumberOfTiles(self): + """ get the number of tiles generated + Should this be implemented as a safeguard, or just use the count of + tiles gotten from processing? (This would make subclassing a little + easier.) """ + return self.numberOfTiles + + def saveXMLOutput(self): + """ save xml metadata about the tiles """ + + if hasattr(self._v_saveFolderObject, 'ImageProperties.xml'): + # allow file to be updated from a changed image, regenerated tiles + self._v_saveFolderObject._delObject('ImageProperties.xml') + self._v_saveFolderObject._setObject('ImageProperties.xml', + File('ImageProperties.xml', '', self.getXMLOutput(), + 'text/xml', '')) + transaction.savepoint() + return + + def saveTile(self, image, scaleNumber, column, row): + """ save the cropped region """ + + w,h = image.size + if w != 0 and h != 0: + tileFileName = self.getTileFileName(scaleNumber, column, row) + tileContainerName = self.getAssignedTileContainerName( + tileFileName=tileFileName) + tileImageData = StringIO() + image.save(tileImageData, 'JPEG', quality=self.qualitySetting) + tileImageData.seek(0) + if hasattr(self._v_saveFolderObject, tileContainerName): + tileFolder = getattr(self._v_saveFolderObject, tileContainerName) + # if an image of this name already exists, delete and replace it. + if hasattr(tileFolder, tileFileName): + tileFolder._delObject(tileFileName) + # finally, save the image data as a Zope Image object + tileFolder._setObject(tileFileName, Image(tileFileName, '', + '', 'image/jpeg', '')) + tileFolder._getOb(tileFileName).manage_upload(tileImageData) + self._v_transactionCount += 1 + if self._v_transactionCount % 10 == 0: + transaction.savepoint() + return + + def encrypto(self): + if self.transformed: + # def encrypto(self,message='Attack at dawn'): + # """encrypto each thing""" + # key ='abcdefghijklmnop' + # iv = Random.new().read(AES.block_size) + # EncodeIV = lambda : base64.b64encode(iv) + # EncodeAES = lambda c, s: base64.b64encode(c.encrypt(message)) + # IVcoded = EncodeIV() + # cipher = AES.new(key,AES.MODE_CFB,iv) + # msg = EncodeAES(cipher,key) + # return msg + pass + else: + pass + return + + def _process(self): + """ the actual zoomify processing workflow """ + + self.createDataContainer() + self.createDefaultViewer() + self.openImage() + self.getImageMetadata() + self.processImage() + self.saveTransformedFile() + self.saveXMLOutput() + self.encrypto() + return + + def saveTransformedFile(self): + pass + return + + def _ZoomifyProcess(self): + """ factored out ZODB connection handling """ + + #import Zope + #app = Zope.app() + #get_transaction().begin() + self._process() + #app._p_jar.close() + #del app + return + + security.declareProtected('Add Documents, Images, and Files', 'ZoomifyProcess') + def ZoomifyProcess(self, id, imageObject=None): + """ factored out threading of process (removed for now) """ + if imageObject: + self._v_imageObject = imageObject + self._v_imageFilename = id + self._ZoomifyProcess() + return + +class ERP5ZoomifyZopeProcessor(ZoomifyZopeProcessor): + + def __init__(self, document,transformed=None): + self.document = document + self.transformed = transformed + self.count = 0 + + def createTileContainer(self, tileContainerName=None): + """ create each TileGroup """ + + self.document.newContent(portal_type="Image Tile Group", + title=tileContainerName, id=tileContainerName, + filename=tileContainerName) + return + + def createDefaultViewer(self): + """ add the default Zoomify viewer to the Zoomify metadata """ + pass + return + + def createDataContainer(self, imageName="None"): + """Creates nothing coz we are already in the container""" + pass + return + + def _updateTransformedFile(self,tile_group_id,tile_title): + """create and save the transform file""" + num = random.choice([0,1]) + while num >= 0: + algorithm = random.choice(['sepia','brightness','noise','lighten', + 'posterize','edge','none']) + if algorithm == 'lighten': + param1 = random.choice([-0.6,-0.5,-0.4,-0.3,-0.2,-0.1,0.1,0.2, + 0.3,0.4,0.5,0.6]) + param2 = 0 + elif algorithm == 'posterize': + param1 = random.choice([4,5,6,7,8,9,10,11,12,13,14,15,16,17, + 18,19,20,21]) + param2 = 0 + elif algorithm == 'brightness': + param1 = random.choice([-80,-60,-40,-20,20,40,60,80]) + param2 = random.choice([-0.3,-0.2,-0.1,0,0.1,0.5,0.9]) + else: + param1 = 0 + param2 = 0 + my_text = '%s %s %s %s %s %s \n' %(tile_group_id, tile_title, + algorithm, param1, param2, num) + self.my_file.write(my_text) + num = num - 1 + + + def saveTile(self, image, scaleNumber, column,row): + """save the cropped region""" + tileFileName = self.getTileFileName(scaleNumber, column, row) + tileContainerName = self.getAssignedTileContainerName( + tileFileName=tileFileName) + namesplit = tileFileName.split('.') + w,h = image.size + if w != 0 and h !=0: + tile_group_id = self.getAssignedTileContainerName() + tile_group=self.document[tile_group_id] + tileImageData= StringIO() + image.save(tileImageData, 'JPEG', quality=self.qualitySetting) + tileImageData.seek(0) + if tile_group is None: + raise AttributeError('unable to fine tile group %r' % tile_group_id) + w = tile_group.newContent(portal_type='Image', title=namesplit[0], + id=namesplit[0], file=tileImageData, filename=namesplit[0]) + if self.transformed: + self._updateTransformedFile(tile_group_id, namesplit[0]) + return + + def saveXMLOutput(self): + """save the xml file""" + my_string = StringIO() + my_string.write(self.getXMLOutput()) + my_string.seek(0) + self.document.newContent(portal_type='Embedded File', + id='ImageProperties.xml', file=my_string, + filename='ImageProperties.xml') + return + + def saveTransformedFile(self): + """add in Zope the transform file """ + if self.transformed: + self.my_file.seek(0) + self.document.newContent(portal_type='Embedded File', + id='TransformFile.txt', file=self.my_file, + filename='TransformFile.txt') + return + + +def getERP5ZoomifyProcessor(document,transformed=False): + return ERP5ZoomifyZopeProcessor(document,transformed) + diff --git a/bt5/erp5_safeimage/bt/revision b/bt5/erp5_safeimage/bt/revision index 9a037142aa3c1b4c490e1a38251620f113465330..9d607966b721abde8931ddd052181fae905db503 100644 --- a/bt5/erp5_safeimage/bt/revision +++ b/bt5/erp5_safeimage/bt/revision @@ -1 +1 @@ -10 \ No newline at end of file +11 \ No newline at end of file diff --git a/bt5/erp5_safeimage/bt/template_extension_id_list b/bt5/erp5_safeimage/bt/template_extension_id_list new file mode 100644 index 0000000000000000000000000000000000000000..f68a94cffa22cba6151b27fd9046ab624d0f5fc8 --- /dev/null +++ b/bt5/erp5_safeimage/bt/template_extension_id_list @@ -0,0 +1,2 @@ +ERP5SafeImage_Selenium +ERP5ZoomifyImage \ No newline at end of file