The One with the Thoughts of Frans

Archive for Browsers

Web Apps Opera Bork Edition

It’s been 18 years since Opera published their classic bork edition, to protest that MSN would have loaded properly in the browser if only it were served the same code as Internet Explorer.

“Hergee berger snooger bork,” says Mary Lambert, product line manager desktop, Opera Software. “This is a joke. However, we are trying to make an important point. The MSN site is sending Opera users what appear to be intentionally distorted pages. The Bork edition illustrates how browsers could also distort content, as the Bork edition does. The real point here is that the success of the Web depends on software and Web site developers behaving well and rising above corporate rivalry.”

Since I’m a Vivaldi user, today’s the first time I noticed that Chrome on Android artificially restricts installing webpages as apps on the homescreen. Only webpages that specify a manifest.json can receive such a hallowed treatment, instead of every single webpage ever made. For the rest of the internet, there’s only a shortcut. While the situation is not quite comparable, I found the design principle sufficiently distasteful to revive Opera’s classic bork script, in this case specifically targeting the Chrome browser.

You can put it on your website or in your TamperMonkey to remind you when you accidentally open Chrome. The classic result looks like this:

A borked Vivaldi announcement as seen in Chrome.
// http://web.archive.org/web/20050301075735/http://www.opera.com/js/bork/enchefizer.js

/* -*- mode: C++; mode: font-lock; tab-width: 4 -*-
 * 2003-02-10
 *
 * The Enchefizer code is based on a script fetched from 
 *   http://tbrowne.best.vwh.net/chef/
 * written by Andriy Rozeluk , which is
 * based on a Java version written by Josh Vura-Weis
 * , which is based on a UNIX version
 * from 1993 written by John Hagerman  and
 * Jeff Allen 
 *
 * Subsequently hacked by Opera Software to work inside a page by 
 * traversing the DOM tree, and to improve performance. 
 *
 * Typical usage is to add the following text to the bottom of a page:
 *       
 */

const classicOperaBork = () => {
    /* USER CONFIGURATION BEGINS */
    var victim=false;   // false (apply to any page) or regex to match page URL
    //var victim=/^http:\/\/(?:(?:www|msdn).microsoft.com|www.msn.com)/;
    var delay=50;       // ms between replacements, set to 0 to disable waiting
    var units=30;       // number of text nodes to translate each time
    var highlight=true; // highlight the text we're working on
    /* USER CONFIGURATION ENDS */

    var textnodes=[];   // text nodes in the doc
    var nextnode=0;     // next node to process

    function nextWordPos(line)
    {
		var p = line.search(/[ \n\t\\,<.>/?;:\'\"\[{\]}|=+\-_!@#$%^&*()~`]/);
        return p == -1 ? line.length+1 : p;
    }

    function encheferizeLine(line)
    {
		var buff="", word="", t="", out="", wp;

		while(line.length > 0)
		{
			wp = nextWordPos(line);
			word = line.substring(0,wp);
			t = line.charAt(wp);
			line = line.substring(wp+1,line.length);
			out = out + encheferizeWord(word) + t;
		}
		if(t == ".")
		{
	        out = out + "\nBork Bork Bork!";
		}

		return out;
    }

    function encheferizeWord(word)
    {
		if(word.toLowerCase() == "bork") return word;
      
		var letter, count, len, buff, i_seen, isLast;
      
		count=0;
		len=word.length;
		buff=""
		i_seen=false;
      
		while(count0){
               
				} 
			} else if(letter=='t'){
				if(count==len-2 && word.charAt(count+1)=='h'){
					buff = buff + "t";
					count+=2;
					continue;
				} else if(count<=len-3 && word.charAt(count+1)=='h'
						  && word.charAt(count+2)=='e'){
					buff = buff + "zee";
					count+=3;
					continue;
				} 
			} else if(letter=='T' && count<=len-3 && word.charAt(count+1)=='h'
					  && word.charAt(count+2)=='e'){                                                
				buff = buff + "Zee";
				count+=3;
				continue;
			} else if(letter=='v'){
				buff = buff + "f";
				count++;
				continue;
			} else if(letter=='V'){
				buff = buff + "F";
				count++;
				continue;
			} else if(letter=='w'){
				buff = buff + "v";
				count++;
				continue;
			} else if(letter=='W'){
				buff = buff + "V";
				count++;
				continue;
			}
			//End of rules.  Whatever is left stays itself
			buff = buff + letter;
			count++;
		}
		
		return(buff);
    }

    function bork()
    {
		var limit = delay == 0 ? Number.MAX_VALUE : units;
		var start=nextnode;
		var oldc = new Array();
		var n, i, candidate;

		if (highlight)
		{
			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				oldc[i] = candidate.parentNode.style.backgroundColor;
			}

			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				candidate.parentNode.style.backgroundColor = 'red';
			}
		}

		for ( i=0 ; i < limit && nextnode < textnodes.length ; nextnode++, i++ )
		{
			candidate = textnodes[nextnode];
            candidate.replaceData(0,candidate.length,encheferizeLine(candidate.data));
		}

		if (highlight)
		{
			for ( n=start, i=0 ; i < limit && n < textnodes.length ; n++, i++ )
			{
				candidate = textnodes[n];
				candidate.parentNode.style.backgroundColor = oldc[i];
			}
		}

		bork_more();
    }

    function bork_more()
    {
		if (nextnode < textnodes.length)
		{
			setTimeout( bork, delay );
		}
    }

    /* In large docs traversal is a bottleneck at startup; we could
       CPS it or otherwise reify the traversal state to interleave
       traversal with the translation.
	*/
    function find_textnodes(elm, acc)
    {
		if (elm.nodeType == 3)
		{
			if (!elm.data.match(/^[\s\n\r]*$/))
			{
				acc.push(elm);
			}
		}
		else
		{
			var c = elm.childNodes;
			for ( var i=0 ; i < c.length ; i++ )
			{
				find_textnodes(c.item(i),acc);
			}
		}
		return acc;
    }

    /* run page's onload handler, then do our thing */
    var res = false;

    if (typeof old_onload == "function")
	{
		res = old_onload();
	}

    if (/*window == top &&*/ (!victim || window.location.href.match(victim)) )
    {
		textnodes = find_textnodes(document.body, new Array());
		nextnode=0;
		bork_more();
    }
    return res;
}
if (window.navigator.userAgentData.brands.filter(e => e.brand === 'Google Chrome').length > 0) {
    document.addEventListener('DOMContentLoaded', classicOperaBork);
}

CommentsTags:

Linkify Addressbar Segments in Vivaldi

Great news! My suggestion for segmented addressbars from Opera 11 Addressbar Revisited has been picked up by Vivaldi. Presumably independently, but this is the kind of development I like to see.

CommentsTags: ,

I’m not sure what to think of the Vivaldi crash icon

The poor thing.

CommentsTags: ,

Disable Firefox Fullscreen Animation

Go to about:config, as ever. Then disable browser.fullscreen.animate.

CommentsTags:

Image Optimization Guide

On the forum I administer, I am forced to run a tight attachment policy. Disk space doesn’t grow on trees. Occasionally this leads to questions about the small attachment size limit of 50 KiB. This guide is intended to clarify that this is not nearly as tiny as you might think. Note that although I’ll mention commands without much explanation for the sake of brevity, you’re always recommended to further explore the possibilities offered by those commands with the --help flag as well as by running man the-command-here.

First you need to ask yourself what kind of file type is appropriate, if you have the choice. On screenshots, the main purpose of attachments on my forum, you’ll often encounter large areas of uniform background colors. PNG is therefore almost invariably the right choice. Crop out everything but what’s relevant. JPEG is appropriate for more dynamic pictures such as photographs. If you want to do a lot with photographs, you might want to consider an external hosting service. My wife likes SmugMug. Still, for thumbnails you might be able to do a fair bit more within a few hundred KiB than you might think. Finally, the vector graphics in SVG result in pictures that always look sharp. You’ll typically have drawn these in a program like Inkscape or Adobe Illustrator.

Table of Contents

  1. 1. Optimizing JPEG
  2. 2. Optimizing PNG
  3. 3. Optimizing SVG
  4. Addendum A: Scanned Documents
  5. Addendum B: Video

1. Optimizing JPEG

Often you’ll want to crop your file. Do not edit your JPEG followed by resaving it because this will result in reduced quality! You can crop losslessly with cropgui. On Windows you can use IrfanView.

If you don’t want to crop, and also potentially for some post-cropgui optimization, use jpegtran -copy none -progressive -optimize file.jpg > file-opt.jpg. Note that this will get rid of all metadata, which may be undesirable. If so, use jpegtran -copy all -progressive -optimize file.jpg > file-opt.jpg.

Of course if you want to scale down your JPEG there’s no point in mucking about with lossless cropping first. After scaling down, check how low your quality can go (also see a little helper script I wrote). In any case, you should avoid introducing any unnecessary compression steps with associated quality loss. Here are some results:

  • The original 11.jpg at 2.19 MB.
  • Losslessly cropped 11-crop.jpg at 1.11 MB.
  • Optimized with -copy all -progressive -optimize 11-crop-opt.jpg at 1.04 MB. -copy none would’ve saved an extra whopping 40-some KiB, which on this kind of filesize has little benefit, and besides, I quite like the metadata. For thumbnail-sized files the balance is likely to be different. For example, the 52.2 KiB SmugMug auto-generated thumbnail below can be insignificantly reduced to 51.1 KiB with --copy all, but to 48.2 KiB with --copy none. I think an 8% reduction is not too shabby, plus it brings the file size down to under the arbitrary 50 KiB limit on my forum.

2. Optimizing PNG

As I wrote in the introduction, for screenshots PNG is typically the right choice. If you want to use lossless PNG, use optipng -o7. In my experience it’s ever so slightly smaller than other solutions like pngcrush. But as long as you use a PNG optimizer it shouldn’t much matter which one you fancy. Also see this comparison.

If you don’t care about potentially losing some color accuracy, use pngquant instead. To top it off, if you really want to squeeze out your PNG, you can pass quality settings with --quality min-max, meaning you can pass --quality 30-50 or just --quality 10. Here are some quick results for the screenshot in the SVG section below, but be sure to check out the pngquant website for some impressive examples.


$ du -h --apparent-size inkscape-plain-svg.png
27K	inkscape-plain-svg.png

$ du -h --apparent-size inkscape-plain-svg-fs8\ default.png 
7.6K	inkscape-plain-svg-fs8 default.png

$ du -h --apparent-size inkscape-plain-svg-fs8\ quality\ 10.png 
4.3K	inkscape-plain-svg-fs8 quality 10.png

In this case there is no visual distinction between the original PNG and the default pngquant settings. The quality 10 result is almost imperceptibly worse unless you look closely, so I didn’t bother to include a sample.

3. Optimizing SVG

For using SVG on the web, I imagine I don’t have to tell you that in Inkscape, you should save your file as Plain SVG.

Save as Plain SVG in Inkscape.

What you may not know is that just like there are lossy PNGs, you can also create what amounts to lossy SVGs. There are some command-line tools to optimize SVGs, including (partially thanks to this SO answer):

  • Scour is probably the best command line tool for some quick optimization. You can just use the defaults like scour < in.svg > out.svg or scour -i in.svg -o out.svg. But I recommend you go further.
  • SVGO (SVG Optimizer)
  • SVG-optimiser (by Peter Collingridge)
  • SVG-editor (by Peter Collingridge)

My personal preference for squeezing out every last byte goes toward the web-based version of the SVG-editor by Peter Collingridge. By running it in a browser with inferior SVG support such as Firefox, you’ll be sure that your optimized SVG still works properly afterward. The command line tools can only safely be used for basic optimizations, whereas the effects of going lossy (such as lowering precision) can only be fully appreciated graphically.

Addendum A: Scanned Documents

Scanned documents are a different item altogether. The best format for private use is DjVu, but for public sharing PDF is probably preferable. To achieve the best results, you should scan your documents in TIFF or PNG, followed by processing with unpaper or ScanTailor. If you’ve already got a PDF you’d like to improve, you can use pdfsandwich or my own readablepdf.

Addendum B: Video

I’m not aware of any lossless optimization for video compression such as offered by jpegtran, but you can often losslessly cut video. In the general purpose editor Avidemux, simply make sure both video and audio are set to copy. There is also a dedicated cross-platform app for lossless trimming of videos called, unsurprisingly, LosslessCut. If you do want to introduce loss for a smaller file size you can use the very same Avidemux with a different setting, ffmpeg, mpv, VLC, and so forth. You can get reasonable quality that’ll play many places with something like:

ffmpeg -i input-file.ext -c:v libx264 -crf 19 -preset slow -c:a libfaac -b:a 192k -ac 2 output-file.mp4

For the open WebM format, you can use something along these lines:

ffmpeg -i input.mp4 -c:v libvpx -b:v 1M -c:a libvorbis output.webm

More examples on the ffmpeg wiki. Note that in many cases you should just copy the audio using -acodec copy, but of course that’s not always an option. Extra compression artifacts in audio detract significantly more from the experience than low-quality video.

Comments (2)Tags:

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

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

Textarea Backup Localstorage v1.21

I figured I’d drop a note that I updated my Textarea Backup UserJS last month. What follows is the description from ExtendOpera.


Textarea Backup Localstorage

Retains what you type into textareas and contentEditable elements.

This script is only compatible with Opera 10.50 and up. If you need to use it with an older version use Textarea Backup but be advised that it comes with some disadvantages.

  1. Can automatically place previously typed text in textareas.
  2. Can add an unobtrusive menu in the top right corner of textareas

Actions menu screenshot (note the transparent object in the top right corner of the textarea):

Development can be followed on GitHub. Don’t be shy, open an issue or send me a pull request if you think you have something to contribute! 😉

Changelog

1.21 July 25, 2013. Sorry, I was a bit hasty about that last one. I passed my testcase, but only noticed that many contentEditables work differently in practice on this very site.

  • Fixed a bug that occurred when BODY was contentEditable, as is typical in iframes.
  • Properly compare initial value of contentEditable element to backed up value so you’ll only get prompted to overwrite when relevant.
  • Full changelog.

1.20 July 25, 2013. I wasn’t going to make yet another release in three days, but these new features could be implemented much faster than I initially predicted.

  • Trustworthy old persistent preferences support added. I also uploaded a sample settings file.
  • Fixed the keep_after_submission bug, so setting it to false is safe again.
  • Removed the form requirement.
  • Support contentEditable. This is a pretty big one, seeing how it was the most obvious missing feature.

1.11 July 24, 2013. Added configuration switches for the new feature.

  • For Opera 11.6x and Opera 12.x it now defaults to off because of potential performance concerns.

1.10 July 23, 2013. Added support for dynamically added textareas.

  • This new feature will only work in Opera 11.6x and up.

Comments

How To Fix SVG Height in Webkit/Blink

It’s pretty simple: also specify a height if you specify a width. To quote from the workaround in my stylesheet, which I added on account of my previous post:

figure svg {
	width:100%;
	/*what follows is because Webkit/Blink is broken https://bugs.webkit.org/show_bug.cgi?id=68995*/
	height:100%
}

I really shouldn’t have added that as I don’t cater to broken browsers anymore, but I have a bit of a soft spot for Opera—even if it’s never required any workarounds before. Presto is clearly superior, and so is Gecko. Chromium still doesn’t support SVG favicons. Gecko does. And just look at this table. Unfortunately Gecko doesn’t support SVG fonts, so it’s all looking pretty miserable without Presto.

Comments (1)Tags:

Why Opera’s XHTML Error Handling Is Superior

I found this old, unfinished post in my drafts. I’m not quite sure when I originally wrote it, but it was over a year ago. Rather than updating the content I decided to publish it as is, as I’m not sure why I didn’t, with a small addendum at the end.


I made a little compilation of the various error messages displayed by browsers upon encountering an XML syntax error. Firefox (Gecko) has the unfriendly looking error on top, Chromium (Webkit) renders the page up to the error, but shows a large error message (albeit not at all useful like in Opera & Fx), and for Opera I included 10.10 and the latest 10.50 pre-alpha build. Note that it’s just the styles behind the error message that changed a bit: the content and helpfulness of the error message is still the same. I’ll run it down a bit more:

  • Firefox displays an error message that’s only useful if you already know sufficiently much about X(HT)ML, whereas Opera’s error message not only highlights more clearly where parsing failed — although ultimately this difference might just be one of preference. More important, its error message might just helpfully link you precisely where you need to go to learn how to avoid it. When I first started messing about with XHTML back in ’03 or so, I probably would’ve appreciated it if Opera had done that. At the time Opera behaved the same as Fx does now.
  • Chromium displays an error message that doesn’t even manage to clearly indicate what’s the problem. This compares negatively to Fx and Opera highlighting the &.
  • Chromium renders the page up to the problem, which may result in a get out of jail free card. The error message doesn’t seem very annoying, but if the error is in the middle of the page it’ll still be in the way. In my sample page it’s at the end, however. (My example page is basically a standard installation of phpGraphy on which I decided to switch to application/xml+xhtml because it claims to be more or less XHTML compliant now — I had to fix all the unclosed meta and link tags first.)
  • Despite rendering the page, you won’t be able to see the page fully in Chromium. You will with Opera’s reparse as HTML function.

I hope that clarifies why I think Opera’s handling is best, both as a user and as an author.


This blog post is now outdated. You can return to the behavior I hailed by disabling the opera:config#UserPrefs|AutomaticallyreparseXHTMLwithparsingerrorsasHTML option.

Comments

Older Entries »