Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3be07b17e | ||
|
|
72f8977071 | ||
|
|
3dbf00344e | ||
|
|
ffdf0b12cd | ||
|
|
a51150c729 | ||
|
|
37e14b397c | ||
|
|
e48af7ee7d | ||
|
|
3eb3dd371a | ||
|
|
8ef6551560 | ||
|
|
b1f5f3dd28 | ||
|
|
6074c4b7bd | ||
|
|
9906dd43c7 | ||
|
|
17699f66f8 | ||
|
|
80a29e654d | ||
|
|
4184fda247 | ||
|
|
7460ff7055 | ||
|
|
3137b86cee | ||
|
|
b2ca84bb7e | ||
|
|
7d692dd730 | ||
|
|
8850a89aa7 | ||
|
|
57b01dd204 | ||
|
|
8aa1da36b6 | ||
|
|
2dbe29d632 |
@@ -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();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
class="no-mp bg-grey-4 text-grey-7"
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
>
|
>
|
||||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||||
|
<q-tab name="images" icon="la la-image" label="Изображения" />
|
||||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +57,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" v-show="selectedTab == 'images'">
|
||||||
|
<div>
|
||||||
|
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||||
|
<div class="row item q-px-sm no-wrap">
|
||||||
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
|
<div class="image-thumb-box row justify-center items-center">
|
||||||
|
<div v-show="!imageLoaded" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
|
||||||
|
<img v-show="imageLoaded" class="image-thumb" :src="imageSrc[item.imageId]"/>
|
||||||
|
</div>
|
||||||
|
<div class="no-expand-button column justify-center items-center">
|
||||||
|
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
|
||||||
|
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
|
||||||
|
</div>
|
||||||
|
<div :style="item.indentStyle"></div>
|
||||||
|
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||||
|
<div class="column justify-center">{{ item.perc }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||||
|
Изображения отсутствуют
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
<div class="tab-panel" v-show="selectedTab == 'bookmarks'">
|
||||||
<div class="column justify-center items-center" style="height: 100px">
|
<div class="column justify-center items-center" style="height: 100px">
|
||||||
Раздел находится в разработке
|
Раздел находится в разработке
|
||||||
@@ -84,6 +110,9 @@ export default @Component({
|
|||||||
class ContentsPage extends Vue {
|
class ContentsPage extends Vue {
|
||||||
selectedTab = 'contents';
|
selectedTab = 'contents';
|
||||||
contents = [];
|
contents = [];
|
||||||
|
images = [];
|
||||||
|
imageSrc = [];
|
||||||
|
imageLoaded = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
@@ -93,7 +122,7 @@ class ContentsPage extends Vue {
|
|||||||
|
|
||||||
//закладки
|
//закладки
|
||||||
|
|
||||||
//далее формаирование оглавления
|
//далее формирование оглавления
|
||||||
if (this.parsed == parsed)
|
if (this.parsed == parsed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -166,6 +195,42 @@ class ContentsPage extends Vue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.contents = newContents;
|
this.contents = newContents;
|
||||||
|
|
||||||
|
//формируем newImages
|
||||||
|
const newImages = [];
|
||||||
|
const ims = parsed.images;
|
||||||
|
for (i = 0; i < ims.length; i++) {
|
||||||
|
const image = ims[i];
|
||||||
|
const bin = parsed.binary[image.id];
|
||||||
|
const type = (bin ? bin.type : '');
|
||||||
|
|
||||||
|
const label = `Изображение ${image.num}`;
|
||||||
|
const indentStyle = getIndentStyle(1);
|
||||||
|
const labelStyle = getLabelStyle(0);
|
||||||
|
|
||||||
|
const p = parsed.para[image.paraIndex];
|
||||||
|
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
|
||||||
|
indentStyle, labelStyle, type, imageId: image.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.images = newImages;
|
||||||
|
|
||||||
|
if (this.selectedTab == 'contents' && !this.contents.length && this.images.length)
|
||||||
|
this.selectedTab = 'images';
|
||||||
|
|
||||||
|
//асинхронная загрузка изображений
|
||||||
|
this.imageSrc = [];
|
||||||
|
this.imageLoaded = false;
|
||||||
|
await utils.sleep(50);
|
||||||
|
(async() => {
|
||||||
|
for (i = 0; i < ims.length; i++) {
|
||||||
|
const id = ims[i].id;
|
||||||
|
const bin = this.parsed.binary[id];
|
||||||
|
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : ''));
|
||||||
|
await utils.sleep(5);
|
||||||
|
}
|
||||||
|
this.imageLoaded = true;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async expandClick(key) {
|
async expandClick(key) {
|
||||||
@@ -244,4 +309,31 @@ class ContentsPage extends Vue {
|
|||||||
.expanded-icon {
|
.expanded-icon {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-type {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 80%;
|
||||||
|
padding: 2px 0 2px 0;
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
.it-jpg-color {
|
||||||
|
background: linear-gradient(to right, #fabc3d, #ffec6d);
|
||||||
|
}
|
||||||
|
.it-png-color {
|
||||||
|
background: linear-gradient(to right, #4bc4e5, #6bf4ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb-box {
|
||||||
|
width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-img-icon {
|
||||||
|
font-size: 250%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -160,12 +160,13 @@ export default class DrawHelper {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) {
|
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength, imageNum, imageLength) {
|
||||||
const pad = 3;
|
const pad = 3;
|
||||||
const fh = h - 2*pad;
|
const fh = h - 2*pad;
|
||||||
const fh2 = fh/2;
|
const fh2 = fh/2;
|
||||||
|
|
||||||
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}`;
|
const tImg = (imageNum > 0 ? ` (${imageNum}/${imageLength})` : '');
|
||||||
|
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}${tImg}`;
|
||||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||||
const read = (bookPos + 1)/textLength;
|
const read = (bookPos + 1)/textLength;
|
||||||
const t2 = `${(read*100).toFixed(2)}%`;
|
const t2 = `${(read*100).toFixed(2)}%`;
|
||||||
@@ -188,7 +189,7 @@ export default class DrawHelper {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||||
|
|
||||||
let out = `<div class="layout" style="` +
|
let out = `<div class="layout" style="` +
|
||||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||||
@@ -206,7 +207,7 @@ export default class DrawHelper {
|
|||||||
|
|
||||||
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
||||||
|
|
||||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength);
|
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||||
|
|
||||||
out += '</div>';
|
out += '</div>';
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -722,8 +722,24 @@ class TextPage extends Vue {
|
|||||||
message = this.statusBarMessage;
|
message = this.statusBarMessage;
|
||||||
if (!message)
|
if (!message)
|
||||||
message = this.title;
|
message = this.title;
|
||||||
|
|
||||||
|
//check image num
|
||||||
|
let imageNum = 0;
|
||||||
|
const len = (lines.length > 2 ? 2 : lines.length);
|
||||||
|
loop:
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
const line = lines[j];
|
||||||
|
for (const part of line.parts) {
|
||||||
|
if (part.image) {
|
||||||
|
imageNum = part.image.num;
|
||||||
|
break loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//drawing
|
||||||
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
|
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
|
||||||
lines[i].end, this.parsed.textLength, message);
|
lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
|
||||||
|
|
||||||
this.bookPosSeen = lines[i].end;
|
this.bookPosSeen = lines[i].end;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -54,12 +54,14 @@ export default class BookParser {
|
|||||||
|
|
||||||
//оглавление
|
//оглавление
|
||||||
this.contents = [];
|
this.contents = [];
|
||||||
|
this.images = [];
|
||||||
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||||
let curSubtitle = {paraIndex: -1, title: ''};
|
let curSubtitle = {paraIndex: -1, title: ''};
|
||||||
let inTitle = false;
|
let inTitle = false;
|
||||||
let inSubtitle = false;
|
let inSubtitle = false;
|
||||||
let sectionLevel = 0;
|
let sectionLevel = 0;
|
||||||
let bodyIndex = 0;
|
let bodyIndex = 0;
|
||||||
|
let imageNum = 0;
|
||||||
|
|
||||||
let paraIndex = -1;
|
let paraIndex = -1;
|
||||||
let paraOffset = 0;
|
let paraOffset = 0;
|
||||||
@@ -202,16 +204,26 @@ export default class BookParser {
|
|||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
const href = attrs.href.value;
|
const href = attrs.href.value;
|
||||||
|
const {id} = this.imageHrefToId(href);
|
||||||
if (href[0] == '#') {//local
|
if (href[0] == '#') {//local
|
||||||
|
imageNum++;
|
||||||
|
|
||||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
if (inPara && !this.showInlineImagesInCenter && !center)
|
||||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||||
else
|
else
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||||
|
|
||||||
|
this.images.push({paraIndex, num: imageNum, id});
|
||||||
|
|
||||||
if (inPara && this.showInlineImagesInCenter)
|
if (inPara && this.showInlineImagesInCenter)
|
||||||
newParagraph(' ', 1);
|
newParagraph(' ', 1);
|
||||||
} else {//external
|
} else {//external
|
||||||
|
imageNum++;
|
||||||
|
|
||||||
dimPromises.push(getExternalImageDimensions(href));
|
dimPromises.push(getExternalImageDimensions(href));
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||||
|
|
||||||
|
this.images.push({paraIndex, num: imageNum, id});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,6 +316,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);
|
||||||
@@ -483,6 +500,15 @@ export default class BookParser {
|
|||||||
return {fb2};
|
return {fb2};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageHrefToId(id) {
|
||||||
|
let local = false;
|
||||||
|
if (id[0] == '#') {
|
||||||
|
id = id.substr(1);
|
||||||
|
local = true;
|
||||||
|
}
|
||||||
|
return {id, local};
|
||||||
|
}
|
||||||
|
|
||||||
findParaIndex(bookPos) {
|
findParaIndex(bookPos) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
//дихотомия
|
//дихотомия
|
||||||
@@ -548,28 +574,21 @@ export default class BookParser {
|
|||||||
case 'image': {
|
case 'image': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
image = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
image.inline = false;
|
||||||
if (id[0] == '#') {
|
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
image = {local, inline: false, id};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'image-inline': {
|
case 'image-inline': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
const img = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
img.inline = true;
|
||||||
if (id[0] == '#') {
|
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: Object.assign({}, style),
|
||||||
image: {local, inline: true, id},
|
image: img,
|
||||||
text: ''
|
text: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -796,6 +815,7 @@ export default class BookParser {
|
|||||||
paraIndex,
|
paraIndex,
|
||||||
w: imageWidth,
|
w: imageWidth,
|
||||||
h: imageHeight,
|
h: imageHeight,
|
||||||
|
num: part.image.num
|
||||||
}});
|
}});
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
line = {begin: line.end + 1, parts: []};
|
line = {begin: line.end + 1, parts: []};
|
||||||
@@ -806,7 +826,7 @@ export default class BookParser {
|
|||||||
line.last = true;
|
line.last = true;
|
||||||
line.parts.push({style, text: ' ',
|
line.parts.push({style, text: ' ',
|
||||||
image: {local: part.image.local, inline: false, id: part.image.id,
|
image: {local: part.image.local, inline: false, id: part.image.id,
|
||||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight}
|
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
|
||||||
});
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -818,7 +838,7 @@ export default class BookParser {
|
|||||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
||||||
imgW += bin.w*imgH/bin.h;
|
imgW += bin.w*imgH/bin.h;
|
||||||
line.parts.push({style, text: '',
|
line.parts.push({style, text: '',
|
||||||
image: {local: part.image.local, inline: true, id: part.image.id}});
|
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (this.recent[result.key]) {
|
||||||
//восстановим из небытия пользовательские данные
|
result = Object.assign({}, this.recent[result.key], result);
|
||||||
if (!result.bookPos)
|
|
||||||
result.bookPos = this.recent[result.key].bookPos;
|
|
||||||
if (!result.bookPosSeen)
|
|
||||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.recentSetLastKey(result.key);
|
await this.recentSetLastKey(result.key);
|
||||||
|
|||||||
@@ -10,18 +10,7 @@ const state = {
|
|||||||
const getters = {};
|
const getters = {};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const 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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// mutations
|
// mutations
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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('Слишком долгое ожидание конвертера');
|
||||||
@@ -102,6 +103,11 @@ class ConvertBase {
|
|||||||
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDataXml(data) {
|
||||||
|
const str = data.toString().trim();
|
||||||
|
return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
formatFb2(fb2) {
|
formatFb2(fb2) {
|
||||||
const out = xmlParser.formatXml({
|
const out = xmlParser.formatXml({
|
||||||
FictionBook: {
|
FictionBook: {
|
||||||
|
|||||||
@@ -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 baseFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
|
||||||
const tifFile = `${inpFile}.tif`;
|
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 = [];
|
await utils.sleep(100);
|
||||||
let loading = [];
|
return await super.run(data, Object.assign({}, opts, {imageFiles: files.map(f => f.name)}));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ class ConvertFb2 extends ConvertBase {
|
|||||||
check(data, opts) {
|
check(data, opts) {
|
||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
return (dataType && dataType.ext == 'xml' && data.toString().indexOf('<FictionBook') >= 0);
|
return (
|
||||||
|
( (dataType && dataType.ext == 'xml') || this.isDataXml(data) ) &&
|
||||||
|
data.toString().indexOf('<FictionBook') >= 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {
|
async run(data, opts) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
//html?
|
//html?
|
||||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
if ( ( (dataType && (dataType.ext == 'html' || dataType.ext == 'xml')) ) || this.isDataXml(data) )
|
||||||
return {isText: false};
|
return {isText: 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;
|
||||||
@@ -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))
|
if (!await fs.pathExists(pdftohtmlPath))
|
||||||
throw new Error('Внешний конвертер pdfalto не найден');
|
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 inText = false;
|
||||||
let author = '';
|
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 => {
|
||||||
//добавим закрывающий тег стиля
|
if (isTextEmpty(line.text))
|
||||||
line.text += line.tClose;
|
return;
|
||||||
|
|
||||||
//проверим, возможно это заголовок
|
//проверим, возможно это заголовок
|
||||||
if (line.fonts.length == 1 && line.pageWidth) {
|
if (line.fontId && line.pageWidth) {
|
||||||
const f = (line.fonts.length ? fonts[line.fonts[0]] : null);
|
|
||||||
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));
|
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) {
|
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 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>' : '');
|
||||||
|
|
||||||
|
line.text += ` ${tOpen}${text}${tClose}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
if (tag == 'textstyle') {
|
if (inText) {
|
||||||
const attrs = sax.getAttrsSync(tail);
|
switch (tag) {
|
||||||
const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
|
case 'i':
|
||||||
const fontStyle = (attrs.fontstyle && attrs.fontstyle.value ? attrs.fontstyle.value : '');
|
italic = true;
|
||||||
const fontSize = (attrs.fontsize && attrs.fontsize.value ? attrs.fontsize.value : '');
|
break;
|
||||||
|
case 'b':
|
||||||
if (fontId) {
|
bold = true;
|
||||||
const styleTags = {bold: 'b', italics: 'i', superscript: 'sup', subscript: 'sub'};
|
break;
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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),
|
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
|
||||||
left: parseInt((attrs.hpos && attrs.hpos.value ? attrs.hpos.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 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,
|
||||||
|
};
|
||||||
|
|
||||||
let tOpen = '';
|
loading.push(loadImage(image));
|
||||||
let tClose = '';
|
images.push(image);
|
||||||
const fontId = (attrs.stylerefs && attrs.stylerefs.value ? attrs.stylerefs.value : '');
|
images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
//author & title
|
||||||
if (fs.pathExists(metaFile)) {
|
let {author, title} = await this.getPdfTitleAndAuthor(inpFile);
|
||||||
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 : '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user