Nov 04, 2011
Image handling personalizzato negli AT
Un'applicazione mobile e la necessità di usare un certo formato immagine mi hanno spinto a capire come Plone gestisca la creazione delle immagini scalate e come ci si può inserire nel processo per fare delle personalizzazioni
Il caso d'uso
Lo sviluppo di una applicazione mobile che deve mostrare una lista di eventi ha fatto sorgere la necessità di avere un formato di immagine piccolo (facile con Plone), quadrato (mmhm...) e senza distorsioni dell'immagine (ok, non ci siamo): qualora l'immagine non fosse quadrata, la si dovrà "croppare".
Il comportamento standard IN plone
Plone base non fornisce la possibilità di prendere una immagine qualsiasi e restituirla in un formato miniaturizzato e sicuramente quadrato.
Prendiamo, ad esempio, questa immagine:

Immagine originale di dimensione 200x150
Agendo sulle funzionalità standard di plone, andiamo in ZMI nelle 'portal_properties/image_properties' del nostro sito, e aggiungiamo fra le 'allowed_size' una 'custom_size 100:100'; poi possiamo verificare caricando l'immagine (ha dimensione 200x150) che se chiamiamo sull'oggetto 'image_custom_size', ci viene tornata un'immagine 100x75:

Immagine ridimensionata con il comportamento standard di Plone
L'immagine è stata scalata in proporzione, non è questo che vogliamo! E quindi?
Studiamo la situazione
La prima domanda che è lecito e doveroso farsi è:
Cosa succede quando facciamo traversing su un oggetto usando come attributo da "attraversare" uno degli scaling disponibili per le immagini?
Stiamo lavorando su Plone 4 e indagando un poco si scopre che è stato registrato un adattatore per gestire il traversing in caso di immagini dentro a plone.app.imaging:
class ImageTraverser(DefaultPublishTraverse):
""" traversal adapter for scaled down versions of image content """
adapts(IBaseObject, IRequest)
def fallback(self, request, name):
return super(ImageTraverser, self).publishTraverse(request, name)
def publishTraverse(self, request, name):
schema = self.context.Schema()
if '_' in name:
fieldname, scale = name.split('_', 1)
else:
fieldname, scale = name, None
field = schema.get(fieldname)
handler = IImageScaleHandler(field, None)
if handler is not None:
image = handler.getScale(self.context, scale)
if image is not None:
return image
return self.fallback(request, name)
All'interno di questo adapter abbiamo il metodo publishTraverse da cui partire e un metodo che si occupa del fallback al traverser di default nel caso non si stia lavorando allo scaling delle immagini.
Inoltre, se si lavora allo scaling delle immagini, si chiama un altro adapter (IImageScaleHandler) che fornirà i metodi per creare l'oggetto con l'immagine ridimensionata:
handler = IImageScaleHandler(field, None)
if handler is not None:
image = handler.getScale(self.context, scale)
if image is not None:
return image
La soluzione
Ok, è sufficiente. Abbiamo tutto quello che ci serve sapere! Come nella maggior parte dello sviluppo che si fa per Plone/Zope, la Zope Component Architecture permetterà di fare tutto in modo relativamente veloce.
Io sto lavorando con delle immagini in un archetype customizzato, per cui la cosa più semplice da fare per me sarà registrare un adapter più specifico per l'interfaccia del mio tipo, ma possiamo banalmente proseguire l'esempio registrandone uno per IATImage:
class MyImageTraverser(DefaultPublishTraverse): """ traversal adapter for scaled down versions of image content """ adapts(IATImage, IRequest) def fallback(self, request, name): ... def publishTraverse(self, request, name): ...
Ora abbiamo il traverser per gli oggetti che implementano IATImage, e siamo già a metà del lavoro. Il successivo e ultimo passo sarà applicare la trasformazione vera e propria.
Come abbiamo già avuto modo di vedere, si richiama un handler nel traverser che si occupa di ottenere l'immagine scalata. Quello che possiamo fare è registrare un nostro handler personalizzato per il field che contiene l'immagine; magari chiamare un handler personalizzato in modo condizionato, per gestire soltanto i casi che ci interessano; qualcosa come:
if scale in crop_and_scale: handler = ICustomImageScaleHandler(field, None) else: handler = IImageScaleHandler(field, None)
L'handler originale si trova in plone.app.image, nello stesso modulo del traverser; analizzando il codice ci si rende conto che se nel sistema è presente plone.app.blob, di default, si userà il BlobImageScaleHandler presente in quest'ultimo pacchetto.
Noi potremo creare un handler personalizzato come adapter sul field che si usa per contenere l'immagine, e essendo presente plone.app.blob, lo creeremo ereditando dalla classe BlobImageScaleHandler.
All'interno dell'handler, ci sono tutti i metodi per ottenere l'immagine scalata.
Per raggiungre il nostro obiettivo, ereditando dal BlobImageScaleHandler, possiamo scrivere un handler personalizzato che conterrà un unico metodo, createScale: l'unico che ci serve personalizzare. Qui potremo applicare le trasformazioni necessarie. Il metodo originale esegue i seguenti passi:
def createScale(self, instance, scale, width, height, data=None):
""" create & return a scaled version of the image as retrieved
from the field or optionally given data """
field = self.context
if HAS_PIL and width and height:
if data is None:
image = field.getRaw(instance)
if not image:
return None
data = str(image.data)
if data:
id = field.getName() + '_' + scale
try:
imgdata, format = field.scale(data, width, height)
except (ConflictError, KeyboardInterrupt):
raise
except Exception:
if not field.swallowResizeExceptions:
raise
else:
exception('could not scale ImageField "%s" of %s',
field.getName(), instance.absolute_url())
return None
content_type = 'image/%s' % format.lower()
filename = field.getFilename(instance)
return dict(id=id, data=imgdata.getvalue(),
content_type=content_type, filename=filename)
return None
Per fare la modifica, nell'handler personalizzato potremo cambiare questa riga
imgdata, format = field.scale(data, width, height)
con quello che fa più comodo, e usando il crop delle PIL potremo trasformare l'immagine come necessario, arrivando a un risultato come questo.

Immagine ridimensionata e croppata fino alla dimensione 100x100