Существует множество приложений для создания скриншотов (clip2net, gyazo, и т.д.), но нет opensource-кроссплатформенного решения для того, чтобы его можно было доработать и использовать для своих собственных нужд (в нашем случае это была необходимость автоматически загружать скриншоты в jira). В связи с этим, было приятно решение реализовать данный функционал внутри браузера (Chrome, Firefox), этого вполне хватит для решения наших задач.
Если для Chrome существует проект с открытым исходным кодом «chrome-screen-capture», который можно использовать без проблем, немного доработав, то для Firefox таких решений нет. Данную проблему мы и решили исправить. Для этого мы портировали под Firefox расширение chrome-screen-capture.
Не буду подробно рассказывать об этапах разработки расширения для Firefox — подробная инструкция есть на сайте Mozilla.
Я хотел бы рассказать о проблемах, с которыми мы столкнулись при портировании:
Наверное, даже не проблема, а особенность: все расширения Firefox для построения интерфейса используют XUL, а в Chrome используют HTML.
Приведу пример интерфейса «Захват области»:
HTML:
XUL:
В данном примере изменения незначительны. И это не может не радовать.
Следующий пример, реализация выпадающего меню:
В Chrome реализуется достаточно просто через manifest:
В popup.html уже выводим нужные пункты меню. В Firefox данная функция реализуется через XUL:
В Firefox для расширений нельзя использовать localstorage, а в chrome-screen-capture он активно применяется. Пришлось реализовывать свой аналог на базе хранилища sqlite, получился вот такой вот полифил localStorage.js:
Пример кода, отвечающего за создание и хранения данных в sqlite sqliteStorage.js:
Для того, чтобы записать данные в хранилище, необходимо вызвать метод localStorage.setItem('fontSize', '16'). А для того, чтобы получить значение, необходимо вызвать localStorage.fontSize, все как в обычном localStorage.
Также возникли проблемы при переносе локализации: в Chrome вся локализация хранится в _locales/*/messages.json, а в Firefox — в двух файлах locale/*/screenshot.dtd и locale/*/screenshot.properties, что не очень удобно. Файл screenshot.dtd используется для локализации XUL элементов, а файл screenshot.properties для локализации внутри JS. В данной схеме есть один большой минус, ее нельзя использовать для локализации HTML. А в chrome-screen-capture встроен HTML-редактор изображений. В связи с этим были добавлены улучшения в файл screenshot.properties:
Было:
Стало:
В Firefox для подключения локализации использовался следующий код:
И для использования в JS:
После внесения изменений в файл локализации, ее стало возможно использовать как в XUL, так и в HTML.
Подключаем файл локализации:
Использование в JS:
Также, для облегчения жизни, был написан скрипт для конвертирования локализации из формата Chrome в формат Firefox convert_locale.js:
В директорию с нужной локализацией перемещаем файл messages.json и запускаем скрипт:
В результате создаются два файла в которых находится локализация в нужном формате:
./screenshot/chrome/locale/de-DE/screenshot.properties
./screenshot/chrome/locale/de-DE/screenshot.dtd
Преимущества перед аналогами:
1. Открытый исходный код (opensource)
2. Область скриншота не ограничена страницей, можно захватить адресную строку или табы.
3. Можно захватить всю страницу целиком, без учёта видимой области окна браузера и полос прокрутки.
4. Если на странице присутствуют объекты с position:fixed, то дублирование объекта не будет происходить при захвате всей станицы.
5. Исправлены ошибки (при изменении размера окна).
Резултат работы одной из функций расширения: скриншот всей страницы.
Так как для наших внутренних целей не было необходимости выгружать скриншоты в сторонние сервисы (Picasa, Facebook, Sina microblog, Imgur), то в проекте не реализована функция выгрузки, файл просто сохраняется на диске. Если проект окажется востребован пользователями, то добавить данный функционал не составит труда. А может, кто-то из читателей Хабра захочет реализовать данную функцию? C радостью ждем ваших коммитов.
Ссылка на github.
Если для Chrome существует проект с открытым исходным кодом «chrome-screen-capture», который можно использовать без проблем, немного доработав, то для Firefox таких решений нет. Данную проблему мы и решили исправить. Для этого мы портировали под Firefox расширение chrome-screen-capture.
Не буду подробно рассказывать об этапах разработки расширения для Firefox — подробная инструкция есть на сайте Mozilla.
Я хотел бы рассказать о проблемах, с которыми мы столкнулись при портировании:
Firefox XUL vs Chrome HTML
Наверное, даже не проблема, а особенность: все расширения Firefox для построения интерфейса используют XUL, а в Chrome используют HTML.
Приведу пример интерфейса «Захват области»:
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>
В данном примере изменения незначительны. И это не может не радовать.
Следующий пример, реализация выпадающего меню:
В Chrome реализуется достаточно просто через manifest:
"browser_action": {
"default_icon": "images/icon_19.png",
"default_title": "Захват экрана",
"default_popup": "popup.html"
}
В popup.html уже выводим нужные пункты меню. В Firefox данная функция реализуется через 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
В Firefox для расширений нельзя использовать localstorage, а в chrome-screen-capture он активно применяется. Пришлось реализовывать свой аналог на базе хранилища sqlite, получился вот такой вот полифил localStorage.js:
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;
})());
Пример кода, отвечающего за создание и хранения данных в 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;
})());
Для того, чтобы записать данные в хранилище, необходимо вызвать метод localStorage.setItem('fontSize', '16'). А для того, чтобы получить значение, необходимо вызвать localStorage.fontSize, все как в обычном localStorage.
Локализация
Также возникли проблемы при переносе локализации: в Chrome вся локализация хранится в _locales/*/messages.json, а в Firefox — в двух файлах locale/*/screenshot.dtd и locale/*/screenshot.properties, что не очень удобно. Файл screenshot.dtd используется для локализации XUL элементов, а файл screenshot.properties для локализации внутри JS. В данной схеме есть один большой минус, ее нельзя использовать для локализации HTML. А в chrome-screen-capture встроен HTML-редактор изображений. В связи с этим были добавлены улучшения в файл screenshot.properties:
Было:
highlight=Highlight
redact=Redact
solid_black=Solid Black
Стало:
var i18n = new Object();
i18n.highlight='Highlight';
i18n.redact='Redact';
i18n.solid_black='Solid Black';
В Firefox для подключения локализации использовался следующий код:
<stringbundleset id="stringbundleset">
<stringbundle id="string-bundle" src="chrome://screenshot/locale/screenshot.properties"/>
</stringbundleset>
И для использования в JS:
var stringsBundle = document.getElementById("string-bundle");
console.log(stringsBundle.getString(highlight) + " ");
После внесения изменений в файл локализации, ее стало возможно использовать как в XUL, так и в HTML.
Подключаем файл локализации:
<script src="chrome://screenshot/locale/screenshot.properties"></script>
Использование в JS:
console.log(i18n['highlight']);
Также, для облегчения жизни, был написан скрипт для конвертирования локализации из формата Chrome в формат Firefox convert_locale.js:
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);
});
}
В директорию с нужной локализацией перемещаем файл messages.json и запускаем скрипт:
node convert_locale.js ./screenshot/chrome/locale/de-DE/messages.json
generation finish: screenshot.properties
generation finish: screenshot.dtd
В результате создаются два файла в которых находится локализация в нужном формате:
./screenshot/chrome/locale/de-DE/screenshot.properties
./screenshot/chrome/locale/de-DE/screenshot.dtd
Выводы
Преимущества перед аналогами:
1. Открытый исходный код (opensource)
2. Область скриншота не ограничена страницей, можно захватить адресную строку или табы.
3. Можно захватить всю страницу целиком, без учёта видимой области окна браузера и полос прокрутки.
4. Если на странице присутствуют объекты с position:fixed, то дублирование объекта не будет происходить при захвате всей станицы.
5. Исправлены ошибки (при изменении размера окна).
Резултат работы одной из функций расширения: скриншот всей страницы.
Так как для наших внутренних целей не было необходимости выгружать скриншоты в сторонние сервисы (Picasa, Facebook, Sina microblog, Imgur), то в проекте не реализована функция выгрузки, файл просто сохраняется на диске. Если проект окажется востребован пользователями, то добавить данный функционал не составит труда. А может, кто-то из читателей Хабра захочет реализовать данную функцию? C радостью ждем ваших коммитов.
Ссылка на github.