// ==UserScript== // @name 行业标准/团体标准信息PDF下载 // @namespace http://tampermonkey.net/ // @version 2026.4.8 // @description 自动下载行业标准/团体标准信息图片并合并为PDF(带进度显示),基于行业标准信息PDF下载改进 // @author yangwenren // @match https://hbba.sacinfo.org.cn/attachment/onlineRead/* // @match https://www.ttbz.org.cn/kkfileview/onlinePreview* // @match https://ttbz.org.cn/kkfileview/onlinePreview* // @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js // @grant GM_xmlhttpRequest // @grant GM_download // @grant GM_notification // @connect hbba.sacinfo.org.cn // @connect www.ttbz.org.cn // @connect ttbz.org.cn // ==/UserScript== (function() { 'use strict'; const hostname = window.location.hostname; const pathname = window.location.pathname; const isTtbzPreview = (hostname === 'ttbz.org.cn' || hostname === 'www.ttbz.org.cn') && pathname.indexOf('/kkfileview/onlinePreview') === 0; const pathParts = pathname.split('/'); const hbbaName = pathParts[3] || 'default_hash'; let pageNum = 0; let imgUrls = []; const doc = new jspdf.jsPDF(); let downloadBtn = null; // 验证页面(仅 hbba 阅读页) if (!isTtbzPreview) { if (!hbbaName || pathParts[2] !== 'onlineRead') { GM_notification({text: '请在标准阅读页面使用本脚本!', timeout: 5000}); return; } } function decodeBase64ToUtf8(base64) { // 兼容 base64/base64url 两种形式 let s = String(base64 || '').replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/'); // 补齐 padding const padLen = s.length % 4; if (padLen) s += '='.repeat(4 - padLen); const binary = atob(s); const bytes = Uint8Array.from(binary, c => c.charCodeAt(0)); return new TextDecoder('utf-8').decode(bytes); } function sanitizeFileName(fileName) { // Windows 文件名禁用字符处理 return String(fileName || '') .replace(/[\\/:*?"<>|]/g, '_') .trim() || 'download.pdf'; } function createButton() { downloadBtn = document.createElement('button'); downloadBtn.id = 'pdfDownloadBtn'; downloadBtn.style = ` position: fixed; top: 20px; right: 20px; padding: 7px 15px; background: #219677; color: white; border: none; border-radius: 4px; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: all 0.3s; `; downloadBtn.textContent = '下载PDF'; downloadBtn.onclick = startDownload; document.body.appendChild(downloadBtn); } function startDownload() { if (isTtbzPreview) { startDownloadTtbz(); return; } // 重置状态 pageNum = 0; imgUrls = []; updateButton('准备下载...', true); fetchNextPage(); } function updateButton(text, disabled = false) { if (!downloadBtn) return; downloadBtn.textContent = text; downloadBtn.style.background = disabled ? '#808080' : '#219677'; downloadBtn.disabled = disabled; } function fetchNextPage() { const url = `https://hbba.sacinfo.org.cn/hbba_onlineRead_page/${hbbaName}/${pageNum}.png?t=1`; GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', headers: { 'Referer': 'https://hbba.sacinfo.org.cn/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' }, onload: function(response) { if (response.status === 200) { const blob = response.response; imgUrls.push(URL.createObjectURL(blob)); pageNum++; updateButton(`下载中...(已下载 ${pageNum} 页)`, true); setTimeout(fetchNextPage, 1000); } else { endDownload(); } }, onerror: function() { endDownload(); } }); } function endDownload() { if (pageNum === 0) { updateButton('下载失败,点击重试'); GM_notification({text: '下载失败,请重试!', timeout: 3000}); return; } updateButton('生成PDF中...', true); generatePDF(); } function generatePDF() { if (imgUrls.length === 0) return; const img = new Image(); img.src = imgUrls[0]; img.onload = function() { const imgWidth = doc.internal.pageSize.getWidth(); const imgHeight = (img.height * imgWidth) / img.width; doc.addImage(imgUrls[0], 'JPEG', 0, 0, imgWidth, imgHeight); for (let i = 1; i < imgUrls.length; i++) { doc.addPage(); doc.addImage(imgUrls[i], 'JPEG', 0, 0, imgWidth, imgHeight); } doc.save(`${hbbaName}.pdf`); // 清理资源 imgUrls.forEach(url => URL.revokeObjectURL(url)); updateButton('下载完成 ✔️'); setTimeout(() => updateButton('下载PDF'), 5000); }; } function startDownloadTtbz() { updateButton('解析下载链接...', true); const params = new URLSearchParams(window.location.search); const encodedUrl = params.get('url'); if (!encodedUrl) { updateButton('链接缺失,重试', false); GM_notification({text: '未找到参数 `url=`,请确保在 tt bz 预览页打开后再点下载', timeout: 5000}); return; } let pdfUrl = ''; try { pdfUrl = decodeBase64ToUtf8(encodedUrl); } catch (e) { console.error(e); updateButton('链接解析失败', false); GM_notification({text: '解析 `url=`(Base64)失败,请检查链接格式', timeout: 5000}); return; } // 从解码后的真实地址尝试抽取文件名(fullfilename) let fileName = 'download.pdf'; try { const u = new URL(pdfUrl); const full = u.searchParams.get('fullfilename'); if (full) fileName = full; } catch (e) { const m = pdfUrl.match(/([^/?#]+\.pdf)(?:$|[?#])/i); if (m) fileName = m[1]; } fileName = sanitizeFileName(fileName); if (!fileName.toLowerCase().endsWith('.pdf')) fileName += '.pdf'; updateButton('开始下载PDF...', true); GM_xmlhttpRequest({ method: 'GET', url: pdfUrl, responseType: 'blob', headers: { 'Referer': window.location.origin + '/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' }, onload: function(response) { if (response.status === 200 || response.status === 0) { const blob = response.response; const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(blobUrl), 30000); updateButton('下载完成 ✔️', false); setTimeout(() => updateButton('下载PDF', false), 5000); } else { console.error('ttbz download failed:', response.status); updateButton('下载失败,重试', false); GM_notification({text: 'ttbz 下载失败,可能需要登录或链接已过期', timeout: 5000}); } }, onerror: function() { updateButton('下载失败,重试', false); GM_notification({text: 'ttbz 下载失败,请重试', timeout: 5000}); } }); } // 初始化 if (isTtbzPreview) { setTimeout(createButton, 1000); } else if (hbbaName !== 'default_hash') { setTimeout(createButton, 1000); } })();