вторник, 29 ноября 2016 г.

Sigil Image downloader plugin

К сожалению, вопрос написания плагинов для Sigil очень мало освещён. Нужно постараться, чтобы найти даже официальный мануал, содержащий скупые описания. И ни они, ни исходники не отвечают на некоторые интересные вопросы.

Полезные ссылки, все на английском:

https://github.com/Sigil-Ebook/Sigil/blob/master/docs/Sigil_Plugin_Framework_rev7.epub - официальный мануал.
http://www.mobileread.com/forums/attachment.php?attachmentid=140885&d=1439113322 - PDF версия с некоторыми примерами.
https://github.com/Sigil-Ebook/Sigil/blob/master/src/Resource_Files/plugin_launchers/python/bookcontainer.py - Один из самых важных заголовочных файлов
Заголовки https://github.com/Sigil-Ebook/Sigil/tree/master/src/Resource_Files/plugin_launchers/python

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases

Итак:
Периодически есть желание сделать копию статьи из интернета, и часто там присутствуют изображения - фотографии, картинки, иллюстрации.
Можно с одной стороны сохранять целиком веб-страничку. А можно написать плагин, и просто копировать нужный фрагмент статьи (или её всю целиком) в Sigil и просто запустить плагин. К счастью, авторы не стали изобретать что-то странное и взяли привычный и простой Python.

Интерпретатор версии 3.4 встроен в версию Sigil для Windows (поставляется вместе), и содержит много полезных библиотек, например PIL, html5lib, regex, lxml, библиотеки для работы с CSS и другие.
В версии для  Windows х64 библиотеки можно увидеть по адресу (по умолчанию): C:\Program Files\Sigil\python3\Lib\site-packages
Для х32 - C:\Program Files (x86)\Sigil\python3\Lib\site-packages
Но можно использовать и свой интерпретатор, указав его в настройках "Управление модулями" Sigil (установка плагинов происходит через неё же, почитать про установку плагинов можно в мануале или здесь: http://www.rshelton.org/2015/10/new-version-of-sigil.html).

Чтобы плагин был плагином, нужно создать plugin.xml с нехитрым содержимым (Его можно будет посмотреть или в мануалах, или в репозитории на GitHub). Единственная особенность: не стоит пытаться указывать совместимость с python3.5 - плагин просто не загрузится.
Только с python3.4 или python2.7 (Рекомендован всё же 3.4).

Сам скрипт, главная часть плагина - имеет имя plugin.py.

Входной точкой в плагин будет функция run, принимающая объект класса BookContainer с именем bk и возвращающая код завершения (успеха) - 0:

def run(bk):
    pass
    # наши действия будем писать здесь.
    return 0

Все остальные функции либо будут вызываться из run, либо написаны "на всякий случай, если скрипт запустят из терминала, без загрузки в Sigil".

Чтобы обойти все файлы с текстом (html, а точнее xhtml странички), мы воспользуемся, как и советуют, следющей конструкцией:

for (_id, href) in bk.text_iter():
    html = bk.readfile(_id)
    if not isinstance(html, text_type):
        html = str(html, 'utf-8')
    html_orig = html
    # наши действия
    if not html == html_orig:  # Проверяем, что содержимое файла наш скрипт изменил
        bk.writefile(_id, html)

С помощью bk.text_iter() обойдём все странички с текстом книги, получая для каждой _id - идентификатор (по факту - имя файла в стиле Section0001.xhtml), и адрес (например: ../Text/Section0001.xhtml). _id Мы потом используем для сохрания текста с помощью bk.writefile(_id, html).

Чтобы выяснить, какие файлы изображений уже есть в книге, используем bk.image_iter():
exists_image_id_list = list(_id for _id, _, _ in bk.image_iter())

Казалось бы. Но не всё так просто - поднимается вопрос "как сохранять наше изображение в книгу"?
Sigil API, а точнее bookcontainer.py предлагает функцию addfile класса BookContainer.
addfile требует загадочный параметр uniqueid, basename и data.
data - это содержимое нашего файла, бинарные данные.
basename - по логике имя файла без пути, т.е. скажем image.png
А uniqueid? Мануал молчит. Экспериментально, (а заодно подсмотрев в те id, что возвращает text_iter() ), было установлено - в этом качестве прекрасно подходит имя файла (то же самое basename).
И фактически (в исходниках используется uniqueid, но смысл тот же) получаем вызов:
bk.addfile(basename, basename, data)
Остальное Sigil сделает самостоятельно.

Проверить существование uniqueid можно либо попытавшись загрузить файл из книги bk.readfile,
либо вызвав bk.basename_to_id(uniqueid), которая вернёт либо None, если такого id ещё не встречалось, либо значение uniqueid, если уже существует и был использован.

Остальное - дело техники:
- Найти ссылки на странице с помощью простого регулярного выражения:
urls = re.findall(r'<img.+?src=["\'](http.+?)["\']', html, re.I + re.M)
- пройтись по каждой из них, получая адреса файлов банальным url[url.rfind('/') + 1:]
- загрузить файл с помощью штатного urllib.request.urlopen
* Для этого создана функция get_data(url), строка 16 скрипта *
* Да, действительно есть прекрасный requests, но это добавляет скрипту лишнюю зависимость. А так хоть и без поддержки сессий HTTP 2, но зато работает из коробки, в том числе на встроенном в Sigil интерпретаторе.*
- Проверить, есть ли у нас такой же файл
* Для проверки используется хэш сумма SHA512 так же из штатной библиотеки:
 hashlib.sha512(data).digest(), которая сравнивается с тем файлом, уже существующим в книге, у которого совпадает имя (оно же uniqueid) *
- Создать уникальное имя файла, и проверить его
* Для этого создана функция make_unique_id_for_img(unique_id, exists_image_id_list), строка 35*
- Записать изображение с помощью bk.addfile
- Исправить ссылку на файл, заменив внешнюю (из интернета), на локальную "../Images/%uniqueid%" банальным html.replace
- Сохранить наш html: bk.writefile(_id, html)
- Добавить обработку ошибок и вывод информации о том, что сейчас происходит.
- Запаковать в ZIP архив, при этом архив должен называться так же, как и папка (со скриптом и XML файлом), которая в него вложена.

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases

четверг, 5 мая 2016 г.

VirtualBox + Lubuntu 16.04

Проблема работы VirtualBox с Lubuntu 16.04, (когда при загрузке на экране некий графический шум), оказалось решается тривиально - переключением на текстовый терминал (Ctrl+Alt+F1), а потом возврат обратно (Ctrl+Alt+F7).

Solution to solve problem VirtualBox + Lubuntu 16.04 : go to text terminal (Ctrl+Alt+F1), and after return to GUI (Ctrl+Alt+F7).

вторник, 3 мая 2016 г.

Nautilus Extension for FictionBook2 (FB2)

Расширение для файлового менеджера Nautilus, показывающее в отдельных колонках (табличный режим просмотра) информацию об авторе книги и её названии.

Исходники можно получить https://github.com/yastrov/nautilus_extension_fictionbook2 , а сборка практически тривиальна и описана в README. (Хотя для версии Nautilus отличной от 3, придётся поправить в Makefile пути к расширениям.)

Наконец-то нашёл время, чтобы осуществить старое желание написать полезное расширение.

В процессе создания пришлось погулять по интернету, причём улов был очень скромный. Очень мало примеров, очень скудная документация, особенно когда дело доходит до не совсем классических вещей.
Поэтому результат представляет собой попытку осмыслить в том числе редкие примеры на языке Си, а так же Python. На последнем примеров больше, хотя мне не кажется скриптование подобных вещей, постоянно находящихся в памяти и работающих, хорошим и оптимальным решением.

Не буду останавливаться на описании интерфейсов самого Nautilus. Тут много рутины, и выглядит оно не самым наглядным на мой взгляд образом. (И, что греха таить - было частично заимствовано из примеров. Но не суть.)

На чём стоит остановиться:

Почему не Python?
Причиной, что кроме любви к Си, было желание сделать компактное (в плане потребляемой памяти) и достаточно быстрое решение. А в качестве бонуса - не смотря на отсутствие бинарников на данный момент, ещё и более простое распространение - установку.

По возможности были использованы функции GLib вместо стандартных Си. Потому, что она рекомендована из-за безопасности - т.е. обёртки над функциями стандартной библиотеки языка, которые она предоставляет, уже содержат проверки, в частности указателей на NULL.

Асинхронная обработка оказалась не настолько понятно описанной как в доках, так и в примерах. И это плохо, потому что при работе с блокирующими операциями нужно использовать именно её. Поэтому сложно ручаться, что она получилась оптимальной. Однако основную задачу - асинхронность, она выполняет. (Все виденные примеры, были написаны на Python.)

Долго думал, использовать для отложенного вызова g_timeout_add с секундной задержкой (что популярно, но всё-таки не совсем правильно: может произойти вызов примерно одновременно для множества файлов) или g_idle_add, обещающую вызов тогда, когда снизится нагрузка (про последнюю не вполне понятно, гарантируется ли её вызов в обозримом будущем, или может отложиться на неопределённое время.) Пока победила g_idle_add.

Приведу небольшой фрагмент кода в качестве иллюстрации:

Для того, чтобы можно было использовать асинхронную обработку, необходимо определить примерно следующую структуру:

typedef struct {
    GClosure *update_complete;
    NautilusInfoProvider *provider;
    NautilusFileInfo *file;
    int operation_handle;
    gboolean cancelled;
} UpdateHandle;

А наш callback будет выглядеть:

gint
timeout_plain_fb2_callback(gpointer data);

Посмотрим же основную функцию, которую будет вызывать Nautilus:

static NautilusOperationResult
fb2_extension_update_file_info (NautilusInfoProvider *provider,
                NautilusFileInfo *file,
                GClosure *update_complete,
                NautilusOperationHandle **handle)
{
    if(nautilus_file_info_is_directory(file))
        return NAUTILUS_OPERATION_COMPLETE;
    ...
 
    if (!data) {
        /* Получаем имя файла именно таким способом. */
        char *filename = nautilus_file_info_get_name(file);
        const int len = strlen(filename);
        if(len > 4 && g_strcmp0(&filename[len-4], ".fb2") == 0) {
            /* Заполним структуру, необходимую для асинхронной работы. */
            UpdateHandle *update_handle = g_new0 (UpdateHandle, 1);
            update_handle->update_complete = g_closure_ref(update_complete);
            update_handle->provider = provider;
            update_handle->file = g_object_ref (file);
            // Или так: g_timeout_add (1,
            /* Но лучше так: */
            g_idle_add(
                timeout_plain_fb2_callback,
                update_handle);
            /* Польза данной операции не вполне очевидна... */
            *handle = update_handle;
            g_free(filename);
            /* Сообщаем, что выполнение операции отложено. */
            return NAUTILUS_OPERATION_IN_PROGRESS;    
        } else {
           ...
            }
        }
        g_free(filename);
        return NAUTILUS_OPERATION_COMPLETE;
    }

    return NAUTILUS_OPERATION_COMPLETE;
}

callback Практически из официального примера бородатого года:
В нём мы с помощью g_object_set_data_full кэшируем данные на будущее, и устанавливаем
с помощью nautilus_file_info_add_string_attribute для отображения сейчас:

gint
timeout_plain_fb2_callback(gpointer data)
{
    UpdateHandle *handle = (UpdateHandle*)data;
    /* Не вполне очевидно, что же может отменить операцию, но раз в доках предлагается, оставим. */
    if (!handle->cancelled) {
        char *filename = g_file_get_path(nautilus_file_info_get_location(handle->file));
        ...
        }
        g_free(filename);
    }
 
    nautilus_info_provider_update_complete_invoke
                        (handle->update_complete,
                         handle->provider,
                         (NautilusOperationHandle*)handle,
                         NAUTILUS_OPERATION_COMPLETE);
    /* С handle мы закончили. */
    g_closure_unref (handle->update_complete);
    g_object_unref (handle->file);
    g_free (handle);
    return 0;
}

Вывод:

Если не считать громоздких и не совсем наглядных (особенно когда есть желание сделать один единственный файл - исходник) объявлений интерфейсов самого Nautilus, и бедной документации, в целом процесс легче, чем может показаться, а возможности по кастомизации - большие.

Так же стоит при написании расширения искать исходники и примеры на других языках программирования.

Подборка ссылок:

http://web.archive.org/web/20090418175132/http://www.campd.org/stuff/docs/extending-nautilus/NautilusExtensions.html Официальный Nautilus Extension manual, теперь доступный только из архива:

https://developer.gnome.org/libnautilus-extension/stable/ Nautilus API

https://developer.gnome.org/glib/stable/ - GLib

четверг, 17 марта 2016 г.

Распространение приложений на Qt, или как найти нужные DLL - windeployqt.exe

Проблема распространения приложения на Qt пользователю - больная тема для большинства разработчиков.
Оказывается, она упрощается с помощью утилиты от самих разработчиков Qt. Но к сожалению эта утилита упоминается очень редко, так что я сам узнал о ней только что. Возможно у неё есть недостатки, пока мне неизвестные но на первый взгляд она удобная, если бы не была консольной. Но с последней проблемой мы можем справиться).

Откроем Qt Creator, зайдём в меню "Инструменты"- подпункт "Параметры", слева пункт "Среда", вкладка "Внешние утилиты".
И в нижнем меню "Дабавить" выберем вначале "Новый раздел" (я выбрал ёмко и просто - ) "Добавить утилиту" (соответственно "windeployqt")
И справа заполним следующим образом:

Описание: "Эта утилита добавляет необходимые DLL в папку с EXE"
Программа: %{CurrentProject:QT_INSTALL_BINS}\windeployqt.exe
Параметры: --compiler-runtime %{CurrentProject:BuildPath}\%{CurrentBuild:Type}
Среда: PATH=C:\Qt\Qt5.6.0\Tools\mingw492_32\bin;${PATH}

*В параметрах нужно указать строку, где создаётся наш EXE.) А поскольку редактировать настройки каждый раз неудобно, соберём путь из заботливо предоставленных авторами Qt переменных. У меня расположение компилятора из системной переменной не подхватилось, на всякий случай добавил.*

В результате должно получиться

Можно добавить параметры, отвечающие за копирование плагинов, платформ и т.д. Подробнее:
http://doc.qt.io/qt-5/windows-deployment.html

Так же придётся добавить в системную переменную следующие значения (пример для Qt 5.6.0):
C:\Qt\Qt5.6.0\Tools\mingw492_32\bin
C:\Qt\Qt5.6.0\5.6\mingw49_32\bin
*Адрес компилятора нужен, поскольку DLL из его поставки нам тоже понадобятся.*
*Чтобы изменить системные переменные, можно в диалоге "Выполнить" ОС Windiws запустить sysdm.cpl .*
*Так же можно поэкспериментировать с переменными среды самого Qt Creator.*

Всё, можно собирать проект, затем вызывать нашу утилиту и смотреть каталог! Для удаление мусора из каталога - пункт "Очистить" из меню "Сборка"..
Не забудьте добавить те библиотеки, которые вы импортировали сами!

Qt Auto Resize Widgets

У многих начинающих разработчиков, использующих Qt Creator (а кому время, потраченное на создание интерфейса лишнее?) возникает проблема, как сделать, чтобы виджеты сами подстраивались под размер окна (Auto Resize Widgets).
И никакие настройки элементов не помогают.
На самом деле решение элементарно.

В режиме дизайнера мы можем это легко исправить.
Достаточно выбрать в "инспекторе объектов" наше главное окно (обычно называется centralWidget типа QWidget). И присмотреться в верхней части окна Designer-а (сразу под привычным нам меню). Там мы видим маленькие значки очень похожие на пиктограммы компоновщиков (если навести на них мышкой, то и подсказки будут соответствующими). Достаточно выбрать один, как наше главное поле превратится в компоновщик!

Забавно, но это единственный известный мне способ, хотя логичнее было бы увидеть соответствующий пункт меню в "инспекторе объектов". *Именно из-за этого я считал, что эти кнопки относятся к обычным компоновщикам.)