Răsfoiți Sursa

Merge branch 'release/0.9.12'

Book Pauk 4 ani în urmă
părinte
comite
ef12a84285

+ 36 - 21
client/components/Reader/ContentsPage/ContentsPage.vue

@@ -67,6 +67,7 @@
                             <img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
                         </div>
                         <div class="no-expand-button column justify-center items-center">
+                            <div class="image-num">{{ item.num }}</div>
                             <div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
                             <div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
                             <div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
@@ -145,25 +146,36 @@ class ContentsPage extends ContentsPageProps {
         await this.$nextTick();
 
         const pc = parsed.contents;
-        const newpc = [];
-        //преобразуем все, кроме первого, разделы body в title-subtitle
-        let curSubtitles = [];
-        let prevBodyIndex = -1;
-        for (let i = 0; i < pc.length; i++) {
-            const cont = pc[i];
-            if (prevBodyIndex != cont.bodyIndex)
-                curSubtitles = [];
-
-            prevBodyIndex = cont.bodyIndex;
-
-            if (cont.bodyIndex > 1) {
-                if (cont.inset < 1) {
-                    newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
+        const ims = parsed.images;
+        const newpc = [];        
+        if (pc.length) {//если есть оглавление
+            //преобразуем все, кроме первого, разделы body в title-subtitle
+            let curSubtitles = [];
+            let prevBodyIndex = -1;
+            for (let i = 0; i < pc.length; i++) {
+                const cont = pc[i];
+                if (prevBodyIndex != cont.bodyIndex)
+                    curSubtitles = [];
+
+                prevBodyIndex = cont.bodyIndex;
+
+                if (cont.bodyIndex > 1) {
+                    if (cont.inset < 1) {
+                        newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
+                    } else {
+                        curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
+                    }
                 } else {
-                    curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
+                    newpc.push(cont);
+                }
+            }
+        } else {//попробуем вытащить из images
+            for (let i = 0; i < ims.length; i++) {
+                const image = ims[i];
+
+                if (image.alt) {
+                    newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
                 }
-            } else {
-                newpc.push(cont);
             }
         }
 
@@ -212,19 +224,18 @@ class ContentsPage extends ContentsPageProps {
 
         //формируем newImages
         const newImages = [];
-        const ims = parsed.images;
         for (i = 0; i < ims.length; i++) {
             const image = ims[i];
             const bin = parsed.binary[image.id];
             const type = (bin ? bin.type : '');
             
-            const label = `Изображение ${image.num}`;
+            const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
             const indentStyle = getIndentStyle(1);
-            const labelStyle = getLabelStyle(0);
+            const labelStyle = getLabelStyle(1);
 
             const p = parsed.para[image.paraIndex];
             newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
-                indentStyle, labelStyle, type, id: image.id, local: image.local});
+                indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
         }
 
         this.images = newImages;
@@ -389,6 +400,10 @@ class ContentsPage extends ContentsPageProps {
     transform: rotate(90deg);
 }
 
+.image-num {
+    font-size: 120%;
+    padding-bottom: 3px;
+}
 .image-type {
     border: 1px solid black;
     border-radius: 6px;

+ 27 - 21
client/components/Reader/Reader.vue

@@ -39,9 +39,9 @@
                         <q-icon name="la la-copy" size="32px"/>
                         <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
                     </button>
-                    <button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
-                        <q-icon name="la la-retweet" size="32px"/>
-                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
+                    <button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
+                        <q-icon name="la la-magic" size="32px"/>
+                        <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
                     </button>
                     <button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
                         <q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
@@ -317,6 +317,10 @@ class Reader extends Vue {
         this.showToolButton = settings.showToolButton;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
+        this.splitToPara = settings.splitToPara;
+        this.djvuQuality = settings.djvuQuality;
+        this.pdfAsText = settings.pdfAsText;
+        this.pdfQuality = settings.pdfQuality;
 
         this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
         this.$root.readerActionByKeyEvent = (event) => {
@@ -336,7 +340,7 @@ class Reader extends Vue {
 
                 let againMes = '';
                 if (this.isFirstNeedUpdateNotify) {
-                    againMes = ' ЕЩЕ один раз';
+                    againMes = ' еще один раз';
                 }
 
                 if (this.version != this.clientVersion)
@@ -345,9 +349,9 @@ class Reader extends Vue {
                 console.error(e);
             } finally {
                 this.checkingNewVersion = false;
-            }
+            }        
+            this.isFirstNeedUpdateNotify = false;
         }
-        this.isFirstNeedUpdateNotify = false;
     }
 
     updateHeaderMinWidth() {
@@ -703,6 +707,12 @@ class Reader extends Vue {
         }
     }
 
+    convOptionsToggle() {
+        this.settingsToggle();
+        if (this.settingsActive)
+            this.$refs.settingsPage.selectedTab = 'convert';
+    }
+
     helpToggle() {
         this.helpActive = !this.helpActive;
         if (this.helpActive) {
@@ -729,15 +739,9 @@ class Reader extends Vue {
         }
     }
 
-    refreshBook(mode) {
+    refreshBook() {
         const mrb = this.mostRecentBook();
-        if (mrb) {
-            if (mode && mode == 'split') {
-                this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, skipCheck: true, isText: true, force: true});
-            } else {
-                this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
-            }
-        }
+        this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
     }
 
     undoAction() {
@@ -777,7 +781,7 @@ class Reader extends Vue {
             case 'scrolling':
             case 'search':
             case 'copyText':
-            case 'splitToPara':
+            case 'convOptions':
             case 'refresh':
             case 'contents':
             case 'libs':
@@ -811,7 +815,6 @@ class Reader extends Vue {
                 case 'contents':
                     classResult = classDisabled;
                     break;
-                case 'splitToPara':
                 case 'refresh':
                 case 'recentBooks':
                     if (!this.mostRecentBookReactive)
@@ -973,10 +976,13 @@ class Reader extends Vue {
             if (!book) {
                 book = await readerApi.loadBook({
                         url,
-                        skipCheck: (opts.skipCheck ? true : false),
-                        isText: (opts.isText ? true : false),
+                        uploadFileName,
                         enableSitesFilter: this.enableSitesFilter,
-                        uploadFileName
+                        skipHtmlCheck: (this.splitToPara ? true : false),
+                        isText: (this.splitToPara ? true : false),
+                        djvuQuality: this.djvuQuality,
+                        pdfAsText: this.pdfAsText,
+                        pdfQuality: this.pdfQuality,
                     },
                     (state) => {
                         progress.setState(state);
@@ -1102,8 +1108,8 @@ class Reader extends Vue {
             case 'copyText':
                 this.copyTextToggle();
                 break;
-            case 'splitToPara':
-                this.refreshBook('split');
+            case 'convOptions':
+                this.convOptionsToggle();
                 break;
             case 'refresh':
                 this.refreshBook();

+ 1 - 52
client/components/Reader/ReaderDialogs/ReaderDialogs.vue

@@ -57,37 +57,6 @@
                 <q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
             </span>
         </Dialog>
-
-        <Dialog ref="dialog3" v-model="liberamaTopVisible">
-            <template slot="header">
-                Здравствуйте, уважаемые читатели!
-            </template>
-
-            <div style="word-break: normal">
-                Создан новый ресурс:<br><br>
-
-                <a href="https://liberama.top" target="_blank">https://liberama.top</a>
-                <br><br>
-                Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
-
-                <ul>
-                    <li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
-                    <li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
-                </ul>
-
-                Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
-                О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
-                Спасибо, что вы с нами!
-                <br><br>
-                <div class="row justify-center">
-                    <q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
-                </div>
-            </div>
-
-            <span slot="footer">
-                <q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
-            </span>
-        </Dialog>
     </div>
 </template>
 
@@ -114,7 +83,6 @@ class ReaderDialogs extends Vue {
     whatsNewVisible = false;
     whatsNewContent = '';
     donationVisible = false;
-    liberamaTopVisible = false;
 
     created() {
         this.commit = this.$store.commit;
@@ -127,14 +95,12 @@ class ReaderDialogs extends Vue {
     async init() {
         await this.showWhatsNew();
         await this.showDonation();
-        await this.showLiberamaTop();
     }
 
     loadSettings() {
         const settings = this.settings;
         this.showWhatsNewDialog = settings.showWhatsNewDialog;
         this.showDonationDialog2020 = settings.showDonationDialog2020;
-        this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
     }
 
     async showWhatsNew() {
@@ -171,7 +137,6 @@ class ReaderDialogs extends Vue {
 
     openDonate() {
         this.donationVisible = false;
-        this.liberamaTopVisible = false;
         this.$emit('donate-toggle');
     }
 
@@ -210,24 +175,8 @@ class ReaderDialogs extends Vue {
         return this.$store.state.reader.donationRemindDate;
     }
 
-    async showLiberamaTop() {
-        const today = utils.formatDate(new Date(), 'coDate');
-
-        if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
-            await utils.sleep(3000);
-            this.liberamaTopVisible = true;
-        }
-    }
-
-    liberamaTopDialogDisable()  {
-        this.liberamaTopVisible = false;
-        if (this.showLiberamaTopDialog2020) {
-            this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
-        }
-    }
-
     keyHook() {
-        if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
+        if (this.$refs.dialog1.active || this.$refs.dialog2.active)
             return true;
         return false;
     }

+ 11 - 2
client/components/Reader/SettingsPage/SettingsPage.vue

@@ -26,6 +26,7 @@
                     <q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
                     <q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
                     <q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
+                    <q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
                     <q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
                     <q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
                     <div v-show="tabsScrollable" class="q-pt-lg"/>
@@ -53,6 +54,10 @@
                 <div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
                     @@include('./include/PageMoveTab.inc');
                 </div>
+                <!-- Конвертирование ------------------------------------------------------------->
+                <div v-if="selectedTab == 'convert'" class="fit tab-panel">
+                    @@include('./include/ConvertTab.inc');
+                </div>
                 <!-- Прочее ---------------------------------------------------------------------->
                 <div v-if="selectedTab == 'others'" class="fit tab-panel">
                     @@include('./include/OthersTab.inc');
@@ -218,6 +223,10 @@ class SettingsPage extends Vue {
         return this.$store.state.config.mode;
     }
 
+    get isExternalConverter() {
+        return this.$store.state.config.useExternalBookConverter;
+    }
+
     get settings() {
         return this.$store.state.reader.settings;
     }
@@ -544,7 +553,7 @@ class SettingsPage extends Vue {
     margin-bottom: 5px;
 }
 
-.label-1 {
+.label-1, .label-7 {
     width: 75px;
 }
 
@@ -556,7 +565,7 @@ class SettingsPage extends Vue {
     width: 100px;
 }
 
-.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
+.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
     display: flex;
     flex-direction: column;
     justify-content: center;

+ 87 - 0
client/components/Reader/SettingsPage/include/ConvertTab.inc

@@ -0,0 +1,87 @@
+<!---------------------------------------------->
+<div class="q-mt-sm column items-center">
+    <span>Настройки конвертирования применяются ко всем</span>
+    <span>вновь загружаемым или обновляемым файлам</span>
+</div>
+
+<!---------------------------------------------->
+<div class="part-header">HTML, XML, TXT</div>
+
+<div class="item row">
+    <div class="label-7">Текст</div>
+    <div class="col row">
+        <q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Опция принудительно включает эвристику разбиения текста на<br>
+                параграфы в случае, если формат файла определен как html,<br>
+                xml или txt. Возможна нечитабельная разметка текста.
+            </q-tooltip>
+        </q-checkbox>
+    </div>
+</div>
+
+<div class="item row">
+    <div class="label-7">Сайты</div>
+    <div class="col row">
+        <q-checkbox v-model="enableSitesFilter" size="xs" label="Включить html-фильтр для сайтов">
+            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                Html-фильтр вырезает лишние элементы со<br>
+                страницы для определенных сайтов, таких как:<br>
+                samlib.ru<br>
+                www.fanfiction.net<br>
+                archiveofourown.org<br>
+                и других
+            </q-tooltip>
+        </q-checkbox>
+    </div>
+</div>
+
+<!---------------------------------------------->
+<div v-if="isExternalConverter">
+    <div class="part-header">PDF</div>
+
+    <div class="item row">
+        <div class="label-7">Формат</div>
+        <div class="col row">
+            <q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
+                    Размер получаемого fb2-файла при этом относительно небольшой.<br>
+                    При отключении этой опции, pdf будет представлен как набор<br>
+                    изображений (аналогично ковертированию djvu).
+                </q-tooltip>
+            </q-checkbox>
+        </div>
+    </div>
+
+    <div class="item row">
+        <div class="label-7">Качество</div>
+        <div class="col row">
+            <NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
+                    размер итогового файла. Если сервер отказывается конвертировать<br>
+                    слишком большой файл, то попробуйте понизить качество.
+                </q-tooltip>
+            </NumInput>
+        </div>
+    </div>
+</div>
+
+<!---------------------------------------------->
+<div v-if="isExternalConverter">
+    <div class="part-header">DJVU</div>
+
+    <div class="item row">
+        <div class="label-7">Качество</div>
+        <div class="col row">
+            <NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
+                <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+                    Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
+                    размер итогового файла. Если сервер отказывается конвертировать<br>
+                    слишком большой файл, то попробуйте понизить качество.
+                </q-tooltip>
+            </NumInput>
+        </div>
+    </div>
+</div>

+ 0 - 16
client/components/Reader/SettingsPage/include/OthersTab.inc

@@ -65,22 +65,6 @@
 <!---------------------------------------------->
 <div class="part-header">Другое</div>
 
-<div class="item row">
-    <div class="label-6">Обработка</div>
-    <div class="col row">
-        <q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
-            <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
-                Html-фильтр вырезает лишние элементы со<br>
-                страницы для определенных сайтов, таких как:<br>
-                samlib.ru<br>
-                www.fanfiction.net<br>
-                archiveofourown.org<br>
-                и других
-            </q-tooltip>
-        </q-checkbox>
-    </div>
-</div>
-
 <div class="item row">
     <div class="label-6">Обработка</div>
     <q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">

+ 3 - 2
client/components/Reader/share/BookParser.js

@@ -205,6 +205,7 @@ export default class BookParser {
                 let attrs = sax.getAttrsSync(tail);
                 if (attrs.href && attrs.href.value) {
                     const href = attrs.href.value;
+                    const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
                     const {id, local} = this.imageHrefToId(href);
                     if (href[0] == '#') {//local
                         imageNum++;
@@ -214,7 +215,7 @@ export default class BookParser {
                         else
                             newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
 
-                        this.images.push({paraIndex, num: imageNum, id, local});
+                        this.images.push({paraIndex, num: imageNum, id, local, alt});
 
                         if (inPara && this.showInlineImagesInCenter)
                             newParagraph(' ', 1);
@@ -224,7 +225,7 @@ export default class BookParser {
                         dimPromises.push(getExternalImageDimensions(href));
                         newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
 
-                        this.images.push({paraIndex, num: imageNum, id, local});
+                        this.images.push({paraIndex, num: imageNum, id, local, alt});
                     }
                 }
             }

+ 14 - 0
client/components/Reader/versionHistory.js

@@ -1,4 +1,18 @@
 export const versionHistory = [
+{
+    showUntil: '2020-12-17',
+    header: '0.9.12 (2020-12-18)',
+    content:
+`
+<ul>
+    <li>добавлена вкладка "Изображения" в окно оглавления</li>
+    <li>настройки конвертирования вынесены в отдельную вкладку</li>
+    <li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
+    <li>улучшения работы конвертеров</li>
+</ul>
+`
+},
+
 {
     showUntil: '2020-12-08',
     header: '0.9.11 (2020-12-09)',

+ 8 - 5
client/store/modules/reader.js

@@ -12,7 +12,7 @@ const readerActions = {
     'setPosition': 'Установить позицию',
     'search': 'Найти в тексте',
     'copyText': 'Скопировать текст со страницы',
-    'splitToPara': 'Обновить с разбиением на параграфы',
+    'convOptions': 'Настроить конвертирование',
     'refresh': 'Принудительно обновить книгу',
     'offlineMode': 'Автономный режим (без интернета)',
     'contents': 'Оглавление/закладки',
@@ -41,7 +41,7 @@ const toolButtons = [
     {name: 'setPosition', show: true},
     {name: 'search',      show: true},
     {name: 'copyText',    show: false},
-    {name: 'splitToPara', show: false},
+    {name: 'convOptions', show: true},
     {name: 'refresh',     show: true},
     {name: 'contents',    show: true},
     {name: 'libs',        show: true},
@@ -60,8 +60,8 @@ const hotKeys = [
     {name: 'scrolling', codes: ['Z']},
     {name: 'setPosition', codes: ['P']},
     {name: 'search', codes: ['Ctrl+F']},
-    {name: 'copyText', codes: ['Ctrl+C']},
-    {name: 'splitToPara', codes: ['Shift+R']},
+    {name: 'copyText', codes: ['Ctrl+C']},    
+    {name: 'convOptions', codes: ['Ctrl+M']},
     {name: 'refresh', codes: ['R']},
     {name: 'contents', codes: ['C']},
     {name: 'libs', codes: ['L']},
@@ -252,11 +252,14 @@ const settingDefaults = {
     imageHeightLines: 100,
     imageFitWidth: true,
     enableSitesFilter: true,
+    splitToPara: false,
+    djvuQuality: 20,
+    pdfAsText: true,
+    pdfQuality: 20,
 
     showServerStorageMessages: true,
     showWhatsNewDialog: true,
     showDonationDialog2020: true,
-    showLiberamaTopDialog2020: true,
     showNeedUpdateNotify: true,
 
     fontShifts: {},

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "Liberama",
-  "version": "0.9.11",
+  "version": "0.9.12",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/liberama",

+ 4 - 1
server/controllers/ReaderController.js

@@ -20,9 +20,12 @@ class ReaderController extends BaseController {
             const workerId = this.readerWorker.loadBookUrl({
                 url: request.url, 
                 enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
-                skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
+                skipHtmlCheck: (request.hasOwnProperty('skipHtmlCheck') ? request.skipHtmlCheck : false),
                 isText: (request.hasOwnProperty('isText') ? request.isText : false),
                 uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
+                djvuQuality: (request.hasOwnProperty('djvuQuality') ? request.djvuQuality : false),
+                pdfAsText: (request.hasOwnProperty('pdfAsText') ? request.pdfAsText : false),
+                pdfQuality: (request.hasOwnProperty('pdfQuality') ? request.pdfQuality : false),
             });
             const state = this.workerState.getState(workerId);
             return (state ? state : {});

+ 1 - 1
server/core/Reader/BookConverter/ConvertBase.js

@@ -104,7 +104,7 @@ class ConvertBase {
     }
 
     isDataXml(data) {
-        const str = data.toString().trim();
+        const str = data.slice(0, 100).toString().trim();
         return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
     }
 

+ 49 - 5
server/core/Reader/BookConverter/ConvertDjvu.js

@@ -16,12 +16,21 @@ class ConvertDjvu extends ConvertJpegPng {
         if (!this.check(data, opts))
             return false;
 
-        const {inputFiles, callback, abort} = opts;
+        let {inputFiles, callback, abort, djvuQuality} = opts;
+
+        djvuQuality = (djvuQuality && djvuQuality <= 100 && djvuQuality >= 10 ? djvuQuality : 20);
+        let jpegQuality = djvuQuality;
+        let tiffQuality = djvuQuality + 30;
+        tiffQuality = (tiffQuality < 85 ? tiffQuality : 85);
 
         const ddjvuPath = '/usr/bin/ddjvu';
         if (!await fs.pathExists(ddjvuPath))
             throw new Error('Внешний конвертер ddjvu не найден');
 
+        const djvusedPath = '/usr/bin/djvused';
+        if (!await fs.pathExists(djvusedPath))
+            throw new Error('Внешний конвертер djvused не найден');
+
         const tiffsplitPath = '/usr/bin/tiffsplit';
         if (!await fs.pathExists(tiffsplitPath))
             throw new Error('Внешний конвертер tiffsplitPath не найден');
@@ -36,7 +45,7 @@ class ConvertDjvu extends ConvertJpegPng {
 
         //конвертируем в tiff
         let perc = 0;
-        await this.execConverter(ddjvuPath, ['-format=tiff', '-quality=50', '-verbose', inputFiles.sourceFile, tifFile], () => {
+        await this.execConverter(ddjvuPath, ['-format=tiff', `-quality=${tiffQuality}`, '-verbose', inputFiles.sourceFile, tifFile], () => {
             perc = (perc < 100 ? perc + 1 : 40);
             callback(perc);
         }, abort);
@@ -53,22 +62,57 @@ class ConvertDjvu extends ConvertJpegPng {
         await fs.remove(tifFile);
 
         //конвертируем в jpg
-        await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
+        await this.execConverter(mogrifyPath, ['-quality', jpegQuality, '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
             perc = (perc < 100 ? perc + 1 : 40);
             callback(perc);
         }, abort);
 
+        limitSize = 2*this.config.maxUploadFileSize;
+        let jpgFilesSize = 0;
         //ищем изображения
         let files = [];
         await utils.findFiles(async(file) => {
-            if (path.extname(file) == '.jpg')
+            if (path.extname(file) == '.jpg') {
+                jpgFilesSize += (await fs.stat(file)).size;
+                if (jpgFilesSize > limitSize) {
+                    throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
+                }
+
                 files.push({name: file, base: path.basename(file)});
+            }
         }, dir);
 
         files.sort((a, b) => a.base.localeCompare(b.base));
 
+        //схема документа (outline)
+        const djvusedResult = await this.execConverter(djvusedPath, ['-u', '-e', 'print-outline', inputFiles.sourceFile], null, abort);
+
+        const outline = [];
+        const lines = djvusedResult.stdout.match(/\(\s*".*"\s*?"#\d+"/g);
+        if (lines) {
+            lines.forEach(l => {
+                const m = l.match(/"(.*)"\s*?"#(\d+)"/);
+                if (m) {
+                    const pageNum = m[2];
+                    let s = outline[pageNum];
+                    if (!s)
+                        s = m[1].trim();
+                    else
+                        s += `${(s[s.length - 1] != '.' ? '.' : '')} ${m[1].trim()}`;
+
+                    outline[pageNum] = s;
+                }
+            });
+        }
+
         await utils.sleep(100);
-        return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
+        let i = 0;
+        const imageFiles = files.map(f => {
+            i++;
+            let alt = (outline[i] ? outline[i] : '');
+            return {src: f.name, alt};
+        });
+        return await super.run(data, Object.assign({}, opts, {imageFiles}));
     }
 }
 

+ 10 - 2
server/core/Reader/BookConverter/ConvertFb2.js

@@ -13,22 +13,30 @@ class ConvertFb2 extends ConvertBase {
     }
 
     async run(data, opts) {
-        let newData = data;
+        let newData = data.slice(0, 1024);
 
-        //Корректируем кодировку, 16-битные кодировки должны стать utf-8
+        //Корректируем кодировку для проверки, 16-битные кодировки должны стать utf-8
         const encoding = textUtils.getEncoding(newData);
         if (encoding.indexOf('UTF-16') == 0) {
             newData = Buffer.from(iconv.decode(newData, encoding));
         }
 
+        //Проверяем
         if (!this.check(newData, opts))
             return false;
 
+        //Корректируем кодировку всего объема
+        newData = data;
+        if (encoding.indexOf('UTF-16') == 0) {
+            newData = Buffer.from(iconv.decode(newData, encoding));
+        }
+
         //Корректируем пробелы, всякие файлы попадаются :(
         if (newData[0] == 32) {
             newData = Buffer.from(newData.toString().trim());
         }
 
+        //Окончательно корректируем кодировку
         return this.checkEncoding(newData);
     }
 

+ 1 - 1
server/core/Reader/BookConverter/ConvertFb3.js

@@ -45,7 +45,7 @@ class ConvertFb3 extends ConvertHtml {
             .replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
             .replace(/<\/subtitle>/g, '</fb2-subtitle>')
         ;
-        return await super.run(Buffer.from(text), {skipCheck: true});
+        return await super.run(Buffer.from(text), {skipHtmlCheck: true});
     }
 }
 

+ 6 - 5
server/core/Reader/BookConverter/ConvertHtml.js

@@ -16,7 +16,7 @@ class ConvertHtml extends ConvertBase {
         }
 
         //из буфера обмена?
-        if (data.toString().indexOf('<buffer>') == 0) {
+        if (data.slice(0, 50).toString().indexOf('<buffer>') == 0) {
             return {isText: false};
         }
 
@@ -24,15 +24,13 @@ class ConvertHtml extends ConvertBase {
     }
 
     async run(data, opts) {
-        let isText = false;
-        if (!opts.skipCheck) {
+        let {isText = false, uploadFileName = ''} = opts;
+        if (!opts.skipHtmlCheck) {
             const checkResult = this.check(data, opts);
             if (!checkResult)
                 return false;
 
             isText = checkResult.isText;
-        } else {
-            isText = opts.isText;
         }
 
         let titleInfo = {};
@@ -242,6 +240,9 @@ class ConvertHtml extends ConvertBase {
             innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
         });
 
+        if (!title)
+            title = uploadFileName;
+        
         titleInfo['book-title'] = title;
         if (author)
             titleInfo.author = {'last-name': author};

+ 11 - 6
server/core/Reader/BookConverter/ConvertJpegPng.js

@@ -27,7 +27,7 @@ class ConvertJpegPng extends ConvertBase {
         } else {
             const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
             await fs.copy(inputFiles.sourceFile, imageFile);
-            files.push(imageFile);
+            files.push({src: imageFile});
         }
 
         //читаем изображения
@@ -55,10 +55,9 @@ class ConvertJpegPng extends ConvertBase {
 
         let images = [];
         let loading = [];
-        files.forEach(f => {
-            const image = {src: f};
-            images.push(image);
-            loading.push(loadImage(image));
+        files.forEach(img => {
+            images.push(img);
+            loading.push(loadImage(img));
         });
 
         await Promise.all(loading);
@@ -82,8 +81,14 @@ class ConvertJpegPng extends ConvertBase {
                 const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data};
                 binary.push(img);
 
+                const attrs = {'l:href': `#${image.name}`};
+                if (image.alt) {
+                    image.alt = (image.alt.length > 256 ? image.alt.substring(0, 256) : image.alt);
+                    attrs.alt = image.alt;
+                }
+
                 pars.push({_n: 'p', _t: ''});
-                pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
+                pars.push({_n: 'image', _attrs: attrs});
             }
         }
         pars.push({_n: 'p', _t: ''});

+ 2 - 3
server/core/Reader/BookConverter/ConvertPdf.js

@@ -15,7 +15,7 @@ class ConvertPdf extends ConvertHtml {
     }
 
     async run(notUsed, opts) {
-        if (!this.check(notUsed, opts))
+        if (!opts.pdfAsText || !this.check(notUsed, opts))
             return false;
 
         await this.checkExternalConverterPresent();
@@ -27,7 +27,6 @@ class ConvertPdf extends ConvertHtml {
         const outFile = `${outBasename}.xml`;
 
         const pdftohtmlPath = '/usr/bin/pdftohtml';
-
         if (!await fs.pathExists(pdftohtmlPath))
             throw new Error('Внешний конвертер pdftohtml не найден');
 
@@ -342,7 +341,7 @@ class ConvertPdf extends ConvertHtml {
 
         //console.log(text);
         await utils.sleep(100);
-        return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
+        return await super.run(Buffer.from(text), {skipHtmlCheck: true, isText: true});
     }
 
     async getPdfTitleAndAuthor(pdfFile) {

+ 115 - 0
server/core/Reader/BookConverter/ConvertPdfImages.js

@@ -0,0 +1,115 @@
+const fs = require('fs-extra');
+const path = require('path');
+const utils = require('../../utils');
+
+const sax = require('../../sax');
+
+const ConvertJpegPng = require('./ConvertJpegPng');
+
+class ConvertPdfImages extends ConvertJpegPng {
+    check(data, opts) {
+        const {inputFiles} = opts;
+
+        return this.config.useExternalBookConverter && 
+            inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'pdf';
+    }
+
+    async run(data, opts) {
+        if (!this.check(data, opts))
+            return false;
+
+        let {inputFiles, callback, abort, pdfQuality} = opts;
+        
+        pdfQuality = (pdfQuality && pdfQuality <= 100 && pdfQuality >= 10 ? pdfQuality : 20);
+
+        const pdftoppmPath = '/usr/bin/pdftoppm';
+        if (!await fs.pathExists(pdftoppmPath))
+            throw new Error('Внешний конвертер pdftoppm не найден');
+
+        const pdftohtmlPath = '/usr/bin/pdftohtml';
+        if (!await fs.pathExists(pdftohtmlPath))
+            throw new Error('Внешний конвертер pdftohtml не найден');
+
+        const inpFile = inputFiles.sourceFile;
+        const dir = `${inputFiles.filesDir}/`;
+        const outBasename = `${dir}${utils.randomHexString(10)}`;
+        const outFile = `${outBasename}.tmp`;
+
+        //конвертируем в jpeg
+        let perc = 0;
+        await this.execConverter(pdftoppmPath, ['-jpeg', '-jpegopt', `quality=${pdfQuality},progressive=y`, inpFile, outFile], () => {
+            perc = (perc < 100 ? perc + 1 : 40);
+            callback(perc);
+        }, abort);
+
+        const limitSize = 2*this.config.maxUploadFileSize;
+        let jpgFilesSize = 0;
+
+        //ищем изображения
+        let files = [];
+        await utils.findFiles(async(file) => {
+            if (path.extname(file) == '.jpg') {
+                jpgFilesSize += (await fs.stat(file)).size;
+                if (jpgFilesSize > limitSize) {
+                    throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
+                }
+
+                files.push({name: file, base: path.basename(file)});
+            }
+        }, dir);
+
+        files.sort((a, b) => a.base.localeCompare(b.base));
+
+        //схема документа (outline)
+        const outXml = `${outBasename}.xml`;
+        await this.execConverter(pdftohtmlPath, ['-nodrm', '-i', '-c', '-s', '-xml', inpFile, outXml], null, abort);
+        const outline = [];
+
+        let inOutline = 0;
+        let inItem = false;
+        let pageNum = 0;
+
+        const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (inOutline > 0 && inItem && pageNum) {
+                outline[pageNum] = text;
+            }
+        };
+
+        const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (tag == 'outline')
+                inOutline++;
+
+            if (inOutline > 0 && tag == 'item') {
+                const attrs = sax.getAttrsSync(tail);
+                pageNum = (attrs.page && attrs.page.value ? attrs.page.value : 0);
+                inItem = true;
+            }
+        };
+
+        const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (tag == 'outline')
+                inOutline--;
+            if (tag == 'item')
+                inItem = false;
+        };
+
+        const dataXml = await fs.readFile(outXml);
+        const buf = this.decode(dataXml).toString();
+        sax.parseSync(buf, {
+            onStartNode, onEndNode, onTextNode
+        });
+
+
+        await utils.sleep(100);
+        //формируем список файлов
+        let i = 0;
+        const imageFiles = files.map(f => {
+            i++;
+            let alt = (outline[i] ? outline[i] : '');
+            return {src: f.name, alt};
+        });
+        return await super.run(data, Object.assign({}, opts, {imageFiles}));
+    }
+}
+
+module.exports = ConvertPdfImages;

+ 1 - 1
server/core/Reader/BookConverter/ConvertSites.js

@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
         if (text === false)
             return false;
 
-        return await super.run(Buffer.from(text), {skipCheck: true});
+        return await super.run(Buffer.from(text), {skipHtmlCheck: true});
     }
 
     getTitle(text) {

+ 1 - 0
server/core/Reader/BookConverter/index.js

@@ -7,6 +7,7 @@ const convertClassFactory = [
     require('./ConvertEpub'),
     require('./ConvertDjvu'),
     require('./ConvertPdf'),
+    require('./ConvertPdfImages'),
     require('./ConvertRtf'),
     require('./ConvertDocX'),
     require('./ConvertFb3'),