releasebird-javascript-sdk 1.0.61 → 1.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/backend-image-service/image-service/Dockerfile +8 -0
- package/backend-image-service/image-service/fonts.conf +39 -0
- package/backend-image-service/image-service/server.js +27 -8
- package/backend-image-service/test-emoji-detailed.js +119 -0
- package/backend-image-service/test-emoji-simple.js +68 -0
- package/backend-image-service/test-emoji.html +51 -0
- package/build/index.js +1 -1
- package/emoji-test.png +0 -0
- package/package.json +1 -1
- package/published/1.0.62/index.js +1 -0
- package/published/latest/index.js +1 -1
- package/src/RbirdScreenshotManager.js +142 -21
|
@@ -185,31 +185,25 @@ export default class RbirdScreenshotManager {
|
|
|
185
185
|
* Returns complete HTML document with minimal modifications
|
|
186
186
|
*/
|
|
187
187
|
async exportHTML() {
|
|
188
|
-
console.log('[Screenshot] Exporting HTML
|
|
188
|
+
console.log('[Screenshot] Exporting HTML with inline resources...');
|
|
189
189
|
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
let bodyHTML = document.body.innerHTML;
|
|
193
|
-
|
|
194
|
-
// IMPORTANT: Keep the markerBoundary SVG content!
|
|
195
|
-
// The SVG drawings are already in the body innerHTML
|
|
196
|
-
|
|
197
|
-
// Create a temporary container to manipulate the body HTML
|
|
198
|
-
const tempContainer = document.createElement('div');
|
|
199
|
-
tempContainer.innerHTML = bodyHTML;
|
|
190
|
+
// Clone the document
|
|
191
|
+
const clone = document.documentElement.cloneNode(true);
|
|
200
192
|
|
|
201
193
|
// Remove UI elements that shouldn't be in the screenshot
|
|
202
194
|
const removeSelectors = [
|
|
203
195
|
'#rbirdScreenshotLoader',
|
|
204
196
|
'#screenshotCloseButton',
|
|
205
197
|
'.menu',
|
|
198
|
+
'script',
|
|
199
|
+
'noscript',
|
|
206
200
|
'iframe[src*="cookiehub"]',
|
|
207
201
|
'iframe[src*="google"]',
|
|
208
202
|
'iframe[src*="facebook"]'
|
|
209
203
|
];
|
|
210
204
|
|
|
211
205
|
removeSelectors.forEach(selector => {
|
|
212
|
-
const elements =
|
|
206
|
+
const elements = clone.querySelectorAll(selector);
|
|
213
207
|
elements.forEach(el => {
|
|
214
208
|
if (el && el.parentNode) {
|
|
215
209
|
el.parentNode.removeChild(el);
|
|
@@ -217,22 +211,149 @@ export default class RbirdScreenshotManager {
|
|
|
217
211
|
});
|
|
218
212
|
});
|
|
219
213
|
|
|
220
|
-
//
|
|
221
|
-
|
|
214
|
+
// Download all images and convert to base64
|
|
215
|
+
await this.downloadAllImages(clone);
|
|
216
|
+
|
|
217
|
+
// Download all CSS and convert url() references to data URLs
|
|
218
|
+
await this.downloadAllCSS(clone);
|
|
222
219
|
|
|
223
220
|
// Build complete HTML
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
${headHTML}
|
|
227
|
-
<body>
|
|
228
|
-
${cleanedBodyHTML}
|
|
229
|
-
</body>
|
|
230
|
-
</html>`;
|
|
221
|
+
const doctype = '<!DOCTYPE html>';
|
|
222
|
+
const html = doctype + clone.outerHTML;
|
|
231
223
|
|
|
232
224
|
console.log(`[Screenshot] Exported HTML: ${html.length} bytes`);
|
|
233
225
|
return html;
|
|
234
226
|
}
|
|
235
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Download all images and convert to base64 data URLs
|
|
230
|
+
*/
|
|
231
|
+
async downloadAllImages(clone) {
|
|
232
|
+
const images = clone.querySelectorAll('img');
|
|
233
|
+
const promises = [];
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < images.length; i++) {
|
|
236
|
+
const img = images[i];
|
|
237
|
+
if (img.src && !img.src.startsWith('data:')) {
|
|
238
|
+
promises.push(
|
|
239
|
+
this.fetchImageAsDataURL(img.src)
|
|
240
|
+
.then(dataURL => {
|
|
241
|
+
if (dataURL) {
|
|
242
|
+
img.src = dataURL;
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
.catch(err => {
|
|
246
|
+
console.warn('[Screenshot] Failed to load image:', img.src, err);
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await Promise.all(promises);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Fetch image and convert to data URL
|
|
257
|
+
*/
|
|
258
|
+
fetchImageAsDataURL(url) {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const xhr = new XMLHttpRequest();
|
|
261
|
+
xhr.onload = function() {
|
|
262
|
+
const reader = new FileReader();
|
|
263
|
+
reader.onloadend = function() {
|
|
264
|
+
resolve(reader.result);
|
|
265
|
+
};
|
|
266
|
+
reader.onerror = function() {
|
|
267
|
+
reject(new Error('Failed to read image'));
|
|
268
|
+
};
|
|
269
|
+
reader.readAsDataURL(xhr.response);
|
|
270
|
+
};
|
|
271
|
+
xhr.onerror = function() {
|
|
272
|
+
reject(new Error('Failed to fetch image'));
|
|
273
|
+
};
|
|
274
|
+
xhr.open('GET', url);
|
|
275
|
+
xhr.responseType = 'blob';
|
|
276
|
+
xhr.send();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Download all CSS and inline with data URLs
|
|
282
|
+
*/
|
|
283
|
+
async downloadAllCSS(clone) {
|
|
284
|
+
const promises = [];
|
|
285
|
+
|
|
286
|
+
// Process all stylesheets
|
|
287
|
+
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
288
|
+
const styleSheet = document.styleSheets[i];
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
let cssText = '';
|
|
292
|
+
|
|
293
|
+
// Get CSS rules
|
|
294
|
+
if (styleSheet.cssRules) {
|
|
295
|
+
for (let j = 0; j < styleSheet.cssRules.length; j++) {
|
|
296
|
+
cssText += styleSheet.cssRules[j].cssText;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (cssText) {
|
|
301
|
+
// Convert url() references to data URLs
|
|
302
|
+
const basePath = styleSheet.href ? styleSheet.href.substring(0, styleSheet.href.lastIndexOf('/')) : window.location.href.substring(0, window.location.href.lastIndexOf('/'));
|
|
303
|
+
|
|
304
|
+
const processedCSS = await this.loadCSSUrlResources(cssText, basePath);
|
|
305
|
+
|
|
306
|
+
// Create inline style tag
|
|
307
|
+
const head = clone.querySelector('head');
|
|
308
|
+
const styleNode = document.createElement('style');
|
|
309
|
+
styleNode.type = 'text/css';
|
|
310
|
+
styleNode.appendChild(document.createTextNode(processedCSS));
|
|
311
|
+
head.appendChild(styleNode);
|
|
312
|
+
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.warn('[Screenshot] Failed to process stylesheet:', err);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Remove original link tags
|
|
319
|
+
const links = clone.querySelectorAll('link[rel="stylesheet"]');
|
|
320
|
+
links.forEach(link => link.remove());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Convert CSS url() references to data URLs
|
|
325
|
+
*/
|
|
326
|
+
async loadCSSUrlResources(cssText, basePath) {
|
|
327
|
+
const urlRegex = /url\(['"]?([^'"()]+)['"]?\)/g;
|
|
328
|
+
const matches = [];
|
|
329
|
+
let match;
|
|
330
|
+
|
|
331
|
+
while ((match = urlRegex.exec(cssText)) !== null) {
|
|
332
|
+
matches.push(match);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const match of matches) {
|
|
336
|
+
const url = match[1];
|
|
337
|
+
|
|
338
|
+
// Skip if already a data URL or absolute URL
|
|
339
|
+
if (url.startsWith('data:') || url.startsWith('http')) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const fullURL = new URL(url, basePath + '/').href;
|
|
345
|
+
const dataURL = await this.fetchImageAsDataURL(fullURL);
|
|
346
|
+
if (dataURL) {
|
|
347
|
+
cssText = cssText.replace(match[0], `url(${dataURL})`);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.warn('[Screenshot] Failed to convert CSS url:', url, err);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return cssText;
|
|
355
|
+
}
|
|
356
|
+
|
|
236
357
|
|
|
237
358
|
/**
|
|
238
359
|
* Compress string using gzip
|