Personal tools
You are here: Home Image handling personalizzato negli AT

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:

python_img

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:

python_img

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.

python_quad

Immagine ridimensionata e croppata fino alla dimensione 100x100

Filed under: , , ,
comments powered by Disqus