Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a018c36d67 |
8 changed files with 197 additions and 1557 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
.kiro/
|
.kiro/
|
||||||
html-to-image/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
108
content.js
108
content.js
|
|
@ -411,42 +411,90 @@ class ScreenshotSelector {
|
||||||
element.classList.remove('screenshot-highlight');
|
element.classList.remove('screenshot-highlight');
|
||||||
this.removeScrollableIndicators();
|
this.removeScrollableIndicators();
|
||||||
|
|
||||||
|
// Store original styles
|
||||||
|
const originalStyles = {
|
||||||
|
overflow: element.style.overflow,
|
||||||
|
overflowY: element.style.overflowY,
|
||||||
|
overflowX: element.style.overflowX,
|
||||||
|
height: element.style.height,
|
||||||
|
maxHeight: element.style.maxHeight,
|
||||||
|
position: element.style.position,
|
||||||
|
zIndex: element.style.zIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporarily modify element for full capture
|
||||||
|
element.style.overflow = 'visible';
|
||||||
|
element.style.overflowY = 'visible';
|
||||||
|
element.style.overflowX = 'visible';
|
||||||
|
element.style.height = `${element.scrollHeight}px`;
|
||||||
|
element.style.maxHeight = 'none';
|
||||||
|
|
||||||
// Wait for fonts and external resources to load
|
// Wait for fonts and external resources to load
|
||||||
await this.waitForFontsAndResources(element);
|
await this.waitForFontsAndResources(element);
|
||||||
|
|
||||||
if (!window.htmlToImage || typeof window.htmlToImage.toCanvas !== 'function') {
|
// Enhanced html2canvas configuration
|
||||||
throw new Error('Screenshot renderer not available: htmlToImage.toCanvas() missing');
|
const canvas = await html2canvas(element, {
|
||||||
}
|
useCORS: true,
|
||||||
|
allowTaint: false,
|
||||||
|
backgroundColor: this.background === 'transparent' ? null :
|
||||||
|
this.background === 'white' ? '#ffffff' : '#000000',
|
||||||
|
|
||||||
const backgroundColor =
|
// Improved rendering options
|
||||||
this.background === 'transparent'
|
scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images
|
||||||
? undefined
|
logging: false, // Disable logging for cleaner console
|
||||||
: this.background === 'white'
|
|
||||||
? '#ffffff'
|
|
||||||
: '#000000';
|
|
||||||
|
|
||||||
// Use html-to-image to render the element to a canvas.
|
// Better handling of external resources
|
||||||
// Important: avoid mutating the live DOM during capture (it can cause reflow/layout drift).
|
imageTimeout: 15000, // Wait longer for images to load
|
||||||
const canvas = await window.htmlToImage.toCanvas(element, {
|
|
||||||
backgroundColor,
|
// Font handling and style preservation
|
||||||
pixelRatio: window.devicePixelRatio || 1,
|
onclone: (clonedDoc, clonedElementParam) => {
|
||||||
// Brightspace/D2L and many sites have CORS-protected stylesheets/fonts which cause noisy,
|
// Ensure all stylesheets are loaded in cloned document
|
||||||
// expected DOMExceptions during CSS rule inspection. Keep logs clean by default.
|
const originalStyleSheets = Array.from(document.styleSheets);
|
||||||
logLevel: 'silent',
|
const clonedHead = clonedDoc.head;
|
||||||
// If the page heavily caches fonts/images, enabling this can help but may slow capture
|
|
||||||
cacheBust: false,
|
// Copy all stylesheets to cloned document
|
||||||
// Capture full scrollable area without reflowing the live element
|
originalStyleSheets.forEach(styleSheet => {
|
||||||
width: element.scrollWidth,
|
try {
|
||||||
height: element.scrollHeight,
|
if (styleSheet.href) {
|
||||||
style: {
|
// External stylesheet
|
||||||
overflow: 'visible',
|
const link = clonedDoc.createElement('link');
|
||||||
overflowX: 'visible',
|
link.rel = 'stylesheet';
|
||||||
overflowY: 'visible',
|
link.href = styleSheet.href;
|
||||||
height: `${element.scrollHeight}px`,
|
link.type = 'text/css';
|
||||||
maxHeight: 'none',
|
clonedHead.appendChild(link);
|
||||||
},
|
} else if (styleSheet.ownerNode && styleSheet.ownerNode.tagName === 'STYLE') {
|
||||||
|
// Inline stylesheet
|
||||||
|
const style = clonedDoc.createElement('style');
|
||||||
|
style.type = 'text/css';
|
||||||
|
try {
|
||||||
|
const cssText = Array.from(styleSheet.cssRules).map(rule => rule.cssText).join('\n');
|
||||||
|
style.textContent = cssText;
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to original text content
|
||||||
|
style.textContent = styleSheet.ownerNode.textContent;
|
||||||
|
}
|
||||||
|
clonedHead.appendChild(style);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Skip stylesheets that can't be accessed (CORS issues)
|
||||||
|
console.log('Skipped stylesheet due to CORS:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply computed styles to preserve appearance
|
||||||
|
const clonedElement = clonedElementParam;
|
||||||
|
const originalElement = element; // from outer scope
|
||||||
|
if (clonedElement && originalElement) {
|
||||||
|
this.preserveComputedStyles(clonedElement, originalElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedDoc;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore original styles
|
||||||
|
Object.assign(element.style, originalStyles);
|
||||||
|
|
||||||
// Conditionally download the image based on saveToPc preference
|
// Conditionally download the image based on saveToPc preference
|
||||||
if (this.saveToPc) {
|
if (this.saveToPc) {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
|
|
@ -549,7 +597,7 @@ class ScreenshotSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
if (error.message.includes('htmlToImage') || error.message.includes('html-to-image')) {
|
if (error.message.includes('html2canvas')) {
|
||||||
errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`;
|
errorMessage = `Screenshot rendering failed. Try selecting a different element or refresh the page.${operationContext}`;
|
||||||
} else if (error.message.includes('timeout')) {
|
} else if (error.message.includes('timeout')) {
|
||||||
errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`;
|
errorMessage = `Screenshot capture timed out. Try selecting a smaller area or simpler element.${operationContext}`;
|
||||||
|
|
|
||||||
1389
html-to-image.js
1389
html-to-image.js
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -24,7 +24,7 @@
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["html-to-image.js", "content.js"],
|
"js": ["html2canvas-pro.min.js", "content.js"],
|
||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,8 @@ global.window = {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock html-to-image
|
// Mock html2canvas
|
||||||
global.htmlToImage = {
|
global.html2canvas = jest.fn();
|
||||||
toCanvas: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
let screenshotSelector;
|
let screenshotSelector;
|
||||||
|
|
@ -104,7 +102,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
|
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
|
||||||
};
|
};
|
||||||
|
|
||||||
global.htmlToImage.toCanvas = jest.fn().mockResolvedValue(mockCanvas);
|
global.html2canvas = jest.fn().mockResolvedValue(mockCanvas);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -185,17 +183,11 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
async captureElement(element) {
|
async captureElement(element) {
|
||||||
try {
|
try {
|
||||||
// Generate canvas with background-specific settings
|
// Generate canvas with background-specific settings
|
||||||
const backgroundColor =
|
const canvas = await global.html2canvas(element, {
|
||||||
this.background === 'transparent'
|
backgroundColor: this.background === 'transparent' ? null :
|
||||||
? undefined
|
this.background === 'white' ? '#ffffff' : '#000000',
|
||||||
: this.background === 'white'
|
useCORS: true,
|
||||||
? '#ffffff'
|
allowTaint: false
|
||||||
: '#000000';
|
|
||||||
|
|
||||||
const canvas = await global.htmlToImage.toCanvas(element, {
|
|
||||||
backgroundColor,
|
|
||||||
pixelRatio: global.window.devicePixelRatio || 1,
|
|
||||||
cacheBust: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always download first
|
// Always download first
|
||||||
|
|
@ -269,8 +261,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
|
|
||||||
const result = await screenshotSelector.captureElement(mockElement);
|
const result = await screenshotSelector.captureElement(mockElement);
|
||||||
|
|
||||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
||||||
backgroundColor: undefined, // Transparent background
|
backgroundColor: null, // Transparent background
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -284,8 +278,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
|
|
||||||
const result = await screenshotSelector.captureElement(mockElement);
|
const result = await screenshotSelector.captureElement(mockElement);
|
||||||
|
|
||||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
||||||
backgroundColor: '#000000', // Black background
|
backgroundColor: '#000000', // Black background
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
@ -298,8 +294,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||||
|
|
||||||
const result = await screenshotSelector.captureElement(mockElement);
|
const result = await screenshotSelector.captureElement(mockElement);
|
||||||
|
|
||||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
||||||
backgroundColor: '#ffffff', // White background
|
backgroundColor: '#ffffff', // White background
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,8 @@ global.window = {
|
||||||
CSS: { escape: jest.fn(str => str) }
|
CSS: { escape: jest.fn(str => str) }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock html-to-image
|
// Mock html2canvas
|
||||||
global.htmlToImage = {
|
global.html2canvas = jest.fn();
|
||||||
toCanvas: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock ScreenshotSelector class for testing
|
// Mock ScreenshotSelector class for testing
|
||||||
class MockScreenshotSelector {
|
class MockScreenshotSelector {
|
||||||
|
|
@ -128,18 +126,10 @@ class MockScreenshotSelector {
|
||||||
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
|
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock html-to-image
|
// Mock html2canvas
|
||||||
global.htmlToImage.toCanvas.mockResolvedValue(mockCanvas);
|
global.html2canvas.mockResolvedValue(mockCanvas);
|
||||||
|
|
||||||
const canvas = await global.htmlToImage.toCanvas(element, {
|
const canvas = await global.html2canvas(element);
|
||||||
backgroundColor: this.background === 'transparent'
|
|
||||||
? undefined
|
|
||||||
: this.background === 'white'
|
|
||||||
? '#ffffff'
|
|
||||||
: '#000000',
|
|
||||||
pixelRatio: global.window.devicePixelRatio || 1,
|
|
||||||
cacheBust: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Conditionally download based on saveToPc setting
|
// Conditionally download based on saveToPc setting
|
||||||
if (this.saveToPc) {
|
if (this.saveToPc) {
|
||||||
|
|
@ -344,11 +334,8 @@ describe('Save to PC and Clipboard Combinations', () => {
|
||||||
test('should still create canvas even when both outputs are disabled', async () => {
|
test('should still create canvas even when both outputs are disabled', async () => {
|
||||||
await screenshotSelector.captureElement(mockElement);
|
await screenshotSelector.captureElement(mockElement);
|
||||||
|
|
||||||
// html-to-image should still be called (needed for potential future operations)
|
// html2canvas should still be called (needed for potential future operations)
|
||||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(
|
expect(global.html2canvas).toHaveBeenCalledWith(mockElement);
|
||||||
mockElement,
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -238,10 +238,8 @@ beforeEach(() => {
|
||||||
global.mockDOMAPIs();
|
global.mockDOMAPIs();
|
||||||
global.mockClipboardAPI('supported');
|
global.mockClipboardAPI('supported');
|
||||||
|
|
||||||
// Mock html-to-image
|
// Mock html2canvas
|
||||||
global.htmlToImage = {
|
global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas());
|
||||||
toCanvas: jest.fn().mockResolvedValue(global.createMockCanvas()),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup after each test
|
// Cleanup after each test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue