I noticed that the rounded corners generator shipped with imagecache_actions can become very slow with bigger images.

I replaced the algorithm with a pure-geometry per-pixel solution looking only at the pixels that actually matter for rounded corners.

Here's the patch. It works fast and accurate for me.
It has effective 10x antialiasing - total overkill you will say, but it's cheap :)
Hey wait, you could even see this as more than 10x, depending how you count.. whatever, it's enough.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

donquixote’s picture

I should not have removed the canarymason tribute comment. I thought his work was the algorithm (which has been replaced), but now i see it was the idea of having independent corners. So, here's the comment back!

dman’s picture

Status: Active » Reviewed & tested by the community

That looks like great code!
I've been tweaking it a bit and rolling it in. Seems to work great.
The original method was indeed inefficient - although part of the reason is that conceptually I wanted to be able to support arbitrary 'masking' of any shape or opacity. That's what the code sets up for.

Rounded corners are the most common (only) use of that so far, and certainly can do with their own special case. The algorithm maths looks great, and yes, it seems quick.

Good comments also - very approachable.

I'm making a few internal changes, just to pick up on some things of mine.

- I want to allow the 'independant corners' array to be undefined if unused. The patch depends on the form elements being available. I'm changing it so the corner indices are always known in advance and will default to a single radius if unset. This means we can invoke these actions from code elsewhere (I run a testsuite) and not have to worry about filling in a redundant array.

- I *do* like strings as keys in the code, not boring numbers :-p. Makes it readable. It's a tight loop, I know, but I don't see a performance difference between integers and strings - especially as you are taking time to re-label the keys anyway.
So I put the semantic labels back in. Also cleaned up the overloaded $color variable (sometimes an index, sometimes an array)

- I am NOT going to mess with _canvasactions_roundedcorners_pixel_opacity()! Thanks.

... At first I was going to try placing the two algorithms side-by-side, to allow the user to choose through the UI. But seeing as the results are indistinguishable ... It's not worth it. I'll retire the old one do documentation. At some point I still want to work in a 'mask' operation, and this is the only proof-of-concept for that.

I'm running tests, but will commit very soon.
I recently committed a bunch of stuff to -dev (mostly docs and text rendering) so sorry if that puts you out of synch a little.

dman’s picture

I'll also remove the antialias option from the UI... No longer a problem!
... unless we are rounding gifs? Meh, no.

dman’s picture

... running it through coder.module code style review I need to tweak the maths a little.
$dx+=.1
needs to become:
$dx+=0.1
etc. Silly coder.module!
I'm doing a full code style review of all my old stuff, so the next commit is going to be a biggie.

donquixote’s picture

Rounding gifs?
What about an additional imagecache action that can turn pngs into gifs, while keeping the transparency?
Possible options:
- some options for how the palette colors are chosen (similar to common graphics programs).
- optional color dithering (usually I hate it, but sometimes it might be a good thing)
- optional transparency dithering (usually I hate it, but whatever)
- transparent palette color applied to pixels with alpha > 63

Maybe there could even be an action that chooses the best possible image format for you.

--------

Boring string keys:
I had been working on some home-grown algorithms using numeric keys, before I stumbled into this one function that returns string keys. At that moment it felt more like an annoyance.
One good thing about numeric keys is that they tell me in which sequence the color arguments go into imagecolorallocatealpha(). And I make fewer typos.
Anyway, use your string keys :)

--------

Maybe this solution could even be ported to imagemagick. The only things the library has to do is reading and writing pixel colors. The _canvasactions_roundedcorners_pixel_opacity does nothing with the image library, it's plain math. I have no experience with imagemagick, so I can't help in that.

dman’s picture

OK. A modified version of this cool code is now in -dev

This commit contains a hundred whitespace changes all over the place to. Bloody coder.module.
Anyone on CVS today is best to replace from the repo, because a merge just ain't going to work!

donquixote’s picture

"Arbitrary masking" is a good idea, but you should consider that a lot of possible masks are sparse (= only affect a few pixels).

Possible solutions:
- positioning a small mask (brush) in different places along the image.
- using a sparse PHP array $pixels[$x][$y], where only few pixels actually have a value.

What are the use cases you are thinking of?

dman’s picture

If I was supporting gifs, I'd have to go with - transparent palette color applied to pixels with alpha > 63
... but I just haven't seen the need to do anything with gifs for ages. We are now solid with PNGs, so let's keep it that way.
I was just thinking of it as one of those transparency gotchas. Not something I need to solve today.

Re string keys, My thoughts are here. The UI uses string keys, PHP GD toolkit uses string keys. People reading the code don't have to guess what $color[3] means when using string keys. ... I like string keys.

I have no clue about imagemagick either. I imagine it's got its own special option to do just this!

donquixote’s picture

I also had the idea that your canvasactions_mask has a high memory consumption on big images. I'm not sure, maybe you need to use imagecolordeallocate to free the memory? Just a guess.

dman’s picture

The mask was entirely inefficient. I was over-generalizing the code down to step out what I was actually doing.
"If this pixel exists with an alpha value in the mask, apply that value to the source image"
... optimization comes later, once I get the big picture down :-)

But yeah, most 'mask' cases should probably be treated in the opposite way - as cut-outs of small regions. I really didn't have any use-cases in mind, I was just over-generalizing (as I usually do in code) - what is the general case I'm solving here? A custom outline shape + an image = a shaped image.

I've found that other effects I've wanted to do - fancy corners, a gradient fade-off - have been easy enough using image overlays rather than masks, so not needed to take it further.

donquixote’s picture

Just for fun (or addiction), I made a new patch which should be even faster and more accurate :)
(this depends, if sin and cos and asin are not significantly slower than sqrt)

It's for PHP5 only, due to some OOP with protected object vars.

I don't have any experience with PHP4, so I'm not sure how to make this fully PHP4 compatible. I could replace the "protected" with "var", but that would be a shame, really.

dman’s picture

OMG. That's entirely different.
Um, if you want that to be available, can you try providing it as a pluggable alternative to the 'roundedcorners' action? Y'know, 'roundedcorners2' or something? I can't merge this one as well, but don't mind it as a stand-alone version.

You can even hijack the settings form with a stub, to keep the code down:

function canvasactions_roundedcorners2_form($action) {
  return canvasactions_roundedcorners_form($action);
}

But I can't re-merge this version.. Esp. if there's a php version issue. Not that I care about PHP4 support, but we don't need to break something that already works.
And against the current DRUPAL-6--1 dev CVS? - Like I said, there's a heap of updates gone in now, triggered by the previous contribution.

donquixote’s picture

Honestly, I think the previous solution is totally fine.
I just couldn't resist to post the new solution, due to ego issues, you know..

dman’s picture

Status: Reviewed & tested by the community » Fixed

I know, it's fun to try out new things when you are on a roll.
If you want to, see if you can think of a good solution for dropshadows :-)
That's next on the wishlist!

My requirements are that it be alpha-transparency-supporting and with a variable amount of blur.
I haven't taken it on myself yet, partly because I ALSO want it to accurately support alpha-transparency in the source image - like behind a cut-out PNG logo, or rounded corners. (This is why I'm thinking in terms of 'masks')

My way of doing that would be incredibly inefficient, so if you've got inspiration, I'm interested.

Marking the previous version as fixed - it's in -dev

donquixote’s picture

Can you show some examples?
Do you want it to be a strictly one-directional shadow, or more like a glow or halo?

dman’s picture

FileSize
122.1 KB

Inspiration:

Currently I can do B (hard corners, no alpha) using existing tools (define canvas)
I'd like to see us do C (user-defined blur, color, offset and alpha)
But I'd not done any code because I was looking too far ahead to D & E - which needed to take shape of the source into account before adding the shadow.

(These mockups produced with a few clicks in Fireworks)
Our sample image from imageAPI : http://drupal.org/node/371374

donquixote’s picture

For performance reasons I would suggest to maintain separate algorithms for C, D and E. For E we need to scan the full picture, for C we only operate on the border, and for D we can do a combined rounded corners + shadow solution, which means we don't need to scan the entire image.

C:
- Create an image that is a bit bigger, to give space for the drop shadow.
- Copy the original image into the top left.
- Paint a big part of the shadow by drawing parallel lines
- draw pixels for the rest.

D:
- Create an image that is a bit bigger, to give space for the drop shadow.
- Copy the original image into the top left.
- Paint the most part of the shadow using parallel lines
- use a combined rounded corners + shadow algorithm

E:
- Create an image that is a bit bigger, to give space for the drop shadow.
- Paint it all black (or grey, if you like) and fully transparent (filledrectangle).
- Copy the alpha channel into the new image, shifted by +x / +y. Maybe we need to do this per pixel, i don't know.
- If we had to copy all of the image (not just alpha), we set the color values back to black (or grey) now.
- Run blur on the new image's alpha channel (if that is possible).
- Reduce the overall opacity: $alpha = 127 - floor((127-$alpha) * 0.5); for every pixel, to make the shadow more transparent.
- Copy the old image into the new one, where it belongs (usually top left). Alpha values need to be correctly merged! (see below)

D+E:
- run the normal rounded corners alg. (not D!)
- run D.
(no need to provide this as a unified action)

---------

How to merge colors with alpha values?
(I hope this is correct now)

<?php

foreach (array('red', 'green', 'blue') as $key) {
  $result[$key] = round(1. * (
    $foreground[$key] * (127 - $foreground['alpha']) * 127
    + $background[$key] * $foreground['alpha'] * (127 - $background['alpha']) 
  ) / (
    127 * 127
    - $foreground['alpha'] * $background['alpha']
  ));
}

$result['alpha'] = round(1. * $foreground['alpha'] * $background['alpha'] / 127);
?>

How to explain that:
- Imagine a 127 x 127 window
- The curtain (foreground) covers an area that is (127 - $foreground['alpha']) wide.
- The shutter (background) covers an area that is (127 - $background['alpha']) high. A part of that is already hidden by the curtain.
- The remaining free area is $foreground['alpha'] wide and $background['alpha'] high.

dman’s picture

O_o
Yeah maybe, but that's why I've taken to just drawing two images and trying to use built in imagecopymerge or something, rather than try to blend on the fly. It breaks it down into two phases, so it marginally less efficient, but it doesn't make my brain heat up.
I hope that recent versions of GD behave better than the one I was testing on 2 years ago, something just when wrong when blending transparency ... although I may have just got the flags wrong.

I suspect there there may be some hidden thing with colors I don't know about. Your maths seems logical to me, but people that know color theory may have a surprise. I've got 'similar' code in watermark.inc ... although that also had a global alpha factor on the overlay, as well as the images alpha.

I found later logic easier if I turned the alpha into a decimal between 0 and 1 as soon as possible :-)

    $watermark_alpha = round(((127 - $watermark_rbg['alpha']) / 127), 2);
donquixote’s picture

Why do you want to round things? Use true floats instead, and use round() when going back to 127 format!
(my above example is still lacking a round() .. I'll fix that)

dman’s picture

The round is meaningless, just a copy+paste from existing code. May have been put there so a debug message was readable... with 2 significant digits, it doesn't seem to have made any difference to the result.
But no, there's no reason for it to be there.

donquixote’s picture

And for the merging:
There are different kinds of merging for different situations.
a) having separate transparent layers on top of each other. This is what I explained.
b) having a mask to manipulate the image. This is what you would do if you have the rounded corner mask. Sometimes your mask is just one alpha channel, sometimes it's rgb or rgba, sometimes you might want a mask with five channels..

donquixote’s picture

I see your point.
I think the best is to work with different names for different concepts:

$transparency = $rgba['alpha'] / 127.0;
$opacity = 1 - $transparency;
donquixote’s picture

I have a new version which is PHP 4.x compatible and can paint a rounded border for the rounded corners.
I did not modify the admin form, just the algorithm - so you would have to modify the admin form yourself.

The algorithm takes a border width and a border color, and the $radii array. It will paint a border on each of the rounded corners and each side, unless both corners on that side have a negative radius.

Example:
If you have $radii = array('tl' => 4, 'tr' => 4, 'bl' => -1, 'br' => -1);, then all sides will get a border except for the bottom.

Unfortunately the latest CVS has double linebreaks everywhere, so I don't want to use it for creating patches. How comes?

dman’s picture

I'm not seeing the double linebreaks, which file? I've been using a linux editor all the time of course, so I don't see how any \r\n may have snuck in. I didn't make any global changes.
Couldn't see it in a fresh cvs DEV checkout either. Odd.

donquixote’s picture

I'm on Windows XP.
canvasactions.inc shows double linebreaks in notepad++ and Firefox "view source" and Opera "view source", but not in windows editor.
imagecache_canvasactions.module does not show any double linebreaks.

Notepad++ text search shows you have \r\r\n in that file, while other files have just \r\n for linebreaks.

dman’s picture

Odd. The file does seem to have been tagged as having Windows-style line endings, although I am not seeing any double lines in Eclipse, vi, Textwrangler etc.
So I've switched it back to proper Unix linefeeds, (which touches every line) and committed it back. Must have been a mix-up somewhere ... historically. Older revisions also seem to be Windows. Meh, must have been like that for ages.

donquixote’s picture

FileSize
39.6 KB

It was my CVS that didn't work properly.
Here is the current version of canvasactions.inc.

What you need is function canvasactions_roundedcorners_image(), function _canvasactions_roundedcorners_pixel_opacity() and function _canvasactions_sparse_circle_matrix(). You do not need class canvasactions_Circle.

canvasactions_roundedcorners_image() will need some modifications, if you want to allow setting border width and border color in the configuration page.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

RikiB’s picture

Status: Closed (fixed) » Active

Id like to add a request for an outer shadow feature that compliments the rounded corners. Similar to option D but it shadow goes evenly around the entire image.

dman’s picture

FileSize
274.72 KB

A little thinking is all that is needed here.

Not as efficient as a new dedicated action probably, but certainly works enough, and is more useful as an approach.

RikiB’s picture

FileSize
102.43 KB

Thats a neat way to get a border around it, but I was hoping for a gradient.

However i tried this and it doesnt work for me because Im having to flatten the background alpha to match the background color of my site. So this just seems to put a border around the square.

donquixote’s picture

So what you want is a halo with gradient transparency.. correct?

I was thinking of this when I did the rounded corners patch, though I think it is a bit too much special-case.

What I could imagine is that instead of a fixed border color + border width you would provide a picture where the picture height is the border radius, and the pixel color at [x,y] would represent the border color at a specified point (depending on corner angle x and distance y from the picture. In other words, the picture defining the border would be stretched and bent around the image you want to decorate.

This method would allow different color shades depending on the angle, and it would allow fancy relief effects.

Another (maybe easier) idea would be to have an image defining the exact look of the corners.

Ideally I would like definition images with 5 or more color channels instead of just rgba. The additional channels could define how much of the original image should be preserved, or what to do with the original image's alpha value.

---

Anyway, this is highly specialized stuff, and a module like this can't provide a generic solution for everything.

My suggestion for more specialized things would be that you write your own imagecache action, either with
(1) a custom module, or with
(2) the built-in "custom action" feature. You can define the function in a module or theme, and then call it from within the "custom action" PHP stored in the database.

Having things in the code (ideally, a module) instead of the database is a good idea anyway, as it makes your work easier to deploy and easier to reuse in other projects, and you can pay an external developer to deliver just that, without introducing him into the details of your project.

donquixote’s picture

@dman:
I totally missed the point of your post!
It's a nice way to do borders, and you can repeat the process to get a gradient. It's not exactly the result you would get with a dedicated anti-aliased action, but would look almost like that.

It's more tricky if you want a half-transparent border or gradient - I don't know if the "define canvas" can do that.

Btw, the second "rounded corners" action should have 17px radius, not 16px (and the next one would have 18px, etc).

dman’s picture

I was wondering whether I should bother with the difference between 16 and 17 px :-B
I didn't see the difference on this example, so just left it!

rikib, it will work when you flatten it - look at what it's doing. It's slicing the rounded corners off twice, the second time with a tiny edge around it.

I know the pitfalls of trying to define a blur... Doing it with corner overlays is possible, but is probably trickier than someone who wants glowing rounded corners in their design could do easily.

Which is why I wanted to be able to solve the blur/dropshadow task using existing image alpha as per D and E. I was thinking of something a lot more laborious than your suggestion for E (multi scans of every pixel at varying intensities). I don't think we've got a blur function available?
I dunno. it would take ages. both to write and to process :-)

RikiB’s picture

There are a few good suggestions that I need to try.

I also wanted to mention css3 also has this feature called box-shadow: http://www.css3.info/preview/box-shadow/

Im not sure if this could be leveraged to add the features mentioned, but its an idea. :)

gooddesignusa’s picture

subscribing

fietserwin’s picture

Issue summary: View changes
Status: Active » Closed (won't fix)

D6 EOL. This module's D6 issues already haven't received any attention for over a year. Closing them all unconditionally now.

donquixote’s picture

Version: 6.x-1.x-dev » 7.x-1.x-dev
Status: Closed (won't fix) » Active

This issue is ages old. But I remember that some effort went into the patch.

I don't know if there is still a use case for rounded corners nowadays. If not, feel free to close it again.

donquixote’s picture

Oh, and: I am currently not actively using this module on any project.
I was simply playing around with some code and then ran across this.

If you think this is all a waste of time, I can understand.