// ==UserScript==
// @name Scribd Enhancer All-in-One (v3.0.1)
// @namespace https://greasyfork.org/users/Eliminater74
// @version 3.0.1
// @description Scribd Enhancer with OCR, TXT/HTML export, Snapshot PDF (pixel-perfect), Rich HTML (images inlined), page-range + quality controls. Draggable/collapsible panel + floating gear with position memory. Rich HTML now de-duplicates layered text/image to avoid doubled content. By Eliminater74.
// @author Eliminater74
// @license MIT
// @match *://*.scribd.com/*
// @grant none
// @icon https://s-f.scribdassets.com/favicon.ico
// ==/UserScript==
(function () {
'use strict';
// ---------- KEYS ----------
const SETTINGS_KEY = 'scribdEnhancerSettings';
const UI_MENU_KEY = 'scribdEnhancer_ui_menu';
const UI_GEAR_KEY = 'scribdEnhancer_ui_gear';
// ---------- SETTINGS ----------
const defaultSettings = {
unblur: true,
autoScrape: false,
darkMode: false,
showPreview: true,
enableOCR: true,
ocrLang: 'auto',
splitEvery: 0,
// Snapshot controls
pageRange: 'all', // 'all' | '1-25' | '5,7,10-12'
snapshotScale: 2, // 1..4
snapshotQuality: 0.92, // 0.8 | 0.92 | 1.0
// NEW: Rich HTML layer preference: 'auto' | 'preferText' | 'preferImage'
richPref: 'auto'
};
const settings = { ...defaultSettings, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}') };
const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
// ---------- LIBS ----------
const loadScript = (src) => { const s = document.createElement('script'); s.src = src; document.head.appendChild(s); return s; };
loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/tesseract.min.js');
loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js');
loadScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js');
// ---------- STYLES ----------
const style = document.createElement('style');
style.textContent = `
#se-gear {
position: fixed; width: 40px; height: 40px; line-height: 40px; text-align: center;
background:#2b2b2b; color:#fff; border-radius: 50%; cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,.45); z-index: 2147483647; user-select:none;
font-size: 20px;
}
#se-panel {
position: fixed; background:#1e1f22; color:#f1f1f1; width: 320px; border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,.6); z-index: 2147483646; font-family: system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
display: none;
}
#se-header {
display:flex; align-items:center; justify-content:space-between; padding:8px 10px; cursor:move;
background:#2a2b2f; border-top-left-radius:12px; border-top-right-radius:12px;
font-weight:600;
}
#se-header .controls { display:flex; gap:6px; }
#se-header .btn {
width:24px; height:24px; line-height:24px; text-align:center; border-radius:6px; background:#3a3b41; cursor:pointer;
user-select:none;
}
#se-body { padding:8px 10px 10px; max-height: 70vh; overflow:auto; }
#se-body label { display:flex; align-items:center; gap:6px; font-size:13px; margin:4px 0; }
#se-body .row { display:flex; gap:8px; }
#se-body .row > * { flex:1; }
#se-body input[type="text"], #se-body select {
width:100%; padding:6px; border-radius:6px; border:1px solid #444; background:#121316; color:#eee; font-size:13px;
}
#se-body button {
width:100%; padding:8px; margin-top:6px; border:none; border-radius:8px; background:#3b3d45; color:#fff;
cursor:pointer; font-size:13px;
}
#se-body button:hover { filter:brightness(1.08); }
#se-preview {
position: fixed; right: 20px; bottom: 80px; width: 380px; top: 12px;
background:#111; color:#eee; border:1px solid #444; border-radius:10px;
padding:10px; font-family: ui-monospace,Menlo,Consolas,monospace; font-size:12px; white-space:pre-wrap;
overflow:auto; z-index: 2147483645;
}
.se-dark #se-preview { background:#222; color:#eee; border-color:#555; }
`;
document.head.appendChild(style);
// ---------- HELPERS ----------
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const safe = (s) => (s || '').toString();
function applyDarkMode() {
document.documentElement.classList.toggle('se-dark', settings.darkMode);
document.body.classList.toggle('se-dark', settings.darkMode);
}
function unblurContent() {
if (!settings.unblur) return;
const cleanup = () => {
document.querySelectorAll('.blurred_page, .promo_div, [unselectable="on"]').forEach(el => el.remove());
document.querySelectorAll('*').forEach(el => {
const cs = getComputedStyle(el);
if (cs.color === 'transparent') el.style.color = '#111';
if (cs.textShadow && cs.textShadow.includes('white')) el.style.textShadow = 'none';
});
};
cleanup();
new MutationObserver(cleanup).observe(document.body, { childList: true, subtree: true });
}
function cleanOCRText(text) {
return text.split('\n').map(t => t.trim())
.filter(line => line.length >= 3 && /[a-zA-Z]/.test(line) && !/^[^a-zA-Z0-9]{3,}$/.test(line))
.join('\n');
}
function detectLanguage(text) {
const map = { spa:/[ñáéíóúü]/i, fra:/[éèêëàâôûùç]/i, deu:/[äöüß]/i, ron:/[șțăîâ]/i };
for (const [k,re] of Object.entries(map)) if (re.test(text)) return k;
return 'eng';
}
async function preprocessImage(src) {
return new Promise(resolve => {
const img = new Image(); img.crossOrigin = 'anonymous';
img.onload = () => {
if (img.naturalWidth < 100 || img.naturalHeight < 100 || /logo|icon|watermark/i.test(src)) return resolve(null);
const c = document.createElement('canvas'); c.width = img.width; c.height = img.height;
const ctx = c.getContext('2d'); ctx.drawImage(img, 0, 0);
const d = ctx.getImageData(0,0,c.width,c.height);
for (let i=0; i<d.data.length; i+=4) {
const avg = (d.data[i]+d.data[i+1]+d.data[i+2])/3;
d.data[i]=d.data[i+1]=d.data[i+2]=avg;
}
ctx.putImageData(d,0,0);
resolve(c.toDataURL('image/png'));
};
img.onerror = () => resolve(null);
img.src = src;
});
}
function getScribdPages() {
return [...document.querySelectorAll(
'.page, .reader_column, [id^="page_container"], .outer_page, .abs_page, .scribd_page, .text_layer'
)];
}
function parsePageRange(rangeText, totalPages) {
const txt = safe(rangeText).trim().toLowerCase();
if (!txt || txt === 'all') return Array.from({length: totalPages}, (_,i)=>i);
const set = new Set();
for (const part of txt.split(/[,;]\s*/)) {
const m = part.match(/^(\d+)\s*-\s*(\d+)$/);
if (m) {
let a = clamp(+m[1],1,totalPages), b = clamp(+m[2],1,totalPages);
if (a>b) [a,b]=[b,a];
for (let p=a; p<=b; p++) set.add(p-1);
} else {
const n = clamp(parseInt(part,10),1,totalPages);
if (!isNaN(n)) set.add(n-1);
}
}
return [...set].sort((x,y)=>x-y);
}
// ---------- EXPORTS ----------
function exportOutput(content, ext) {
const split = settings.splitEvery | 0;
const parts = content.split(/(?=\[Page \d+])/);
if (!split || split < 1) {
const blob = new Blob([content], { type: ext==='html' ? 'text/html' : 'text/plain' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_output.${ext}`; a.click();
return;
}
for (let i=0; i<parts.length; i+=split) {
const chunk = parts.slice(i,i+split).join('\n');
const blob = new Blob([chunk], { type: ext==='html' ? 'text/html' : 'text/plain' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `scribd_part${Math.floor(i/split)+1}.${ext}`; a.click();
}
}
function printToPDF(content) {
const win = window.open('', 'PrintView');
win.document.write(`<html><head><title>Scribd Print</title></head><body><pre>${content}</pre></body></html>`);
win.document.close(); win.focus(); setTimeout(() => win.print(), 600);
}
async function exportSnapshotPDF(allPages) {
await new Promise(r => { const chk = () => (window.html2canvas && window.jspdf) ? r() : setTimeout(chk,100); chk(); });
const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');
const scale = clamp(+settings.snapshotScale || 2, 1, 4);
const quality = +settings.snapshotQuality || 0.92;
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({ unit:'pt', format:'a4', compress:true });
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
for (let i=0; i<pages.length; i++) {
const node = pages[i];
node.scrollIntoView({block:'center'}); await sleep(220);
const canvas = await window.html2canvas(node, { useCORS:true, allowTaint:true, backgroundColor:'#ffffff', scale });
const imgData = canvas.toDataURL('image/jpeg', quality);
const imgW = pageW, imgH = (canvas.height/canvas.width) * imgW;
if (i>0) pdf.addPage();
const finalH = imgH > pageH ? pageH : imgH;
const finalW = imgH > pageH ? (pageH/imgH)*imgW : imgW;
pdf.addImage(imgData, 'JPEG', 0, 0, finalW, finalH);
if (i % 10 === 0) await sleep(40);
}
pdf.save('scribd_snapshot.pdf');
}
function getPagesInRange(allPages) {
const idxs = parsePageRange(settings.pageRange, allPages.length);
return idxs.map(i => allPages[i]).filter(Boolean);
}
// --- Rich HTML (DOM clone + images inlined) with layer de-dup ---
async function exportRichHTML(allPages) {
const pages = getPagesInRange(allPages); if (!pages.length) return alert('No pages selected.');
const sections = [];
for (let i=0; i<pages.length; i++) {
const clone = pages[i].cloneNode(true);
// Remove hidden bits that can become visible offline
clone.querySelectorAll('[aria-hidden="true"], [style*="opacity:0"], [style*="opacity: 0"], [style*="visibility:hidden"]').forEach(n => n.remove());
// Decide which layer to keep
const hasTextLayer = !!clone.querySelector('.text_layer, [class*="textLayer"]');
const preferText = settings.richPref === 'preferText' || (settings.richPref === 'auto' && hasTextLayer);
if (preferText) {
// Keep selectable text: drop canvases and likely page-wide images
clone.querySelectorAll('canvas').forEach(n => n.remove());
clone.querySelectorAll('img').forEach(img => {
const cls = img.className || '';
const w = (img.getAttribute('width') || '') + (img.style?.width || '');
const h = (img.getAttribute('height') || '') + (img.style?.height || '');
if (/page|render|canvas|background/i.test(cls) || /100%/.test(w+h)) img.remove();
});
} else {
// Keep raster layer: drop absolutely-positioned text
clone.querySelectorAll('.text_layer, [class*="textLayer"]').forEach(n => n.remove());
}
// Inline images (best effort)
const imgs = [...clone.querySelectorAll('img')];
await Promise.all(imgs.map(async (img) => {
try {
const src = img.getAttribute('src') || img.src;
if (!src) return;
img.setAttribute('src', await imageToDataURL(src));
} catch { /* keep original src */ }
}));
// Strip scripts/styles inside clone
clone.querySelectorAll('script, link[rel="stylesheet"]').forEach(n => n.remove());
sections.push(`<section style="page-break-after:always">${clone.outerHTML}</section>`);
if (i % 20 === 0) await sleep(15);
}
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Scribd Rich Export</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* keep it readable offline */
*{transform:none !important}
body{margin:16px;font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;}
section{margin:0 auto; max-width:900px;}
img{max-width:100%; height:auto;}
</style>
</head>
<body>
${sections.join('\n')}
</body>
</html>`;
const blob = new Blob([html], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'scribd_rich.html';
a.click();
}
function imageToDataURL(src) {
return new Promise(resolve => {
const img = new Image(); img.crossOrigin = 'anonymous';
img.onload = () => {
try {
const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight;
const ctx = c.getContext('2d'); ctx.drawImage(img,0,0); resolve(c.toDataURL('image/png'));
} catch { resolve(src); }
};
img.onerror = () => resolve(src);
const bust = src.includes('?') ? '&' : '?'; img.src = src + bust + 'x=' + Date.now();
});
}
// ---------- SCRAPER ----------
async function scrapePages(pages, preview) {
const concurrency = 4; let index = 0; const firstText = [];
async function scrape(page, i) {
page.scrollIntoView(); await sleep(300);
let found = false;
const text = page.innerText.trim();
if (text) { preview.textContent += `[Page ${i+1}] ✅\n${text}\n\n`; firstText.push(text); found = true; }
if (settings.enableOCR && window.Tesseract) {
const imgs = page.querySelectorAll('img');
for (let img of imgs) {
const src = img.src || ''; const processed = await preprocessImage(src);
if (!processed) continue;
const lang = settings.ocrLang === 'auto' ? detectLanguage(firstText.join(' ')) : settings.ocrLang;
try {
const res = await window.Tesseract.recognize(processed, lang);
const ocrText = cleanOCRText(res.data.text || '');
if (ocrText) { preview.textContent += `[OCR] ${ocrText}\n\n`; found = true; }
} catch {}
}
}
if (!found) preview.textContent += `[Page ${i+1}] ❌ No content\n\n`;
}
const workers = Array(concurrency).fill().map(async ()=>{ while (index < pages.length) { const i = index++; await scrape(pages[i], i); }});
await Promise.all(workers);
alert(`✅ Scraped ${pages.length} pages.`);
}
// ---------- DRAGGABLE + UI ----------
function makeDraggable(el, storageKey, fallbackPos) {
el.style.position = 'fixed'; el.style.touchAction = 'none';
try {
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) {
el.style.left = saved.x + 'px'; el.style.top = saved.y + 'px';
} else if (fallbackPos) {
const {x,y} = fallbackPos(); el.style.left = x + 'px'; el.style.top = y + 'px';
}
} catch {}
let startX, startY, startL, startT, moved=false;
const onDown = (e) => {
moved = false;
const p = e.touches ? e.touches[0] : e;
startX=p.clientX; startY=p.clientY;
const r = el.getBoundingClientRect(); startL=r.left; startT=r.top;
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
document.addEventListener('touchmove', onMove, {passive:false});
document.addEventListener('touchend', onUp);
};
const onMove = (e) => {
const p = e.touches ? e.touches[0] : e;
if (e.cancelable) e.preventDefault();
moved = true;
const nx = clamp(startL + (p.clientX-startX), 0, window.innerWidth - el.offsetWidth);
const ny = clamp(startT + (p.clientY-startY), 0, window.innerHeight - el.offsetHeight);
el.style.left = nx + 'px'; el.style.top = ny + 'px';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onUp);
const r = el.getBoundingClientRect();
localStorage.setItem(storageKey, JSON.stringify({x:r.left, y:r.top}));
if (moved) { el.dataset.justDragged = '1'; setTimeout(()=>delete el.dataset.justDragged,150); }
};
el.addEventListener('mousedown', onDown);
el.addEventListener('touchstart', onDown, {passive:false});
}
function buildUI() {
// Gear
const gear = document.createElement('div');
gear.id = 'se-gear'; gear.textContent = '⚙️';
document.body.appendChild(gear);
makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));
// Panel
const panel = document.createElement('div'); panel.id = 'se-panel';
panel.innerHTML = `
<div id="se-header">
<div>📚 Scribd Enhancer</div>
<div class="controls">
<div id="se-min" class="btn" title="Collapse">–</div>
<div id="se-close" class="btn" title="Close">✕</div>
</div>
</div>
<div id="se-body">
<label><input type="checkbox" id="opt-unblur"> Unblur</label>
<label><input type="checkbox" id="opt-autoscrape"> Auto Scrape</label>
<label><input type="checkbox" id="opt-dark"> Dark Mode</label>
<label><input type="checkbox" id="opt-preview"> Show Preview</label>
<div class="row">
<label style="flex:1">OCR
<select id="opt-lang">
<option value="auto">Auto</option>
<option value="eng">English</option>
<option value="spa">Spanish</option>
<option value="fra">French</option>
<option value="deu">German</option>
</select>
</label>
<label style="flex:1">Split
<select id="opt-split">
<option value="0">Off</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
</select>
</label>
</div>
<label>Export Page Range
<input id="opt-range" type="text" placeholder="all | 1-25 | 5,7,10-12">
</label>
<div class="row">
<label>Scale
<select id="opt-scale">
<option value="1">1x</option>
<option value="2">2x</option>
<option value="3">3x</option>
<option value="4">4x</option>
</select>
</label>
<label>JPEG
<select id="opt-quality">
<option value="0.8">0.80</option>
<option value="0.92">0.92</option>
<option value="1.0">1.00</option>
</select>
</label>
</div>
<label>Rich Export Preference
<select id="opt-richpref">
<option value="auto">Auto (prefer text layer if present)</option>
<option value="preferText">Keep Text (remove page images)</option>
<option value="preferImage">Keep Images (remove text layer)</option>
</select>
</label>
<button id="btn-scrape">📖 Scrape Pages (Text/OCR)</button>
<button id="btn-export">💾 Export TXT</button>
<button id="btn-html">🧾 Export Plain HTML</button>
<button id="btn-print">🖨️ Print (Text)</button>
<button id="btn-snapshot-pdf">📸 Export Snapshot PDF</button>
<button id="btn-rich-html">🖼️ Export Rich HTML</button>
</div>
`;
document.body.appendChild(panel);
makeDraggable(panel, UI_MENU_KEY, () => ({ x: window.innerWidth - 360, y: window.innerHeight - 360 }));
// Open/Close & collapse
const togglePanel = () => {
if (gear.dataset.justDragged) return;
panel.style.display = (panel.style.display === 'none' || !panel.style.display) ? 'block' : 'none';
};
gear.addEventListener('click', togglePanel);
panel.querySelector('#se-close').addEventListener('click', () => panel.style.display = 'none');
const body = panel.querySelector('#se-body');
let collapsed = false;
panel.querySelector('#se-min').addEventListener('click', () => {
collapsed = !collapsed;
body.style.display = collapsed ? 'none' : 'block';
panel.querySelector('#se-min').textContent = collapsed ? '+' : '–';
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'g') togglePanel();
if (e.key === 'Escape') panel.style.display = 'none';
});
// Bind controls
const bind = (sel, prop, parser = v=>v) => {
const el = panel.querySelector(sel);
el.value = (prop in settings) ? settings[prop] : el.value;
if (el.type === 'checkbox') el.checked = !!settings[prop];
el.addEventListener('change', () => {
settings[prop] = el.type === 'checkbox' ? el.checked : parser(el.value);
saveSettings();
applyDarkMode();
if (prop === 'showPreview') {
if (settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
if (!settings.showPreview && document.getElementById('se-preview')) preview.remove();
}
});
return el;
};
bind('#opt-unblur', 'unblur');
bind('#opt-autoscrape','autoScrape');
bind('#opt-dark', 'darkMode');
bind('#opt-preview', 'showPreview');
bind('#opt-lang', 'ocrLang');
bind('#opt-split', 'splitEvery', v=>parseInt(v,10)||0);
bind('#opt-range', 'pageRange', v=>safe(v)||'all');
bind('#opt-scale', 'snapshotScale', v=>clamp(parseInt(v,10)||2,1,4));
bind('#opt-quality', 'snapshotQuality', v=>Number(v)||0.92);
bind('#opt-richpref', 'richPref');
// Actions
panel.querySelector('#btn-scrape').onclick = () => {
const pages = getScribdPages();
if (!pages.length) return alert('❌ No pages found.');
if (settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
scrapePages(pages, preview);
};
panel.querySelector('#btn-export').onclick = () => exportOutput(preview.textContent, 'txt');
panel.querySelector('#btn-html').onclick = () => exportOutput(`<html><body><pre>${preview.textContent}</pre></body></html>`, 'html');
panel.querySelector('#btn-print').onclick = () => printToPDF(preview.textContent);
panel.querySelector('#btn-snapshot-pdf').onclick = async () => {
const pages = getScribdPages();
if (!pages.length) return alert('❌ No pages found.');
try { await exportSnapshotPDF(pages); } catch (e) { console.error(e); alert('Snapshot export failed. Try Rich HTML.'); }
};
panel.querySelector('#btn-rich-html').onclick = async () => {
const pages = getScribdPages();
if (!pages.length) return alert('❌ No pages found.');
try { await exportRichHTML(pages); } catch (e) { console.error(e); alert('Rich HTML export failed.'); }
};
return { gear, panel };
}
// Preview box
function createPreview() {
const preview = document.createElement('div');
preview.id = 'se-preview';
if (settings.showPreview) {
preview.textContent = '[Preview Initialized]\n';
document.body.appendChild(preview);
}
return preview;
}
// ---------- BOOT ----------
applyDarkMode();
unblurContent();
const preview = createPreview();
const { gear } = buildUI();
makeDraggable(gear, UI_GEAR_KEY, () => ({ x: window.innerWidth - 70, y: window.innerHeight - 70 }));
// Auto-scrape if desired
if (settings.autoScrape) {
const pages = getScribdPages();
if (pages.length && settings.showPreview && !document.getElementById('se-preview')) document.body.appendChild(preview);
if (pages.length) scrapePages(pages, preview);
}
})();