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/
|
||||
.kiro/
|
||||
html-to-image/
|
||||
.vscode/
|
||||
108
content.js
108
content.js
|
|
@ -411,42 +411,90 @@ class ScreenshotSelector {
|
|||
element.classList.remove('screenshot-highlight');
|
||||
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
|
||||
await this.waitForFontsAndResources(element);
|
||||
|
||||
if (!window.htmlToImage || typeof window.htmlToImage.toCanvas !== 'function') {
|
||||
throw new Error('Screenshot renderer not available: htmlToImage.toCanvas() missing');
|
||||
}
|
||||
// Enhanced html2canvas configuration
|
||||
const canvas = await html2canvas(element, {
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: this.background === 'transparent' ? null :
|
||||
this.background === 'white' ? '#ffffff' : '#000000',
|
||||
|
||||
const backgroundColor =
|
||||
this.background === 'transparent'
|
||||
? undefined
|
||||
: this.background === 'white'
|
||||
? '#ffffff'
|
||||
: '#000000';
|
||||
// Improved rendering options
|
||||
scale: window.devicePixelRatio || 1, // Use device pixel ratio for crisp images
|
||||
logging: false, // Disable logging for cleaner console
|
||||
|
||||
// Use html-to-image to render the element to a canvas.
|
||||
// Important: avoid mutating the live DOM during capture (it can cause reflow/layout drift).
|
||||
const canvas = await window.htmlToImage.toCanvas(element, {
|
||||
backgroundColor,
|
||||
pixelRatio: window.devicePixelRatio || 1,
|
||||
// Brightspace/D2L and many sites have CORS-protected stylesheets/fonts which cause noisy,
|
||||
// expected DOMExceptions during CSS rule inspection. Keep logs clean by default.
|
||||
logLevel: 'silent',
|
||||
// If the page heavily caches fonts/images, enabling this can help but may slow capture
|
||||
cacheBust: false,
|
||||
// Capture full scrollable area without reflowing the live element
|
||||
width: element.scrollWidth,
|
||||
height: element.scrollHeight,
|
||||
style: {
|
||||
overflow: 'visible',
|
||||
overflowX: 'visible',
|
||||
overflowY: 'visible',
|
||||
height: `${element.scrollHeight}px`,
|
||||
maxHeight: 'none',
|
||||
},
|
||||
// Better handling of external resources
|
||||
imageTimeout: 15000, // Wait longer for images to load
|
||||
|
||||
// Font handling and style preservation
|
||||
onclone: (clonedDoc, clonedElementParam) => {
|
||||
// Ensure all stylesheets are loaded in cloned document
|
||||
const originalStyleSheets = Array.from(document.styleSheets);
|
||||
const clonedHead = clonedDoc.head;
|
||||
|
||||
// Copy all stylesheets to cloned document
|
||||
originalStyleSheets.forEach(styleSheet => {
|
||||
try {
|
||||
if (styleSheet.href) {
|
||||
// External stylesheet
|
||||
const link = clonedDoc.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = styleSheet.href;
|
||||
link.type = 'text/css';
|
||||
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
|
||||
if (this.saveToPc) {
|
||||
const link = document.createElement('a');
|
||||
|
|
@ -549,7 +597,7 @@ class ScreenshotSelector {
|
|||
}
|
||||
|
||||
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}`;
|
||||
} else if (error.message.includes('timeout')) {
|
||||
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": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["html-to-image.js", "content.js"],
|
||||
"js": ["html2canvas-pro.min.js", "content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -53,10 +53,8 @@ global.window = {
|
|||
})
|
||||
};
|
||||
|
||||
// Mock html-to-image
|
||||
global.htmlToImage = {
|
||||
toCanvas: jest.fn(),
|
||||
};
|
||||
// Mock html2canvas
|
||||
global.html2canvas = jest.fn();
|
||||
|
||||
describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
||||
let screenshotSelector;
|
||||
|
|
@ -104,7 +102,7 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
|||
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) {
|
||||
try {
|
||||
// Generate canvas with background-specific settings
|
||||
const backgroundColor =
|
||||
this.background === 'transparent'
|
||||
? undefined
|
||||
: this.background === 'white'
|
||||
? '#ffffff'
|
||||
: '#000000';
|
||||
|
||||
const canvas = await global.htmlToImage.toCanvas(element, {
|
||||
backgroundColor,
|
||||
pixelRatio: global.window.devicePixelRatio || 1,
|
||||
cacheBust: false
|
||||
const canvas = await global.html2canvas(element, {
|
||||
backgroundColor: this.background === 'transparent' ? null :
|
||||
this.background === 'white' ? '#ffffff' : '#000000',
|
||||
useCORS: true,
|
||||
allowTaint: false
|
||||
});
|
||||
|
||||
// Always download first
|
||||
|
|
@ -269,8 +261,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
|||
|
||||
const result = await screenshotSelector.captureElement(mockElement);
|
||||
|
||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
||||
backgroundColor: undefined, // Transparent background
|
||||
expect(global.html2canvas).toHaveBeenCalledWith(mockElement, expect.objectContaining({
|
||||
backgroundColor: null, // Transparent background
|
||||
useCORS: true,
|
||||
allowTaint: false
|
||||
}));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
|
@ -284,8 +278,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
|||
|
||||
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
|
||||
useCORS: true,
|
||||
allowTaint: false
|
||||
}));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
|
@ -298,8 +294,10 @@ describe('Integration Scenarios - Clipboard with Different Backgrounds', () => {
|
|||
|
||||
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
|
||||
useCORS: true,
|
||||
allowTaint: false
|
||||
}));
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
|
|
|||
|
|
@ -53,10 +53,8 @@ global.window = {
|
|||
CSS: { escape: jest.fn(str => str) }
|
||||
};
|
||||
|
||||
// Mock html-to-image
|
||||
global.htmlToImage = {
|
||||
toCanvas: jest.fn(),
|
||||
};
|
||||
// Mock html2canvas
|
||||
global.html2canvas = jest.fn();
|
||||
|
||||
// Mock ScreenshotSelector class for testing
|
||||
class MockScreenshotSelector {
|
||||
|
|
@ -128,18 +126,10 @@ class MockScreenshotSelector {
|
|||
toDataURL: jest.fn(() => 'data:image/png;base64,mockdata')
|
||||
};
|
||||
|
||||
// Mock html-to-image
|
||||
global.htmlToImage.toCanvas.mockResolvedValue(mockCanvas);
|
||||
// Mock html2canvas
|
||||
global.html2canvas.mockResolvedValue(mockCanvas);
|
||||
|
||||
const canvas = await global.htmlToImage.toCanvas(element, {
|
||||
backgroundColor: this.background === 'transparent'
|
||||
? undefined
|
||||
: this.background === 'white'
|
||||
? '#ffffff'
|
||||
: '#000000',
|
||||
pixelRatio: global.window.devicePixelRatio || 1,
|
||||
cacheBust: false
|
||||
});
|
||||
const canvas = await global.html2canvas(element);
|
||||
|
||||
// Conditionally download based on saveToPc setting
|
||||
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 () => {
|
||||
await screenshotSelector.captureElement(mockElement);
|
||||
|
||||
// html-to-image should still be called (needed for potential future operations)
|
||||
expect(global.htmlToImage.toCanvas).toHaveBeenCalledWith(
|
||||
mockElement,
|
||||
expect.any(Object)
|
||||
);
|
||||
// html2canvas should still be called (needed for potential future operations)
|
||||
expect(global.html2canvas).toHaveBeenCalledWith(mockElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -238,10 +238,8 @@ beforeEach(() => {
|
|||
global.mockDOMAPIs();
|
||||
global.mockClipboardAPI('supported');
|
||||
|
||||
// Mock html-to-image
|
||||
global.htmlToImage = {
|
||||
toCanvas: jest.fn().mockResolvedValue(global.createMockCanvas()),
|
||||
};
|
||||
// Mock html2canvas
|
||||
global.html2canvas = jest.fn().mockResolvedValue(global.createMockCanvas());
|
||||
});
|
||||
|
||||
// Cleanup after each test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue