Показаны сообщения с ярлыком Digital Books. Показать все сообщения
Показаны сообщения с ярлыком Digital Books. Показать все сообщения

вторник, 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

вторник, 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