From f04dbcaf2e43e1f5558c307ee5f575c8c28bb210 Mon Sep 17 00:00:00 2001
From: Brendan Dahl <brendan.dahl@gmail.com>
Date: Mon, 15 Jul 2013 15:37:03 -0700
Subject: [PATCH 1/2] Use dummy font for testing when pdf fonts are loaded.

---
 src/fonts.js | 205 ++++++++++++++++++++++++---------------------------
 1 file changed, 95 insertions(+), 110 deletions(-)

diff --git a/src/fonts.js b/src/fonts.js
index 184737d4b..9800967eb 100644
--- a/src/fonts.js
+++ b/src/fonts.js
@@ -538,7 +538,50 @@ function mapPrivateUseChars(code) {
 }
 
 var FontLoader = {
+  insertRule: function fontLoaderInsertRule(rule) {
+    var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG');
+    if (!styleElement) {
+        styleElement = document.createElement('style');
+        styleElement.id = 'PDFJS_FONT_STYLE_TAG';
+        document.documentElement.getElementsByTagName('head')[0].appendChild(
+          styleElement);
+    }
+
+    var styleSheet = styleElement.sheet;
+    styleSheet.insertRule(rule, styleSheet.cssRules.length);
+  },
 //#if !(MOZCENTRAL)
+  get loadTestFont() {
+    // This is a CFF font with 1 glyph for '.' that fills its entire width and
+    // height.
+    return shadow(this, 'loadTestFont', atob(
+      'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQ' +
+      'AABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwA' +
+      'AAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbm' +
+      'FtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAA' +
+      'AADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6A' +
+      'ABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAA' +
+      'MQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAA' +
+      'AAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAA' +
+      'AAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQ' +
+      'AAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMA' +
+      'AQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAA' +
+      'EAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAA' +
+      'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAA' +
+      'AAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgc' +
+      'A/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF' +
+      'hYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQA' +
+      'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' +
+      'ABAAAAAAAAAAAD6AAAAAAAAA=='
+    ));
+  },
+
+  loadTestFontId: 0,
+
   loadingContext: {
     requests: [],
     nextRequestId: 0
@@ -611,128 +654,79 @@ var FontLoader = {
     return request;
   },
 
-  // Set things up so that at least one pdfjsFontLoad event is
-  // dispatched when all the @font-face |rules| for |fonts| have been
-  // loaded in a subdocument.  It's expected that the load of |rules|
-  // has already started in this (outer) document, so that they should
-  // be ordered before the load in the subdocument.
   prepareFontLoadEvent: function fontLoaderPrepareFontLoadEvent(rules,
                                                                 fonts,
                                                                 request) {
       /** Hack begin */
-      // There's no event when a font has finished downloading so the
+      // There's currently no event when a font has finished downloading so the
       // following code is a dirty hack to 'guess' when a font is
-      // ready.  This code will be obsoleted by Mozilla bug 471915.
-      //
-      // The only reliable way to know if a font is loaded in Gecko
-      // (at the moment) is document.onload in a document with
-      // a @font-face rule defined in a "static" stylesheet.  We use a
-      // subdocument in an <iframe>, set up properly, to know when
-      // our @font-face rule was loaded.  However, the subdocument and
-      // outer document can't share CSS rules, so the inner document
-      // is only part of the puzzle.  The second piece is an invisible
-      // div created in order to force loading of the @font-face in
-      // the *outer* document.  (The font still needs to be loaded for
-      // its metrics, for reflow).  We create the div first for the
-      // outer document, then create the iframe.  Unless something
-      // goes really wonkily, we expect the @font-face for the outer
-      // document to be processed before the inner.  That's still
-      // fragile, but seems to work in practice.
-      //
-      // The postMessage() hackery was added to work around chrome bug
-      // 82402.
+      // ready. It's assumed fonts are loaded in order, so add a known test
+      // font after the desired fonts and then test for the loading of that
+      // test font.
 
-      var requestId = request.id;
-      // Validate the requestId parameter -- the value used to construct HTML.
-      if (!/^[\w\-]+$/.test(requestId)) {
-        error('Invalid request id: ' + requestId);
+      var canvas = document.createElement('canvas');
+      canvas.width = 1;
+      canvas.height = 1;
+      var ctx = canvas.getContext('2d');
 
-        // Normally the error-function throws. But if a malicious code
-        // intercepts the function call then the return is needed.
-        return;
+      var called = 0;
+      function isFontReady(name, callback) {
+        called++;
+        // With setTimeout clamping this gives the font ~100ms to load.
+        if(called > 30) {
+          warn('Load test font never loaded.');
+          callback();
+          return;
+        }
+        ctx.font = '30px ' + name;
+        ctx.fillText('.', 0, 20);
+        var imageData = ctx.getImageData(0, 0, 1, 1);
+        if (imageData.data[3] > 0) {
+          callback();
+          return;
+        }
+        setTimeout(isFontReady.bind(null, name, callback));
       }
 
+      var loadTestFontId = 'lt' + Date.now() + this.loadTestFontId++;
+      // Chromium seems to cache fonts based on a hash of the actual font data,
+      // so the font must be modified for each load test else it will appear to
+      // be loaded already.
+      // TODO: This could maybe be made faster by avoiding the btoa of the full
+      // font by splitting it in chunks before hand and padding the font id.
+      var data = this.loadTestFont;
+      var COMMENT_OFFSET = 973;
+      var chunk1 = data.substr(0, COMMENT_OFFSET);
+      var chunk2 = data.substr(COMMENT_OFFSET + loadTestFontId.length);
+      data = chunk1 + loadTestFontId + chunk2;
+
+      var url = 'url(data:font/opentype;base64,' + btoa(data) + ');';
+      var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' +
+                 url + '}';
+      FontLoader.insertRule(rule);
+
       var names = [];
       for (var i = 0, ii = fonts.length; i < ii; i++)
         names.push(fonts[i].loadedName);
-
-      // Validate the names parameter -- the values can used to construct HTML.
-      if (!/^\w+$/.test(names.join(''))) {
-        error('Invalid font name(s): ' + names.join());
-
-        // Normally the error-function throws. But if a malicious code
-        // intercepts the function call then the return is needed.
-        return;
-      }
+      names.push(loadTestFontId);
 
       var div = document.createElement('div');
       div.setAttribute('style',
                        'visibility: hidden;' +
                        'width: 10px; height: 10px;' +
                        'position: absolute; top: 0px; left: 0px;');
-      var html = '';
       for (var i = 0, ii = names.length; i < ii; ++i) {
-        html += '<span style="font-family:' + names[i] + '">Hi</span>';
+        var span = document.createElement('span');
+        span.textContent = 'Hi';
+        span.style.fontFamily = names[i];
+        div.appendChild(span);
       }
-      div.innerHTML = html;
       document.body.appendChild(div);
 
-      window.addEventListener(
-        'message',
-        function fontLoaderMessage(e) {
-          if (e.data !== requestId)
-            return;
-          for (var i = 0, ii = fonts.length; i < ii; ++i) {
-            var font = fonts[i];
-            font.loading = false;
-          }
-          request.complete();
-          // cleanup
-          if (frame) {
-            document.body.removeChild(frame);
-          }
-          window.removeEventListener('message', fontLoaderMessage, false);
-        },
-        false);
-
-      // XXX we should have a time-out here too, and maybe fire
-      // pdfjsFontLoadFailed?
-      var src = '<!DOCTYPE HTML><html><head><meta charset="utf-8">';
-      src += '<style type="text/css">';
-      for (var i = 0, ii = rules.length; i < ii; ++i) {
-        src += rules[i];
-      }
-      src += '</style>';
-      src += '<script type="application/javascript">';
-      src += '  window.onload = function fontLoaderOnload() {\n';
-      src += '    parent.postMessage("' + requestId + '", "*");\n';
-      // Chrome stuck on loading (see chrome issue 145227) - resetting url
-      src += '    window.location = "about:blank";\n';
-      src += '  }';
-      // Hack so the end script tag isn't counted if this is inline JS.
-      src += '</scr' + 'ipt></head><body>';
-      for (var i = 0, ii = names.length; i < ii; ++i) {
-        src += '<p style="font-family:\'' + names[i] + '\'">Hi</p>';
-      }
-      src += '</body></html>';
-
-      var MAX_IFRAME_SRC_LENGTH = 1000000, IFRAME_TIMEOUT = 2000;
-      // Chrome fails for long src attributes (see issue 174023)
-      if (src.length > MAX_IFRAME_SRC_LENGTH) {
-        // ... waiting for some fixed period of time instead
-        window.setTimeout(function() {
-          window.postMessage(requestId, '*');
-        }, IFRAME_TIMEOUT);
-        return;
-      }
-
-      var frame = document.createElement('iframe');
-      frame.src = 'data:text/html,' + src;
-      frame.setAttribute('style',
-                         'visibility: hidden;' +
-                         'width: 10px; height: 10px;' +
-                         'position: absolute; top: 0px; left: 0px;');
-      document.body.appendChild(frame);
+      isFontReady(loadTestFontId, function() {
+        document.body.removeChild(div);
+        request.complete();
+      });
       /** Hack end */
   }
 //#else
@@ -4570,16 +4564,7 @@ var Font = (function FontClosure() {
                  window.btoa(data) + ');');
       var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}';
 
-      var styleElement = document.getElementById('PDFJS_FONT_STYLE_TAG');
-      if (!styleElement) {
-          styleElement = document.createElement('style');
-          styleElement.id = 'PDFJS_FONT_STYLE_TAG';
-          document.documentElement.getElementsByTagName('head')[0].appendChild(
-            styleElement);
-      }
-
-      var styleSheet = styleElement.sheet;
-      styleSheet.insertRule(rule, styleSheet.cssRules.length);
+      FontLoader.insertRule(rule);
 
       if (PDFJS.pdfBug && 'FontInspector' in globalScope &&
           globalScope['FontInspector'].enabled)

From e9f5336cc902587e35f4b50c868047c03c2124a4 Mon Sep 17 00:00:00 2001
From: Brendan Dahl <brendan.dahl@gmail.com>
Date: Wed, 17 Jul 2013 10:26:12 -0700
Subject: [PATCH 2/2] Add atob polyfill. Remove uneeded data uri polyfill.

---
 test/features/tests.js | 12 +++++++++
 web/compatibility.js   | 59 +++++++++++++++++++++---------------------
 2 files changed, 42 insertions(+), 29 deletions(-)

diff --git a/test/features/tests.js b/test/features/tests.js
index 6b1d1fb68..934760b99 100644
--- a/test/features/tests.js
+++ b/test/features/tests.js
@@ -275,6 +275,18 @@ var tests = [
     impact: 'Critical',
     area: 'Core'
   },
+  {
+    id: 'atob',
+    name: 'atob() is present',
+    run: function () {
+      if ('atob' in window)
+        return { output: 'Success', emulated: '' };
+      else
+        return { output: 'Failed', emulated: 'Yes' };
+    },
+    impact: 'Critical',
+    area: 'Core'
+  },
   {
     id: 'Function-bind',
     name: 'Function.prototype.bind is present',
diff --git a/web/compatibility.js b/web/compatibility.js
index 1b65872dd..17a36ff83 100644
--- a/web/compatibility.js
+++ b/web/compatibility.js
@@ -268,6 +268,36 @@ if (typeof PDFJS === 'undefined') {
   };
 })();
 
+// window.atob (base64 encode function) ?
+(function checkWindowAtobCompatibility() {
+  if ('atob' in window)
+    return;
+
+  // https://github.com/davidchambers/Base64.js
+  var digits =
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+  window.atob = function (input) {
+    input = input.replace(/=+$/, '');
+    if (input.length % 4 == 1) throw new Error('bad atob input');
+    for (
+      // initialize result and counters
+      var bc = 0, bs, buffer, idx = 0, output = '';
+      // get next character
+      buffer = input.charAt(idx++);
+      // character found in table?
+      // initialize bit storage and add its ascii value
+      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
+        // and if not first of each 4 characters,
+        // convert the first 8 bits to one ascii character
+        bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
+    ) {
+      // try to find character in table (0-63, not found => -1)
+      buffer = digits.indexOf(buffer);
+    }
+    return output;
+  };
+})();
+
 // Function.prototype.bind ?
 (function checkFunctionPrototypeBindCompatibility() {
   if (typeof Function.prototype.bind !== 'undefined')
@@ -283,35 +313,6 @@ if (typeof PDFJS === 'undefined') {
   };
 })();
 
-// IE9-11 text/html data URI
-(function checkDataURICompatibility() {
-  if (!('documentMode' in document) || document.documentMode > 11)
-    return;
-  // overriding the src property
-  var originalSrcDescriptor = Object.getOwnPropertyDescriptor(
-    HTMLIFrameElement.prototype, 'src');
-  Object.defineProperty(HTMLIFrameElement.prototype, 'src', {
-    get: function htmlIFrameElementPrototypeSrcGet() { return this.$src; },
-    set: function htmlIFrameElementPrototypeSrcSet(src) {
-      this.$src = src;
-      if (src.substr(0, 14) != 'data:text/html') {
-        originalSrcDescriptor.set.call(this, src);
-        return;
-      }
-      // for text/html, using blank document and then
-      // document's open, write, and close operations
-      originalSrcDescriptor.set.call(this, 'about:blank');
-      setTimeout((function htmlIFrameElementPrototypeSrcOpenWriteClose() {
-        var doc = this.contentDocument;
-        doc.open('text/html');
-        doc.write(src.substr(src.indexOf(',') + 1));
-        doc.close();
-      }).bind(this), 0);
-    },
-    enumerable: true
-  });
-})();
-
 // HTMLElement dataset property
 (function checkDatasetProperty() {
   var div = document.createElement('div');