Re: [Lift committers] Re: Image resize code

45 views
Skip to first unread message

Ross Mellgren

unread,
Jan 15, 2010, 10:03:25 AM1/15/10
to liftweb
According to Jon on the call, he said he was putting together just such a lift module (for image stuff), so I figured I'd toss this over in case it was of use to him in that module.

If anyone else wants to use it independently, consider it a contribution to lift, and licensed the same way.

-Ross

On Jan 15, 2010, at 9:51 AM, Peter Robinett wrote:

> Ross, this looks nice. Imagine resizing code is something that I've
> personally had to do many times and is always annoying, so perhaps
> this would make a good Lift module? Anyway, this is probably best
> discussed on the main list...
>
> Peter
>
> On Jan 14, 3:01 am, Ross Mellgren <dri...@gmail.com> wrote:
>> Oh I nearly forgot I said on the conference call the other day that I'd send the image resize code we built at work, in case it would be helpful. Here it is:
>>
>> import java.awt.{Graphics, RenderingHints, Transparency}
>> import java.awt.geom.AffineTransform
>> import java.awt.image.{AffineTransformOp, BufferedImage, ColorModel, IndexColorModel}
>>
>> /**
>> * Helpers for manipulating images
>> */
>> object ImageHelpers
>> {
>> // Some code here omitted -- Ed.
>>
>> /** Rendering hints set up for the highest quality rendering */
>> val highQualityHints = {
>> val h = new RenderingHints(null)
>> h.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)
>> h.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY)
>> h.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
>> h.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
>> h.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
>> h
>> }
>>
>> // Some code here omitted -- Ed.
>>
>> /**
>> * Resize an image of the given source type by the given ratios, properly handling GIF transparency, giving back the resized
>> * image and the new image format type that should be used.
>> *
>> * The image type might change if the input type is an indexed color model, because it is a hard problem to choose an optimized
>> * palette, and currently we don't. This function will return "png" as the new type in this case.
>> *
>> * If the input image is not using an indexed color model with transparency, then the target format and color model will be
>> * identical to the source.
>> */
>> def resize(source: BufferedImage, inputFormat: String, dx: Double, dy: Double): (BufferedImage, String) = {
>> var sourceColorModel = source.getColorModel
>> val targetColorModel = source.getColorModel
>> val standardColorModel = ColorModel.getRGBdefault
>>
>> val (targetWidth, targetHeight) = (((source.getWidth: Double) * dx).asInstanceOf[Int], ((source.getHeight: Double) * dy).asInstanceOf[Int])
>>
>> def resize(src: BufferedImage, dst: BufferedImage) {
>> val g = dst.createGraphics
>> try {
>> g.setRenderingHints(highQualityHints)
>> g.drawImage(src, new AffineTransformOp(AffineTransform.getScaleInstance(dx, dy), AffineTransformOp.TYPE_BICUBIC), 0, 0)
>> } finally {
>> g.dispose
>> }
>> }
>>
>> // GIF support in Java is very ornery. For GIFs we have to manually do the masking on input, and then just punt on outputting GIFs and instead output PNGs.
>> if (sourceColorModel.isInstanceOf[IndexColorModel] &&
>> sourceColorModel.hasAlpha &&
>> sourceColorModel.getTransparency == Transparency.BITMASK &&
>> sourceColorModel.asInstanceOf[IndexColorModel].getTransparentPixel >= 0) {
>>
>> val indexColorModel = sourceColorModel.asInstanceOf[IndexColorModel]
>> val transparent = indexColorModel.getRGB(indexColorModel.getTransparentPixel)
>>
>> val masked = new BufferedImage(standardColorModel, standardColorModel.createCompatibleWritableRaster(source.getWidth, source.getHeight), standardColorModel.isAlphaPremultiplied, null)
>> var w = masked.getWidth
>> var h = masked.getHeight
>>
>> val buf = new Array[Int](w)
>>
>> var y = 0
>> while (y < h) {
>> source.getRGB(0, y, w, 1, buf, 0, 1)
>>
>> var x = 0
>> while (x < w) {
>> val c = buf(x)
>> if (c == transparent) {
>> buf(x) = 0
>> }
>> x += 1
>> }
>>
>> masked.setRGB(0, y, w, 1, buf, 0, 1)
>> y += 1
>> }
>>
>> val resized = new BufferedImage(standardColorModel, standardColorModel.createCompatibleWritableRaster(targetWidth, targetHeight), standardColorModel.isAlphaPremultiplied, null)
>> resize(masked, resized)
>> (resized, "png")
>> } else if (sourceColorModel.isInstanceOf[IndexColorModel]) {
>> // The input color model is indexed, and we know we won't be able to generate a tolerable palette to make another indexed color model, so use sRGB and upgrade to PNG.
>> val resized = new BufferedImage(standardColorModel, standardColorModel.createCompatibleWritableRaster(targetWidth, targetHeight), standardColorModel.isAlphaPremultiplied, null)
>> resize(source, resized)
>> (resized, "png")
>> } else {
>> val resized = new BufferedImage(targetColorModel, targetColorModel.createCompatibleWritableRaster(targetWidth, targetHeight), targetColorModel.isAlphaPremultiplied, null)
>> resize(source, resized)
>> (resized, inputFormat)
>> }
>> }
>>
>> }
>>
>> It isn't perhaps the ideal implementation, as mentioned in some of the comments, but perhaps Jon or others might find it useful in whole or part.
>>
>> -Ross
> --
> You received this message because you are subscribed to the Google Groups "Lift-committers" group.
> To post to this group, send email to lift-co...@googlegroups.com.
> To unsubscribe from this group, send email to lift-committe...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/lift-committers?hl=en.
>
>

Timothy Perrett

unread,
Jan 15, 2010, 10:17:56 AM1/15/10
to lif...@googlegroups.com
Cool stuff Ross, whats the overhead like in terms of memory etc?

I might have a bit of time to put this into a module and stuff it on review board.

Cheers, Tim

> You received this message because you are subscribed to the Google Groups "Lift" group.
> To post to this group, send email to lif...@googlegroups.com.
> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.
>
>

Ross Mellgren

unread,
Jan 15, 2010, 10:33:37 AM1/15/10
to lif...@googlegroups.com
Well, it has to keep the whole source (packed) in memory and the target (unpacked / 32 bit RGBA) in memory, so I would assume that as long as AffineTransformOp is not doing something untoward it's probably around (src width * src height * src bytes/pixel) + (dest width * dest height * 4 bytes) plus a little bit extra.

I'm not a Java2D guru or anything, so I'm not sure how that could be improved offhand.

If you want to wrap it up and put it in, that'd be awesome!

-Ross

Jonathan Hoffman

unread,
Jan 15, 2010, 3:47:05 PM1/15/10
to lif...@googlegroups.com
Hi Ross,

That looks good. It seems you're doing a much better job of handling
gif than I would have. Maybe we should move this discussion to the
main list to get everyone's input on what features would be useful.

Tim, If you've already created a branch, let me know and I'll get my
stuff in there too-- My resize code uses the sanselan project to read
exif orientation information.

Ross Mellgren

unread,
Jan 15, 2010, 3:52:19 PM1/15/10
to lif...@googlegroups.com
This is already on the main list, I moved it there after Peter mentioned it. Sorry, I have a bad habit of starting conversations on lift-committers I'm trying to break before David breaks it for me ;-)

GIF resizing was a huge sticking point for the project here at Paytronix that this code was originally written for. I'd prefer it if all resize transforms could resize into the same format as the source (right now it's GIF -> PNG, and most everything else is source type -> dest type), but:
- Shrunk GIFs are ugly due to the limited color palette and single-bit transparency.
- Calculating an ideal palette and doing any dithering is not trivial, and for the application I had it was fine to change the format.

-Ross

Timothy Perrett

unread,
Jan 15, 2010, 5:50:34 PM1/15/10
to Lift
I've added the code here:

http://github.com/dpp/liftweb/tree/wip_tim_285

Cheers, Tim

> >>>>> For more options, visit this group athttp://groups.google.com/group/lift-committers?hl=en.


>
> >>>> --
> >>>> You received this message because you are subscribed to the Google Groups "Lift" group.
> >>>> To post to this group, send email to lif...@googlegroups.com.
> >>>> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.

> >>>> For more options, visit this group athttp://groups.google.com/group/liftweb?hl=en.


>
> >>> --
> >>> You received this message because you are subscribed to the Google Groups "Lift" group.
> >>> To post to this group, send email to lif...@googlegroups.com.
> >>> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.

> >>> For more options, visit this group athttp://groups.google.com/group/liftweb?hl=en.


>
> >> --
> >> You received this message because you are subscribed to the Google Groups "Lift" group.
> >> To post to this group, send email to lif...@googlegroups.com.
> >> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.

> >> For more options, visit this group athttp://groups.google.com/group/liftweb?hl=en.


>
> > --
> > You received this message because you are subscribed to the
>

> ...
>
> read more »

Naftoli Gugenheim

unread,
Jan 19, 2010, 4:09:21 PM1/19/10
to lif...@googlegroups.com
Does JAI need everything in RAM?
Also, how do you deal with DPI?

-------------------------------------
Ross Mellgren<dri...@gmail.com> wrote:

-Ross

>>> For more options, visit this group at http://groups.google.com/group/lift-committers?hl=en.


>>>
>>>
>>
>> --
>> You received this message because you are subscribed to the Google Groups "Lift" group.
>> To post to this group, send email to lif...@googlegroups.com.
>> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.

>> For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.


>>
>>
>
> --
> You received this message because you are subscribed to the Google Groups "Lift" group.
> To post to this group, send email to lif...@googlegroups.com.
> To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.

> For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.

Naftoli Gugenheim

unread,
Jan 19, 2010, 4:11:28 PM1/19/10
to lif...@googlegroups.com
I think both Java2D and JAI have API for tiles. Do you know if that has anything to do with loading only part at a time, or what they're for?

Ross Mellgren

unread,
Jan 19, 2010, 4:11:58 PM1/19/10
to lif...@googlegroups.com
I'm not sure, it might depend on a variety of factors. I know Java ImageIO can use disk-backed caching, but that's just for manufacturing the BufferedImage and so I don't know if it's disk-backed-ness carries over into the BufferedImage it produces.

I don't handle DPI -- I create a BufferedImage of mostly the same properties and do a pixel-pixel rescale. I suspect the DPI would only come into play when you need to write it to disk with an image format that supports DPI, however I surely don't know.

-Ross

Strom

unread,
Jan 28, 2010, 2:25:58 AM1/28/10
to Lift
Where exactly is the code? I don't see anything about image resizing
here.

Thanks,
Strom

On Jan 15, 2:50 pm, Timothy Perrett <timo...@getintheloop.eu> wrote:
> I've added the code here:
>
> http://github.com/dpp/liftweb/tree/wip_tim_285
>
> Cheers, Tim
>
> On Jan 15, 8:52 pm, Ross Mellgren <dri...@gmail.com> wrote:
>
> > This is already on the main list, I moved it there after Peter mentioned it. Sorry, I have a bad habit of starting conversations on lift-committers I'm trying to break before David breaks it for me ;-)
>

> > GIF resizing was a huge sticking point for the project here at Paytronix that this code was originally written for. I'd prefer it if allresizetransforms couldresizeinto the same format as the source (right now it's GIF -> PNG, and most everything else is source type -> dest type), but:


> >   - Shrunk GIFs are ugly due to the limited color palette and single-bit transparency.
> >   - Calculating an ideal palette and doing any dithering is not trivial, and for the application I had it was fine to change the format.
>
> > -Ross
>
> > On Jan 15, 2010, at 3:47 PM, Jonathan Hoffman wrote:
>
> > > Hi Ross,
>
> > > That looks good.  It seems you're doing a much better job of handling
> > > gif than I would have.  Maybe we should move this discussion to the
> > > main list to get everyone's input on what features would be useful.
>
> > > Tim, If you've already created a branch, let me know and I'll get my

> > > stuff in there too-- Myresizecode uses the sanselan project to read


> > > exif orientation information.
>
> > > On Fri, Jan 15, 2010 at 10:33 AM, Ross Mellgren <dri...@gmail.com> wrote:
> > >> Well, it has to keep the whole source (packed) in memory and the target (unpacked / 32 bit RGBA) in memory, so I would assume that as long as AffineTransformOp is not doing something untoward it's probably around (src width * src height * src bytes/pixel) + (dest width * dest height * 4 bytes) plus a little bit extra.
>
> > >> I'm not a Java2D guru or anything, so I'm not sure how that could be improved offhand.
>
> > >> If you want to wrap it up and put it in, that'd be awesome!
>
> > >> -Ross
>
> > >> On Jan 15, 2010, at 10:17 AM, Timothy Perrett wrote:
>
> > >>> Cool stuff Ross, whats the overhead like in terms of memory etc?
>
> > >>> I might have a bit of time to put this into a module and stuff it on review board.
>
> > >>> Cheers, Tim
>
> > >>> On 15 Jan 2010, at 15:03, Ross Mellgren wrote:
>

> > >>>> According to Jon on the call, he said he was putting together just such a lift module (forimagestuff), so I figured I'd toss this over in case it was of use to him in that module.


>
> > >>>> If anyone else wants to use it independently, consider it a contribution to lift, and licensed the same way.
>
> > >>>> -Ross
>
> > >>>> On Jan 15, 2010, at 9:51 AM, Peter Robinett wrote:
>
> > >>>>> Ross, this looks nice. Imagine resizing code is something that I've
> > >>>>> personally had to do many times and is always annoying, so perhaps
> > >>>>> this would make a good Lift module? Anyway, this is probably best
> > >>>>> discussed on the main list...
>
> > >>>>> Peter
>
> > >>>>> On Jan 14, 3:01 am, Ross Mellgren <dri...@gmail.com> wrote:

> > >>>>>> Oh I nearly forgot I said on the conference call the other day that I'd send theimageresizecode we built at work, in case it would be helpful. Here it is:


>
> > >>>>>> import java.awt.{Graphics, RenderingHints, Transparency}
> > >>>>>> import java.awt.geom.AffineTransform
> > >>>>>> import java.awt.image.{AffineTransformOp, BufferedImage, ColorModel, IndexColorModel}
>
> > >>>>>> /**
> > >>>>>> * Helpers for manipulating images
> > >>>>>> */
> > >>>>>> object ImageHelpers
> > >>>>>> {
> > >>>>>> // Some code here omitted -- Ed.
>
> > >>>>>>   /** Rendering hints set up for the highest quality rendering */
> > >>>>>>   val highQualityHints = {
> > >>>>>>       val h = new RenderingHints(null)
> > >>>>>>       h.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY)
> > >>>>>>       h.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY)
> > >>>>>>       h.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
> > >>>>>>       h.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
> > >>>>>>       h.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
> > >>>>>>       h
> > >>>>>>   }
>
> > >>>>>> // Some code here omitted -- Ed.
>
> > >>>>>>   /**

> > >>>>>>    *Resizeanimageof the given source type by the given ratios, properly handling GIF transparency, giving back the resized
> > >>>>>>    *imageand the newimageformat type that should be used.
> > >>>>>>    *
> > >>>>>>    * Theimagetype might change if the input type is an indexed color model, because it is a hard problem to choose an optimized


> > >>>>>>    * palette, and currently we don't. This function will return "png" as the new type in this case.
> > >>>>>>    *

> > >>>>>>    * If the inputimageis not using an indexed color model with transparency, then the target format and color model will be


> > >>>>>>    * identical to the source.
> > >>>>>>    */

> > >>>>>>   defresize(source: BufferedImage, inputFormat: String, dx: Double, dy: Double): (BufferedImage, String) = {


> > >>>>>>       var sourceColorModel = source.getColorModel
> > >>>>>>       val targetColorModel = source.getColorModel
> > >>>>>>       val standardColorModel = ColorModel.getRGBdefault
>
> > >>>>>>       val (targetWidth, targetHeight) = (((source.getWidth: Double) * dx).asInstanceOf[Int], ((source.getHeight: Double) * dy).asInstanceOf[Int])
>

> > >>>>>>       defresize(src: BufferedImage, dst: BufferedImage) {

> ...
>
> read more »

Jeppe Nejsum Madsen

unread,
Jan 28, 2010, 6:38:16 AM1/28/10
to lif...@googlegroups.com
Strom <strom...@gmail.com> writes:

> Where exactly is the code? I don't see anything about image resizing
> here.

Try here

http://github.com/dpp/liftweb/tree/master/framework/lift-modules/lift-imaging/


/Jeppe

Jonathan Hoffman

unread,
Jan 28, 2010, 11:46:32 AM1/28/10
to lif...@googlegroups.com
I'm also working on adding some additional functionality and merging it with the function in ImageHelpers.

Let me know if there's anything you want to see.

Here's what I have so far: http://github.com/dpp/liftweb/commit/63614a64a053c619c4330a8bad364677af49e8a2

Strom

unread,
Jan 28, 2010, 1:26:54 PM1/28/10
to Lift
Thanks for pointing out the location. ImageHelpers is a good idea. As
far as want to see, is there any way to effectively group this helper
with Image serving and uploading and make them have some sort of
caching? That would be really useful to be able to have a decently
working image cache with multiple sizes. Don't know what is possible
right now because I've only quickly glanced at the ImageHelper and
resizer code.

Strom

On Jan 28, 8:46 am, Jonathan Hoffman <jonhoff...@gmail.com> wrote:
> I'm also working on adding some additional functionality and merging it with the function in ImageHelpers.
>
> Let me know if there's anything you want to see.
>

> Here's what I have so far:http://github.com/dpp/liftweb/commit/63614a64a053c619c4330a8bad364677...


>
> On Jan 28, 2010, at 6:38 AM, Jeppe Nejsum Madsen wrote:
>

> > Strom <strommo...@gmail.com> writes:
>
> >> Where exactly is the code? I don't see anything about image resizing
> >> here.
>
> > Try here
>

> >http://github.com/dpp/liftweb/tree/master/framework/lift-modules/lift...

Jonathan Hoffman

unread,
Jan 28, 2010, 2:26:08 PM1/28/10
to lif...@googlegroups.com
By caching, I assume that you mean persistent storage? You could store onto the file system after resizing.

I was snooping around Ross's github projects and noticed this example for serving images back from the filesystem: http://github.com/Dridus/test-image/blob/master/src/main/scala/test/ImageLogic.scala

If you need an example of file uploading take a look a the source to demo.liftweb.net: http://github.com/dpp/liftweb/tree/master/examples/example


- Jon

Strom

unread,
Jan 28, 2010, 3:25:34 PM1/28/10
to Lift
By caching I mean like EHCache, where you serve an image from the
cache (some folder on the file system) instead of retrieving the blob
from the db to reduce db fetching. So I guess it would be stored in
the file system :)

I might just be tempted to implement EHCache anyways, but I'll take a
gander at Ross' code.

Thanks.
Strom

On Jan 28, 11:26 am, Jonathan Hoffman <jonhoff...@gmail.com> wrote:
> By caching, I assume that you mean persistent storage?  You could store onto the file system after resizing.
>

> I was snooping around Ross's github projects and noticed this example for serving images back from the filesystem:http://github.com/Dridus/test-image/blob/master/src/main/scala/test/I...

Timothy Perrett

unread,
Jan 31, 2010, 6:36:47 AM1/31/10
to lif...@googlegroups.com
IMO, these are separate concerns. lift-imaging is purely for image manipulation and caching etc does not belong there.

Cheers, Tim

Reply all
Reply to author
Forward
0 new messages