Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6074c4b7bd | ||
|
|
9906dd43c7 | ||
|
|
17699f66f8 | ||
|
|
80a29e654d | ||
|
|
4184fda247 | ||
|
|
7460ff7055 | ||
|
|
3137b86cee | ||
|
|
b2ca84bb7e | ||
|
|
7d692dd730 | ||
|
|
8850a89aa7 | ||
|
|
57b01dd204 | ||
|
|
8aa1da36b6 | ||
|
|
2dbe29d632 | ||
|
|
7fa891b4fc | ||
|
|
6cb7412cf3 | ||
|
|
157322834b | ||
|
|
1a13a0fee1 | ||
|
|
37256255bf | ||
|
|
75e01c899e | ||
|
|
ef0d6eab89 | ||
|
|
5d54b1b0f4 | ||
|
|
522f953b4f | ||
|
|
15f02c7115 | ||
|
|
174c877eee | ||
|
|
fd9ec736d7 | ||
|
|
2c94025ba3 | ||
|
|
bfadf35c40 | ||
|
|
f3b69caa12 | ||
|
|
18a83a5b0b | ||
|
|
bd9669b782 | ||
|
|
e05713aa7f | ||
|
|
bc3e1f0a6f | ||
|
|
063d01b5ca | ||
|
|
81c38d7749 | ||
|
|
a29842b084 | ||
|
|
bb5adcdaf6 | ||
|
|
537e17a219 | ||
|
|
03ce50153e | ||
|
|
15d01ad7fc | ||
|
|
e2b29e2c2f | ||
|
|
ce7ae84e0f | ||
|
|
01eb545f15 | ||
|
|
706738c7f1 |
@@ -66,7 +66,7 @@ class Reader {
|
||||
await utils.sleep(refreshPause);
|
||||
|
||||
i++;
|
||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||
throw new Error('Слишком долгое время ожидания');
|
||||
}
|
||||
//проверка воркера
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="fit row">
|
||||
<Notify ref="notify"/>
|
||||
<StdDialog ref="stdDialog"/>
|
||||
<keep-alive>
|
||||
<keep-alive v-if="showPage">
|
||||
<router-view class="col"></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
@@ -12,8 +12,11 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
|
||||
import miscApi from '../api/misc';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
@@ -30,6 +33,8 @@ export default @Component({
|
||||
|
||||
})
|
||||
class App extends Vue {
|
||||
showPage = false;
|
||||
|
||||
itemRuText = {
|
||||
'/cardindex': 'Картотека',
|
||||
'/reader': 'Читалка',
|
||||
@@ -42,7 +47,6 @@ class App extends Vue {
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
this.dispatch = this.$store.dispatch;
|
||||
this.state = this.$store.state;
|
||||
this.uistate = this.$store.state.uistate;
|
||||
this.config = this.$store.state.config;
|
||||
@@ -116,18 +120,24 @@ class App extends Vue {
|
||||
this.$root.notify = this.$refs.notify;
|
||||
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();
|
||||
(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
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
navigator.storage.persist();
|
||||
|
||||
@@ -133,6 +133,9 @@ import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
||||
import bookManager from './share/bookManager';
|
||||
import rstore from '../../store/modules/reader';
|
||||
import readerApi from '../../api/reader';
|
||||
import miscApi from '../../api/misc';
|
||||
|
||||
import {versionHistory} from './versionHistory';
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
@@ -229,7 +232,6 @@ class Reader extends Vue {
|
||||
this.rstore = rstore;
|
||||
this.loading = true;
|
||||
this.commit = this.$store.commit;
|
||||
this.dispatch = this.$store.dispatch;
|
||||
this.reader = this.$store.state.reader;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
@@ -293,6 +295,16 @@ class Reader extends Vue {
|
||||
|
||||
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() {
|
||||
@@ -304,6 +316,7 @@ class Reader extends Vue {
|
||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||
|
||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||
this.$root.readerActionByKeyEvent = (event) => {
|
||||
@@ -313,6 +326,30 @@ class Reader extends Vue {
|
||||
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() {
|
||||
const showButtonCount = Object.values(this.showToolButton).reduce((a, b) => a + (b ? 1 : 0), 0);
|
||||
if (this.$refs.buttons)
|
||||
@@ -394,6 +431,16 @@ class Reader extends Vue {
|
||||
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() {
|
||||
let result = '';
|
||||
const path = this.$route.fullPath;
|
||||
@@ -963,6 +1010,8 @@ class Reader extends Vue {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
} finally {
|
||||
this.checkNewVersionAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,18 @@
|
||||
Показывать уведомление "Что нового"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления "Что нового"<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-checkbox>
|
||||
</div>
|
||||
|
||||
@@ -77,9 +77,15 @@ export default class DrawHelper {
|
||||
let j = 0;
|
||||
//формируем строку
|
||||
for (const part of line.parts) {
|
||||
let tOpen = (part.style.bold ? '<b>' : '');
|
||||
let tOpen = '';
|
||||
tOpen += (part.style.bold ? '<b>' : '');
|
||||
tOpen += (part.style.italic ? '<i>' : '');
|
||||
let tClose = (part.style.italic ? '</i>' : '');
|
||||
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||
let tClose = '';
|
||||
tClose += (part.style.sub ? '</span>' : '');
|
||||
tClose += (part.style.sup ? '</span>' : '');
|
||||
tClose += (part.style.italic ? '</i>' : '');
|
||||
tClose += (part.style.bold ? '</b>' : '');
|
||||
|
||||
let text = '';
|
||||
|
||||
@@ -285,7 +285,7 @@ export default class BookParser {
|
||||
sectionLevel++;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`<${tag}>`, 0);
|
||||
}
|
||||
|
||||
@@ -304,6 +304,11 @@ export default class BookParser {
|
||||
bold = true;
|
||||
center = true;
|
||||
|
||||
if (curTitle.paraIndex < 0) {
|
||||
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||
this.contents.push(curTitle);
|
||||
}
|
||||
|
||||
inSubtitle = true;
|
||||
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||
curTitle.subtitles.push(curSubtitle);
|
||||
@@ -343,7 +348,7 @@ export default class BookParser {
|
||||
sectionLevel--;
|
||||
}
|
||||
|
||||
if (tag == 'emphasis' || tag == 'strong') {
|
||||
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||
growParagraph(`</${tag}>`, 0);
|
||||
}
|
||||
|
||||
@@ -507,7 +512,7 @@ export default class BookParser {
|
||||
|
||||
splitToStyle(s) {
|
||||
let result = [];/*array of {
|
||||
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number},
|
||||
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
|
||||
image: {local: Boolean, inline: Boolean, id: String},
|
||||
text: String,
|
||||
}*/
|
||||
@@ -530,6 +535,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = true;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = true;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = true;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = true;
|
||||
break;
|
||||
@@ -580,6 +591,12 @@ export default class BookParser {
|
||||
case 'emphasis':
|
||||
style.italic = false;
|
||||
break;
|
||||
case 'sup':
|
||||
style.sup = false;
|
||||
break;
|
||||
case 'sub':
|
||||
style.sub = false;
|
||||
break;
|
||||
case 'center':
|
||||
style.center = false;
|
||||
break;
|
||||
|
||||
@@ -169,7 +169,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async deflateWithProgress(data, callback) {
|
||||
const chunkSize = 128*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const deflator = new utils.pako.Deflate({level: 5});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -203,7 +203,7 @@ class BookManager {
|
||||
}
|
||||
|
||||
async inflateWithProgress(data, callback) {
|
||||
const chunkSize = 64*1024;
|
||||
const chunkSize = 512*1024;
|
||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||
|
||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||
@@ -410,16 +410,12 @@ class BookManager {
|
||||
}
|
||||
|
||||
async setRecentBook(value) {
|
||||
const result = this.metaOnly(value);
|
||||
let result = this.metaOnly(value);
|
||||
result.touchTime = Date.now();
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-12-08',
|
||||
header: '0.9.11 (2020-12-09)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>оптимизации, улучшения работы конвертеров</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-12-10',
|
||||
header: '0.9.10 (2020-12-03)',
|
||||
|
||||
@@ -10,18 +10,7 @@ const state = {
|
||||
const getters = {};
|
||||
|
||||
// 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
|
||||
const mutations = {
|
||||
|
||||
@@ -251,11 +251,13 @@ const settingDefaults = {
|
||||
compactTextPerc: 0,
|
||||
imageHeightLines: 100,
|
||||
imageFitWidth: true,
|
||||
enableSitesFilter: true,
|
||||
|
||||
showServerStorageMessages: true,
|
||||
showWhatsNewDialog: true,
|
||||
showDonationDialog2020: true,
|
||||
showLiberamaTopDialog2020: true,
|
||||
enableSitesFilter: true,
|
||||
showNeedUpdateNotify: true,
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.10",
|
||||
"version": "0.9.11",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.9.10",
|
||||
"version": "0.9.11",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
@@ -8,7 +8,7 @@
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --exec 'node server'",
|
||||
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
|
||||
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
|
||||
|
||||
@@ -136,7 +136,7 @@ class WebSocketController {
|
||||
break;
|
||||
|
||||
i++;
|
||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
||||
if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ const he = require('he');
|
||||
const LimitedQueue = require('../../LimitedQueue');
|
||||
const textUtils = require('./textUtils');
|
||||
const utils = require('../../utils');
|
||||
const xmlParser = require('../../xmlParser');
|
||||
|
||||
const queue = new LimitedQueue(3, 20, 3*60*1000);//3 минуты ожидание подвижек
|
||||
const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек
|
||||
|
||||
class ConvertBase {
|
||||
constructor(config) {
|
||||
@@ -14,7 +15,6 @@ class ConvertBase {
|
||||
|
||||
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
||||
this.sofficePath = '/usr/bin/soffice';
|
||||
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
|
||||
}
|
||||
|
||||
async run(data, opts) {// eslint-disable-line no-unused-vars
|
||||
@@ -27,9 +27,6 @@ class ConvertBase {
|
||||
|
||||
if (!await fs.pathExists(this.sofficePath))
|
||||
throw new Error('Внешний конвертер LibreOffice не найден');
|
||||
|
||||
if (!await fs.pathExists(this.pdfToHtmlPath))
|
||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||
}
|
||||
|
||||
async execConverter(path, args, onData, abort) {
|
||||
@@ -42,26 +39,38 @@ class ConvertBase {
|
||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||
}
|
||||
|
||||
abort = (abort ? abort : () => false);
|
||||
const myAbort = () => {
|
||||
return q.abort() || abort();
|
||||
}
|
||||
|
||||
try {
|
||||
if (myAbort())
|
||||
throw new Error('abort');
|
||||
|
||||
const result = await utils.spawnProcess(path, {
|
||||
killAfter: 3600,//1 час
|
||||
args,
|
||||
onData: (data) => {
|
||||
q.resetTimeout();
|
||||
if (queue.freed > 0)
|
||||
q.resetTimeout();
|
||||
onData(data);
|
||||
},
|
||||
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
|
||||
onUsage: (stats) => {
|
||||
if (queue.freed > 1 && stats.cpu >= 10)
|
||||
if (queue.freed > 0 && stats.cpu >= 10) {
|
||||
q.resetTimeout();
|
||||
onData('.');
|
||||
}
|
||||
},
|
||||
onUsageInterval: 10,
|
||||
abort
|
||||
abort: myAbort
|
||||
});
|
||||
if (result.code != 0) {
|
||||
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
||||
}
|
||||
return result;
|
||||
} catch(e) {
|
||||
if (e.status == 'killed') {
|
||||
throw new Error('Слишком долгое ожидание конвертера');
|
||||
@@ -95,61 +104,14 @@ class ConvertBase {
|
||||
}
|
||||
|
||||
formatFb2(fb2) {
|
||||
let out = '<?xml version="1.0" encoding="utf-8"?>';
|
||||
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
|
||||
out += this.formatFb2Node(fb2);
|
||||
out += '</FictionBook>';
|
||||
return out;
|
||||
}
|
||||
|
||||
formatFb2Node(node, name) {
|
||||
let out = '';
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const n of node) {
|
||||
out += this.formatFb2Node(n);
|
||||
const out = xmlParser.formatXml({
|
||||
FictionBook: {
|
||||
_attrs: {xmlns: 'http://www.gribuser.ru/xml/fictionbook/2.0', 'xmlns:l': 'http://www.w3.org/1999/xlink'},
|
||||
_a: [fb2],
|
||||
}
|
||||
} else if (typeof node == 'string') {
|
||||
if (name)
|
||||
out += `<${name}>${this.repSpaces(node)}</${name}>`;
|
||||
else
|
||||
out += this.repSpaces(node);
|
||||
} else {
|
||||
if (node._n)
|
||||
name = node._n;
|
||||
}, 'utf-8', this.repSpaces);
|
||||
|
||||
let attrs = '';
|
||||
if (node._attrs) {
|
||||
for (let attrName in node._attrs) {
|
||||
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
||||
}
|
||||
}
|
||||
|
||||
let tOpen = '';
|
||||
let tBody = '';
|
||||
let tClose = '';
|
||||
if (name)
|
||||
tOpen += `<${name}${attrs}>`;
|
||||
if (node.hasOwnProperty('_t'))
|
||||
tBody += this.repSpaces(node._t);
|
||||
|
||||
for (let nodeName in node) {
|
||||
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
||||
continue;
|
||||
|
||||
const n = node[nodeName];
|
||||
tBody += this.formatFb2Node(n, nodeName);
|
||||
}
|
||||
|
||||
if (name)
|
||||
tClose += `</${name}>`;
|
||||
|
||||
if (attrs == '' && name == 'p' && tBody.trim() == '')
|
||||
out += '<empty-line/>'
|
||||
else
|
||||
out += `${tOpen}${tBody}${tClose}`;
|
||||
}
|
||||
return out;
|
||||
return out.replace(/<p>\s*?<\/p>/g, '<empty-line/>');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const ConvertHtml = require('./ConvertHtml');
|
||||
const ConvertJpegPng = require('./ConvertJpegPng');
|
||||
|
||||
class ConvertDjvu extends ConvertHtml {
|
||||
class ConvertDjvu extends ConvertJpegPng {
|
||||
check(data, opts) {
|
||||
const {inputFiles} = opts;
|
||||
|
||||
@@ -16,7 +16,7 @@ class ConvertDjvu extends ConvertHtml {
|
||||
if (!this.check(data, opts))
|
||||
return false;
|
||||
|
||||
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||
const {inputFiles, callback, abort} = opts;
|
||||
|
||||
const ddjvuPath = '/usr/bin/ddjvu';
|
||||
if (!await fs.pathExists(ddjvuPath))
|
||||
@@ -31,8 +31,8 @@ class ConvertDjvu extends ConvertHtml {
|
||||
throw new Error('Внешний конвертер mogrifyPath не найден');
|
||||
|
||||
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
|
||||
let perc = 0;
|
||||
@@ -42,9 +42,9 @@ class ConvertDjvu extends ConvertHtml {
|
||||
}, abort);
|
||||
|
||||
const tifFileSize = (await fs.stat(tifFile)).size;
|
||||
let limitSize = 3*this.config.maxUploadFileSize;
|
||||
let limitSize = 4*this.config.maxUploadFileSize;
|
||||
if (tifFileSize > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| ${tifFileSize} > ${limitSize}`);
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| tifFileSize: ${tifFileSize} > ${limitSize}`);
|
||||
}
|
||||
|
||||
//разбиваем на файлы
|
||||
@@ -53,17 +53,12 @@ class ConvertDjvu extends ConvertHtml {
|
||||
await fs.remove(tifFile);
|
||||
|
||||
//конвертируем в 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);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
|
||||
//читаем изображения
|
||||
const loadImage = async(image) => {
|
||||
image.data = (await fs.readFile(image.file)).toString('base64');
|
||||
image.name = path.basename(image.file);
|
||||
}
|
||||
|
||||
//ищем изображения
|
||||
let files = [];
|
||||
await utils.findFiles(async(file) => {
|
||||
if (path.extname(file) == '.jpg')
|
||||
@@ -72,30 +67,8 @@ class ConvertDjvu extends ConvertHtml {
|
||||
|
||||
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);
|
||||
|
||||
//формируем текст
|
||||
limitSize = 2*this.config.maxUploadFileSize;
|
||||
let title = '';
|
||||
if (uploadFileName)
|
||||
title = uploadFileName;
|
||||
let text = `<title>${title}</title>`;
|
||||
for (const image of images) {
|
||||
text += `<fb2-image type="image/jpeg" name="${image.name}">${image.data}</fb2-image>`;
|
||||
|
||||
if (text.length > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||
}
|
||||
}
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
|
||||
await utils.sleep(100);
|
||||
return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
||||
|
||||
const ConvertHtml = require('./ConvertHtml');
|
||||
|
||||
class ConvertDocX extends ConvertHtml {
|
||||
class ConvertFb3 extends ConvertHtml {
|
||||
async check(data, opts) {
|
||||
const {inputFiles} = opts;
|
||||
if (this.config.useExternalBookConverter &&
|
||||
@@ -39,13 +39,14 @@ class ConvertDocX extends ConvertHtml {
|
||||
const title = this.getTitle(text)
|
||||
.replace(/<\/?p>/g, '')
|
||||
;
|
||||
text = `<title>${title}</title>` + text
|
||||
text = `<fb2-title>${title}</fb2-title>` + text
|
||||
.replace(/<title>/g, '<br><b>')
|
||||
.replace(/<\/title>/g, '</b><br>')
|
||||
.replace(/<subtitle>/g, '<br><br><subtitle>')
|
||||
.replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
|
||||
;
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConvertDocX;
|
||||
module.exports = ConvertFb3;
|
||||
|
||||
@@ -34,7 +34,6 @@ class ConvertHtml extends ConvertBase {
|
||||
} else {
|
||||
isText = opts.isText;
|
||||
}
|
||||
let {cutTitle} = opts;
|
||||
|
||||
let titleInfo = {};
|
||||
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||
@@ -44,12 +43,17 @@ class ConvertHtml extends ConvertBase {
|
||||
let fb2 = [desc, body, binary];
|
||||
|
||||
let title = '';
|
||||
let author = '';
|
||||
let inTitle = false;
|
||||
let inSectionTitle = false;
|
||||
let inAuthor = false;
|
||||
let inSubTitle = false;
|
||||
let inImage = false;
|
||||
let image = {};
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
let superscript = false;
|
||||
let subscript = false;
|
||||
let begining = true;
|
||||
|
||||
let spaceCounter = [];
|
||||
@@ -62,7 +66,7 @@ class ConvertHtml extends ConvertBase {
|
||||
};
|
||||
|
||||
const growParagraph = (text) => {
|
||||
if (!pars.length)
|
||||
if (!pars.length || pars[pars.length - 1]._n != 'p')
|
||||
newParagraph();
|
||||
|
||||
const l = pars.length;
|
||||
@@ -94,12 +98,16 @@ class ConvertHtml extends ConvertBase {
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
text = this.escapeEntities(text);
|
||||
|
||||
if (!cutCounter && !(cutTitle && inTitle)) {
|
||||
if (!(cutCounter || inTitle || inSectionTitle || inSubTitle)) {
|
||||
let tOpen = '';
|
||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (superscript ? '<sup>' : '');
|
||||
tOpen += (subscript ? '<sub>' : '');
|
||||
let tClose = ''
|
||||
tClose += (subscript ? '</sub>' : '');
|
||||
tClose += (superscript ? '</sup>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||
@@ -110,12 +118,22 @@ class ConvertHtml extends ConvertBase {
|
||||
if (inTitle && !title)
|
||||
title = text;
|
||||
|
||||
if (inAuthor && !author)
|
||||
author = text;
|
||||
|
||||
if (inSectionTitle) {
|
||||
pars.unshift({_n: 'title', _t: text});
|
||||
}
|
||||
|
||||
if (inSubTitle) {
|
||||
pars.push({_n: 'subtitle', _t: text});
|
||||
}
|
||||
|
||||
if (inImage) {
|
||||
image._t = text;
|
||||
binary.push(image);
|
||||
|
||||
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
||||
newParagraph();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -140,15 +158,27 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag == 'sup')
|
||||
superscript = true;
|
||||
|
||||
if (tag == 'sub')
|
||||
subscript = true;
|
||||
}
|
||||
|
||||
if (tag == 'title' || tag == 'cut-title') {
|
||||
if (tag == 'title' || tag == 'fb2-title') {
|
||||
inTitle = true;
|
||||
if (tag == 'cut-title')
|
||||
cutTitle = true;
|
||||
}
|
||||
|
||||
if (tag == 'subtitle') {
|
||||
if (tag == 'fb2-author') {
|
||||
inAuthor = true;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-section-title') {
|
||||
inSectionTitle = true;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-subtitle') {
|
||||
inSubTitle = true;
|
||||
}
|
||||
|
||||
@@ -156,7 +186,7 @@ class ConvertHtml extends ConvertBase {
|
||||
inImage = true;
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
image = {_n: 'binary', _attrs: {id: attrs.name.value, 'content-type': attrs.type.value}, _t: ''};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
@@ -179,12 +209,26 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag == 'sup')
|
||||
superscript = false;
|
||||
|
||||
if (tag == 'sub')
|
||||
subscript = false;
|
||||
}
|
||||
|
||||
if (tag == 'title' || tag == 'cut-title')
|
||||
if (tag == 'title' || tag == 'fb2-title')
|
||||
inTitle = false;
|
||||
|
||||
if (tag == 'subtitle')
|
||||
if (tag == 'fb2-author') {
|
||||
inAuthor = false;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-section-title') {
|
||||
inSectionTitle = false;
|
||||
}
|
||||
|
||||
if (tag == 'fb2-subtitle')
|
||||
inSubTitle = false;
|
||||
|
||||
if (tag == 'fb2-image')
|
||||
@@ -195,10 +239,17 @@ class ConvertHtml extends ConvertBase {
|
||||
|
||||
sax.parseSync(buf, {
|
||||
onStartNode, onEndNode, onTextNode,
|
||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image'])
|
||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
|
||||
});
|
||||
|
||||
titleInfo['book-title'] = title;
|
||||
if (author)
|
||||
titleInfo.author = {'last-name': author};
|
||||
|
||||
body.section._a[0] = pars;
|
||||
|
||||
//console.log(JSON.stringify(fb2, null, 2));
|
||||
|
||||
//подозрение на чистый текст, надо разбить на параграфы
|
||||
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
|
||||
let total = 0;
|
||||
@@ -228,56 +279,49 @@ class ConvertHtml extends ConvertBase {
|
||||
if (parIndent > 2) parIndent--;
|
||||
|
||||
let newPars = [];
|
||||
let curPar = {};
|
||||
const newPar = () => {
|
||||
newPars.push({_n: 'p', _t: ''});
|
||||
curPar = {_n: 'p', _t: ''};
|
||||
newPars.push(curPar);
|
||||
};
|
||||
|
||||
const growPar = (text) => {
|
||||
if (!newPars.length)
|
||||
newPar();
|
||||
|
||||
const l = newPars.length;
|
||||
newPars[l - 1]._t += text;
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (const par of pars) {
|
||||
if (par._n != 'p') {
|
||||
newPars.push(par);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > 0)
|
||||
newPar();
|
||||
i++;
|
||||
|
||||
let j = 0;
|
||||
newPar();
|
||||
|
||||
const lines = par._t.split('\n');
|
||||
for (let line of lines) {
|
||||
line = repCrLfTab(line);
|
||||
for (let j = 0; j < lines.length; j++) {
|
||||
const line = repCrLfTab(lines[j]);
|
||||
|
||||
let l = 0;
|
||||
while (l < line.length && line[l] == ' ') {
|
||||
l++;
|
||||
}
|
||||
|
||||
if (l >= parIndent || line == '') {
|
||||
if (j > 0)
|
||||
newPar();
|
||||
j++;
|
||||
if (j > 0 &&
|
||||
(l >= parIndent ||
|
||||
(j < lines.length - 1 && line == '')
|
||||
)
|
||||
) {
|
||||
newPar();
|
||||
}
|
||||
growPar(line.trim() + ' ');
|
||||
|
||||
curPar._t += line.trim() + ' ';
|
||||
}
|
||||
}
|
||||
|
||||
body.section._a[0] = newPars;
|
||||
} else {
|
||||
body.section._a[0] = pars;
|
||||
}
|
||||
|
||||
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
||||
bold = false;
|
||||
italic = false;
|
||||
superscript = false;
|
||||
subscript = false;
|
||||
inSubTitle = false;
|
||||
pars = body.section._a[0];
|
||||
for (let i = 0; i < pars.length; i++) {
|
||||
@@ -297,7 +341,11 @@ class ConvertHtml extends ConvertBase {
|
||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||
tOpen += (bold ? '<strong>' : '');
|
||||
tOpen += (italic ? '<emphasis>' : '');
|
||||
tOpen += (superscript ? '<sup>' : '');
|
||||
tOpen += (subscript ? '<sub>' : '');
|
||||
let tClose = ''
|
||||
tClose += (subscript ? '</sub>' : '');
|
||||
tClose += (superscript ? '</sup>' : '');
|
||||
tClose += (italic ? '</emphasis>' : '');
|
||||
tClose += (bold ? '</strong>' : '');
|
||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||
@@ -313,6 +361,10 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = true;
|
||||
if (tag == 'emphasis')
|
||||
italic = true;
|
||||
if (tag == 'sup')
|
||||
superscript = true;
|
||||
if (tag == 'sub')
|
||||
subscript = true;
|
||||
if (tag == 'subtitle')
|
||||
inSubTitle = true;
|
||||
}
|
||||
@@ -322,6 +374,10 @@ class ConvertHtml extends ConvertBase {
|
||||
bold = false;
|
||||
if (tag == 'emphasis')
|
||||
italic = false;
|
||||
if (tag == 'sup')
|
||||
superscript = false;
|
||||
if (tag == 'sub')
|
||||
subscript = false;
|
||||
if (tag == 'subtitle')
|
||||
inSubTitle = false;
|
||||
}
|
||||
|
||||
95
server/core/Reader/BookConverter/ConvertJpegPng.js
Normal file
95
server/core/Reader/BookConverter/ConvertJpegPng.js
Normal file
@@ -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;
|
||||
@@ -1,3 +1,4 @@
|
||||
//const _ = require('lodash');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
@@ -22,11 +23,17 @@ class ConvertPdf extends ConvertHtml {
|
||||
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||
|
||||
const inpFile = inputFiles.sourceFile;
|
||||
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
||||
const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
|
||||
const outFile = `${outBasename}.xml`;
|
||||
|
||||
const pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||
|
||||
if (!await fs.pathExists(pdftohtmlPath))
|
||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||
|
||||
//конвертируем в xml
|
||||
let perc = 0;
|
||||
await this.execConverter(this.pdfToHtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
|
||||
await this.execConverter(pdftohtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
|
||||
perc = (perc < 80 ? perc + 10 : 40);
|
||||
callback(perc);
|
||||
}, abort);
|
||||
@@ -35,17 +42,24 @@ class ConvertPdf extends ConvertHtml {
|
||||
const data = await fs.readFile(outFile);
|
||||
callback(90);
|
||||
|
||||
await utils.sleep(100);
|
||||
|
||||
//парсим xml
|
||||
let lines = [];
|
||||
let pagelines = [];
|
||||
let line = {text: ''};
|
||||
let page = {};
|
||||
let fonts = {};
|
||||
let sectionTitleFound = false;
|
||||
|
||||
let images = [];
|
||||
let loading = [];
|
||||
|
||||
let inText = false;
|
||||
let bold = false;
|
||||
let italic = false;
|
||||
let title = '';
|
||||
let prevTop = 0;
|
||||
|
||||
let i = -1;
|
||||
let titleCount = 0;
|
||||
|
||||
const loadImage = async(image) => {
|
||||
const src = path.parse(image.src);
|
||||
@@ -59,7 +73,7 @@ class ConvertPdf extends ConvertHtml {
|
||||
image.type = type;
|
||||
image.name = src.base;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const putImage = (curTop) => {
|
||||
if (!isNaN(curTop) && images.length) {
|
||||
@@ -69,7 +83,72 @@ class ConvertPdf extends ConvertHtml {
|
||||
images.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
pagelines.sort((a, b) => (Math.abs(a.top - b.top) > 3 ? a.top - b.top : 0)*10000 + (a.left - b.left))
|
||||
|
||||
//объединяем в одну строку равные по высоте
|
||||
const pl = [];
|
||||
let pt = 0;
|
||||
let j = -1;
|
||||
pagelines.forEach(line => {
|
||||
if (isTextEmpty(line.text))
|
||||
return;
|
||||
|
||||
//проверим, возможно это заголовок
|
||||
if (line.fontId && line.pageWidth) {
|
||||
const centerLeft = (line.pageWidth - line.width)/2;
|
||||
if (isTextBold(line.text) && Math.abs(centerLeft - line.left) < 10) {
|
||||
if (!sectionTitleFound) {
|
||||
line.isSectionTitle = true;
|
||||
sectionTitleFound = true;
|
||||
} else {
|
||||
line.isSubtitle = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//объединяем
|
||||
if (pt == 0 || Math.abs(pt - line.top) > 3) {
|
||||
j++;
|
||||
pl[j] = line;
|
||||
} else {
|
||||
pl[j].text += ` ${line.text}`;
|
||||
}
|
||||
pt = line.top;
|
||||
});
|
||||
|
||||
//заполняем lines
|
||||
const lastIndex = i;
|
||||
pl.forEach(line => {
|
||||
putImage(line.top);
|
||||
|
||||
//добавим пустую строку, если надо
|
||||
const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
|
||||
if (prevLine && !prevLine.isImage) {
|
||||
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++;
|
||||
lines[i] = {text: '<br>'};
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
lines[i] = line;
|
||||
});
|
||||
pagelines = [];
|
||||
putImage(100000);
|
||||
};
|
||||
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (!cutCounter && inText) {
|
||||
@@ -78,67 +157,80 @@ class ConvertPdf extends ConvertHtml {
|
||||
let tClose = (italic ? '</i>' : '');
|
||||
tClose += (bold ? '</b>' : '');
|
||||
|
||||
lines[i].text += `${tOpen}${text}${tClose} `;
|
||||
if (titleCount < 2 && text.trim() != '') {
|
||||
title += text + (titleCount ? '' : ' - ');
|
||||
titleCount++;
|
||||
}
|
||||
line.text += ` ${tOpen}${text}${tClose}`;
|
||||
}
|
||||
};
|
||||
|
||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (!cutCounter) {
|
||||
if (inText) {
|
||||
switch (tag) {
|
||||
case 'i':
|
||||
italic = true;
|
||||
break;
|
||||
case 'b':
|
||||
bold = true;
|
||||
break;
|
||||
}
|
||||
if (inText) {
|
||||
switch (tag) {
|
||||
case 'i':
|
||||
italic = true;
|
||||
break;
|
||||
case 'b':
|
||||
bold = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'text' && !inText) {
|
||||
let attrs = sax.getAttrsSync(tail);
|
||||
const line = {
|
||||
text: '',
|
||||
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),
|
||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
||||
if (tag == 'page') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
page = {
|
||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||
};
|
||||
|
||||
putPageLines();
|
||||
}
|
||||
|
||||
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);
|
||||
line = {
|
||||
text: '',
|
||||
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),
|
||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
||||
isSectionTitle: false,
|
||||
isSubtitle: false,
|
||||
pageWidth: page.width,
|
||||
fontId: (attrs.font && attrs.font.value ? attrs.font.value : ''),
|
||||
};
|
||||
|
||||
if (line.width != 0 || line.height != 0) {
|
||||
inText = true;
|
||||
pagelines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'image') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
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,
|
||||
};
|
||||
|
||||
if (line.width != 0 || line.height != 0) {
|
||||
inText = true;
|
||||
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
|
||||
putImage(line.top);
|
||||
i++;
|
||||
lines[i] = line;
|
||||
}
|
||||
prevTop = line.top;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'image') {
|
||||
const attrs = sax.getAttrsSync(tail);
|
||||
const 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,
|
||||
};
|
||||
loading.push(loadImage(image));
|
||||
images.push(image);
|
||||
images.sort((a, b) => a.top - b.top)
|
||||
}
|
||||
}
|
||||
|
||||
if (tag == 'page') {
|
||||
putImage(100000);
|
||||
loading.push(loadImage(image));
|
||||
images.push(image);
|
||||
images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -164,9 +256,10 @@ class ConvertPdf extends ConvertHtml {
|
||||
onStartNode, onEndNode, onTextNode
|
||||
});
|
||||
|
||||
putImage(100000);
|
||||
putPageLines();
|
||||
|
||||
await Promise.all(loading);
|
||||
await utils.sleep(100);
|
||||
|
||||
//найдем параграфы и отступы
|
||||
const indents = [];
|
||||
@@ -187,13 +280,24 @@ class ConvertPdf extends ConvertHtml {
|
||||
}
|
||||
indents[0] = 0;
|
||||
|
||||
//формируем текст
|
||||
const limitSize = 2*this.config.maxUploadFileSize;
|
||||
//author & title
|
||||
let {author, title} = await this.getPdfTitleAndAuthor(inpFile);
|
||||
|
||||
if (!title && uploadFileName)
|
||||
title = uploadFileName;
|
||||
let text = `<title>${title}</title>`;
|
||||
|
||||
//console.log(JSON.stringify(lines, null, 2));
|
||||
//формируем текст
|
||||
const limitSize = 2*this.config.maxUploadFileSize;
|
||||
let text = '';
|
||||
if (title)
|
||||
text += `<fb2-title>${title}</fb2-title>`;
|
||||
if (author)
|
||||
text += `<fb2-author>${author}</fb2-author>`;
|
||||
|
||||
let concat = '';
|
||||
let sp = '';
|
||||
let firstLine = true;
|
||||
for (const line of lines) {
|
||||
if (text.length > limitSize) {
|
||||
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||
@@ -204,6 +308,21 @@ class ConvertPdf extends ConvertHtml {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.isSectionTitle) {
|
||||
if (firstLine)
|
||||
text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
|
||||
else
|
||||
text += `<fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
firstLine = false;
|
||||
|
||||
if (line.isSubtitle) {
|
||||
text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (concat == '') {
|
||||
const left = line.left || 0;
|
||||
sp = ' '.repeat(indents[left]);
|
||||
@@ -221,8 +340,36 @@ class ConvertPdf extends ConvertHtml {
|
||||
if (concat)
|
||||
text += sp + concat + "\n";
|
||||
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
|
||||
//console.log(text);
|
||||
await utils.sleep(100);
|
||||
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;
|
||||
|
||||
@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
|
||||
if (text === false)
|
||||
return false;
|
||||
|
||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
||||
return await super.run(Buffer.from(text), {skipCheck: true});
|
||||
}
|
||||
|
||||
getTitle(text) {
|
||||
@@ -79,7 +79,7 @@ class ConvertSites extends ConvertHtml {
|
||||
let book = this.getTitle(text);
|
||||
book = book.replace(' (fb2) | Флибуста', '');
|
||||
|
||||
const title = `<title>${author}${(author ? ' - ' : '')}${book}</title>`;
|
||||
const title = `<fb2-title>${author}${(author ? ' - ' : '')}${book}</fb2-title>`;
|
||||
|
||||
let begin = '<h3 class="book">';
|
||||
if (text.indexOf(begin) <= 0)
|
||||
@@ -95,12 +95,12 @@ class ConvertSites extends ConvertHtml {
|
||||
return text.substring(l, r)
|
||||
.replace(/blockquote class="?book"?/g, 'p')
|
||||
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
||||
.replace(/<h3 class="?book"?>/g, '<br><br><subtitle>')
|
||||
.replace(/<h5 class="?book"?>/g, '<br><br><subtitle>')
|
||||
.replace(/<h3>/g, '<br><br><subtitle>')
|
||||
.replace(/<h5>/g, '<br><br><subtitle>')
|
||||
.replace(/<\/h3>/g, '</subtitle><br>')
|
||||
.replace(/<\/h5>/g, '</subtitle><br>')
|
||||
.replace(/<h3 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h5 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h3>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<h5>/g, '<br><br><fb2-subtitle>')
|
||||
.replace(/<\/h3>/g, '</fb2-subtitle><br>')
|
||||
.replace(/<\/h5>/g, '</fb2-subtitle><br>')
|
||||
.replace(/<div class="?stanza"?>/g, '<br>')
|
||||
.replace(/<div>/g, '<br>')
|
||||
+ title;
|
||||
|
||||
@@ -3,6 +3,7 @@ const FileDetector = require('../../FileDetector');
|
||||
|
||||
//порядок важен
|
||||
const convertClassFactory = [
|
||||
require('./ConvertJpegPng'),
|
||||
require('./ConvertEpub'),
|
||||
require('./ConvertDjvu'),
|
||||
require('./ConvertPdf'),
|
||||
|
||||
@@ -12,7 +12,7 @@ const utils = require('../utils');
|
||||
const log = new (require('../AppLogger'))().log;//singleton
|
||||
|
||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||
const queue = new LimitedQueue(5, 100, 4*60*1000);//4 минуты ожидание подвижек
|
||||
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||
|
||||
let instance = null;
|
||||
|
||||
@@ -130,7 +130,8 @@ class ReaderWorker {
|
||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||
wState.set({progress});
|
||||
q.resetTimeout();
|
||||
if (queue.freed > 0)
|
||||
q.resetTimeout();
|
||||
}, q.abort);
|
||||
|
||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||
|
||||
@@ -6,7 +6,8 @@ function parseSync(xstr, options) {
|
||||
onCdata: _onCdata = dummy,
|
||||
onComment: _onComment = dummy,
|
||||
onProgress: _onProgress = dummy,
|
||||
innerCut = new Set()
|
||||
innerCut = new Set(),
|
||||
lowerCase = true,
|
||||
} = options;
|
||||
|
||||
let i = 0;
|
||||
@@ -91,7 +92,8 @@ function parseSync(xstr, options) {
|
||||
} else {
|
||||
tag = tagData;
|
||||
}
|
||||
tag = tag.toLowerCase();
|
||||
if (lowerCase)
|
||||
tag = tag.toLowerCase();
|
||||
|
||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||
if (!cutCounter)
|
||||
@@ -146,7 +148,8 @@ async function parse(xstr, options) {
|
||||
onCdata: _onCdata = dummy,
|
||||
onComment: _onComment = dummy,
|
||||
onProgress: _onProgress = dummy,
|
||||
innerCut = new Set()
|
||||
innerCut = new Set(),
|
||||
lowerCase = true,
|
||||
} = options;
|
||||
|
||||
let i = 0;
|
||||
@@ -231,7 +234,8 @@ async function parse(xstr, options) {
|
||||
} else {
|
||||
tag = tagData;
|
||||
}
|
||||
tag = tag.toLowerCase();
|
||||
if (lowerCase)
|
||||
tag = tag.toLowerCase();
|
||||
|
||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||
if (!cutCounter)
|
||||
@@ -276,7 +280,7 @@ async function parse(xstr, options) {
|
||||
await _onProgress(100);
|
||||
}
|
||||
|
||||
function getAttrsSync(tail) {
|
||||
function getAttrsSync(tail, lowerCase = true) {
|
||||
let result = {};
|
||||
let name = '';
|
||||
let value = '';
|
||||
@@ -287,13 +291,16 @@ function getAttrsSync(tail) {
|
||||
let waitEq = false;
|
||||
|
||||
const pushResult = () => {
|
||||
if (lowerCase)
|
||||
name = name.toLowerCase();
|
||||
if (name != '') {
|
||||
const fn = name;
|
||||
let ns = '';
|
||||
if (name.indexOf(':') >= 0) {
|
||||
[ns, name] = name.split(':');
|
||||
if (fn.indexOf(':') >= 0) {
|
||||
[ns, name] = fn.split(':');
|
||||
}
|
||||
|
||||
result[name] = {value, ns};
|
||||
result[name] = {value, ns, fn};
|
||||
}
|
||||
name = '';
|
||||
value = '';
|
||||
|
||||
143
server/core/xmlParser.js
Normal file
143
server/core/xmlParser.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const sax = require('./sax');
|
||||
|
||||
function formatXml(xmlParsed, encoding = 'utf-8', textFilterFunc) {
|
||||
let out = `<?xml version="1.0" encoding="${encoding}"?>`;
|
||||
out += formatXmlNode(xmlParsed, textFilterFunc);
|
||||
return out;
|
||||
}
|
||||
|
||||
function formatXmlNode(node, textFilterFunc) {
|
||||
textFilterFunc = (textFilterFunc ? textFilterFunc : text => text);
|
||||
|
||||
const formatNode = (node, name) => {
|
||||
let out = '';
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const n of node) {
|
||||
out += formatNode(n);
|
||||
}
|
||||
} else if (typeof node == 'string') {
|
||||
if (name)
|
||||
out += `<${name}>${textFilterFunc(node)}</${name}>`;
|
||||
else
|
||||
out += textFilterFunc(node);
|
||||
} else {
|
||||
if (node._n)
|
||||
name = node._n;
|
||||
|
||||
let attrs = '';
|
||||
if (node._attrs) {
|
||||
for (let attrName in node._attrs) {
|
||||
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
||||
}
|
||||
}
|
||||
|
||||
let tOpen = '';
|
||||
let tBody = '';
|
||||
let tClose = '';
|
||||
if (name)
|
||||
tOpen += `<${name}${attrs}>`;
|
||||
if (node.hasOwnProperty('_t'))
|
||||
tBody += textFilterFunc(node._t);
|
||||
|
||||
for (let nodeName in node) {
|
||||
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
||||
continue;
|
||||
|
||||
const n = node[nodeName];
|
||||
tBody += formatNode(n, nodeName);
|
||||
}
|
||||
|
||||
if (name)
|
||||
tClose += `</${name}>`;
|
||||
|
||||
out += `${tOpen}${tBody}${tClose}`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
return formatNode(node);
|
||||
}
|
||||
|
||||
function parseXml(xmlString, lowerCase = true) {
|
||||
let result = {};
|
||||
let node = result;
|
||||
|
||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
node._t = text;
|
||||
};
|
||||
|
||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (tag == '?xml')
|
||||
return;
|
||||
|
||||
const newNode = {_n: tag, _p: node};
|
||||
|
||||
if (tail) {
|
||||
const parsedAttrs = sax.getAttrsSync(tail, lowerCase);
|
||||
const atKeys = Object.keys(parsedAttrs);
|
||||
if (atKeys.length) {
|
||||
const attrs = {};
|
||||
for (let i = 0; i < atKeys.length; i++) {
|
||||
const attrName = atKeys[i];
|
||||
attrs[parsedAttrs[attrName].fn] = parsedAttrs[attrName].value;
|
||||
}
|
||||
|
||||
newNode._attrs = attrs;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node._a)
|
||||
node._a = [];
|
||||
node._a.push(newNode);
|
||||
node = newNode;
|
||||
};
|
||||
|
||||
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||
if (node._p && node._n == tag)
|
||||
node = node._p;
|
||||
};
|
||||
|
||||
sax.parseSync(xmlString, {
|
||||
onStartNode, onEndNode, onTextNode, lowerCase
|
||||
});
|
||||
|
||||
if (result._a)
|
||||
result = result._a[0];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function simplifyXmlParsed(node) {
|
||||
|
||||
const simplifyNodeArray = (a) => {
|
||||
const result = {};
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const child = a[i];
|
||||
if (child._n && !result[child._n]) {
|
||||
result[child._n] = {};
|
||||
if (child._a) {
|
||||
result[child._n] = simplifyNodeArray(child._a);
|
||||
}
|
||||
if (child._t) {
|
||||
result[child._n]._t = child._t;
|
||||
}
|
||||
if (child._attrs) {
|
||||
result[child._n]._attrs = child._attrs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return simplifyNodeArray([node]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatXml,
|
||||
formatXmlNode,
|
||||
parseXml,
|
||||
simplifyXmlParsed
|
||||
}
|
||||
Reference in New Issue
Block a user