1// DOCX Document Renderer using docx-preview 2// Provides high-fidelity rendering with multi-page support 3(function() { 4 'use strict'; 5 6 // Cache for rendered documents - keeps DOM alive during page navigation 7 const documentCache = new Map(); 8 9 function hashString(str) { 10 let hash = 0; 11 for (let i = 0; i < Math.min(str.length, 1000); i++) { 12 const char = str.charCodeAt(i); 13 hash = ((hash << 5) - hash) + char; 14 hash = hash & hash; 15 } 16 return hash.toString(16); 17 } 18 19 function base64ToArrayBuffer(base64) { 20 // Use native Uint8Array.fromBase64 if available (Chrome 128+, Firefox 133+, Safari 18.2+) 21 if (typeof Uint8Array.fromBase64 === 'function') { 22 return Uint8Array.fromBase64(base64).buffer; 23 } 24 25 // Fallback for older browsers 26 const binaryString = atob(base64); 27 const bytes = new Uint8Array(binaryString.length); 28 for (let i = 0; i < binaryString.length; i++) { 29 bytes[i] = binaryString.charCodeAt(i); 30 } 31 return bytes.buffer; 32 } 33 34 // Render document to a hidden container and return rendering info 35 async function renderDocument(base64DocData, cacheKey) { 36 // Use provided cache key or generate one from the data 37 const effectiveCacheKey = cacheKey || hashString(base64DocData); 38 39 // Check cache 40 if (documentCache.has(effectiveCacheKey)) { 41 return documentCache.get(effectiveCacheKey); 42 } 43 44 if (!window.SharedDocxRenderer) { 45 throw new Error('SharedDocxRenderer not loaded'); 46 } 47 48 const arrayBuffer = base64ToArrayBuffer(base64DocData); 49 50 // Create hidden container for rendering 51 const container = document.createElement('div'); 52 container.style.position = 'absolute'; 53 container.style.left = '-9999px'; 54 container.style.top = '0'; 55 container.style.width = window.SharedDocxRenderer.A4_WIDTH_96DPI + 'px'; 56 container.style.backgroundColor = 'white'; 57 container.style.overflow = 'hidden'; 58 document.body.appendChild(container); 59 60 try { 61 // Use shared renderer (handles docx.renderAsync, image loading, and page splitting) 62 const renderResult = await window.SharedDocxRenderer.renderDocxToContainer(arrayBuffer, container, { 63 className: 'docx-wrapper', 64 splitPages: true, 65 waitForImages: true 66 }); 67 68 // Calculate page boundaries from split sections for image capture 69 const pages = calculatePageBoundaries(container); 70 71 const result = { 72 container: container, 73 pages: pages, 74 pageCount: renderResult.pageCount, 75 cacheKey: effectiveCacheKey 76 }; 77 78 // Cache for reuse during page navigation 79 documentCache.set(effectiveCacheKey, result); 80 81 return result; 82 } catch (error) { 83 document.body.removeChild(container); 84 throw error; 85 } 86 } 87 88 // Helper to calculate page boundaries after SharedDocxRenderer has split sections 89 function calculatePageBoundaries(container) { 90 const pages = []; 91 const sections = container.querySelectorAll('section'); 92 93 sections.forEach((section, index) => { 94 pages.push({ 95 index: pages.length, 96 top: section.offsetTop, 97 height: section.offsetHeight, 98 element: section, 99 sectionIndex: index 100 }); 101 }); 102 103 return pages; 104 } 105 106 async function capturePageToImage(renderInfo, pageIndex, maxWidth, dpi) { 107 if (!window.html2canvas) { 108 throw new Error('html2canvas library not loaded'); 109 } 110 111 const { container, pages } = renderInfo; 112 113 if (pageIndex >= pages.length) { 114 throw new Error(`Page index ${pageIndex} out of range. Document has ${pages.length} pages.`); 115 } 116 117 const page = pages[pageIndex]; 118 119 // Detect actual page dimensions from the rendered container 120 const pageDimensions = window.SharedDocxRenderer.detectPageDimensions(container); 121 const sourceWidth = pageDimensions.width; 122 const sourceHeight = page.height || pageDimensions.height; 123 124 // Fixed scale for performance (trade-off: slightly lower quality but 30-40% faster) 125 const scale = 1.5; 126 127 // If page has a specific element, capture just that 128 if (page.element) { 129 const canvas = await html2canvas(page.element, { 130 scale: scale, 131 backgroundColor: '#ffffff', 132 logging: false,
133 useCORS: false, 134 allowTaint: true, 135 windowWidth: page.element.offsetWidth, 136 windowHeight: page.element.offsetHeight 137 }); 138 139 return canvasToBase64Jpeg(canvas, maxWidth); 140 } 141 142 // Otherwise, capture a region of the container 143 const cloneContainer = document.createElement('div'); 144 cloneContainer.style.position = 'absolute'; 145 cloneContainer.style.left = '-9999px'; 146 cloneContainer.style.top = '0'; 147 cloneContainer.style.width = sourceWidth + 'px'; 148 cloneContainer.style.height = sourceHeight + 'px'; 149 cloneContainer.style.overflow = 'hidden'; 150 cloneContainer.style.backgroundColor = 'white'; 151 document.body.appendChild(cloneContainer); 152 153 const contentClone = container.cloneNode(true); 154 contentClone.style.position = 'absolute'; 155 contentClone.style.top = (-page.top) + 'px'; 156 contentClone.style.left = '0'; 157 cloneContainer.appendChild(contentClone); 158 159 try { 160 const canvas = await html2canvas(cloneContainer, { 161 scale: scale, 162 width: sourceWidth, 163 height: sourceHeight, 164 backgroundColor: '#ffffff', 165 logging: false, 166 useCORS: false, 167 allowTaint: true, 168 windowWidth: sourceWidth, 169 windowHeight: sourceHeight 170 }); 171 return canvasToBase64Jpeg(canvas, maxWidth); 172 } finally { 173 document.body.removeChild(cloneContainer); 174 } 175 } 176 177 function canvasToBase64Jpeg(canvas, maxWidth) { 178 return new Promise((resolve, reject) => { 179 // Validate canvas dimensions 180 if (!canvas || canvas.width <= 0 || canvas.height <= 0) { 181 reject(new Error(`Invalid canvas dimensions: ${canvas?.width}x${canvas?.height}`)); 182 return; 183 } 184 185 let finalCanvas = canvas; 186 if (canvas.width > maxWidth) { 187 const scale = maxWidth / canvas.width; 188 finalCanvas = document.createElement('canvas'); 189 finalCanvas.width = maxWidth; 190 finalCanvas.height = Math.round(canvas.height * scale); 191 const ctx = finalCanvas.getContext('2d'); 192 ctx.fillStyle = 'white'; 193 ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); 194 ctx.drawImage(canvas, 0, 0, finalCanvas.width, finalCanvas.height); 195 } 196 197 finalCanvas.toBlob( 198 (blob) => { 199 if (!blob) { 200 console.error('[DocxRenderer] toBlob returned null for canvas:', { 201 width: finalCanvas.width, 202 height: finalCanvas.height 203 }); 204 reject(new Error('Could not generate image blob')); 205 return; 206 } 207 const reader = new FileReader(); 208 reader.onloadend = () => resolve(reader.result.split(',')[1]); 209 reader.onerror = () => reject(new Error('Could not read image blob')); 210 reader.readAsDataURL(blob); 211 }, 212 'image/jpeg', 213 0.85 214 ); 215 }); 216 } 217 218 function cleanupDocument(cacheKey) { 219 if (documentCache.has(cacheKey)) { 220 const cached = documentCache.get(cacheKey); 221 if (cached.container && cached.container.parentNode) { 222 cached.container.parentNode.removeChild(cached.container); 223 } 224 documentCache.delete(cacheKey); 225 } 226 } 227 228 // Public API 229 window.DocxRenderer = { 230 renderPage: async function(base64DocData, pageIndex, maxWidth, dpi) { 231 try { 232 const renderInfo = await renderDocument(base64DocData); 233 const thumbnailBase64 = await capturePageToImage(renderInfo, pageIndex, maxWidth, dpi); 234 const html = renderInfo.container.innerHTML; 235 const pageCount = renderInfo.pageCount; 236 237 // Return everything directly (no token/chunking - C# will handle caching) 238 return JSON.stringify({ 239 thumbnail: thumbnailBase64, 240 html: html, 241 pageCount: pageCount 242 }); 243 } catch (error) { 244 console.error('DOCX rendering failed:', error); 245 throw new Error(`DOCX document rendering failed: ${error.message}`); 246 } 247 } 248 }; 249})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.