From: Bartosz Dziewoński Date: Wed, 17 Sep 2014 21:14:46 +0000 (+0200) Subject: CSSMin: Do not base64-encode non-binary files when embedding X-Git-Tag: 1.31.0-rc.0~13744^2 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dcompta/comptes/journal.php?a=commitdiff_plain;h=fa223d65d6293170b25a81770ae943936ffb7589;p=lhc%2Fweb%2Fwiklou.git CSSMin: Do not base64-encode non-binary files when embedding Do not base64-encode non-binary files (containing only whitespace and printable ASCII characters, which matches sane SVG files). For SVG files the percent-encoded URIs are actually slightly longer than the base64-encoded ones (~10%), but compress a lot better resulting on 15-20% less data to transfer after gzip compression. (The effect is best seen on the 'oojs-ui' module, which consists mostly of SVG icons – especially after commenting out everything other than 'oojs-ui.svg.css'.) I tried this for binary files too, just in case; but as expected, they suffer from a noticeable size increase even with compression (~15%). Bug: 67341 Change-Id: Iddaf863b6be98570a2bb8e606f13946a96345f65 --- diff --git a/RELEASE-NOTES-1.25 b/RELEASE-NOTES-1.25 index 04a215c432..d05a61fe7c 100644 --- a/RELEASE-NOTES-1.25 +++ b/RELEASE-NOTES-1.25 @@ -15,6 +15,9 @@ production. for 'languageScripts'. * Added a new hook, "ContentAlterParserOutput", to allow extensions to modify the parser output for a content object before links update. +* (bug 67341) SVG images will no longer be base64-encoded when being embedded + in CSS. This results in slight size increase before gzip compression (due to + percent-encoding), but up to 20% decrease after it. === Bug fixes in 1.25 === diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php index c69e79f5b2..91f2c61e32 100644 --- a/includes/libs/CSSMin.php +++ b/includes/libs/CSSMin.php @@ -38,6 +38,7 @@ class CSSMin { * which when base64 encoded will result in a 1/3 increase in size. */ const EMBED_SIZE_LIMIT = 24576; + const DATA_URI_SIZE_LIMIT = 32768; const URL_REGEX = 'url\(\s*[\'"]?(?P[^\?\)\'"]*?)(?P\?[^\)\'"]*?|)[\'"]?\s*\)'; const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/'; const COMMENT_REGEX = '\/\*.*?\*\/'; @@ -100,10 +101,11 @@ class CSSMin { } /** - * Encode an image file as a base64 data URI. - * If the image file has a suitable MIME type and size, encode it as a - * base64 data URI. Return false if the image type is unfamiliar or exceeds - * the size limit. + * Encode an image file as a data URI. + * + * If the image file has a suitable MIME type and size, encode it as a data URI, base64-encoded + * for binary files or just percent-encoded otherwise. Return false if the image type is + * unfamiliar or file exceeds the size limit. * * @param string $file Image file to encode. * @param string|null $type File's MIME type or null. If null, CSSMin will @@ -111,7 +113,7 @@ class CSSMin { * @param int|bool $sizeLimit If the size of the target file is greater than * this value, decline to encode the image file and return false * instead. If $sizeLimit is false, no limit is enforced. - * @return string|bool: Image contents encoded as a data URI or false. + * @return string|bool Image contents encoded as a data URI or false. */ public static function encodeImageAsDataURI( $file, $type = null, $sizeLimit = self::EMBED_SIZE_LIMIT @@ -125,8 +127,23 @@ class CSSMin { if ( !$type ) { return false; } - $data = base64_encode( file_get_contents( $file ) ); - return 'data:' . $type . ';base64,' . $data; + + $contents = file_get_contents( $file ); + // Only whitespace and printable ASCII characters + $isText = (bool)preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ); + + if ( $isText ) { + // Do not base64-encode non-binary files (sane SVGs), unless that'd exceed URI length limit. + // (This often produces longer URLs, but they compress better, yielding a net smaller size.) + $uri = 'data:' . $type . ',' . rawurlencode( $contents ); + if ( strlen( $uri ) >= self::DATA_URI_SIZE_LIMIT ) { + $uri = 'data:' . $type . ';base64,' . base64_encode( $contents ); + } + } else { + $uri = 'data:' . $type . ';base64,' . base64_encode( $contents ); + } + + return $uri; } /** diff --git a/tests/phpunit/data/cssmin/circle.svg b/tests/phpunit/data/cssmin/circle.svg new file mode 100644 index 0000000000..6b7d1afd87 --- /dev/null +++ b/tests/phpunit/data/cssmin/circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tests/phpunit/includes/libs/CSSMinTest.php b/tests/phpunit/includes/libs/CSSMinTest.php index 43c5086950..4a1885af89 100644 --- a/tests/phpunit/includes/libs/CSSMinTest.php +++ b/tests/phpunit/includes/libs/CSSMinTest.php @@ -147,9 +147,12 @@ class CSSMinTest extends MediaWikiTestCase { // Full paths start with http://localhost/w/. // Timestamps in output are replaced with 'timestamp'. - // data: URIs for red.gif and green.gif + // data: URIs for red.gif, green.gif, circle.svg $red = 'data:image/gif;base64,R0lGODlhAQABAIAAAP8AADAAACwAAAAAAQABAAACAkQBADs='; $green = 'data:image/gif;base64,R0lGODlhAQABAIAAAACAADAAACwAAAAAAQABAAACAkQBADs='; + $svg = 'data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A' + . '%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%228%22%20height%3D' + . '%228%22%3E%0A%3Ccircle%20cx%3D%224%22%20cy%3D%224%22%20r%3D%222%22%2F%3E%0A%3C%2Fsvg%3E%0A'; return array( array( @@ -233,6 +236,12 @@ class CSSMinTest extends MediaWikiTestCase { 'foo { /* @embed */ background: url(large.png); }', "foo { background: url(http://localhost/w/large.png?timestamp); }", ), + array( + 'SVG files are embedded without base64 encoding', + 'foo { /* @embed */ background: url(circle.svg); }', + "foo { background: url($svg); " + . "background: url(http://localhost/w/circle.svg?timestamp)!ie; }", + ), array( 'Two regular files in one rule', 'foo { background: url(red.gif), url(green.gif); }',