Saturday, June 17, 2006

Optimizing Image Resizing for Web Uploads

Our popular vertical market application - CondoConduit - allows users to upload their own images to web sites personalized for their homeowner associations.

People are getting these images from lots of different sources - the web, digital cameras, cell-phone cameras, etc - and these users have varying levels of computer competancy.

For a long time, we were blindly resizing these uploaded images to a max of 300 x 300 pixels - which often led to really ugly results. Blech!

All we did was see if either side of the image was over the max size for the page. If so, we took the longer of the two sides and figured out the % reduction to make it the max size, then applied that to both sides and resized the image using the WebConnect ResizeImage function.

Depending upon the size of the original image, the results would really... suck.

I found that when Windows GDI+ resizes an image (reduces the number of pixels), it does 'pixel color averaging' to figure out the color of the resulting 'combined' pixel. As you can imagine, if you resize 6 pixels to 3 pixels, it's pretty sharp (just average 3 pixels together to get the resulting pixel color)... but if you resize 8 pixels to 3 pixels, the color averaging takes place over two adjoining pixels rather than one so the resulting image gets really blurry (I'm sure I'm technically wrong about the exact calculations used to pixel average, but the results support this general concept).

So for awhile I found I would have to manually take each large image and resize it in my image editing software using these general guidelines: Crop first (if possible) to get the image into a multiple of 300 pixels (600, 900, etc), then resize to the final image size and uplod to the site.

I asked around in all the usual places about whether they had an automated VFP/GDI+ routine to fill what seemed to be a common need. Getting no responses, I finally bit the bullet and takled the task myself.

What I decided to do was to look for a common denominator smaller than the desired max image size for both the image width and the height. The result should be as close to the desired image size as possible, but had to be a divisor of both sides.

Being too mathematically challenged to do this in a graceful way, I chose brute force. I just count down from the max size by one until I find a whole number to divide by that works with both sides. I am only willing to go down 50 pixels (so the resulting image is larger than a postage stamp):

************************************************************************
***  Function: IntegerResize
***    Assume:
***   Created: 03/28/2006
***   Revised:
*** Copyright: (c) 2006, Ideate, LLC
************************************************************************
FUNCTION  IntegerResize()

LPARAMETERS pcDiskFileName, pnMaxHoriz, pnMaxVert

#IF .F.
    LOCAL Request as wwRequest, Response as wwResponse
#ENDIF

pnHoriz = 0
pnVert = 0
pnResolution = 0
pnNewHoriz = 0
pnNewVert  = 0
pnClosestCrop  = 1000
pnCropHoriz = 0
pnCropVert = 0
plCropFirst = .T.
pnCompression = 0

IF GetImageInfo(lcDiskFileName, @pnHoriz, @pnVert, @pnResolution)       
    IF pnHoriz > pnMaxHoriz OR pnVert > pnMaxVert
        pcTempFileName = ADDBS(JUSTPATH(pcDiskFileName)) + "Delete_Me." + JustExt(pcDiskFileName)
        IF FILE(pcTempFileName)
            ERASE (pcTempFileName)
        ENDIF
        RENAME &pcDiskFileName TO &pcTempFileName
        IF pnMaxHoriz/pnHoriz > pnMaxVert/pnVert
            pcBiggerSide = [Vert]
        ELSE
            pcBiggerSide = [Horiz]
        ENDIF
        FOR i = 0 TO 50
            pnDenominator = pnMax&pcBiggerSide - i
            IF MOD(pnHoriz,pnDenominator) = 0 AND MOD(pnVert,pnDenominator) = 0
                * We have a hit
                pnNewHoriz = pnHoriz/(pn&pcBiggerSide/pnDenominator)
                pnNewVert  = pnVert/(pn&pcBiggerSide/pnDenominator)
                plCropFirst = .F.
                EXIT
            ELSE
                * keep track of the closest for cropping or padding
                IF MOD(pnHoriz,pnDenominator) + MOD(pnVert,pnDenominator) < pnClosestCrop
                    pnClosestCrop = MOD(pnHoriz,pnDenominator) + MOD(pnVert,pnDenominator)
                    pnNewHoriz = INT(pnHoriz/(pn&pcBiggerSide/pnDenominator))
                    pnNewVert  = INT(pnVert/(pn&pcBiggerSide/pnDenominator))
                    pnCropHoriz = MAX(INT(pnHoriz/pnNewHoriz - pnMaxHoriz),0)
                    pnCropVert = MAX(INT(pnVert/pnNewVert - pnMaxVert),0)
                ENDIF
            ENDIF
        NEXT
        IF plCropFirst    && we didn't find a value
            * Crop and set variables for resizing
            *************************************
            ** TO DO!
            *************************************
            * PAD images for negative crop values
            *************************************
            pnLeft = INT(pnCropHoriz/2)
            pnTop = INT(pnCropVert/2)
            ReadImage(pcTempFileName,pcDiskFileName,pnLeft,pnTop,pnHoriz-pnCropHoriz,pnVert-pnCropVert)
            ERASE (pcTempFileName)
            RENAME &pcDiskFileName TO &pcTempFileName
        ENDIF
        * CreateThumbnail(pcTempFileName,pcDiskFileName,pnNewHoriz,pnNewVert)
        ResizeImage(pcTempFileName, pcDiskFileName, pnNewHoriz, pnNewVert, pnCompression)
        ERASE (pcTempFileName)
    ENDIF
ENDIF

ENDFUNC    && IntegerResize

Now resized image are crisp and clear - and I can use the time I used to fill resizing my client's images to write a blog!


Blogged with Flock

Strip ALL unwanted characters from string in one line - CHRTRANEXCEPT()

Doing mostly VFP/WebConnect web applications means I do not have the nice 'picture' and 'format' properties that are available in the desktop VFP development environment.
A frequent requirement is stripping unallowed characters from an input field. Now, CHRTRAN is a great way to do this kind of thing - except that to use it you would have to figure out any and every character the user might have entered, and include all possibilities in a very long string. What we really need is a CHRTRANEXCEPT() function.

Several years ago I came up with this little meta-diddy for stripping all non-numeric characters from an entry field - so I can save a clean number withouy dollar signs or commas that a user might enter in my web forms. In this example I am assigning the asking price of a widgit to the variable m.Asking, and stripping it of all non-numeric info:
m.Asking = VAL(chrtran(m.Asking,chrtran(m.Asking,'0123456789.',''),''))
The inner chrtran extracts any nonallowed characters from the value (if any) and uses that string as the search string for the OUTER chrtran.

So, if we start with 123XYZ!*^, the inner chrtran will result in “XYZ!*^”, resulting in the final string 123.

This is also a cool way of removing ANY unallowed characters by simply specifying the allowed characters.

Very handy.

Sunday, May 14, 2006

Dealing with HTML checkbox return values

Checkboxes are a special creature in the HTML world. They require special handling.

When a checkbox is checked, it passes on its VALUE property to the web server - as you would expect. The problem is that when it is UNchecked, nothing passed to the web server indicates that checbox ever existed! It's as if the checkbox was never there!

So if you check for the existence of the object value and the object does not exist, you can surmise that the checkbox was un-checked. This works, but it encourages developers to hard-code their logic for checkboxes - which is a drag if your checkboxes vary from user to user, or if you add or remove checkbox options.

A better solution is to use the same logic that generated your checkboxes in the first place to create a list of POSSIBLE checkbox values. You can then scan through the possible checkbox names and check for the existence of each.

Here's an example of how I generate the checkboxes on one of my HTML forms (using West-Wind WebConnect):

lcNotifyBoxes = []
SELECT Services.*, MenuTitle ;
FROM .\Condo\LinkSvcs, .\Condo\Services ;
WHERE Services.SvcCode=LinkSvcs.SvcCode ;
AND LinkSvcs.PropCode = lcCondoCode ;
AND AccessLvl <= lnAccessLvl ;
AND !EMPTY(NotifyCode);
ORDER BY OrderCode ;
INTO CURSOR TLocalQuery
IF _TALLY>0
lnColCount=INT(_TALLY/3) && For creating three columns
lcNotifyBoxes=[<table width="100%" border="0">]+CR+[<tr>]+CR+[<td>]
lnDummy=0
SCAN
IF TLocalQuery.NotifyCode = [H] AND VAL(lnPosition) < 22
&& Keep work req notification out for non-board members
ELSE
lcNotifyBoxes=lcNotifyBoxes+;
[<input type="checkbox" name="]+TLocalQuery.SvcCode+[" value="Yes" class="chkbox"]+;
IIF(TLocalQuery.NotifyCode $ lnNotifyList," checked","")+[>]+TLocalQuery.MenuTitle+[<br>]
IF lnDummy=lnColCount
lcNotifyBoxes=lcNotifyBoxes+[</td>]+CR+[<td>]
lnDummy=0
ELSE
lnDummy=lnDummy+1
ENDIF
ENDIF
ENDSCAN
lcNotifyBoxes=lcNotifyBoxes+[</td>]+CR+[</tr>]+CR+[</table>]+CR
ENDIF
So let's use the same SQL Select in order to create a cursor to scan, and determine the values of the checkboxes: 
SELECT Services.*, MenuTitle ;
FROM .\Condo\LinkSvcs, .\Condo\Services ;
WHERE Services.SvcCode=LinkSvcs.SvcCode ;
AND LinkSvcs.PropCode = lcCondoCode ;
AND AccessLvl <= lnAccessLvl ;
AND !EMPTY(NotifyCode);
ORDER BY OrderCode ;
INTO CURSOR TLocalQuery
IF _TALLY>0
SCAN
IF Request.IsFormVar(TLocalQuery.SvcCode) AND “Y“ $ Request.Form(TLocalQuery.SvcCode)
* Value is checked (second check above is probably not necessary)
WAIT WINDOW TLocalQuery.SvcCode + [ is CHECKED]
ELSE
* Value is NOT checked
WAIT WINDOW TLocalQuery.SvcCode + [ is NOT checked]
ENDIF
ENDSCAN

ENDIF

Saturday, May 13, 2006

Trouble with Foxite Blogger...

I've been trying for a few weeks to access my Foxite blog admin interface to create some new blog posts, but I can no longer log in... nor can I get support to get to it (so much for that...)

I'll transpose my (few) older blogs here and use this blog area instead. Stay tuned for new ones.

Saturday, December 31, 2005

Everything old is new again

It seems it is today's outlaw that becomes tomorrow's genius.

Decades ago in NYC, there were hundreds of people walking down the street talking to themselves, gesticulating wildly. I found myself steering clear of them. Little did I know they were beta-testing cell phones with bluetooth headsets! Now I find people EVERYWHERE having personal conversations about their love life or home frustrations - wandering around as if they were not in the middle of a crowd of strangers. Who knew?

Likewise, decades ago the cops were arresting the creators of advertising on the sides of busses and subway cars. A short time after that, some of the most talented were commissioned to do public graffiti on the sides of buildings. It now appears that the least talented are now buying and selling the sides of trains, busses, trolleys and even billboards on trucks. Who knew?

So, what other nuisances will become tomorrow's marketing breakthroughs? Garbage cans that, when banged together, generate an audio commercial in the wee hours of the morning? Discarded newspapers that lose their newsprint after a couple of days to reveal colorful advertising beneath? Sidewalks designed to crack to reveal an advertising message beneath your feet?

Perhaps spammers will hire novelists and cartoonists to write serialized fiction - including a snippet in each spam message to encourage poeple to save and forward the spam. We might actually look forward to our next spam blast! Like most technological breakthroughs on the internet, it will probably be the porn sites that discover this technique first.

Just a thought...

Wednesday, March 12, 2003

They don't know what they know....

My wife was cleaning out some old papers last weekend. "I don't really want to throw all this stuff out - we may need it someday!"

"As a matter of fact, I just found something we could have used a couple of months ago when we were looking for information on..."

I don't remember what we were looking for two months ago, and clearly at the time we needed it she didn't remember she had the information. Of course, if you don't know you have the information it's the same as not having it at all (if a tree falls in the woods...)!

Some firms use a similar method for assembling their library of 'standard details'. That is, grab every detail on every project and dump it in a central location for re-use. This central location can be thought of as a 'trash can' - or perhaps more appropriately a 30 yard construction container.

My usual response to firms considering that kind of information repository is "At what point is it easier to reconstruct the detail from scratch than it is to rummage through years of accumulated trash to find that one gem you're looking for?" Such an arrangement may have a small percentage of good information in it, but since it's buried under a pile of .... uhhh... 'refuse', it's seldom worth the search.

Where do team members get their 'reference details' from to avoid re-drafting? Ask around and I'm sure you'll find details are swiped from each team member's personal experience - their recent past jobs. The quality of the detail will be dependent upon the success of that same detail in the recent past. It's called 'tacit knowledge'. Not a bad arrangement, all in all. It does have it's limitations, though.

For example, the catalogue of past details is in the head of each team member. If a better detail was used on a recent project but no-one on the current project team has worked on that project - well, it won't get used. Also, as people leave the firm, they take their 'tacit knowledge' with them: even though the details themselves are still in the firm's computers, current team members won't know to look for them.

One method of sharing these mental catalogues is cross-team communication. If projects are presented to the firm on a regular basis, it not only builds staff knowledge and personal presentation skills but also promotes the sharing of the 'internal mental catalogue' of details.

Another catalogue sharing method is to encourage technical reviews for each project. A technical reviewer or team has seen most of the recent details that have gone out the doors, and has the expertise to evaluate them. If that reviewer can advise the project team early enough and frequently enough to advise the team to 'look at this detail on project X' when the needs arise, the best details will get the most use.

Of course, one can actually assign a person or people to take the best details from all your projects, technically evaluate them, clean them up to conform to drafting standards, catalogue them for internal team members, and regularly re-evaluate the library. Experience has shown that this technique is very expensive and time consuming. Old details that are no longer appropriate (due to new building codes, or new building technology) are too seldom pruned from the library, and new details take to long to go through the rigorous review and approval process usually imposed on them. If you currently use this method, try to assess just how many people are using the library vs. how many are using 'the last job they worked on' as their personal library.

Another more technical solution is to use a real search program. If you have not already used it, go to www.google.com Google searches billions of web pages (3,083,324,652 pages at last count) and indexes them by keyword. Google has provided their search and indexing technology in an appliance that plugs into your network.

Docu-Point (www.docu-point.com) sells a 'Drawing Searcher' application which will scan all the drawings on your network and make them searchable (and viewable) through your web browser. They also have an add-on option for including common business application document formats like DOC, XLS and PDF. The last I checked, this product did not have the capability of maintaining search capabilities for off-line files.

A potential danger of any 'library of construction details' is that there is seldom any post-construction follow up. Many years ago, I grabbed a 'standard' counter swinging door detail from the firm's library of fully reviewed and approved details. While drafting a copy of this detail, I noticed that the cut in the countertop was slanted in the wrong direction for the countertop to actually swing open. Apparently, this 'standard detail' was used in several projects and no doubt either corrected in shop drawings or in the field - but the correction never made it's way back to the 'standard' record. I'm assuming the detail was corrected because I never heard that the emergency carpenter swat brigade was ever called out with the jaws of life to extricate a poor employee from behind the counter.