You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Extracts a representative image from various sources like video files, YouTube URLs, or audio file cover art.
* The operation is asynchronous and returns a Promise that resolves to an HTMLCanvasElement.
*
* @param {Image} originalImg - A JavaScript Image object. This parameter is required by the prompt's structure but is not used in this function's logic.
* @param {string} sourceUrl - The URL of the source media. This can be a direct link to a video/audio file or a YouTube page URL.
* @param {string} sourceType - The type of the source. Can be 'youtube', 'video', 'audio', or 'auto'. If 'auto', the function will try to guess the type from the URL. Defaults to 'auto'.
* @param {number} timeInSeconds - For video sources, the timestamp (in seconds) from which to capture the frame. Defaults to 0 (the beginning).
* @returns {Promise<HTMLCanvasElement>} A promise that resolves with a canvas element containing the extracted image, or an error message if extraction fails.
*/
async function processImage(originalImg, sourceUrl = '', sourceType = 'auto', timeInSeconds = 0) {
/**
* Creates a canvas with an error message.
* @param {string} message - The error message to display.
* @returns {HTMLCanvasElement} A canvas element with the message drawn on it.
*/
const createErrorCanvas = (message) => {
const canvas = document.createElement('canvas');
canvas.width = 480;
canvas.height = 270;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#2d2d2d';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '16px "Arial", sans-serif'; // Web-safe font
// Simple text wrapping
const words = message.split(' ');
let line = '';
let y = canvas.height / 2 - (words.length > 5 ? 10 : 0);
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > canvas.width - 40 && n > 0) {
ctx.fillText(line, canvas.width / 2, y);
line = words[n] + ' ';
y += 20;
} else {
line = testLine;
}
}
ctx.fillText(line, canvas.width / 2, y);
return canvas;
};
/**
* Extracts a video ID from a YouTube URL.
* @param {string} url - The YouTube URL.
* @returns {string|null} The video ID or null if not found.
*/
const getYouTubeId = (url) => {
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[2].length === 11) ? match[2] : null;
};
/**
* Extracts a thumbnail image from a YouTube URL.
* @param {string} url - The YouTube URL.
* @returns {Promise<HTMLCanvasElement>} A canvas with the thumbnail.
*/
const extractFromYouTube = (url) => {
return new Promise((resolve, reject) => {
const videoId = getYouTubeId(url);
if (!videoId) {
return reject(new Error('Could not extract YouTube video ID.'));
}
const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
const fallbackUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
const img = new Image();
img.crossOrigin = 'Anonymous';
const tryUrl = (src) => {
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
resolve(canvas);
};
img.onerror = () => {
if (src === thumbnailUrl) {
tryUrl(fallbackUrl); // Try fallback quality
} else {
reject(new Error('Failed to load YouTube thumbnail. It may be private or deleted.'));
}
};
img.src = src;
};
tryUrl(thumbnailUrl);
});
};
/**
* Extracts a frame from a video URL at a specific time.
* @param {string} url - The URL to the video file.
* @param {number} time - The time in seconds to capture the frame.
* @returns {Promise<HTMLCanvasElement>} A canvas with the video frame.
*/
const extractFromVideo = (url, time) => {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.crossOrigin = 'Anonymous';
const onSeeked = () => {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
if(canvas.width === 0 || canvas.height === 0){
return reject(new Error('Invalid video dimensions.'));
}
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
cleanup();
resolve(canvas);
};
const onLoadedMetadata = () => {
video.currentTime = Math.min(Math.max(0, time), video.duration);
};
const onError = () => {
reject(new Error('Failed to load video. Check URL and CORS policy.'));
cleanup();
};
const cleanup = () => {
video.removeEventListener('seeked', onSeeked);
video.removeEventListener('loadedmetadata', onLoadedMetadata);
video.removeEventListener('error', onError);
video.remove();
};
video.addEventListener('loadedmetadata', onLoadedMetadata);
video.addEventListener('seeked', onSeeked);
video.addEventListener('error', onError);
video.src = url;
video.load();
});
};
/**
* Extracts cover art from an audio file URL.
* @param {string} url - The URL to the audio file.
* @returns {Promise<HTMLCanvasElement>} A canvas with the cover art.
*/
const extractFromAudio = async (url) => {
// Dynamically import jsmediatags library if not already present
if (typeof window.jsmediatags === 'undefined') {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jsmediatags@3.9.8/dist/jsmediatags.min.js';
document.head.appendChild(script);
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load required audio metadata library.'));
});
}
return new Promise((resolve, reject) => {
window.jsmediatags.read(url, {
onSuccess: (tag) => {
const { picture } = tag.tags;
if (picture) {
const blob = new Blob([new Uint8Array(picture.data)], { type: picture.format });
const objectUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(objectUrl);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Failed to parse image from audio metadata.'));
}
img.src = objectUrl;
} else {
resolve(createErrorCanvas('No cover art found in the audio file.'));
}
},
onError: (error) => {
reject(new Error(`Failed to read audio file: ${error.info}`));
}
});
});
};
/**
* Detects the source type from a URL.
* @param {string} url - The source URL.
* @returns {string} The detected source type ('youtube', 'video', 'audio', 'unknown').
*/
const detectSourceType = (url) => {
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname.toLowerCase();
const pathname = urlObj.pathname.toLowerCase();
if (hostname.includes('youtube.com') || hostname.includes('youtu.be')) {
return 'youtube';
}
if (/\.(mp4|webm|ogv|mov)$/i.test(pathname)) {
return 'video';
}
if (/\.(mp3|flac|m4a|ogg|wav|aac)$/i.test(pathname)) {
return 'audio';
}
} catch(e) { /* Invalid URL, return unknown */ }
return 'unknown';
};
// --- Main Function Logic ---
if (!sourceUrl) {
return createErrorCanvas('No source URL provided.');
}
let type = sourceType === 'auto' ? detectSourceType(sourceUrl) : sourceType;
try {
switch (type) {
case 'youtube':
return await extractFromYouTube(sourceUrl);
case 'video':
return await extractFromVideo(sourceUrl, timeInSeconds);
case 'audio':
return await extractFromAudio(sourceUrl);
default:
return createErrorCanvas(`Unsupported or unknown source type for URL.`);
}
} catch (error) {
console.error("Image Extractor Error:", error);
return createErrorCanvas(error.message || 'An unexpected error occurred.');
}
}
Apply Changes