PDFTron Image Extractor (v1.2 - Manual Control, Linter Fixes, EN)

Manually start/stop downloading images from PDFTron viewer iframe, prevents duplicate UI.

  1. // ==UserScript==
  2. // @name PDFTron Image Extractor (v1.2 - Manual Control, Linter Fixes, EN)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Manually start/stop downloading images from PDFTron viewer iframe, prevents duplicate UI.
  6. // @author Tinaut1986
  7. // @match https://pdftron-viewer-quasar.pro.iberley.net/webviewer/ui/index.html*
  8. // @grant GM_download
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @connect pdftron.pro.iberley.net
  14. // @run-at document-end
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // --- Constants ---
  21. const IMAGE_PATTERN_KEY = 'pdfTronImagePattern';
  22. const DEFAULT_IMAGE_PATTERN = /\/pageimg\d+\.jpg/i; // Default pattern
  23. const UI_ID = 'pdftron-downloader-ui-9k4h'; // UI container ID
  24. const STATUS_ID = 'pdftron-status-display-a8fj'; // Status text ID
  25. const START_BUTTON_ID = 'pdftron-start-button-x7gt';// Start/Stop button ID
  26. const FOLDER_KEY = 'pdfTronDestinationFolder';
  27. const DEFAULT_FOLDER = 'PDFTron_Images'; // Default subfolder
  28.  
  29. const VERIFICATION_INTERVAL = 5000; // ms
  30.  
  31. // --- Global Variables ---
  32. let latestImages = new Set();
  33. let destinationFolder = GM_getValue(FOLDER_KEY, DEFAULT_FOLDER);
  34. let imagePattern;
  35. let observer = null;
  36. let cacheCheckInterval = null;
  37. let isDownloadingActive = false;
  38.  
  39. // --- Initialize Image Pattern ---
  40. function initializeImagePattern() {
  41. const savedPattern = GM_getValue(IMAGE_PATTERN_KEY, DEFAULT_IMAGE_PATTERN.source);
  42. try {
  43. imagePattern = new RegExp(savedPattern, 'i');
  44. log(`Image pattern initialized: ${imagePattern.source}`);
  45. } catch (e) {
  46. log(`Error creating RegExp from saved pattern "${savedPattern}". Using default. Error: ${e.message}`, 'error');
  47. imagePattern = DEFAULT_IMAGE_PATTERN;
  48. GM_setValue(IMAGE_PATTERN_KEY, DEFAULT_IMAGE_PATTERN.source);
  49. }
  50. }
  51.  
  52. // --- Logging Utility ---
  53. function log(message, type = 'info') {
  54. const timestamp = new Date().toISOString();
  55. const formattedMessage = `[PDFTron Extractor][${timestamp}] ${message}`;
  56. switch (type) {
  57. case 'error': console.error(formattedMessage); break;
  58. case 'warn': console.warn(formattedMessage); break;
  59. default: console.log(formattedMessage);
  60. }
  61. }
  62.  
  63. // --- Configuration Functions ---
  64. function configureFolder() {
  65. const message = `Enter the subfolder name.\nThis folder will be created inside your browser's main download location.\n\nExample: PDFTron_Images\nCurrent: ${destinationFolder}`;
  66. const newFolder = prompt(message, destinationFolder);
  67. if (newFolder !== null) {
  68. destinationFolder = newFolder.replace(/[\\/]/g, '').trim();
  69. if (!destinationFolder) {
  70. destinationFolder = DEFAULT_FOLDER;
  71. alert(`Folder name cannot be empty. Using default: ${DEFAULT_FOLDER}`);
  72. }
  73. GM_setValue(FOLDER_KEY, destinationFolder);
  74. log(`Destination subfolder set to: ${destinationFolder}`);
  75. updateInterface();
  76. }
  77. }
  78.  
  79. function configureImagePattern() {
  80. const currentSource = imagePattern ? imagePattern.source : DEFAULT_IMAGE_PATTERN.source;
  81. const message = `Enter a regular expression (RegExp) pattern to find the image URLs.\nThis allows you to match specific filenames, such as those containing page numbers.\n\nExample: /pageimg\\d+\\.jpg/i\n(This matches filenames starting with 'pageimg', followed by numbers, ending in '.jpg', case-insensitive)\n\nIf you're not familiar with regular expressions, you may want to look up online guides on how to create them.\n\nCurrent pattern: ${currentSource}`;
  82. const newPatternSource = prompt(message, currentSource);
  83. if (newPatternSource === null) return;
  84.  
  85. try {
  86. const testRegExp = new RegExp(newPatternSource, 'i');
  87. imagePattern = testRegExp;
  88. GM_setValue(IMAGE_PATTERN_KEY, newPatternSource);
  89. log(`Image pattern configured manually: ${imagePattern.source}`);
  90. latestImages.clear();
  91. log("Detected images list cleared due to pattern change.");
  92. if (isDownloadingActive) {
  93. stopDownloading();
  94. startDownloading();
  95. log("Download process restarted with new pattern.");
  96. }
  97. updateInterface();
  98. } catch (error) {
  99. log(`Invalid pattern format: ${error.message}`, 'error');
  100. alert(`Invalid pattern format: ${error.message}\nPlease enter a valid pattern (like the example).`);
  101. }
  102. }
  103.  
  104. // --- User Interface ---
  105. function createInterface() {
  106. if (document.getElementById(UI_ID)) {
  107. log(`UI with ID ${UI_ID} already exists in this iframe. Ensuring button state is correct.`);
  108. updateInterface();
  109. return;
  110. }
  111.  
  112. log(`Creating UI (ID: ${UI_ID}) inside the iframe...`);
  113.  
  114. const container = document.createElement('div');
  115. container.id = UI_ID;
  116. container.style.cssText = `
  117. position: fixed; top: 10px; right: 10px; z-index: 2147483647;
  118. padding: 12px; background: rgba(240, 240, 240, 0.95); border: 1px solid #ccc;
  119. border-radius: 5px; box-shadow: 0 2px 8px rgba(0,0,0,0.2);
  120. font-family: Arial, sans-serif; font-size: 14px; color: #333;
  121. display: flex; flex-direction: column; gap: 8px; max-width: 250px;
  122. `;
  123.  
  124. const title = document.createElement('h3');
  125. title.textContent = 'PDFTron Extractor';
  126. title.style.cssText = 'margin: 0 0 5px 0; font-size: 16px; text-align: center;';
  127.  
  128. const startButton = document.createElement('button');
  129. startButton.id = START_BUTTON_ID;
  130. startButton.textContent = '▶️ Start Download';
  131. startButton.onclick = toggleDownloadState;
  132. startButton.style.cssText = 'padding: 8px 10px; font-size: 1em; margin-bottom: 5px; cursor: pointer; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7;';
  133.  
  134. const configButtonContainer = document.createElement('div');
  135. configButtonContainer.style.cssText = 'display: flex; justify-content: space-around; gap: 5px;';
  136.  
  137. const folderButton = document.createElement('button');
  138. folderButton.textContent = 'Folder';
  139. folderButton.title = 'Configure download subfolder';
  140. folderButton.onclick = configureFolder;
  141. folderButton.style.cssText = 'padding: 5px 10px; flex-grow: 1; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7; cursor: pointer;';
  142.  
  143. const patternButton = document.createElement('button');
  144. patternButton.textContent = 'Pattern';
  145. patternButton.title = 'Configure image URL pattern';
  146. patternButton.onclick = configureImagePattern;
  147. patternButton.style.cssText = 'padding: 5px 10px; flex-grow: 1; border-radius: 3px; border: 1px solid #bbb; background-color: #e7e7e7; cursor: pointer;';
  148.  
  149. configButtonContainer.append(folderButton, patternButton);
  150.  
  151. const status = document.createElement('div');
  152. status.id = STATUS_ID;
  153. status.style.cssText = 'margin-top: 8px; font-size: 0.9em; white-space: pre-wrap; word-wrap: break-word; background: #fff; border: 1px solid #ddd; padding: 5px; border-radius: 3px;';
  154.  
  155. container.append(title, startButton, configButtonContainer, status);
  156.  
  157. try {
  158. document.body.appendChild(container);
  159. log(`UI added to the iframe body.`);
  160. updateInterface();
  161. } catch (e) {
  162. log(`Error adding UI to iframe body: ${e.message}.`, 'error');
  163. }
  164. }
  165.  
  166. function updateInterface() {
  167. const statusElement = document.getElementById(STATUS_ID);
  168. const startButton = document.getElementById(START_BUTTON_ID);
  169.  
  170. if (statusElement) {
  171. const patternSource = imagePattern ? imagePattern.source : 'N/A (Error?)';
  172. const activeStatus = isDownloadingActive ? '🟢 Active' : '🔴 Idle';
  173. statusElement.textContent = `Status: ${activeStatus}\nFolder: ${destinationFolder || '(None)'}\nPattern: ${patternSource}\nDownloaded: ${latestImages.size}`;
  174. }
  175.  
  176. if (startButton) {
  177. startButton.textContent = isDownloadingActive ? '⏹️ Stop Download' : '▶️ Start Download';
  178. startButton.style.backgroundColor = isDownloadingActive ? '#ffdddd' : '#ddffdd';
  179. }
  180. }
  181.  
  182. // --- Download Control Functions ---
  183. function toggleDownloadState() {
  184. if (isDownloadingActive) {
  185. stopDownloading();
  186. } else {
  187. startDownloading();
  188. }
  189. updateInterface();
  190. }
  191.  
  192. function startDownloading() {
  193. if (isDownloadingActive) return;
  194. log("Starting download process...");
  195. isDownloadingActive = true;
  196.  
  197. setupPerformanceObserver();
  198.  
  199. if (cacheCheckInterval) clearInterval(cacheCheckInterval);
  200. log("Running initial cache check...");
  201. verifyCache().then(() => {
  202. log("Initial cache check complete.");
  203. cacheCheckInterval = setInterval(verifyCache, VERIFICATION_INTERVAL);
  204. log(`Periodic cache check started (interval: ${VERIFICATION_INTERVAL}ms).`);
  205. }).catch(err => {
  206. log(`Error during initial cache check: ${err.message}`, 'error');
  207. cacheCheckInterval = setInterval(verifyCache, VERIFICATION_INTERVAL);
  208. log(`Periodic cache check started DESPITE initial error (interval: ${VERIFICATION_INTERVAL}ms).`);
  209. });
  210.  
  211. log("Detection and download system ACTIVATED.");
  212. updateInterface();
  213. }
  214.  
  215. function stopDownloading() {
  216. if (!isDownloadingActive) return;
  217. log("Stopping download process...");
  218. isDownloadingActive = false;
  219.  
  220. if (observer) {
  221. observer.disconnect();
  222. log("PerformanceObserver stopped.");
  223. }
  224.  
  225. if (cacheCheckInterval) {
  226. clearInterval(cacheCheckInterval);
  227. cacheCheckInterval = null;
  228. log("Periodic cache check stopped.");
  229. }
  230. log("Detection and download system DEACTIVATED.");
  231. updateInterface();
  232. }
  233.  
  234. // --- Image Processing and Downloading ---
  235. function processImage(url) {
  236. if (!isDownloadingActive) {
  237. return;
  238. }
  239. if (!url || typeof url !== 'string') {
  240. log(`[processImage] Invalid URL provided: ${url}`, 'warn');
  241. return;
  242. }
  243. if (!imagePattern) {
  244. log(`[processImage] Image pattern not initialized. Aborting process for ${url}.`, 'error');
  245. return;
  246. }
  247. if (!imagePattern.test(url)) {
  248. log(`[processImage] URL unexpectedly failed pattern test inside processImage: ${url}`, 'warn');
  249. return;
  250. }
  251.  
  252. const cleanUrl = url.split('?')[0];
  253. const name = cleanUrl.split('/').pop();
  254.  
  255. if (latestImages.has(cleanUrl)) {
  256. return;
  257. }
  258.  
  259. log(`[processImage] New image URL detected: ${cleanUrl}`);
  260. latestImages.add(cleanUrl);
  261.  
  262. const fullPath = destinationFolder ? `${destinationFolder}/${name}` : name;
  263. log(`[processImage] Preparing direct download for: ${name} to path: ${fullPath} from URL: ${url}`);
  264.  
  265. try {
  266. GM_download({
  267. url: url,
  268. name: fullPath,
  269. saveAs: false,
  270. headers: {
  271. 'Referer': location.href
  272. },
  273. timeout: 20000,
  274. onload: () => {
  275. log(`✅ [GM_download] Download successful: ${fullPath}`);
  276. updateInterface();
  277. },
  278. onerror: (error) => {
  279. let errorDetails = error?.error || 'unknown';
  280. let finalUrl = error?.details?.finalUrl || url;
  281. let httpStatus = error?.details?.httpStatus;
  282. log(`❌ [GM_download] Error: ${errorDetails}. Status: ${httpStatus || 'N/A'}. Final URL: ${finalUrl}. Path: ${fullPath}`, 'error');
  283. latestImages.delete(cleanUrl);
  284. updateInterface();
  285. },
  286. ontimeout: () => {
  287. log(`❌ [GM_download] Timeout downloading: ${fullPath}`, 'error');
  288. latestImages.delete(cleanUrl);
  289. updateInterface();
  290. }
  291. });
  292. log(`[processImage] GM_download call initiated for ${name}. Waiting for callbacks...`);
  293.  
  294. } catch (e) {
  295. log(`❌ [processImage] CRITICAL Exception calling GM_download: ${e.message}`, 'error');
  296. latestImages.delete(cleanUrl);
  297. updateInterface();
  298. }
  299. }
  300.  
  301. // --- Resource Detection Mechanisms ---
  302. function setupPerformanceObserver() {
  303. try {
  304. if (observer) {
  305. observer.disconnect();
  306. log('[Observer] Disconnected existing observer.');
  307. }
  308.  
  309. log('[Observer] Setting up PerformanceObserver...');
  310. observer = new PerformanceObserver((list) => {
  311. if (!isDownloadingActive) return;
  312.  
  313. list.getEntriesByType('resource').forEach(entry => {
  314. const url = entry.name;
  315. if (imagePattern && imagePattern.test(url)) {
  316. log(`[Observer] MATCHED pattern: ${url}`);
  317. processImage(url);
  318. } else {
  319. if (!url.startsWith('data:') && !url.endsWith('.css') && !url.endsWith('.js') && !url.includes('favicon')) {
  320. // log(`[Observer] Ignored resource (no pattern match): ${url}`);
  321. }
  322. }
  323. });
  324. });
  325. observer.observe({ type: 'resource', buffered: true });
  326. log('[Observer] PerformanceObserver started and listening.');
  327. } catch (e) {
  328. log('[Observer] Error starting PerformanceObserver: ' + e.message, 'error');
  329. }
  330. }
  331.  
  332. // --- Cache Verification (FIXED) ---
  333. async function verifyCache() {
  334. if (!isDownloadingActive) {
  335. return;
  336. }
  337. // log('[Cache] Verifying cache (active)...'); // Can be verbose
  338. try {
  339. const keys = await caches.keys(); // Get all cache storage keys
  340. for (const key of keys) { // Loop through cache keys (outer loop)
  341. try {
  342. const cache = await caches.open(key); // Open specific cache
  343. const requests = await cache.keys(); // Get all request objects (keys) in this cache
  344. // *** FIXED: Use for...of instead of forEach to avoid linter warnings ***
  345. for (const request of requests) { // Loop through requests in *this* cache (inner loop)
  346. const url = request.url;
  347. // Check if the cached request URL matches the pattern
  348. if (imagePattern && imagePattern.test(url)) {
  349. log(`[Cache] MATCHED pattern: ${url}`);
  350. processImage(url); // Hand off to processing function
  351. }
  352. }
  353. } catch (cacheError) {
  354. // Log errors accessing specific caches if needed for debugging
  355. // log(`[Cache] Could not access/read cache '${key}': ${cacheError.message}`, 'warn');
  356. }
  357. }
  358. } catch (error) {
  359. log('[Cache] General error verifying cache: ' + error.message, 'error');
  360. }
  361. }
  362.  
  363. // --- Main Initialization ---
  364. function initialize() {
  365. initializeImagePattern();
  366. log(`Initializing script in IFRAME: ${location.href}`);
  367. createInterface();
  368. try {
  369. GM_registerMenuCommand('🖼️ Configure Download Subfolder', configureFolder);
  370. GM_registerMenuCommand('🔍 Configure Image URL Pattern', configureImagePattern);
  371. } catch (e) {
  372. log(`Error registering menu commands (already registered?): ${e.message}`, 'warn');
  373. }
  374. log('Script ready. Press "Start Download" to begin.');
  375. }
  376.  
  377. // --- Run Initialization ---
  378. if (document.readyState === 'interactive' || document.readyState === 'complete') {
  379. initialize();
  380. } else {
  381. document.addEventListener('DOMContentLoaded', initialize);
  382. }
  383.  
  384. })(); // End of userscript