├── v2 ├── chrome │ ├── data │ ├── common.js │ └── manifest.json └── firefox │ ├── data │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 20.png │ │ ├── 24.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 512.png │ │ ├── 64.png │ │ ├── part.png │ │ ├── entire.png │ │ └── visual.png │ ├── popup │ │ ├── index.html │ │ ├── index.css │ │ └── index.js │ ├── inject │ │ ├── inject.css │ │ └── inject.js │ └── options │ │ ├── index.html │ │ └── index.js │ ├── manifest.json │ └── common.js ├── v1 ├── drawings │ ├── promo.png │ ├── promo.xcf │ ├── safari.png │ ├── safari.xcf │ ├── large-tile.png │ ├── large-tile.xcf │ ├── screenshot.png │ └── small-tile.xcf ├── src │ ├── data │ │ ├── icons │ │ │ ├── 16.png │ │ │ ├── 20.png │ │ │ ├── 24.png │ │ │ ├── 32.png │ │ │ ├── 48.png │ │ │ ├── 64.png │ │ │ ├── 128.png │ │ │ ├── 256.png │ │ │ ├── 512.png │ │ │ ├── part.png │ │ │ ├── entire.png │ │ │ └── visual.png │ │ ├── content_script │ │ │ ├── inject.css │ │ │ └── inject.js │ │ └── firefox │ │ │ └── inject.js │ ├── lib │ │ ├── config.js │ │ ├── firefox │ │ │ ├── mm.js │ │ │ └── firefox.js │ │ ├── common.js │ │ └── chrome │ │ │ └── chrome.js │ ├── manifest.json │ └── package.json ├── builds │ └── packed │ │ ├── icon.png │ │ ├── chrome.zip │ │ ├── firefox.xpi │ │ └── icon64.png ├── package.json └── gulpfile.js ├── v3 ├── data │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 20.png │ │ ├── 24.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 48.png │ │ ├── 512.png │ │ ├── 64.png │ │ ├── entire.png │ │ ├── part.png │ │ └── visual.png │ ├── inject │ │ ├── connect.js │ │ ├── inject.css │ │ └── inject.js │ ├── copy │ │ ├── index.html │ │ ├── index.css │ │ └── index.js │ ├── options │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ └── popup │ │ ├── index.js │ │ ├── index.html │ │ └── index.css ├── _locales │ ├── zh_CN │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── it │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── de │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ └── pt_PT │ │ └── messages.json ├── manifest.json ├── ceds.js └── worker.js ├── .github └── FUNDING.yml ├── README.md └── LICENSE /v2/chrome/data: -------------------------------------------------------------------------------- 1 | ../firefox/data -------------------------------------------------------------------------------- /v2/chrome/common.js: -------------------------------------------------------------------------------- 1 | ../firefox/common.js -------------------------------------------------------------------------------- /v1/drawings/promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/promo.png -------------------------------------------------------------------------------- /v1/drawings/promo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/promo.xcf -------------------------------------------------------------------------------- /v3/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/128.png -------------------------------------------------------------------------------- /v3/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/16.png -------------------------------------------------------------------------------- /v3/data/icons/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/20.png -------------------------------------------------------------------------------- /v3/data/icons/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/24.png -------------------------------------------------------------------------------- /v3/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/256.png -------------------------------------------------------------------------------- /v3/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/32.png -------------------------------------------------------------------------------- /v3/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/48.png -------------------------------------------------------------------------------- /v3/data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/512.png -------------------------------------------------------------------------------- /v3/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/64.png -------------------------------------------------------------------------------- /v1/drawings/safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/safari.png -------------------------------------------------------------------------------- /v1/drawings/safari.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/safari.xcf -------------------------------------------------------------------------------- /v1/src/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/16.png -------------------------------------------------------------------------------- /v1/src/data/icons/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/20.png -------------------------------------------------------------------------------- /v1/src/data/icons/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/24.png -------------------------------------------------------------------------------- /v1/src/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/32.png -------------------------------------------------------------------------------- /v1/src/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/48.png -------------------------------------------------------------------------------- /v1/src/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/64.png -------------------------------------------------------------------------------- /v3/data/icons/entire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/entire.png -------------------------------------------------------------------------------- /v3/data/icons/part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/part.png -------------------------------------------------------------------------------- /v3/data/icons/visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v3/data/icons/visual.png -------------------------------------------------------------------------------- /v1/builds/packed/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/builds/packed/icon.png -------------------------------------------------------------------------------- /v1/drawings/large-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/large-tile.png -------------------------------------------------------------------------------- /v1/drawings/large-tile.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/large-tile.xcf -------------------------------------------------------------------------------- /v1/drawings/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/screenshot.png -------------------------------------------------------------------------------- /v1/drawings/small-tile.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/drawings/small-tile.xcf -------------------------------------------------------------------------------- /v1/src/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/128.png -------------------------------------------------------------------------------- /v1/src/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/256.png -------------------------------------------------------------------------------- /v1/src/data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/512.png -------------------------------------------------------------------------------- /v1/src/data/icons/part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/part.png -------------------------------------------------------------------------------- /v1/builds/packed/chrome.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/builds/packed/chrome.zip -------------------------------------------------------------------------------- /v1/builds/packed/firefox.xpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/builds/packed/firefox.xpi -------------------------------------------------------------------------------- /v1/builds/packed/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/builds/packed/icon64.png -------------------------------------------------------------------------------- /v1/src/data/icons/entire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/entire.png -------------------------------------------------------------------------------- /v1/src/data/icons/visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v1/src/data/icons/visual.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/128.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/16.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/20.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/24.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/256.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/32.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/48.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/512.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/64.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/part.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/entire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/entire.png -------------------------------------------------------------------------------- /v2/firefox/data/icons/visual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/easy-screenshot/HEAD/v2/firefox/data/icons/visual.png -------------------------------------------------------------------------------- /v3/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "视觉部分、整个页面或具有自动滚动和编辑支持的选择性区域的全功能捕捉工具。" 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "시각적, 전체 페이지 또는 선택 영역을 자동 스크롤 및 편집 지원으로 캡처하는 올인원 도구." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "ビジュアル部分、ページ全体、または自動スクロールと編集をサポートする選択領域のためのオールインワンキャプチャーツール" 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Outil de capture tout-en-un : page, zone ou visuel, avec défilement auto et édition." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "All-in-een capture-tool: pagina, gebied of visueel, met auto-scrollen en bewerking." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Strumento di cattura tuttouno: pagina, area o visivo, con scorrimento auto e modifica." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Herramienta de captura todo en uno: página, área o visual, con desplazamiento auto y edición." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "all-in-on capturing tool for the visual part, the entire page, or a selective area with auto-scrolling and editing support" 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Универсальный инструмент для захвата экрана: всей страницы, области или визуальной части с автопрокруткой и редактированием." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Tool zur Erfassung des sichtbaren Bereichs, der gesamten Seite oder eines Abschnitts mit Auto-Scroll und Bearbeitungsfunktion." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Ferramenta tudo-em-um para capturar a parte visual, página inteira ou área selecionada, com suporte a rolagem automática e edição." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v3/_locales/pt_PT/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": { 3 | "message": "Ferramenta completa para capturar a parte visual, página inteira ou área selecionada, com suporte para rolagem automática e edição." 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /v1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name" : "", 4 | "version" : "", 5 | "dependencies" : 6 | { 7 | "cli-color": "0.2", 8 | "commander": "1.1", 9 | "glob": "4.0.6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /v3/data/inject/connect.js: -------------------------------------------------------------------------------- 1 | // open the requested gid on jsPaint 2 | if (location.hash.includes('gid=')) { 3 | const gid = location.hash.split('gid=')[1]; 4 | chrome.runtime.sendMessage({ 5 | method: 'jspaint-load-resource', 6 | gid 7 | }); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /v3/data/copy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copy to Clipboard :: Easy Screenshot 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /v3/data/copy/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | body { 6 | font-family: "Helvetica Neue",Helvetica,sans-serif; 7 | font-size: 13px; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | margin: 0; 13 | color: #414141; 14 | background-color: #fff; 15 | } 16 | 17 | input[type=button] { 18 | height: 28px; 19 | color: #444; 20 | background-image: linear-gradient(rgb(237, 237, 237), rgb(237, 237, 237) 38%, rgb(222, 222, 222)); 21 | box-shadow: rgba(0, 0, 0, 0.08) 0 1px 0, rgba(255, 255, 255, 0.75) 0 1px 2px inset; 22 | text-shadow: rgb(240, 240, 240) 0 1px 0; 23 | border: solid 1px rgba(0, 0, 0, 0.25); 24 | } 25 | 26 | .note:not(:empty) { 27 | padding: 10px; 28 | text-align: center; 29 | border: solid 2px #FFC107; 30 | background-color: #fff7b1; 31 | margin: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /v2/firefox/data/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Capture Entire Screen
9 |
Capture Visual Part
10 |
Capture a Portion
11 | 12 |

13 | 14 | 15 |

16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: webextension 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /v2/firefox/data/inject/inject.css: -------------------------------------------------------------------------------- 1 | .itrisearch-box { 2 | box-sizing: border-box; 3 | position: fixed; 4 | z-index: 2147483646; 5 | border: gray 1px dotted; 6 | box-shadow: 0 0 0 50000px rgba(0, 0, 0, 0.2); 7 | } 8 | .itrisearch-box::before { 9 | content: ''; 10 | display: block; 11 | width: calc(100% + 20px); 12 | height: calc(100% + 20px); 13 | margin-left: -10px; 14 | margin-top: -10px; 15 | cursor: crosshair; 16 | } 17 | 18 | .itrisearch-guide-1, 19 | .itrisearch-guide-2 { 20 | box-sizing: border-box; 21 | position: fixed; 22 | z-index: 2147483646; 23 | } 24 | .itrisearch-guide-1 { 25 | border-right: dashed 1px gray; 26 | top: 0; 27 | left: 0; 28 | height: 100%; 29 | } 30 | .itrisearch-guide-2 { 31 | border-bottom: dashed 1px gray; 32 | top: 0; 33 | width: 100%; 34 | } 35 | .itrisearch-guide-3 { 36 | z-index: 2147483645; 37 | position: fixed; 38 | top: 0; 39 | left: 0; 40 | width: 100%; 41 | height: 100%; 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | all-in-on capturing tool for the visual part, the entire page, or a selective area with auto-scrolling and editing support 2 | 3 | ### Features: 4 | 5 | * Capture the full screen, encompassing both horizontal and vertical scroll bars 6 | * Select and capture a defined area of the display 7 | * Snapshot the content currently visible within the viewport 8 | 9 | ### YouTube Preview 10 | [![YouTube Preview](https://img.youtube.com/vi/8l8z9SLuzWs/0.jpg)](https://www.youtube.com/watch?v=8l8z9SLuzWs) 11 | 12 | ### Links: 13 | * FAQs page: https://webextension.org/listing/screenshot.html 14 | * Chrome Webstore: https://chromewebstore.google.com/detail/easy-screenshot/ilihnlnookgcpkpnjafigbkdniceonip 15 | * Firefox add-ons: https://addons.mozilla.org/firefox/addon/web-clipper-easy-screenshot/ 16 | * Opera addons: https://addons.opera.com/extensions/details/easy-screenshot/ 17 | * Edge add-ons: https://microsoftedge.microsoft.com/addons/detail/gfnppipflpjedfjepacmjcoehkiplaef 18 | -------------------------------------------------------------------------------- /v1/src/data/content_script/inject.css: -------------------------------------------------------------------------------- 1 | .itrisearch-box { 2 | box-sizing: border-box; 3 | position: fixed; 4 | z-index: 2147483646; 5 | border: gray 1px dotted; 6 | box-shadow: 0 0 0 50000px rgba(0, 0, 0, 0.2); 7 | box-size 8 | } 9 | .itrisearch-box:before { 10 | content: ''; 11 | display: block; 12 | width: calc(100% + 20px); 13 | height: calc(100% + 20px); 14 | margin-left: -10px; 15 | margin-top: -10px; 16 | cursor: crosshair; 17 | } 18 | 19 | .itrisearch-guide-1, 20 | .itrisearch-guide-2 { 21 | box-sizing: border-box; 22 | position: fixed; 23 | z-index: 2147483646; 24 | } 25 | .itrisearch-guide-1 { 26 | border-right: dashed 1px gray; 27 | top: 0; 28 | left: 0; 29 | height: 100%; 30 | } 31 | .itrisearch-guide-2 { 32 | border-bottom: dashed 1px gray; 33 | top: 0; 34 | width: 100%; 35 | } 36 | .itrisearch-guide-3 { 37 | z-index: 2147483645; 38 | position: fixed; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | } 44 | -------------------------------------------------------------------------------- /v1/src/lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = app || require('./firefox/firefox'); 4 | var config = typeof exports === 'undefined' ? {} : exports; 5 | 6 | config.options = { 7 | get timestamp () { 8 | return app.storage.read('timestamp') === 'false' ? false : true; 9 | } 10 | }; 11 | 12 | config.welcome = { 13 | get version () { 14 | return app.storage.read('version'); 15 | }, 16 | set version (val) { 17 | app.storage.write('version', val); 18 | }, 19 | timeout: 3 20 | }; 21 | // Complex get and set 22 | config.get = function (name) { 23 | return name.split('.').reduce(function (p, c) { 24 | return p[c]; 25 | }, config); 26 | }; 27 | config.set = function (name, value) { 28 | function set(name, value, scope) { 29 | name = name.split('.'); 30 | if (name.length > 1) { 31 | set.call((scope || this)[name.shift()], name.join('.'), value); 32 | } 33 | else { 34 | this[name[0]] = value; 35 | } 36 | } 37 | set(name, value, config); 38 | }; 39 | -------------------------------------------------------------------------------- /v1/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Clipper : Easy Screenshot", 3 | "short_name": "iescreenshot", 4 | "description": "Easy Screenshot is an easy-to-use screen capturing tool from right-click context menu.", 5 | "author": "Jeremy Schomery", 6 | "version": "0.2.0", 7 | "manifest_version": 2, 8 | "permissions": [ 9 | "tabs", 10 | "contextMenus", 11 | "" 12 | ], 13 | "background": { 14 | "scripts": [ 15 | "lib/chrome/chrome.js", 16 | "lib/config.js", 17 | "lib/common.js" 18 | ] 19 | }, 20 | "content_scripts": [ 21 | { 22 | "matches": [""], 23 | "css": ["data/content_script/inject.css"], 24 | "js": ["data/content_script/inject.js"], 25 | "run_at": "document_start", 26 | "all_frames": false 27 | } 28 | ], 29 | "homepage_url": "http://mybrowseraddon.com/screenshot.html", 30 | "icons": { 31 | "16": "data/icons/16.png", 32 | "48": "data/icons/48.png", 33 | "128": "data/icons/128.png" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /v2/firefox/data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options Page :: Easy Screenshot 5 | 6 | 7 | 8 |

9 | 10 |
11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
Delay in milliseconds between each capture (entire page)
Scroll offset (entire page)
23 | 24 |

25 | 26 | 27 |

28 |

29 | - 30 | 31 |

32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /v3/data/inject/inject.css: -------------------------------------------------------------------------------- 1 | .itrisearch-box { 2 | all: initial; 3 | box-sizing: border-box; 4 | position: fixed; 5 | z-index: 2147483646; 6 | border: gray 1px dotted; 7 | box-shadow: 0 0 0 50000px rgba(0, 0, 0, 0.2); 8 | background-color: transparent; 9 | } 10 | .itrisearch-box::before { 11 | content: ''; 12 | display: block; 13 | width: calc(100% + 20px); 14 | height: calc(100% + 20px); 15 | margin-left: -10px; 16 | margin-top: -10px; 17 | cursor: crosshair; 18 | } 19 | 20 | .itrisearch-guide-1, 21 | .itrisearch-guide-2 { 22 | all: initial; 23 | background-color: transparent; 24 | box-sizing: border-box; 25 | position: fixed; 26 | z-index: 2147483646; 27 | } 28 | .itrisearch-guide-1 { 29 | border-right: dashed 1px gray; 30 | top: 0; 31 | left: 0; 32 | height: 100%; 33 | } 34 | .itrisearch-guide-2 { 35 | border-bottom: dashed 1px gray; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | } 40 | .itrisearch-guide-3 { 41 | all: initial; 42 | background: transparent; 43 | z-index: 2147483645; 44 | position: fixed; 45 | top: 0; 46 | left: 0; 47 | width: 100%; 48 | height: 100%; 49 | } 50 | -------------------------------------------------------------------------------- /v3/data/options/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 13px; 3 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 4 | background-color: #fff; 5 | color: #4d5156; 6 | margin: 10px; 7 | } 8 | select, 9 | button, 10 | input[type=submit], 11 | input[type=button] { 12 | color: #444; 13 | background-image: linear-gradient(rgb(237, 237, 237), rgb(237, 237, 237) 38%, rgb(222, 222, 222)); 14 | box-shadow: rgba(0, 0, 0, 0.08) 0 1px 0, rgba(255, 255, 255, 0.75) 0 1px 2px inset; 15 | text-shadow: rgb(240, 240, 240) 0 1px 0; 16 | padding: 5px; 17 | } 18 | select, 19 | button, 20 | textarea, 21 | input { 22 | border: solid 1px rgba(0, 0, 0, 0.25); 23 | } 24 | input[type=button]:disabled { 25 | opacity: 0.5; 26 | } 27 | textarea { 28 | width: 100%; 29 | box-sizing: border-box; 30 | display: block; 31 | } 32 | textarea, 33 | input[type=text], 34 | input[type=number] { 35 | padding: 5px; 36 | outline: none; 37 | } 38 | textarea:focus, 39 | input[type=text]:focus, 40 | input[type=number]:focus { 41 | background-color: #e5f8ff; 42 | } 43 | a, 44 | a:visited { 45 | color: #07c; 46 | } 47 | .opts { 48 | display: grid; 49 | grid-template-columns: 1fr min-content; 50 | grid-gap: 10px; 51 | align-items: start; 52 | } 53 | -------------------------------------------------------------------------------- /v2/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Easy Screenshot - a Web Clipper", 3 | "description": "an easy-to-use screen capturing tool for the visual part, the entire page, or a selective area with auto-scroll support", 4 | "version": "0.3.7", 5 | "manifest_version": 2, 6 | "permissions": [ 7 | "contextMenus", 8 | "activeTab", 9 | "downloads", 10 | "notifications", 11 | "storage" 12 | ], 13 | "optional_permissions": [ 14 | "clipboardWrite" 15 | ], 16 | "offline_enabled": true, 17 | "background": { 18 | "persistent": false, 19 | "scripts": [ 20 | "common.js" 21 | ] 22 | }, 23 | "homepage_url": "https://add0n.com/screenshot.html", 24 | "icons": { 25 | "16": "data/icons/16.png", 26 | "20": "data/icons/20.png", 27 | "24": "data/icons/24.png", 28 | "32": "data/icons/32.png", 29 | "48": "data/icons/48.png", 30 | "64": "data/icons/64.png", 31 | "128": "data/icons/128.png", 32 | "256": "data/icons/256.png", 33 | "512": "data/icons/512.png" 34 | }, 35 | "options_ui": { 36 | "page": "data/options/index.html", 37 | "chrome_style": true 38 | }, 39 | "browser_action": { 40 | "default_popup": "data/popup/index.html" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /v1/src/lib/firefox/mm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var self = require('sdk/self'); 4 | var data = self.data; 5 | var tabs = require('sdk/tabs'); 6 | var tabsUtils = require('sdk/tabs/utils'); 7 | var unload = require('sdk/system/unload'); 8 | var {viewFor} = require('sdk/view/core'); 9 | var callback; 10 | 11 | function getMM (tab) { 12 | return tabsUtils.getBrowserForTab(viewFor(tab)).messageManager; 13 | } 14 | 15 | function connect (e) { 16 | if (callback) { 17 | callback(e.data); 18 | } 19 | } 20 | 21 | /* detach */ 22 | unload.when(function () { 23 | for (let tab of tabs) { 24 | var mm = getMM(tab); 25 | mm.removeMessageListener(self.name + '-connect', connect); 26 | mm.sendAsyncMessage(self.name + '-detach'); 27 | } 28 | }); 29 | 30 | exports.connect = function (c) { 31 | callback = c; 32 | }; 33 | exports.emit = function (name, obj) { 34 | let tab = tabs.activeTab; 35 | if (!tab.isInstalled) { 36 | let path = data.url('./firefox/inject.js') + '?' + Math.random(); 37 | var mm = getMM(tab); 38 | mm.loadFrameScript(path, true); 39 | mm.addMessageListener(self.name + '-connect', connect); 40 | tab.isInstalled = true; 41 | } 42 | if (tab) { 43 | getMM(tab).sendAsyncMessage(self.name + '-' + name, obj); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /v2/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Easy Screenshot - a Web Clipper", 3 | "description": "an easy-to-use screen capturing tool for the visual part, the entire page, or a selective area with auto-scroll support", 4 | "version": "0.3.7", 5 | "manifest_version": 2, 6 | "permissions": [ 7 | "contextMenus", 8 | "tabs", 9 | "", 10 | "downloads", 11 | "notifications", 12 | "storage" 13 | ], 14 | "optional_permissions": [ 15 | "clipboardWrite" 16 | ], 17 | "offline_enabled": true, 18 | "background": { 19 | "persistent": false, 20 | "scripts": [ 21 | "common.js" 22 | ] 23 | }, 24 | "homepage_url": "https://add0n.com/screenshot.html", 25 | "icons": { 26 | "16": "data/icons/16.png", 27 | "20": "data/icons/20.png", 28 | "24": "data/icons/24.png", 29 | "32": "data/icons/32.png", 30 | "48": "data/icons/48.png", 31 | "64": "data/icons/64.png", 32 | "128": "data/icons/128.png", 33 | "256": "data/icons/256.png", 34 | "512": "data/icons/512.png" 35 | }, 36 | "options_ui": { 37 | "page": "data/options/index.html", 38 | "chrome_style": true 39 | }, 40 | "browser_action": { 41 | "default_popup": "data/popup/index.html" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /v1/src/data/firefox/inject.js: -------------------------------------------------------------------------------- 1 | /* globals content, addMessageListener, removeMessageListener, sendAsyncMessage */ 2 | 'use strict'; 3 | 4 | (function (observers) { 5 | var active = true; 6 | var id = 'iescreenshot'; 7 | 8 | function connect (obj) { 9 | sendAsyncMessage(id + '-connect', obj); 10 | } 11 | 12 | observers.screenshot = function (e) { 13 | var thumbnail = content.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); 14 | var left = e.data.left || 0; 15 | var top = e.data.top || 0; 16 | var width = e.data.width || content.innerWidth; 17 | var height = e.data.height || content.innerHeight; 18 | thumbnail.width = width; 19 | thumbnail.height = height; 20 | var ctx = thumbnail.getContext('2d'); 21 | ctx.drawWindow(content, content.scrollX + left, content.scrollY + top, width, height, '#fff'); 22 | connect(thumbnail.toDataURL()); 23 | }; 24 | 25 | function detach () { 26 | for (var name in observers) { 27 | removeMessageListener(id + '-' + name, observers[name]); 28 | } 29 | removeMessageListener(id + '-detach', detach); 30 | active = false; 31 | } 32 | if (active) { 33 | for (var name in observers) { 34 | addMessageListener(id + '-' + name, observers[name]); 35 | } 36 | addMessageListener(id + '-detach', detach); 37 | } 38 | })({}); 39 | -------------------------------------------------------------------------------- /v2/firefox/data/popup/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 200px; 3 | display: flex; 4 | flex-direction: column; 5 | font-family: "Helvetica Neue",Helvetica,sans-serif; 6 | font-size: 13px; 7 | } 8 | 9 | div[data-cmd] { 10 | background-position: left 10px center; 11 | background-repeat: no-repeat; 12 | width: 48px; 13 | height: 48px; 14 | line-height: 48px; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | width: 100%; 19 | text-indent: 48px; 20 | cursor: pointer; 21 | } 22 | div[data-cmd]:hover { 23 | background-color: rgba(0, 0, 0, 0.1); 24 | } 25 | div[data-cmd]:active { 26 | opacity: 0.5; 27 | } 28 | div[data-cmd="capture-entire"] { 29 | background-image: url(../icons/entire.png); 30 | } 31 | div[data-cmd="capture-visual"] { 32 | background-image: url(../icons/visual.png); 33 | } 34 | div[data-cmd="capture-portion"] { 35 | background-image: url(../icons/part.png); 36 | } 37 | 38 | body[data-disabled=true] div[data-cmd] { 39 | opacity: 0.3; 40 | pointer-events: none; 41 | } 42 | 43 | #mode { 44 | display: flex; 45 | flex-direction: column; 46 | border-top: solid 3px #ccc; 47 | padding-top: 10px; 48 | margin: 10px 0 0 0; 49 | } 50 | #mode > label { 51 | padding: 5px; 52 | display: flex; 53 | align-items: center; 54 | cursor: pointer; 55 | } 56 | #mode > label:not(:last-child) { 57 | 58 | } 59 | #mode > label:hover { 60 | background-color: rgba(0, 0, 0, 0.1); 61 | } 62 | #mode input { 63 | margin-right: 5px; 64 | } 65 | -------------------------------------------------------------------------------- /v1/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Web Clipper : Easy Screenshot", 3 | "name": "iescreenshot", 4 | "description": "Easy Screenshot is an easy-to-use screen capturing tool from right-click context menu.", 5 | "id": "jid0-SnuIiIyRmNnMhukLu6VK8DQkq12@jetpack", 6 | "license": "MPL 2.0", 7 | "version": "0.2.0", 8 | "author": "Jeremy Schomery", 9 | "main": "./lib/common.js", 10 | "url": "http://mybrowseraddon.com/screenshot.html", 11 | "permissions": { 12 | "private-browsing": true, 13 | "multiprocess": true 14 | }, 15 | "preferences": [ 16 | { 17 | "description": "Display desktop notification when screen is captured or an error occurred during screen capturing", 18 | "type": "bool", 19 | "name": "notification", 20 | "value": true, 21 | "title": "Desktop notification permission:" 22 | }, 23 | { 24 | "type": "directory", 25 | "name": "directory", 26 | "title": "Custom save directory:" 27 | }, 28 | { 29 | "description": "When a new screenshot is captured, a timestamp is appended to the filename prefix the page title.", 30 | "type": "bool", 31 | "name": "timestamp", 32 | "value": true, 33 | "title": "Append timestamp to filenames" 34 | }, 35 | { 36 | "type": "control", 37 | "label": "Learn more", 38 | "name": "tineye", 39 | "title": "Perform Reverse Image Search:", 40 | "description": "'Capture, Reverse Image Search' extension is a tool based on Easy Screenshot that captures a portion of screen and performs reverse image search on it." 41 | }] 42 | } 43 | -------------------------------------------------------------------------------- /v2/firefox/data/popup/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let tab; 4 | 5 | document.addEventListener('click', e => { 6 | const cmd = e.target.dataset.cmd; 7 | if (cmd) { 8 | chrome.runtime.sendMessage({ 9 | method: 'popup', 10 | cmd, 11 | tab: { 12 | id: tab.id, 13 | title: tab.title 14 | } 15 | }, window.close); 16 | } 17 | }); 18 | 19 | chrome.tabs.query({ 20 | active: true, 21 | currentWindow: true 22 | }, ([t]) => { 23 | tab = t; 24 | if ( 25 | tab.url.startsWith('chrome') || 26 | tab.url.startsWith('mozilla') || 27 | tab.url.startsWith('about') 28 | ) { 29 | document.body.dataset.disabled = true; 30 | } 31 | }); 32 | 33 | chrome.storage.local.get({ 34 | 'save-clipboard': false, 35 | 'save-disk': true 36 | }, prefs => { 37 | document.getElementById('save-clipboard').checked = prefs['save-clipboard']; 38 | document.getElementById('save-disk').checked = prefs['save-disk']; 39 | }); 40 | 41 | document.getElementById('save-clipboard').onchange = e => { 42 | if (e.target.checked) { 43 | chrome.permissions.request({ 44 | permissions: ['clipboardWrite'], 45 | origins: [] 46 | }, granted => { 47 | if (granted === false) { 48 | e.target.checked = false; 49 | } 50 | console.log(e.target.checked); 51 | chrome.storage.local.set({ 52 | 'save-clipboard': e.target.checked 53 | }); 54 | }); 55 | } 56 | else { 57 | chrome.storage.local.set({ 58 | 'save-clipboard': e.target.checked 59 | }); 60 | } 61 | }; 62 | document.getElementById('save-disk').onchange = e => chrome.storage.local.set({ 63 | 'save-disk': e.target.checked 64 | }); 65 | -------------------------------------------------------------------------------- /v3/data/popup/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let tab; 4 | 5 | document.addEventListener('click', e => { 6 | const cmd = e.target.dataset.cmd; 7 | if (cmd) { 8 | chrome.runtime.sendMessage({ 9 | method: 'popup', 10 | cmd, 11 | tab: { 12 | id: tab.id, 13 | title: tab.title, 14 | windowId: tab.windowId 15 | } 16 | }, window.close); 17 | } 18 | }); 19 | 20 | chrome.tabs.query({ 21 | active: true, 22 | currentWindow: true 23 | }, ([t]) => { 24 | tab = t; 25 | if ( 26 | tab.url.startsWith('chrome') || 27 | tab.url.startsWith('mozilla') || 28 | tab.url.startsWith('about') 29 | ) { 30 | document.body.dataset.disabled = true; 31 | } 32 | }); 33 | 34 | chrome.storage.local.get({ 35 | 'save-clipboard': false, 36 | 'save-disk': true, 37 | 'edit-online': false 38 | }, prefs => { 39 | document.getElementById('save-clipboard').checked = prefs['save-clipboard']; 40 | document.getElementById('save-disk').checked = prefs['save-disk']; 41 | document.getElementById('edit-online').checked = prefs['edit-online']; 42 | }); 43 | 44 | document.getElementById('save-clipboard').onchange = e => { 45 | chrome.storage.local.set({ 46 | 'save-clipboard': e.target.checked 47 | }); 48 | }; 49 | document.getElementById('save-disk').onchange = e => chrome.storage.local.set({ 50 | 'save-disk': e.target.checked 51 | }); 52 | document.getElementById('edit-online').onchange = e => chrome.storage.local.set({ 53 | 'edit-online': e.target.checked 54 | }); 55 | 56 | // Firefox 57 | if (typeof chrome.debugger === 'undefined') { 58 | for (const div of document.querySelectorAll('[data-cmd*="-debugger"]')) { 59 | div.classList.add('disabled'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /v3/data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options Page :: Easy Screenshot 5 | 6 | 7 | 8 | 9 |

10 | 11 | 12 |

13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 33 | 34 | 35 |
36 | 37 |

38 | 39 | 40 |

41 |

42 | - 43 | 44 |

45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /v3/data/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 31 | 32 |

33 | 34 | 35 | 36 |

37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /v2/firefox/data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toast = document.getElementById('toast'); 4 | 5 | function restore() { 6 | chrome.storage.local.get({ 7 | delay: 500, 8 | offset: 50, 9 | timestamp: true, 10 | saveAs: false 11 | }, prefs => { 12 | document.getElementById('delay').value = prefs.delay; 13 | document.getElementById('offset').value = prefs.offset; 14 | document.getElementById('timestamp').checked = prefs.timestamp; 15 | document.getElementById('saveAs').checked = prefs.saveAs; 16 | }); 17 | } 18 | 19 | function save() { 20 | const delay = Math.max(document.getElementById('delay').value, 100); 21 | const offset = Math.max(document.getElementById('offset').value, 10); 22 | const timestamp = document.getElementById('timestamp').checked; 23 | const saveAs = document.getElementById('saveAs').checked; 24 | 25 | chrome.storage.local.set({ 26 | delay, 27 | offset, 28 | timestamp, 29 | saveAs 30 | }, () => { 31 | toast.textContent = 'Options saved.'; 32 | setTimeout(() => toast.textContent = '', 750); 33 | restore(); 34 | }); 35 | } 36 | 37 | document.addEventListener('DOMContentLoaded', restore); 38 | document.getElementById('save').addEventListener('click', save); 39 | 40 | // reset 41 | document.getElementById('reset').addEventListener('click', e => { 42 | if (e.detail === 1) { 43 | toast.textContent = 'Double-click to reset!'; 44 | window.setTimeout(() => toast.textContent = '', 750); 45 | } 46 | else { 47 | localStorage.clear(); 48 | chrome.storage.local.clear(() => { 49 | chrome.runtime.reload(); 50 | window.close(); 51 | }); 52 | } 53 | }); 54 | // support 55 | document.getElementById('support').addEventListener('click', () => chrome.tabs.create({ 56 | url: chrome.runtime.getManifest().homepage_url + '?rd=donate' 57 | })); 58 | // preview 59 | document.getElementById('yt').addEventListener('click', () => chrome.tabs.create({ 60 | url: 'https://www.youtube.com/watch?v=BfUtaaGO4HA' 61 | })); 62 | -------------------------------------------------------------------------------- /v3/data/copy/index.js: -------------------------------------------------------------------------------- 1 | const args = new URLSearchParams(location.search); 2 | 3 | const copy = async e => { 4 | let timeout = 1000; 5 | 6 | try { 7 | if (args.has('content')) { 8 | await navigator.clipboard.writeText(args.get('content')); 9 | } 10 | else if (args.has('gid')) { 11 | const href = copy.href || await chrome.runtime.sendMessage({ 12 | method: 'read-gid', 13 | gid: args.get('gid') 14 | }); 15 | copy.href = href; 16 | const r = await fetch(href); 17 | const blob = await r.blob(); 18 | const mime = r.headers.get('content-type'); 19 | 20 | try { 21 | await navigator.clipboard.write([new ClipboardItem({ 22 | [mime]: blob 23 | })]); 24 | } 25 | catch (e) { 26 | // if format is not supported 27 | if (mime.includes('png') === false) { 28 | document.getElementById('toast').textContent = 29 | 'Clipboard API does not yet suppot "' + blob.type + '". Storing PNG instead...'; 30 | timeout = 5000; 31 | 32 | const img = await createImageBitmap(blob); 33 | const canvas = new OffscreenCanvas(img.width, img.height); 34 | const ctx = canvas.getContext('2d'); 35 | ctx.drawImage(img, 0, 0); 36 | const pngBlob = await canvas.convertToBlob({ 37 | type: 'image/png' 38 | }); 39 | 40 | await navigator.clipboard.write([ 41 | new ClipboardItem({ 42 | [pngBlob.type]: pngBlob 43 | }) 44 | ]); 45 | } 46 | else { 47 | throw e; 48 | } 49 | } 50 | } 51 | else { 52 | throw Error('NO_SUPPORTED_ARG'); 53 | } 54 | await new Promise(resolve => setTimeout(resolve, e && e.isTrusted ? 0 : timeout)); 55 | window.close(); 56 | } 57 | catch (err) { 58 | console.error(err); 59 | if (e?.isTrusted) { 60 | alert(err.message); 61 | } 62 | } 63 | }; 64 | 65 | copy(); 66 | document.getElementById('copy').addEventListener('click', copy); 67 | -------------------------------------------------------------------------------- /v3/data/popup/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 400px; 3 | display: flex; 4 | flex-direction: column; 5 | font-family: "Helvetica Neue",Helvetica,sans-serif; 6 | font-size: 13px; 7 | } 8 | 9 | div.menu { 10 | display: grid; 11 | grid-template-columns: 1fr min-content; 12 | grid-gap: 5px; 13 | } 14 | div[data-cmd] { 15 | display: grid; 16 | grid-column: 1 / 3; 17 | grid-template-columns: subgrid; 18 | align-items: center; 19 | white-space: nowrap; 20 | cursor: pointer; 21 | 22 | * { 23 | pointer-events: none; 24 | } 25 | &:hover { 26 | background-color: rgba(0, 0, 0, 0.1); 27 | } 28 | &:active { 29 | opacity: 0.5; 30 | } 31 | span.item { 32 | background-position: left 5px center; 33 | background-repeat: no-repeat; 34 | text-indent: 42px; 35 | line-height: 32px; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | } 39 | span.tag { 40 | background-color: #8BC34A; 41 | font-size: 80%; 42 | border-radius: 2px; 43 | padding: 5px; 44 | color: #273914; 45 | margin-right: 5px; 46 | } 47 | } 48 | div[data-cmd="capture-entire"] span.item, 49 | div[data-cmd="capture-entire-debugger"] span.item, 50 | div[data-cmd="capture-entire-debugger-steps"] span.item { 51 | background-image: url("../icons/entire.png"); 52 | } 53 | div[data-cmd="capture-visual"] span.item, 54 | div[data-cmd="capture-visual-debugger"] span.item { 55 | background-image: url('../icons/visual.png'); 56 | } 57 | div[data-cmd="capture-portion"] span.item { 58 | background-image: url("../icons/part.png"); 59 | } 60 | 61 | body[data-disabled=true] div[data-cmd], 62 | div[data-cmd].disabled { 63 | opacity: 0.3; 64 | pointer-events: none; 65 | } 66 | 67 | #mode { 68 | display: flex; 69 | flex-direction: column; 70 | border-top: solid 3px #ccc; 71 | padding-top: 10px; 72 | margin: 10px 0 0 0; 73 | } 74 | #mode > label { 75 | padding: 5px; 76 | display: flex; 77 | align-items: center; 78 | cursor: pointer; 79 | } 80 | #mode > label:hover { 81 | background-color: rgba(0, 0, 0, 0.1); 82 | } 83 | #mode input { 84 | margin-right: 5px; 85 | } 86 | -------------------------------------------------------------------------------- /v3/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Easy Screenshot - a Web Clipper", 3 | "description": "__MSG_description__", 4 | "default_locale": "en", 5 | "version": "0.5.4", 6 | "manifest_version": 3, 7 | "permissions": [ 8 | "contextMenus", 9 | "activeTab", 10 | "scripting", 11 | "downloads", 12 | "notifications", 13 | "storage", 14 | "debugger" 15 | ], 16 | "host_permissions": [ 17 | "*://webbrowsertools.com/jspaint/pwa/build/*" 18 | ], 19 | "offline_enabled": true, 20 | "background": { 21 | "service_worker": "worker.js", 22 | "scripts": ["ceds.js", "worker.js"] 23 | }, 24 | "homepage_url": "https://webextension.org/listing/screenshot.html", 25 | "icons": { 26 | "16": "data/icons/16.png", 27 | "20": "data/icons/20.png", 28 | "24": "data/icons/24.png", 29 | "32": "data/icons/32.png", 30 | "48": "data/icons/48.png", 31 | "64": "data/icons/64.png", 32 | "128": "data/icons/128.png", 33 | "256": "data/icons/256.png", 34 | "512": "data/icons/512.png" 35 | }, 36 | "options_ui": { 37 | "page": "data/options/index.html" 38 | }, 39 | "action": { 40 | "default_popup": "data/popup/index.html" 41 | }, 42 | "content_scripts": [{ 43 | "matches": ["*://webbrowsertools.com/jspaint/pwa/build/*"], 44 | "js": ["data/inject/connect.js"], 45 | "run_at": "document_start", 46 | "all_frames": false 47 | }], 48 | "commands": { 49 | "_execute_action": {}, 50 | "capture-visual": { 51 | "description": "Capture Visual Part" 52 | }, 53 | "capture-portion": { 54 | "description": "Capture a Portion" 55 | }, 56 | "capture-entire": { 57 | "description": "Capture Entire Screen (steps)" 58 | }, 59 | "capture-entire-debugger": { 60 | "description": "Capture Entire Screen (debugger)" 61 | }, 62 | "capture-entire-debugger-steps": { 63 | "description": "Capture Entire Screen (debugger + steps)" 64 | } 65 | }, 66 | "browser_specific_settings": { 67 | "gecko": { 68 | "id": "{79ae10fd-30c6-41b5-918c-5865ea0c0dfc}", 69 | "strict_min_version": "128.0" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /v1/src/lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = app || require('./firefox/firefox'); 4 | var config = config || require('./config'); 5 | 6 | // welcome 7 | (function () { 8 | var version = config.welcome.version; 9 | if (app.version() !== version) { 10 | app.timer.setTimeout(function () { 11 | app.tab.open( 12 | 'http://mybrowseraddon.com/screenshot.html?v=' + app.version() + (version ? '&p=' + version + '&type=upgrade' : '&type=install') 13 | ); 14 | config.welcome.version = app.version(); 15 | }, config.welcome.timeout); 16 | } 17 | })(); 18 | 19 | function name (title) { 20 | if (config.options.timestamp) { 21 | title = title += ' ' + ((new Date()).toLocaleString()).replace(/\:/g, '-'); 22 | } 23 | return title 24 | .replace(/\+/g, ' ') 25 | .replace(/[:\?\¿]/g, '') 26 | .replace(/[\\\/]/g, '-') 27 | .replace(/[\*]/g, '^') 28 | .replace(/[\"]/g, '\'') 29 | .replace(/[<]/g, '[') 30 | .replace(/[\>]/g, ']') 31 | .replace(/[|]/g, '-'); 32 | } 33 | function visual () { 34 | app.inject.send('visual'); 35 | } 36 | app.inject.receive('visual', function (obj) { 37 | app.screenshot().then(function (dataURL) { 38 | app.download(dataURL, name(obj.title) + '.png'); 39 | }); 40 | }); 41 | 42 | function entire () { 43 | app.inject.send('entire'); 44 | } 45 | app.inject.receive('entire', function (obj) { 46 | app.screenshot(-obj.scrollX, -obj.scrollY, obj.maxWidth, obj.maxHeight).then(function (dataURL) { 47 | app.download(dataURL, name(obj.title) + '.png'); 48 | }); 49 | }); 50 | 51 | function part () { 52 | app.inject.send('capture'); 53 | } 54 | app.inject.receive('capture', function (obj) { 55 | app.screenshot(obj.left, obj.top, obj.width, obj.height, obj.devicePixelRatio).then(function (dataURL) { 56 | app.download(dataURL, name(obj.title) + '.png'); 57 | }); 58 | }); 59 | 60 | (function () { 61 | var items = [ 62 | ['Capture Visual Part', 'icons/visual.png', visual], 63 | ['Capture a Portion', 'icons/part.png', part] 64 | ]; 65 | if (app.manifest.fullScreen) { 66 | items.unshift(['Capture Entire Screen', 'icons/entire.png', entire]); 67 | } 68 | app.contextMenu.create('Easy Screenshot', 'icons/32.png', items); 69 | })(); 70 | -------------------------------------------------------------------------------- /v3/data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toast = document.getElementById('toast'); 4 | 5 | function restore() { 6 | chrome.storage.local.get({ 7 | 'delay': 600, 8 | 'offset': 50, 9 | 'mask': '[date] - [time] - [title]', 10 | 'saveAs': false, 11 | 'format': 'png', 12 | 'format-canvas': 'png', 13 | 'quality': 0.95 14 | }, prefs => { 15 | document.getElementById('delay').value = prefs.delay; 16 | document.getElementById('offset').value = prefs.offset; 17 | document.getElementById('mask').value = prefs.mask; 18 | document.getElementById('saveAs').checked = prefs.saveAs; 19 | document.getElementById('format').value = prefs.format; 20 | document.getElementById('format-canvas').value = prefs['format-canvas']; 21 | document.getElementById('quality').value = prefs.quality; 22 | }); 23 | } 24 | 25 | function save() { 26 | const delay = Math.max(document.getElementById('delay').value, 100); 27 | const offset = Math.max(document.getElementById('offset').value, 10); 28 | const mask = document.getElementById('mask').value; 29 | const saveAs = document.getElementById('saveAs').checked; 30 | const format = document.getElementById('format').value; 31 | const quality = Math.min(Math.max(document.getElementById('quality').valueAsNumber, 0.3), 1); 32 | 33 | chrome.storage.local.set({ 34 | delay, 35 | offset, 36 | mask, 37 | saveAs, 38 | format, 39 | quality, 40 | 'format-canvas': document.getElementById('format-canvas').value 41 | }, () => { 42 | toast.textContent = 'Options saved.'; 43 | setTimeout(() => toast.textContent = '', 750); 44 | restore(); 45 | }); 46 | } 47 | 48 | document.addEventListener('DOMContentLoaded', restore); 49 | document.getElementById('save').addEventListener('click', save); 50 | 51 | // reset 52 | document.getElementById('reset').addEventListener('click', e => { 53 | if (e.detail === 1) { 54 | toast.textContent = 'Double-click to reset!'; 55 | window.setTimeout(() => toast.textContent = '', 750); 56 | } 57 | else { 58 | localStorage.clear(); 59 | chrome.storage.local.clear(() => { 60 | chrome.runtime.reload(); 61 | window.close(); 62 | }); 63 | } 64 | }); 65 | // support 66 | document.getElementById('support').addEventListener('click', () => chrome.tabs.create({ 67 | url: chrome.runtime.getManifest().homepage_url + '?rd=donate' 68 | })); 69 | // preview 70 | document.getElementById('yt').addEventListener('click', () => chrome.tabs.create({ 71 | url: 'https://www.youtube.com/watch?v=BfUtaaGO4HA' 72 | })); 73 | 74 | // links 75 | for (const a of [...document.querySelectorAll('[data-href]')]) { 76 | if (a.hasAttribute('href') === false) { 77 | a.href = chrome.runtime.getManifest().homepage_url + '#' + a.dataset.href; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /v3/ceds.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const ceds = async tab => { 3 | const target = { 4 | tabId: tab.id 5 | }; 6 | 7 | const tabId = tab.id; 8 | chrome.action.setBadgeText({tabId, text: 'R'}); 9 | 10 | const r = await chrome.scripting.executeScript({ 11 | target, 12 | func: () => { 13 | const width = Math.max(document.body.scrollWidth, document.documentElement.scrollWidth); 14 | const height = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); 15 | window.scrollTo({ 16 | left: 0, 17 | top: 0, 18 | behavior: 'instant' 19 | }); 20 | 21 | return { 22 | ratio: window.devicePixelRatio, 23 | width, 24 | height 25 | }; 26 | }, 27 | injectImmediately: true 28 | }); 29 | 30 | await chrome.debugger.attach(target, '1.3'); 31 | 32 | const info = r[0].result; 33 | 34 | const prefs = await chrome.storage.local.get({ 35 | 'delay': 600, 36 | 'step': 2000, 37 | 'quality': 0.95, 38 | 'format': 'png', 39 | 'format-canvas': 'png' 40 | }); 41 | 42 | const mx = Math.ceil(info.width / prefs.step) * Math.ceil(info.height / prefs.step); 43 | let p = 0; 44 | 45 | const section = async rect => { 46 | // await chrome.scripting.executeScript({ 47 | // target, 48 | // func: rect => window.scrollTo({ 49 | // left: rect.x, 50 | // top: rect.y, 51 | // behavior: 'instant' 52 | // }), 53 | // args: [rect] 54 | // }); 55 | await new Promise(resolve => setTimeout(resolve, prefs.delay)); 56 | const result = await chrome.debugger.sendCommand(target, 'Page.captureScreenshot', { 57 | format: prefs.format, 58 | quality: parseInt(prefs.quality * 100), 59 | captureBeyondViewport: true, 60 | clip: { 61 | ...rect, 62 | scale: 1 63 | } 64 | }); 65 | 66 | if (!result) { 67 | throw Error('Failed to capture screenshot'); 68 | } 69 | return 'data:image/' + prefs.format + ';base64,' + result.data; 70 | }; 71 | 72 | const canvas = new OffscreenCanvas(info.width * info.ratio, info.height * info.ratio); 73 | const ctx = canvas.getContext('2d'); 74 | 75 | try { 76 | for (let x = 0; x < info.width; x += prefs.step) { 77 | for (let y = 0; y < info.height; y += prefs.step) { 78 | p += 1; 79 | chrome.action.setBadgeText({tabId, text: (p / mx * 100).toFixed(0) + '%'}); 80 | 81 | const width = Math.min(x + prefs.step, info.width) - x; 82 | const height = Math.min(y + prefs.step, info.height) - y; 83 | 84 | const du = await section({ 85 | x, 86 | y, 87 | width, 88 | height 89 | }); 90 | const b = await fetch(du).then(r => r.blob()); 91 | const img = await createImageBitmap(b); 92 | ctx.drawImage( 93 | img, 94 | 0, 0, img.width, img.height, 95 | x * info.ratio, y * info.ratio, img.width, img.height 96 | ); 97 | } 98 | } 99 | chrome.action.setBadgeText({tabId, text: '...'}); 100 | chrome.debugger.detach(target); 101 | 102 | return canvas.convertToBlob({ 103 | type: 'image/' + prefs['format-canvas'], 104 | quality: prefs.quality 105 | }).then(du => { 106 | chrome.action.setBadgeText({tabId, text: ''}); 107 | return du; 108 | }); 109 | } 110 | catch (e) { 111 | chrome.action.setBadgeText({tabId, text: ''}); 112 | chrome.debugger.detach(target); 113 | throw e; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /v1/src/lib/chrome/chrome.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = {}; 4 | var config = {}; // jshint ignore:line 5 | 6 | if (!Promise.defer) { 7 | Promise.defer = function () { 8 | let deferred = {}; 9 | let promise = new Promise(function (resolve, reject) { 10 | deferred.resolve = resolve; 11 | deferred.reject = reject; 12 | }); 13 | deferred.promise = promise; 14 | return deferred; 15 | }; 16 | } 17 | app.Promise = Promise; 18 | 19 | app.manifest = { 20 | fullScreen: false 21 | }; 22 | 23 | app.storage = { 24 | read: function (id) { 25 | return localStorage[id] || null; 26 | }, 27 | write: function (id, data) { 28 | localStorage[id] = data + ''; 29 | } 30 | }; 31 | 32 | app.inject = { 33 | send: function (id, data, global) { 34 | let options = global ? {} : {active: true, currentWindow: true}; 35 | chrome.tabs.query(options, function (tabs) { 36 | tabs.forEach(function (tab) { 37 | chrome.tabs.sendMessage(tab.id, {method: id, data: data}, function () {}); 38 | }); 39 | }); 40 | }, 41 | receive: function (id, callback) { 42 | chrome.extension.onRequest.addListener(function (request, sender) { 43 | if (request.method === id && sender.tab) { 44 | callback(request.data); 45 | } 46 | }); 47 | } 48 | }; 49 | 50 | app.tab = { 51 | open: function (url, inBackground, inCurrent) { 52 | if (inCurrent) { 53 | chrome.tabs.update(null, {url: url}); 54 | } 55 | else { 56 | chrome.tabs.create({ 57 | url: url, 58 | active: typeof inBackground === 'undefined' ? true : !inBackground 59 | }); 60 | } 61 | } 62 | }; 63 | 64 | app.contextMenu = { 65 | create: function (label, img, arr) { 66 | arr.forEach(function (a) { 67 | chrome.contextMenus.create({ 68 | 'title': a[0], 69 | 'contexts': ['page', 'selection', 'link'], 70 | 'onclick': function () { 71 | a[2](); 72 | } 73 | }); 74 | }); 75 | } 76 | }; 77 | 78 | app.version = function () { 79 | return chrome[chrome.runtime && chrome.runtime.getManifest ? 'runtime' : 'extension'].getManifest().version; 80 | }; 81 | 82 | app.timer = window; 83 | 84 | app.screenshot = function (left, top, width, height, devicePixelRatio) { 85 | let d = Promise.defer(); 86 | left = left * devicePixelRatio; 87 | top = top * devicePixelRatio; 88 | width = width * devicePixelRatio; 89 | height = height * devicePixelRatio; 90 | 91 | chrome.tabs.query({ 92 | active: true, 93 | currentWindow: true 94 | }, function (tab) { 95 | chrome.tabs.captureVisibleTab(tab.windowId, {format: 'png'}, function (dataUrl) { 96 | let canvas = document.createElement('canvas'); 97 | let ctx = canvas.getContext('2d'); 98 | let img = new Image(); 99 | img.onload = function () { 100 | canvas.width = width || img.width; 101 | canvas.height = height || img.height; 102 | if (width && height) { 103 | ctx.drawImage(img, left, top, width, height, 0, 0, width, height); 104 | } 105 | else { 106 | ctx.drawImage(img, 0, 0); 107 | } 108 | d.resolve(canvas.toDataURL()); 109 | }; 110 | img.src = dataUrl; 111 | }); 112 | }); 113 | return d.promise; 114 | }; 115 | 116 | app.download = function (url, filename) { 117 | //url = url.replace(/data\:[^\;]*/, 'data:application/octet-stream'); 118 | let link = document.createElement('a'); 119 | link.download = filename; 120 | link.href = url; 121 | link.click(); 122 | }; 123 | -------------------------------------------------------------------------------- /v2/firefox/data/inject/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var monitor = window.monitor; 4 | var capture = window.capture; 5 | var guide = window.guide; 6 | 7 | try { 8 | guide.remove(); 9 | capture.remove(); 10 | monitor.remove(); 11 | } 12 | catch (e) {} 13 | 14 | capture = (function() { 15 | let box; 16 | let _left; 17 | let _top; 18 | let left; 19 | let top; 20 | let width; 21 | let height; 22 | 23 | function update(e) { 24 | left = (e.clientX > _left ? _left : e.clientX - 1); 25 | top = (e.clientY > _top ? _top : e.clientY - 1); 26 | width = Math.abs(e.clientX - _left); 27 | height = Math.abs(e.clientY - _top); 28 | box.style.left = left + 'px'; 29 | box.style.top = top + 'px'; 30 | box.style.width = width + 'px'; 31 | box.style.height = height + 'px'; 32 | } 33 | function remove() { 34 | chrome.runtime.sendMessage({ 35 | method: 'captured', 36 | left: left + 1, 37 | top: top + 1, 38 | width: width - 2, 39 | height: height - 2, 40 | devicePixelRatio: window.devicePixelRatio, 41 | title: document.title, 42 | service: window.service // used by Reverse Image Search extension 43 | }); 44 | guide.remove(); 45 | capture.remove(); 46 | monitor.remove(); 47 | } 48 | function mousedown(e) { 49 | // prevent content selection on Firefox 50 | e.stopPropagation(); 51 | e.preventDefault(); 52 | box = document.createElement('div'); 53 | box.setAttribute('class', 'itrisearch-box'); 54 | 55 | _left = e.clientX; 56 | _top = e.clientY; 57 | 58 | document.addEventListener('mousemove', update, false); 59 | document.addEventListener('mouseup', remove, false); 60 | document.body.appendChild(box); 61 | } 62 | 63 | return { 64 | install: function() { 65 | document.addEventListener('mousedown', mousedown, false); 66 | }, 67 | remove: function() { 68 | document.removeEventListener('mousedown', mousedown, false); 69 | document.removeEventListener('mousemove', update, false); 70 | document.removeEventListener('mouseup', remove, false); 71 | if (box && box.parentNode) { 72 | box.parentNode.removeChild(box); 73 | } 74 | } 75 | }; 76 | })(); 77 | 78 | guide = (function() { 79 | let guide1; 80 | let guide2; 81 | let guide3; 82 | function position(left, top) { 83 | guide1.style.width = left + 'px'; 84 | guide2.style.height = top + 'px'; 85 | } 86 | function update(e) { 87 | position(e.clientX, e.clientY); 88 | } 89 | return { 90 | install: function() { 91 | guide1 = document.createElement('div'); 92 | guide2 = document.createElement('div'); 93 | guide3 = document.createElement('div'); 94 | guide1.setAttribute('class', 'itrisearch-guide-1'); 95 | guide2.setAttribute('class', 'itrisearch-guide-2'); 96 | guide3.setAttribute('class', 'itrisearch-guide-3'); 97 | document.body.appendChild(guide3); 98 | document.body.appendChild(guide1); 99 | document.body.appendChild(guide2); 100 | document.addEventListener('mousemove', update, false); 101 | }, 102 | remove: function() { 103 | document.removeEventListener('mousemove', update, false); 104 | if (guide1 && guide1.parentNode) { 105 | guide1.parentNode.removeChild(guide1); 106 | } 107 | if (guide2 && guide2.parentNode) { 108 | guide2.parentNode.removeChild(guide2); 109 | } 110 | if (guide3 && guide3.parentNode) { 111 | guide3.parentNode.removeChild(guide3); 112 | } 113 | capture.remove(); 114 | } 115 | }; 116 | })(); 117 | 118 | monitor = (function() { 119 | function keydown(e) { 120 | if (e.keyCode === 27) { 121 | guide.remove(); 122 | capture.remove(); 123 | monitor.remove(); 124 | } 125 | } 126 | return { 127 | install: function() { 128 | window.addEventListener('keydown', keydown, false); 129 | }, 130 | remove: function() { 131 | window.removeEventListener('keydown', keydown, false); 132 | } 133 | }; 134 | })(); 135 | 136 | guide.install(); 137 | capture.install(); 138 | monitor.install(); 139 | -------------------------------------------------------------------------------- /v1/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var change = require('gulp-change'); 5 | var babel = require('gulp-babel'); 6 | var gulpif = require('gulp-if'); 7 | var gulpFilter = require('gulp-filter'); 8 | var shell = require('gulp-shell'); 9 | var wait = require('gulp-wait'); 10 | var clean = require('gulp-clean'); 11 | var zip = require('gulp-zip'); 12 | var rename = require('gulp-rename'); 13 | var util = require('gulp-util'); 14 | var runSequence = require('run-sequence'); 15 | 16 | /* clean */ 17 | gulp.task('clean', function () { 18 | return gulp.src([ 19 | 'builds/unpacked/chrome/*', 20 | 'builds/unpacked/firefox/*', 21 | ], {read: false}) 22 | .pipe(clean()); 23 | }); 24 | /* chrome build */ 25 | gulp.task('chrome-build', function () { 26 | gulp.src([ 27 | 'src/**/*' 28 | ]) 29 | .pipe(gulpFilter(function (f) { 30 | if (f.relative.indexOf('.DS_Store') !== -1 || f.relative.indexOf('Thumbs.db') !== -1) { 31 | return false; 32 | } 33 | if (f.relative.indexOf('firefox') !== -1 && f.relative.indexOf('firefox.png') === -1) { 34 | return false; 35 | } 36 | if (f.path.indexOf('/locale') !== -1) { 37 | return false; 38 | } 39 | if (f.relative.indexOf('safari') !== -1) { 40 | return false; 41 | } 42 | if (f.relative.split('/').length === 1) { 43 | return f.relative === 'manifest.json' ? true : false; 44 | } 45 | return true; 46 | })) 47 | .pipe(gulpif(function (f) { 48 | return f.path.indexOf('.js') !== -1 && f.path.indexOf('.json') === -1; 49 | }, change(function (content) { 50 | return content.replace(/\/\*\* wrapper[\s\S]*\\*\*\*\//m, ''); 51 | }))) 52 | .pipe(gulpif(function (f) { 53 | return f.path.indexOf('.html') !== -1; 54 | }, change(function (content) { 55 | return content.replace(/.*shadow_index\.js.*/, ' \n '); 56 | }))) 57 | .pipe(gulp.dest('builds/unpacked/chrome')) 58 | .pipe(zip('chrome.zip')) 59 | .pipe(gulp.dest('builds/packed')); 60 | }); 61 | gulp.task('chrome-install', function () { 62 | gulp.src('') 63 | .pipe(wait(1000)) 64 | .pipe(shell([ 65 | '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --load-and-launch-app=`pwd` &' 66 | ], { 67 | cwd: './builds/unpacked/chrome' 68 | })); 69 | }); 70 | 71 | /* firefox build */ 72 | gulp.task('firefox-build', function () { 73 | gulp.src([ 74 | 'src/**/*' 75 | ]) 76 | .pipe(gulpFilter(function (f) { 77 | if (f.relative.indexOf('.DS_Store') !== -1 || f.relative.indexOf('Thumbs.db') !== -1) { 78 | return false; 79 | } 80 | if (f.path.indexOf('_locales') !== -1) { 81 | return false; 82 | } 83 | if (f.relative.indexOf('chrome') !== -1 && 84 | f.relative !== 'chrome.manifest' && 85 | f.relative.indexOf('chrome.png') === -1 && 86 | f.relative.indexOf('firefox/chrome') === -1 87 | ) { 88 | return false; 89 | } 90 | if (f.relative.indexOf('shadow_index.js') !== -1) { 91 | return false; 92 | } 93 | if (f.relative.indexOf('safari') !== -1) { 94 | return false; 95 | } 96 | if (f.relative.split('/').length === 1) { 97 | return ['package.json', 'chrome.manifest'].indexOf(f.relative) !== -1; 98 | } 99 | return true; 100 | })) 101 | .pipe(gulpif(function (f) { 102 | return f.path.indexOf('.html') !== -1; 103 | }, change(function (content) { 104 | return content.replace(/\n.*shadow_index\.js.*/, ''); 105 | }))) 106 | .pipe(gulp.dest('builds/unpacked/firefox')); 107 | }); 108 | /* firefox pack */ 109 | gulp.task('firefox-pack', function () { 110 | gulp.src('') 111 | .pipe(wait(1000)) 112 | .pipe(shell([ 113 | 'jpm xpi', 114 | 'mv *.xpi ../../packed/firefox.xpi', 115 | 'jpm post --post-url http://localhost:8888/' 116 | ], { 117 | cwd: './builds/unpacked/firefox' 118 | })) 119 | .pipe(shell([ 120 | 'zip firefox.xpi icon.png icon64.png', 121 | ], { 122 | cwd: './builds/packed' 123 | })); 124 | }); 125 | /* */ 126 | gulp.task('chrome', function (callback) { 127 | runSequence('clean', 'chrome-build', 'chrome-install', callback); 128 | }); 129 | gulp.task('firefox', function (callback) { 130 | runSequence('clean', 'firefox-build', 'firefox-pack', callback); 131 | }); 132 | -------------------------------------------------------------------------------- /v1/src/lib/firefox/firefox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; var self = require('sdk/self'), data = self.data, sp = require('sdk/simple-prefs'), prefs = sp.prefs, pageMod = require('sdk/page-mod'), tabs = require('sdk/tabs'), timers = require('sdk/timers'), loader = require('@loader/options'), contextMenu = require('sdk/context-menu'), array = require('sdk/util/array'), {Ci} = require('chrome'), mm = require('./mm'), notifications = require('sdk/notifications'), {defer, resolve} = require('sdk/core/promise'); var {Downloads} = require('resource://gre/modules/Downloads.jsm'); var {FileUtils} = require('resource://gre/modules/FileUtils.jsm'); exports.manifest = { fullScreen: true }; exports.inject = (function () { let workers = [], callbacks = []; pageMod.PageMod({ include: ['http://*', 'https://*', 'file:///*'], contentScriptFile: data.url('./content_script/inject.js'), contentScriptWhen: 'start', contentStyleFile : data.url('./content_script/inject.css'), contentScriptOptions: { base: loader.prefixURI + loader.name + '/' }, attachTo: ['top', 'existing'], onAttach: function (worker) { array.add(workers, worker); worker.on('pageshow', function () {array.add(workers, this);}); worker.on('pagehide', function () {array.remove(workers, this);}); worker.on('detach', function () {array.remove(workers, this);}); callbacks.forEach(function (arr) { worker.port.on(arr[0], arr[1]); }); } }); return { send: function (id, data, global) { workers.forEach(function (worker) { if (!global && worker.tab !== tabs.activeTab) { return; } if (!worker) { return; } worker.port.emit(id, data); }); }, receive: function (id, callback) { callbacks.push([id, callback]); workers.forEach(function (worker) { worker.port.on(id, callback); }); } }; })(); exports.storage = { read: function (id) { return (prefs[id] || prefs[id] + '' === 'false') ? (prefs[id] + '') : null; }, write: function (id, data) { data = data + ''; if (data === 'true' || data === 'false') { prefs[id] = data === 'true' ? true : false; } else if (parseInt(data) + '' === data) { prefs[id] = parseInt(data); } else { prefs[id] = data + ''; } } }; exports.tab = { open: function (url, inBackground, inCurrent) { if (inCurrent) { tabs.activeTab.url = url; } else { tabs.open({ url: url, inBackground: typeof inBackground === 'undefined' ? false : inBackground }); } } }; exports.contextMenu = { create: function (label, img, arr) { function addOne ([title, img, callback]) { return contextMenu.Item({ label: title, image: data.url(img), contentScript: 'self.on("click", function () {self.postMessage();});', onMessage: function () { callback(); } }); } contextMenu.Menu({ label: label, image: data.url(img), items: arr.map(addOne), context: contextMenu.PredicateContext(function (context) { return context.documentURL.indexOf('http') !== -1 || context.documentURL.indexOf('file') !== -1; }) }); } }; exports.version = function () { return self.version; }; exports.timer = timers; sp.on('tineye', function () { exports.tab.open('https://addons.mozilla.org/en-US/firefox/addon/capture-reverse-image-search/'); }); function notify (text, onClick) { if (!sp.prefs.notification) { return; } notifications.notify({ title: 'Easy Screenshot', text, iconURL: self.data.url('./icons/64.png'), onClick }); } (function (d) { mm.connect(function (obj) { if (d) { d.resolve(obj); } }); exports.screenshot = function (left, top, width, height) { d = defer(); mm.emit('screenshot', {left, top, width, height}); return d.promise; }; exports.download = function (uri, name) { let dir = sp.prefs.directory; (dir ? resolve(dir) : Downloads.getPreferredDownloadsDirectory()) .then(path => new FileUtils.File(path)) .then(file => { file.append(name); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); return Downloads.fetch(uri, file).then( () => notify('Saving as "' + file.path + '"', () => file.reveal()) ); }) .catch(e => notify((e.message || e) + '')); }; })(); -------------------------------------------------------------------------------- /v3/data/inject/inject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | 'use strict'; 3 | 4 | var monitor = window.monitor; 5 | var capture = window.capture; 6 | var guide = window.guide; 7 | 8 | try { 9 | guide.remove(); 10 | capture.remove(); 11 | monitor.remove(); 12 | } 13 | catch (e) {} 14 | 15 | capture = (function() { 16 | let box; 17 | let _left; 18 | let _top; 19 | let left; 20 | let top; 21 | let width; 22 | let height; 23 | 24 | function update(e) { 25 | left = (e.clientX > _left ? _left : e.clientX - 1); 26 | top = (e.clientY > _top ? _top : e.clientY - 1); 27 | width = Math.abs(e.clientX - _left); 28 | height = Math.abs(e.clientY - _top); 29 | box.style.left = left + 'px'; 30 | box.style.top = top + 'px'; 31 | box.style.width = width + 'px'; 32 | box.style.height = height + 'px'; 33 | } 34 | function remove(e) { 35 | const props = { 36 | devicePixelRatio: window.devicePixelRatio, 37 | title: document.title, 38 | service: window.service // used by Reverse Image Search extension 39 | }; 40 | 41 | chrome.runtime.sendMessage(!left && !width ? { 42 | method: 'captured', 43 | left: e.clientX, 44 | top: e.clientY, 45 | width: 32, 46 | height: 32, 47 | ...props 48 | } : { 49 | method: 'captured', 50 | left: left + 1, 51 | top: top + 1, 52 | width: width - 2, 53 | height: height - 2, 54 | ...props 55 | }); 56 | 57 | guide.remove(); 58 | capture.remove(); 59 | monitor.remove(); 60 | } 61 | function mousedown(e) { 62 | // prevent content selection on Firefox 63 | e.stopPropagation(); 64 | e.preventDefault(); 65 | box = document.createElement('div'); 66 | box.setAttribute('class', 'itrisearch-box'); 67 | 68 | _left = e.clientX; 69 | _top = e.clientY; 70 | 71 | document.addEventListener('mousemove', update, false); 72 | document.addEventListener('mouseup', remove, false); 73 | document.documentElement.appendChild(box); 74 | } 75 | 76 | return { 77 | install: function() { 78 | document.addEventListener('mousedown', mousedown, false); 79 | }, 80 | remove: function() { 81 | document.removeEventListener('mousedown', mousedown, false); 82 | document.removeEventListener('mousemove', update, false); 83 | document.removeEventListener('mouseup', remove, false); 84 | 85 | for (const e of document.querySelectorAll('.itrisearch-box')) { 86 | e.remove(); 87 | } 88 | } 89 | }; 90 | })(); 91 | 92 | guide = (function() { 93 | let guide1; 94 | let guide2; 95 | let guide3; 96 | function position(left, top) { 97 | guide1.style.width = left + 'px'; 98 | guide2.style.height = top + 'px'; 99 | } 100 | function update(e) { 101 | position(e.clientX, e.clientY); 102 | } 103 | return { 104 | install: function() { 105 | guide1 = document.createElement('div'); 106 | guide2 = document.createElement('div'); 107 | guide3 = document.createElement('div'); 108 | 109 | guide1.setAttribute('class', 'itrisearch-guide-1'); 110 | guide2.setAttribute('class', 'itrisearch-guide-2'); 111 | guide3.setAttribute('class', 'itrisearch-guide-3'); 112 | document.documentElement.append(guide3, guide2, guide1); 113 | document.addEventListener('mousemove', update, false); 114 | }, 115 | remove: function() { 116 | document.removeEventListener('mousemove', update, false); 117 | for (const e of document.querySelectorAll('.itrisearch-guide-1, .itrisearch-guide-2, .itrisearch-guide-3')) { 118 | e.remove(); 119 | } 120 | 121 | capture.remove(); 122 | } 123 | }; 124 | })(); 125 | 126 | monitor = (function() { 127 | function keydown(e) { 128 | if (e.keyCode === 27) { 129 | guide.remove(); 130 | capture.remove(); 131 | monitor.remove(); 132 | } 133 | } 134 | function contextmenu() { 135 | guide.remove(); 136 | capture.remove(); 137 | monitor.remove(); 138 | } 139 | return { 140 | install: function() { 141 | window.addEventListener('keydown', keydown, false); 142 | window.addEventListener('contextmenu', contextmenu, false); 143 | }, 144 | remove: function() { 145 | window.removeEventListener('keydown', keydown, false); 146 | window.removeEventListener('contextmenu', contextmenu, false); 147 | } 148 | }; 149 | })(); 150 | 151 | guide.install(); 152 | capture.install(); 153 | monitor.install(); 154 | -------------------------------------------------------------------------------- /v1/src/data/content_script/inject.js: -------------------------------------------------------------------------------- 1 | /* global self, safari*/ 2 | 'use strict'; 3 | 4 | var background = {}, manifest = {}; 5 | /**** wrapper (start) ****/ 6 | if (typeof self !== 'undefined' && self.port) { //Firefox 7 | background.send = function (id, data) { 8 | self.port.emit(id, data); 9 | }; 10 | background.receive = function (id, callback) { 11 | self.port.on(id, callback); 12 | }; 13 | manifest.url = self.options.base; 14 | self.port.on('detach', function () { 15 | try { 16 | capture.remove(); 17 | guide.remove(); 18 | } 19 | catch (e) {} 20 | }); 21 | } 22 | else if (typeof safari !== 'undefined') { // Safari 23 | background.send = function (id, obj) { 24 | safari.self.tab.dispatchMessage('message', { 25 | id: id, 26 | data: obj 27 | }); 28 | }; 29 | background.receive = (function () { 30 | var callbacks = {}; 31 | safari.self.addEventListener('message', function (e) { 32 | if (callbacks[e.name]) { 33 | callbacks[e.name](e.message); 34 | } 35 | }, false); 36 | 37 | return function (id, callback) { 38 | callbacks[id] = callback; 39 | }; 40 | })(); 41 | manifest.url = safari.extension.baseURI; 42 | 43 | document.addEventListener('contextmenu', function () { 44 | var selectedText = window.getSelection().toString(); 45 | try { 46 | safari.self.tab.setContextMenuEventUserInfo(event, { 47 | selectedText: selectedText 48 | }); 49 | } catch (e) {} 50 | }, false); 51 | } 52 | else { // Chrome 53 | background.send = function (id, data) { 54 | chrome.extension.sendRequest({method: id, data: data}); 55 | }; 56 | background.receive = function (id, callback) { 57 | chrome.runtime.onMessage.addListener(function (request) { 58 | if (request.method === id) { 59 | callback(request.data); 60 | } 61 | }); 62 | }; 63 | manifest.url = chrome.extension.getURL('./'); 64 | } 65 | /**** wrapper (end) ****/ 66 | 67 | var capture = (function () { 68 | var box, _left, _top, left, top, width, height; 69 | 70 | function update (e) { 71 | left = (e.clientX > _left ? _left : e.clientX - 1); 72 | top = (e.clientY > _top ? _top : e.clientY - 1); 73 | width = Math.abs(e.clientX - _left); 74 | height = Math.abs(e.clientY - _top); 75 | box.style.left = left + 'px'; 76 | box.style.top = top + 'px'; 77 | box.style.width = width + 'px'; 78 | box.style.height = height + 'px'; 79 | } 80 | function remove () { 81 | background.send('capture', { 82 | left: left + 1, 83 | top: top + 1, 84 | width: width - 2, 85 | height: height - 2, 86 | devicePixelRatio: window.devicePixelRatio, 87 | title: document.title 88 | }); 89 | guide.remove(); 90 | capture.remove(); 91 | monitor.remove(); 92 | } 93 | function mousedown(e) { 94 | // prevent content selection on Firefox 95 | e.stopPropagation(); 96 | e.preventDefault(); 97 | box = document.createElement('div'); 98 | box.setAttribute('class', 'itrisearch-box'); 99 | 100 | _left = e.clientX; 101 | _top = e.clientY; 102 | 103 | document.addEventListener('mousemove', update, false); 104 | document.addEventListener('mouseup', remove, false); 105 | document.body.appendChild(box); 106 | } 107 | 108 | return { 109 | install: function () { 110 | document.addEventListener('mousedown', mousedown, false); 111 | }, 112 | remove: function () { 113 | document.removeEventListener('mousedown', mousedown, false); 114 | document.removeEventListener('mousemove', update, false); 115 | document.removeEventListener('mouseup', remove, false); 116 | if (box && box.parentNode) { 117 | box.parentNode.removeChild(box); 118 | } 119 | } 120 | }; 121 | })(); 122 | 123 | var guide = (function () { 124 | var guide1, guide2, guide3; 125 | function position (left, top) { 126 | guide1.style.width = left + 'px'; 127 | guide2.style.height = top + 'px'; 128 | } 129 | function update (e) { 130 | position(e.clientX, e.clientY); 131 | } 132 | return { 133 | install: function () { 134 | guide1 = document.createElement('div'); 135 | guide2 = document.createElement('div'); 136 | guide3 = document.createElement('div'); 137 | guide1.setAttribute('class', 'itrisearch-guide-1'); 138 | guide2.setAttribute('class', 'itrisearch-guide-2'); 139 | guide3.setAttribute('class', 'itrisearch-guide-3'); 140 | document.body.appendChild(guide3); 141 | document.body.appendChild(guide1); 142 | document.body.appendChild(guide2); 143 | document.addEventListener('mousemove', update, false); 144 | }, 145 | remove: function () { 146 | document.removeEventListener('mousemove', update, false); 147 | if (guide1 && guide1.parentNode) { 148 | guide1.parentNode.removeChild(guide1); 149 | } 150 | if (guide2 && guide2.parentNode) { 151 | guide2.parentNode.removeChild(guide2); 152 | } 153 | if (guide3 && guide3.parentNode) { 154 | guide3.parentNode.removeChild(guide3); 155 | } 156 | capture.remove(); 157 | } 158 | }; 159 | })(); 160 | 161 | var monitor = (function () { 162 | function keydown (e) { 163 | if (e.keyCode === 27) { 164 | guide.remove(); 165 | capture.remove(); 166 | monitor.remove(); 167 | } 168 | } 169 | return { 170 | install: function () { 171 | window.addEventListener('keydown', keydown, false); 172 | }, 173 | remove: function () { 174 | window.removeEventListener('keydown', keydown, false); 175 | } 176 | }; 177 | })(); 178 | 179 | background.receive('capture', function () { 180 | guide.install(); 181 | capture.install(); 182 | monitor.install(); 183 | }); 184 | 185 | background.receive('entire', function () { 186 | background.send('entire', { 187 | title: document.title, 188 | scrollX: window.scrollX, 189 | scrollY: window.scrollY, 190 | maxWidth: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.body.clientWidth, document.documentElement.clientWidth), 191 | maxHeight: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight), 192 | }); 193 | }); 194 | 195 | background.receive('visual', function () { 196 | background.send('visual', { 197 | title: document.title, 198 | scrollX: window.scrollX, 199 | scrollY: window.scrollY, 200 | maxWidth: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.body.clientWidth, document.documentElement.clientWidth), 201 | maxHeight: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight), 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /v2/firefox/common.js: -------------------------------------------------------------------------------- 1 | /* global browser */ 2 | 'use strict'; 3 | 4 | const notify = e => chrome.notifications.create({ 5 | type: 'basic', 6 | iconUrl: '/data/icons/48.png', 7 | title: chrome.runtime.getManifest().name, 8 | message: e.message || e 9 | }); 10 | 11 | function capture(request) { 12 | return new Promise(function(resolve, reject) { 13 | chrome.tabs.captureVisibleTab(null, {format: 'png'}, dataUrl => { 14 | if (!request) { 15 | return resolve(dataUrl); 16 | } 17 | 18 | const left = request.left * request.devicePixelRatio; 19 | const top = request.top * request.devicePixelRatio; 20 | const width = request.width * request.devicePixelRatio; 21 | const height = request.height * request.devicePixelRatio; 22 | 23 | const canvas = document.createElement('canvas'); 24 | const ctx = canvas.getContext('2d'); 25 | const img = new Image(); 26 | img.onload = () => { 27 | canvas.width = width || img.width; 28 | canvas.height = height || img.height; 29 | if (width && height) { 30 | ctx.drawImage(img, left, top, width, height, 0, 0, width, height); 31 | } 32 | else { 33 | ctx.drawImage(img, 0, 0); 34 | } 35 | resolve(canvas.toDataURL()); 36 | }; 37 | img.onerror = e => reject(e); 38 | img.src = dataUrl; 39 | }); 40 | }); 41 | } 42 | 43 | function save(url, filename) { 44 | chrome.storage.local.get({ 45 | 'timestamp': true, 46 | 'saveAs': false, 47 | 'save-disk': true, 48 | 'save-clipboard': false 49 | }, prefs => { 50 | if (prefs['save-disk'] || prefs['save-clipboard'] === false) { 51 | if (prefs.timestamp) { 52 | const time = new Date(); 53 | filename = filename += ' ' + time.toLocaleDateString() + ' ' + time.toLocaleTimeString(); 54 | } 55 | filename = filename 56 | .replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>{}[\]\\/]/gi, '-'); 57 | filename += '.png'; 58 | 59 | fetch(url).then(res => res.blob()).then(blob => { 60 | const url = URL.createObjectURL(blob); 61 | chrome.downloads.download({ 62 | url, 63 | filename, 64 | saveAs: prefs.saveAs 65 | }, () => { 66 | if (chrome.runtime.lastError) { 67 | chrome.downloads.download({ 68 | url, 69 | filename: 'image.png' 70 | }); 71 | } 72 | setTimeout(() => URL.revokeObjectURL(url), 20000); 73 | }); 74 | }); 75 | } 76 | if (prefs['save-clipboard']) { 77 | if (/Firefox/.test(navigator.userAgent)) { 78 | fetch(url).then(response => response.arrayBuffer()).then(buffer => { 79 | browser.clipboard.setImageData(buffer, 'png'); 80 | }); 81 | } 82 | else { 83 | chrome.tabs.executeScript({ 84 | code: ` 85 | fetch('${url}').then(res => res.blob()).then(blob => navigator.clipboard.write([new ClipboardItem({ 86 | 'image/png': blob 87 | })])).catch(e => alert(e.message)); 88 | ` 89 | }); 90 | } 91 | } 92 | }); 93 | } 94 | 95 | function matrix(id) { 96 | return new Promise(function(resolve, reject) { 97 | chrome.storage.local.get({ 98 | delay: 500, 99 | offset: 50 100 | }, prefs => { 101 | const locations = []; 102 | const cache = []; 103 | let devicePixelRatio = 1; 104 | const canvas = document.createElement('canvas'); 105 | const ctx = canvas.getContext('2d'); 106 | 107 | function two() { 108 | if (cache.length) { 109 | const obj = cache.shift(); 110 | const img = new Image(); 111 | img.onload = () => { 112 | ctx.drawImage( 113 | img, 0, 0, 114 | img.width * devicePixelRatio, 115 | img.height * devicePixelRatio, 116 | obj.x * devicePixelRatio, 117 | obj.y * devicePixelRatio, 118 | img.width * devicePixelRatio, 119 | img.height * devicePixelRatio 120 | ); 121 | two(); 122 | }; 123 | img.onerror = e => reject(e); 124 | img.src = obj.dataUrl; 125 | } 126 | else { 127 | resolve(canvas.toDataURL()); 128 | } 129 | } 130 | 131 | function one() { 132 | if (locations.length) { 133 | const [x, y] = locations.shift(); 134 | chrome.tabs.executeScript(id, { 135 | 'code': ` 136 | window.scroll(${x}, ${y}); 137 | [ 138 | document.body.scrollLeft || document.documentElement.scrollLeft, 139 | document.body.scrollTop || document.documentElement.scrollTop 140 | ] 141 | ` 142 | }, rtn => { 143 | const [x, y] = rtn[0]; 144 | window.setTimeout(() => { 145 | capture().then(dataUrl => { 146 | // save(dataUrl, x + '-' + y + '.png'); 147 | cache.push({x, y, dataUrl}); 148 | one(); 149 | }); 150 | }, prefs.delay); 151 | }); 152 | } 153 | else { 154 | two(); 155 | } 156 | } 157 | 158 | chrome.tabs.executeScript(id, { 159 | 'code': ` 160 | [ 161 | document.body.scrollWidth, 162 | document.body.scrollHeight, 163 | document.documentElement.clientWidth, 164 | document.documentElement.clientHeight, 165 | window.devicePixelRatio 166 | ] 167 | ` 168 | }, rtn => { 169 | const [scrollWidth, scrollHeight, innerWidth, innerHeight] = rtn[0]; 170 | devicePixelRatio = rtn[0][4]; 171 | canvas.width = scrollWidth * devicePixelRatio; 172 | canvas.height = scrollHeight * devicePixelRatio; 173 | for (let x = 0; x < scrollWidth - prefs.offset; x += innerWidth - prefs.offset) { 174 | for (let y = 0; y < scrollHeight - prefs.offset; y += innerHeight - prefs.offset) { 175 | locations.push([x, y]); 176 | } 177 | } 178 | one(); 179 | }); 180 | }); 181 | }); 182 | } 183 | 184 | { 185 | const once = () => { 186 | chrome.contextMenus.create({ 187 | 'id': 'capture-visual', 188 | 'title': 'Capture Visual Part', 189 | 'contexts': ['page', 'selection', 'link'] 190 | }); 191 | chrome.contextMenus.create({ 192 | 'id': 'capture-portion', 193 | 'title': 'Capture a Portion', 194 | 'contexts': ['page', 'selection', 'link'] 195 | }); 196 | chrome.contextMenus.create({ 197 | 'id': 'capture-entire', 198 | 'title': 'Capture Entire Screen', 199 | 'contexts': ['page', 'selection', 'link'] 200 | }); 201 | }; 202 | if (chrome.runtime && chrome.runtime.onInstalled) { 203 | chrome.runtime.onInstalled.addListener(once); 204 | } 205 | else { 206 | once(); 207 | } 208 | } 209 | 210 | function onCommand(cmd, tab) { 211 | if (cmd === 'capture-visual') { 212 | capture().then(a => save(a, tab.title)).catch(e => notify(e.message || e)); 213 | } 214 | else if (cmd === 'capture-portion') { 215 | chrome.tabs.insertCSS(tab.id, { 216 | file: 'data/inject/inject.css' 217 | }, () => { 218 | chrome.tabs.executeScript(tab.id, { 219 | file: 'data/inject/inject.js' 220 | }); 221 | }); 222 | } 223 | else if (cmd === 'capture-entire') { 224 | matrix(tab.id).then(a => save(a, tab.title)).catch(e => notify(e.message || e)); 225 | } 226 | } 227 | 228 | chrome.contextMenus.onClicked.addListener((info, tab) => { 229 | onCommand(info.menuItemId, tab); 230 | }); 231 | 232 | chrome.runtime.onMessage.addListener((request, sender) => { 233 | if (request.method === 'captured') { 234 | capture(request).then(a => save(a, sender.tab.title)).catch(e => notify(e.message || e)); 235 | } 236 | if (request.method === 'popup') { 237 | onCommand(request.cmd, request.tab); 238 | } 239 | }); 240 | 241 | /* FAQs & Feedback */ 242 | { 243 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome; 244 | if (navigator.webdriver !== true) { 245 | const page = getManifest().homepage_url; 246 | const {name, version} = getManifest(); 247 | onInstalled.addListener(({reason, previousVersion}) => { 248 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({ 249 | 'faqs': true, 250 | 'last-update': 0 251 | }, prefs => { 252 | if (reason === 'install' || (prefs.faqs && reason === 'update')) { 253 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45; 254 | if (doUpdate && previousVersion !== version) { 255 | tabs.query({active: true, currentWindow: true}, tbs => tabs.create({ 256 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason, 257 | active: reason === 'install', 258 | ...(tbs && tbs.length && {index: tbs[0].index + 1}) 259 | })); 260 | storage.local.set({'last-update': Date.now()}); 261 | } 262 | } 263 | })); 264 | }); 265 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /v3/worker.js: -------------------------------------------------------------------------------- 1 | /* global ceds */ 2 | 3 | if (typeof importScripts !== 'undefined') { 4 | self.importScripts('ceds.js'); 5 | } 6 | 7 | const isFF = /Firefox/.test(navigator.userAgent); 8 | 9 | chrome.runtime.onConnect.addListener(p => { 10 | p.onDisconnect.addListener(() => { 11 | console.info('port is closed', p.name); 12 | }); 13 | }); 14 | 15 | const notify = e => chrome.notifications.create({ 16 | type: 'basic', 17 | iconUrl: '/data/icons/48.png', 18 | title: chrome.runtime.getManifest().name, 19 | message: e.message || e 20 | }, id => setTimeout(chrome.notifications.clear, 5000, id)); 21 | 22 | const sanitizeFilename = filename => { 23 | // Common replacements 24 | filename = filename.replace(/[\\/:"*?<>|]/g, '_'); // Replace disallowed characters with underscores 25 | filename = filename.replace(/^\.+/g, ''); // Remove leading periods 26 | 27 | // OS-specific restrictions 28 | const platform = navigator.platform.toLowerCase(); 29 | if (platform.includes('win')) { 30 | // Windows specific restrictions 31 | filename = filename.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, ''); // Remove reserved file names 32 | filename = filename.replace(/[\x00-\x1F\x7F-\x9F]/g, '_'); // Remove control characters 33 | filename = filename.substring(0, 255); // Windows max filename length is 255 characters 34 | } 35 | else if (platform.includes('mac') || platform.includes('linux')) { 36 | // macOS and Linux specific restrictions 37 | filename = filename.trim(); // Trim leading/trailing whitespace 38 | filename = filename.replace(/^\./g, ''); // Remove leading periods 39 | filename = filename.substring(0, 255); // macOS and Linux max filename length is 255 characters 40 | } 41 | 42 | return filename; 43 | }; 44 | 45 | async function capture(request) { 46 | const prefs = await chrome.storage.local.get({ 47 | 'format': 'png', 48 | 'format-canvas': 'png', 49 | 'quality': 0.95 50 | }); 51 | 52 | const dataUrl = await chrome.tabs.captureVisibleTab(null, { 53 | format: prefs.format, 54 | quality: parseInt(prefs.quality * 100) 55 | }); 56 | 57 | if (!request) { 58 | return fetch(dataUrl).then(r => r.blob()); 59 | } 60 | 61 | const left = request.left * request.devicePixelRatio; 62 | const top = request.top * request.devicePixelRatio; 63 | const width = request.width * request.devicePixelRatio; 64 | const height = request.height * request.devicePixelRatio; 65 | 66 | const canvas = new OffscreenCanvas(width, height); 67 | const ctx = canvas.getContext('2d'); 68 | 69 | const r = await fetch(dataUrl); 70 | const blob = await r.blob(); 71 | 72 | const img = await createImageBitmap(blob); 73 | 74 | if (width && height) { 75 | ctx.drawImage(img, left, top, width, height, 0, 0, width, height); 76 | } 77 | else { 78 | ctx.drawImage(img, 0, 0); 79 | } 80 | 81 | return canvas.convertToBlob({ 82 | type: 'image/' + prefs['format-canvas'], 83 | quality: prefs.quality 84 | }); 85 | } 86 | 87 | async function copy(content, tab) { 88 | // Firefox 89 | try { 90 | await navigator.clipboard.writeText(content); 91 | } 92 | catch (e) { 93 | try { 94 | await chrome.scripting.executeScript({ 95 | target: { 96 | tabId: tab.id 97 | }, 98 | func: content => { 99 | navigator.clipboard.writeText(content).catch(() => chrome.runtime.sendMessage({ 100 | method: 'copy-interface', 101 | content 102 | })); 103 | }, 104 | args: [content] 105 | }); 106 | } 107 | catch (e) { 108 | copy.interface(content); 109 | } 110 | } 111 | } 112 | copy.interface = async (value, type = 'content') => { 113 | const win = await chrome.windows.getCurrent(); 114 | const args = new URLSearchParams(); 115 | args.set(type, value); 116 | 117 | chrome.windows.create({ 118 | url: '/data/copy/index.html?' + args.toString(), 119 | width: 400, 120 | height: 300, 121 | left: win.left + Math.round((win.width - 400) / 2), 122 | top: win.top + Math.round((win.height - 300) / 2), 123 | type: 'popup' 124 | }); 125 | }; 126 | 127 | async function save(blob, tab) { 128 | const prefs = await chrome.storage.local.get({ 129 | 'saveAs': false, 130 | 'save-disk': true, 131 | 'edit-online': false, 132 | 'save-clipboard': false, 133 | 'mask': '[date] - [time] - [title]' 134 | }); 135 | // prefs.saveAs = false; // saveAs is not supported on v3 136 | 137 | const filename = prefs['mask'] 138 | .replace('[title]', tab.title) 139 | .replace('[date]', new Intl.DateTimeFormat('en-CA').format()) 140 | .replace('[time]', new Intl.DateTimeFormat('en-CA', { 141 | hour: '2-digit', 142 | minute: '2-digit', 143 | second: '2-digit', 144 | hour12: false 145 | }).format()); 146 | 147 | // convert to data uri with caching 148 | const href = () => { 149 | if (typeof blob === 'string') { 150 | return Promise.resolve(blob); 151 | } 152 | if (href.cache) { 153 | return Promise.resolve(href.cache); 154 | } 155 | 156 | return new Promise(resolve => { 157 | const reader = new FileReader(); 158 | reader.onload = () => { 159 | href.cache = reader.result; 160 | resolve(reader.result); 161 | }; 162 | reader.readAsDataURL(blob); 163 | }); 164 | }; 165 | 166 | // save to clipboard 167 | if (prefs['save-clipboard']) { 168 | chrome.scripting.executeScript({ 169 | target: {tabId: tab.id}, 170 | func: async href => { 171 | try { 172 | const r = await fetch(href); 173 | const blob = await r.blob(); 174 | await navigator.clipboard.write([new ClipboardItem({ 175 | [r.headers.get('content-type')]: blob 176 | })]); 177 | } 178 | catch (e) { 179 | chrome.runtime.sendMessage({ 180 | method: 'save-to-clipboard', 181 | href 182 | }); 183 | } 184 | }, 185 | args: [await href()], 186 | injectImmediately: true 187 | }); 188 | } 189 | // edit online 190 | if (prefs['edit-online']) { 191 | const hd = await href(); 192 | const id = Math.random(); 193 | save.cache[id] = hd; 194 | chrome.tabs.create({ 195 | url: 'https://webbrowsertools.com/jspaint/pwa/build/index.html#gid=' + id 196 | }); 197 | } 198 | // save to disk 199 | if (prefs['save-disk'] || (prefs['save-clipboard'] === false && prefs['edit-online'] === false)) { 200 | let mime = blob.type; 201 | let url; 202 | if (isFF) { 203 | if (typeof blob === 'string') { 204 | const b = await fetch(blob).then(r => r.blob()); 205 | mime = b.type; 206 | url = URL.createObjectURL(b); 207 | } 208 | else { 209 | url = URL.createObjectURL(blob); 210 | } 211 | } 212 | else { 213 | url = await href(); 214 | } 215 | mime = mime || url.split(',')[0].split(':')[1].split(';')[0]; 216 | 217 | const extension = mime.split('/')[1].split(';')[0]; 218 | chrome.downloads.download({ 219 | url, 220 | filename: filename + '.' + extension, 221 | saveAs: prefs.saveAs 222 | }, () => { 223 | const lastError = chrome.runtime.lastError; 224 | if (lastError) { 225 | chrome.downloads.download({ 226 | url, 227 | filename: sanitizeFilename(filename) + '.' + extension, 228 | saveAs: prefs.saveAs 229 | }, () => { 230 | const lastError = chrome.runtime.lastError; 231 | if (lastError) { 232 | chrome.downloads.download({ 233 | url, 234 | filename: 'image.' + extension, 235 | saveAs: prefs.saveAs 236 | }); 237 | } 238 | }); 239 | } 240 | }); 241 | } 242 | } 243 | save.cache = {}; 244 | 245 | async function matrix(tab) { 246 | const tabId = tab.id; 247 | const prefs = await chrome.storage.local.get({ 248 | 'delay': 600, 249 | 'offset': 50, 250 | 'quality': 0.95, 251 | 'format-canvas': 'png' 252 | }); 253 | prefs.delay = Math.max(prefs.delay, 1000 / chrome.tabs.MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND || 2); 254 | 255 | const r = await chrome.scripting.executeScript({ 256 | target: {tabId}, 257 | func: () => { 258 | self.port = chrome.runtime.connect({ 259 | name: 'matrix' 260 | }); 261 | 262 | return { 263 | width: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth), 264 | height: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight), 265 | w: document.documentElement.clientWidth, 266 | h: document.documentElement.clientHeight, 267 | ratio: window.devicePixelRatio 268 | }; 269 | }, 270 | injectImmediately: true 271 | }); 272 | let {ratio, width, height, w, h} = r[0].result; 273 | 274 | // OffscreenCanvasRenderingContext2D.drawImage: Canvas exceeds max size. 275 | if (isFF) { 276 | const ms = 32767 / ratio; 277 | width = Math.min(ms, width); 278 | height = Math.min(ms, height); 279 | w = Math.min(ms, w); 280 | h = Math.min(ms, h); 281 | } 282 | 283 | const canvas = new OffscreenCanvas(width * ratio, height * ratio); 284 | const ctx = canvas.getContext('2d'); 285 | 286 | chrome.action.setBadgeText({tabId, text: 'R'}); 287 | 288 | const mx = Math.ceil( 289 | (width - prefs.offset) / (w - prefs.offset)) * Math.ceil((height - prefs.offset) / (h - prefs.offset) 290 | ); 291 | let p = 0; 292 | 293 | for (let x = 0; x < width - prefs.offset; x += w - prefs.offset) { 294 | for (let y = 0; y < height - prefs.offset; y += h - prefs.offset) { 295 | p += 1; 296 | chrome.action.setBadgeText({tabId, text: (p / mx * 100).toFixed(0) + '%'}); 297 | 298 | // move to the location 299 | await chrome.scripting.executeScript({ 300 | target: {tabId}, 301 | func: (x, y) => window.scrollTo({ 302 | left: x, 303 | top: y, 304 | behavior: 'instant' 305 | }), 306 | args: [x, y], 307 | injectImmediately: true 308 | }); 309 | // wait 310 | await new Promise(resolve => setTimeout(resolve, prefs.delay)); 311 | // read with delay 312 | const [{ 313 | result: [i, j] 314 | }] = await chrome.scripting.executeScript({ 315 | target: {tabId}, 316 | func: () => [ 317 | document.body.scrollLeft || document.documentElement.scrollLeft, 318 | document.body.scrollTop || document.documentElement.scrollTop 319 | ], 320 | injectImmediately: true 321 | }); 322 | 323 | // capture 324 | await chrome.tabs.update(tabId, { 325 | highlighted: true 326 | }); 327 | await chrome.windows.update(tab.windowId, { 328 | focused: true 329 | }); 330 | 331 | const blob = await capture(); 332 | // write 333 | const img = await createImageBitmap(blob); 334 | ctx.drawImage( 335 | img, 336 | 0, 0, img.width, img.height, 337 | i * ratio, j * ratio, img.width, img.height 338 | ); 339 | } 340 | } 341 | 342 | chrome.action.setBadgeText({tabId, text: '...'}); 343 | const blob = await canvas.convertToBlob({ 344 | type: 'image/' + prefs['format-canvas'], 345 | quality: prefs.quality 346 | }); 347 | chrome.action.setBadgeText({tabId, text: ''}); 348 | chrome.scripting.executeScript({ 349 | target: {tabId}, 350 | func: () => { 351 | try { 352 | self.port.disconnect(); 353 | } 354 | catch (e) {} 355 | } 356 | }); 357 | return blob; 358 | } 359 | 360 | { 361 | const once = () => { 362 | if (once.done) { 363 | return; 364 | } 365 | once.done = true; 366 | 367 | chrome.contextMenus.create({ 368 | 'id': 'capture-portion', 369 | 'title': 'Capture a Portion', 370 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 371 | 'contexts': ['page', 'selection', 'link'] 372 | }); 373 | chrome.contextMenus.create({ 374 | 'id': 'capture-visual', 375 | 'title': 'Capture Visual Part', 376 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 377 | 'contexts': ['page', 'selection', 'link'] 378 | }); 379 | chrome.contextMenus.create({ 380 | 'id': 'capture-entire', 381 | 'title': 'Capture Entire Screen (steps)', 382 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 383 | 'contexts': ['page', 'selection', 'link'] 384 | }); 385 | chrome.contextMenus.create({ 386 | 'id': 'capture-entire-debugger', 387 | 'title': 'Capture Entire Screen (debugger)', 388 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 389 | 'contexts': ['page', 'selection', 'link'], 390 | 'visible': isFF === false 391 | }); 392 | chrome.contextMenus.create({ 393 | 'id': 'capture-entire-debugger-steps', 394 | 'title': 'Capture Entire Screen (debugger + steps)', 395 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 396 | 'contexts': ['page', 'selection', 'link'], 397 | 'visible': isFF === false 398 | }); 399 | chrome.contextMenus.create({ 400 | 'id': 'capture-element', 401 | 'title': 'Capture Selected Element', 402 | 'documentUrlPatterns': ['http://*/*', 'https://*/*'], 403 | 'contexts': ['selection'], 404 | 'visible': false 405 | }); 406 | }; 407 | 408 | chrome.runtime.onInstalled.addListener(once); 409 | chrome.runtime.onStartup.addListener(once); 410 | } 411 | 412 | async function capturewithdebugger(options, tab) { 413 | const target = { 414 | tabId: tab.id 415 | }; 416 | 417 | await chrome.debugger.attach(target, '1.3'); 418 | 419 | const prefs = await chrome.storage.local.get({ 420 | format: 'png', 421 | quality: 0.95 422 | }); 423 | 424 | try { 425 | const result = await chrome.debugger.sendCommand(target, 'Page.captureScreenshot', { 426 | format: prefs.format, 427 | quality: parseInt(prefs.quality * 100), 428 | ...options 429 | }); 430 | if (!result) { 431 | throw Error('Failed to capture screenshot'); 432 | } 433 | save('data:image/' + prefs.format + ';base64,' + result.data, tab); 434 | chrome.debugger.detach(target); 435 | } 436 | catch (e) { 437 | chrome.debugger.detach(target).catch(e => {}); 438 | 439 | throw Error(e); 440 | } 441 | } 442 | 443 | function onCommand(cmd, tab, info) { 444 | if (cmd === 'capture-visual') { 445 | capture().then(blob => save(blob, tab)).catch(e => { 446 | console.warn(e); 447 | notify(e.message || e); 448 | }); 449 | } 450 | else if (cmd === 'capture-portion') { 451 | chrome.scripting.insertCSS({ 452 | target: {tabId: tab.id}, 453 | files: ['data/inject/inject.css'] 454 | }, () => { 455 | const lastError = chrome.runtime.lastError; 456 | if (lastError) { 457 | return notify(lastError); 458 | } 459 | chrome.scripting.executeScript({ 460 | target: {tabId: tab.id}, 461 | files: ['data/inject/inject.js'], 462 | injectImmediately: true 463 | }); 464 | }); 465 | } 466 | else if (cmd === 'capture-entire') { 467 | matrix(tab).then(a => save(a, tab)).catch(e => { 468 | console.warn(e); 469 | notify(e.message || e); 470 | }); 471 | } 472 | else if (cmd === 'capture-entire-debugger') { 473 | capturewithdebugger({ 474 | captureBeyondViewport: true 475 | }, tab).catch(e => { 476 | console.error(e); 477 | notify(e.message); 478 | }); 479 | } 480 | // alt method 481 | else if (cmd === 'capture-entire-debugger-steps') { 482 | ceds(tab).then(du => save(du, tab)).catch(e => { 483 | console.error(e); 484 | notify(e.message); 485 | }); 486 | } 487 | else if (cmd === 'capture-element') { 488 | if (info.frameId !== 0) { 489 | return notify('Currently this function only works on top frame document'); 490 | } 491 | 492 | chrome.scripting.executeScript({ 493 | target: { 494 | tabId: tab.id, 495 | frameIds: [info.frameId] 496 | }, 497 | func: () => { 498 | const range = getSelection().getRangeAt(0); 499 | const clientRect = range.getBoundingClientRect(); 500 | range.collapse(); 501 | 502 | return { 503 | x: document.documentElement.scrollLeft + clientRect.x, 504 | y: document.documentElement.scrollTop + clientRect.y, 505 | width: clientRect.width, 506 | height: clientRect.height 507 | }; 508 | }, 509 | injectImmediately: true 510 | }).then(r => capturewithdebugger({ 511 | clip: { 512 | ...r[0].result, 513 | scale: 1 514 | }, 515 | captureBeyondViewport: true 516 | }, tab)).catch(e => { 517 | console.error(e); 518 | notify(e.message); 519 | }); 520 | } 521 | } 522 | 523 | chrome.contextMenus.onClicked.addListener((info, tab) => { 524 | onCommand(info.menuItemId, tab, info); 525 | }); 526 | 527 | chrome.commands.onCommand.addListener(cmd => chrome.tabs.query({ 528 | active: true, 529 | lastFocusedWindow: true 530 | }, tabs => tabs && tabs[0] && onCommand(cmd, tabs[0]))); 531 | 532 | chrome.runtime.onMessage.addListener((request, sender, response) => { 533 | if (request.method === 'captured') { 534 | capture(request).then(a => save(a, sender.tab)).catch(e => { 535 | console.warn(e); 536 | notify(e.message || e); 537 | }); 538 | } 539 | else if (request.method === 'popup') { 540 | onCommand(request.cmd, request.tab); 541 | 542 | response(true); 543 | } 544 | else if (request.method === 'copy-interface') { 545 | copy.interface(request.content); 546 | } 547 | else if (request.method === 'jspaint-load-resource') { 548 | chrome.scripting.executeScript({ 549 | target: { 550 | tabId: sender.tab.id 551 | }, 552 | func: (gid, href) => { 553 | if (typeof self.open_from_URI !== 'undefined') { 554 | self.open_from_URI(href); 555 | return true; 556 | } 557 | document.addEventListener('DOMContentLoaded', () => { 558 | self.open_from_URI(href); 559 | }); 560 | }, 561 | args: [request.gid, save.cache[request.gid]], 562 | world: 'MAIN' 563 | }).then(r => { 564 | // what if the PWA is not loaded 565 | if (r && r[0] && r[0].result === true) { 566 | delete save.cache[request.gid]; 567 | } 568 | }); 569 | } 570 | else if (request.method === 'read-gid') { 571 | response(save.cache[request.gid]); 572 | delete save.cache[request.gid]; 573 | } 574 | else if (request.method === 'save-to-clipboard') { 575 | const gid = Math.random(); 576 | save.cache[gid] = request.href; 577 | copy.interface(gid, 'gid'); 578 | } 579 | }); 580 | 581 | /* FAQs & Feedback */ 582 | { 583 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome; 584 | if (navigator.webdriver !== true) { 585 | const {homepage_url: page, name, version} = getManifest(); 586 | onInstalled.addListener(({reason, previousVersion}) => { 587 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({ 588 | 'faqs': true, 589 | 'last-update': 0 590 | }, prefs => { 591 | if (reason === 'install' || (prefs.faqs && reason === 'update')) { 592 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45; 593 | if (doUpdate && previousVersion !== version) { 594 | tabs.query({active: true, lastFocusedWindow: true}, tbs => tabs.create({ 595 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason, 596 | active: reason === 'install', 597 | ...(tbs && tbs.length && {index: tbs[0].index + 1}) 598 | })); 599 | storage.local.set({'last-update': Date.now()}); 600 | } 601 | } 602 | })); 603 | }); 604 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version); 605 | } 606 | } 607 | --------------------------------------------------------------------------------