📜 ⬆️ ⬇️

Porting an extension from Chrome to Firefox

There are many applications for creating screenshots (clip2net, gyazo, etc.), but there is no opensource-cross-platform solution so that it can be improved and used for your own needs (in our case it was the need to automatically upload screenshots to jira ). In this regard, it was nice solution to implement this functionality inside the browser (Chrome, Firefox), this is quite enough to solve our problems.




If for Chrome there is an open source project " chrome-screen-capture " that can be used without problems, with a little refinement, then there are no such solutions for Firefox. This problem we decided to fix. To do this, we ported the chrome-screen-capture extension under Firefox.
')
I will not talk in detail about the development stages of the extension for Firefox - there is a detailed instruction on the Mozilla website .

I would like to talk about the problems that we encountered when porting:

Firefox XUL vs Chrome HTML


Probably not even a problem, but a feature: all Firefox extensions use XUL to build the interface, while Chrome uses HTML.

I will give an example of the interface "Capture the area":


HTML:
<div id="sc_drag_area_protector"> <div id="sc_drag_shadow_top" style="height: 56px; width: 766px;"></div> <div id="sc_drag_shadow_bottom" style="height: 205px; width: 765px;"></div> <div id="sc_drag_shadow_left" style="height: 356px; width: 515px;"></div> <div id="sc_drag_shadow_right" style="height: 207px; width: 514px;"></div> <div id="sc_drag_area" style="left: 515px; top: 56px; width: 250px; height: 150px;"> <div id="sc_drag_container"></div> <div id="sc_drag_size">0 x 0</div> <div id="sc_drag_cancel"></div> <div id="sc_drag_crop">OK</div> <div id="sc_drag_north_west"></div> <div id="sc_drag_north_east"></div> <div id="sc_drag_south_east"></div> <div id="sc_drag_south_west"></div> </div> </div> 

XUL:
 <box id="sc_drag_area_protector"> <box id="sc_drag_shadow_top" style="height: 167px; width: 766px;"></box> <box id="sc_drag_shadow_bottom" style="height: 130px; width: 765px;"></box> <box id="sc_drag_shadow_left" style="height: 281px; width: 515px;"></box> <box id="sc_drag_shadow_right" style="height: 318px; width: 514px;"></box> <box id="sc_drag_area" style="left: 515px; top: 167px; width: 250px; height: 150px;"> <box id="sc_drag_container"></box> <box id="sc_drag_size">0 x 0</box> <box id="sc_drag_cancel"></box> <box id="sc_drag_crop">OK</box> <box id="sc_drag_north_west"></box> <box id="sc_drag_north_east"></box> <box id="sc_drag_south_east"></box> <box id="sc_drag_south_west"></box> </box> </box> 


In this example, the changes are minor. And this is good news.

The following example implements a drop-down menu:


Chrome is implemented quite simply through manifest:
 "browser_action": { "default_icon": "images/icon_19.png", "default_title": " ", "default_popup": "popup.html" } 

In popup.html already display the necessary menu items. In Firefox, this feature is implemented via XUL:
 <toolbarpalette id="BrowserToolbarPalette"> <toolbarbutton id="poputchikScreen" type="menu-button" label="Screenshot" class="toolbarbutton-1" oncommand="screenshot.screen.lastAction(); event.stopPropagation();" image="chrome://screenshot/skin/img/icon.png"> <menupopup> <menuitem label=" " image="chrome://screenshot/skin/img/custom.png" oncommand="screenshot.screen.captureArea(); event.stopPropagation();" class="menuitem-iconic"/> <menuitem label="  " image="chrome://screenshot/skin/img/screen.png" oncommand="screenshot.screen.captureWindow(); event.stopPropagation();" class="menuitem-iconic"/> <menuitem label="  " image="chrome://screenshot/skin/img/whole.png" oncommand="screenshot.screen.captureWebpage(); event.stopPropagation();" class="menuitem-iconic"/> </menupopup> </toolbarbutton> </toolbarpalette> 


LocalStorage


In Firefox, localstorage cannot be used for extensions, and it is actively used in chrome-screen-capture. I had to implement my counterpart on the basis of the sqlite repository, this is the localStorage.js polyfile :
 Object.defineProperty(window, "localStorage", new (function () { var aKeys = [], oStorage = {}; Object.defineProperty(oStorage, "getItem", { value: function (sKey) { return sqliteStorage.getItem(escape(sKey)); }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(oStorage, "key", { value: function (nKeyId) { return aKeys[nKeyId]; }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(oStorage, "setItem", { value: function (sKey, sValue) { if(!sKey) { return; } sqliteStorage.setItem(escape(sKey), escape(sValue)); }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(oStorage, "length", { get: function () { return aKeys.length; }, configurable: false, enumerable: false }); Object.defineProperty(oStorage, "removeItem", { value: function (sKey) { if(!sKey) { return; } sqliteStorage.removeItem(escape(sKey)); }, writable: false, configurable: false, enumerable: false }); this.get = function () { var iThisIndx; for (var sKey in oStorage) { iThisIndx = aKeys.indexOf(sKey); if (iThisIndx === -1) { oStorage.setItem(sKey, oStorage[sKey]); } else { aKeys.splice(iThisIndx, 1); } delete oStorage[sKey]; } for (aKeys; aKeys.length > 0; aKeys.splice(0, 1)) { oStorage.removeItem(aKeys[0]); } var aCouples = sqliteStorage.getAllItems(); for (var iKey in aCouples) { iKey = unescape(iKey); oStorage[iKey] = unescape(aCouples[iKey]); aKeys.push(iKey); } return oStorage; }; this.configurable = false; this.enumerable = true; })()); 

An example of code responsible for creating and storing data in sqlite sqliteStorage.js :
 Object.defineProperty(window, "sqliteStorage", new (function () { var file = Components.classes["@mozilla.org/file/directory_service;1"] .getService(Components.interfaces.nsIProperties) .get("ProfD", Components.interfaces.nsIFile); var storageService = Components.classes["@mozilla.org/storage/service;1"] .getService(Components.interfaces.mozIStorageService); var mDBConn = null; var tableName = 'screenshot'; var aKeys = [], sStorage = {}; file.append("ScreenshotData"); if( !file.exists() || !file.isDirectory() ) { file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777); } file.append("screenshot.sqlite"); mDBConn = storageService.openDatabase(file); var create = function () { mDBConn.createTable(tableName, "id integer primary key autoincrement, Name_key TEXT, Key_value TEXT"); mDBConn.executeSimpleSQL('CREATE UNIQUE INDEX idx_name_key ON ' + tableName + ' (Name_key)'); }; Object.defineProperty(sStorage, "getItem", { value: function (sKey) { var statement = null; var result = null; if (!mDBConn.tableExists(tableName)) { create(); } statement = mDBConn.createStatement("SELECT Key_value FROM " + tableName + " where Name_key = '" + sKey + "'"); while (statement.step()) { result = statement.row['Key_value']; } return result; }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(sStorage, "setItem", { value: function (sKey, sValue) { if (!mDBConn.tableExists(tableName)) { create(); } mDBConn.executeSimpleSQL("REPLACE INTO " + tableName + " (Name_key, Key_value) VALUES ('"+sKey+"', '"+sValue+"')"); }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(sStorage, "removeItem", { value: function (sKey) { if (!mDBConn.tableExists(tableName)) { create(); } mDBConn.executeSimpleSQL("DELETE FROM " + tableName + " WHERE Name_key = '"+sKey+"'"); }, writable: false, configurable: false, enumerable: false }); Object.defineProperty(sStorage, "getAllItems", { value: function () { var statement = null; var result = {}; if (!mDBConn.tableExists(tableName)) { create(); } statement = mDBConn.createStatement("SELECT Name_key, Key_value FROM " + tableName + ""); while (statement.step()) { result[statement.row['Name_key']] = statement.row['Key_value']; } return result; }, writable: false, configurable: false, enumerable: false }); this.get = function () { var iThisIndx; for (var sKey in sStorage) { iThisIndx = aKeys.indexOf(sKey); if (iThisIndx === -1) { sStorage.setItem(sKey, sStorage[sKey]); } else { aKeys.splice(iThisIndx, 1); } delete sStorage[sKey]; } for (aKeys; aKeys.length > 0; aKeys.splice(0, 1)) { sStorage.removeItem(aKeys[0]); } return sStorage; }; this.configurable = false; this.enumerable = true; })()); 

In order to write data to the repository, you must call the localStorage.setItem method ('fontSize', '16'). And in order to get the value, you must call localStorage.fontSize, all as in the usual localStorage.

Localization


Also, there were problems with the localization transfer: in Chrome, all localization is stored in _locales / * / messages.json, and in Firefox - in two files locale / * / screenshot.dtd and locale / * / screenshot.properties, which is not very convenient. The screenshot.dtd file is used to localize XUL elements, and the screenshot.properties file is used for localization inside JS. In this scheme there is one big minus, it can not be used to localize HTML. And the HTML image editor is built into the chrome-screen-capture. In this regard, improvements were added to the screenshot.properties file:

It was:
 highlight=Highlight redact=Redact solid_black=Solid Black 

It became:
 var i18n = new Object(); i18n.highlight='Highlight'; i18n.redact='Redact'; i18n.solid_black='Solid Black'; 

In Firefox, the following code was used to connect localization:
 <stringbundleset id="stringbundleset"> <stringbundle id="string-bundle" src="chrome://screenshot/locale/screenshot.properties"/> </stringbundleset> 

And for use in JS:
 var stringsBundle = document.getElementById("string-bundle"); console.log(stringsBundle.getString(highlight) + " "); 

After making changes to the localization file, it became possible to use it in both XUL and HTML.

We connect the localization file:
 <script src="chrome://screenshot/locale/screenshot.properties"></script> 

Use in JS:
 console.log(i18n['highlight']); 

Also, to make life easier, a script was written to convert localization from Chrome format to Firefox convert_locale.js format:
 var fs = require('fs'); var path = require('path'); var filePath = process.argv[2]; var dirPath = path.dirname(filePath); var messages = {}; fs.readFile(filePath, function (err, data) { if (err) throw err; messages = JSON.parse(data); generationProp(messages); generationDTD(messages); }); function generationDTD (msg) { var resultDTD = ''; for (var key in msg) { resultDTD += '<!ENTITY ' + key + ' "' + msg[key].message + '">' + "\n"; } writeFile('screenshot.dtd', resultDTD); } function generationProp(msg) { var resultProp = ''; for (var key in msg) { resultProp += key + '=' + msg[key].message + "\n"; } writeFile('screenshot.properties', resultProp); } function writeFile(fileName, data) { var writeFile = path.join(dirPath, fileName); fs.writeFile(writeFile, data, function (err) { if (err) throw err; console.log('generation finish: ' + fileName); }); } 

In the directory with the desired localization, move the file messages.json and run the script:
 node convert_locale.js ./screenshot/chrome/locale/de-DE/messages.json generation finish: screenshot.properties generation finish: screenshot.dtd 

As a result, two files are created in which the localization is in the required format:

./screenshot/chrome/locale/de-DE/screenshot.properties
./screenshot/chrome/locale/de-DE/screenshot.dtd

findings


Advantages over analogues:
1. Open source (opensource)
2. The area of ​​the screenshot is not limited to the page, you can capture the address bar or tabs.
3. You can capture the entire page entirely, without taking into account the visible area of ​​the browser window and scroll bars.
4. If there are objects with position: fixed on the page, then duplication of the object will not occur when the entire page is captured.
5. Fixed errors (when resizing the window).

The result of one of the expansion functions: a screenshot of the entire page .

Since for our internal purposes there was no need to upload screenshots to third-party services (Picasa, Facebook, Sina microblog, Imgur), the upload function does not implement the upload function, the file is simply saved to disk. If the project will be in demand by users, then add this functionality is not difficult. Or maybe someone from Habr's readers wants to implement this function? We are looking forward to your commits.

Link to github .

Source: https://habr.com/ru/post/205698/


All Articles