releasebird-javascript-sdk 1.0.66 → 1.0.68

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.
@@ -208,6 +208,9 @@ export default class RbirdScreenshotManager {
208
208
  async exportHTML() {
209
209
  console.log('[Screenshot] Exporting HTML with inline resources...');
210
210
 
211
+ // Capture scroll positions BEFORE cloning
212
+ this.captureScrollPositions();
213
+
211
214
  // Clone the document
212
215
  const clone = document.documentElement.cloneNode(true);
213
216
 
@@ -246,6 +249,63 @@ export default class RbirdScreenshotManager {
246
249
  return html;
247
250
  }
248
251
 
252
+ /**
253
+ * Capture scroll positions of all scrollable elements and store as data attributes
254
+ * This allows the backend to restore scroll positions before taking the screenshot
255
+ */
256
+ captureScrollPositions() {
257
+ console.log('[Screenshot] Capturing scroll positions...');
258
+ let scrollableCount = 0;
259
+
260
+ // Find all elements that might be scrollable
261
+ const allElements = document.querySelectorAll('*');
262
+
263
+ allElements.forEach((el, index) => {
264
+ // Skip elements that are part of our UI
265
+ if (el.closest('#rbirdScreenshotLoader') ||
266
+ el.closest('#screenshotCloseButton') ||
267
+ el.closest('.menu') ||
268
+ el.closest('.markerBoundary')) {
269
+ return;
270
+ }
271
+
272
+ const scrollLeft = el.scrollLeft;
273
+ const scrollTop = el.scrollTop;
274
+
275
+ // Only mark elements that are actually scrolled
276
+ if (scrollLeft > 0 || scrollTop > 0) {
277
+ // Add a unique identifier if the element doesn't have an ID
278
+ if (!el.id) {
279
+ el.setAttribute('data-rbird-scroll-id', `rbird-scroll-${index}`);
280
+ }
281
+
282
+ // Store scroll positions as data attributes
283
+ if (scrollLeft > 0) {
284
+ el.setAttribute('data-rbird-scroll-left', scrollLeft.toString());
285
+ }
286
+ if (scrollTop > 0) {
287
+ el.setAttribute('data-rbird-scroll-top', scrollTop.toString());
288
+ }
289
+
290
+ scrollableCount++;
291
+ console.log(`[Screenshot] Captured scroll position for element: scrollLeft=${scrollLeft}, scrollTop=${scrollTop}`);
292
+ }
293
+ });
294
+
295
+ // Also capture the main document/body scroll
296
+ const docScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
297
+ const docScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
298
+
299
+ if (docScrollLeft > 0) {
300
+ document.body.setAttribute('data-rbird-scroll-left', docScrollLeft.toString());
301
+ }
302
+ if (docScrollTop > 0) {
303
+ document.body.setAttribute('data-rbird-scroll-top', docScrollTop.toString());
304
+ }
305
+
306
+ console.log(`[Screenshot] Captured scroll positions for ${scrollableCount} elements`);
307
+ }
308
+
249
309
  /**
250
310
  * Download all images and convert to base64 data URLs with resizing
251
311
  */
@@ -291,6 +351,7 @@ export default class RbirdScreenshotManager {
291
351
 
292
352
  /**
293
353
  * Resize image to reduce payload size
354
+ * Preserves transparency by detecting image format and using PNG for images with alpha channel
294
355
  * @param {string} base64Str - Base64 encoded image
295
356
  * @param {number} maxWidth - Maximum width (default 500)
296
357
  * @param {number} maxHeight - Maximum height (default 500)
@@ -324,10 +385,34 @@ export default class RbirdScreenshotManager {
324
385
  canvas.height = height;
325
386
 
326
387
  const ctx = canvas.getContext('2d');
388
+
389
+ // Detect if source is PNG/GIF/WebP (formats that support transparency)
390
+ const isPNG = base64Str.includes('data:image/png');
391
+ const isGIF = base64Str.includes('data:image/gif');
392
+ const isWebP = base64Str.includes('data:image/webp');
393
+ const isSVG = base64Str.includes('data:image/svg');
394
+ const hasTransparency = isPNG || isGIF || isWebP || isSVG;
395
+
396
+ if (hasTransparency) {
397
+ // Preserve transparency - clear canvas with transparent background
398
+ ctx.clearRect(0, 0, width, height);
399
+ } else {
400
+ // Fill with white background for JPEG
401
+ ctx.fillStyle = '#FFFFFF';
402
+ ctx.fillRect(0, 0, width, height);
403
+ }
404
+
327
405
  ctx.drawImage(img, 0, 0, width, height);
328
406
 
329
- // Convert to JPEG with quality setting (better compression than PNG)
330
- const resizedDataURL = canvas.toDataURL('image/jpeg', quality);
407
+ // Use PNG for images with potential transparency, JPEG for others
408
+ let resizedDataURL;
409
+ if (hasTransparency) {
410
+ // PNG preserves transparency
411
+ resizedDataURL = canvas.toDataURL('image/png');
412
+ } else {
413
+ // JPEG for better compression on non-transparent images
414
+ resizedDataURL = canvas.toDataURL('image/jpeg', quality);
415
+ }
331
416
  resolve(resizedDataURL);
332
417
  };
333
418
 
@@ -370,12 +455,46 @@ export default class RbirdScreenshotManager {
370
455
  async downloadAllCSS(clone) {
371
456
  console.log(`[Screenshot] Processing ${document.styleSheets.length} stylesheets...`);
372
457
  const processedStylesheets = [];
458
+ const googleFontsLinks = [];
373
459
 
374
460
  // Process all stylesheets
375
461
  for (let i = 0; i < document.styleSheets.length; i++) {
376
462
  const styleSheet = document.styleSheets[i];
377
463
 
378
464
  try {
465
+ // Skip Google Fonts, icon fonts, and other common font services - keep them as link tags
466
+ if (styleSheet.href && (
467
+ // Google Fonts
468
+ styleSheet.href.includes('fonts.googleapis.com') ||
469
+ styleSheet.href.includes('fonts.gstatic.com') ||
470
+ // TypeKit
471
+ styleSheet.href.includes('use.typekit.net') ||
472
+ styleSheet.href.includes('cloud.typography.com') ||
473
+ // FontAwesome
474
+ styleSheet.href.includes('fontawesome') ||
475
+ styleSheet.href.includes('font-awesome') ||
476
+ styleSheet.href.includes('fa-') ||
477
+ // Bootstrap Icons
478
+ styleSheet.href.includes('bootstrap-icons') ||
479
+ // Material Icons
480
+ styleSheet.href.includes('material-icons') ||
481
+ styleSheet.href.includes('material-symbols') ||
482
+ // Ionicons
483
+ styleSheet.href.includes('ionicons') ||
484
+ // Feather Icons
485
+ styleSheet.href.includes('feather-icons') ||
486
+ // Heroicons
487
+ styleSheet.href.includes('heroicons') ||
488
+ // Generic icon detection
489
+ styleSheet.href.includes('/icons') ||
490
+ styleSheet.href.includes('icon.css') ||
491
+ styleSheet.href.includes('icons.css')
492
+ )) {
493
+ console.log(`[Screenshot] Skipping font/icon service stylesheet: ${styleSheet.href}`);
494
+ googleFontsLinks.push(styleSheet.href);
495
+ continue;
496
+ }
497
+
379
498
  let cssText = '';
380
499
 
381
500
  // Try to access cssRules (may fail due to CORS)
@@ -426,10 +545,44 @@ export default class RbirdScreenshotManager {
426
545
 
427
546
  console.log(`[Screenshot] Successfully processed ${processedStylesheets.length} stylesheets`);
428
547
 
429
- // Remove original link tags
548
+ // Remove original link tags except Google Fonts and icon fonts
430
549
  const links = clone.querySelectorAll('link[rel="stylesheet"]');
431
- console.log(`[Screenshot] Removing ${links.length} original link tags`);
432
- links.forEach(link => link.remove());
550
+ console.log(`[Screenshot] Removing ${links.length} original link tags (keeping font/icon services)`);
551
+ links.forEach(link => {
552
+ const href = link.getAttribute('href');
553
+ // Keep Google Fonts, icon fonts, and other font service links
554
+ const isFontOrIconService = href && (
555
+ // Google Fonts
556
+ href.includes('fonts.googleapis.com') ||
557
+ href.includes('fonts.gstatic.com') ||
558
+ // TypeKit
559
+ href.includes('use.typekit.net') ||
560
+ href.includes('cloud.typography.com') ||
561
+ // FontAwesome
562
+ href.includes('fontawesome') ||
563
+ href.includes('font-awesome') ||
564
+ href.includes('fa-') ||
565
+ // Bootstrap Icons
566
+ href.includes('bootstrap-icons') ||
567
+ // Material Icons
568
+ href.includes('material-icons') ||
569
+ href.includes('material-symbols') ||
570
+ // Ionicons
571
+ href.includes('ionicons') ||
572
+ // Feather Icons
573
+ href.includes('feather-icons') ||
574
+ // Heroicons
575
+ href.includes('heroicons') ||
576
+ // Generic icon detection
577
+ href.includes('/icons') ||
578
+ href.includes('icon.css') ||
579
+ href.includes('icons.css')
580
+ );
581
+
582
+ if (!isFontOrIconService) {
583
+ link.remove();
584
+ }
585
+ });
433
586
  }
434
587
 
435
588
  /**
@@ -0,0 +1,148 @@
1
+ const FormData = require('form-data');
2
+ const fs = require('fs');
3
+ const axios = require('axios');
4
+ const path = require('path');
5
+
6
+ async function testImageResize() {
7
+ console.log('Testing Image Resize Endpoint...\n');
8
+
9
+ // Use the emoji test image from the project
10
+ const testImagePath = path.join(__dirname, 'emoji-test.png');
11
+
12
+ if (!fs.existsSync(testImagePath)) {
13
+ console.error('Test image not found:', testImagePath);
14
+ console.log('Please ensure emoji-test.png exists in the project root');
15
+ return;
16
+ }
17
+
18
+ const backendUrl = 'http://releasebook-backend-dev.eu-central-1.elasticbeanstalk.com/api/images/resize';
19
+
20
+ // Test 1: Resize with width only (maintain aspect ratio)
21
+ console.log('Test 1: Resize with width only (maintain aspect ratio)');
22
+ try {
23
+ const formData1 = new FormData();
24
+ formData1.append('file', fs.createReadStream(testImagePath));
25
+ formData1.append('width', '300');
26
+ formData1.append('maintainAspectRatio', 'true');
27
+
28
+ const response1 = await axios.post(backendUrl, formData1, {
29
+ headers: formData1.getHeaders(),
30
+ responseType: 'arraybuffer'
31
+ });
32
+
33
+ console.log('✓ Status:', response1.status);
34
+ console.log('✓ Content-Type:', response1.headers['content-type']);
35
+ console.log('✓ Content-Length:', response1.headers['content-length'], 'bytes');
36
+
37
+ // Save the resized image
38
+ const outputPath1 = path.join(__dirname, 'resized-test-1.png');
39
+ fs.writeFileSync(outputPath1, response1.data);
40
+ console.log('✓ Saved resized image to:', outputPath1);
41
+ console.log();
42
+ } catch (error) {
43
+ console.error('✗ Error:', error.response?.status, error.response?.statusText || error.message);
44
+ console.log();
45
+ }
46
+
47
+ // Test 2: Resize with height only (maintain aspect ratio)
48
+ console.log('Test 2: Resize with height only (maintain aspect ratio)');
49
+ try {
50
+ const formData2 = new FormData();
51
+ formData2.append('file', fs.createReadStream(testImagePath));
52
+ formData2.append('height', '200');
53
+ formData2.append('maintainAspectRatio', 'true');
54
+
55
+ const response2 = await axios.post(backendUrl, formData2, {
56
+ headers: formData2.getHeaders(),
57
+ responseType: 'arraybuffer'
58
+ });
59
+
60
+ console.log('✓ Status:', response2.status);
61
+ console.log('✓ Content-Type:', response2.headers['content-type']);
62
+ console.log('✓ Content-Length:', response2.headers['content-length'], 'bytes');
63
+
64
+ const outputPath2 = path.join(__dirname, 'resized-test-2.png');
65
+ fs.writeFileSync(outputPath2, response2.data);
66
+ console.log('✓ Saved resized image to:', outputPath2);
67
+ console.log();
68
+ } catch (error) {
69
+ console.error('✗ Error:', error.response?.status, error.response?.statusText || error.message);
70
+ console.log();
71
+ }
72
+
73
+ // Test 3: Resize with both width and height (maintain aspect ratio - should fit within bounds)
74
+ console.log('Test 3: Resize with both width and height (maintain aspect ratio)');
75
+ try {
76
+ const formData3 = new FormData();
77
+ formData3.append('file', fs.createReadStream(testImagePath));
78
+ formData3.append('width', '400');
79
+ formData3.append('height', '300');
80
+ formData3.append('maintainAspectRatio', 'true');
81
+
82
+ const response3 = await axios.post(backendUrl, formData3, {
83
+ headers: formData3.getHeaders(),
84
+ responseType: 'arraybuffer'
85
+ });
86
+
87
+ console.log('✓ Status:', response3.status);
88
+ console.log('✓ Content-Type:', response3.headers['content-type']);
89
+ console.log('✓ Content-Length:', response3.headers['content-length'], 'bytes');
90
+
91
+ const outputPath3 = path.join(__dirname, 'resized-test-3.png');
92
+ fs.writeFileSync(outputPath3, response3.data);
93
+ console.log('✓ Saved resized image to:', outputPath3);
94
+ console.log();
95
+ } catch (error) {
96
+ console.error('✗ Error:', error.response?.status, error.response?.statusText || error.message);
97
+ console.log();
98
+ }
99
+
100
+ // Test 4: Resize with exact dimensions (no aspect ratio)
101
+ console.log('Test 4: Resize with exact dimensions (no aspect ratio maintenance)');
102
+ try {
103
+ const formData4 = new FormData();
104
+ formData4.append('file', fs.createReadStream(testImagePath));
105
+ formData4.append('width', '500');
106
+ formData4.append('height', '200');
107
+ formData4.append('maintainAspectRatio', 'false');
108
+
109
+ const response4 = await axios.post(backendUrl, formData4, {
110
+ headers: formData4.getHeaders(),
111
+ responseType: 'arraybuffer'
112
+ });
113
+
114
+ console.log('✓ Status:', response4.status);
115
+ console.log('✓ Content-Type:', response4.headers['content-type']);
116
+ console.log('✓ Content-Length:', response4.headers['content-length'], 'bytes');
117
+
118
+ const outputPath4 = path.join(__dirname, 'resized-test-4.png');
119
+ fs.writeFileSync(outputPath4, response4.data);
120
+ console.log('✓ Saved resized image to:', outputPath4);
121
+ console.log();
122
+ } catch (error) {
123
+ console.error('✗ Error:', error.response?.status, error.response?.statusText || error.message);
124
+ console.log();
125
+ }
126
+
127
+ // Test 5: Error case - no dimensions provided
128
+ console.log('Test 5: Error case - no dimensions provided');
129
+ try {
130
+ const formData5 = new FormData();
131
+ formData5.append('file', fs.createReadStream(testImagePath));
132
+
133
+ const response5 = await axios.post(backendUrl, formData5, {
134
+ headers: formData5.getHeaders(),
135
+ responseType: 'arraybuffer'
136
+ });
137
+
138
+ console.log('✗ Should have failed but got status:', response5.status);
139
+ console.log();
140
+ } catch (error) {
141
+ console.log('✓ Expected error:', error.response?.status, error.response?.statusText || error.message);
142
+ console.log();
143
+ }
144
+
145
+ console.log('Image resize endpoint tests completed!');
146
+ }
147
+
148
+ testImageResize().catch(console.error);