Service tax and tips

This came about because I don’t like ++’s behind prices. If you tell me the price of the food is $14.90, then when I’m paying, it’d better be just $14.90. I don’t like doing mental arithmetic gymnastics. I also don’t like whipping out my phone to use the calculator.

It’s probably the main reason why I don’t go to fancy restaurants. It’s not so much the high price (though it’s a big factor), but that I feel deceived about the price. The listed price is not the price I have to pay.

Consumption tax

The Goods and Service Tax (GST) was established in Singapore, 1994. It started out as 3% in April 1994, then 4% in January 2003, then 5% January 2004, and is currently 7% from July 2007 onwards.

This caused some confusion because what used to be “round” prices now became weird. $5 became $5.15, and pray you had a human calculator with you if a price was something weird, like $4.95.

Because the GST is a consumption tax, it generally applies to everything. From food to clothes to electronic gadgets. Almost every price is affected.

The progression from 3% to 7% made things worse. I remember having to design and code a software system to deal with taxes. Based on the date of the transaction, a different tax rate had to be used. For example, in December 2002 it would be 3%, but January 2003 it would be 4%.

Because it generally confused the Singapore consumer, most shops simply incorporated the GST into their prices. Instead of saying an item costs $4.95 with 7% GST, the listed price is $5.30 (actual is $5.2965). The consumer doesn’t have to do any mental calculations.

It is also particularly fortunate that the Singapore 1 cent is no longer issued, because then businesses forced their prices to be rounded to the nearest 5 cents. Apparently, even 5 cent coins are no longer issued, we might see prices rounded to 10 cents. This rounding will come into play later on.

Also, if you’re in America, death to pennies… It costs more to produce a penny than a penny is worth.

Waiting tips

It is not common in Singapore (or Asia in general) to tip a waiter or waitress. I think East Asians and South-East Asians believe that it’s their job. Any tips are a bonus, not a given.

I’ve read that in Western countries, particularly in America, the wage for waiters and waitresses are lousy. Some wait staff survive a month because they had tips.

The tip is another unknown factor in my calculation of payment. How much to tip? I would much rather you work everything I have to pay into the price, and I decide beforehand whether I want to consume food and beverage at your eating establishment.

While I’m against exploitative wages, I also don’t want to fire up the neurons needed to calculate the appropriate tip amount.

Besides, the tips given to the waiter or waitress probably don’t even go directly to them. The tips go to a general pool, where it’s either spent on something for all the wait staff, or divided evenly amongst the wait staff. This also generally discourages staff not to be extra attentive to customers, since all their colleagues gain at their expense.

One lump price

So for most Singapore businesses, the GST is included in the price. The consumer is charged at one price, and the business has to calculate the correct GST to pay the Singapore government. Remember, tax evasion is a criminal offense.

However, there are 2 ways to calculate the GST. One is to calculate the GST portion first, the other is to calculate the sales price first.

For example, $14.90 with GST included. Using 7% as the GST, we divide $14.90 by 107 (because $14.90 is 107% of the sales price) and then multiply by 7 to get the GST amount. Which is $0.97 (rounded from $0.9748). So the sales price is $14.95 – $0.97 = $13.93.

In the second case, the sales price is $14.90 / 107 * 100 = $13.93 (rounded from 13.925). And so the GST amount is $14.90 – $13.93 = $0.97

So far that seemed fine. Both calculations give the same results. Aahhh, but what if the government don’t care to accept values not in multiples of 5 cents? Or what if the business fudges the calculations a bit?

If you round the GST amount $0.97 to $1, then the final sales price is $13.90.

If you round the sales price $13.93 to $13.95, then the GST amount is $0.95, which makes the GST rate as $0.95/$14.90 * 100 = 6.38%. Not quite the 7%.

I’m not saying businesses do this manipulation (which is easy if you have transaction history and just tilt calculations in your favour), but rounding is the bane of financial applications.

10% service tax

Because Singapore (or Asia in general) don’t have a practice of tipping the wait staff, I think that’s why businesses set a 10% flat service charge. Basically, 10% of the meal price work as tips.

While I don’t have a complaint about this, I do complain that a $14.90 meal can become $17.45 (rounded from $17.433). How? Because the service tax and GST weren’t included. Thus the final price is $14.90 * (10% + 7%)

I don’t care if there’s a big asterisk or ++ at the end of your price. I hate reading footnotes directed from asterisks. And ++’s? They just scream clever/obnoxious/brilliant/crafty to me. Why do you think programmers prefer pre-increment (++i) than post-increment (i++) for readability?

Making it easier for me

I was telling this to my mom, and she had an answer (she’s a sales person). Businesses don’t include all those taxes into one price because it’s easier for them to calculate their service tax and GST.

If $17.45 was the listed price, they’d have to back-calculate. The GST amount would be $17.45 / 117 * 7 = $1.04. The service tax would be $17.45 / 117 * 10 = $1.49.

If they only work with $14.90 as the only price, then it’s a “simple” matter of $14.90 * 10% = $1.49 for service tax, and $14.90 * 7% = $1.04 (or $1.05?).

I don’t really care. Your business probably handles hundreds and thousands of transactions every day. You’re not going to do this by hand. You have a fancy financial application that does it for you. Your financial application doesn’t care if it needs to do one more arithmetic operation per transaction. Computers crunch numbers for breakfast. Your financial revenue report will still look pretty.

You know, I don’t really know the point of this article. Probably a rant. Make it easier for me, ok? Or at least be more honest with your prices. I might just patronise your eating establishment more frequently.

Accidental leadership

The following might sound egotistic or even narcissistic. You’re free to skip reading this. But if you’re in the mood for some (real life) stories, you’re in luck.

So I’m going to tell you how I accidentally assumed roles of leadership, even though I never asked for them.

Glorified rubbish cleaner

I moved to another place when I was in Primary 4. New place, new school, new friends. On hindsight, my best friend from the old place probably wasn’t my “best” friend, and might not even be in the category of “friend”. But that’s another story. Starting all over, my 10 year old self just wanted to hunker down, study and get good grades, and see what happened. Being the new kid in town can be tough…

So when I was in Primary 6, I only wanted to get through the PSLE (Primary School Leaving Examinations), a crucial part because it determines which secondary school I could go to.

But one fine day (beginning of school year, I think), my form teacher (an assigned teacher to be in charge of the class, but not necessarily have to teach the class in any subject) needed school prefects. Well, the school needed prefects, and I was in the top class (*cough*), so my class was supposed to churn out more prefects.

It turned out that my form teacher was short of one prefect. So she asked the other prefects to nominate someone. Whaddaya know, all of them unanimously pointed at me. My attempts to reject the exalted post were declined.

And so it’s such that I was the school prefect in charge of the parking lot. Oh and the school fence. Don’t go thinking just because I had a large piece of land that I was powerful. I was just a glorified rubbish cleaner, making sure my domain was litter-free.

One of my duties was to be the “staircase master”. My school had 4 stories, and the staircases wound anti-clockwise. After major school gatherings, prefects would be placed at the stairs telling students to “Keep to your right” while students went back to their classrooms. That was my first taste of command, or of ordering people around. I didn’t quite like it. I guess I don’t like ordering people around, nor being ordered around.

Handling other people’s money

When I was in secondary school, I joined the Chinese Orchestra. It turned out that my Chinese teacher was also in charge of the Chinese Orchestra. I was on fairly close terms with the class monitor (who’s one of the popular guys, so don’t let it be said that nerds/geeks can’t mix with the popular people), and he was also in the Chinese Orchestra (was I also the “deputy class monitor”? I can’t remember…).

The Chinese teacher had a high opinion of me. Once, I was nominated to represent the class in a Chinese debate. My Chinese was so-so, and my debating skills were highly questionable. I don’t know why I was chosen…

Where was I?

So what with all my “connections”, somehow I ended up being the Treasurer of the Chinese Orchestra. I had to make sure there were enough picks (for strumming) and strings (for the stringed instruments), and buy them if necessary. When cash was low, I was the one asking all the members to chip in. Part of that was to pay for the 2 instructors too. This “asking other people for money” will surface again later…

Once, I tallied the accounts and found out the cash I had on hand was different from what the books said (in case you don’t know, this is a financial no-no, even if it’s just a cent off). I couldn’t sleep for days. I think I solved most of the inconsistencies, and paid the (small) difference from my own pocket. Handling other people’s money can be harrowing… And I was just 15 years old.

Affability trumped popularity

I graduated from secondary school to junior college, hoping that I didn’t have to be in charge of anything. Oh that had a snowball’s chance in the desert of happening.

Once the class monitor elections came around, I shot to the top as the clear winner, voted unanimously it seemed. And I didn’t even do anything to promote myself. I was coming up with new camouflage techniques to blend in with the wall. I must try to be less affable…

There were popular guys. There were cool guys. There were good-looking guys. There were smart guys. But I was chosen. I hate landslide victories…

Chance can be cruel

After graduating from junior college, I was conscripted into the Singapore army. All 18 year old Singapore males are conscripted.

So on that fateful day, my dad accompanied me to the military training camp. I signed in and was assigned a bunk. It turned out that my bunk was the first bunk of a section. That meant I was to be the section leader of that room.

Oh joy of joys.

And after my dad explained what a section leader was, it was like cranberry out the wazoo, and it was pointed at me. You’d think the section leader would be selected based on capabilities. But no. I wasn’t physically fit, nor dextrous, nor motivated. To use Internet terms, I was a giant nerd.

In case you’re not familiar with military terms, a military company is composed of platoons, which is composed of sections. A typical section has 7 to 12 soldiers. Because we’re just in basic military training, the section was swelled to have 14 men. Me, and 13 other young men. This did not bode well…

Interesting asides: I’ve been noted as “whiny” during my section leader “reign”. Good thing I didn’t have to ask them to do stuff, since if we didn’t clean the floor real good, we don’t get to leave camp. I was unfriended (this was before Facebook) by my best friend in primary school. My parents divorced when I was in secondary school. There were family problems. Being “selected” as the section leader was exceptionally unfortunate. I didn’t want that position. Also, my bunk buddy was continually sick (benefit of doubt, since he always seemed hale and hearty), and I had to collect food for him and other stuff. As if I didn’t have enough things to do. So give me a break.

Half a caterpillar

During the later days of my 2.5 years in the military, I somehow got into the good books of the S4 (the officer in charge of logistics). The good major often got me to help him with documentation (I was in the administration department).

*sigh* With great power comes great responsibility.

Now my mates and I would usually go to the mess hall for our lunches. It’s free, and all you really need to do is clean up the trays afterwards. This was back in the days when we still had cooks who were serving National Service (what we call conscription in Singapore).

Well, there was the one time when my mates and I went for lunch, and one of them found half a caterpillar in his vegetables. He didn’t make a fuss out of it. Though he did say that if it was ants, he’d just eat it/them. But he drew a line with caterpillars.

Anyway, the major knew that we went to the mess hall for lunch and he asked us how the food was like. He was in charge of logistics in the camp after all. All of us kept quiet. I mean, we’re just sergeants and corporals and lowly ranking people. This was a major.

The words came out of my mouth before I really thought about it. I told the major about the caterpillar incident. My mates were shocked, whether at my audacity or my stupidity, I’m not sure.

The next day, the air at the mess hall seemed tense.

I resolved to be more careful with my words with the major.

Tighter than Schwarzenegger’s fists

The major had a dedicated clerk to help him. However, the clerk finished his conscription service, and … guess. Go ahead, just guess. That’s right. Yours truly became the unofficial dedicated clerk next in line.

Sometimes, during the morning assembly, the major would drive up and get out of his car. We would just stand in attention. The major would then say “Vincent, come to my office later.” I would later get a bunch of paper documents to type into Word documents and save them into a floppy disk. Yeah, those floppy disks. My mates would just let me use their computers, because I hadn’t one of my own.

Anyway, one of those duties the dedicated S4 clerk was to collect monthly mess fees from people in my office with ranks corporal and below. Corporals pay $3, lance corporals $2, and privates and recruits $1. It’s not a lot, but it was harder to wrestle a couple of dollars from them than grabbing a dumbbell away from Arnold Schwarzenegger. While I had experience asking people for money as a treasurer in my secondary school days, these were “hardened veterans” of real life…

I was supposed to hand in the mess fees within a few days from the start of the month. However, the excuse was always “I don’t have money.” And so I wait till just after the 15th, because that’s when the army pays out the salary. So my excuse to the people at the canteen office was that my mates didn’t have money, but I could hand in the mess fees just after the 15th. It worked.

In conclusion

I didn’t ask for them. Did I tell you the story where I worked in “Law of Large Numbers” into a PowerPoint slide for a presentation to a group of managers and directors where I was the task force leader to investigate why people in the office were afraid to speak up?

Optimal width and height after image rotation

A while ago, a blog reader Fabien sent me some code (you can read it here. Thanks Fabien!). The PHP code is a modification of my image rotation code with some upgrades.

I was looking through his code (French variable names!) and was puzzled by the initial section. I believe he based his code on my code where the resulting image wasn’t clipped after rotation, meaning the whole image was still in the picture/bitmap (though rotated).

In that piece of code, I just used the diagonal length of the image (from top-left corner to bottom-right corner) as the final length and breadth of the resulting image. This gave the simplest resulting image dimension without doing complicated maths calculations (a square in this case).

However, what if you want to know the optimal width and height of the resulting image after rotation? Meaning the best-fit width and height that just manages to contain the resulting rotated image. For that, I need to tell you some basic trigonometry and geometry.

Image rotation, optimal width and height

Suppose you have a rectangle with L as the length and H as the height. It is rotated t angles. I’m not going to explain the maths behind it. It involves complementary angles, supplementary angles, rotation symmetry and trigonometry with sines and cosines. Convince yourself that the diagram is true.

So after rotating t angles, the optimal width is L * cos(t) + H * cos(90 – t)

The optimal height is L * sin(t) + H * sin(90 – t)

Short digression: You might notice that any lengths that lie parallel to the x-axis usually involve cosines, and lengths that lie parallel to the y-axis usually involve sines. It’s just the way trigonometry works.

Now, although the image rotation is carried out with respect to the image’s centre, rotating by the top-left corner will result in the same optimal width and height. Again, this is basic maths so you’ll just have to convince yourself it’s true (and that I don’t really want to explain it…).

But that’s if t is an acute angle. What about other angles?

Image rotation, optimal width and height

For those angles, we just need to calculate the acute angle based on the initial rotation angle. After that, just substitute that calculated acute angle into our formula above. I have absolute confidence in your ability to check which quadrant in the Cartesian coordinate system does your rotation angle lie in.

UPDATE: In case you are unable to view images, if your rotation angle is in the 2nd quadrant, the calculated angle is (180 – t). If in the 3rd quadrant, it’s (t – 180). And if in the 4th quadrant, it’s (360 – t).

In practice, you might still want to pad a couple of pixels around. But that should give you the smallest image dimension which can still contain your rotated image.

Announcing SpreadsheetLight version 1.1

After much researching and programming and suffering (and tea), I’m happy to announce that version 1.1 of SpreadsheetLight is up! SpreadsheetLight is a spreadsheet manipulation library based on Open XML.

I added some functionality for defined names and clearing out data from rows/columns/cells. You can also split worksheet panes now. Version 1 didn’t have split pane functionality because I couldn’t find a way to make it easy for the programmer. Well, I wrote a function that allows you to do it with a minimum of fuss.

But the main bulk of version 1.1 updates is the insert/delete rows/columns part. Say you want to insert 4 rows at the 3rd row. This means all the rows from the 3rd row (including the 3rd row) must be shifted downwards 4 rows. Every merged cell, cell formula, table and defined name must be accounted for.

Merged cells need to be enlarged or trimmed off as needed. Or just simply deleted if it’s entirely within the delete range.

Tables need to be updated so the cell references within are correctly adjusted. If it used to contain C3:F6, after inserting 4 rows, it has to be C7:F10.

Cell formulas and defined names are sort of related, with the former typically of a more complex form than the latter. Consider taking on something like this:

= A1 +LOG10 +”BCD32″ -SUM($H6:Sheet1!K$9) *LOG10($F$5) + BCD32 – SUM(F2:G3)

That has to become this:

= A1 +LOG14 +”BCD32″ -SUM($H10:Sheet1!K$13) *LOG10($F$9) + BCD36 – SUM(F2:G7)

Note the LOG10 part. The first instance is a cell reference. The second is the logarithm function, base 10, of the cell originally $F$5. Note also that the first instance of BCD32 is a literal string. Literal strings should not be changed, even if they contain a valid cell reference.

That part took me quite a while. Let’s just say I’m glad I’m fairly proficient in regular expressions…

The only thing I left out was pictures (aka worksheet drawings) for insert/delete rows/columns. To create similar behaviour to when you’re doing it in Excel requires more arcane coding…

So if you’re looking for a spreadsheet library that’s light-weight yet capable of heavy-weight functionality, with simple-to-use functions, then consider SpreadsheetLight. Or tell your boss.

I’ve also included a Platinum version, which includes the source code of SpreadsheetLight (licensed with the MIT license). So if you want to know how I accomplished all that magic, consider the Platinum version then. Lots of comments to make the hairy parts less hairy…

Special drawing rights

No, it has nothing to do with paintings. And it’s “special” in the sense of “unique” as in “His appearance? Let’s just say he has a … unique look.”, or “different” as in “Oh no we didn’t unfriend each other. We just had … different opinions.”.

It’s actually a form of currency. Ok, no, a claim to currency. For example, an American dollar is USD 1, and a Singapore dollar is SGD 1. In this case, it’s SDR 1.

At first, I thought it’s a special case of Singapore dollar. You know, Singapore DollaR (I really thought some marketing staff or programmer made a mistake of currency abbreviations). I encountered it when I maintained an accounting software back when I was at a telecommunications company. The currencies supported were USD, SGD, AUD (the company had a wholly owned subsidiary in Australia) and SDR.

I remembered asking my supervisor why we had accounts with outstanding amounts in SDR. The amounts hadn’t been paid in years. You’d think the billing department would’ve wanted to settle accounts so we could all move on with our lives. It seemed obvious the account owners had no intention of ever paying the company in SDR (or whatever value of SDR in appropriate currencies then. Or ever).

So SDRs are a basket of currencies, currently of euros, Japanese yen, pound sterlings and the US dollar. It works something like a world currency, but not quite. It’s not widely used because (from Wikipedia):

One reason SDRs may not see much use as foreign exchange reserve assets is that they must be exchanged into a currency before use.

Meaning something like you’ve got to exchange 50000 Starbuck points to $5 before you can use it to buy a cappuccino.

This is due in part to the fact private parties do not hold SDRs, they are only used and held by IMF member countries, the IMF itself, and a select few organizations licensed to do so by the IMF.

Meaning normal people like you and I don’t even know about SDRs in the first place.

While all that is very interesting, I have a more important question. Why did the people who wanted the accounting software allow SDR in the first place? Didn’t they know SDRs are very illiquid (you can’t just use it as cash)? Few people, ok, scratch that, since individuals probably don’t have access to SDRs. Few organisations and companies would be using SDR as a form of currency. Why was SDR supported in the first place?

The people who wanted the software might be at fault. They might just have wanted to support that one customer who wanted the option to pay in SDR. The software developers were also at fault. They should have advised the software commissioners (that’s a new term) to remove SDR as a support option.

An accounting software used for tens of thousands of customers, and an option was built into the software to support a few of those customers. This kind of decision makes for all sorts of bugs and maintenance problems, so I really hope a lot of thought went into this.

If we programmers simply create software according to the specs laid down by users, then you can see why users don’t think we’re important. They can just outsource to some exotic place in Asia where programmers are hired on the dime.

We need to think. We need to analyse. We need to question. That’s the value we add as creative professionals, software developers, programmers and what-have-you.

Prepaid Postpaid Convergence

What’s one of the most important concerns of any business? Getting paid.

What’s the next most important concern of any business? Getting paid on time.

There’s this concept called the “prepaid postpaid convergence”, which hailed from the telecommunications industry (where I spent the majority of my professional life in). You’re probably on a postpaid mobile phone plan, where you’re charged for what you used in the previous month. Or you might be on a prepaid plan, where you dump a bunch of cash into your plan/phone/card/thingy and you can text and call until your money runs out.

This is a particular problem in the telecommunications industry because billing customers is challenging. It’s difficult to bill in one generic way to handle both prepaid and postpaid charges.

Think of it this way. I could dump $50 into my prepaid card and not use my phone for 6 months. But for 6 months, the company has to keep track of that $50. The worst thing is, depending on the billing requirements and laws in the country, the company may not include that $50 as “revenue earned”. The company can only include any monetary amount after I used some of that prepaid cash. Say I used $11.25, then the company “earned” $11.25 (even though I’ve paid $50 upfront).

Prepaid billed amounts are “future money”. Postpaid billed amounts are “past money”.

There are 3 general payment modes:

  • You used a product/service but haven’t paid yet.
  • You pay at the point of usage.
  • You pay first, but haven’t used the product/service.

The timing is important. For the purposes of this article, we won’t be strict about the point of usage and point of payment. In some restaurants, you get to eat first before you pay. I will consider that as “payment at the point of usage”, unless you intend to get a free meal and bolt the moment you get a chance to.

So why is this important to you?

The business models are different

Let’s consider the Apple App Store. You find an app you like. You purchase it. 2 business models come into play.

First, Apple charges you immediately on your credit card. You pay immediately upon purchasing the app. Apple gets the $0.99 from the credit card company, and can report that $0.99 on their profit/revenue report.

Second, the credit card company bills you the next month, including that $0.99 app you bought. You’ve enjoyed using the app first, before actually paying for it.

Apple and the credit card company have 2 different business models, even though the “product” is the same. (technically, the credit card company isn’t selling the app, they’re selling you the service of having a credit card, but to you it’s the same thing)

Web hosting companies use the prepaid model (that I know of). They may charge you for 3 years worth of hosting your website. You pay for 3 years first, then you get to have your website hosted. Even if you decide to cancel after 6 months, and you might get a 2.5 year refund back (depending on whether the web hosting company has this policy. Please check), but you still had to have paid for 3 years upfront.

I’m assuming you’re a developer. The trend is that the Internet is going to be a big thing. Let’s say you’re a startup founder. Choose your business model wisely, because it’s going to be hard to change it.

The Amazon S3 service lets you store stuff. You pay for what you use (the more data transfers made, the more you pay). This is postpaid.

iStockphoto lets you buy credits which you can then use to buy images. You pay first (in a large lump sum) then you buy images with the credits (that were bought with cash). This is prepaid.

Prepaid has this annoying thing…

It’s called “keeping track”.

Let’s say a text message costs $0.10, and you bought a prepaid card for $10. That means you can make 100 text messages.

A postpaid customer will be billed $0.70 for the 7 messages she sent in the last month. She will also be billed $1.00 for the 10 messages she sent this month (in the next month).

You on the other hand, won’t be billed. Because you already paid $10. Theoretically, you could hold onto that $10 value in your phone card for eternity. Depending on the billing requirements, the telecommunications company might include that $10 as revenue. But since you haven’t actually sent any text messages (or phone calls), it’s kinda shady since the company hasn’t rendered any services to you yet they “earned” $10 for nothing. In any case, you’ve paid $10 for nothing (since you didn’t use it).

The worst part is the telecommunications company has to store that $10 as a line entry in a database somewhere. And without any corresponding entries to “deduct” that amount. That database entry will stay there forever without resolution. The telecommunications company now officially hates you.

This is why most prepaid items have an expiry date, even if they’re virtual products and won’t decay over time. For example, iStockphoto credits last 1 year. Vouchers from movie theatres, supermarkets, restaurants also typically have a 1 year expiry date. They’re vouchers, it’s not like they’re going to wither away. But companies need to keep track of them.

When the prepaid items expire, the companies can then include the value of the items into revenue, because the customer at that point can no longer use the item, hence it’s taken as the customer had already used it.

Imagine you have to write a program and design a database to support the prepaid billing structure. Your program has to keep track of anything the prepaid customer does, and deduct the corresponding amount from the stored value in a database. Every single text message and phone call the customer makes, the program has to go check if there’s enough in customer’s stored value. Technically, there’s a switchboard that bars the customer from making a text message or phone call before the text message or phone call can be made (that’s the hardware check). Then your program breathes a sigh of relief when the stored value is gone or expired… And then the customer tops up his stored value with $50. Your program grows an unreasonable hatred for this customer.

Final thoughts

A postpaid model requires you to trust that the customer will pay you. A prepaid model lets you get money first, but keeping track might pose a small challenge.

With the Internet being real-time, the better model for online products and services is the “pay immediately” model. You don’t have to keep track of inventory (if at all) and you get the money immediately to your bottom line.

I know there’s also the subscription model. But do you charge customers for the month they haven’t yet used, or for the month they’ve already used?

I’m pretty sure I left something out, or haven’t explained something properly, so leave a comment or send me an email.

Image rotation with bilinear interpolation and alpha progressive borders

So a blog reader, Fabien Auréjac, emailed me an improvement over the code I posted on image rotation. Here’s the one with bilinear interpolation, and here’s the one with bilinear interpolation and no clipping.

You can find Fabien here and here (warning: French ahead), and also at Stack Overflow.

Fabien translated the core of my code into PHP. The improvement was on assigning alpha values to the edge pixels after rotation. Edge pixels are pixels beside the “blank” pixels (I used black in my code, for instance). The alpha values mean the edge pixels are “softer” and thus the resulting image looks smoother.

I suppose if you really want to, you could also “dumb down” the values of the red, green and blue colour components for more softening (in addition to the alpha component). I say “dumb down” because the blank pixels I used are black (meaning zero for the RGB values). You’re free to go ahead and do more interpolation.

Fabien has given permission for me to post the code here. I’ll leave it as an exercise for you to translate to your programming language.

$distOmbre=3;
$flouOmbre=4;
$angleRot=60;
$img=imagecreatefromjpeg("media/diapo-Chinon.jpg");
$size=getimagesize("media/diapo-Chinon.jpg");
$LsupH=($size[0]>$size[1])?1:0;
$angleBool=(int)($angleRot/90)%2==0?0:1;
if (($angleBool+$LsupH)%2==0) {
	$largeur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
} else {
	$largeur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
}
$largeur+=$distOmbre+$flouOmbre*2;
$hauteur+=$distOmbre+$flouOmbre*2;
$angleRot*=pi()/180;
$imgRot=imagecreatetruecolor($largeur, $hauteur);
imagealphablending($imgRot, true);
imageantialias($imgRot, true);
for ($i=0; $i<$hauteur; $i++) {
	for ($j=0; $j<$largeur; $j++) {
		// convert raster to Cartesian
        $x = $j - $largeur*0.5;
        $y = $hauteur*0.5 - $i;

        // convert Cartesian to polar
        $fDistance = sqrt($x * $x + $y * $y);
   	 	$fPolarAngle = atan2($y, $x);

        // the crucial rotation part
        // "reverse" rotate, so minus instead of plus
        $fPolarAngle -= $angleRot;
		 // convert polar to Cartesian
        $fTrueX = $fDistance * cos($fPolarAngle);
        $fTrueY = $fDistance * sin($fPolarAngle);

        // convert Cartesian to raster
        $fTrueX = $fTrueX + $size[0]*0.5;
        $fTrueY = $size[1]*0.5 - $fTrueY;

        $iFloorX = (int)(floor($fTrueX));
        $iFloorY = (int)(floor($fTrueY));
        $iCeilingX = (int)(ceil($fTrueX));
        $iCeilingY = (int)(ceil($fTrueY));
        //echo $fTrueX." ".$fTrueY." ".$iFloorX." ".$iCeilingX." ".$iFloorY." ".$iCeilingY."<br>";
		if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {
			$fDeltaX = $fTrueX - $iFloorX;
			$fDeltaY = $fTrueY - $iFloorY;
			$clrTopLeft = imagecolorat($img, $iFloorX, $iFloorY);
			$colorsTopLeft = imagecolorsforindex($img, $clrTopLeft);
			$clrTopRight = imagecolorat($img, $iCeilingX, $iFloorY); 
			$colorsTopRight = imagecolorsforindex($img, $clrTopRight);
			$clrBottomLeft = imagecolorat($img, $iFloorX, $iCeilingY);
			$colorsBottomLeft = imagecolorsforindex($img, $clrBottomLeft);
			$clrBottomRight = imagecolorat($img, $iCeilingX, $iCeilingY);
			$colorsBottomRight = imagecolorsforindex($img, $clrBottomRight);
			// linearly interpolate horizontally between top neighbours
			$fTopRed = (1 - $fDeltaX) * $colorsTopLeft['red'] + $fDeltaX * $colorsTopRight['red'];
			$fTopGreen = (1 - $fDeltaX) * $colorsTopLeft['green'] + $fDeltaX * $colorsTopRight['green'];
			$fTopBlue = (1 - $fDeltaX) * $colorsTopLeft['blue'] + $fDeltaX * $colorsTopRight['blue'];
			// linearly interpolate horizontally between bottom neighbours
			$fBottomRed = (1 - $fDeltaX) * $colorsBottomLeft['red'] + $fDeltaX * $colorsBottomRight['red'];
			$fBottomGreen = (1 - $fDeltaX) * $colorsBottomLeft['green'] + $fDeltaX * $colorsBottomRight['green'];
			$fBottomBlue = (1 - $fDeltaX) * $colorsBottomLeft['blue'] + $fDeltaX * $colorsBottomRight['blue'];
			// linearly interpolate vertically between top and bottom interpolated results
			$iRed = (int)(round((1 - $fDeltaY) * $fTopRed + $fDeltaY * $fBottomRed));
			$iGreen = (int)(round((1 - $fDeltaY) * $fTopGreen + $fDeltaY * $fBottomGreen));
			$iBlue = (int)(round((1 - $fDeltaY) * $fTopBlue + $fDeltaY * $fBottomBlue));
			// make sure colour values are valid
			if ($iRed < 0) $iRed = 0;
			if ($iRed > 255) $iRed = 255;
			if ($iGreen < 0) $iGreen = 0;
			if ($iGreen > 255) $iGreen = 255;
			if ($iBlue < 0) $iBlue = 0;
			if ($iBlue > 255) $iBlue = 255;
			if ($iFloorX > 0 && $iCeilingX > 0 && $iFloorX < $size[0]-1 && $iCeilingX < $size[0]-1 && $iFloorY > 0 && $iCeilingY > 0 && $iFloorY < $size[1]-1 && $iCeilingY < $size[1]-1) {
				$colorallocation=imagecolorallocate($imgRot, $iRed, $iGreen, $iBlue);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX == 0 && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {//left
				$alpha=round((1-abs($fDeltaX))*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iFloorY == 0) {//top
				$alpha=round((1-abs($fDeltaY))*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iCeilingX == $size[0]-1 && $iFloorY >= 0 && $iCeilingY >= 0 && $iFloorY < $size[1] && $iCeilingY < $size[1]) {//right
				$alpha=round(abs($fDeltaX)*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			} else if ($iFloorX >= 0 && $iCeilingX >= 0 && $iFloorX < $size[0] && $iCeilingX < $size[0] && $iCeilingY == $size[1]-1) {//bottom
				$alpha=round(abs($fDeltaY)*127);
				$colorallocation=imagecolorallocatealpha($imgRot, $iRed, $iGreen, $iBlue, $alpha);
				imagesetpixel($imgRot, $j, $i, $colorallocation);
			}
		}
	}
}

Fabien is French (I think), which is why you get variable names such as distOmbre (shadow distance?), flouOmbre (fuzzy shadow?), largeur (width), hauteur (height). And this one took me a bit more time to translate… LsupH is probably “width greater than height?”. The “L” probably refers to “largeur”, and “H” refers to “hauteur”.

Reading international programming code is fun. *smile*

There’s also an interesting piece of code:

$size=getimagesize("media/diapo-Chinon.jpg");
$LsupH=($size[0]>$size[1])?1:0;
$angleBool=(int)($angleRot/90)%2==0?0:1;
if (($angleBool+$LsupH)%2==0) {
	$largeur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
} else {
	$largeur=round(abs($size[0]*cos($angleRot%90*pi()/180))+abs($size[1]*cos((90-$angleRot%90)*pi()/180)));
	$hauteur=round(abs($size[0]*sin($angleRot%90*pi()/180))+abs($size[1]*sin((90-$angleRot%90)*pi()/180)));
}

So here’s your mission, should you choose to accept it (I recently watched Mission Impossible…). What is Fabien trying to accomplish in that section of code? Hint: it has something to do with getting a “nice” resulting image width and height.

I’ll tell you a more “elegant” alternative to that code section. But it’ll involve some mathematics. And drawings. Prepare for poorly drawn diagrams…