summaryrefslogtreecommitdiffstats
path: root/browser/extensions/screenshots/build
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /browser/extensions/screenshots/build
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/extensions/screenshots/build')
-rw-r--r--browser/extensions/screenshots/build/inlineSelectionCss.js667
-rw-r--r--browser/extensions/screenshots/build/selection.js126
-rw-r--r--browser/extensions/screenshots/build/shot.js888
-rw-r--r--browser/extensions/screenshots/build/thumbnailGenerator.js190
4 files changed, 1871 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/build/inlineSelectionCss.js b/browser/extensions/screenshots/build/inlineSelectionCss.js
new file mode 100644
index 0000000000..fa31b642df
--- /dev/null
+++ b/browser/extensions/screenshots/build/inlineSelectionCss.js
@@ -0,0 +1,667 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Created from build/server/static/css/inline-selection.css */
+window.inlineSelectionCss = `
+.button, .highlight-button-cancel, .highlight-button-download, .highlight-button-copy {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ column-gap: 8px;
+ border: 0;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 400;
+ height: 40px;
+ min-width: 40px;
+ outline: none;
+ padding: 0 10px;
+ position: relative;
+ text-align: center;
+ text-decoration: none;
+ transition: background 150ms cubic-bezier(0.07, 0.95, 0, 1), border 150ms cubic-bezier(0.07, 0.95, 0, 1);
+ user-select: none;
+ white-space: nowrap; }
+ .button.hidden, .hidden.highlight-button-cancel, .hidden.highlight-button-download, .hidden.highlight-button-copy {
+ display: none; }
+ .button.small, .small.highlight-button-cancel, .small.highlight-button-download, .small.highlight-button-copy {
+ height: 32px;
+ line-height: 32px;
+ padding: 0 8px; }
+ .button.active, .active.highlight-button-cancel, .active.highlight-button-download, .active.highlight-button-copy {
+ background-color: #dedede; }
+ .button.tiny, .tiny.highlight-button-cancel, .tiny.highlight-button-download, .tiny.highlight-button-copy {
+ font-size: 14px;
+ height: 26px;
+ border: 1px solid #c7c7c7; }
+ .button.tiny:hover, .tiny.highlight-button-cancel:hover, .tiny.highlight-button-download:hover, .tiny.highlight-button-copy:hover, .button.tiny:focus, .tiny.highlight-button-cancel:focus, .tiny.highlight-button-download:focus, .tiny.highlight-button-copy:focus {
+ background: #ededf0;
+ border-color: #989898; }
+ .button.tiny:active, .tiny.highlight-button-cancel:active, .tiny.highlight-button-download:active, .tiny.highlight-button-copy:active {
+ background: #dedede;
+ border-color: #989898; }
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ border: 0;
+ border-inline-end: 1px solid #c7c7c7;
+ box-shadow: 0;
+ border-radius: 0;
+ flex-shrink: 0;
+ font-size: 20px;
+ height: 100px;
+ line-height: 100%;
+ overflow: hidden; }
+ @media (max-width: 719px) {
+ .button.block-button, .block-button.highlight-button-cancel, .block-button.highlight-button-download, .block-button.highlight-button-copy {
+ justify-content: flex-start;
+ font-size: 16px;
+ height: 72px;
+ margin-inline-end: 10px;
+ padding: 0 5px; } }
+ .button.block-button:hover, .block-button.highlight-button-cancel:hover, .block-button.highlight-button-download:hover, .block-button.highlight-button-copy:hover {
+ background: #ededf0; }
+ .button.block-button:active, .block-button.highlight-button-cancel:active, .block-button.highlight-button-download:active, .block-button.highlight-button-copy:active {
+ background: #dedede; }
+ .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy, .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy, .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy, .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy, .button.flag, .flag.highlight-button-cancel, .flag.highlight-button-download, .flag.highlight-button-copy {
+ background-repeat: no-repeat;
+ background-size: 50%;
+ background-position: center;
+ margin-inline-end: 10px;
+ transition: background-color 150ms cubic-bezier(0.07, 0.95, 0, 1); }
+ .button.download, .download.highlight-button-cancel, .download.highlight-button-download, .download.highlight-button-copy {
+ background-image: url("chrome://browser/content/screenshots/download.svg"); }
+ .button.download:hover, .download.highlight-button-cancel:hover, .download.highlight-button-download:hover, .download.highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.download:active, .download.highlight-button-cancel:active, .download.highlight-button-download:active, .download.highlight-button-copy:active {
+ background-color: #dedede; }
+ .button.share, .share.highlight-button-cancel, .share.highlight-button-download, .share.highlight-button-copy {
+ background-image: url("../img/icon-share.svg"); }
+ .button.share:hover, .share.highlight-button-cancel:hover, .share.highlight-button-download:hover, .share.highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.share.active, .share.active.highlight-button-cancel, .share.active.highlight-button-download, .share.active.highlight-button-copy, .button.share:active, .share.highlight-button-cancel:active, .share.highlight-button-download:active, .share.highlight-button-copy:active {
+ background-color: #dedede; }
+ .button.share.newicon, .share.newicon.highlight-button-cancel, .share.newicon.highlight-button-download, .share.newicon.highlight-button-copy {
+ background-image: url("../img/icon-share-alternate.svg"); }
+ .button.trash, .trash.highlight-button-cancel, .trash.highlight-button-download, .trash.highlight-button-copy {
+ background-image: url("../img/icon-trash.svg"); }
+ .button.trash:hover, .trash.highlight-button-cancel:hover, .trash.highlight-button-download:hover, .trash.highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.trash:active, .trash.highlight-button-cancel:active, .trash.highlight-button-download:active, .trash.highlight-button-copy:active {
+ background-color: #dedede; }
+ .button.edit, .edit.highlight-button-cancel, .edit.highlight-button-download, .edit.highlight-button-copy {
+ background-image: url("../img/icon-edit.svg"); }
+ .button.edit:hover, .edit.highlight-button-cancel:hover, .edit.highlight-button-download:hover, .edit.highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.edit:active, .edit.highlight-button-cancel:active, .edit.highlight-button-download:active, .edit.highlight-button-copy:active {
+ background-color: #dedede; }
+
+.app-body {
+ background: #f9f9fa;
+ color: #38383d; }
+ .app-body a {
+ color: #0a84ff; }
+
+.highlight-color-scheme {
+ background: #0a84ff;
+ color: #fff; }
+ .highlight-color-scheme a {
+ color: #fff;
+ text-decoration: underline; }
+
+.alt-color-scheme {
+ background: #38383d;
+ color: #f9f9fa; }
+ .alt-color-scheme h1 {
+ color: #6f7fb6; }
+ .alt-color-scheme a {
+ color: #e1e1e6;
+ text-decoration: underline; }
+
+.button.primary, .primary.highlight-button-cancel, .highlight-button-download, .primary.highlight-button-copy {
+ background-color: #0a84ff;
+ color: #fff; }
+ .button.primary:hover, .primary.highlight-button-cancel:hover, .highlight-button-download:hover, .primary.highlight-button-copy:hover, .button.primary:focus, .primary.highlight-button-cancel:focus, .highlight-button-download:focus, .primary.highlight-button-copy:focus {
+ background-color: #0072e5; }
+ .button.primary:active, .primary.highlight-button-cancel:active, .highlight-button-download:active, .primary.highlight-button-copy:active {
+ background-color: #0065cc; }
+
+.button.secondary, .highlight-button-cancel, .secondary.highlight-button-download, .highlight-button-copy {
+ background-color: #f9f9fa;
+ color: #38383d; }
+ .button.secondary:hover, .highlight-button-cancel:hover, .secondary.highlight-button-download:hover, .highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.secondary:active, .highlight-button-cancel:active, .secondary.highlight-button-download:active, .highlight-button-copy:active {
+ background-color: #dedede; }
+
+.button.transparent, .transparent.highlight-button-cancel, .transparent.highlight-button-download, .transparent.highlight-button-copy {
+ background-color: transparent;
+ color: #38383d; }
+ .button.transparent:hover, .transparent.highlight-button-cancel:hover, .transparent.highlight-button-download:hover, .transparent.highlight-button-copy:hover {
+ background-color: #ededf0; }
+ .button.transparent:focus, .transparent.highlight-button-cancel:focus, .transparent.highlight-button-download:focus, .transparent.highlight-button-copy:focus, .button.transparent:active, .transparent.highlight-button-cancel:active, .transparent.highlight-button-download:active, .transparent.highlight-button-copy:active {
+ background-color: #dedede; }
+
+.button.warning, .warning.highlight-button-cancel, .warning.highlight-button-download, .warning.highlight-button-copy {
+ color: #fff;
+ background: #d92215; }
+ .button.warning:hover, .warning.highlight-button-cancel:hover, .warning.highlight-button-download:hover, .warning.highlight-button-copy:hover, .button.warning:focus, .warning.highlight-button-cancel:focus, .warning.highlight-button-download:focus, .warning.highlight-button-copy:focus {
+ background: #b81d12; }
+ .button.warning:active, .warning.highlight-button-cancel:active, .warning.highlight-button-download:active, .warning.highlight-button-copy:active {
+ background: #a11910; }
+
+.subtitle-link {
+ color: #0a84ff; }
+
+.loader {
+ background: rgba(12, 12, 13, 0.2);
+ border-radius: 2px;
+ height: 4px;
+ overflow: hidden;
+ position: relative;
+ width: 200px; }
+
+.loader-inner {
+ animation: bounce infinite alternate 1250ms cubic-bezier(0.7, 0, 0.3, 1);
+ background: #45a1ff;
+ border-radius: 2px;
+ height: 4px;
+ transform: translateX(-40px);
+ width: 50px; }
+
+@keyframes bounce {
+ 0% {
+ transform: translateX(-40px); }
+ 100% {
+ transform: translate(190px); } }
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0; }
+ 100% {
+ opacity: 1; } }
+
+@keyframes pop {
+ 0% {
+ transform: scale(1); }
+ 97% {
+ transform: scale(1.04); }
+ 100% {
+ transform: scale(1); } }
+
+@keyframes pulse {
+ 0% {
+ opacity: 0.3;
+ transform: scale(1); }
+ 70% {
+ opacity: 0.25;
+ transform: scale(1.04); }
+ 100% {
+ opacity: 0.3;
+ transform: scale(1); } }
+
+@keyframes slide-left {
+ 0% {
+ opacity: 0;
+ transform: translate3d(160px, 0, 0); }
+ 100% {
+ opacity: 1;
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes bounce-in {
+ 0% {
+ opacity: 0;
+ transform: scale(1); }
+ 60% {
+ opacity: 1;
+ transform: scale(1.02); }
+ 100% {
+ transform: scale(1); } }
+
+.mover-target {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ position: absolute;
+ z-index: 5; }
+
+.highlight,
+.mover-target {
+ background-color: transparent;
+ background-image: none; }
+
+.mover-target,
+.bghighlight {
+ border: 0; }
+
+.hover-highlight {
+ animation: fade-in 125ms forwards cubic-bezier(0.07, 0.95, 0, 1);
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 1px;
+ pointer-events: none;
+ position: absolute;
+ z-index: 10000000000; }
+ .hover-highlight::before {
+ border: 2px dashed rgba(255, 255, 255, 0.4);
+ bottom: 0;
+ content: "";
+ inset-inline-start: 0;
+ position: absolute;
+ inset-inline-end: 0;
+ top: 0; }
+ /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */
+ @media (forced-colors: active) {
+ .hover-highlight {
+ background-color: white;
+ opacity: 0.2; } }
+
+.mover-target.direction-topLeft {
+ cursor: nwse-resize;
+ height: 60px;
+ left: -30px;
+ top: -30px;
+ width: 60px; }
+
+.mover-target.direction-top {
+ cursor: ns-resize;
+ height: 60px;
+ inset-inline-start: 0;
+ top: -30px;
+ width: 100%;
+ z-index: 4; }
+
+.mover-target.direction-topRight {
+ cursor: nesw-resize;
+ height: 60px;
+ right: -30px;
+ top: -30px;
+ width: 60px; }
+
+.mover-target.direction-left {
+ cursor: ew-resize;
+ height: 100%;
+ left: -30px;
+ top: 0;
+ width: 60px;
+ z-index: 4; }
+
+.mover-target.direction-right {
+ cursor: ew-resize;
+ height: 100%;
+ right: -30px;
+ top: 0;
+ width: 60px;
+ z-index: 4; }
+
+.mover-target.direction-bottomLeft {
+ bottom: -30px;
+ cursor: nesw-resize;
+ height: 60px;
+ left: -30px;
+ width: 60px; }
+
+.mover-target.direction-bottom {
+ bottom: -30px;
+ cursor: ns-resize;
+ height: 60px;
+ inset-inline-start: 0;
+ width: 100%;
+ z-index: 4; }
+
+.mover-target.direction-bottomRight {
+ bottom: -30px;
+ cursor: nwse-resize;
+ height: 60px;
+ right: -30px;
+ width: 60px; }
+
+.mover-target:hover .mover {
+ transform: scale(1.05); }
+
+.mover {
+ background-color: #fff;
+ border-radius: 50%;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
+ height: 16px;
+ opacity: 1;
+ position: relative;
+ transition: transform 125ms cubic-bezier(0.07, 0.95, 0, 1);
+ width: 16px; }
+ .small-selection .mover {
+ height: 10px;
+ width: 10px; }
+
+.direction-topLeft .mover,
+.direction-left .mover,
+.direction-bottomLeft .mover {
+ left: -1px; }
+
+.direction-topLeft .mover,
+.direction-top .mover,
+.direction-topRight .mover {
+ top: -1px; }
+
+.direction-topRight .mover,
+.direction-right .mover,
+.direction-bottomRight .mover {
+ right: -1px; }
+
+.direction-bottomRight .mover,
+.direction-bottom .mover,
+.direction-bottomLeft .mover {
+ bottom: -1px; }
+
+.bghighlight {
+ background-color: rgba(0, 0, 0, 0.7);
+ position: absolute;
+ z-index: 9999999999; }
+ /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */
+ @media (forced-colors: active) {
+ .bghighlight {
+ background-color: black;
+ opacity: 0.7; } }
+
+.preview-overlay {
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.7);
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ inset-inline-start: 0;
+ margin: 0;
+ padding: 0;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 9999999999; }
+ /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */
+ @media (forced-colors: active) {
+ .preview-overlay {
+ background-color: black;
+ opacity: 0.7; } }
+
+.precision-cursor {
+ cursor: crosshair; }
+
+.highlight {
+ border-radius: 1px;
+ border: 2px dashed rgba(255, 255, 255, 0.8);
+ box-sizing: border-box;
+ cursor: move;
+ position: absolute;
+ z-index: 9999999999; }
+ /* When prefers contrast is fully supported, we should change these quereies to cover both high and low prefers contrast cases */
+ @media (forced-colors: active) {
+ .highlight {
+ border: 2px dashed white;
+ opacity: 1.0; } }
+
+.highlight-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ bottom: -58px;
+ position: absolute;
+ inset-inline-end: 5px;
+ z-index: 6; }
+ .bottom-selection .highlight-buttons {
+ bottom: 5px; }
+ .left-selection .highlight-buttons {
+ inset-inline-end: auto;
+ inset-inline-start: 5px; }
+ .highlight-buttons > button {
+ box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); }
+
+.highlight-button-cancel {
+ margin: 5px;
+ width: 40px; }
+
+.highlight-button-download {
+ margin: 5px;
+ width: auto;
+ font-size: 18px; }
+
+.highlight-button-download img {
+ height: 16px;
+ width: 16px;
+}
+
+.highlight-button-download:-moz-locale-dir(rtl) {
+ flex-direction: reverse;
+}
+
+.highlight-button-download img:-moz-locale-dir(ltr) {
+ padding-inline-end: 8px;
+}
+
+.highlight-button-download img:-moz-locale-dir(rtl) {
+ padding-inline-start: 8px;
+}
+
+.highlight-button-copy {
+ margin: 5px;
+ width: auto; }
+
+.highlight-button-copy img {
+ height: 16px;
+ width: 16px;
+}
+
+.highlight-button-copy:-moz-locale-dir(rtl) {
+ flex-direction: reverse;
+}
+
+.highlight-button-copy img:-moz-locale-dir(ltr) {
+ padding-inline-end: 8px;
+}
+
+.highlight-button-copy img:-moz-locale-dir(rtl) {
+ padding-inline-start: 8px;
+}
+
+.pixel-dimensions {
+ position: absolute;
+ pointer-events: none;
+ font-weight: bold;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 70%;
+ color: #000;
+ text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; }
+
+.preview-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding-inline-end: 4px;
+ inset-inline-end: 0;
+ width: 100%;
+ position: absolute;
+ height: 60px;
+ border-radius: 4px 4px 0 0;
+ background: rgba(249, 249, 250, 0.8);
+ top: 0;
+ border: 1px solid rgba(249, 249, 250, 0.2);
+ border-bottom: 0;
+ box-sizing: border-box; }
+
+.preview-image {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+ margin: 24px auto;
+ position: relative;
+ max-width: 80%;
+ max-height: 95%;
+ text-align: center;
+ animation-delay: 50ms;
+ display: flex; }
+
+.preview-image-wrapper {
+ background: rgba(249, 249, 250, 0.8);
+ border-radius: 0 0 4px 4px;
+ display: block;
+ height: auto;
+ max-width: 100%;
+ min-width: 320px;
+ overflow-y: scroll;
+ padding: 0 60px;
+ margin-top: 60px;
+ border: 1px solid rgba(249, 249, 250, 0.2);
+ border-top: 0; }
+
+.preview-image-wrapper > img {
+ box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1);
+ height: auto;
+ margin-bottom: 60px;
+ max-width: 100%;
+ width: 100%; }
+
+.fixed-container {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ justify-content: center;
+ inset-inline-start: 0;
+ margin: 0;
+ padding: 0;
+ pointer-events: none;
+ position: fixed;
+ top: 0;
+ width: 100%; }
+
+.face-container {
+ position: relative;
+ width: 64px;
+ height: 64px; }
+
+.face {
+ width: 62.4px;
+ height: 62.4px;
+ display: block;
+ background-image: url("chrome://browser/content/screenshots/icon-welcome-face-without-eyes.svg"); }
+
+.eye {
+ background-color: #fff;
+ width: 10.8px;
+ height: 14.6px;
+ position: absolute;
+ border-radius: 100%;
+ overflow: hidden;
+ inset-inline-start: 16.4px;
+ top: 19.8px; }
+
+.eyeball {
+ position: absolute;
+ width: 6px;
+ height: 6px;
+ background-color: #000;
+ border-radius: 50%;
+ inset-inline-start: 2.4px;
+ top: 4.3px;
+ z-index: 10; }
+
+.left {
+ margin-inline-start: 0; }
+
+.right {
+ margin-inline-start: 20px; }
+
+.preview-instructions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: pulse 125mm cubic-bezier(0.07, 0.95, 0, 1);
+ color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 24px;
+ line-height: 32px;
+ text-align: center;
+ padding-top: 20px;
+ width: 400px;
+ user-select: none; }
+
+.cancel-shot {
+ background-color: transparent;
+ cursor: pointer;
+ outline: none;
+ border-radius: 3px;
+ border: 1px #9b9b9b solid;
+ color: #fff;
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, "segoe ui", "helvetica neue", helvetica, ubuntu, roboto, noto, arial, sans-serif;
+ font-size: 16px;
+ margin-top: 40px;
+ padding: 10px 25px;
+ pointer-events: all; }
+
+.all-buttons-container {
+ display: flex;
+ flex-direction: row-reverse;
+ background: #f5f5f5;
+ border-radius: 2px;
+ box-sizing: border-box;
+ height: 80px;
+ padding: 8px;
+ position: absolute;
+ inset-inline-end: 8px;
+ top: 8px;
+ box-shadow: 0 0 0 1px rgba(12, 12, 13, 0.1), 0 2px 8px rgba(12, 12, 13, 0.1); }
+ .all-buttons-container .spacer {
+ background-color: #c9c9c9;
+ flex: 0 0 1px;
+ height: 80px;
+ margin: 0 10px;
+ position: relative;
+ top: -8px; }
+ .all-buttons-container button {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: flex-end;
+ color: #3e3d40;
+ background-color: #f5f5f5;
+ background-position: center top;
+ background-repeat: no-repeat;
+ background-size: 46px 46px;
+ border: 1px solid transparent;
+ cursor: pointer;
+ height: 100%;
+ min-width: 90px;
+ padding: 46px 5px 5px;
+ pointer-events: all;
+ transition: border 150ms cubic-bezier(0.07, 0.95, 0, 1), background-color 150ms cubic-bezier(0.07, 0.95, 0, 1);
+ white-space: nowrap; }
+ .all-buttons-container button:hover {
+ background-color: #ebebeb;
+ border: 1px solid #c7c7c7; }
+ .all-buttons-container button:active {
+ background-color: #dedede;
+ border: 1px solid #989898; }
+ .all-buttons-container .full-page {
+ background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg"); }
+ .all-buttons-container .visible {
+ background-image: url("chrome://browser/content/screenshots/menu-visible.svg"); }
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1); }
+ 50% {
+ transform: scale(1.06); }
+ 100% {
+ transform: scale(1); } }
+
+@keyframes fade-in {
+ 0% {
+ opacity: 0; }
+ 100% {
+ opacity: 1; } }
+
+`;
+null;
diff --git a/browser/extensions/screenshots/build/selection.js b/browser/extensions/screenshots/build/selection.js
new file mode 100644
index 0000000000..db93dce72b
--- /dev/null
+++ b/browser/extensions/screenshots/build/selection.js
@@ -0,0 +1,126 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.selection = (function () {
+ let exports = {};
+ class Selection {
+ constructor(x1, y1, x2, y2) {
+ this.x1 = x1;
+ this.y1 = y1;
+ this.x2 = x2;
+ this.y2 = y2;
+ }
+
+ get top() {
+ return Math.min(this.y1, this.y2);
+ }
+ set top(val) {
+ if (this.y1 < this.y2) {
+ this.y1 = val;
+ } else {
+ this.y2 = val;
+ }
+ }
+
+ get bottom() {
+ return Math.max(this.y1, this.y2);
+ }
+ set bottom(val) {
+ if (this.y1 > this.y2) {
+ this.y1 = val;
+ } else {
+ this.y2 = val;
+ }
+ }
+
+ get left() {
+ return Math.min(this.x1, this.x2);
+ }
+ set left(val) {
+ if (this.x1 < this.x2) {
+ this.x1 = val;
+ } else {
+ this.x2 = val;
+ }
+ }
+
+ get right() {
+ return Math.max(this.x1, this.x2);
+ }
+ set right(val) {
+ if (this.x1 > this.x2) {
+ this.x1 = val;
+ } else {
+ this.x2 = val;
+ }
+ }
+
+ get width() {
+ return Math.abs(this.x2 - this.x1);
+ }
+ get height() {
+ return Math.abs(this.y2 - this.y1);
+ }
+
+ rect() {
+ return {
+ top: Math.floor(this.top),
+ left: Math.floor(this.left),
+ bottom: Math.floor(this.bottom),
+ right: Math.floor(this.right),
+ };
+ }
+
+ union(other) {
+ return new Selection(
+ Math.min(this.left, other.left),
+ Math.min(this.top, other.top),
+ Math.max(this.right, other.right),
+ Math.max(this.bottom, other.bottom)
+ );
+ }
+
+ /** Sort x1/x2 and y1/y2 so x1<x2, y1<y2 */
+ sortCoords() {
+ if (this.x1 > this.x2) {
+ [this.x1, this.x2] = [this.x2, this.x1];
+ }
+ if (this.y1 > this.y2) {
+ [this.y1, this.y2] = [this.y2, this.y1];
+ }
+ }
+
+ clone() {
+ return new Selection(this.x1, this.y1, this.x2, this.y2);
+ }
+
+ toJSON() {
+ return {
+ left: this.left,
+ right: this.right,
+ top: this.top,
+ bottom: this.bottom,
+ };
+ }
+
+ static getBoundingClientRect(el) {
+ if (!el.getBoundingClientRect) {
+ // Typically the <html> element or somesuch
+ return null;
+ }
+ const rect = el.getBoundingClientRect();
+ if (!rect) {
+ return null;
+ }
+ return new Selection(rect.left, rect.top, rect.right, rect.bottom);
+ }
+ }
+
+ if (typeof exports !== "undefined") {
+ exports.Selection = Selection;
+ }
+
+ return exports;
+})();
+null;
diff --git a/browser/extensions/screenshots/build/shot.js b/browser/extensions/screenshots/build/shot.js
new file mode 100644
index 0000000000..7153562de3
--- /dev/null
+++ b/browser/extensions/screenshots/build/shot.js
@@ -0,0 +1,888 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals process, require */
+
+this.shot = (function () {
+ let exports = {}; // Note: in this library we can't use any "system" dependencies because this can be used from multiple
+ // environments
+
+ const isNode =
+ typeof process !== "undefined" &&
+ Object.prototype.toString.call(process) === "[object process]";
+ const URL = (isNode && require("url").URL) || window.URL;
+
+ /** Throws an error if the condition isn't true. Any extra arguments after the condition
+ are used as console.error() arguments. */
+ function assert(condition, ...args) {
+ if (condition) {
+ return;
+ }
+ console.error("Failed assertion", ...args);
+ throw new Error(`Failed assertion: ${args.join(" ")}`);
+ }
+
+ /** True if `url` is a valid URL */
+ function isUrl(url) {
+ try {
+ const parsed = new URL(url);
+
+ if (parsed.protocol === "view-source:") {
+ return isUrl(url.substr("view-source:".length));
+ }
+
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ function isValidClipImageUrl(url) {
+ return isUrl(url) && !(url.indexOf(")") > -1);
+ }
+
+ function assertUrl(url) {
+ if (!url) {
+ throw new Error("Empty value is not URL");
+ }
+ if (!isUrl(url)) {
+ const exc = new Error("Not a URL");
+ exc.scheme = url.split(":")[0];
+ throw exc;
+ }
+ }
+
+ function isSecureWebUri(url) {
+ return isUrl(url) && url.toLowerCase().startsWith("https");
+ }
+
+ function assertOrigin(url) {
+ assertUrl(url);
+ if (url.search(/^https?:/i) !== -1) {
+ let newUrl = new URL(url);
+ if (newUrl.pathname != "/") {
+ throw new Error("Bad origin, might include path");
+ }
+ }
+ }
+
+ function originFromUrl(url) {
+ if (!url) {
+ return null;
+ }
+ if (url.search(/^https?:/i) === -1) {
+ // Non-HTTP URLs don't have an origin
+ return null;
+ }
+ try {
+ let tryUrl = new URL(url);
+ return tryUrl.origin;
+ } catch {
+ return null;
+ }
+ }
+
+ /** Check if the given object has all of the required attributes, and no extra
+ attributes exception those in optional */
+ function checkObject(obj, required, optional) {
+ if (typeof obj !== "object" || obj === null) {
+ throw new Error(
+ "Cannot check non-object: " +
+ typeof obj +
+ " that is " +
+ JSON.stringify(obj)
+ );
+ }
+ required = required || [];
+ for (const attr of required) {
+ if (!(attr in obj)) {
+ return false;
+ }
+ }
+ optional = optional || [];
+ for (const attr in obj) {
+ if (!required.includes(attr) && !optional.includes(attr)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Create a JSON object from a normal object, given the required and optional
+ attributes (filtering out any other attributes). Optional attributes are
+ only kept when they are truthy. */
+ function jsonify(obj, required, optional) {
+ required = required || [];
+ const result = {};
+ for (const attr of required) {
+ result[attr] = obj[attr];
+ }
+ optional = optional || [];
+ for (const attr of optional) {
+ if (obj[attr]) {
+ result[attr] = obj[attr];
+ }
+ }
+ return result;
+ }
+
+ /** True if the two objects look alike. Null, undefined, and absent properties
+ are all treated as equivalent. Traverses objects and arrays */
+ function deepEqual(a, b) {
+ if ((a === null || a === undefined) && (b === null || b === undefined)) {
+ return true;
+ }
+ if (typeof a !== "object" || typeof b !== "object") {
+ return a === b;
+ }
+ if (Array.isArray(a)) {
+ if (!Array.isArray(b)) {
+ return false;
+ }
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (!deepEqual(a[i], b[i])) {
+ return false;
+ }
+ }
+ }
+ if (Array.isArray(b)) {
+ return false;
+ }
+ const seen = new Set();
+ for (const attr of Object.keys(a)) {
+ if (!deepEqual(a[attr], b[attr])) {
+ return false;
+ }
+ seen.add(attr);
+ }
+ for (const attr of Object.keys(b)) {
+ if (!seen.has(attr)) {
+ if (!deepEqual(a[attr], b[attr])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ function makeRandomId() {
+ // Note: this isn't for secure contexts, only for non-conflicting IDs
+ let id = "";
+ while (id.length < 12) {
+ let num;
+ if (!id) {
+ num = Date.now() % Math.pow(36, 3);
+ } else {
+ num = Math.floor(Math.random() * Math.pow(36, 3));
+ }
+ id += num.toString(36);
+ }
+ return id;
+ }
+
+ class AbstractShot {
+ constructor(backend, id, attrs) {
+ attrs = attrs || {};
+ assert(
+ /^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/.test(id),
+ "Bad ID (should be alphanumeric):",
+ JSON.stringify(id)
+ );
+ this._backend = backend;
+ this._id = id;
+ this.origin = attrs.origin || null;
+ this.fullUrl = attrs.fullUrl || null;
+ if (!attrs.fullUrl && attrs.url) {
+ console.warn("Received deprecated attribute .url");
+ this.fullUrl = attrs.url;
+ }
+ if (this.origin && !isSecureWebUri(this.origin)) {
+ this.origin = "";
+ }
+ if (this.fullUrl && !isSecureWebUri(this.fullUrl)) {
+ this.fullUrl = "";
+ }
+ this.docTitle = attrs.docTitle || null;
+ this.userTitle = attrs.userTitle || null;
+ this.createdDate = attrs.createdDate || Date.now();
+ this.siteName = attrs.siteName || null;
+ this.images = [];
+ if (attrs.images) {
+ this.images = attrs.images.map(json => new this.Image(json));
+ }
+ this.openGraph = attrs.openGraph || null;
+ this.twitterCard = attrs.twitterCard || null;
+ this.documentSize = attrs.documentSize || null;
+ this.thumbnail = attrs.thumbnail || null;
+ this.abTests = attrs.abTests || null;
+ this.firefoxChannel = attrs.firefoxChannel || null;
+ this._clips = {};
+ if (attrs.clips) {
+ for (const clipId in attrs.clips) {
+ const clip = attrs.clips[clipId];
+ this._clips[clipId] = new this.Clip(this, clipId, clip);
+ }
+ }
+
+ const isProd =
+ typeof process !== "undefined" && process.env.NODE_ENV === "production";
+
+ for (const attr in attrs) {
+ if (
+ attr !== "clips" &&
+ attr !== "id" &&
+ !this.REGULAR_ATTRS.includes(attr) &&
+ !this.DEPRECATED_ATTRS.includes(attr)
+ ) {
+ if (isProd) {
+ console.warn("Unexpected attribute: " + attr);
+ } else {
+ throw new Error("Unexpected attribute: " + attr);
+ }
+ } else if (attr === "id") {
+ console.warn("passing id in attrs in AbstractShot constructor");
+ console.trace();
+ assert(attrs.id === this.id);
+ }
+ }
+ }
+
+ /** Update any and all attributes in the json object, with deep updating
+ of `json.clips` */
+ update(json) {
+ const ALL_ATTRS = ["clips"].concat(this.REGULAR_ATTRS);
+ assert(
+ checkObject(json, [], ALL_ATTRS),
+ "Bad attr to new Shot():",
+ Object.keys(json)
+ );
+ for (const attr in json) {
+ if (attr === "clips") {
+ continue;
+ }
+ if (
+ typeof json[attr] === "object" &&
+ typeof this[attr] === "object" &&
+ this[attr] !== null
+ ) {
+ let val = this[attr];
+ if (val.toJSON) {
+ val = val.toJSON();
+ }
+ if (!deepEqual(json[attr], val)) {
+ this[attr] = json[attr];
+ }
+ } else if (json[attr] !== this[attr] && (json[attr] || this[attr])) {
+ this[attr] = json[attr];
+ }
+ }
+ if (json.clips) {
+ for (const clipId in json.clips) {
+ if (!json.clips[clipId]) {
+ this.delClip(clipId);
+ } else if (!this.getClip(clipId)) {
+ this.setClip(clipId, json.clips[clipId]);
+ } else if (
+ !deepEqual(this.getClip(clipId).toJSON(), json.clips[clipId])
+ ) {
+ this.setClip(clipId, json.clips[clipId]);
+ }
+ }
+ }
+ }
+
+ /** Returns a JSON version of this shot */
+ toJSON() {
+ const result = {};
+ for (const attr of this.REGULAR_ATTRS) {
+ let val = this[attr];
+ if (val && val.toJSON) {
+ val = val.toJSON();
+ }
+ result[attr] = val;
+ }
+ result.clips = {};
+ for (const attr in this._clips) {
+ result.clips[attr] = this._clips[attr].toJSON();
+ }
+ return result;
+ }
+
+ /** A more minimal JSON representation for creating indexes of shots */
+ asRecallJson() {
+ const result = { clips: {} };
+ for (const attr of this.RECALL_ATTRS) {
+ let val = this[attr];
+ if (val && val.toJSON) {
+ val = val.toJSON();
+ }
+ result[attr] = val;
+ }
+ for (const name of this.clipNames()) {
+ result.clips[name] = this.getClip(name).toJSON();
+ }
+ return result;
+ }
+
+ get backend() {
+ return this._backend;
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get url() {
+ return this.fullUrl || this.origin;
+ }
+ set url(val) {
+ throw new Error(".url is read-only");
+ }
+
+ get fullUrl() {
+ return this._fullUrl;
+ }
+ set fullUrl(val) {
+ if (val) {
+ assertUrl(val);
+ }
+ this._fullUrl = val || undefined;
+ }
+
+ get origin() {
+ return this._origin;
+ }
+ set origin(val) {
+ if (val) {
+ assertOrigin(val);
+ }
+ this._origin = val || undefined;
+ }
+
+ get isOwner() {
+ return this._isOwner;
+ }
+
+ set isOwner(val) {
+ this._isOwner = val || undefined;
+ }
+
+ get filename() {
+ let filenameTitle = this.title;
+ const date = new Date(this.createdDate);
+ /* eslint-disable no-control-regex */
+ filenameTitle = filenameTitle
+ .replace(/[\\/]/g, "_")
+ .replace(/[\u200e\u200f\u202a-\u202e]/g, "")
+ .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ")
+ .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
+ /* eslint-enable no-control-regex */
+ filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
+ const currentDateTime = new Date(
+ date.getTime() - date.getTimezoneOffset() * 60 * 1000
+ ).toISOString();
+ const filenameDate = currentDateTime.substring(0, 10);
+ const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-");
+ let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`;
+
+ // Crop the filename size at less than 246 bytes, so as to leave
+ // room for the extension and an ellipsis [...]. Note that JS
+ // strings are UTF16 but the filename will be converted to UTF8
+ // when saving which could take up more space, and we want a
+ // maximum of 255 bytes (not characters). Here, we iterate
+ // and crop at shorter and shorter points until we fit into
+ // 255 bytes.
+ let suffix = "";
+ for (let cropSize = 246; cropSize >= 0; cropSize -= 32) {
+ if (new Blob([clipFilename]).size > 246) {
+ clipFilename = clipFilename.substring(0, cropSize);
+ suffix = "[...]";
+ } else {
+ break;
+ }
+ }
+
+ clipFilename += suffix;
+
+ const 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 (/^https?:\/\//i.test(this.url)) {
+ let txt = this.url;
+ txt = txt.replace(/^[a-z]{1,4000}:\/\//i, "");
+ txt = txt.replace(/\/.{0,4000}/, "");
+ txt = txt.replace(/^www\./i, "");
+ return txt;
+ } else if (this.url.startsWith("data:")) {
+ return "data:url";
+ }
+ let txt = this.url;
+ txt = txt.replace(/\?.{0,4000}/, "");
+ return txt;
+ }
+
+ get viewUrl() {
+ const url = this.backend + "/" + this.id;
+ return url;
+ }
+
+ get creatingUrl() {
+ let url = `${this.backend}/creating/${this.id}`;
+ url += `?title=${encodeURIComponent(this.title || "")}`;
+ url += `&url=${encodeURIComponent(this.url)}`;
+ return url;
+ }
+
+ get jsonUrl() {
+ return this.backend + "/data/" + this.id;
+ }
+
+ get oembedUrl() {
+ return this.backend + "/oembed?url=" + encodeURIComponent(this.viewUrl);
+ }
+
+ get docTitle() {
+ return this._title;
+ }
+ set docTitle(val) {
+ assert(val === null || typeof val === "string", "Bad docTitle:", val);
+ this._title = val;
+ }
+
+ get openGraph() {
+ return this._openGraph || null;
+ }
+ set openGraph(val) {
+ assert(val === null || typeof val === "object", "Bad openGraph:", val);
+ if (val) {
+ assert(
+ checkObject(val, [], this._OPENGRAPH_PROPERTIES),
+ "Bad attr to openGraph:",
+ Object.keys(val)
+ );
+ this._openGraph = val;
+ } else {
+ this._openGraph = null;
+ }
+ }
+
+ get twitterCard() {
+ return this._twitterCard || null;
+ }
+ set twitterCard(val) {
+ assert(val === null || typeof val === "object", "Bad twitterCard:", val);
+ if (val) {
+ assert(
+ checkObject(val, [], this._TWITTERCARD_PROPERTIES),
+ "Bad attr to twitterCard:",
+ Object.keys(val)
+ );
+ this._twitterCard = val;
+ } else {
+ this._twitterCard = null;
+ }
+ }
+
+ get userTitle() {
+ return this._userTitle;
+ }
+ set userTitle(val) {
+ assert(val === null || typeof val === "string", "Bad userTitle:", val);
+ this._userTitle = val;
+ }
+
+ get title() {
+ // FIXME: we shouldn't support both openGraph.title and ogTitle
+ const ogTitle = this.openGraph && this.openGraph.title;
+ const twitterTitle = this.twitterCard && this.twitterCard.title;
+ let title =
+ this.userTitle || ogTitle || twitterTitle || this.docTitle || this.url;
+ if (Array.isArray(title)) {
+ title = title[0];
+ }
+ if (!title) {
+ title = "Screenshot";
+ }
+ return title;
+ }
+
+ get createdDate() {
+ return this._createdDate;
+ }
+ set createdDate(val) {
+ assert(val === null || typeof val === "number", "Bad createdDate:", val);
+ this._createdDate = val;
+ }
+
+ clipNames() {
+ const names = Object.getOwnPropertyNames(this._clips);
+ names.sort(function (a, b) {
+ return a.sortOrder < b.sortOrder ? 1 : 0;
+ });
+ return names;
+ }
+ getClip(name) {
+ return this._clips[name];
+ }
+ addClip(val) {
+ const name = makeRandomId();
+ this.setClip(name, val);
+ return name;
+ }
+ setClip(name, val) {
+ const clip = new this.Clip(this, name, val);
+ this._clips[name] = clip;
+ }
+ delClip(name) {
+ if (!this._clips[name]) {
+ throw new Error("No existing clip with id: " + name);
+ }
+ delete this._clips[name];
+ }
+ delAllClips() {
+ this._clips = {};
+ }
+ biggestClipSortOrder() {
+ let biggest = 0;
+ for (const clipId in this._clips) {
+ biggest = Math.max(biggest, this._clips[clipId].sortOrder);
+ }
+ return biggest;
+ }
+ updateClipUrl(clipId, clipUrl) {
+ const clip = this.getClip(clipId);
+ if (clip && clip.image) {
+ clip.image.url = clipUrl;
+ } else {
+ console.warn("Tried to update the url of a clip with no image:", clip);
+ }
+ }
+
+ get siteName() {
+ return this._siteName || null;
+ }
+ set siteName(val) {
+ assert(typeof val === "string" || !val);
+ this._siteName = val;
+ }
+
+ get documentSize() {
+ return this._documentSize;
+ }
+ set documentSize(val) {
+ assert(typeof val === "object" || !val);
+ if (val) {
+ assert(
+ checkObject(
+ val,
+ ["height", "width"],
+ "Bad attr to documentSize:",
+ Object.keys(val)
+ )
+ );
+ assert(typeof val.height === "number");
+ assert(typeof val.width === "number");
+ this._documentSize = val;
+ } else {
+ this._documentSize = null;
+ }
+ }
+
+ get thumbnail() {
+ return this._thumbnail;
+ }
+ set thumbnail(val) {
+ assert(typeof val === "string" || !val);
+ if (val) {
+ assert(isUrl(val));
+ this._thumbnail = val;
+ } else {
+ this._thumbnail = null;
+ }
+ }
+
+ get abTests() {
+ return this._abTests;
+ }
+ set abTests(val) {
+ if (val === null || val === undefined) {
+ this._abTests = null;
+ return;
+ }
+ assert(
+ typeof val === "object",
+ "abTests should be an object, not:",
+ typeof val
+ );
+ assert(!Array.isArray(val), "abTests should not be an Array");
+ for (const name in val) {
+ assert(
+ val[name] && typeof val[name] === "string",
+ `abTests.${name} should be a string:`,
+ typeof val[name]
+ );
+ }
+ this._abTests = val;
+ }
+
+ get firefoxChannel() {
+ return this._firefoxChannel;
+ }
+ set firefoxChannel(val) {
+ if (val === null || val === undefined) {
+ this._firefoxChannel = null;
+ return;
+ }
+ assert(
+ typeof val === "string",
+ "firefoxChannel should be a string, not:",
+ typeof val
+ );
+ this._firefoxChannel = val;
+ }
+ }
+
+ AbstractShot.prototype.REGULAR_ATTRS = `
+origin fullUrl docTitle userTitle createdDate images
+siteName openGraph twitterCard documentSize
+thumbnail abTests firefoxChannel
+`.split(/\s+/g);
+
+ // Attributes that will be accepted in the constructor, but ignored/dropped
+ AbstractShot.prototype.DEPRECATED_ATTRS = `
+microdata history ogTitle createdDevice head body htmlAttrs bodyAttrs headAttrs
+readable hashtags comments showPage isPublic resources url
+fullScreenThumbnail favicon
+`.split(/\s+/g);
+
+ AbstractShot.prototype.RECALL_ATTRS = `
+url docTitle userTitle createdDate openGraph twitterCard images thumbnail
+`.split(/\s+/g);
+
+ AbstractShot.prototype._OPENGRAPH_PROPERTIES = `
+title type url image audio description determiner locale site_name video
+image:secure_url image:type image:width image:height
+video:secure_url video:type video:width image:height
+audio:secure_url audio:type
+article:published_time article:modified_time article:expiration_time article:author article:section article:tag
+book:author book:isbn book:release_date book:tag
+profile:first_name profile:last_name profile:username profile:gender
+`.split(/\s+/g);
+
+ AbstractShot.prototype._TWITTERCARD_PROPERTIES = `
+card site title description image
+player player:width player:height player:stream player:stream:content_type
+`.split(/\s+/g);
+
+ /** Represents one found image in the document (not a clip) */
+ class _Image {
+ // FIXME: either we have to notify the shot of updates, or make
+ // this read-only
+ constructor(json) {
+ assert(typeof json === "object", "Clip Image given a non-object", json);
+ assert(
+ checkObject(json, ["url"], ["dimensions", "title", "alt"]),
+ "Bad attrs for Image:",
+ Object.keys(json)
+ );
+ assert(isUrl(json.url), "Bad Image url:", json.url);
+ this.url = json.url;
+ assert(
+ !json.dimensions ||
+ (typeof json.dimensions.x === "number" &&
+ typeof json.dimensions.y === "number"),
+ "Bad Image dimensions:",
+ json.dimensions
+ );
+ this.dimensions = json.dimensions;
+ assert(
+ typeof json.title === "string" || !json.title,
+ "Bad Image title:",
+ json.title
+ );
+ this.title = json.title;
+ assert(
+ typeof json.alt === "string" || !json.alt,
+ "Bad Image alt:",
+ json.alt
+ );
+ this.alt = json.alt;
+ }
+
+ toJSON() {
+ return jsonify(this, ["url"], ["dimensions"]);
+ }
+ }
+
+ AbstractShot.prototype.Image = _Image;
+
+ /** Represents a clip, either a text or image clip */
+ class _Clip {
+ constructor(shot, id, json) {
+ this._shot = shot;
+ assert(
+ checkObject(json, ["createdDate", "image"], ["sortOrder"]),
+ "Bad attrs for Clip:",
+ Object.keys(json)
+ );
+ assert(typeof id === "string" && id, "Bad Clip id:", id);
+ this._id = id;
+ this.createdDate = json.createdDate;
+ if ("sortOrder" in json) {
+ assert(
+ typeof json.sortOrder === "number" || !json.sortOrder,
+ "Bad Clip sortOrder:",
+ json.sortOrder
+ );
+ }
+ if ("sortOrder" in json) {
+ this.sortOrder = json.sortOrder;
+ } else {
+ const biggestOrder = shot.biggestClipSortOrder();
+ this.sortOrder = biggestOrder + 100;
+ }
+ this.image = json.image;
+ }
+
+ toString() {
+ return `[Shot Clip id=${this.id} sortOrder=${this.sortOrder} image ${this.image.dimensions.x}x${this.image.dimensions.y}]`;
+ }
+
+ toJSON() {
+ return jsonify(this, ["createdDate"], ["sortOrder", "image"]);
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get createdDate() {
+ return this._createdDate;
+ }
+ set createdDate(val) {
+ assert(typeof val === "number" || !val, "Bad Clip createdDate:", val);
+ this._createdDate = val;
+ }
+
+ get image() {
+ return this._image;
+ }
+ set image(image) {
+ if (!image) {
+ this._image = undefined;
+ return;
+ }
+ 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 === "fullPageTruncated" ||
+ !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" &&
+ image.location.topLeftOffset &&
+ typeof image.location.topLeftOffset.x === "number" &&
+ typeof image.location.topLeftOffset.y === "number" &&
+ typeof image.location.bottomRightElement === "string" &&
+ image.location.bottomRightOffset &&
+ typeof image.location.bottomRightOffset.x === "number" &&
+ typeof image.location.bottomRightOffset.y === "number",
+ "Bad Clip image element location:",
+ image.location
+ );
+ }
+ this._image = image;
+ }
+
+ isDataUrl() {
+ if (this.image) {
+ return this.image.url.startsWith("data:");
+ }
+ return false;
+ }
+
+ get sortOrder() {
+ return this._sortOrder || null;
+ }
+ set sortOrder(val) {
+ assert(typeof val === "number" || !val, "Bad Clip sortOrder:", val);
+ this._sortOrder = val;
+ }
+ }
+
+ AbstractShot.prototype.Clip = _Clip;
+
+ if (typeof exports !== "undefined") {
+ exports.AbstractShot = AbstractShot;
+ exports.originFromUrl = originFromUrl;
+ exports.isValidClipImageUrl = isValidClipImageUrl;
+ }
+
+ return exports;
+})();
+null;
diff --git a/browser/extensions/screenshots/build/thumbnailGenerator.js b/browser/extensions/screenshots/build/thumbnailGenerator.js
new file mode 100644
index 0000000000..c80ccb6bac
--- /dev/null
+++ b/browser/extensions/screenshots/build/thumbnailGenerator.js
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.thumbnailGenerator = (function () {
+ let exports = {}; // This is used in webextension/background/takeshot.js,
+ // server/src/pages/shot/controller.js, and
+ // server/scr/pages/shotindex/view.js. It is used in a browser
+ // environment.
+
+ // Resize down 1/2 at a time produces better image quality.
+ // Not quite as good as using a third-party filter (which will be
+ // slower), but good enough.
+ const maxResizeScaleFactor = 0.5;
+
+ // The shot will be scaled or cropped down to 210px on x, and cropped or
+ // scaled down to a maximum of 280px on y.
+ // x: 210
+ // y: <= 280
+ const maxThumbnailWidth = 210;
+ const maxThumbnailHeight = 280;
+
+ /**
+ * @param {int} imageHeight Height in pixels of the original image.
+ * @param {int} imageWidth Width in pixels of the original image.
+ * @returns {width, height, scaledX, scaledY}
+ */
+ function getThumbnailDimensions(imageWidth, imageHeight) {
+ const displayAspectRatio = 3 / 4;
+ const imageAspectRatio = imageWidth / imageHeight;
+ let thumbnailImageWidth, thumbnailImageHeight;
+ let scaledX, scaledY;
+
+ if (imageAspectRatio > displayAspectRatio) {
+ // "Landscape" mode
+ // Scale on y, crop on x
+ const yScaleFactor =
+ imageHeight > maxThumbnailHeight
+ ? maxThumbnailHeight / imageHeight
+ : 1.0;
+ thumbnailImageHeight = scaledY = Math.round(imageHeight * yScaleFactor);
+ scaledX = Math.round(imageWidth * yScaleFactor);
+ thumbnailImageWidth = Math.min(scaledX, maxThumbnailWidth);
+ } else {
+ // "Portrait" mode
+ // Scale on x, crop on y
+ const xScaleFactor =
+ imageWidth > maxThumbnailWidth ? maxThumbnailWidth / imageWidth : 1.0;
+ thumbnailImageWidth = scaledX = Math.round(imageWidth * xScaleFactor);
+ scaledY = Math.round(imageHeight * xScaleFactor);
+ // The CSS could widen the image, in which case we crop more off of y.
+ thumbnailImageHeight = Math.min(
+ scaledY,
+ maxThumbnailHeight,
+ maxThumbnailHeight / (maxThumbnailWidth / imageWidth)
+ );
+ }
+
+ return {
+ width: thumbnailImageWidth,
+ height: thumbnailImageHeight,
+ scaledX,
+ scaledY,
+ };
+ }
+
+ /**
+ * @param {dataUrl} String Data URL of the original image.
+ * @param {int} imageHeight Height in pixels of the original image.
+ * @param {int} imageWidth Width in pixels of the original image.
+ * @param {String} urlOrBlob 'blob' for a blob, otherwise data url.
+ * @returns A promise that resolves to the data URL or blob of the thumbnail image, or null.
+ */
+ function createThumbnail(dataUrl, imageWidth, imageHeight, urlOrBlob) {
+ // There's cost associated with generating, transmitting, and storing
+ // thumbnails, so we'll opt out if the image size is below a certain threshold
+ const thumbnailThresholdFactor = 1.2;
+ const thumbnailWidthThreshold =
+ maxThumbnailWidth * thumbnailThresholdFactor;
+ const thumbnailHeightThreshold =
+ maxThumbnailHeight * thumbnailThresholdFactor;
+
+ if (
+ imageWidth <= thumbnailWidthThreshold &&
+ imageHeight <= thumbnailHeightThreshold
+ ) {
+ // Do not create a thumbnail.
+ return Promise.resolve(null);
+ }
+
+ const thumbnailDimensions = getThumbnailDimensions(imageWidth, imageHeight);
+
+ return new Promise((resolve, reject) => {
+ const thumbnailImage = new Image();
+ let srcWidth = imageWidth;
+ let srcHeight = imageHeight;
+ let destWidth, destHeight;
+
+ thumbnailImage.onload = function () {
+ destWidth = Math.round(srcWidth * maxResizeScaleFactor);
+ destHeight = Math.round(srcHeight * maxResizeScaleFactor);
+ if (
+ destWidth <= thumbnailDimensions.scaledX ||
+ destHeight <= thumbnailDimensions.scaledY
+ ) {
+ srcWidth = Math.round(
+ srcWidth * (thumbnailDimensions.width / thumbnailDimensions.scaledX)
+ );
+ srcHeight = Math.round(
+ srcHeight *
+ (thumbnailDimensions.height / thumbnailDimensions.scaledY)
+ );
+ destWidth = thumbnailDimensions.width;
+ destHeight = thumbnailDimensions.height;
+ }
+
+ const thumbnailCanvas = document.createElement("canvas");
+ thumbnailCanvas.width = destWidth;
+ thumbnailCanvas.height = destHeight;
+ const ctx = thumbnailCanvas.getContext("2d");
+ ctx.imageSmoothingEnabled = false;
+
+ ctx.drawImage(
+ thumbnailImage,
+ 0,
+ 0,
+ srcWidth,
+ srcHeight,
+ 0,
+ 0,
+ destWidth,
+ destHeight
+ );
+
+ if (
+ thumbnailCanvas.width <= thumbnailDimensions.width ||
+ thumbnailCanvas.height <= thumbnailDimensions.height
+ ) {
+ if (urlOrBlob === "blob") {
+ thumbnailCanvas.toBlob(blob => {
+ resolve(blob);
+ });
+ } else {
+ resolve(thumbnailCanvas.toDataURL("image/png"));
+ }
+ return;
+ }
+
+ srcWidth = destWidth;
+ srcHeight = destHeight;
+ thumbnailImage.src = thumbnailCanvas.toDataURL();
+ };
+ thumbnailImage.src = dataUrl;
+ });
+ }
+
+ function createThumbnailUrl(shot) {
+ const image = shot.getClip(shot.clipNames()[0]).image;
+ if (!image.url) {
+ return Promise.resolve(null);
+ }
+ return createThumbnail(
+ image.url,
+ image.dimensions.x,
+ image.dimensions.y,
+ "dataurl"
+ );
+ }
+
+ function createThumbnailBlobFromPromise(shot, blobToUrlPromise) {
+ return blobToUrlPromise.then(dataUrl => {
+ const image = shot.getClip(shot.clipNames()[0]).image;
+ return createThumbnail(
+ dataUrl,
+ image.dimensions.x,
+ image.dimensions.y,
+ "blob"
+ );
+ });
+ }
+
+ if (typeof exports !== "undefined") {
+ exports.getThumbnailDimensions = getThumbnailDimensions;
+ exports.createThumbnailUrl = createThumbnailUrl;
+ exports.createThumbnailBlobFromPromise = createThumbnailBlobFromPromise;
+ }
+
+ return exports;
+})();
+null;