<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>tlrobinson.net / blog &#187; Uncategorized</title>
	<atom:link href="http://tlrobinson.net/blog/category/uncategorized/feed/" rel="self" type="application/rss+xml" />
	<link>http://tlrobinson.net/blog</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2009 08:37:15 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.8.4</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<cloud domain='tlrobinson.net' port='80' path='/blog/?rsscloud=notify' registerProcedure='' protocol='http-post' />
		<item>
		<title>Using OLPC XO as an ebook reader for O&#8217;Reilly&#8217;s Safari Books Online</title>
		<link>http://tlrobinson.net/blog/2009/04/06/using-olpc-xo-as-an-ebook-reader-for-oreillys-safari-books-online/</link>
		<comments>http://tlrobinson.net/blog/2009/04/06/using-olpc-xo-as-an-ebook-reader-for-oreillys-safari-books-online/#comments</comments>
		<pubDate>Mon, 06 Apr 2009 08:37:15 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Hacks]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[XO]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=101</guid>
		<description><![CDATA[A year ago I received an OLPC XO (the &#8220;$100 laptop&#8221;) through their Give One Get One program. I played with it for a few days and found it essentially useless due to unstable and slow software (and lack of WPA support), so it quickly began gathering dust on a shelf (it has since improved).

Last [...]]]></description>
			<content:encoded><![CDATA[<p>A year ago I received an <a href="http://laptop.org/">OLPC</a> XO (the &#8220;$100 laptop&#8221;) through their Give One Get One program. I played with it for a few days and found it essentially useless due to unstable and slow software (and lack of WPA support), so it quickly began gathering dust on a shelf (it has since improved).</p>

<p>Last week I was thinking about how <a href="http://www.amazon.com/Kindle-Safari-Online-Access-Support/forum/FxBVKST06PWP9B/Tx13VFZZSWRC994/1?_encoding=UTF8&#038;asin=B000FI73MA">cool it would be</a> if <a href="http://amazon.com/kindle">Amazon&#8217;s Kindle</a> supported <a href="http://www.safaribooksonline.com/">O&#8217;Reilly&#8217;s Safari Books Online</a> service, and I decided to dust off the XO to see if it could be used as an ebook reader for Safari Books. With a little help, it can.</p>

<p>In ebook mode you can scroll in all four directions, page up/down, and jump to the top or bottom of a page, but you cannot click the next/previous buttons within Safari Books. However, GreaseMonkey and a simple userscript can solve that.</p>

<p>The first step is to install the <a href="http://wiki.laptop.org/go/Activities/All#General_Search_and_Discovery">Firefox &#8220;Activity&#8221;</a>, or a <a href="http://www.olpcnews.com/forum/index.php?topic=4053.0l">version of Linux</a> that runs a stock Firefox. Then install <a href="https://addons.mozilla.org/en-US/firefox/addon/748">GreaseMonkey</a>. Finally, install this userscript:</p>

<p><a href="http://tlrobinson.net/userscripts/xo-safari.user.js">http://tlrobinson.net/userscripts/xo-safari.user.js</a></p>

<p>This simple userscript intercepts page up and page down (the &#8220;O&#8221; and &#8220;X&#8221; game pad) buttons and maps them to &#8220;previous&#8221; and &#8220;next&#8221; actions in Safari Books, allowing you to easily switch pages in ebook mode.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2009/04/06/using-olpc-xo-as-an-ebook-reader-for-oreillys-safari-books-online/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>ropen: Remote &#8220;open&#8221; command for opening remote files locally on OS X</title>
		<link>http://tlrobinson.net/blog/2009/04/04/ropen-remote-open-command/</link>
		<comments>http://tlrobinson.net/blog/2009/04/04/ropen-remote-open-command/#comments</comments>
		<pubDate>Sun, 05 Apr 2009 04:40:15 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Command line]]></category>
		<category><![CDATA[Hacks]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Mac]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=94</guid>
		<description><![CDATA[The Problem

Most Mac OS X power users know about the &#8220;open&#8221; command line tool which opens the files specified as arguments in their default (or a specified) OS X application. Additionally, many OS X text editors, such as TextMate (&#8221;mate&#8221;) and SubEthaEdit (&#8221;see&#8221;), come with command line tools which can be used to open files.

These [...]]]></description>
			<content:encoded><![CDATA[<h2>The Problem</h2>

<p>Most Mac OS X power users know about the <a href="http://tuvix.apple.com/documentation/Darwin/Reference/ManPages/man1/open.1.html">&#8220;open&#8221;</a> command line tool which opens the files specified as arguments in their default (or a specified) OS X application. Additionally, many OS X text editors, such as TextMate (&#8221;mate&#8221;) and SubEthaEdit (&#8221;see&#8221;), come with command line tools which can be used to open files.</p>

<p>These are great when working locally, but obviously do no work remotely. Often when working on remote servers you end up using command line editors which you may not be as familiar with.</p>

<h2>ropen&#8217;s Solution</h2>

<p>The <a href="http://github.com/tlrobinson/ropen">ropen</a> tool solves this problem using two simple shell scripts, which make use of MacFuse&#8217;s sshfs. You run the &#8220;ropen&#8221; program on your remote machine(s) when you want to open a remote file locally (this is equivalent to the OS X &#8220;open&#8221; command). The &#8220;ropend&#8221; daemon runs on your local OS X machine waiting for open requests, and the &#8220;ropen.php&#8221; PHP script proxies requests from ropen to ropend.</p>

<h2>How it works</h2>

<ol>
<li>When ropen is executed it makes an HTTP request to ropen.php with the paths to be opened and application to open them with, if any, as well as the SSH user, host, and port of the remote machine.</li>
<li>ropen.php stores this open request in a queue that is tied to ROPEN_SECRET via PHP&#8217;s sessions.</li>
<li>ropend polls ropen.php every 1 second waiting for open requests. When it receives one it mounts the remote filesystem using sshfs (if it&#8217;s not already mounted) and opens the files or directories specified.</li>
</ol>

<h2>More information</h2>

<p>See more information about ropen on the <a href="http://github.com/tlrobinson/ropen">ropen project page</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2009/04/04/ropen-remote-open-command/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Automagically Wrapping JavaScript Callback Functions</title>
		<link>http://tlrobinson.net/blog/2008/10/22/wrapping-javascript-callbacks/</link>
		<comments>http://tlrobinson.net/blog/2008/10/22/wrapping-javascript-callbacks/#comments</comments>
		<pubDate>Wed, 22 Oct 2008 11:43:30 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Hacks]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Web]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=59</guid>
		<description><![CDATA[One very nice thing about JavaScript is it&#8217;s support for first-class functions and closures. Crockford calls JavaScript &#8220;Lisp in C&#8217;s Clothing&#8221;. I&#8217;m no Lisper, but I enjoy I discovering new tricks or applications of functional programming in JavaScript.

I wanted to hook all the browser&#8217;s asynchronous JavaScript &#8220;entry points&#8221; : events, timers, asynchronous XMLHttpRequests, script tags, [...]]]></description>
			<content:encoded><![CDATA[<p>One very nice thing about JavaScript is it&#8217;s support for first-class functions and closures. Crockford calls JavaScript <a href="http://www.crockford.com/javascript/javascript.html">&#8220;Lisp in C&#8217;s Clothing&#8221;</a>. I&#8217;m no Lisper, but I enjoy I discovering new tricks or applications of functional programming in JavaScript.</p>

<p>I wanted to hook all the browser&#8217;s asynchronous JavaScript &#8220;entry points&#8221; : events, timers, asynchronous XMLHttpRequests, script tags, and &#8220;javascript:&#8221; URLs. This article deals with the first two, events and timers.</p>

<p>To do this, I figured I could somehow &#8220;wrap&#8221; all the callback functions passed to setTimeout(), setInterval(), and addEventListener(). By &#8220;wrapped&#8221; I mean another function that <em>we specify</em> calls the original function, rather than the original function getting called directly. This allows us do whatever we want right before and after calling the original function, including manipulating the arguments and return value, logging to the console, calling other functions, putting it in a try/catch, etc.</p>

<p>Here&#8217;s the implementation of &#8220;callbackWrap&#8221;, the function that does all the work:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">callbackWrap</span>(object, property, argumentIndex, wrapperFactory, extra) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> original = object[property];<br />
&nbsp;&nbsp;&nbsp;&nbsp;object[property] = <span style="color:#881350;">function</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;arguments[argumentIndex] = <span style="color:#003369;">wrapperFactory</span>(arguments[argumentIndex], extra);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> original.<span style="color:#003369;">apply</span>(<span style="color:#881350;">this</span>, arguments);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> original;<br />
}</div>

<p>To use it, you need function that takes another function as a parameter and returns a wrapped version of that function which does whatever you want it to. In this case it just prints out &#8220;whoooaaaaa:&#8221; followed by the &#8220;extra&#8221; parameter:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">logWrapper</span>(func, extra) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#881350;">function</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;console.<span style="color:#003369;">log</span>(<span style="color:#760f15;">&quot;whoooaaaaa: &quot;</span> + extra);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> func.<span style="color:#003369;">apply</span>(<span style="color:#881350;">this</span>, arguments);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}</div>

<p>Finally, to actually use this, we call callbackWrap with the object that contains the function we want to wrap, the name of the function property, the index of the callback argument to the function (0 for setTimeout/setInterval, 1 for addEventListener), a wrapper &#8220;factory&#8221; function, and optionally an extra argument that&#8217;s made available (via the closure) to the wrapped function. The extra parameter can be used for any data you need to pass to the wrapper function, or it can be ignored.</p>

<p>Here&#8217;s how this would be used on setTimeout, setInterval, and addEventListener on window, Element.prototype and Document.prototype (so it applies to all Elements and Documents):</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#003369;">callbackWrap</span>(window, <span style="color:#760f15;">&quot;setTimeout&quot;</span>, <span style="color:#0000ff;">0</span>, logWrapper, <span style="color:#760f15;">&quot;wrapped a window.setTimeout!&quot;</span>);<br />
<span style="color:#003369;">callbackWrap</span>(window, <span style="color:#760f15;">&quot;setInterval&quot;</span>, <span style="color:#0000ff;">0</span>, logWrapper, <span style="color:#760f15;">&quot;wrapped a window.setInterval!&quot;</span>);<br />
<span style="color:#003369;">callbackWrap</span>(window, <span style="color:#760f15;">&quot;addEventListener&quot;</span>, <span style="color:#0000ff;">1</span>, logWrapper, <span style="color:#760f15;">&quot;wrapped a window.addEventListener!&quot;</span>);<br />
<span style="color:#003369;">callbackWrap</span>(Element.prototype, <span style="color:#760f15;">&quot;addEventListener&quot;</span>, <span style="color:#0000ff;">1</span>, logWrapper, <span style="color:#760f15;">&quot;wrapped a Element.addEventListener!&quot;</span>);<br />
<span style="color:#003369;">callbackWrap</span>(Document.prototype, <span style="color:#760f15;">&quot;addEventListener&quot;</span>, <span style="color:#0000ff;">1</span>, logWrapper, <span style="color:#760f15;">&quot;wrapped a Document.addEventListener!&quot;</span>);</div>

<p><a href="http://tlrobinson.net/misc/wrap.html">Here&#8217;s a live example.</a> <em>(edit: the button example seems to be broken in Firefox)</em></p>

<p>The first thing callbackWrap does is save the original function (window.setTimeout, for this example) in a local variable, &#8220;original&#8221;, since we&#8217;ll need it later. We then replace the original with a new function. When this new function is called (the new window.setTimeout), we first call the &#8220;wrapperMaker&#8221; function (&#8221;logWrapper&#8221; in the example), passing it the callback function, which is the argument at the argumentIndex position, and the optional &#8220;extra&#8221; argument. That function returns a new function which replaces the original callback argument, before we then call the original function (saved in the local variable &#8220;original&#8221;) with the same arguments (except the callback is now wrapped).</p>

<p>So really this blog post should have been called <em>&#8220;a function that wraps other functions such that all callback functions passed to it are wrapped by yet another function&#8221;</em>. There&#8217;s <em>two</em> levels of wrapping going on: the original function is wrapped, such that <em>it</em> wraps every <em>callback</em> given to it. If you&#8217;re confused I don&#8217;t blame you.</p>

<p>Why would I want to do this, you ask? There are a couple scenarios I had in mind, and I&#8217;m sure you can think of others.</p>

<p>First, I wanted to catch all uncaught exceptions for a debugging tool I&#8217;m working on. Sure, you can assign an error handler to the &#8220;onerror&#8221; property of the window object, but this only gives you the error name, file name, and line number – I wanted the full exception object to be caught by a try/catch.</p>

<p>Here&#8217;s a wrapper that catches all uncaught exceptions and alerts them:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">exceptionWrapper</span>(func, extra) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#881350;">function</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">try</span> {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> func.<span style="color:#003369;">apply</span>(<span style="color:#881350;">this</span>, arguments);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <span style="color:#881350;">catch</span><span style="color:#003369;"> </span>(e) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">alert</span>(e);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}</div>

<p>Second, in <a href="http://cappuccino.org">Cappuccino</a> we mimic Cocoa&#8217;s concept of a &#8220;run loop&#8221;, but due to the way browsers work the implementation is a bit different. A consequence of this was that we couldn&#8217;t automatically call &#8220;[[CPRunLoop currentRunLoop] performSelectors];&#8221; at the end of each run loop, which is required for various pieces of Cappuccino. If you&#8217;ve ever come across a situation where you perform some action but the UI doesn&#8217;t update until you move the mouse, it&#8217;s probably because performSelectors isn&#8217;t being called. This isn&#8217;t common, since Cappuccino encapsulates events, XMLHttpRequests, and now timers with CPEvent, CPURLConnection, and CPTimer, respectively. These Cappuccino classes handle calling performSelectors for you. However, if you wanted to integrate with a third party library or use setTimeout, XMLHttpRequest, etc directly, currently you would need to call performSelectors manually. With this new trick we can call it automagically.</p>

<p>Here&#8217;s the wrapper that does exactly that (note the inline Objective-J call to &#8220;[[CPRunLoop currentRunLoop] performSelectors]&#8221;):</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">performSelectorsWrapper</span>(func, extra) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#881350;">function</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> result = func.<span style="color:#003369;">apply</span>(<span style="color:#881350;">this</span>, arguments);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> [[CPRunLoop currentRunLoop] performSelectors];<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}</div>

<p>One minor feature I didn&#8217;t demonstrate was that callbackWrap returns the original function that it wrapped. If you need the original for any reason you can save it to some other variable.</p>

<p>callbackWrap could use a few improvements. Currently this breaks on cases where you pass a string instead of a function. A simple check for the argument type, and appropriate handling would solve this (either skipping the wrapping, or compiling the the string with a &#8220;new Function()&#8221; call). But using strings instead of functions is considered poor practice anyway.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/10/22/wrapping-javascript-callbacks/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Recovering Censored Text Using Photoshop and JavaScript</title>
		<link>http://tlrobinson.net/blog/2008/10/08/recovering-censored-text-using-adobe-photoshop-cs3/</link>
		<comments>http://tlrobinson.net/blog/2008/10/08/recovering-censored-text-using-adobe-photoshop-cs3/#comments</comments>
		<pubDate>Wed, 08 Oct 2008 11:45:09 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Hacks]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=52</guid>
		<description><![CDATA[My friend Andrew recently posted a teaser for a new project he&#8217;s working on, but with part of the headline pixelated to obscure what the project actually is. My curiosity got the best of me and I decided to do what any self-respecting geek would do: write a program to figure out what the censored [...]]]></description>
			<content:encoded><![CDATA[<p>My friend Andrew recently posted a <a href="http://tmblg.com/post/53529111/its-coming">teaser</a> for a new project he&#8217;s working on, but with part of the headline pixelated to obscure what the project actually is. My curiosity got the best of me and I decided to do what any self-respecting geek would do: write a program to figure out what the censored text said.</p>

<p>Ultimately I failed to recover most of the censored text (except &#8220;to&#8221;), so I had to cheat a little. The following video is the program running on a very similar image I created. This proves it works in ideal conditions, but needs some improvement to work in less than ideal cases.</p>

<p><object width="400" height="300">   <param name="allowfullscreen" value="true" />   <param name="allowscriptaccess" value="always" />   <param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1913931&amp;server=vimeo.com&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;fullscreen=1" />   <embed src="http://vimeo.com/moogaloop.swf?clip_id=1913931&amp;server=vimeo.com&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;fullscreen=1" type="application/x-shockwave-flash" allowfullscreen="true" allowscriptaccess="always" width="400" height="300"></embed></object><br /></p>

<p>(and no, as far as I know my friend&#8217;s project has nothing to do with eating monkeys)</p>

<p>Applying a filter like Photoshop&#8217;s &#8220;mosaic&#8221; filter obscures the original data, but doesn&#8217;t remove it entirely. If we can reconstruct an image with <em>known</em> text that looks very similar to original image, then we can be pretty sure the original text is the same as our known text. This is very similar in principle to brute-force cracking a password hash. For a more detailed explanation <a href="http://dheera.net/projects/blur.php">see this article</a>.</p>

<p>Photoshop was an obvious choice since I needed to recreate the exact same fancy styling as the original image, then apply the exact same mosaic filter. I figured I would have to write a script that tells Photoshop to generate images, then use an external tool to actually compare them to the original.</p>

<p>It turns out that Photoshop CS3 has all the features necessary to pull the whole thing off without any other programs or tools. The most important feature is the JavaScript scripting environment built into Photoshop, which is far more powerful than the AppleScript environment (and a <em>much</em> nicer language, in my opinion).</p>

<p>CS3 added two other features that are critical to this task: Smart Filters, and Measurements. Smart Filters lets you edit a layer (namely the text with effects applied) <em>after</em> you apply a filter that would have previously require rasterization. This lets us apply the censoring filter to our styled text, and later change the text without having to manually reapply the filter. The &#8220;measurements&#8221; feature lets you record various statistics about an image or portion of an image: in our case we&#8217;ll want the &#8220;average gray value&#8221; of the &#8220;difference&#8221; between the original and generated images.</p>

<p><a href='http://tlrobinson.net/blog/wp-content/uploads/2008/10/picture-10.png' title='picture-10.png'><img src='http://tlrobinson.net/blog/wp-content/uploads/2008/10/picture-10.png' alt='picture-10.png' /></a></p>

<p>First we need to prepare the environment. Open the original image in Photoshop, and attempt to replicate the original un-censored text as closely as possible (you need <em>some</em> uncensored text as a reference). Place your text layer on top of the original and toggle between normal and &#8220;difference&#8221; blending modes to see how you close you are. Ideally everything will be black in &#8220;difference&#8221; mode. It&#8217;s very important to precisely match the font, size, spacing, color, effects like drop shadows or outlines, and even the background. If these are off even by a little bit it will throw things off. I ended up having to cheat because I couldn&#8217;t match the slick styling of the original text with my lame Photoshop design skills.</p>

<p>Once the text matches and is lined up perfectly, select the layer then choose &#8220;Convert for Smart Filters&#8221; from the &#8220;Filter&#8221; menu. Now select the censored portion of the text and apply the same filter used on the original image, again matching it as closely as possible. For the mosaic filter, you can line up the &#8220;grid&#8221; by adjusting the origin and size of the selection (yeah, it&#8217;s a pain).</p>

<p><a href='http://tlrobinson.net/blog/wp-content/uploads/2008/10/picture-11.png' title='picture-11.png'><img src='http://tlrobinson.net/blog/wp-content/uploads/2008/10/picture-11.png' alt='picture-11.png' /></a></p>

<p>Finally, make sure your layer is on top of the original, and the blending mode on your layer is set to &#8220;difference&#8221;. Double-click the Smart Object layer to open it&#8217;s source document, and adjust the variables listed at the top of the JavaScript to match the names and layers. Also, in the menu &#8220;Analysis&#8221;: &#8220;Select Data Points&#8221;: &#8220;Custom&#8230;&#8221; make sure only &#8220;Gray Value (Mean)&#8221; is checked.</p>

<h3>Code</h3>

<p>Rather than attempting to explain it in detail here, just read the code and comments. Here&#8217;s a quick summary:</p>

<ol>
<li>Start with the first character. Try setting it to each of the possibilities (a through z, and a space), and record the difference score between the original image and generated image. Only look at the first half of the current character (since the second half will be influenced by the *next* character).</li>
<li>Sort the results. Lower scores are better (less different)</li>
<li>Now try each of the top 3 characters along with every possibility for the *next* character. This time record score for the whole width of the current character since we&#8217;re checking the next character as well.</li>
<li>Pick the best choice, either the best permutation out of all 81 combinations (3 best * 27 possible), or out of the 3 averages for each best.</li>
<li>Repeat for the next character until done.</li>
</ol>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#236e25;">// change these parameters based on document names and layer ordering<br />
</span>baseDocName = <span style="color:#760f15;">&quot;base.psd&quot;</span>;<br />
baseDocTextLayer = <span style="color:#0000ff;">0</span>;<br />
textDocName = <span style="color:#760f15;">&quot;The easy way to do somethingss12.psb&quot;</span>;<br />
textDocTextLayer = <span style="color:#0000ff;">0</span>;<br />
<br />
knownString = <span style="color:#760f15;">&quot;The easy way &quot;</span>; <span style="color:#236e25;">// the part of the string that&#8217;s already known<br />
</span>missingLength = <span style="color:#0000ff;">20</span>; <span style="color:#236e25;">// number of characters to figure out<br />
</span><br />
method = <span style="color:#0000ff;">3</span>;<br />
debug = <span style="color:#881350;">false</span>;<br />
<br />
<span style="color:#881350;">function</span> <span style="color:#003369;">main</span>()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;baseDoc = documents[baseDocName];<br />
&nbsp;&nbsp;&nbsp;&nbsp;textDoc = documents[textDocName];<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// get the top left corner of the text layer in the main doc<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> mainBounds = baseDoc.artLayers[baseDocTextLayer].bounds,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mainX = mainBounds[<span style="color:#0000ff;">0</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;mainY = mainBounds[<span style="color:#0000ff;">1</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// possible characters include space and lowercase.<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> possibleCharacters = [<span style="color:#760f15;">&quot; &quot;</span>];<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> i = <span style="color:#0000ff;">0</span>; i &lt; <span style="color:#0000ff;">26</span>; i++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;possibleCharacters.<span style="color:#003369;">push</span>(String.<span style="color:#003369;">fromCharCode</span>(<span style="color:#760f15;">&quot;a&quot;</span>.<span style="color:#003369;">charCodeAt</span>(<span style="color:#0000ff;">0</span>) + i));<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">//possibleCharacters.push(String.fromCharCode(&quot;A&quot;.charCodeAt(0) + i)); // uncomment for uppercase letters<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;}<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> fudgeFactor = <span style="color:#0000ff;">3</span>, &nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// number of top choices to try<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess = <span style="color:#760f15;">&quot;&quot;</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// guessed letters so far<br />
</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> charNum = <span style="color:#0000ff;">0</span>; charNum &lt; missingLength; charNum++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;results = [];<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// get the beginning and potential end (width of a &quot;M&quot;) of the next character<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> w1 = <span style="color:#003369;">getStringBounds</span>(knownString + guess),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;w2 = <span style="color:#003369;">getStringBounds</span>(knownString + guess + <span style="color:#760f15;">&quot;M&quot;</span>);<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// PASS 1: half the potential width, since we&#8217;re not looking at the next character yet<br />
</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// half the width of &quot;M&quot;<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">setSelection</span>(mainX, mainY, (w1[<span style="color:#0000ff;">2</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>) + w2[<span style="color:#0000ff;">2</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>)) <span style="color:#760f15;">/ 2, 15);/</span>/w2[<span style="color:#0000ff;">3</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>));<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// get the score for every letter<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> i = <span style="color:#0000ff;">0</span>; i &lt; possibleCharacters.length; i++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> val = <span style="color:#003369;">getStringScore</span>(knownString + guess + possibleCharacters[i])<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> res = { ch: possibleCharacters[i], v: val };<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;results.<span style="color:#003369;">push</span>(res);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// sort from best (lowest) to worst score<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;results = results.<span style="color:#003369;">sort</span>(<span style="color:#881350;">function</span><span style="color:#003369;"> </span>(a,b) { <span style="color:#881350;">return</span> a.v &#8211; b.v; });<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// method 1: too simple, poor results<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(method == <span style="color:#0000ff;">1</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess += results[<span style="color:#0000ff;">0</span>].ch;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">else</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// PASS 2: full (potential) width of the current character, testing each of the few top matches and every possible next character<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// full width of &quot;M&quot;<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">setSelection</span>(mainX, mainY, w2[<span style="color:#0000ff;">2</span>].<span style="color:#003369;">as</span>(<span style="color:#760f15;">&quot;px&quot;</span>), <span style="color:#0000ff;">15</span>);<span style="color:#236e25;">//w2[3].as(&quot;px&quot;));<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> minValue = Number.MAX_VALUE,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minChar = null,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minSum = Number.MAX_VALUE,<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minSumChar = null;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// try the few best from the first pass<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> i = <span style="color:#0000ff;">0</span>; i &lt; fudgeFactor; i++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> sum = <span style="color:#0000ff;">0</span>;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> j = <span style="color:#0000ff;">0</span>; j &lt; possibleCharacters.length; j++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// get the score for the potential best PLUS each possible next character<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> val = <span style="color:#003369;">getStringScore</span>(knownString + guess + results[i].ch + possibleCharacters[j])<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sum += val;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(val &lt; minValue)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minValue = val;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minChar = results[i].ch;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(sum &lt; minSum)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minSum = sum;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minSumChar = results[i].ch;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// if the results aren&#8217;t consistent let us know<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(debug &amp;&amp; results[<span style="color:#0000ff;">0</span>].ch != minSumChar || minChar != minSumChar)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">alert</span>(minChar + <span style="color:#760f15;">&quot;,&quot;</span> + minSumChar + <span style="color:#760f15;">&quot; (&quot;</span> +results[<span style="color:#0000ff;">0</span>].ch + <span style="color:#760f15;">&quot;,&quot;</span> + results[<span style="color:#0000ff;">1</span>].ch+ <span style="color:#760f15;">&quot;,&quot;</span> + results[<span style="color:#0000ff;">2</span>].ch+ <span style="color:#760f15;">&quot;)&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(method == <span style="color:#0000ff;">2</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// method 2: best of all permutations<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess += minChar;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">else</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// method 3: best average <br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;guess += minSumChar;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">WaitForRedraw</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}<br />
<br />
<span style="color:#236e25;">// measure the gray value mean in the current selection<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">getMeasurement</span>()<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// delete existing measurements<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.measurementLog.<span style="color:#003369;">deleteMeasurements</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// record new measurement<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument = baseDoc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.<span style="color:#003369;">recordMeasurements</span>();<span style="color:#236e25;">//MeasurementSource.MEASURESELECTION, [&quot;GrayValueMean&quot;]);<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// export measurements to a file<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> f = <span style="color:#881350;">new</span> <span style="color:#003369;">File </span>(<span style="color:#760f15;">&quot;/tmp/crack-tmp-file.txt&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.measurementLog.<span style="color:#003369;">exportMeasurements</span>(f);<span style="color:#236e25;">//, MeasurementRange.ACTIVEMEASUREMENTS, [&quot;GrayValueMean&quot;]);<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// open the file, read, and parse<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;f.<span style="color:#003369;">open</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> line = f.<span style="color:#003369;">read</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> matches = line.<span style="color:#003369;">match</span>(<span style="color:#760f15;">/[0-9]+(\.[0-9]+)?/</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span><span style="color:#003369;"> </span>(matches)<br />
&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> val = <span style="color:#003369;">parseFloat</span>(matches[<span style="color:#0000ff;">0</span>]);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> val;<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> null;<br />
}<br />
<br />
<span style="color:#236e25;">// sets the value of the test string<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">setString</span>(string)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument = textDoc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.artLayers[textDocTextLayer].textItem.contents = string;<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">WaitForRedraw</span>();<br />
}<br />
<br />
<span style="color:#236e25;">// gets the difference between the original and test strings in the currently selected area <br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">getStringScore</span>(string)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">setString</span>(string);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// save document to propagate changes parent of smart object<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument = textDoc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.<span style="color:#003369;">save</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// return the average gray value<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#003369;">getMeasurement</span>();<br />
}<br />
<br />
<span style="color:#236e25;">// get the bounds of the text<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">getStringBounds</span>(string)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument = textDoc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// set the string of the text document<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">setString</span>(string);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// select top left pixel. change this if it&#8217;s not empty<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.selection.<span style="color:#003369;">select</span>([[<span style="color:#0000ff;">0</span>,<span style="color:#0000ff;">0</span>], [<span style="color:#0000ff;">0</span>,<span style="color:#0000ff;">1</span>], [<span style="color:#0000ff;">1</span>,<span style="color:#0000ff;">1</span>], [<span style="color:#0000ff;">1</span>,<span style="color:#0000ff;">0</span>]]);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// select similar pixels (i.e. everything that&#8217;s not text)<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.selection.<span style="color:#003369;">similar</span>(<span style="color:#0000ff;">1</span>, <span style="color:#881350;">false</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// invert selection to get just the text<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.selection.<span style="color:#003369;">invert</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// return the bounds of the resulting selection<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> app.activeDocument.selection.bounds;<br />
}<br />
<br />
<span style="color:#236e25;">// sets the base document&#8217;s selection to the given rectange<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">setSelection</span>(x, y, w, h)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument = baseDoc;<br />
&nbsp;&nbsp;&nbsp;&nbsp;app.activeDocument.selection.<span style="color:#003369;">select</span>([[x,y], [x,y+h], [x+w,y+h], [x+w,y]]);<br />
}<br />
<br />
<span style="color:#236e25;">// pauses for Photoshop to redraw. taken from reference docs.<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">WaitForRedraw</span>() <br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;">// return; // uncomment for slight speed boost<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> eventWait = <span style="color:#003369;">charIDToTypeID</span>(<span style="color:#760f15;">&quot;Wait&quot;</span>) <br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> enumRedrawComplete = <span style="color:#003369;">charIDToTypeID</span>(<span style="color:#760f15;">&quot;RdCm&quot;</span>) <br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> typeState = <span style="color:#003369;">charIDToTypeID</span>(<span style="color:#760f15;">&quot;Stte&quot;</span>) <br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> keyState = <span style="color:#003369;">charIDToTypeID</span>(<span style="color:#760f15;">&quot;Stte&quot;</span>) <br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> desc = <span style="color:#881350;">new</span> <span style="color:#003369;">ActionDescriptor</span>() <br />
&nbsp;&nbsp;&nbsp;&nbsp;desc.<span style="color:#003369;">putEnumerated</span>(keyState, typeState, enumRedrawComplete) <br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">executeAction</span>(eventWait, desc, DialogModes.NO) <br />
}<br />
<br />
<span style="color:#003369;">main</span>();<br />
</div>

<p>The raw code and sample Photoshop file are <a href="http://github.com/tlrobinson/tlrobinson/tree/master/recover/">available on GitHub</a>.</p>

<h3>Issues</h3>

<p>This problem is particularly tricky for proportional fonts, since if you get any character wrong and it&#8217;s width is different than the actual character, then all subsequent characters will be misaligned, causing more incorrect guesses, compounding the problem even more, and so on. I&#8217;m not sure how to deal with this, other than improving the overall matching quality. Ideally we would test every possible combination for the entire string, but that would require 27^n tests, where n is the number of unknown characters. This is obviously not feasible.</p>

<p>With the simplistic method of iterating over each position and trying each possible character, it turned out that almost every single &#8220;guess&#8221; was for the letters &#8220;m&#8221; or &#8220;w&#8221;. This was because for positions where the original was narrower characters, the &#8220;m&#8221; would &#8220;bleed&#8221; over into the <em>next</em> position, improving the score regardless of how well it actually matched the current character. To get around this, we only look at the difference for the first <em>half</em> of the character&#8217;s position.</p>

<p>Since looking at the first half of the character removes some valuable information, we then do a second pass using the top several guesses from the first pass, this time looking at the full width of the current character along with each of the possible next characters (27 tests + 3 runs times 27 tests results in 108 tests per character).</p>

<p>Further improvements could definitely be made, but I&#8217;ve already spent several hours too many on this.</p>

<p>The current algorithm runs at about 3 characters per minute. The overhead of Photoshop saving the Smart Object document on every individual test case is significant. If this were a special purpose program manipulating images directly it would likely be much faster. The tradeoff, of course, is you have all of Photoshop&#8217;s flexibility at your disposal for matching the original document&#8217;s font, size, style, spacing, and censoring effects, which is very important. For small amounts of text speed isn&#8217;t a problem.</p>

<h3>Conclusion</h3>

<p>While my original goal of recovering the censored text on my friend&#8217;s page was never achieved, the project was a success. It works well on my test image, and I learned about 3 obscure but cool and useful features of Photoshop!</p>

<p>Oh, and <em>that&#8217;s</em> why &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588; uses black ink to &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588; their &#x2588;&#x2588;&#x2588;&#x2588;&#x2588;&#x2588;!</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/10/08/recovering-censored-text-using-adobe-photoshop-cs3/feed/</wfw:commentRss>
		<slash:comments>48</slash:comments>
		</item>
		<item>
		<title>A simple Google Charts API &#8220;framework&#8221;</title>
		<link>http://tlrobinson.net/blog/2008/09/21/a-simple-google-charts-api-framework/</link>
		<comments>http://tlrobinson.net/blog/2008/09/21/a-simple-google-charts-api-framework/#comments</comments>
		<pubDate>Sun, 21 Sep 2008 12:10:13 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=49</guid>
		<description><![CDATA[The Google Charts API is a useful little tool for generating charts. The &#8220;API&#8221; is actually just a set of parameters you pass to a single URL endpoint: http://chart.apis.google.com/chart



It&#8217;s a very capable API, and you could write an entire framework around it (as some people have), but I don&#8217;t think it&#8217;s necessary. A few little [...]]]></description>
			<content:encoded><![CDATA[<p>The <a href="http://code.google.com/apis/chart/">Google Charts API</a> is a useful little tool for generating charts. The &#8220;API&#8221; is actually just a set of parameters you pass to a single URL endpoint: http://chart.apis.google.com/chart</p>

<p><img src="http://chart.apis.google.com/chart?cht=bvs&#038;chs=400x250&#038;chbh=14,2,0&#038;chd=t:3,4,4,1,3,2,2,0,1,3,11,5,7,5,5,7,7,5,3,2,3,3,6,6&#038;chds=0,11" alt="sample chart" /></p>

<p>It&#8217;s a very capable API, and you could write an entire framework around it (as some people have), but I don&#8217;t think it&#8217;s necessary. A few little helper functions and Google&#8217;s documentation is all you really need. Here&#8217;s the heart of my &#8220;framework&#8221; in JavaScript:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">gchart_build</span>(options)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> params = [];<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(option <span style="color:#881350;">in</span> options)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;params.<span style="color:#003369;">push</span>(option + <span style="color:#760f15;">&quot;=&quot;</span> + options[option]);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#760f15;">&quot;http://chart.apis.google.com/chart?&quot;</span> + params.<span style="color:#003369;">join</span>(<span style="color:#760f15;">&quot;&amp;&quot;</span>);<br />
}</div>

<p>And PHP:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">function</span> <span style="color:#003369;">gchart_build</span>(<span style="color:#825900;">$options</span>)<br />
{<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#825900;">$params</span> = <span style="color:#881350;">array</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">foreach</span><span style="color:#003369;"> </span>(<span style="color:#825900;">$options</span> <span style="color:#881350;">as</span> <span style="color:#825900;">$option</span> =&gt; <span style="color:#825900;">$value</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#825900;">$params</span>[] = <span style="color:#825900;">$option</span> . <span style="color:#eb7300;">&quot;=&quot;</span>. <span style="color:#825900;">$value</span>;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span> <span style="color:#eb7300;">&quot;http://chart.apis.google.com/chart?&quot;</span> . <span style="color:#661aa9;">implode</span>(<span style="color:#eb7300;">&quot;&amp;&quot;</span>, <span style="color:#825900;">$params</span>);<br />
}</div>

<p>And here&#8217;s a simple bar chart example in PHP:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#825900;">$chart_url</span> = <span style="color:#003369;">gchart_build</span>(<span style="color:#881350;">array</span>(<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#eb7300;">&quot;cht&quot;</span> &nbsp;&nbsp;=&gt; <span style="color:#eb7300;">&quot;bvs&quot;</span>,<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#eb7300;">&quot;chs&quot;</span> &nbsp;&nbsp;=&gt; <span style="color:#eb7300;">&quot;400&#215;250&quot;</span>,<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#eb7300;">&quot;chbh&quot;</span> &nbsp;=&gt; <span style="color:#eb7300;">&quot;14,2,0&quot;</span>,<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#eb7300;">&quot;chd&quot;</span> &nbsp;&nbsp;=&gt; <span style="color:#eb7300;">&quot;t:3,4,4,1,3,2,2,0,1,3,11,5,7,5,5,7,7,5,3,2,3,3,6,6&quot;</span>,<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#eb7300;">&quot;chds&quot;</span> &nbsp;=&gt; <span style="color:#eb7300;">&quot;0,11&quot;</span><br />
));</div>

<p>The nice thing about this &#8220;frameworks&#8221; is it takes 30 seconds to implement in many languages, and the actual API is identical across every language, since it just uses a hash object and the original API as a very simple domain-specific language.</p>

<p>This could be improved with a few more helper functions for different parts of the Google Chart API, but this function remains the most important.</p>

<p>Here&#8217;s a simple testbed for trying out the JavaScript version. Simple edit the JavaScript and click &#8220;Update!&#8221; to see the results:
<a href="http://tlrobinson.net/projects/js/gchart_tester.html">gchart_tester.html</a></p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/09/21/a-simple-google-charts-api-framework/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Adding Growl Notifications to Facebook</title>
		<link>http://tlrobinson.net/blog/2008/09/01/adding-growl-notifications-to-facebook/</link>
		<comments>http://tlrobinson.net/blog/2008/09/01/adding-growl-notifications-to-facebook/#comments</comments>
		<pubDate>Mon, 01 Sep 2008 09:32:11 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=48</guid>
		<description><![CDATA[Yesterday Facebook added the &#8220;Live Feed&#8221;, which is like the existing News Feed, but automatically updates the page without requiring the user to refresh. It simply polls Facebook&#8217;s servers every few seconds, rather than the slightly fancier &#8220;comet&#8221;-style long-polling they do for Facebook chat:



Polling is perfectly sufficient for this sort of updating (it&#8217;s not particularly [...]]]></description>
			<content:encoded><![CDATA[<p>Yesterday Facebook added the <a href="http://www.new.facebook.com/home.php?tab=2">&#8220;Live Feed&#8221;</a>, which is like the existing News Feed, but automatically updates the page without requiring the user to refresh. It simply polls Facebook&#8217;s servers every few seconds, rather than the slightly fancier &#8220;comet&#8221;-style long-polling they do for Facebook chat:</p>

<p><img src="http://tlrobinson.net/skitches/facebook-requests-20080901-052955.png" alt="Live Feed vs Facebook Chat" width="500"/></p>

<p>Polling is perfectly sufficient for this sort of updating (it&#8217;s not particularly latency sensitive), but none of this is relevant to the rest of this post anyhow.</p>

<p>As neat as it is, I&#8217;m not going to sit around all day watching Facebook, waiting for updates. I want to be notified of updates unobtrusively, at which point I can decide if I want to ignore them or check it out. <a href="http://growl.info/">Growl</a> is perfect for this.</p>

<p>Many OS X apps use Growl to display notifications to the user. They use either <a href="http://developer.apple.com/documentation/Cocoa/Conceptual/DistrObjects/DistrObjects.html">Distributed Objects</a> or a UDP protocol called <a href="http://growl.info/documentation/developer/protocol.php">GrowlTalk</a> to post the notifications, neither of which is suitable for a client-side web app. Google Gears promises to provide <a href="http://code.google.com/p/gears/wiki/NotificationAPI">NotificationAPI</a> at some point, but it&#8217;s not currently ready. <a href="http://fluidapp.com/">Fluid</a> also has a notification API, but we need Firefox&#8217;s Greasemonkey plugin to inject some JavaScript.</p>

<p>Brian Dunnington wrote a version of <a href="http://ajaxian.com/archives/growls-for-windows-and-a-web-notification-api">Growl for Windows</a>, which adds one neat feature: an HTTP interface. This lets the browser (or anything else the speaks HTTP) post notifications directly. Unfortunately the OS X version of Growl doesn&#8217;t have this interface built in, but it&#8217;s nearly trivial to create a bridge to the GrowlTalk protocol in Python:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#881350;">from</span> BaseHTTPServer <span style="color:#881350;">import</span> BaseHTTPRequestHandler, HTTPServer<br />
<span style="color:#881350;">from</span> socket <span style="color:#881350;">import</span> AF_INET, SOCK_DGRAM, socket<br />
<span style="color:#881350;">from</span> urlparse <span style="color:#881350;">import</span> urlparse<br />
<span style="color:#881350;">from</span> cgi <span style="color:#881350;">import</span> parse_qs<br />
<span style="color:#881350;">import</span> simplejson<br />
<span style="color:#881350;">import</span> netgrowl<br />
<br />
<span style="color:#881350;">class</span> GrowlBridgeHandler(BaseHTTPRequestHandler):<br />
<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">def</span> do_GET(<span style="color:#881350;">self</span>):<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">try</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># parse url<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;u = urlparse(<span style="color:#881350;">self</span>.path)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span> u.path == <span style="color:#760f15;">&quot;/&quot;</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># parse query string<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;q = parse_qs(u.query)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> q<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># parse json payload<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;j = simplejson.loads(q[<span style="color:#760f15;">'d'</span>][<span style="color:#0000ff;">0</span>])<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> j<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># create and send the notification<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p = netgrowl.GrowlNotificationPacket(<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;description=(j.has_key(<span style="color:#760f15;">&#8216;description&#8217;</span>) <span style="color:#881350;">and</span> j[<span style="color:#760f15;">'description'</span>] <span style="color:#881350;">or</span> <span style="color:#760f15;">&quot;Description&quot;</span>),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;title=(j.has_key(<span style="color:#760f15;">&#8216;title&#8217;</span>) <span style="color:#881350;">and</span> j[<span style="color:#760f15;">'title'</span>] <span style="color:#881350;">or</span> <span style="color:#760f15;">&quot;Title&quot;</span>),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;priority=(j.has_key(<span style="color:#760f15;">&#8216;priority&#8217;</span>) <span style="color:#881350;">and</span> j[<span style="color:#760f15;">'priority'</span>] <span style="color:#881350;">or</span> <span style="color:#0000ff;">0</span>),<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sticky=<span style="color:#881350;">True</span>) <span style="color:#236e25;">#(j.has_key(&#8217;sticky&#8217;) and j['sticky'] or False))<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;growlserver.sendto(p.payload(), addr)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># send a 200 http response<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">self</span>.send_response(<span style="color:#0000ff;">200</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">self</span>.send_header(<span style="color:#760f15;">&#8216;Content-type&#8217;</span>, <span style="color:#760f15;">&#8216;text/html&#8217;</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">self</span>.end_headers()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">return</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">except</span> <span style="color:#f79600;">IOError</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">self</span>.send_error(<span style="color:#0000ff;">404</span>, <span style="color:#760f15;">&#8216;File Not Found: %s&#8217;</span> % <span style="color:#881350;">self</span>.path)<br />
<br />
<span style="color:#881350;">def</span> main():<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">try</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># prepare the growl socket<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">global</span> addr, growlserver<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;addr = (<span style="color:#760f15;">&quot;localhost&quot;</span>, netgrowl.GROWL_UDP_PORT)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;growlserver = socket(AF_INET,SOCK_DGRAM)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> <span style="color:#760f15;">&quot;Assembling registration packet like growlnotify&#8217;s (no password)&quot;</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p = netgrowl.GrowlRegistrationPacket()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p.addNotification()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> <span style="color:#760f15;">&quot;Sending registration packet&quot;</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;growlserver.sendto(p.payload(), addr)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#236e25;"># start the http server<br />
</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;httpserver = HTTPServer((<span style="color:#760f15;">&#8221;</span>, <span style="color:#0000ff;">9889</span>), GrowlBridgeHandler)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> <span style="color:#760f15;">&#8217;started growlbridge&#8230;&#8217;</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;httpserver.serve_forever()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">except</span> <span style="color:#f79600;">KeyboardInterrupt</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> <span style="color:#760f15;">&#8216;^C received, shutting down server&#8217;</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;httpserver.socket.close()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;growlserver.close()<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">print</span> <span style="color:#760f15;">&quot;Done.&quot;</span><br />
<br />
<span style="color:#881350;">if</span> __name__ == <span style="color:#760f15;">&#8216;__main__&#8217;</span>:<br />
&nbsp;&nbsp;&nbsp;&nbsp;main()<br />
<br />
</div>

<p><a href="http://tlrobinson.net/projects/js/fbnotify/growlbridge.py.gz">growlbridge.py.gz</a></p>

<p>Of course this opens up a port on your machine, so you should take the necessary precautions to firewall it. It uses Rui Carmo&#8217;s <a href="http://the.taoofmac.com/space/Projects/netgrowl">netgrowl</a> and also requires Bob Ippolito&#8217;s <a href="http://www.undefined.org/python/">simplejson</a>.</p>

<p>We can now use Brian&#8217;s <a href="http://www.tripthevortex.com/growl/growl.js">growl.js</a> library with both Mac OS X and Windows versions of Growl. The next step is to connect it up to Facebook with Greasemonkey:</p>

<div style="text-align:left;color:#000000; background-color:#ffffff; border:solid black 1px; padding:0.5em 1em 0.5em 1em; overflow:auto;font-size:small; font-family:monospace; "><span style="color:#236e25;">// ==UserScript==<br />
// @name &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Facebook News Feed Notifier<br />
// @namespace &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;http://tlrobinson.net/<br />
// @description &nbsp;&nbsp;&nbsp;Notify user of new Facebook News Feed items via Growl<br />
// @include &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;http://*.facebook.com/home.php<br />
// ==/UserScript==<br />
</span><br />
<span style="color:#881350;">function</span> <span style="color:#003369;">GM_init</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> fbNewsFeedNotification = <span style="color:#881350;">new</span> Growl.<span style="color:#003369;">NotificationType</span>(<span style="color:#760f15;">&quot;Facebook News Feed&quot;</span>, <span style="color:#881350;">true</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;Growl.<span style="color:#003369;">register</span>(<span style="color:#760f15;">&quot;Facebook&quot;</span>, [fbNewsFeedNotification]);<br />
&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;unsafeWindow.HomeFeed.prototype._addStoriesToQueueOriginal = unsafeWindow.HomeFeed.prototype._addStoriesToQueue<br />
&nbsp;&nbsp;&nbsp;&nbsp;unsafeWindow.HomeFeed.prototype._addStoriesToQueue = <span style="color:#881350;">function</span>(stories) {<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">this</span>.<span style="color:#003369;">_addStoriesToQueueOriginal</span>(stories);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> testDiv = document.<span style="color:#003369;">createElement</span>(<span style="color:#760f15;">&quot;div&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">for</span><span style="color:#003369;"> </span>(<span style="color:#881350;">var</span> i = <span style="color:#0000ff;">0</span>; i &lt; stories.length; i++)<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;testDiv.innerHTML = stories[i];<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> spans = testDiv.<span style="color:#003369;">getElementsByTagName</span>(<span style="color:#760f15;">&quot;span&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">var</span> message = (spans.length &gt; <span style="color:#0000ff;">0</span>) ? spans[<span style="color:#0000ff;">0</span>].textContent : <span style="color:#760f15;">&quot;Unknown update&quot;</span>;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Growl.<span style="color:#003369;">notify</span>(fbNewsFeedNotification, <span style="color:#760f15;">&quot;Facebook News Feed&quot;</span>, message, Growl.Priority.Normal, <span style="color:#881350;">false</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}<br />
<br />
<span style="color:#236e25;">// Add growl.js<br />
</span><span style="color:#881350;">var</span> GM_GROWL = document.<span style="color:#003369;">createElement</span>(<span style="color:#760f15;">&#8217;script&#8217;</span>);<br />
GM_GROWL.src = <span style="color:#760f15;">&#8216;http://www.tripthevortex.com/growl/growl.js&#8217;</span>;<br />
GM_GROWL.type = <span style="color:#760f15;">&#8216;text/javascript&#8217;</span>;<br />
document.<span style="color:#003369;">getElementsByTagName</span>(<span style="color:#760f15;">&#8216;head&#8217;</span>)[<span style="color:#0000ff;">0</span>].<span style="color:#003369;">appendChild</span>(GM_GROWL);<br />
<br />
<span style="color:#236e25;">// Check if growl.js&#8217;s loaded<br />
</span><span style="color:#881350;">function</span> <span style="color:#003369;">GM_wait</span>() {<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">if</span>(<span style="color:#881350;">typeof</span> unsafeWindow.Growl == <span style="color:#760f15;">&#8216;undefined&#8217;</span>)<br />
&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;console.<span style="color:#003369;">log</span>(<span style="color:#760f15;">&quot;waiting&quot;</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;window.<span style="color:#003369;">setTimeout</span>(GM_wait, <span style="color:#0000ff;">100</span>);<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#881350;">else</span><br />
&nbsp;&nbsp;&nbsp;&nbsp;{<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Growl = unsafeWindow.Growl;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="color:#003369;">GM_init</span>();<br />
&nbsp;&nbsp;&nbsp;&nbsp;}<br />
}<br />
<span style="color:#003369;">GM_wait</span>();</div>

<p>This fairly simple userscript loads growl.js, then overwrites one of Facebook&#8217;s JavaScript functions that gets called when updating the feed, HomeFeed.prototype._addStoriesToQueue(). The new function should call the original one (so that the feed is still updated), but it should also post a new notification for each new feed story.</p>

<p>That&#8217;s about it. Run &#8220;python growlbridge.py&#8221; if you&#8217;re on a Mac (make sure you have netgrowl and simplejson), or Brian Dunnington&#8217;s Growl for Windows, install the above Greasemonkey userscript, and open up <a href="http://www.new.facebook.com/home.php?tab=2">http://www.new.facebook.com/home.php?tab=2</a>.</p>

<p>Unfortunately, there appears to be some bugs (or new security restrictions) in Firefox 3 that prevents this userscript from working correctly, but it works fine in Firefox 2.</p>

<p>In the future growl.js could be swapped out for the Gears or Fluid notification APIs, or anything else (ideally some standard). Also, hopefully Growl for OS X will have the same HTTP interface built in.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/09/01/adding-growl-notifications-to-facebook/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>MobileMe and (lack of) encryption</title>
		<link>http://tlrobinson.net/blog/2008/08/17/mobileme-and-lack-of-encryption/</link>
		<comments>http://tlrobinson.net/blog/2008/08/17/mobileme-and-lack-of-encryption/#comments</comments>
		<pubDate>Mon, 18 Aug 2008 04:21:03 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=46</guid>
		<description><![CDATA[AppleInsider.com posted an article today claiming the lack of SSL on MobileMe has caused &#8220;unnecessary panic&#8221; and MobileMe is in fact secure. This is 100% false.

I&#8217;m not sure what to make of the article. I feel like the author said a bunch of big words hoping that most people would assume he knew what he [...]]]></description>
			<content:encoded><![CDATA[<p>AppleInsider.com posted an article today <a href="http://www.appleinsider.com/articles/08/08/15/inside_mobileme_web_3_and_web_client_server_apps.html&#038;page=2">claiming the lack of SSL on MobileMe has caused &#8220;unnecessary panic&#8221;</a> and MobileMe is in fact secure. This is 100% false.</p>

<p>I&#8217;m not sure what to make of the article. I feel like the author said a bunch of big words hoping that most people would assume he knew what he was talking about and move on. Let&#8217;s try to break it down:</p>

<blockquote>Data transaction security in MobileMe&#8217;s web apps is based upon authenticated handling of JSON data exchanges between the self contained JavaScript client apps and Apple&#8217;s cloud, rather than the SSL web page encryption used by HTTPS.</blockquote>

<p>So the &#8220;JSON data exchanges&#8221; are &#8220;authenticated&#8221;. This is like saying &#8220;You can only read your email if you&#8217;re logged in&#8221;. I should hope so.</p>

<blockquote>The only real web pages MobileMe exchanges with the server are the HTML, JavaScript, and CSS files that make up the application, which have no need for SSL encryption following the initial user authentication</blockquote>

<p>This implies that while the app itself isn&#8217;t encrypted, it doesn&#8217;t need to be since the data itself is. This would be nice, but:</p>

<ul>
    <li>As <a href="http://twitter.com/wooster/statuses/890542074">Andrew Wooster points out</a>, if a man-in-the-middle attack is possible, the app itself can by hijacked, and all bets are off. In addition to providing encryption, SSL can protect against MITM attacks. See below.</li>
    <li>The data is actually NOT encrypted. Doh. See screenshots below.</li>
    <li>Even if MITM attacks are taken out of the equation, and Apple wanted to encrypt the data but not the app, it&#8217;s simply not possible to make HTTPS requests from a page loaded over plain old HTTP due to the <a href="http://en.wikipedia.org/wiki/Same_origin_policy">same origin policy</a>. There are numerous tricks (iframes, script tags, etc) to get around the same origin policy but none that I&#8217;m aware of would be useful in this sort of situation.</li>
</ul>

<p>Viewing MobileMe&#8217;s traffic in your network sniffer of choice (I prefer <a href="http://www.charlesproxy.com/">Charles</a> and <a href="http://www.wireshark.org/">Wireshark</a>) shows your data is actually unencrypted.</p>

<p>Calender:</p>

<p><center><a href="http://tlrobinson.net/skitches/mobileme_not_encrypted-20080817-174838.png"><img src="http://tlrobinson.net/skitches/mobileme_not_encrypted-20080817-174838.png" alt=""  width="450" /></a></center></p>

<p>Email:</p>

<p><center><a href="http://tlrobinson.net/skitches/mobileme_not_encrypted-20080817-172825.png"><img src="http://tlrobinson.net/skitches/mobileme_not_encrypted-20080817-172825.png" alt="" width="450" /></a></center></p>

<p>Finally, the article states:</p>

<blockquote>This has caused some unnecessary panic among web users who have equated their browser&#8217;s SSL lock icon with web security. And of course, Internet email is not a secured medium anyway once it leaves your server.</blockquote>

<p>SSL is, in fact, the standard for securing web apps. I am much more inclined to trust SSL, which is known to work well, than a proprietary solution (or in MobileMe&#8217;s case, none at all).</p>

<p>Regarding email being unencrypted, it is true that email is often unencrypted between mail servers, but the more important link is between the user and their mail server, especially with widespread WiFi usage. Any sane person will use POP, IMAP, or web mail over SSL, which is more than MobileMe can claim to offer. And don&#8217;t forget it&#8217;s not just email we&#8217;re talking about: calendars, contacts, files, etc are also transmitted in the clear by MobileMe.</p>

<p><em>[Clarification: this article only pertains to the MobileMe web interface. IMAP email and OS X syncing do offer encryption]</em></p>

<p>The article reminded me of the recent <a href="http://www.cs.uml.edu/~ntuck/mozilla/">Mozilla SSL policy bad for the Web</a> article and ensuing comments on <a href="http://news.ycombinator.com/item?id=277057">Hacker News</a>. Some people are upset that Firefox 3 makes it harder for users to visit sites with self-signed SSL certificates. They claim this is bad for the web because it forces anyone who wishes to use encryption to pay a certificate authority (CA) for a signed SSL certificate, which goes against the openness of the Web.</p>

<p>They point to the fact that self-signed certificates can offer encryption without authentication. This is true, until a MITM attack is possible, at which point the encryption becomes useless. The attacker simply inserts himself between you and the server, encrypting both channels with his own self-signed certificate, while still intercepting all communication. This is much worse than no encryption at all, since the user may naively believe it&#8217;s a secure connection.</p>

<p>If browsers were to blindly accept self-signed certificates, the system would break down. Just because you wanted to save $15 on a SSL certificate, I would no longer be warned of MITM attack on my bank website. Clearly not acceptable.</p>

<p>So they suggest making the warning less obtrusive, but then average users who don&#8217;t know what SSL will simply ignore it. The more sites that use self-signed certs, the more they ignore it, and the more they become used to it, until the day they go to their bank&#8217;s website and get a warning they ignore out of habit. It&#8217;s a user interface problem just as much as a technical problem.</p>

<p>Making the warning scary and difficult to ignore accomplishes two things. It makes it harder for the user to accidentally ignore it, and it encourages webmasters to use CA signed certificates, which are far more secure.</p>

<p>My point is that when thinking about security you must take the whole system into account, not just pieces of it. SSL encryption on MobileMe&#8217;s login page is useless if the attacker can then sniff all the data, just as SSL encryption is useless if you can&#8217;t be sure who you&#8217;re talking to.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/08/17/mobileme-and-lack-of-encryption/feed/</wfw:commentRss>
		<slash:comments>32</slash:comments>
		</item>
		<item>
		<title>Multitouch JavaScript &#8220;Virtual Light Table&#8221; on iPhone v2.0</title>
		<link>http://tlrobinson.net/blog/2008/07/11/multitouch-javascript-virtual-light-table-on-iphone-v20/</link>
		<comments>http://tlrobinson.net/blog/2008/07/11/multitouch-javascript-virtual-light-table-on-iphone-v20/#comments</comments>
		<pubDate>Fri, 11 Jul 2008 23:50:58 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[iPhone]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=42</guid>
		<description><![CDATA[Now that iPhone 2.0 is out I started playing around with some of the new web features, and soon found that I had created the prototypical virtual light table that&#8217;s an essential demo for any new multitouch technology.

It&#8217;s about 100 lines of JavaScript. It grabs the 10 latest photos from Flickr&#8217;s &#8220;interesting photos&#8221; API and [...]]]></description>
			<content:encoded><![CDATA[<p>Now that iPhone 2.0 is out I started playing around with some of the new web features, and soon found that I had created the prototypical virtual light table that&#8217;s an essential demo for <a href="http://www.cs.nyu.edu/~jhan/ftirtouch/index.html">any</a> <a href="http://gizmodo.com/391103/full+screen-multitouch-mac-os-x-is-here">new</a> <a href="http://www.roughlydrafted.com/RD/RDM.Tech.Q2.07/BE8D0C58-313E-453E-9E8B-D443BE6E1DDE.html">multitouch</a> <a href="http://www.microsoft.com/surface/">technology</a>.</p>

<p>It&#8217;s about 100 lines of JavaScript. It grabs the 10 latest photos from Flickr&#8217;s &#8220;interesting photos&#8221; API and randomly places them on the screen for you to play with:</p>

<p><object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/-XKb8We_uzg&#038;hl=en&#038;fs=1"></param><param name="allowFullScreen" value="true"></param><embed src="http://www.youtube.com/v/-XKb8We_uzg&#038;hl=en&#038;fs=1" type="application/x-shockwave-flash" allowfullscreen="true" width="425" height="344"></embed></object></p>

<p>This is great if you have an iPhone with the 2.0 software, but desktop browsers should get some multitouch love too. So I started writing a little bridge that fakes multitouch events in desktop browsers. It&#8217;s far from complete, but it&#8217;s just good enough to get the virtual light table demo working.</p>

<p>So go ahead and load it up in the new iPhone MobileSafari or Safari 3.1+ / <a href="http://nightly.webkit.org/">WebKit nightly</a> (requires <a href="http://webkit.org/blog/130/css-transforms/">CSS transforms</a>):</p>

<p><a href="http://tlrobinson.net/iphone/lighttable/">http://tlrobinson.net/iphone/lighttable/</a></p>

<p>In desktop browsers it uses the previous clicked location as a second &#8220;touch&#8221;, so you can click a photo then click and drag another spot on the photo to resize and rotate (notice the yellow dot).</p>

<p>For a good overview of touch events and gestures, check out this <a href="http://www.sitepen.com/blog/2008/07/10/touching-and-gesturing-on-the-iphone/">SitePen blog post</a> and <a href="http://developer.apple.com/webapps/">Apple&#8217;s documentation</a>.</p>

<p>Here&#8217;s the source for the fake multitouch bridge:</p>

<p><a href="http://tlrobinson.net/iphone/lighttable/multitouch-fake.js">http://tlrobinson.net/iphone/lighttable/multitouch-fake.js</a>.</p>

<p>Clearly the reverse of this bridge would be even more useful, since iPhone only sends mouse events under specific conditions. The mousedown, mouseup, and mousemove events could be emulated using the touch equivalents to make certain web apps work on the iPhone without much additional work. Of course you would need to either cancel the default actions (i.e. panning and zooming) on touch events, or have some way to manage the interactions between them.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/07/11/multitouch-javascript-virtual-light-table-on-iphone-v20/feed/</wfw:commentRss>
		<slash:comments>30</slash:comments>
		</item>
		<item>
		<title>What&#8217;s wrong with Yahoo&#8217;s OpenID implementation</title>
		<link>http://tlrobinson.net/blog/2008/01/30/whats-wrong-with-yahoos-openid-implementation/</link>
		<comments>http://tlrobinson.net/blog/2008/01/30/whats-wrong-with-yahoos-openid-implementation/#comments</comments>
		<pubDate>Thu, 31 Jan 2008 00:07:38 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Uncategorized]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=33</guid>
		<description><![CDATA[Today Yahoo launched support for OpenID. On the surface this seems great for OpenID. Unfortunately there are a number of problems with it.

For those unfamiliar with OpenID, it is a single sign-on system, which allows users to remember a single username and password for signing in to any site which supports OpenID . There are [...]]]></description>
			<content:encoded><![CDATA[<p>Today Yahoo <a href="http://open.login.yahoo.com/">launched support</a> for <a href="http://openid.net/">OpenID</a>. On the surface this seems great for OpenID. Unfortunately there are a number of problems with it.</p>

<p>For those unfamiliar with OpenID, it is a <a href="http://en.wikipedia.org/wiki/Single_sign_on">single sign-on</a> system, which allows users to remember a single username and password for signing in to any site which supports OpenID . There are two basic parts to the OpenID system: sites which wish to allow users to sign in using an OpenID (the &#8220;relying party&#8221;), and sites which host your OpenID (the &#8220;OpenID provider&#8221;). Yahoo has chosen to be the latter, an OpenID provider.</p>

<p>Most OpenID providers give their users a simple, easy to remember OpenID like &#8220;username.livejournal.com&#8221; or &#8220;username.wordpress.com&#8221;. However, by default Yahoo provides their users with an obscure OpenID like &#8220;me.yahoo.com/a/1bjkvd893414lka09i23&#8243;, impossible for any normal person to remember. Why not use &#8220;me.yahoo.com/username&#8221; like most other OpenID providers, you ask? Simple: so Yahoo can force other sites (the &#8220;relying parties&#8221;) into placing &#8220;Sign in using Yahoo&#8221; buttons on their login pages. If a site wants to allow millions of Yahoo users to easily sign in, they must include this button. Free advertising for Yahoo.</p>

<p>If other OpenID providers follow this trend we&#8217;ll soon end up with login pages covered with dozens of &#8220;Sign in using ________&#8221; buttons. This is definitely <em>not</em> then intention of OpenID. Any user with any OpenID provider should be able to type their OpenID into any site which supports OpenID, and it should just work.</p>

<p>Additionally, Yahoo has chosen not to be a relying party themselves. This means that users who have OpenIDs from any number of other providers can&#8217;t sign into Yahoo using their existing OpenID. They&#8217;re basically saying &#8220;Yeah we support OpenID&#8230; as long as WE&#8217;RE in control&#8221;.</p>

<p>To become an acceptable OpenID provider, Yahoo should:</p>

<ul>
<li>give users https://me.yahoo.com/username by DEFAULT, not as an option buried somewhere in the settings.</li>
<li>educate users to type either me.yahoo.com/username or yahoo.com into OpenID login pages, NOT have Yahoo-specific buttons.</li>
<li>become an OpenID relying party, i.e. allow other people to log into Yahoo using their OWN OpenIDs.</li>
</ul>

<p>In the meantime, I suggest getting an OpenID from an <a href="http://openid.net/get/">another provider</a> such as <a href="https://www.myopenid.com/">myopenid.com</a>. If you have a personal website or blog, you can easily use it&#8217;s URL as your OpenID via delegation. Sam Ruby has an <a href="http://www.intertwingly.net/blog/2007/01/03/OpenID-for-non-SuperUsers">excellent overview of various OpenID options</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/01/30/whats-wrong-with-yahoos-openid-implementation/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>Readline and rlwrap</title>
		<link>http://tlrobinson.net/blog/2008/01/10/readline-and-rlwrap/</link>
		<comments>http://tlrobinson.net/blog/2008/01/10/readline-and-rlwrap/#comments</comments>
		<pubDate>Thu, 10 Jan 2008 23:00:29 +0000</pubDate>
		<dc:creator>Tom</dc:creator>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Command line]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[readline]]></category>

		<guid isPermaLink="false">http://tlrobinson.net/blog/?p=32</guid>
		<description><![CDATA[I came across a neat little tool called rlwrap, which essentially wraps the functionality of readline (line editing, history, etc) for any other command line utility. For example, it works well with my GCCalc hack, which I didn&#8217;t bother to integrate readline into, but rlwrap gives you the same thing for free:

rlwrap gccalc


It&#8217;s useful with [...]]]></description>
			<content:encoded><![CDATA[<p>I came across a neat little tool called <a href="http://utopia.knoware.nl/~hlub/rlwrap/">rlwrap</a>, which essentially wraps the functionality of <a href="http://tiswww.case.edu/php/chet/readline/rltop.html">readline</a> (line editing, history, etc) for any other command line utility. For example, it works well with my <a href="http://tlrobinson.net/blog/?p=31">GCCalc</a> hack, which I didn&#8217;t bother to integrate readline into, but rlwrap gives you the same thing for free:</p>

<pre><code>rlwrap gccalc
</code></pre>

<p>It&#8217;s useful with many other tools, like telnet and netcat, and interactive interpreters that don&#8217;t have line editing or history, like <a href="http://www.mozilla.org/rhino/">Rhino</a> (Javascript) and others.</p>
]]></content:encoded>
			<wfw:commentRss>http://tlrobinson.net/blog/2008/01/10/readline-and-rlwrap/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
