Commit d40354ca authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1128: New plugin: avatar resizer

This commit adds a prototype plugin, based on code provided in the
Ignite Realtime community: https://community.igniterealtime.org/thread/58477
parent 279a998f
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.igniterealtime.openfire.plugin.avatarresizer.AvatarResizerPlugin</class>
<name>Avatar Resizer</name>
<description>Resizes Avatars obtained from the VCardManager</description>
<author>Guus der Kinderen</author>
<version>1.0.0</version>
<date>4/12/2016</date>
</plugin>
package org.igniterealtime.openfire.plugin.avatarresizer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.vcard.VCardManager;
import org.jivesoftware.openfire.vcard.VCardProvider;
import org.jivesoftware.util.JiveGlobals;
import java.io.File;
/**
* A plugin that intercepts avatars in vCards retrieved from the vCardManager, and re-sizes them when appropriate.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class AvatarResizerPlugin implements Plugin
{
@Override
public void initializePlugin( PluginManager manager, File pluginDirectory )
{
final VCardProvider provider = VCardManager.getProvider();
if ( provider != null && !( provider instanceof DelegateVCardProvider ) )
{
// Setting the property will cause the VCardProvider to re-initialize.
JiveGlobals.setProperty( "provider.vcard.className", DelegateVCardProvider.class.getCanonicalName() );
final DelegateVCardProvider delegateVCardProvider = (DelegateVCardProvider) VCardManager.getProvider();
delegateVCardProvider.setDelegate( provider );
}
}
@Override
public void destroyPlugin()
{
final VCardProvider provider = VCardManager.getProvider();
if ( provider != null && provider instanceof DelegateVCardProvider )
{
final DelegateVCardProvider delegateVCardProvider = (DelegateVCardProvider) provider;
final VCardProvider originalProvider = delegateVCardProvider.getDelegate();
JiveGlobals.setProperty( "provider.vcard.className", originalProvider.getClass().getCanonicalName() );
}
}
}
package org.igniterealtime.openfire.plugin.avatarresizer;
import org.dom4j.Element;
import org.jivesoftware.openfire.vcard.VCardProvider;
import org.jivesoftware.util.AlreadyExistsException;
import org.jivesoftware.util.NotFoundException;
/**
* A vCard Provider that delegates to another provider, applying image resizing on the results from the delegate.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class DelegateVCardProvider implements VCardProvider
{
private VCardProvider delegate;
public DelegateVCardProvider()
{
}
public VCardProvider getDelegate()
{
return delegate;
}
public void setDelegate( VCardProvider delegate )
{
if ( delegate == null )
{
throw new IllegalArgumentException( "Argument 'delegate' cannot be null." );
}
if ( delegate instanceof DelegateVCardProvider )
{
throw new IllegalArgumentException( "Argument 'delegate' cannot be an instance of DelegateVCardProvider." );
}
this.delegate = delegate;
}
@Override
public Element loadVCard( String username )
{
final Element element = delegate.loadVCard( username );
Resizer.resizeAvatar( element );
return element;
}
@Override
public Element createVCard( String username, Element vCardElement ) throws AlreadyExistsException
{
final Element element = delegate.createVCard( username, vCardElement );
Resizer.resizeAvatar( element );
return element;
}
@Override
public Element updateVCard( String username, Element vCardElement ) throws NotFoundException
{
final Element element = delegate.updateVCard( username, vCardElement );
Resizer.resizeAvatar( element );
return element;
}
@Override
public void deleteVCard( String username )
{
delegate.deleteVCard( username );
}
@Override
public boolean isReadOnly()
{
return delegate.isReadOnly();
}
}
package org.igniterealtime.openfire.plugin.avatarresizer;
import org.dom4j.Element;
import org.jivesoftware.util.Base64;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
/**
* Image resizing utility methods.
*/
public class Resizer
{
private static final Logger Log = LoggerFactory.getLogger( Resizer.class );
public static void resizeAvatar( final Element vCardElement )
{
if ( vCardElement == null )
{
return;
}
// XPath didn't work?
if ( vCardElement.element( "PHOTO" ) == null )
{
return;
}
if ( vCardElement.element( "PHOTO" ).element( "BINVAL" ) == null || vCardElement.element( "PHOTO" ).element( "TYPE" ) == null )
{
return;
}
final Element element = vCardElement.element( "PHOTO" ).element( "BINVAL" );
if ( element.getTextTrim() == null || element.getTextTrim().isEmpty() )
{
return;
}
// Get a writer (check if we can generate a new image for the type of the original).
final String type = vCardElement.element( "PHOTO" ).element( "TYPE" ).getTextTrim();
final Iterator it = ImageIO.getImageWritersByMIMEType( type );
if ( !it.hasNext() )
{
Log.debug( "Cannot resize avatar. No writers available for MIME type {}.", type );
return;
}
final ImageWriter iw = (ImageWriter) it.next();
// Extract the original avatar from the VCard.
final byte[] original = Base64.decode( element.getTextTrim() );
// Crop and shrink, if needed.
final int targetDimension = JiveGlobals.getIntProperty( "avatar.resize.targetdimension", 96 );
final byte[] resized = cropAndShrink( original, targetDimension, iw );
// If a resized image was created, replace to original avatar in the VCard.
if ( resized != null )
{
Log.debug( "Replacing original avatar in vcard with a resized variant." );
vCardElement.element( "PHOTO" ).element( "BINVAL" ).setText( Base64.encodeBytes( resized ) );
}
}
public static byte[] cropAndShrink( final byte[] bytes, final int targetDimension, final ImageWriter iw )
{
Log.debug( "Original image size: {} bytes.", bytes.length );
BufferedImage avatar;
try ( final ByteArrayInputStream stream = new ByteArrayInputStream( bytes ) )
{
avatar = ImageIO.read( stream );
if ( avatar.getWidth() <= targetDimension && avatar.getHeight() <= targetDimension )
{
Log.debug( "Original image dimension ({}x{}) is within acceptable bounds ({}x{}). No need to resize.", avatar.getWidth(), avatar.getHeight(), targetDimension, targetDimension );
return null;
}
}
catch ( IOException | RuntimeException ex )
{
Log.warn( "Failed to resize avatar. An unexpected exception occurred while reading the original image.", ex );
return null;
}
/* We're going to be resizing, let's crop the image so that it's square and figure out the new starting size. */
Log.debug( "Original image is " + avatar.getWidth() + "x" + avatar.getHeight() + " pixels" );
final int targetWidth, targetHeight;
if ( avatar.getHeight() == avatar.getWidth() )
{
Log.debug( "Original image is already square ({}x{})", avatar.getWidth(), avatar.getHeight() );
targetWidth = targetHeight = avatar.getWidth();
}
else
{
final int x, y;
if ( avatar.getHeight() > avatar.getWidth() )
{
Log.debug( "Original image is taller ({}) than wide ({}).", avatar.getHeight(), avatar.getWidth() );
x = 0;
y = ( avatar.getHeight() - avatar.getWidth() ) / 2;
targetWidth = targetHeight = avatar.getWidth();
}
else
{
Log.debug( "Original image is wider ({}) than tall ({}).", avatar.getWidth(), avatar.getHeight() );
x = ( avatar.getWidth() - avatar.getHeight() ) / 2;
y = 0;
targetWidth = targetHeight = avatar.getHeight();
}
// pull out a square image, centered.
avatar = avatar.getSubimage( x, y, targetWidth, targetHeight );
}
/* Let's crop/scale the image as necessary out the new dimensions. */
final BufferedImage resizedAvatar = new BufferedImage( targetDimension, targetDimension, avatar.getType() );
final AffineTransform scale = AffineTransform.getScaleInstance( (double) targetDimension / (double) targetWidth, (double) targetDimension / (double) targetHeight );
final Graphics2D g = resizedAvatar.createGraphics();
g.drawRenderedImage( avatar, scale );
Log.debug( "Resized image is {}x{}.", resizedAvatar.getWidth(), resizedAvatar.getHeight() );
/* Now we have to dump the new jpeg, png, etc. to a byte array */
try ( final ByteArrayOutputStream bostream = new ByteArrayOutputStream();
final ImageOutputStream iostream = new MemoryCacheImageOutputStream( bostream ) )
{
iw.setOutput( iostream );
iw.write( resizedAvatar );
final byte[] data = bostream.toByteArray();
Log.debug( "Resized image size: {} bytes.", data.length );
return data;
}
catch ( IOException | RuntimeException ex )
{
Log.warn( "Failed to resize avatar. An unexpected exception occurred while writing the resized image.", ex );
return null;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment