Bladeren bron

Merge branch 'release/0.9.11-1'

Book Pauk 4 jaren geleden
bovenliggende
commit
6074c4b7bd

+ 22 - 12
client/components/App.vue

@@ -2,7 +2,7 @@
     <div class="fit row">
     <div class="fit row">
         <Notify ref="notify"/>
         <Notify ref="notify"/>
         <StdDialog ref="stdDialog"/>
         <StdDialog ref="stdDialog"/>
-        <keep-alive>
+        <keep-alive v-if="showPage">
             <router-view class="col"></router-view>
             <router-view class="col"></router-view>
         </keep-alive>
         </keep-alive>
     </div>
     </div>
@@ -12,8 +12,11 @@
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 import Vue from 'vue';
 import Vue from 'vue';
 import Component from 'vue-class-component';
 import Component from 'vue-class-component';
+
 import Notify from './share/Notify.vue';
 import Notify from './share/Notify.vue';
 import StdDialog from './share/StdDialog.vue';
 import StdDialog from './share/StdDialog.vue';
+
+import miscApi from '../api/misc';
 import * as utils from '../share/utils';
 import * as utils from '../share/utils';
 
 
 export default @Component({
 export default @Component({
@@ -30,6 +33,8 @@ export default @Component({
 
 
 })
 })
 class App extends Vue {
 class App extends Vue {
+    showPage = false;
+
     itemRuText = {
     itemRuText = {
         '/cardindex': 'Картотека',
         '/cardindex': 'Картотека',
         '/reader': 'Читалка',
         '/reader': 'Читалка',
@@ -42,7 +47,6 @@ class App extends Vue {
 
 
     created() {
     created() {
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
-        this.dispatch = this.$store.dispatch;
         this.state = this.$store.state;
         this.state = this.$store.state;
         this.uistate = this.$store.state.uistate;
         this.uistate = this.$store.state.uistate;
         this.config = this.$store.state.config;
         this.config = this.$store.state.config;
@@ -116,18 +120,24 @@ class App extends Vue {
         this.$root.notify = this.$refs.notify;
         this.$root.notify = this.$refs.notify;
         this.$root.stdDialog = this.$refs.stdDialog;
         this.$root.stdDialog = this.$refs.stdDialog;
 
 
-        this.dispatch('config/loadConfig');
-        this.$watch('apiError', function(newError) {
-            if (newError) {
-                let mes = newError.message;
-                if (newError.response && newError.response.config)
-                    mes = newError.response.config.url + '<br>' + newError.response.statusText;
-                this.$root.notify.error(mes, 'Ошибка API');
-            }
-        });
-
         this.setAppTitle();
         this.setAppTitle();
         (async() => {
         (async() => {
+            //загрузим конфиг сревера
+            try {
+                const config = await miscApi.loadConfig();
+                this.commit('config/setConfig', config);
+                this.showPage = true;
+            } catch(e) {
+                //проверим, не получен ли конфиг ранее
+                if (!this.mode) {
+                    this.$root.notify.error(e.message, 'Ошибка API');
+                } else {
+                    //вероятно, работаем в оффлайне
+                    this.showPage = true;
+                }
+                console.error(e);
+            }
+
             //запросим persistent storage
             //запросим persistent storage
             if (navigator.storage && navigator.storage.persist) {
             if (navigator.storage && navigator.storage.persist) {
                 navigator.storage.persist();
                 navigator.storage.persist();

+ 50 - 1
client/components/Reader/Reader.vue

@@ -133,6 +133,9 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
 import bookManager from './share/bookManager';
 import bookManager from './share/bookManager';
 import rstore from '../../store/modules/reader';
 import rstore from '../../store/modules/reader';
 import readerApi from '../../api/reader';
 import readerApi from '../../api/reader';
+import miscApi from '../../api/misc';
+
+import {versionHistory} from './versionHistory';
 import * as utils from '../../share/utils';
 import * as utils from '../../share/utils';
 
 
 export default @Component({
 export default @Component({
@@ -229,7 +232,6 @@ class Reader extends Vue {
         this.rstore = rstore;
         this.rstore = rstore;
         this.loading = true;
         this.loading = true;
         this.commit = this.$store.commit;
         this.commit = this.$store.commit;
-        this.dispatch = this.$store.dispatch;
         this.reader = this.$store.state.reader;
         this.reader = this.$store.state.reader;
         this.config = this.$store.state.config;
         this.config = this.$store.state.config;
 
 
@@ -293,6 +295,16 @@ class Reader extends Vue {
 
 
             await this.$refs.dialogs.init();
             await this.$refs.dialogs.init();
         })();
         })();
+
+        (async() => {
+            this.isFirstNeedUpdateNotify = true;
+            //вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
+            while (true) {// eslint-disable-line no-constant-condition
+                await this.checkNewVersionAvailable();
+                await utils.sleep(3600*1000); //каждый час
+            }
+            //дальше кода нет
+        })();
     }
     }
 
 
     loadSettings() {
     loadSettings() {
@@ -304,6 +316,7 @@ class Reader extends Vue {
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.blinkCachedLoad = settings.blinkCachedLoad;
         this.showToolButton = settings.showToolButton;
         this.showToolButton = settings.showToolButton;
         this.enableSitesFilter = settings.enableSitesFilter;
         this.enableSitesFilter = settings.enableSitesFilter;
+        this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
 
 
         this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
         this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
         this.$root.readerActionByKeyEvent = (event) => {
         this.$root.readerActionByKeyEvent = (event) => {
@@ -313,6 +326,30 @@ class Reader extends Vue {
         this.updateHeaderMinWidth();
         this.updateHeaderMinWidth();
     }
     }
 
 
+    async checkNewVersionAvailable() {
+        if (!this.checkingNewVersion && this.showNeedUpdateNotify) {
+            this.checkingNewVersion = true;
+            try {
+                await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
+                const config = await miscApi.loadConfig();
+                this.commit('config/setConfig', config);
+
+                let againMes = '';
+                if (this.isFirstNeedUpdateNotify) {
+                    againMes = ' ЕЩЕ один раз';
+                }
+
+                if (this.version != this.clientVersion)
+                    this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
+            } catch(e) {
+                console.error(e);
+            } finally {
+                this.checkingNewVersion = false;
+            }
+        }
+        this.isFirstNeedUpdateNotify = false;
+    }
+
     updateHeaderMinWidth() {
     updateHeaderMinWidth() {
         const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
         const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
         if (this.$refs.buttons)
         if (this.$refs.buttons)
@@ -394,6 +431,16 @@ class Reader extends Vue {
         return this.$store.state.config.mode;
         return this.$store.state.config.mode;
     }
     }
 
 
+    get version() {
+        return this.$store.state.config.version;
+    }
+
+    get clientVersion() {
+        let v = versionHistory[0].header;
+        v = v.split(' ')[0];
+        return v;
+    }
+
     get routeParamUrl() {
     get routeParamUrl() {
         let result = '';
         let result = '';
         const path = this.$route.fullPath;
         const path = this.$route.fullPath;
@@ -963,6 +1010,8 @@ class Reader extends Vue {
             progress.hide(); this.progressActive = false;
             progress.hide(); this.progressActive = false;
             this.loaderActive = true;
             this.loaderActive = true;
             this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
             this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
+        } finally {
+            this.checkNewVersionAvailable();
         }
         }
     }
     }
 
 

+ 12 - 1
client/components/Reader/SettingsPage/include/OthersTab.inc

@@ -36,7 +36,18 @@
         Показывать уведомление "Что нового"
         Показывать уведомление "Что нового"
         <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
         <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
             Показывать уведомления "Что нового"<br>
             Показывать уведомления "Что нового"<br>
-            при каждом выходе новой версии читалки
+            при появлении новой версии читалки
+        </q-tooltip>
+    </q-checkbox>
+</div>
+
+<div class="item row">
+    <div class="label-6">Уведомление</div>
+    <q-checkbox size="xs" v-model="showNeedUpdateNotify">
+        Показывать уведомление о новой версии
+        <q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
+            Напоминать о необходимости обновления страницы<br>
+            при появлении новой версии читалки
         </q-tooltip>
         </q-tooltip>
     </q-checkbox>
     </q-checkbox>
 </div>
 </div>

+ 5 - 0
client/components/Reader/share/BookParser.js

@@ -304,6 +304,11 @@ export default class BookParser {
                     bold = true;
                     bold = true;
                     center = true;
                     center = true;
 
 
+                    if (curTitle.paraIndex < 0) {
+                        curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
+                        this.contents.push(curTitle);
+                    }
+
                     inSubtitle = true;
                     inSubtitle = true;
                     curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
                     curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
                     curTitle.subtitles.push(curSubtitle);
                     curTitle.subtitles.push(curSubtitle);

+ 3 - 7
client/components/Reader/share/bookManager.js

@@ -410,16 +410,12 @@ class BookManager {
     }
     }
 
 
     async setRecentBook(value) {
     async setRecentBook(value) {
-        const result = this.metaOnly(value);
+        let result = this.metaOnly(value);
         result.touchTime = Date.now();
         result.touchTime = Date.now();
         result.deleted = 0;
         result.deleted = 0;
 
 
-        if (this.recent[result.key] && this.recent[result.key].deleted) {
-            //восстановим из небытия пользовательские данные
-            if (!result.bookPos)
-                result.bookPos = this.recent[result.key].bookPos;
-            if (!result.bookPosSeen)
-                result.bookPosSeen = this.recent[result.key].bookPosSeen;
+        if (this.recent[result.key]) {
+            result = Object.assign({}, this.recent[result.key], result);
         }
         }
 
 
         await this.recentSetLastKey(result.key);
         await this.recentSetLastKey(result.key);

+ 1 - 12
client/store/modules/config.js

@@ -10,18 +10,7 @@ const state = {
 const getters = {};
 const getters = {};
 
 
 // actions
 // actions
-const actions = {
-    async loadConfig({ commit, state }) {
-        commit('setApiError', null, { root: true });
-        commit('setConfig', {});
-        try {
-            const config = await miscApi.loadConfig();
-            commit('setConfig', config);
-        } catch (e) {
-            commit('setApiError', e, { root: true });
-        }
-    },
-};
+const actions = {};
 
 
 // mutations
 // mutations
 const mutations = {
 const mutations = {

+ 3 - 1
client/store/modules/reader.js

@@ -251,11 +251,13 @@ const settingDefaults = {
     compactTextPerc: 0,
     compactTextPerc: 0,
     imageHeightLines: 100,
     imageHeightLines: 100,
     imageFitWidth: true,
     imageFitWidth: true,
+    enableSitesFilter: true,
+
     showServerStorageMessages: true,
     showServerStorageMessages: true,
     showWhatsNewDialog: true,
     showWhatsNewDialog: true,
     showDonationDialog2020: true,
     showDonationDialog2020: true,
     showLiberamaTopDialog2020: true,
     showLiberamaTopDialog2020: true,
-    enableSitesFilter: true,
+    showNeedUpdateNotify: true,
 
 
     fontShifts: {},
     fontShifts: {},
     showToolButton: {},
     showToolButton: {},

+ 1 - 13
docs/omnireader.ru/README.md

@@ -32,23 +32,11 @@ sudo -u www-data mkdir -p /home/liberama/data/calibre
 sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
 sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
 ```
 ```
 
 
-### external converter `pdfalto`, github https://github.com/kermitt2/pdfalto
-```
-git clone https://github.com/kermitt2/pdfalto
-cd pdfalto
-git submodule update --init --recursive
-cmake ./
-добавить в начало CMakeLists.txt строчку: set(CMAKE_EXE_LINKER_FLAGS "-no-pie")
-make
-
-sudo -u www-data mkdir -p /home/liberama/data/pdfalto
-sudo -u www-data cp pdfalto /home/liberama/data/pdfalto
-```
-
 ### external converters
 ### external converters
 ```
 ```
 sudo apt install rar
 sudo apt install rar
 sudo apt install libreoffice
 sudo apt install libreoffice
+sudo apt install poppler-utils
 sudo apt install djvulibre-bin
 sudo apt install djvulibre-bin
 sudo apt install libtiff-tools
 sudo apt install libtiff-tools
 sudo apt install graphicsmagick-imagemagick-compat
 sudo apt install graphicsmagick-imagemagick-compat

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

@@ -70,6 +70,7 @@ class ConvertBase {
                 const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
                 const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
                 throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
                 throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
             }
             }
+            return result;
         } catch(e) {
         } catch(e) {
             if (e.status == 'killed') {
             if (e.status == 'killed') {
                 throw new Error('Слишком долгое ожидание конвертера');
                 throw new Error('Слишком долгое ожидание конвертера');

+ 11 - 55
server/core/Reader/BookConverter/ConvertDjvu.js

@@ -2,9 +2,9 @@ const fs = require('fs-extra');
 const path = require('path');
 const path = require('path');
 const utils = require('../../utils');
 const utils = require('../../utils');
 
 
-const ConvertBase = require('./ConvertBase');
+const ConvertJpegPng = require('./ConvertJpegPng');
 
 
-class ConvertDjvu extends ConvertBase {
+class ConvertDjvu extends ConvertJpegPng {
     check(data, opts) {
     check(data, opts) {
         const {inputFiles} = opts;
         const {inputFiles} = opts;
 
 
@@ -16,7 +16,7 @@ class ConvertDjvu extends ConvertBase {
         if (!this.check(data, opts))
         if (!this.check(data, opts))
             return false;
             return false;
 
 
-        const {inputFiles, callback, abort, uploadFileName} = opts;
+        const {inputFiles, callback, abort} = opts;
 
 
         const ddjvuPath = '/usr/bin/ddjvu';
         const ddjvuPath = '/usr/bin/ddjvu';
         if (!await fs.pathExists(ddjvuPath))
         if (!await fs.pathExists(ddjvuPath))
@@ -31,8 +31,8 @@ class ConvertDjvu extends ConvertBase {
             throw new Error('Внешний конвертер mogrifyPath не найден');
             throw new Error('Внешний конвертер mogrifyPath не найден');
 
 
         const dir = `${inputFiles.filesDir}/`;
         const dir = `${inputFiles.filesDir}/`;
-        const inpFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
-        const tifFile = `${inpFile}.tif`;
+        const baseFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
+        const tifFile = `${baseFile}.tif`;
 
 
         //конвертируем в tiff
         //конвертируем в tiff
         let perc = 0;
         let perc = 0;
@@ -42,9 +42,9 @@ class ConvertDjvu extends ConvertBase {
         }, abort);
         }, abort);
 
 
         const tifFileSize = (await fs.stat(tifFile)).size;
         const tifFileSize = (await fs.stat(tifFile)).size;
-        let limitSize = 3*this.config.maxUploadFileSize;
+        let limitSize = 4*this.config.maxUploadFileSize;
         if (tifFileSize > limitSize) {
         if (tifFileSize > limitSize) {
-            throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
+            throw new Error(`Файл для конвертирования слишком большой|FORLOG| tifFileSize: ${tifFileSize} > ${limitSize}`);
         }
         }
 
 
         //разбиваем на файлы
         //разбиваем на файлы
@@ -53,25 +53,12 @@ class ConvertDjvu extends ConvertBase {
         await fs.remove(tifFile);
         await fs.remove(tifFile);
 
 
         //конвертируем в jpg
         //конвертируем в jpg
-        await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
+        await this.execConverter(mogrifyPath, ['-quality', '20', '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
             perc = (perc < 100 ? perc + 1 : 40);
             perc = (perc < 100 ? perc + 1 : 40);
             callback(perc);
             callback(perc);
         }, abort);
         }, abort);
 
 
-        //читаем изображения
-        limitSize = 2*this.config.maxUploadFileSize;
-        let imagesSize = 0;
-
-        const loadImage = async(image) => {
-            image.data = (await fs.readFile(image.file)).toString('base64');
-            image.name = path.basename(image.file);
-
-            imagesSize += image.data.length;
-            if (imagesSize > limitSize) {
-                throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
-            }
-        }
-
+        //ищем изображения
         let files = [];
         let files = [];
         await utils.findFiles(async(file) => {
         await utils.findFiles(async(file) => {
             if (path.extname(file) == '.jpg')
             if (path.extname(file) == '.jpg')
@@ -80,39 +67,8 @@ class ConvertDjvu extends ConvertBase {
 
 
         files.sort((a, b) => a.base.localeCompare(b.base));
         files.sort((a, b) => a.base.localeCompare(b.base));
 
 
-        let images = [];
-        let loading = [];
-        files.forEach(f => {
-            const image = {file: f.name};
-            images.push(image);
-            loading.push(loadImage(image));
-        });
-
-        await Promise.all(loading);
-
-        //формируем fb2
-        let titleInfo = {};
-        let desc = {_n: 'description', 'title-info': titleInfo};
-        let pars = [];
-        let body = {_n: 'body', section: {_a: [pars]}};
-        let binary = [];
-        let fb2 = [desc, body, binary];
-
-        let title = '';
-        if (uploadFileName)
-            title = uploadFileName;
-
-        titleInfo['book-title'] = title;
-
-        for (const image of images) {
-            const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': 'image/jpeg'}, _t: image.data};
-            binary.push(img);
-
-            pars.push({_n: 'p', _t: ''});
-            pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
-        }
-
-        return this.formatFb2(fb2);
+        await utils.sleep(100);
+        return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
     }
     }
 }
 }
 
 

+ 95 - 0
server/core/Reader/BookConverter/ConvertJpegPng.js

@@ -0,0 +1,95 @@
+const fs = require('fs-extra');
+const path = require('path');
+//const utils = require('../../utils');
+
+const ConvertBase = require('./ConvertBase');
+
+class ConvertJpegPng extends ConvertBase {
+    check(data, opts) {
+        const {inputFiles} = opts;
+
+        return this.config.useExternalBookConverter && 
+            inputFiles.sourceFileType && 
+            (inputFiles.sourceFileType.ext == 'jpg' || inputFiles.sourceFileType.ext == 'png' );
+    }
+
+    async run(data, opts) {
+        const {inputFiles, uploadFileName, imageFiles} = opts;
+
+        if (!imageFiles) {
+            if (!this.check(data, opts))
+                return false;
+        }
+
+        let files = [];
+        if (imageFiles) {
+            files = imageFiles;
+        } else {
+            const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
+            await fs.copy(inputFiles.sourceFile, imageFile);
+            files.push(imageFile);
+        }
+
+        //читаем изображения
+        const limitSize = 2*this.config.maxUploadFileSize;
+        let imagesSize = 0;
+
+        const loadImage = async(image) => {
+            const src = path.parse(image.src);
+            let type = 'unknown';
+            switch (src.ext) {
+                case '.jpg': type = 'image/jpeg'; break;
+                case '.png': type = 'image/png'; break;
+            }
+            if (type != 'unknown') {
+                image.data = (await fs.readFile(image.src)).toString('base64');
+                image.type = type;
+                image.name = src.base;
+
+                imagesSize += image.data.length;
+                if (imagesSize > limitSize) {
+                    throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
+                }
+            }
+        }
+
+        let images = [];
+        let loading = [];
+        files.forEach(f => {
+            const image = {src: f};
+            images.push(image);
+            loading.push(loadImage(image));
+        });
+
+        await Promise.all(loading);
+
+        //формируем fb2
+        let titleInfo = {};
+        let desc = {_n: 'description', 'title-info': titleInfo};
+        let pars = [];
+        let body = {_n: 'body', section: {_a: [pars]}};
+        let binary = [];
+        let fb2 = [desc, body, binary];
+
+        let title = '';
+        if (uploadFileName)
+            title = uploadFileName;
+
+        titleInfo['book-title'] = title;
+
+        for (const image of images) {
+            if (image.type) {
+                const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data};
+                binary.push(img);
+
+                pars.push({_n: 'p', _t: ''});
+                pars.push({_n: 'image', _attrs: {'l:href': `#${image.name}`}});
+            }
+        }
+        pars.push({_n: 'p', _t: ''});
+        
+        return this.formatFb2(fb2);
+    }
+}
+
+module.exports = ConvertJpegPng;

+ 125 - 98
server/core/Reader/BookConverter/ConvertPdf.js

@@ -5,7 +5,6 @@ const path = require('path');
 const sax = require('../../sax');
 const sax = require('../../sax');
 const utils = require('../../utils');
 const utils = require('../../utils');
 const ConvertHtml = require('./ConvertHtml');
 const ConvertHtml = require('./ConvertHtml');
-const xmlParser = require('../../xmlParser');
 
 
 class ConvertPdf extends ConvertHtml {
 class ConvertPdf extends ConvertHtml {
     check(data, opts) {
     check(data, opts) {
@@ -26,16 +25,15 @@ class ConvertPdf extends ConvertHtml {
         const inpFile = inputFiles.sourceFile;
         const inpFile = inputFiles.sourceFile;
         const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
         const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
         const outFile = `${outBasename}.xml`;
         const outFile = `${outBasename}.xml`;
-        const metaFile = `${outBasename}_metadata.xml`;
 
 
-        const pdfaltoPath = `${this.config.dataDir}/pdfalto/pdfalto`;
+        const pdftohtmlPath = '/usr/bin/pdftohtml';
 
 
-        if (!await fs.pathExists(pdfaltoPath))
-            throw new Error('Внешний конвертер pdfalto не найден');
+        if (!await fs.pathExists(pdftohtmlPath))
+            throw new Error('Внешний конвертер pdftohtml не найден');
 
 
         //конвертируем в xml
         //конвертируем в xml
         let perc = 0;
         let perc = 0;
-        await this.execConverter(pdfaltoPath, [inpFile, outFile], () => {
+        await this.execConverter(pdftohtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
             perc = (perc < 80 ? perc + 10 : 40);
             perc = (perc < 80 ? perc + 10 : 40);
             callback(perc);
             callback(perc);
         }, abort);
         }, abort);
@@ -57,8 +55,10 @@ class ConvertPdf extends ConvertHtml {
         let images = [];
         let images = [];
         let loading = [];
         let loading = [];
 
 
-        let title = '';
-        let author = '';
+        let inText = false;
+        let bold = false;
+        let italic = false;
+
         let i = -1;
         let i = -1;
 
 
         const loadImage = async(image) => {
         const loadImage = async(image) => {
@@ -85,22 +85,30 @@ class ConvertPdf extends ConvertHtml {
             }
             }
         };
         };
 
 
+        const isTextBold = (text) => {
+            const m = text.trim().match(/^<b>(.*)<\/b>$/);
+            return m && !m[1].match(/<b>|<\/b>|<i>|<\/i>/g);
+        };
+
+        const isTextEmpty = (text) => {
+            return text.replace(/<b>|<\/b>|<i>|<\/i>/g, '').trim() == '';
+        };
+
         const putPageLines = () => {
         const putPageLines = () => {
-            pagelines.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left))
+            pagelines.sort((a, b) => (Math.abs(a.top - b.top) > 3 ? a.top - b.top : 0)*10000 + (a.left - b.left))
             
             
             //объединяем в одну строку равные по высоте
             //объединяем в одну строку равные по высоте
             const pl = [];
             const pl = [];
             let pt = 0;
             let pt = 0;
             let j = -1;
             let j = -1;
             pagelines.forEach(line => {
             pagelines.forEach(line => {
-                //добавим закрывающий тег стиля
-                line.text += line.tClose;
+                if (isTextEmpty(line.text))
+                    return;
 
 
                 //проверим, возможно это заголовок
                 //проверим, возможно это заголовок
-                if (line.fonts.length == 1 && line.pageWidth) {
-                    const f = (line.fonts.length ? fonts[line.fonts[0]] : null);
+                if (line.fontId && line.pageWidth) {
                     const centerLeft = (line.pageWidth - line.width)/2;
                     const centerLeft = (line.pageWidth - line.width)/2;
-                    if (f && f.isBold && Math.abs(centerLeft - line.left) < 3) {
+                    if (isTextBold(line.text) && Math.abs(centerLeft - line.left) < 10) {
                         if (!sectionTitleFound) {
                         if (!sectionTitleFound) {
                             line.isSectionTitle = true;
                             line.isSectionTitle = true;
                             sectionTitleFound = true;
                             sectionTitleFound = true;
@@ -128,8 +136,8 @@ class ConvertPdf extends ConvertHtml {
                 //добавим пустую строку, если надо
                 //добавим пустую строку, если надо
                 const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
                 const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
                 if (prevLine && !prevLine.isImage) {
                 if (prevLine && !prevLine.isImage) {
-                    const f = (prevLine.fonts.length ? fonts[prevLine.fonts[0]] : (line.fonts.length ? fonts[line.fonts[0]] : null));
-                    if (f && f.fontSize && !line.isImage && line.top - prevLine.top > f.fontSize*1.8) {
+                    const f = (prevLine.fontId ? fonts[prevLine.fontId] : (line.fontId ? fonts[line.fontId] : null));
+                    if (f && f.fontSize && !line.isImage && line.top - prevLine.top > f.fontSize * 1.8) {
                         i++;
                         i++;
                         lines[i] = {text: '<br>'};
                         lines[i] = {text: '<br>'};
                     }
                     }
@@ -142,29 +150,26 @@ class ConvertPdf extends ConvertHtml {
             putImage(100000);
             putImage(100000);
         };
         };
 
 
-        const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
-            if (tag == 'textstyle') {
-                const attrs = sax.getAttrsSync(tail);
-                const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
-                const fontStyle = (attrs.fontstyle && attrs.fontstyle.value ? attrs.fontstyle.value : '');
-                const fontSize = (attrs.fontsize && attrs.fontsize.value ? attrs.fontsize.value : '');
+        const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (!cutCounter && inText) {
+                let tOpen = (bold ? '<b>' : '');
+                tOpen += (italic ? '<i>' : '');
+                let tClose = (italic ? '</i>' : '');
+                tClose += (bold ? '</b>' : '');
 
 
-                if (fontId) {
-                    const styleTags = {bold: 'b', italics: 'i', superscript: 'sup', subscript: 'sub'};
-                    const f = fonts[fontId] = {tOpen: '', tClose: '', isBold: false, fontSize};
-
-                    if (fontStyle) {
-                        const styles = fontStyle.split(' ');
-                        styles.forEach(style => {
-                            const s = styleTags[style];
-                            if (s) {
-                                f.tOpen += `<${s}>`;
-                                f.tClose = `</${s}>${f.tClose}`;
-                                if (s == 'b')
-                                    f.isBold = true;
-                            }
-                        });
-                    }
+                line.text += ` ${tOpen}${text}${tClose}`;
+            }
+        };
+
+        const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (inText) {
+                switch (tag) {
+                    case 'i':
+                        italic = true;
+                        break;
+                    case 'b':
+                        bold = true;
+                        break;
                 }
                 }
             }
             }
 
 
@@ -177,80 +182,78 @@ class ConvertPdf extends ConvertHtml {
                 putPageLines();
                 putPageLines();
             }
             }
 
 
-            if (tag == 'textline') {
+            if (tag == 'fontspec') {
+                const attrs = sax.getAttrsSync(tail);
+                const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
+                const fontSize = (attrs.size && attrs.size.value ? attrs.size.value : '');
+
+                if (fontId) {
+                    fonts[fontId] = {fontSize};
+
+                }
+            }
+
+            if (tag == 'text' && !inText) {
                 const attrs = sax.getAttrsSync(tail);
                 const attrs = sax.getAttrsSync(tail);
                 line = {
                 line = {
                     text: '',
                     text: '',
-                    top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10),
-                    left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10),
+                    top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
+                    left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10),
                     width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
                     width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
                     height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
                     height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
-                    tOpen: '',
-                    tClose: '',
                     isSectionTitle: false,
                     isSectionTitle: false,
                     isSubtitle: false,
                     isSubtitle: false,
                     pageWidth: page.width,
                     pageWidth: page.width,
-                    fonts: [],
+                    fontId: (attrs.font && attrs.font.value ? attrs.font.value : ''),
                 };
                 };
 
 
                 if (line.width != 0 || line.height != 0) {
                 if (line.width != 0 || line.height != 0) {
+                    inText = true;
                     pagelines.push(line);
                     pagelines.push(line);
                 }
                 }
             }
             }
 
 
-            if (tag == 'string') {
+            if (tag == 'image') {
                 const attrs = sax.getAttrsSync(tail);
                 const attrs = sax.getAttrsSync(tail);
-                if (attrs.content && attrs.content.value) {
-
-                    let tOpen = '';
-                    let tClose = '';
-                    const fontId = (attrs.stylerefs && attrs.stylerefs.value ? attrs.stylerefs.value : '');
-                    if (fontId && fonts[fontId]) {
-                        tOpen = fonts[fontId].tOpen;
-                        tClose = fonts[fontId].tClose;
-                        if (!line.fonts.length || line.fonts[0] != fontId)
-                            line.fonts.push(fontId);
-                    }
-
-                    if (line.tOpen != tOpen) {
-                        line.text += line.tClose + tOpen;
-                        line.tOpen = tOpen;
-                        line.tClose = tClose;
-                    }
-
-                    line.text += `${line.text.length ? ' ' : ''}${attrs.content.value}`;
+                let src = (attrs.src && attrs.src.value ? attrs.src.value : '');
+                if (src) {
+                    const image = {
+                        isImage: true,
+                        src,
+                        data: '',
+                        type: '',
+                        top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
+                        left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10) || 0,
+                        width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10) || 0,
+                        height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10) || 0,
+                    };
+
+                    loading.push(loadImage(image));
+                    images.push(image);
+                    images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
                 }
                 }
             }
             }
+        };
 
 
-            if (tag == 'illustration') {
-                const attrs = sax.getAttrsSync(tail);
-                if (attrs.type && attrs.type.value == 'image') {
-                    let src = (attrs.fileid && attrs.fileid.value ? attrs.fileid.value : '');
-                    if (src) {
-                        const image = {
-                            isImage: true,
-                            src,
-                            data: '',
-                            type: '',
-                            top: parseInt((attrs.vpos && attrs.vpos.value ? attrs.vpos.value : null), 10) || 0,
-                            left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.value : null), 10) || 0,
-                            width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10) || 0,
-                            height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10) || 0,
-                        };
-                        const exists = images.filter(img => (img.top == image.top && img.left == image.left && img.width == image.width && img.height == image.height));
-                        if (!exists.length) {
-                            loading.push(loadImage(image));
-                            images.push(image);
-                            images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
-                        }
-                    }
+        const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
+            if (inText) {
+                switch (tag) {
+                    case 'i':
+                        italic = false;
+                        break;
+                    case 'b':
+                        bold = false;
+                        break;
                 }
                 }
             }
             }
+
+            if (tag == 'text')
+                inText = false;
         };
         };
 
 
         let buf = this.decode(data).toString();
         let buf = this.decode(data).toString();
         sax.parseSync(buf, {
         sax.parseSync(buf, {
-            onStartNode
+            onStartNode, onEndNode, onTextNode
         });
         });
 
 
         putPageLines();
         putPageLines();
@@ -277,16 +280,8 @@ class ConvertPdf extends ConvertHtml {
         }
         }
         indents[0] = 0;
         indents[0] = 0;
 
 
-        //title
-        if (fs.pathExists(metaFile)) {
-            const metaXmlString = (await fs.readFile(metaFile)).toString();
-            let metaXmlParsed = xmlParser.parseXml(metaXmlString);
-            metaXmlParsed = xmlParser.simplifyXmlParsed(metaXmlParsed);
-            if (metaXmlParsed.metadata) {
-                title = (metaXmlParsed.metadata.title ? metaXmlParsed.metadata.title._t : '');
-                author = (metaXmlParsed.metadata.author ? metaXmlParsed.metadata.author._t : '');
-            }
-        }
+        //author & title
+        let {author, title} = await this.getPdfTitleAndAuthor(inpFile);
 
 
         if (!title && uploadFileName)
         if (!title && uploadFileName)
             title = uploadFileName;
             title = uploadFileName;
@@ -302,6 +297,7 @@ class ConvertPdf extends ConvertHtml {
 
 
         let concat = '';
         let concat = '';
         let sp = '';
         let sp = '';
+        let firstLine = true;
         for (const line of lines) {
         for (const line of lines) {
             if (text.length > limitSize) {
             if (text.length > limitSize) {
                 throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
                 throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
@@ -313,10 +309,15 @@ class ConvertPdf extends ConvertHtml {
             }
             }
 
 
             if (line.isSectionTitle) {
             if (line.isSectionTitle) {
-                text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
+                if (firstLine)
+                    text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
+                else
+                    text += `<fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
                 continue;
                 continue;
             }
             }
 
 
+            firstLine = false;
+
             if (line.isSubtitle) {
             if (line.isSubtitle) {
                 text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
                 text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
                 continue;
                 continue;
@@ -343,6 +344,32 @@ class ConvertPdf extends ConvertHtml {
         await utils.sleep(100);
         await utils.sleep(100);
         return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
         return await super.run(Buffer.from(text), {skipCheck: true, isText: true});
     }
     }
+
+    async getPdfTitleAndAuthor(pdfFile) {
+        const result = {author: '', title: ''};
+
+        const pdfinfoPath = '/usr/bin/pdfinfo';
+
+        if (!await fs.pathExists(pdfinfoPath))
+            throw new Error('Внешний конвертер pdfinfo не найден');
+
+        const execResult = await this.execConverter(pdfinfoPath, [pdfFile]);
+
+        const titlePrefix = 'Title:';
+        const authorPrefix = 'Author:';
+
+        const stdout = execResult.stdout.split("\n");
+        stdout.forEach(line => {
+            if (line.indexOf(titlePrefix) == 0) 
+                result.title = line.substring(titlePrefix.length).trim();
+
+            if (line.indexOf(authorPrefix) == 0)
+                result.author = line.substring(authorPrefix.length).trim();
+        });
+
+        return result;
+    }
 }
 }
 
 
+
 module.exports = ConvertPdf;
 module.exports = ConvertPdf;

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

@@ -3,6 +3,7 @@ const FileDetector = require('../../FileDetector');
 
 
 //порядок важен
 //порядок важен
 const convertClassFactory = [
 const convertClassFactory = [
+    require('./ConvertJpegPng'),
     require('./ConvertEpub'),
     require('./ConvertEpub'),
     require('./ConvertDjvu'),
     require('./ConvertDjvu'),
     require('./ConvertPdf'),
     require('./ConvertPdf'),