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.
- package/IMAGE_RESIZE_USAGE.md +173 -0
- package/backend-image-service/image-service/server.js +59 -8
- package/build/index.js +1 -1
- package/package.json +4 -2
- package/published/1.0.66/index.js +1 -1
- package/published/1.0.67/index.js +1 -0
- package/published/1.0.68/index.js +1 -0
- package/published/latest/index.js +1 -1
- package/src/RbirdScreenshotManager.js +158 -5
- package/test-image-resize.js +148 -0
|
@@ -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
|
-
//
|
|
330
|
-
|
|
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 =>
|
|
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);
|