├── custom_style.css ├── manifest.json ├── popup.html ├── viewer_styles.css ├── README.md ├── popup.css └── popup.js /custom_style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | overflow: auto !important; 3 | height: auto !important; 4 | user-select: text !important; 5 | } 6 | 7 | #upgrade-overlay, 8 | .banner-wrapper, 9 | [class*="paywall"], 10 | [class*="overlay"], 11 | #page-container-wrapper + div, 12 | .advertisement { 13 | display: none !important; 14 | z-index: -9999 !important; 15 | } 16 | 17 | .pf, .pc, #document-wrapper { 18 | display: block !important; 19 | visibility: visible !important; 20 | opacity: 1 !important; 21 | filter: none !important; 22 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Studocu Cleaner", 4 | "version": "1.0", 5 | "permissions": [ 6 | "cookies", 7 | "scripting", 8 | "activeTab" 9 | ], 10 | "host_permissions": [ 11 | "*://*.studocu.com/*", 12 | "*://*.studocu.vn/*" 13 | ], 14 | "action": { 15 | "default_popup": "popup.html", 16 | "default_title": "Studocu Tools" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": ["*://*.studocu.com/*", "*://*.studocu.vn/*"], 21 | "css": ["custom_style.css"], 22 | "run_at": "document_start" 23 | } 24 | ], 25 | "web_accessible_resources": [ 26 | { 27 | "resources": ["viewer_styles.css"], 28 | "matches": ["*://*.studocu.com/*", "*://*.studocu.vn/*"] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Studocu Tools 7 | 8 | 9 | 10 | 11 |
12 | 16 | v1.0 17 |
18 | 19 |
20 |
21 | 30 | 31 | 40 |
41 | 42 |
43 | 44 | Sẵn sàng hoạt động 45 |
46 | 47 | 50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /viewer_styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f6f7fb !important; 3 | margin: 0 !important; 4 | overflow: auto !important; 5 | } 6 | 7 | body > *:not(#clean-viewer-container) { 8 | display: none !important; 9 | } 10 | 11 | #clean-viewer-container { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | padding: 30px 0; 20 | z-index: 9999; 21 | } 22 | 23 | .std-page { 24 | position: relative !important; 25 | background-color: white; 26 | box-shadow: 0 4px 15px rgba(0,0,0,0.1); 27 | margin-bottom: 20px; 28 | display: block !important; 29 | overflow: hidden !important; 30 | border: none !important; 31 | } 32 | 33 | .std-page::after { 34 | content: ""; 35 | position: absolute; 36 | bottom: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 4px; 40 | background-color: white !important; 41 | z-index: 9999; 42 | pointer-events: none; 43 | } 44 | 45 | .layer-bg { 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | height: 100%; 51 | z-index: 1; 52 | pointer-events: none; 53 | } 54 | 55 | .layer-bg img { 56 | width: 100%; 57 | height: 100%; 58 | object-fit: cover; 59 | object-position: top center; 60 | display: block; 61 | } 62 | 63 | .layer-text { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | width: 100%; 68 | height: 100%; 69 | z-index: 10; 70 | overflow: visible !important; 71 | } 72 | 73 | .layer-text .pc, 74 | .layer-text .pc * { 75 | transform: none !important; 76 | overflow: visible !important; 77 | max-width: none !important; 78 | max-height: none !important; 79 | } 80 | 81 | @media print { 82 | @page { 83 | margin: 0; 84 | size: auto; 85 | } 86 | 87 | body { 88 | background-color: white !important; 89 | -webkit-print-color-adjust: exact; 90 | print-color-adjust: exact; 91 | } 92 | 93 | #clean-viewer-container { 94 | position: static !important; 95 | width: 100% !important; 96 | padding: 0 !important; 97 | margin: 0 !important; 98 | } 99 | 100 | .std-page { 101 | margin: 0 !important; 102 | margin-bottom: 0 !important; 103 | box-shadow: none !important; 104 | page-break-after: always !important; 105 | break-after: always !important; 106 | border: none !important; 107 | } 108 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Studocu Helper 2 | 3 | Một tiện ích mở rộng nhẹ dành cho trình duyệt, giúp tối ưu hóa trải nghiệm đọc và lưu trữ tài liệu trên Studocu. 4 | 5 | > **Trạng thái:** v1.0 6 | 7 | ## 📖 Giới thiệu 8 | 9 | Công cụ này được phát triển để giải quyết các vấn đề hiển thị gây cản trở khi xem tài liệu học tập. Thay vì phải thao tác thủ công phức tạp, extension cung cấp giải pháp "một click" để làm sạch giao diện và xuất tài liệu ra định dạng in ấn chuẩn. 10 | 11 | ## ✨ Tính năng chính 12 | 13 | ### 1. Bypass Blur & Remove Watermark 14 | Đây là tính năng cốt lõi giúp hiển thị nội dung nguyên bản của tài liệu: 15 | - **Xóa lớp phủ mờ (Unblur):** Loại bỏ các layer che khuất nội dung, giúp văn bản hiển thị rõ nét 100%. 16 | - **Xóa Watermark:** Tự động ẩn các logo chìm, text quảng cáo hoặc các popup gây rối mắt đè lên nội dung. 17 | - **Tối ưu hiển thị:** Giữ lại định dạng gốc (font chữ, bố cục) để người dùng có trải nghiệm đọc tốt nhất. 18 | 19 | ### 2. PDF Export 20 | Tính năng hỗ trợ lưu tài liệu về máy để in ấn hoặc đọc offline: 21 | - **Render tự động:** Tự động cuộn và tải toàn bộ các trang tài liệu trước khi xuất. 22 | - **Chuẩn khổ giấy A4:** Tự động căn chỉnh lề và kích thước trang phù hợp với máy in thông dụng. 23 | - **Fix lỗi hiển thị:** Đã xử lý triệt để lỗi xuất hiện "vạch đen" (black line artifact) ở cuối trang thường gặp khi lưu trang web thành PDF. 24 | 25 | --- 26 | 27 | ## 🛠 Hướng dẫn cài đặt 28 | 29 | Do đây là công cụ phát triển cá nhân (chưa đưa lên Store), bạn cần cài đặt thủ công qua chế độ Developer: 30 | 31 | 1. **Tải mã nguồn:** Tải file `.zip` của dự án về và giải nén (hoặc clone repository này). 32 | 2. **Mở trình quản lý tiện ích:** Truy cập đường dẫn `chrome://extensions/` trên trình duyệt (Chrome, Edge, Cốc Cốc...). 33 | 3. **Bật Developer Mode:** Gạt công tắc **"Developer mode"** ở góc trên bên phải màn hình. 34 | 4. **Tải tiện ích:** Nhấn nút **"Load unpacked"** và chọn thư mục chứa mã nguồn vừa giải nén. 35 | dụng 36 | ## Cách sử dụng 37 | 38 | 1. Truy cập vào tài liệu Studocu cần xem. 39 | 2. Nếu bị chặn watermark và mờ thì bắt buộc mở extension và chọn Bypass mờ và watermark để công đoạn tiếp theo thành công 40 | 3. Cuộn chuột xuống cuối trang để đảm bảo toàn bộ nội dung đã được tải. 41 | 4. Mở Extension và nhấn nút **"Tạo File PDF"**. 42 | 5. Chờ vài giây để tool xử lý, sau đó hộp thoại lưu PDF sẽ tự động hiện ra. 43 | 44 | --- 45 | 46 | Video Testing: 47 | 48 | https://github.com/user-attachments/assets/0f98de3a-cdbb-464d-8209-b9953b0721ee 49 | 50 | ## ⚠️ Lưu ý (Disclaimer) 51 | Công cụ này được tạo ra với mục đích hỗ trợ học tập và nghiên cứu cá nhân. Vui lòng sử dụng có trách nhiệm và tôn trọng bản quyền của tài liệu gốc. 52 | 53 | --- 54 | ## Stars ⭐ 55 | 56 | 57 | 58 | star-history-20251211 (1) 59 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #007bff; 3 | --primary-hover: #0056b3; 4 | --secondary-color: #ffeaea; 5 | --secondary-text: #d9534f; 6 | --secondary-hover: #ffdada; 7 | --bg-color: #f8f9fa; 8 | --card-bg: #ffffff; 9 | --text-main: #2c3e50; 10 | --text-sub: #7f8c8d; 11 | --shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 12 | --radius: 12px; 13 | } 14 | 15 | body { 16 | width: 300px; 17 | margin: 0; 18 | padding: 0; 19 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 20 | background-color: var(--bg-color); 21 | color: var(--text-main); 22 | box-sizing: border-box; 23 | } 24 | 25 | header { 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | padding: 16px 20px; 30 | background: var(--card-bg); 31 | border-bottom: 1px solid #eee; 32 | } 33 | 34 | .logo { 35 | display: flex; 36 | align-items: center; 37 | gap: 8px; 38 | color: var(--primary-color); 39 | } 40 | 41 | .logo h1 { 42 | font-size: 16px; 43 | font-weight: 700; 44 | margin: 0; 45 | letter-spacing: -0.5px; 46 | color: #333; 47 | } 48 | 49 | .version { 50 | font-size: 10px; 51 | background: #eee; 52 | padding: 2px 6px; 53 | border-radius: 4px; 54 | color: #666; 55 | font-weight: 600; 56 | } 57 | 58 | main { 59 | padding: 20px; 60 | } 61 | 62 | .card { 63 | display: flex; 64 | flex-direction: column; 65 | gap: 12px; 66 | } 67 | 68 | .btn { 69 | display: flex; 70 | align-items: center; 71 | width: 100%; 72 | padding: 12px 16px; 73 | border: none; 74 | border-radius: var(--radius); 75 | cursor: pointer; 76 | transition: all 0.2s ease; 77 | text-align: left; 78 | position: relative; 79 | overflow: hidden; 80 | } 81 | 82 | .btn:active { 83 | transform: scale(0.98); 84 | } 85 | 86 | .icon-box { 87 | display: flex; 88 | align-items: center; 89 | justify-content: center; 90 | width: 36px; 91 | height: 36px; 92 | border-radius: 8px; 93 | margin-right: 14px; 94 | flex-shrink: 0; 95 | } 96 | 97 | .btn-text { 98 | display: flex; 99 | flex-direction: column; 100 | } 101 | 102 | .btn-title { 103 | font-size: 14px; 104 | font-weight: 600; 105 | line-height: 1.4; 106 | } 107 | 108 | .btn-desc { 109 | font-size: 11px; 110 | opacity: 0.8; 111 | font-weight: 400; 112 | } 113 | 114 | .btn-primary { 115 | background: linear-gradient(135deg, var(--primary-color), #00d2ff); 116 | color: white; 117 | box-shadow: 0 4px 10px rgba(0, 123, 255, 0.3); 118 | } 119 | 120 | .btn-primary .icon-box { 121 | background: rgba(255, 255, 255, 0.2); 122 | } 123 | 124 | .btn-primary:hover { 125 | box-shadow: 0 6px 14px rgba(0, 123, 255, 0.4); 126 | filter: brightness(1.05); 127 | } 128 | 129 | .btn-secondary { 130 | background-color: var(--secondary-color); 131 | color: var(--secondary-text); 132 | border: 1px solid transparent; 133 | } 134 | 135 | .btn-secondary .icon-box { 136 | background: rgba(217, 83, 79, 0.1); 137 | } 138 | 139 | .btn-secondary:hover { 140 | background-color: var(--secondary-hover); 141 | border-color: rgba(217, 83, 79, 0.2); 142 | } 143 | 144 | .status-bar { 145 | margin-top: 16px; 146 | font-size: 12px; 147 | color: var(--text-sub); 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | gap: 6px; 152 | background: #fff; 153 | padding: 8px; 154 | border-radius: 8px; 155 | border: 1px solid #eee; 156 | } 157 | 158 | .status-dot { 159 | width: 6px; 160 | height: 6px; 161 | background-color: #2ecc71; 162 | border-radius: 50%; 163 | display: inline-block; 164 | } 165 | 166 | .status-bar.processing .status-dot { 167 | background-color: #f1c40f; 168 | animation: blink 1s infinite; 169 | } 170 | 171 | @keyframes blink { 172 | 0% { opacity: 1; } 173 | 50% { opacity: 0.4; } 174 | 100% { opacity: 1; } 175 | } 176 | 177 | footer { 178 | text-align: center; 179 | font-size: 11px; 180 | color: #b0b8c1; 181 | margin-top: 16px; 182 | padding-bottom: 4px; 183 | user-select: none; 184 | font-weight: 500; 185 | letter-spacing: 0.3px; 186 | } 187 | 188 | footer .heart { 189 | color: #ff4757; 190 | display: inline-block; 191 | font-size: 12px; 192 | transition: transform 0.2s ease; 193 | } 194 | 195 | footer .author { 196 | color: var(--primary-color); 197 | font-weight: 700; 198 | position: relative; 199 | cursor: default; 200 | } 201 | 202 | footer:hover .heart { 203 | animation: heartbeat 1.2s infinite; 204 | } 205 | 206 | footer:hover .author { 207 | text-decoration: underline; 208 | text-underline-offset: 2px; 209 | } 210 | 211 | @keyframes heartbeat { 212 | 0% { transform: scale(1); } 213 | 14% { transform: scale(1.3); } 214 | 28% { transform: scale(1); } 215 | 42% { transform: scale(1.3); } 216 | 70% { transform: scale(1); } 217 | } -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | // Status 2 | function updateStatus(msg, isProcessing = false) { 3 | const statusText = document.getElementById('status-text'); 4 | const statusBar = document.getElementById('status'); 5 | 6 | if (statusText && statusBar) { 7 | statusText.innerText = msg; 8 | if (isProcessing) { 9 | statusBar.classList.add('processing'); 10 | } else { 11 | statusBar.classList.remove('processing'); 12 | } 13 | } else { 14 | const oldStatus = document.getElementById('status'); 15 | if (oldStatus) oldStatus.textContent = msg; 16 | } 17 | } 18 | 19 | // Button to delete cookies & reload 20 | document.getElementById('clearBtn').addEventListener('click', async () => { 21 | updateStatus("Đang quét và xóa cookie...", true); 22 | 23 | try { 24 | const allCookies = await chrome.cookies.getAll({}); 25 | let count = 0; 26 | for (const cookie of allCookies) { 27 | if (cookie.domain.includes('studocu')) { 28 | let cleanDomain = cookie.domain.startsWith('.') ? cookie.domain.substring(1) : cookie.domain; 29 | const protocol = cookie.secure ? "https:" : "http:"; 30 | const url = `${protocol}//${cleanDomain}${cookie.path}`; 31 | await chrome.cookies.remove({ url: url, name: cookie.name, storeId: cookie.storeId }); 32 | count++; 33 | } 34 | } 35 | updateStatus(`Đã xóa ${count} cookies! Đang tải lại...`, false); 36 | 37 | setTimeout(() => { 38 | chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { 39 | if(tabs[0]) chrome.tabs.reload(tabs[0].id); 40 | }); 41 | }, 1000); 42 | 43 | } catch (e) { 44 | updateStatus("Lỗi: " + e.message, false); 45 | } 46 | }); 47 | 48 | // Pê đê ép 49 | document.getElementById('checkBtn').addEventListener('click', async () => { 50 | const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 51 | 52 | // Inject viewer styles 53 | chrome.scripting.insertCSS({ 54 | target: { tabId: tab.id }, 55 | files: ["viewer_styles.css"] 56 | }); 57 | 58 | chrome.scripting.executeScript({ 59 | target: { tabId: tab.id }, 60 | func: runCleanViewer 61 | }); 62 | }); 63 | 64 | function runCleanViewer() { 65 | const pages = document.querySelectorAll('div[data-page-index]'); 66 | if (pages.length === 0) { 67 | alert("⚠️ Không tìm thấy trang nào.\n(Hãy cuộn chuột xuống cuối tài liệu để web tải hết nội dung trước!)"); 68 | return; 69 | } 70 | 71 | if (!confirm(`Tìm thấy ${pages.length} trang.\nBấm OK để xử lý và tạo PDF...`)) return; 72 | 73 | const SCALE_FACTOR = 4; 74 | const HEIGHT_SCALE_DIVISOR = 4; 75 | 76 | // Functions 77 | function copyComputedStyle(source, target, scaleFactor, shouldScaleHeight = false, shouldScaleWidth = false, heightScaleDivisor = 4, widthScaleDivisor = 4, shouldScaleMargin = false, marginScaleDivisor = 4) { 78 | const computedStyle = window.getComputedStyle(source); 79 | 80 | const normalProps = [ 81 | 'position', 'left', 'top', 'bottom', 'right', 82 | 'font-family', 'font-weight', 'font-style', 83 | 'color', 'background-color', 84 | 'text-align', 'white-space', 85 | 'display', 'visibility', 'opacity', 'z-index', 86 | 'text-shadow', 'unicode-bidi', 'font-feature-settings', 'padding' 87 | ]; 88 | 89 | const scaleProps = ['font-size', 'line-height']; 90 | let styleString = ''; 91 | 92 | normalProps.forEach(prop => { 93 | const value = computedStyle.getPropertyValue(prop); 94 | if (value && value !== 'none' && value !== 'auto' && value !== 'normal') { 95 | styleString += `${prop}: ${value} !important; `; 96 | } 97 | }); 98 | 99 | const widthValue = computedStyle.getPropertyValue('width'); 100 | if (widthValue && widthValue !== 'none' && widthValue !== 'auto') { 101 | if (shouldScaleWidth) { 102 | const numValue = parseFloat(widthValue); 103 | if (!isNaN(numValue) && numValue > 0) { 104 | const unit = widthValue.replace(numValue.toString(), ''); 105 | styleString += `width: ${numValue / widthScaleDivisor}${unit} !important; `; 106 | } else { 107 | styleString += `width: ${widthValue} !important; `; 108 | } 109 | } else { 110 | styleString += `width: ${widthValue} !important; `; 111 | } 112 | } 113 | 114 | const heightValue = computedStyle.getPropertyValue('height'); 115 | if (heightValue && heightValue !== 'none' && heightValue !== 'auto') { 116 | if (shouldScaleHeight) { 117 | const numValue = parseFloat(heightValue); 118 | if (!isNaN(numValue) && numValue > 0) { 119 | const unit = heightValue.replace(numValue.toString(), ''); 120 | styleString += `height: ${numValue / heightScaleDivisor}${unit} !important; `; 121 | } else { 122 | styleString += `height: ${heightValue} !important; `; 123 | } 124 | } else { 125 | styleString += `height: ${heightValue} !important; `; 126 | } 127 | } 128 | 129 | ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'].forEach(prop => { 130 | const value = computedStyle.getPropertyValue(prop); 131 | if (value && value !== 'auto') { 132 | const numValue = parseFloat(value); 133 | if (!isNaN(numValue)) { 134 | if (shouldScaleMargin && numValue !== 0) { 135 | const unit = value.replace(numValue.toString(), ''); 136 | styleString += `${prop}: ${numValue / marginScaleDivisor}${unit} !important; `; 137 | } else { 138 | styleString += `${prop}: ${value} !important; `; 139 | } 140 | } 141 | } 142 | }); 143 | 144 | scaleProps.forEach(prop => { 145 | const value = computedStyle.getPropertyValue(prop); 146 | if (value && value !== 'none' && value !== 'auto' && value !== 'normal') { 147 | const numValue = parseFloat(value); 148 | if (!isNaN(numValue) && numValue !== 0) { 149 | const unit = value.replace(numValue.toString(), ''); 150 | styleString += `${prop}: ${numValue / scaleFactor}${unit} !important; `; 151 | } else { 152 | styleString += `${prop}: ${value} !important; `; 153 | } 154 | } 155 | }); 156 | 157 | let transformOrigin = computedStyle.getPropertyValue('transform-origin'); 158 | if (transformOrigin) { 159 | styleString += `transform-origin: ${transformOrigin} !important; -webkit-transform-origin: ${transformOrigin} !important; `; 160 | } 161 | 162 | styleString += 'overflow: visible !important; max-width: none !important; max-height: none !important; clip: auto !important; clip-path: none !important; '; 163 | target.style.cssText += styleString; 164 | } 165 | 166 | function deepCloneWithStyles(element, scaleFactor, heightScaleDivisor, depth = 0) { 167 | const clone = element.cloneNode(false); 168 | const hasTextClass = element.classList && element.classList.contains('t'); 169 | const hasUnderscoreClass = element.classList && element.classList.contains('_'); 170 | 171 | const shouldScaleMargin = element.tagName === 'SPAN' && 172 | element.classList && 173 | element.classList.contains('_') && 174 | Array.from(element.classList).some(cls => /^_(?:\d+[a-z]*|[a-z]+\d*)$/i.test(cls)); 175 | 176 | copyComputedStyle(element, clone, scaleFactor, hasTextClass, hasUnderscoreClass, heightScaleDivisor, 4, shouldScaleMargin, scaleFactor); 177 | 178 | if (element.classList && element.classList.contains('pc')) { 179 | clone.style.setProperty('transform', 'none', 'important'); 180 | clone.style.setProperty('-webkit-transform', 'none', 'important'); 181 | clone.style.setProperty('overflow', 'visible', 'important'); 182 | clone.style.setProperty('max-width', 'none', 'important'); 183 | clone.style.setProperty('max-height', 'none', 'important'); 184 | } 185 | 186 | if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) { 187 | clone.textContent = element.textContent; 188 | } else { 189 | element.childNodes.forEach(child => { 190 | if (child.nodeType === 1) { 191 | clone.appendChild(deepCloneWithStyles(child, scaleFactor, heightScaleDivisor, depth + 1)); 192 | } else if (child.nodeType === 3) { 193 | clone.appendChild(child.cloneNode(true)); 194 | } 195 | }); 196 | } 197 | return clone; 198 | } 199 | 200 | // Build 201 | const viewerContainer = document.createElement('div'); 202 | viewerContainer.id = 'clean-viewer-container'; 203 | 204 | let successCount = 0; 205 | 206 | pages.forEach((page, index) => { 207 | const pc = page.querySelector('.pc'); 208 | let width = 595.3; //Fallback A4 209 | let height = 841.9; 210 | 211 | if (pc) { 212 | const pcStyle = window.getComputedStyle(pc); 213 | const pcWidth = parseFloat(pcStyle.width); 214 | const pcHeight = parseFloat(pcStyle.height); 215 | 216 | if (!isNaN(pcWidth) && pcWidth > 0 && !isNaN(pcHeight) && pcHeight > 0) { 217 | width = pcWidth; 218 | height = pcHeight; 219 | } else { 220 | const rect = pc.getBoundingClientRect(); 221 | if (rect.width > 10 && rect.height > 10) { 222 | width = rect.width; 223 | height = rect.height; 224 | } 225 | } 226 | } 227 | 228 | const newPage = document.createElement('div'); 229 | newPage.className = 'std-page'; 230 | newPage.id = `page-${index + 1}`; 231 | newPage.setAttribute('data-page-number', index + 1); 232 | 233 | newPage.style.width = width + 'px'; 234 | newPage.style.height = height + 'px'; 235 | 236 | // Layer ảnh 237 | const originalImg = page.querySelector('img.bi') || page.querySelector('img'); 238 | if (originalImg) { 239 | const bgLayer = document.createElement('div'); 240 | bgLayer.className = 'layer-bg'; 241 | const imgClone = originalImg.cloneNode(true); 242 | imgClone.style.cssText = 'width: 100%; height: 100%; object-fit: cover; object-position: top center'; 243 | bgLayer.appendChild(imgClone); 244 | newPage.appendChild(bgLayer); 245 | } 246 | 247 | // Layer Text 248 | const originalPc = page.querySelector('.pc'); 249 | if (originalPc) { 250 | const textLayer = document.createElement('div'); 251 | textLayer.className = 'layer-text'; 252 | const pcClone = deepCloneWithStyles(originalPc, SCALE_FACTOR, HEIGHT_SCALE_DIVISOR); 253 | 254 | pcClone.querySelectorAll('img').forEach(img => img.style.display = 'none'); 255 | textLayer.appendChild(pcClone); 256 | newPage.appendChild(textLayer); 257 | } 258 | 259 | viewerContainer.appendChild(newPage); 260 | successCount++; 261 | }); 262 | 263 | document.body.appendChild(viewerContainer); 264 | 265 | setTimeout(() => { 266 | window.print(); 267 | }, 1000); 268 | } --------------------------------------------------------------------------------