Commit b92b19a2 authored by daryl herzmann's avatar daryl herzmann

Merge pull request #578 from guusdk/avatar-resizer

OF-1128: New plugin to resize vCard-based avatars
parents ea76ccd0 46671dfc
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Avatar Resizer Plugin Changelog</title>
<style type="text/css">
BODY {
font-size: 100%;
}
BODY, TD, TH {
font-family: tahoma, verdana, arial, helvetica, sans-serif;
font-size: 0.8em;
}
H2 {
font-size: 10pt;
font-weight: bold;
padding-left: 1em;
}
A:hover {
text-decoration: none;
}
H1 {
font-family: tahoma, arial, helvetica, sans-serif;
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px #ccc solid;
padding-bottom: 2px;
}
TT {
font-family: courier new;
font-weight: bold;
color: #060;
}
PRE {
font-family: courier new;
font-size: 100%;
}
</style>
</head>
<body>
<h1>
Avatar Resizer Plugin Changelog
</h1>
<p><b>1.0.1</b> -- April 14, 2016</p>
<ul>
<li>Added proper documentation.</li>
</ul>
<p><b>1.0.0</b> -- April 12, 2016</p>
<ul>
<li>First stab at porting the original code of Aaron Sierra. Distributed as a proof-of-concept in the <a
href="https://community.igniterealtime.org/message/256783">IgniteRealtime community</a>.
</li>
</ul>
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.igniterealtime.openfire.plugin.avatarresizer.AvatarResizerPlugin</class>
<name>Avatar Resizer</name>
<description>Ensures vCard-based avatars are not to large for comfort.</description>
<author>Guus der Kinderen</author>
<version>1.0.1</version>
<date>4/14/2016</date>
</plugin>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Avatar Resizer Plugin Readme</title>
<style type="text/css">
BODY {
font-size : 100%;
}
BODY, TD, TH {
font-family : tahoma, verdana, arial, helvetica, sans-serif;
font-size : 0.8em;
text-align: left;
}
CAPTION {
font-size: 0.7em;
margin-top: 1.2em;
}
H2 {
font-size : 10pt;
font-weight : bold;
}
A:hover {
text-decoration : none;
}
H1 {
font-family : tahoma, arial, helvetica, sans-serif;
font-size : 1.4em;
font-weight: bold;
border-bottom : 1px #ccc solid;
padding-bottom : 2px;
}
TT {
font-family : courier new;
font-weight : bold;
color : #060;
}
PRE {
font-family : courier new;
font-size : 100%;
}
</style>
</head>
<body>
<h1>
Avatar Resizer Plugin Readme
</h1>
<h2>Overview</h2>
<p>
The avatar resizer plugin will scale down VCard-based avatars, when the corresponding vCard is stored in Openfire
(it will not affect avatars that are being transferred between end-users).
</p>
<p>
Using avatars that are large can introduce problems for clients - not only will all of the data need to be
transferred (which is done with inefficient base64-encoding), some clients have trouble displaying large sets. RAM
and CPU spikes can occur as a result.
</p>
<p>
To prevent these problems, this plugin scales Avatars from vCards, but only if the vCards are provided by Openfire
itself (through one of its <a href="https://www.igniterealtime.org/builds/openfire/docs/latest/documentation/javadoc/org/jivesoftware/openfire/vcard/VCardProvider.html">VCardProvider</a>
implementations, which include the default, database-oriented provider, an LDAP provider and a Atlassian Crowd
provider).
</p>
<p>
Avatar scaling of this plugin is in accordance with the guidelines listed in
<a href="https://www.xmpp.org/extensions/xep-0153.html#bizrules-image">XEP&#8209;0153: vCard-Based Avatars</a>.
Specifically, this plugin shrinks and crops the avatar to a 96 by 96 pixel square image (although the size is
configurable, see below).
</p>
<h2>Installation</h2>
<p>
Copy avatarResizer.jar into the plugins directory of your Openfire installation. The plugin will then be
automatically deployed. To upgrade to a new version, copy the new avatarResizer.jar file over the existing file.
</p>
<h2>Configuration</h2>
<p>
The plugin does not need configuration. Simply installing the plugin will enable it. The properties defined here can
be set to override default behavior of the plugin.
</p>
<table>
<caption>Openfire properties for the Avatar Resizer plugin</caption>
<thead>
<tr><th>Property name</th><th>Default value</th><th>Description</th></tr>
</thead>
<tbody>
<tr>
<td><tt>avatar.resize.targetdimension</tt></td>
<td><pre>96</pre></td>
<td>Size (in pixels) of the resized avatar.</td>
</tr>
</tbody>
</table>
<h2>Credits</h2>
<p>
This plugin is based on an implementation by Aaron Sierra, which was contributed to IgniteRealtime through Mike Ray.
</p>
<p>
Icons made by <a href="http://www.flaticon.com/authors/stephen-hutchings" title="Stephen Hutchings">Stephen Hutchings</a>
from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a>, licensed by
<a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a>.
</p>
</body>
</html>
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