Files
MangaCircus/reading-direction.js
T

72 lines
3.5 KiB
JavaScript

'use strict';
// Shared by the scanner and publisher. Dimensions identify strip layout, not
// language or the reading order of a printed page. RTL is only a default.
const KEY = 'x-reading-direction';
const aliases = { rtl: 'rtl', ltr: 'ltr', ttb: 'ttb', vertical: 'ttb', webtoon: 'ttb' };
const normalizeDirection = value => aliases[String(value || '').trim().toLowerCase()] || null;
function manualDirection(meta = {}) {
const override = normalizeDirection(meta.directionOverride);
if (override) return override;
const value = normalizeDirection(meta.direction);
if (!value) return null;
const record = meta[KEY];
// Legacy plain directions are ambiguous: never assume a human didn't set one.
// Editing a generated value without its provenance is also a manual override.
if (!record || record.version !== 1 || record.value !== value ||
!['detected', 'fallback'].includes(record.source)) return value;
return null;
}
function detectVerticalStrip(pages = []) {
const dimensions = pages.map(p => {
const rotated = p.r === 90 || p.r === 270;
return { w: Number(rotated ? p.h : p.w), h: Number(rotated ? p.w : p.h) };
}).filter(p => Number.isFinite(p.w) && Number.isFinite(p.h) && p.w > 0 && p.h > 0);
const tall = dimensions.filter(p => p.h / p.w >= 4);
const widths = tall.map(p => p.w).sort((a, b) => a - b);
const middle = widths.length ? widths[Math.floor(widths.length / 2)] : 0;
const consistent = tall.filter(p => Math.abs(p.w - middle) <= middle * .25).length;
const evidence = { pages: pages.length, knownPages: dimensions.length, tallPages: tall.length,
consistentTallPages: consistent, medianStripWidth: middle };
// At least three coherent strips, >=75% of known pages, and >=75% known
// dimensions. A landscape credits card may coexist with the vertical body.
const detected = tall.length >= 3 && dimensions.length >= pages.length * .75 &&
tall.length >= dimensions.length * .75 && consistent >= tall.length * .75;
return detected ? { value: 'ttb', source: 'detected', evidence } : null;
}
function fallbackDirection() {
return { direction: 'rtl', [KEY]: { version: 1, value: 'rtl', source: 'fallback' } };
}
// Scopes are chapter -> book -> manga (or existing -> incoming when publishing).
// Any explicit choice beats inference; a more specific explicit choice wins.
function resolveReadingDirection(scopes = [], pages = []) {
for (let i = 0; i < scopes.length; i++) {
const value = manualDirection(scopes[i]);
if (value) return { value, source: 'manual', scope: i };
}
const detected = detectVerticalStrip(pages);
if (detected) return detected;
for (let i = 0; i < scopes.length; i++) {
const meta = scopes[i] || {}, record = meta[KEY];
if (record?.version === 1 && record.source === 'detected' &&
record.value === normalizeDirection(meta.direction) && record.value === 'ttb') {
return { value: 'ttb', source: 'detected', scope: i, evidence: record.evidence };
}
}
return { value: 'rtl', source: 'fallback' };
}
function directionFields(scopes, pages) {
const { value, source, evidence } = resolveReadingDirection(scopes, pages);
return { direction: value,
...(source === 'manual' && scopes.some(m => normalizeDirection(m?.directionOverride)) ? { directionOverride: value } : {}),
[KEY]: { version: 1, value, source, ...(evidence ? { evidence } : {}) } };
}
module.exports = { KEY, normalizeDirection, manualDirection, detectVerticalStrip,
fallbackDirection, resolveReadingDirection, directionFields };