PageSourceSearch

https://app.paranoid.com/photos/_content/Paranoid.Photos.Client.Common/js/docxRenderer.js

js paranoid.com collected 2026-09-26 06:13:16 UTC 9,426 bytes, 249 lines download raw bytes

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.