Bug 1402932 - Export Screenshots 19.1.0 to Firefox draft
authorIan Bicking <ianb@colorstudy.com>
Mon, 25 Sep 2017 13:17:43 -0500
changeset 670811 8b36c6c1418baa7a573d084268c95257f252935b
parent 670035 33b7b8e81b4befcba503c0e48cd5370aeb715085
child 733313 027e8457a7d0f91f4ed1aa942835238bb86edc2a
push id81715
push userbmo:ianb@mozilla.com
push dateTue, 26 Sep 2017 21:38:10 +0000
bugs1402932
milestone58.0a1
Bug 1402932 - Export Screenshots 19.1.0 to Firefox MozReview-Commit-ID: A49wIA23mH9
browser/extensions/screenshots/install.rdf
browser/extensions/screenshots/moz.build
browser/extensions/screenshots/webextension/background/main.js
browser/extensions/screenshots/webextension/background/selectorLoader.js
browser/extensions/screenshots/webextension/background/startBackground.js
browser/extensions/screenshots/webextension/background/takeshot.js
browser/extensions/screenshots/webextension/blobConverters.js
browser/extensions/screenshots/webextension/build/buildSettings.js
browser/extensions/screenshots/webextension/build/shot.js
browser/extensions/screenshots/webextension/manifest.json
browser/extensions/screenshots/webextension/selector/shooter.js
--- a/browser/extensions/screenshots/install.rdf
+++ b/browser/extensions/screenshots/install.rdf
@@ -7,14 +7,14 @@
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>57.0a1</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
       </Description>
     </em:targetApplication>
     <em:type>2</em:type>
-    <em:version>19.0.0</em:version>
+    <em:version>19.1.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:homepageURL>https://screenshots.firefox.com/</em:homepageURL>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
   </Description>
 </RDF>
--- a/browser/extensions/screenshots/moz.build
+++ b/browser/extensions/screenshots/moz.build
@@ -13,16 +13,17 @@ FINAL_TARGET_FILES.features['screenshots
 ]
 
 # This file list is automatically generated by Screenshots' export scripts.
 # AUTOMATIC INSERTION START
 FINAL_TARGET_FILES.features['screenshots@mozilla.org']["webextension"] += [
   'webextension/assertIsBlankDocument.js',
   'webextension/assertIsTrusted.js',
   'webextension/blank.html',
+  'webextension/blobConverters.js',
   'webextension/catcher.js',
   'webextension/clipboard.js',
   'webextension/domainFromUrl.js',
   'webextension/log.js',
   'webextension/makeUuid.js',
   'webextension/manifest.json',
   'webextension/randomString.js',
   'webextension/sitehelper.js'
--- a/browser/extensions/screenshots/webextension/background/main.js
+++ b/browser/extensions/screenshots/webextension/background/main.js
@@ -1,9 +1,9 @@
-/* globals selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror, startBackground */
+/* globals selectorLoader, analytics, communication, catcher, log, makeUuid, auth, senderror, startBackground, blobConverters */
 
 "use strict";
 
 this.main = (function() {
   let exports = {};
 
   const pasteSymbol = (window.navigator.platform.match(/Mac/i)) ? "\u2318" : "Ctrl";
   const { sendEvent } = analytics;
@@ -218,19 +218,17 @@ this.main = (function() {
         message: browser.i18n.getMessage("notificationLinkCopiedDetails", pasteSymbol)
       });
     }
   });
 
   communication.register("downloadShot", (sender, info) => {
     // 'data:' urls don't work directly, let's use a Blob
     // see http://stackoverflow.com/questions/40269862/save-data-uri-as-file-using-downloads-download-api
-    const binary = atob(info.url.split(',')[1]); // just the base64 data
-    const data = Uint8Array.from(binary, char => char.charCodeAt(0))
-    const blob = new Blob([data], {type: "image/png"})
+    const blob = blobConverters.dataUrlToBlob(info.url);
     let url = URL.createObjectURL(blob);
     let downloadId;
     let onChangedCallback = catcher.watchFunction(function(change) {
       if (!downloadId || downloadId != change.id) {
         return;
       }
       if (change.state && change.state.current != "in_progress") {
         URL.revokeObjectURL(url);
--- a/browser/extensions/screenshots/webextension/background/selectorLoader.js
+++ b/browser/extensions/screenshots/webextension/background/selectorLoader.js
@@ -10,16 +10,17 @@ this.selectorLoader = (function() {
   // These modules are loaded in order, first standardScripts, then optionally onboardingScripts, and then selectorScripts
   // The order is important due to dependencies
   const standardScripts = [
     "build/buildSettings.js",
     "log.js",
     "catcher.js",
     "assertIsTrusted.js",
     "assertIsBlankDocument.js",
+    "blobConverters.js",
     "background/selectorLoader.js",
     "selector/callBackground.js",
     "selector/util.js"
   ];
 
   const selectorScripts = [
     "clipboard.js",
     "makeUuid.js",
--- a/browser/extensions/screenshots/webextension/background/startBackground.js
+++ b/browser/extensions/screenshots/webextension/background/startBackground.js
@@ -9,16 +9,17 @@
 
 this.startBackground = (function() {
   let exports = {};
 
   const backgroundScripts = [
     "log.js",
     "makeUuid.js",
     "catcher.js",
+    "blobConverters.js",
     "background/selectorLoader.js",
     "background/communication.js",
     "background/auth.js",
     "background/senderror.js",
     "build/raven.js",
     "build/shot.js",
     "background/analytics.js",
     "background/deviceInfo.js",
--- a/browser/extensions/screenshots/webextension/background/takeshot.js
+++ b/browser/extensions/screenshots/webextension/background/takeshot.js
@@ -1,9 +1,9 @@
-/* globals communication, shot, main, auth, catcher, analytics, buildSettings */
+/* globals communication, shot, main, auth, catcher, analytics, buildSettings, blobConverters */
 
 "use strict";
 
 this.takeshot = (function() {
   let exports = {};
   const Shot = shot.AbstractShot;
   const { sendEvent } = analytics;
 
@@ -28,20 +28,20 @@ this.takeshot = (function() {
               y: selectedPos.bottom - selectedPos.top
             }
           }
         });
       });
     }
     let convertBlobPromise = Promise.resolve();
     if (buildSettings.uploadBinary && !imageBlob) {
-      imageBlob = base64ToBinary(shot.getClip(shot.clipNames()[0]).image.url);
+      imageBlob = blobConverters.dataUrlToBlob(shot.getClip(shot.clipNames()[0]).image.url);
       shot.getClip(shot.clipNames()[0]).image.url = "";
     } else if (!buildSettings.uploadBinary && imageBlob) {
-      convertBlobPromise = blobToDataUrl(imageBlob).then((dataUrl) => {
+      convertBlobPromise = blobConverters.blobToDataUrl(imageBlob).then((dataUrl) => {
         shot.getClip(shot.clipNames()[0]).image.url = dataUrl;
       });
       imageBlob = null;
     }
     let shotAbTests = {};
     let abTests = auth.getAbTests();
     for (let testName of Object.keys(abTests)) {
       if (abTests[testName].shotField) {
@@ -115,60 +115,32 @@ this.takeshot = (function() {
           );
           let result = canvas.toDataURL();
           resolve(result);
         });
       });
     }));
   }
 
-  function base64ToBinary(url) {
-    const binary = atob(url.split(',')[1]);
-    const data = Uint8Array.from(binary, char => char.charCodeAt(0));
-    const blob = new Blob([data], {type: "image/png"});
-    return blob;
-  }
-
   /** Combines two buffers or Uint8Array's */
   function concatBuffers(buffer1, buffer2) {
     var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
     tmp.set(new Uint8Array(buffer1), 0);
     tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
     return tmp.buffer;
   }
 
-  /** Returns a promise that converts a Blob to a TypedArray */
-  function blobToArray(blob) {
-    return new Promise((resolve, reject) => {
-      let reader = new FileReader();
-      reader.addEventListener("loadend", function() {
-        resolve(reader.result);
-      });
-      reader.readAsArrayBuffer(blob);
-    });
-  }
-
-  function blobToDataUrl(blob) {
-    return new Promise((resolve, reject) => {
-      let reader = new FileReader();
-      reader.addEventListener("loadend", function() {
-        resolve(reader.result);
-      });
-      reader.readAsDataURL(blob);
-    });
-  }
-
   /** Creates a multipart TypedArray, given {name: value} fields
       and {name: blob} files
 
       Returns {body, "content-type"}
       */
   function createMultipart(fields, fileField, fileFilename, blob) {
     let boundary = "---------------------------ScreenshotBoundary" + Date.now();
-    return blobToArray(blob).then((blobAsBuffer) => {
+    return blobConverters.blobToArray(blob).then((blobAsBuffer) => {
       let body = [];
       for (let name in fields) {
         body.push("--" + boundary);
         body.push(`Content-Disposition: form-data; name="${name}"`);
         body.push("");
         body.push(fields[name]);
       }
       body.push("--" + boundary);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/screenshots/webextension/blobConverters.js
@@ -0,0 +1,44 @@
+this.blobConverters = (function () {
+  let exports = {};
+
+  exports.dataUrlToBlob = function(url) {
+    const binary = atob(url.split(',', 2)[1]);
+    let contentType = exports.getTypeFromDataUrl(url);
+    if (contentType != "image/png" && contentType != "image/jpeg") {
+      contentType = "image/png";
+    }
+    const data = Uint8Array.from(binary, char => char.charCodeAt(0));
+    const blob = new Blob([data], {type: contentType});
+    return blob;
+  };
+
+  exports.getTypeFromDataUrl = function(url) {
+    let contentType = url.split(',', 1)[0];
+    contentType = contentType.split(';', 1)[0];
+    contentType = contentType.split(':', 2)[1];
+    return contentType;
+  };
+
+  exports.blobToArray = function(blob) {
+    return new Promise((resolve, reject) => {
+      let reader = new FileReader();
+      reader.addEventListener("loadend", function() {
+        resolve(reader.result);
+      });
+      reader.readAsArrayBuffer(blob);
+    });
+  };
+
+  exports.blobToDataUrl = function(blob) {
+    return new Promise((resolve, reject) => {
+      let reader = new FileReader();
+      reader.addEventListener("loadend", function() {
+        resolve(reader.result);
+      });
+      reader.readAsDataURL(blob);
+    });
+  };
+
+  return exports;
+})();
+null;
--- a/browser/extensions/screenshots/webextension/build/buildSettings.js
+++ b/browser/extensions/screenshots/webextension/build/buildSettings.js
@@ -1,8 +1,9 @@
 window.buildSettings = {
   defaultSentryDsn: "https://904ccdd4866247c092ae8fc1a4764a63:940d44bdc71d4daea133c19080ccd38d@sentry.prod.mozaws.net/224",
   logLevel: "" || "warn",
   captureText: ("" === "true"),
-  uploadBinary: ("" === "true")
+  uploadBinary: ("" === "true"),
+  pngToJpegCutoff: parseInt("" || 2500000, 10)
 };
 null;
 
--- a/browser/extensions/screenshots/webextension/build/shot.js
+++ b/browser/extensions/screenshots/webextension/build/shot.js
@@ -369,17 +369,24 @@ class AbstractShot {
     filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
     let clipFilename = `Screenshot-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${filenameTitle}`;
     const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16
     if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string
       const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]"
       clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars);
       clipFilename = clipFilename + '[...]';
     }
-    return clipFilename + '.png';
+    let clip = this.getClip(this.clipNames()[0]);
+    let extension = ".png";
+    if (clip && clip.image && clip.image.type) {
+      if (clip.image.type == "jpeg") {
+        extension = ".jpg";
+      }
+    }
+    return clipFilename + extension;
   }
 
   get urlDisplay() {
     if (!this.url) {
       return null;
     }
     if (this.url.search(/^https?/i) != -1) {
       let txt = this.url;
@@ -693,23 +700,26 @@ class _Clip {
   get image() {
     return this._image;
   }
   set image(image) {
     if (!image) {
       this._image = undefined;
       return;
     }
-    assert(checkObject(image, ["url"], ["dimensions", "text", "location", "captureType"]), "Bad attrs for Clip Image:", Object.keys(image));
+    assert(checkObject(image, ["url"], ["dimensions", "text", "location", "captureType", "type"]), "Bad attrs for Clip Image:", Object.keys(image));
     assert(isValidClipImageUrl(image.url), "Bad Clip image URL:", image.url);
     assert(image.captureType == "madeSelection" || image.captureType == "selection" || image.captureType == "visible" || image.captureType == "auto" || image.captureType == "fullPage" || !image.captureType, "Bad image.captureType:", image.captureType);
     assert(typeof image.text == "string" || !image.text, "Bad Clip image text:", image.text);
     if (image.dimensions) {
       assert(typeof image.dimensions.x == "number" && typeof image.dimensions.y == "number", "Bad Clip image dimensions:", image.dimensions);
     }
+    if (image.type) {
+      assert(image.type == "png" || image.type == "jpeg", "Unexpected image type:", image.type);
+    }
     assert(image.location &&
       typeof image.location.left == "number" &&
       typeof image.location.right == "number" &&
       typeof image.location.top == "number" &&
       typeof image.location.bottom == "number", "Bad Clip image pixel location:", image.location);
     if (image.location.topLeftElement || image.location.topLeftOffset ||
         image.location.bottomRightElement || image.location.bottomRightOffset) {
       assert(typeof image.location.topLeftElement == "string" &&
--- a/browser/extensions/screenshots/webextension/manifest.json
+++ b/browser/extensions/screenshots/webextension/manifest.json
@@ -1,12 +1,12 @@
 {
   "manifest_version": 2,
   "name": "Firefox Screenshots",
-  "version": "19.0.0",
+  "version": "19.1.0",
   "description": "__MSG_addonDescription__",
   "author": "__MSG_addonAuthorsList__",
   "homepage_url": "https://github.com/mozilla-services/screenshots",
   "applications": {
     "gecko": {
       "id": "screenshots@mozilla.org",
       "strict_min_version": "57.0a1"
     }
--- a/browser/extensions/screenshots/webextension/selector/shooter.js
+++ b/browser/extensions/screenshots/webextension/selector/shooter.js
@@ -1,10 +1,10 @@
 /* globals global, documentMetadata, util, uicontrol, ui, catcher */
-/* globals buildSettings, domainFromUrl, randomString, shot */
+/* globals buildSettings, domainFromUrl, randomString, shot, blobConverters */
 
 "use strict";
 
 this.shooter = (function() { // eslint-disable-line no-unused-vars
   let exports = {};
   const { AbstractShot } = shot;
 
   const RANDOM_STRING_LENGTH = 16;
@@ -24,23 +24,16 @@ this.shooter = (function() { // eslint-d
     const origin = new RegExp(`${regexpEscape(window.location.origin)}[^ \t\n\r",>]*`, 'g');
     const json = JSON.stringify(data)
       .replace(href, 'REDACTED_HREF')
       .replace(origin, 'REDACTED_URL');
     const result = JSON.parse(json);
     return result;
   }
 
-  function base64ToBinary(url) {
-    const binary = atob(url.split(',')[1]);
-    const data = Uint8Array.from(binary, char => char.charCodeAt(0));
-    const blob = new Blob([data], {type: "image/png"});
-    return blob;
-  }
-
   catcher.registerHandler((errorObj) => {
     callBackground("reportError", sanitizeError(errorObj));
   });
 
   catcher.watchFunction(() => {
     let canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
     let ctx = canvas.getContext('2d');
     supportsDrawWindow = !!ctx.drawWindow;
@@ -65,17 +58,26 @@ this.shooter = (function() { // eslint-d
       ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
     }
     ui.iframe.hide();
     try {
       ctx.drawWindow(window, selectedPos.left, selectedPos.top, width, height, "#fff");
     } finally {
       ui.iframe.unhide();
     }
-    return canvas.toDataURL();
+    let limit = buildSettings.pngToJpegCutoff;
+    let dataUrl = canvas.toDataURL();
+    if (limit && dataUrl.length > limit) {
+      let jpegDataUrl = canvas.toDataURL("image/jpeg");
+      if (jpegDataUrl.length < dataUrl.length) {
+        // Only use the JPEG if it is actually smaller
+        dataUrl = jpegDataUrl;
+      }
+    }
+    return dataUrl;
   };
 
   let isSaving = null;
 
   exports.takeShot = function(captureType, selectedPos, url) {
     // isSaving indicates we're aleady in the middle of saving
     // we use a timeout so in the case of a failure the button will
     // still start working again
@@ -101,23 +103,26 @@ this.shooter = (function() { // eslint-d
       isSaving = null;
     }, 1000);
     selectedPos = selectedPos.asJson();
     let captureText = "";
     if (buildSettings.captureText) {
       captureText = util.captureEnclosedText(selectedPos);
     }
     let dataUrl = url || screenshotPage(selectedPos, captureType);
+    let type = blobConverters.getTypeFromDataUrl(dataUrl);
+    type = type ? type.split("/", 2)[1] : null;
     if (dataUrl) {
-      imageBlob = base64ToBinary(dataUrl);
+      imageBlob = blobConverters.dataUrlToBlob(dataUrl);
       shotObject.delAllClips();
       shotObject.addClip({
         createdDate: Date.now(),
         image: {
           url: "data:",
+          type,
           captureType,
           text: captureText,
           location: selectedPos,
           dimensions: {
             x: selectedPos.right - selectedPos.left,
             y: selectedPos.bottom - selectedPos.top
           }
         }
@@ -168,16 +173,27 @@ this.shooter = (function() { // eslint-d
         {
           scrollX: window.scrollX,
           scrollY: window.scrollY,
           innerHeight: window.innerHeight,
           innerWidth: window.innerWidth
         });
     }
     catcher.watchPromise(promise.then((dataUrl) => {
+      let type = blobConverters.getTypeFromDataUrl(dataUrl);
+      type = type ? type.split("/", 2)[1] : null;
+      shotObject.delAllClips();
+      shotObject.addClip({
+        createdDate: Date.now(),
+        image: {
+          url: dataUrl,
+          type,
+          location: selectedPos
+        }
+      });
       ui.triggerDownload(dataUrl, shotObject.filename);
       uicontrol.deactivate();
     }));
   };
 
   exports.sendEvent = function(...args) {
     callBackground("sendEvent", ...args);
   };