--- 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);
};