Introducing Nimbler: A Window Switcher

When I discovered SmartTab.org about a decade ago, I was quite happy. Never before had my window switching been so fast and nimble. The most important feature was of course the list-based view of window titles, rather than the standard mysterious icons that only coughed up their secret window titles once you landed on them. But one thing I hadn’t yet conceived of was switching to a far-off window without pressing Tab a dozen times or more. SmartTab.org quickly became so ingrained into my workflow that I even resisted changing my operating system because of it. If Windows 7 would mean giving up on all of my trusty tools like SmartTab.org and ASD Clock, I might as well upgrade to something completely different. In 2011 I switched to Debian Squeeze as my main OS and I haven’t looked back, barring perhaps the occasional game. To me, Linux is just so much more user-friendly these days. But enough about that. You can get Nimbler here or you can continue reading about my window switching philosophy.

Even before I switched, I looked for SmartTab.org alternatives. I discovered that both Openbox and KWin can display sane window lists, but superswitcher was much closer to what I wanted. Unfortunately, its C-based code was too complicated for now. I’d pretty much have to learn C first. Fast forward a few years and Fuzzy Window Switcher came out. It was a lovely little application, and it managed to scratch my itch. Someone quipped that a Compiz plugin could perform the same task. I replied that “The Compiz (plugins) source had absolutely no documentation when I last checked, nor was it so obvious that none was needed. Above all else I see [Fuzzy Window Switcher] as a great place to start for hacking together your own thing. Besides, not everyone uses Compiz. This can be useful regardless of whether you use Compiz, Mutter, xfwm4, OpenBox, KWin or whatever else there is.”

Still, I wasn’t ready. I had to learn the much easier Python first, which I didn’t start with until a few months later. By that time I was too busy, and when my schedule finally opened up again I’d forgotten about my plans. Until a couple of weeks ago a bug report reminded me that I could actually mimic what I liked about SmartTab.org. Several hours of coding later I can present Nimbler. Bear with me if you actually know GTK+ 3; I’m figuring this stuff out as I go along. Long story short, this is what it looks like:

So how does it work? I like to think it couldn’t be much more intuitive. You press the shortcut — at the moment I use <Super>grave — and the window pops up. Then you can immediately switch to any window using its identifier, the arrow keys or the mouse. You can also switch workspaces using F1-F12.

Currently non-functional, if you press colon (:) a text input box is added to the window. At some undefined point in the future I probably intend to couple this with Fuzzy Window Switcher’s fuzzy matching code, but don’t hold your breath. ;) If I do get around to implementing something like that, I figure there should also be a configuration setting for this fuzzy-mode where you don’t have to press colon first. This sounds like a wasteful duplication of Fuzzy Window Switcher itself, but my thoughts are that if the text input is merely one character, no fuzzy matching would occur and instead it would just be treated as a window identifier. Theoretically I suppose you could also use such functionality to introduce double-character identifiers, easily quadrupling the amount of potential options.

Quadrupling, you ask? The current amount of identifiers is a little more than 90, so you would expect to easily get over 8,000 unique identifiers out of it. However, for usability purposes aa would is much faster than aA, let alone a0. Besides, who could possibly keep so many windows straight?

I hope someone besides me finds this useful. Enjoy!

Comments

Fixing Up Scanned PDFs with Scan Tailor

Scanned PDFs come my way quite often and I don’t infrequently wish they were nicer to use on digital devices. One semi-solution might include running off to the library and rescanning them personally, but there is a middle road between doing nothing and doing too much: digital manipulation. The knight in shining armor is called Scan Tailor. Note that this post is not about merely cropping away some black edges. When you’re just looking for a tool to cut off some unwanted edges, I’d recommend PDF Scissors instead. If you just want to fix some incorrect rotation once and for all, try the pdftools found in texlive-extra-utils, which gives you simple shorthands like pdf90, pdf180 and pdf270. This post is about splitting up double scanned pages, increasing clarity, and adding an OCR layer on top. With that out of the way, if you’re impatient, you can skip to the script I wrote to automate the process.

Coaxing Scan Tailor

Unfortunately Scan Tailor doesn’t directly load scanned PDFs, which is what seems to be produced by copiers by default and what you’re most likely to receive from other people. Luckily this is easy to work around. If you want to use the program on documents you scan yourself, selecting e.g. TIFF in the output options could be a better choice.

To extract the images from PDF files, I use pdfimages. I believe it tends to come preinstalled, but if not grab poppler-utils with sudo apt install poppler-utils.

pdfimages -png filename.pdf outputname

You might want to take a look at man pdfimages. The -j flag makes sure JPEG files are output as is rather than being converted to something else, for instance, while the -tiff option would convert the output to TIFF. Like PNG, that is lossless. What might also be of interest are -ccitt and -all, but in this case I’d want the images as JPEG, PNG or TIFF because that’s what Scan Tailor takes as input.

At this point you could consider cropping your images to aid processing with Scan Tailor, but I’m not entirely sure how to automate it out of the way. Perhaps unpaper with a few flags could work to remove (some) black edges, but functionally speaking Scan Tailor is pretty much unpaper with a better (G)UI. In any case, this could be investigated.

You’ll want to use pdfimages once more to obtain the DPI of your images for use with Scan Tailor, unless you like to calculate the DPI yourself using the document dimensions and the number of pixels. Both PNG and TIFF support this information, but unfortunately pdfimages doesn’t write it.

$ pdfimages -list filename.pdf
page   num  type   width height color comp bpc  enc interp  object ID x-ppi y-ppi size ratio
--------------------------------------------------------------------------------------------
   1     0 image    1664  2339  gray    1   1  ccitt  no         4  0   200   200  110K  23%
   2     1 image    1664  2339  gray    1   1  ccitt  no         9  0   200   200  131K  28%

Clearly our PDF was scanned at a somewhat disappointing 200 DPI. Now you can start Scan Tailor, create a new project based on the images you just extracted, enter the correct DPI, and just follow the very intuitive steps. For more guidance, read the manual. With any setting you apply, take care to apply it to all pages if you wish, because by default the program quite sensibly applies it only to one page. Alternatively you could run scantailor-cli to automate the process, which could reduce your precious time spent to practically zero. I prefer to take a minute or two to make sure everything’s alright. I’m sure I’ll make up the difference by not having to scroll left and right and whatnot afterwards.

By default Scan Tailor wants to output to 600 DPI, but with my 200 DPI input file that just seemed odd. Apparently it has something to do with the conversion to pure black and white, which necessitates a higher DPI to preserve some information. That being said, 600 DPI seems almost laughably high for 200 DPI input. Perhaps “merely” twice the input DPI would be sufficient. Either way, be sure to use mixed mode on pages with images.

Scan Tailor’s output is not a PDF yet. It’ll require a little bit of post-processing.

Simple Post-Processing

The way I usually go about trying to find new commands already installed on my computer is simply by typing the relevant phrase, in this case tiff. Press Tab for autocomplete. If that fails, you could try apt search tiff, although I prefer a GUI like Synaptic for that. The next stop is a search engine, where you can usually find results faster by focusing on the Arch, Debian or Ubuntu Wikis. On the other hand, blog and forum posts often contain useful information.

$ tiff
tiff2bw     tiff2ps     tiffcmp     tiffcrop    tiffdump    tiffmedian  tiffsplit   
tiff2pdf    tiff2rgba   tiffcp      tiffdither  tiffinfo    tiffset     tifftopnm

tiff2pdf sounds just like what we need. Unfortunately it only processes one file at a time. Easy to fix with a simple shell script, but rtfm (man tiff2pdf) for useful info. “If you have multiple TIFF files to convert into one PDF file then use tiffcp or other program to concatenate the files into a multiple page TIFF file.

tiffcp *.tif out.tif

You could easily stop there, but for sharing or use on devices a PDF (or DjVu) file is superior. My phone doesn’t even come with a TIFF viewer by default and the one in Dropbox — why does that app open almost all documents by itself anyway? — just treats it as a collection of images, which is significantly less convenient than your average document viewer. Meanwhile, apps like the appropriately named Document Viewer deal well with both PDF and DjVu.

tiff2pdf -o bla.pdf out.tif

Wikipedia suggests the CCITT compression used for black and white text is lossless, which is nice. Interestingly, an 1.8 MB low-quality 200 DPI PDF more than doubled in size with this treatment, but a 20MB 400 DPI document was reduced in size to 13MB. Anyway, for most purposes you could consider compressing it with JBIG2, for instance using jbig2enc. Another option might be to ignore such PDF difficulties and use pdf2djvu or to compile a DjVu document directly from the TIFF files. At this point we’re tentatively done.

Harder but Neater Post-Processing

After I’d already written most of this section, I came across this Spanish page that pretty much explains it all. So it goes. Because of that page I decided to add a little note about checkinstall, a program I’ve been using for years but apparently always failed to mention.

You’re going to need jbig2enc. You can grab the latest source or an official release. But first let’s get some generic stuff required for compilation:

sudo apt install  automake autotools-dev libtool

And the jbig2enc-specific dependencies:

sudo apt install libleptonica-dev libjpeg8-dev libpng12-dev libtiff5-dev zlib1g-dev

In the jbig2enc-master directory, compile however you like. I tend to do something along these lines:

./autogen.sh
mkdir build
cd build
./configure
make

Now you can sudo make install to install, but you’ll have to keep the source directory around if you want to run sudo make uninstall later. Instead you can use checkinstall (sudo apt install checkinstall, you know the drill). Be careful with this stuff though.

sudo checkinstall make install

You have to enter a name such as jbig2enc, a proper version number (e.g. 0.28-0 instead of 0.28) and that’s about it. That wasn’t too hard.

At this point you could produce significantly smaller PDFs using jbig2enc itself (some more background information):

jbig2 -b outputbasename -p -s whatever-*.tif
pdf.py outputbasename > output.pdf

However, it doesn’t deal with mixed images as well as tiff2pdf does. And while we’re at it, we might just as well set up our environment for some OCR goodness. Mind you, the idea here is just to add a little extra value with no extra time spent after the initial setup. I have absolutely no intention of doing any kind of proofreading or some such on this stuff. The simple fact is that the Scan Tailor treatment drastically improved the chances of OCR success, so it’d be crazy not to do it. There’s a tool called pdfbeads that can automatically pull it all together, but it needs a little setup first.

You need to install ruby-rmagick, ruby-hpricot if you want to do stuff with OCRed text (which is kind of the point), and ruby-dev.

sudo apt install ruby-rmagick ruby-hpricot ruby-dev

Then you can install pdfbeads:

sudo gem install pdfbeads

Apparently there are some issues with iconv or something? Whatever it is, I have no interest in Ruby at the moment and the problem can be fixed with a simple sudo gem install iconv. If iconv is added to the pdfbeads dependencies or if it switches to whatever method Ruby would prefer, this shouldn’t be an issue in the future.

At this point we’re ready for the OCR. sudo apt install tesseract-ocr and whatever languages you might want, such as tesseract-ocr-nld. The -l switch is irrelevant if you just want English, which is the default.

parallel tesseract -l eng+nld {} {.} hocr ::: *.tif

GNU Parallel speeds this up by automatically running as many different tesseracts as you’ve got CPU cores. Install with sudo apt install parallel if you don’t have it, obviously. I’m pretty patient about however much time this stuff might take as long as it proceeds by itself without requiring any attention, but on my main computer this will make everything proceed almost four times as quickly. Why wait any longer than you have to? The OCR results are actually of extremely high quality: it has some issues with italics and that’s pretty much it. It’s not an issue with the characters, but it doesn’t seem to detect the spaces in between words. But what do I care, other than that minor detail it’s close to perfect and this wasn’t even part of the original plan. It’s a very nice bonus.

Once that’s done, we can produce our final result:

pdfbeads *.tif > out.pdf

My 20 MB input file now is a more usable and legible 3.7 MB PDF with decent OCR to boot. Neat. A completely JPEG-based file I tried went from 46.8 MB to 2.6 MB. Now it’s time to automate the workflow with some shell scripting.

ReadablePDF, the script

Using the following script you can automate the entire workflow described above, although I’d always recommend double-checking Scan Tailor’s automated results. The better the input, the better the machine output, but even so there might just be one misdetected page hiding out. The script could still use a few refinements here and there, so I put it up on Github. Feel free to fork and whatnot. I licensed it under the GNU General Public License version 3.

#!/bin/bash
# readablepdf
# ReadablePDF streamlines the effort of turning a not so great PDF into
# a more easily readable PDF (or of course a pretty decent PDF into an
# even better one). This script depends on poppler-utils, imagemagick,
# scantailor, tesseract-ocr, jbic2enc, and pdfbeads.
#
# Unfortunately only the first four are available in the Debian repositories.
# sudo apt install poppler-utils imagemagick scantailor tesseract-ocr
#
# For more background information and how to install jbig2enc and pdfbeads,
# see http://fransdejonge.com/2014/10/fixing-up-scanned-pdfs-with-scan-tailor#harder-post-processing
#
# GNU Aspell and GNU Parallel are recommended but not required.
# sudo apt install aspell parallel
#
# Aspell dictionaries tend to be called things like aspell-en, aspell-nl, etc.

BASENAME=${@%.pdf} # or `basename "@%" .pdf`

# It would seem that at some point in its internal processing, pdfbeads has issues with spaces.
# Let's strip them and perhaps some other special characters so as still to provide
# meaningful working directory and file names.
BASENAME_SAFE=$(echo "${BASENAME}" | tr ' ' '_') # Replace all spaces with underscores.
#BASENAME_SAFE=$(echo "${BASENAME_SAFE}" | tr -cd 'A-Za-z0-9_-') # Strip other potentially harmful chars just in case?

SCRIPTNAME=$(basename "$0" .sh)
TMP_DIR=${SCRIPTNAME}-${BASENAME_SAFE}

TESSERACT_PARAMS="-l eng+nld"


# If project file exists, change directory and assume everything's in order.
# Else do the preprocessing and initiation of a new project.
if [ -f "${TMP_DIR}/${BASENAME_SAFE}.ScanTailor" ]; then
	echo "File ${TMP_DIR}/${BASENAME_SAFE}.ScanTailor exists."
	cd "${TMP_DIR}"
else
	echo "File ${TMP_DIR}/${BASENAME_SAFE}.ScanTailor does not exist."
	
	# Let's get started.
	mkdir "${TMP_DIR}"
	cd "${TMP_DIR}"
	
	# Only output PNG to prevent any potential further quality loss.
	pdfimages -png "../${BASENAME}.pdf" "${BASENAME_SAFE}"
	
	# This is basically what happens in https://github.com/virantha/pypdfocr as well
	# get the x-dpi; no logic for different X and Y DPI or different DPI within PDF file
	# y-dpi would be pdfimages -list out.pdf | sed -n 3p | awk '{print $14}'
	DPI=$(pdfimages -list "../${BASENAME}.pdf" | sed -n 3p | awk '{print $13}')
	
#<<'end_long_comment'
	# TODO Skip all this based on a rotation command-line flag!
	# Adapted from http://stackoverflow.com/a/9778277
	# Scan Tailor says it can't automatically figure out the rotation.
	# I'm not a programmer, but I think I can do well enough by (ab)using OCR. :)
	file="${BASENAME_SAFE}-000.png"
	
	TMP="/tmp/rotation-calc"
	mkdir ${TMP}

	# Make copies in all four orientations (the src file is 0; copy it to make 
	# things less confusing)
	north_file="${TMP}/0"
	east_file="${TMP}/90"
	south_file="${TMP}/180"
	west_file="${TMP}/270"

	cp "$file" "$north_file"
	convert -rotate 90 "$file" "$east_file"
	convert -rotate 180 "$file" "$south_file"
	convert -rotate 270 "$file" "$west_file"

	# OCR each (just append ".txt" to the path/name of the image)
	north_text="$north_file.txt"
	east_text="$east_file.txt"
	south_text="$south_file.txt"
	west_text="$west_file.txt"

	# tesseract appends .txt automatically
	tesseract "$north_file" "$north_file"
	tesseract "$east_file" "$east_file"
	tesseract "$south_file" "$south_file"
	tesseract "$west_file" "$west_file"

	# Get the word count for each txt file (least 'words' == least whitespace junk
	# resulting from vertical lines of text that should be horizontal.)
	wc_table="$TMP/wc_table"
	echo "$(wc -w ${north_text}) ${north_file}" > $wc_table
	echo "$(wc -w ${east_text}) ${east_file}" >> $wc_table
	echo "$(wc -w ${south_text}) ${south_file}" >> $wc_table
	echo "$(wc -w ${west_text}) ${west_file}" >> $wc_table

	# Spellcheck. The lowest number of misspelled words is most likely the 
	# correct orientation.
	misspelled_words_table="$TMP/misspelled_words_table"
	while read record; do
		txt=$(echo "$record" | awk '{ print $2 }')
		# This is harder to automate away, pretend we only deal with English and Dutch for now.
		misspelled_word_count=$(< "${txt}" aspell -l en list | aspell -l nl list | wc -w)
		echo "$misspelled_word_count $record" >> $misspelled_words_table
	done < $wc_table

	# Do the sort, overwrite the input file, save out the text
	winner=$(sort -n $misspelled_words_table | head -1)
	rotated_file=$(echo "${winner}" | awk '{ print $4 }')
	
	rotation=$(basename "${rotated_file}")
	
	echo "Rotating ${rotation} degrees"

	# Clean up.
	if [ -d ${TMP} ]; then
		rm -r ${TMP}
	fi
	# TODO end skip
	
	if [[ ${rotation} -ne 0 ]]; then
		mogrify -rotate "${rotation}" "${BASENAME_SAFE}-*.png"
	fi
#end_long_comment
	
	# consider --color-mode=mixed --despeckle=cautious
	scantailor-cli --dpi="${DPI}" --margins=5 --output-project="${BASENAME_SAFE}.ScanTailor" ./*.png ./
fi

while true; do
	read -p "Please ensure automated detection proceeded correctly by opening the project file ${TMP_DIR}/${BASENAME_SAFE}.ScanTailor in Scan Tailor. Enter [Y] to continue now and [N] to abort. If you restart the script, it'll continue from this point unless you delete the directory ${TMP_DIR}. " yn
	case $yn in
		[Yy]* ) break;;
		[Nn]* ) exit;;
		* ) echo "Please answer yes or no.";;
	esac
done

# Use GNU Parallel to speed things up if it exists.
if command -v parallel >/dev/null; then
	parallel tesseract {} {.} ${TESSERACT_PARAMS} hocr ::: *.tif
else
	for i in ./*.tif; do tesseract $i $(basename $i) ${TESSERACT_PARAMS} hocr; done;
fi

# pdfbeads doesn't play nice with filenames with spaces. There's nothing we can do
# about that here, but that's ${BASENAME_SAFE} is generated up at the beginning.
# 
# Also pdfbeads ./*.tif > "${BASENAME_SAFE}.pdf" doesn't work,
# so you're in trouble if your PDF's name starts with "-".
# See http://www.dwheeler.com/essays/filenames-in-shell.html#prefixglobs
pdfbeads *.tif > "${BASENAME_SAFE}.pdf"

#OUTPUT_BASENAME=${BASENAME}-output@DPI${DPI}
mv "${BASENAME_SAFE}.pdf" ../"${BASENAME}-readable.pdf"

Alternatives

If you’re not interested in the space savings of JBIG2 because the goal of ease of use and better legibility has been achieved (and you’d be quite right; digging further is just something I like to do), after tiff2pdf you could still consider tossing in pdfsandwich. You might as well, for the extra effort only consists of installing an extra package. Instead, OCRmyPDF might also work, or perhaps even plain Tesseract 3.03 and up. pdfsandwich just takes writing the wrapper out of your hands. But again, this part is just a nice bonus.

pdfsandwich -lang nld+eng filename.pdf -o filename-ocr.pdf

The resulting file doesn’t strike my fancy after playing with the tools mentioned above, but hey, it takes less time to setup and it works.

DjVu

DjVu is probably a superior alternative to PDF, so it ought to be worth investigating. This link might help.

Other Matters of Potential Interest

Note that I’m explicitly not interested in archiving a book digitally or some such. That is, I want to obtain a digital copy of a document or book that avoids combining the worst of both digital and paper into one document, but I’m not interested in anything beyond that unless it can be done automatically. Moreover, attempting to replicate original margins would actually make the digital files less usable. For digital archiving you’ll obviously have to redo that not-so-great 200 DPI scan and do a fair bit more to boot. It looks like Spreads is a great way to automate the kind of workflow desired in that case. This link dump might offer some further inspiration.

Conclusion

My goal has been achieved. Creating significantly improved PDFs shouldn’t take more than a minute or two of my time from now on, depending a bit on the quality of the input document. Enjoy.

Comments

Connecting to Belgacom FON: It’s Still Possible

I was happily using Belgacom FON Autologin instead of the behemoth of an official Belgacom app, but ever since Belgacom updated their portal code I’ve barely been able to connect at all. That’s Belgacom’s fault, not the app’s. I haven’t been able to connect through the web interface or the official app either, because it just times out or gives mysterious errors. Unfortunately Belgacom FON Autologin pretends to be a browser by sending information over the HTTP protocol, so it equally fails to connect.

Luckily I just came across FON AccessFon, an app that utilizes the WISPr protocol. In a magnificent 33 kB it manages to connect to Belgacom FON quickly and efficiently, and for every FON router rather than just Belgacom’s to boot.

Comments

How I Put Real Firefox on Debian

With a bit of trial and error it worked out like this. Alternatively you could use /opt.

Create a file called something like firefox.desktop in /home/username/.local/share/applications/

In that file, put:

[Desktop Entry]
Type=Application
Name=Firefox
GenericName=Web Browser
X-GNOME-FullName=Firefox Web Browser
Exec=/home/username/programs/firefox/firefox %u
Terminal=false
X-MultipleArgs=false
Type=Application
Icon=/home/username/programs/firefox/browser/icons/mozicon128.png
Categories=Network;Webbrowser;
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/vnd.mozilla.xul+xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;
StartupWMClass=Firefox-bin
StartupNotify=true

To make it the default system-wide, manually add it to Xfce’s Preferred applications or your window manager’s equivalent. Also add it to Debian’s alternative system.

$ sudo update-alternatives --install /usr/bin/x-www-browser x-www-browser /home/username/programs/firefox/firefox 20
[sudo] password for username: 
$ sudo update-alternatives --config x-www-browser
There are 2 choices for the alternative x-www-browser (providing /usr/bin/x-www-browser).

  Selection    Path                                   Priority   Status
------------------------------------------------------------
* 0            /usr/bin/iceweasel                      70        auto mode
  1            /home/monkey/programs/firefox/firefox   20        manual mode
  2            /usr/bin/iceweasel                      70        manual mode

Press enter to keep the current choice[*], or type selection number: 1
update-alternatives: using /home/monkey/programs/firefox/firefox to provide /usr/bin/x-www-browser (x-www-browser) in manual mode

It’s not very pretty, but it does the trick.

Comments

The Yetimology of Vaseline

To fill the void, here’s a silly little crosspost.


vaseline

As is well known, most Dutch-American women in the early American republic were called Eline, including all of Martin van Buren’s female relatives. After the Revolutions of 1848, many Germans emigrated to the United States, and they all quickly fell in love with these attractive Dutch-American women. In turn enamored with the sculpted musculature of these Prussian emigrees, many an Eline answered in the affirmative following a marriage proposal. Unfortunately, as the romance wore off so did the ability to surpass the linguistic distance between Dutch and German, and throughout New York City calls of “Was, Eline?” could be frequently heard.

When Robert Chesebrough first came to America, he was welcomed by a German-immigrant greeter. Unfortunately for Robert, the unknown German-American had become rather used to saying “Was, Eline?” instead of simply “was” or “what”. So when Robert asked our immigrant to describe the spirit of American optimism in one word, he answered “vaseline?” The German immigrant hadn’t understood the question, but Robert thought he’d obtained the perfect brand name for a future product. And the rest, as they say, is history.

Comments

Stop Hijacking Annoyances

Here’s another rescue from My Opera. The script was compiled by the user pehage on February 3, 2012, after a quick pointer by me. It is Opera UserJS, so it won’t work in any other browser. Don’t forget to enable User JavaScript on HTTPS if you so desire. In Opera 12 it no longer pops up a warning all the time.

// ==UserScript==
// @name Stop Hijacking Annoyances
// @include *
// ==/UserScript==

var annoyances = ["focus", "focusin", "focusout", /*"click",*/ "dblclick", "mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "select", "submit", "keydown", "keypress", "keyup"];

for (var i=0; i<annoyances.length; i++) {
	//opera.postError(annoyances[i]);
	window.opera.addEventListener("BeforeEventListener." + annoyances[i], function (e) {
		e.preventDefault();
		//opera.postError(e);
	}, false);
}

And here is a simple testcase I created.

<!DOCTYPE html>
<html>
<head>
<title>keypress hijacker</title>
<script>
document.addEventListener('keypress', function(e){alert('document.addEventlistener on keypress ' + String.fromCharCode(e.which))}, false);
window.onkeydown = function(e){alert('window.onkeydown ' + String.fromCharCode(e.which))}
</script>
</head>
<body>
This page hijacks keypress events in order to display an alert message.
</body>
</html>

Comments

Soft Cheese

A play for two actors.

Act 1

A rustic village in Normandy. Candide, a recent émigré from the big city, has become quite fond of the rural lifestyle.

Candide enters the local cheese shop. Gaute, the store owner, looks up at the sound of the bell.

Gaute
Good morning, Candide! It’s so nice to welcome you again to my humble realm.
Candide
I inspected my garden this morning and some wonderful champignons matured nicely. Last week I ate mushrooms with Vieuxchatêl as per your suggestion, but I’m looking for a different type of local soft cheese today. Do you have any recommendations?
Gaute
Certainly, a new cheesemaker opened up shop in this area recently! They call it La Vache Qui Pleure. Would you like to sample some?
Candide
No thanks Gaute, that’s alright. Your judgment in these matters has been absolutely impeccable in these past few months, and whatever you recommend is always a culinary delight.
Gaute
Very well! That’ll be €3 please.
Candide
Here you are. Thank you so much!

Candide exits the store.

Act 2

The next morning, an angry Candide rushes into the store. The bell jingles violently.

Candide
Why didn’t you tell me this was processed cheese?! Why did you lie to me?
Gaute
Lied? But monsieur Candide, this is locally produced soft cheese, just like you asked for.
Candide
I don’t care if it’s technically soft and produced locally. You knew very well I wanted a real local cheese and you gave me this mass-produced junk.
Gaute
But the product I sold you already had artificial penicillium camemberti flavor added to it!
Candide
Artificial fungus flavor? Are you insane? You lied to me.
Gaute
You will quit making these libelous remarks! And besides, the cheese was never truly without fungus. You could have sprinkled some fungus on it yourself and it might’ve taken to the cheese.
Candide
You’re insane. This is no proper cheese.
Gaute
Sir, I will take you to court for libel and slander.
Candide
Do your worst. The truth is plain for all to see.

Act 3

A courtroom. Candide is down on his knees in front of a judge in court dress with his eyes toward the ground.

Judge
Are you Candide, the man who libeled against Gaute the cheese store owner?
Candide
I am Candide, but I did not libel against Gaute. He promised me a local soft cheese and he gave me mass-produced processed cheese.
Judge
But it was a local product. Your libelous lies make a mockery of this court.

Candide finally looks up at the judge. His face changes into a shocked expression of understanding.

Candide
You!
Gaute
In a small rural village such as this, I have to take on the role of judge, jury, and sometimes executioner.
Candide
This is a travesty of justice!
Gaute
The accused shall not yell in court.
Candide
You did not sell me proper cheese!
Gaute
The defendant is found guilty of libel. Gaute did sell cheese.
Candide
But proper cheese does not contain emulsifiers! It has the right fungus growing on the outside! It does not stay good for weeks while unrefrigerated!
Gaute
The defendant is also found guilty of trolling.
Candide
You cannot be serious. Until this very week you never sold me cheese with such artificial properties and additives. You sold me real cheese.
Gaute
The court finds itself obliged to forgive you for your ignorance of what is technically allowed to qualify as cheese. But by insisting on your own definition of this so-called proper cheese, you have proved yourself guilty of conceitedness. Bailiff, this man shall hang for the crime of being full of himself. Prepare the gallows.

Fade to black.

Comments (1)

Torn Apart

Here’s a silly “children’s story” I wrote around mid December. It’s a bit rough around the edges, probably a bit odd, and too graphical. So it goes.


It was a perfectly nice day when Esprit stumbled and broke his front right leg. When the little boy did not find the horse at his trough a few hours later he thought nothing of it; sometimes Esprit would run off and play and forget about dinner. To make up for lost time, the boy promised himself he’d visit the horse the next morning as early as he could.

The next day he noticed with mild consternation that the horse did not appear to be in his stable. The boy went out to search the property, trying to remember what were Esprit’s favorite spots. He remembered the open hill, where Esprit would often lie in the sun.

Time stood still when the little boy discovered what had happened. Whether running back to his house, alerting his parents, and calling the veterinarian helicopter emergency service took minutes or hours was not a question he’d be able to answer, but the sky was already turning red as a thunderous cavalcade of chopped air signaled the arrival of the helicopter. Esprit was harnessed in with great care so the broken leg wouldn’t shift, was given a sedative, and was swiftly lifted up. The little boy waved at the horse, certain the veterinarian would be able to mend and properly set Esprit’s leg.

Unbeknown to the helicopter pilot, distracted by the glint of the setting sun in her eye, the rescue pulley system malfunctioned and started lowering the horse just as the helicopter was starting to pick up speed. The little boy stared in wide-eyed horror as Esprit started bouncing left and right from tree to tree. Gapes and gashes appeared and started to bleed, and despite the sedative the poor horse woke up when its broken leg got stuck between the top branches of a particularly sturdy evergreen tree and was torn off like a cocktail pricker. The horse started to howl the most agonizing scream the little boy would ever hear, but that was not the end of Esprit’s misery. A particularly sharp branch a few trees over impaled the horse, which consequently produced a guttural, almost stuttering, and above all angry sound. The pilot finally noticed that something was awry when the helicopter refused to go forward anymore, but it was already too late. The surprisingly elastic tree finally gave up under the barrage of the helicopter’s brute force and snapped like a twig, propelling the helicopter on its now downward trajectory into the trees. Like in the Hollywood movies the little boy loved so much, the rotored machine made a squealing noise before exploding in a fiery ball of death.

The little boy was finally able to break out of his trance, and he collapsed into a sobbing mass. For months after he was bedridden, and each night he claimed Esprit came limping to his window while floating through the air, on three legs and a bloody stump, with mad, bloodshot eyes. The boy’s parents were worried sick, and their little boy’s German-accented psychiatrist was having the time of his life writing article after article about the little boy’s disturbed unconscious. When they found the broken window and the little boy’s dead body with the missing right arm, the autopsy report bluntly stated he had inflicted all these injuries on himself—that he had torn off his own arm. The hoofmarks all over his body were left out of the official report. The estate was up for sale the next day already, and no one’s lived there in sixty years. The locals are still weary of the place where the little boy died, but a real-estate developer drove by the other day and is planning to turn it into a hotel. The organization he represented put up a sign already: “The Hillcrest Hotel. Opening in May 2016, just in time for the holidays!”

Comments (1)

Data URI Bookmarklet

This is a repost of something I wrote on My Opera forums on 8 January 2013. The My Opera forums will be shutting down on 1 March 2014.


The easiest method [to base64 encode an image] might be to use the newly supported (since v12) FileReader object, which has a readAsDataURL method. I wrote a quick proof of concept using XHR because I’m not really sure how else to get data to it (aside from a file input which is no good here). Alternatively you could load the image in a canvas and call toDataURL, but then you’ll lose stuff like metadata and I’m not even sure if the compression and such will remain the same.

var xhr = new XMLHttpRequest(),
	reader = new FileReader();
xhr.open('GET', location.href, true);
xhr.responseType = 'blob';
xhr.addEventListener('load', function () {
	if (xhr.status === 200) {
		reader.addEventListener('load', function (e) {
			location.href = e.target.result;
		});
		var responseWithMimeType = new Blob(new Array(xhr.response), {
			'type': xhr.getResponseHeader('Content-Type')
		});
		reader.readAsDataURL(responseWithMimeType);
	}
});
xhr.send();

It does seem a bit roundabout, so I’d love to hear it if anyone has more efficient suggestions.

I just realized you may not be aware of how to make a bookmarklet. To save myself the effort of removing comments and such I used http://chris.zarate.org/bookmarkleter to obtain the following result:

javascript:(function(){var%20xhr=new%20XMLHttpRequest(),reader=new%20FileReader();xhr.open('GET',location.href,true);xhr.responseType='blob';xhr.addEventListener('load',function(){if(xhr.status===200){reader.addEventListener('load',function(e){location.href=e.target.result;});var%20responseWithMimeType=new%20Blob(new%20Array(xhr.response),{'type':xhr.getResponseHeader('Content-Type')});reader.readAsDataURL(responseWithMimeType);}});xhr.send();})();

Create a new bookmark, paste that into the URL and give it a nickname of your choice.

Easier still, just drag this or right-click, bookmark link: To Data URI. There might be some security restrictions on MyOpera against javascript links, so I’m not sure if it’ll work.

But for local files a simple page would do just fine. All you need is input type=”file” and a tiny bit of scripting similar to the above. Something like this should do:

<!DOCTYPE html>
<html>
<head>
<title>File to Data URL</title>
</head>
<body>

<form>
<input type="file">
</form>

<script>
var file = document.querySelector('input[type=file]'), reader = new FileReader();

file.addEventListener('change', function(e) {
	reader.readAsDataURL(e.target.files[0]);
});

reader.addEventListener('load', function(e) {
	location.href = e.target.result;
});
</script>

</body>
</html>

Comments

Adding a User Font Size Preference to Simple Machines

Users should be able to choose their default font size easily through their browser, and their choice should be respected. But because all browsers default to 16px and most people never change the default, many sites—including Wikipedia—set their font-size to about 80% of that value to end up at a font size of 12px or 13px. Thus users might be prevented from lowering their font size on a site that actually respects their preferences if they so desire.

Because I don’t believe that either joining the 80% crowd or specifying a size in pixels is generally the right answer, I decided to add a user preference that overrides the font-size on the HTML element. For this to work as intended, your stylesheet needs to be entirely in percentages, em, rem, or use equivalent keywords like small and large.

First, in Admin > Configuration > Core Features, enable Advanced Profile Fields. Then, you can add custom profile fields in Admin > Configuration > Features and Options > Profile Fields. I added one named “Font size” with an input mask of /^[0-9]{1,3}(\.[0-9]{1,2})?(em|pt|px|%)$/.

Assuming your theme is based on the default, start editing index.template.php. Then, under the linked stylesheets you can put the override:

	// Custom user font-size
	if ($context['user']['is_logged']) {
		global $user_profile;
		loadMemberData($context['user']['id']);
		
		if (!empty($user_profile[$context['user']['id']]['options']['cust_fontsi'])) {
			echo '
	<style>', 'html {font-size: ', $user_profile[$context['user']['id']]['options']['cust_fontsi'], '}', '</style>';
		}
	}

To find out exactly what kind of useful values you can obtain from e.g. $context and $user_profile, you can use var_dump($user_profile).

Comments

Older Entries »