Personal tools

images

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

Jul 08, 2010

Scripted CSS Injection (or whatever better name you can find for this technique)

While trying to close a request for one of our customer for obtaining a random image portlet I tested an alternative way to deliver CSS. Using Javascript.

When Web pages load and run things

Let's start with CSS. Browsers load HTML source from the Web. Inside the page you will find resources that are CSS file. Immediately the resource is loaded and the rules inside are applied to your HTML.

Now switch to Javascript resources. For Javascript... it's the same. The Javascript code is executed as soon as it is found in the page...

...but for this reason, when we need to act using Javascript on an already loaded DOM, we rely onto Javascript events.

We read immediately the code, but the execution is postponed later, when the page is fully loaded.

As the use of jQuery became standard for those tasks (especially in Plone) we always use something like this:

jq(document).ready(function() {
    // do something
});

When this lead to problems

Although we have really no choice, there are some cases where this "postpone things" is not perfect: when we need to apply (using Javascript) CSS classes on page elements at page load time.

But we can't avoid making those actions when page is loaded.

If we don't rely on onload event, we have no ready DOM to traverse. So we can't load and change a DOM node if the page is not fully loaded (even if we put the Javascript script after the HTML that define the node).

<html>
<body>
    <div id="foo">Hello world</div>
    <script type="text/javascript">
    <!--
        var foo = document.getElementById("foo");
        alert(foo.innerHTML);
    // -->
    </script>
</body>
</html>

The code above is bad, even if you are using or not jQuery... So we really need to wait for the moment when DOM is ready. You can't act of the page DOM before it is fully loaded.

However: what is the problem applying CSS style when the DOM is loaded?

The nasty effect can be a visual flip.

The page in the browser show the DOM node with the original CSS style, then after some time (that can be not so brief sometimes if the page is full of elements and heavy scripts) the Javascript engine run your code, and the node is changed: your new CSS class or your new scripted style is applied.

<html>
<head>
    <style type="text/css">
    #foo {
        background-color: red;
    }
    </style>
    <script type="text/javascript">
    <!--
        window.onload = function() {
            ... MANY OTHER EXPENSIVE OPERATIONS
            var foo = document.getElementById("foo");
            alert(foo.innerHTML);
        }
    // -->
    </script>
</head>
<body>
    <div id="foo">Hello world</div>
    ... A LOT OF MANY AND MANY HTML NODES
    <script type="text/javscript">
    <!--
        var foo = document.getElementById("foo");
        alert(foo.innerHTML);
    // -->
    </script>
</body>
</html>

A practical example

A customer ask us to develop a Plone portlet that:

  • show some random images when the page is load
  • works behind a reverse proxy (Varnish)
  • works with Javascript disabled (accessibility and graceful degradation)

Step 1

Varnish is caching all our resource, images and also HTML for every page. We can't (and don't want) change this.
How to cache everything but some little images inside a portlet?

The idea is to use Javascript  for performing AJAX request for this portlet and obtain a structure of data. The cache of this kind of request can be avoided easily.

Step 2

So we are able to load an HTML for the portlet without images then, when the DOM is ready, we can populate the portlet waiting for the AJAX call to the server. For some time the visitor see and empty portlet that magically begin load images. The effect is pleasant (at least... it's not annoying).

But we can't!
The portlet must work also with disabled Javascript... So we must load random images also when the page is loaded.

NB: if the visitor use a browser with Javascript disabled, we can only give him some random pre-loaded images, but we can't prevent Varnish cache of the whole page. Reloading the same page will show him the same images for some minutes. This is acceptable for us (and for the customer!).

Step 3

The final result is to load the first "static" images in the portlet itself, then use Javascript as described at step 1: changing those images with new ones obtained from AJAX call.

This lead to the ugly visual flip effect I talked above.

I can't explain why (this is not my work), but see an empty section that is filled after a little delay is not ugly... instead seeing a set of images that suddenly change to other is... bothersome!

Step 4?

Ok, so we can simply load static images hidden by some CSS class, then using Javascript we can show them only after the AJAX call and...
Opss!

But in this way we don't see any image when Javascript is disabled!

Ok... step 4 aborted.

Scripted CSS Injection

The perfect world is the one where the step 4 is performed, but only with Javascript enabled.

I need a CSS that is loaded early like all other CSS in the page (so its style is applied immediately to the page) but only when Javascript is enabled.

I found a way to do this, but surfing the web I was not able to find other example like this one. So I called this approach Scripted CSS Injection (SCI)... maybe someone can point me to other original name or example?

However... how this works? Simply generating the additional CSS... with Javascript!
For this we use the standard window.write Javascript API. The window.write command is used commonly to write HTML inside windows (is more common to use it in popup windows for generating the contained HTML from scratch).

The additional Javascript is load in the page head section and it doesn't wait for DOM load. The one in our product is only one line:

document.write('<style type="text/css">.hideFlag img {display: none}</style>');

As I said at the beginning, Javascript is interpreted as CSS, so immediately when found in the page.
The browser will add to HTML the style node immediatly.

What is nice of SCI approach is obvious: a browser with no Javascript support can't add the CSS rule to the page!

Fairytale gone well

This technique finally lead us to a portlet that:

  • will show cached images if without Javascript support, but images are still random (chosen server side and changed with some delay)
  • will show random (and not cachable) images client side if Javascript is enabled
  • No ugly visual flip effects. With Javascript enabled static images are loaded hidden, then new dynamic ones are taken from the server and show. Thanks to SCI approach.

For more info, check the code of auslfe.portlet.multimedia.

Apr 24, 2010

Cool as Cooliris

Come inserire una galleria fotografica in Plone dalla spiaggia sorseggiando un drink

Cooliris ti permette di ottenere info da una risorsa multimediale web e di creare da queste una galleria fotografica estremamente attraente. Cooliris è stato pensato per le fonti più famose (flickr, youtube, picasa e facebook). In generale però ti permette di utilizzare un media rss generico.

Non è un tool plone, non è fortemente integrato, è tutto bellamente esterno. Se ne occupa qualcun altro a farlo funzionare. ahhh, relax.

 

Ma perchè ci interessa in ambito Plone?

Plone può essere utilizzato come portale, come blog, come intranet ecc. chiaramente inserire una galleria Cooliris vien comodo.

Questo blog è ovviamente realizzato in plone (what else? :) ed è stato banale inserire la gallery qui sopra. Ho semplicemente utilizzato il sistema Express di Cooliris e generato quello che loro chiamano "wall". Poi l'ho incluso in questo post.

D'altro canto, Plone è anche un generatore di contenuti.

Tutti sappiamo che possiamo inserire immagini in un portale plone, ma anche inserirle all'interno di una news o anche creare tipi ad-hoc che le prevedano.

Chiaramente anche eventuali video possono essere gestiti, così come (ad esempio se si utilizza redturtle.video) ottenere riferimenti a contenuti che sono su youtube.

Vien facile quindi pensare che, con un po' di programmazione, si possa dire a plone di generare, magari a partire da una collezione, un media rss che cooliris possa lavorare per noi. Un po' come si fa per il classico rss della collezione stessa.

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
      <rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
      xmlns:atom="http://www.w3.org/2005/Atom">
      <channel>
          <item>
               <title>Picture A</title>
               <media:description> This one's my favorite.</media:description>
               <link>pl_images/A.jpg</link>
               <media:thumbnail url="http://example.com/pl_thumbs/A.jpg"/>
               <media:content url="http://example.com/pl_images/A.jpg"/>
          </item>
          <item>
              <title>Video B</title>
              <link>http://example.com/pl_images/B.jpg</link>
              <media:thumbnail url="http://example.com/pl_thumbs/B.jpg"/>
              <media:content type="video/x-flv"
              url="http://example.com/pl_images/B.flv"/>
          </item>
      </channel>
      </rss>

 

In questo modo puoi:

  • avere contenuti "social" aggregati in qualche modo sul tuo portale
  • avere contenuti del tuo portale che diventano "social" via i media rss
  • avere i tuoi contenuti in una gallery "cool" nel tuo stesso portale (o in altri che tu stesso gestisci)

In aggiunta al servizio Express hai anche a disposizione un po' di API (se non hai delle API non sei nessuno :) e scegliere se utilzzare javascript o flash per realizzare la tua gallery. Chiaramente non è un lavoro per comuni mortali, serve sempre almeno tuo cugino, quello che sa di internet, o forse magari un bravo professionista :D

Tutto facile, tutto già pronto?

Un buon uomo di marketing ti direbbe: "thatì's all folks! enjoy"

Per vostra fortuna non sono ancora passato al lato oscuro della forza. Mi piace, quindi, ricordarvi che, per un sacco di buone ragioni, il buon Plone non ti permette di inserire tag object all'interno del corpo della pagina.

Niente di devastate comunque, occorre che vi ricordiate di abilitarlo esplicitamente. E il nostro amico Sam Knox ci ricorda come.

Chiaramente questo settaggio farà sì che ogni utente (si tutti, anche quello cui stai pensando e che fa sempre dei danni) abilitato sul vostro portale o blog Plone potrà effettuare questa operazione. Be careful :)

L'altro punto da tenere in considerazione è proprio il fatto che si tratta di un sistema esterno, che non controllo, che ha le sue politiche, che da un giorno all'altro potrebbe variarle.

Ma anche questo è questo il web 2.0, no? Occorre conoscere, pensare e valutare se un tool fa al caso nostro o meno. Una volta fatto, si vive tranquilli.

Oct 06, 2009

Improve the compatibility of collective.plonetruegallery

Filed Under:

I created a branch of the product collective.plonetruegallery, extendible.gallery, which allows to use the features of the package for new types of content and not only for the type "Gallery".

I tried to use the functionality provided by the product collective.plonetruegallery, such as the view "gallery", for my content type, and I had a problem: these features are applicable only to the content type "Gallery". This is because in the code of the package conditions are evaluated on the value portal_type of the content type, eg. if(object.portal_type == 'Gallery').

So I changed some parts of the original product by ensuring that the controls are no longer performed on the value portal_type of the type of content, but on the interfaces implemented by the type of content. In this way the functionalities of collective.plonetruegallery can be applied to any type of content, different from "Gallery", if it implements the interface "IGallery", eg.: IGallery.providedBy (object).

I take advantage using my content type "Cartridge" that extends ATBTreeFolder, which functions as a folder can hold a large number of elements, rather than the type of content "Gallery" which is not a large folder, and then was not suitable for my purposes. But the point is that I could still use the view "gallery" of collective.plonetruegallery to view images from the cartridge, because my type of content implements the interface "IGallery".

I have proposed these changes to the developers of the product and now see if they are interested in applying this modern approach!