286 lines
8.9 KiB
JavaScript
286 lines
8.9 KiB
JavaScript
/**
|
|
* File Processor
|
|
*
|
|
* Handles processing of dropped files (images, videos, and JSON configs).
|
|
* Extracts file reading and type detection logic for cleaner separation of concerns.
|
|
*
|
|
* @module FileProcessor
|
|
*/
|
|
|
|
/** Module identifier for console logging */
|
|
const MODULE_LOG_PREFIX = 'Myxeliums Battlemap Importer';
|
|
|
|
/**
|
|
* @typedef {Object} ProcessedImageData
|
|
* @property {string} dataUrl - Base64 data URL of the image
|
|
* @property {string} filename - Original filename
|
|
* @property {File} file - The original File object
|
|
* @property {boolean} isVideo - Always false for images
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ProcessedVideoData
|
|
* @property {string} blobUrl - Blob URL for the video
|
|
* @property {string} filename - Original filename
|
|
* @property {File} file - The original File object
|
|
* @property {boolean} isVideo - Always true for videos
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} ProcessedJsonData
|
|
* @property {Object} parsedContent - The parsed JSON object
|
|
* @property {string} filename - Original filename
|
|
*/
|
|
|
|
/** Supported image file extensions */
|
|
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
|
|
|
|
/** Supported video file extensions */
|
|
const VIDEO_EXTENSIONS = ['.webm', '.mp4', '.mov', '.m4v', '.avi', '.mkv', '.ogv', '.ogg'];
|
|
|
|
/**
|
|
* Service class for processing dropped files.
|
|
* Handles reading files and determining their types.
|
|
*/
|
|
export class FileProcessor {
|
|
|
|
/**
|
|
* Process a dropped image file and return its data.
|
|
* Reads the file as a base64 data URL.
|
|
*
|
|
* @param {File} imageFile - The image file to process
|
|
* @returns {Promise<ProcessedImageData>} Processed image data
|
|
*
|
|
* @example
|
|
* const processor = new FileProcessor();
|
|
* const imageData = await processor.processImageFile(droppedFile);
|
|
* // imageData.dataUrl contains the base64 image
|
|
*/
|
|
processImageFile(imageFile) {
|
|
return new Promise((resolve, reject) => {
|
|
const fileReader = new FileReader();
|
|
|
|
fileReader.onload = (loadEvent) => {
|
|
resolve({
|
|
dataUrl: loadEvent.target.result,
|
|
filename: imageFile.name,
|
|
file: imageFile,
|
|
isVideo: false
|
|
});
|
|
};
|
|
|
|
fileReader.onerror = (error) => {
|
|
reject(new Error(`Failed to read image file: ${error.message}`));
|
|
};
|
|
|
|
fileReader.readAsDataURL(imageFile);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process a dropped video file and return its data.
|
|
* Creates a blob URL for the video.
|
|
*
|
|
* @param {File} videoFile - The video file to process
|
|
* @returns {ProcessedVideoData} Processed video data
|
|
*
|
|
* @example
|
|
* const processor = new FileProcessor();
|
|
* const videoData = processor.processVideoFile(droppedFile);
|
|
* // videoData.blobUrl can be used as video src
|
|
*/
|
|
processVideoFile(videoFile) {
|
|
const blobUrl = URL.createObjectURL(videoFile);
|
|
|
|
return {
|
|
blobUrl: blobUrl,
|
|
filename: videoFile.name,
|
|
file: videoFile,
|
|
isVideo: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Process a dropped JSON configuration file.
|
|
* Reads and parses the JSON content.
|
|
*
|
|
* @param {File} jsonFile - The JSON file to process
|
|
* @returns {Promise<ProcessedJsonData>} Processed JSON data
|
|
* @throws {Error} If the JSON is invalid
|
|
*
|
|
* @example
|
|
* const processor = new FileProcessor();
|
|
* const config = await processor.processJsonFile(droppedFile);
|
|
* // config.parsedContent contains the parsed object
|
|
*/
|
|
processJsonFile(jsonFile) {
|
|
return new Promise((resolve, reject) => {
|
|
const fileReader = new FileReader();
|
|
|
|
fileReader.onload = (loadEvent) => {
|
|
try {
|
|
const parsedContent = JSON.parse(loadEvent.target.result);
|
|
resolve({
|
|
parsedContent: parsedContent,
|
|
filename: jsonFile.name
|
|
});
|
|
} catch (parseError) {
|
|
reject(new Error(`Invalid JSON: ${parseError.message}`));
|
|
}
|
|
};
|
|
|
|
fileReader.onerror = (error) => {
|
|
reject(new Error(`Failed to read JSON file: ${error.message}`));
|
|
};
|
|
|
|
fileReader.readAsText(jsonFile);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Determine the type of a file based on MIME type and extension.
|
|
*
|
|
* @param {File} file - The file to classify
|
|
* @returns {'image' | 'video' | 'json' | 'unknown'} The file type category
|
|
*
|
|
* @example
|
|
* const fileType = processor.getFileType(droppedFile);
|
|
* if (fileType === 'image') { ... }
|
|
*/
|
|
getFileType(file) {
|
|
const lowercaseFilename = file.name.toLowerCase();
|
|
const mimeType = file.type.toLowerCase();
|
|
|
|
// Check by MIME type first
|
|
if (mimeType.startsWith('image/')) {
|
|
return 'image';
|
|
}
|
|
|
|
if (mimeType.startsWith('video/')) {
|
|
return 'video';
|
|
}
|
|
|
|
if (mimeType === 'application/json') {
|
|
return 'json';
|
|
}
|
|
|
|
// Fall back to extension checking
|
|
if (this.hasExtension(lowercaseFilename, IMAGE_EXTENSIONS)) {
|
|
return 'image';
|
|
}
|
|
|
|
if (this.hasExtension(lowercaseFilename, VIDEO_EXTENSIONS)) {
|
|
return 'video';
|
|
}
|
|
|
|
if (lowercaseFilename.endsWith('.json')) {
|
|
return 'json';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Check if a filename has one of the specified extensions.
|
|
*
|
|
* @param {string} filename - The filename to check (lowercase)
|
|
* @param {string[]} extensions - Array of extensions to match
|
|
* @returns {boolean} True if filename ends with one of the extensions
|
|
*/
|
|
hasExtension(filename, extensions) {
|
|
return extensions.some(ext => filename.endsWith(ext));
|
|
}
|
|
|
|
/**
|
|
* Revoke a blob URL to free memory.
|
|
* Safe to call with non-blob URLs (will be ignored).
|
|
*
|
|
* @param {string} url - The URL to potentially revoke
|
|
*/
|
|
revokeBlobUrl(url) {
|
|
try {
|
|
if (url && typeof url === 'string' && url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
} catch (_error) {
|
|
// Ignore errors during cleanup
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the dimensions of an image from a data URL.
|
|
*
|
|
* @param {string} imageDataUrl - Base64 data URL of the image
|
|
* @returns {Promise<{width: number, height: number}>} Image dimensions
|
|
*/
|
|
async getImageDimensions(imageDataUrl) {
|
|
return new Promise((resolve, reject) => {
|
|
const imageElement = new Image();
|
|
|
|
imageElement.onload = () => {
|
|
resolve({
|
|
width: imageElement.width,
|
|
height: imageElement.height
|
|
});
|
|
};
|
|
|
|
imageElement.onerror = () => {
|
|
reject(new Error('Failed to load image for dimension measurement'));
|
|
};
|
|
|
|
imageElement.src = imageDataUrl;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the dimensions of a video from a URL.
|
|
*
|
|
* @param {string} videoUrl - URL or blob URL of the video
|
|
* @returns {Promise<{width: number|undefined, height: number|undefined}>} Video dimensions
|
|
*/
|
|
async getVideoDimensions(videoUrl) {
|
|
return new Promise((resolve) => {
|
|
const videoElement = document.createElement('video');
|
|
videoElement.preload = 'metadata';
|
|
|
|
const cleanup = () => {
|
|
videoElement.onloadedmetadata = null;
|
|
videoElement.onerror = null;
|
|
};
|
|
|
|
videoElement.onloadedmetadata = () => {
|
|
cleanup();
|
|
resolve({
|
|
width: videoElement.videoWidth || undefined,
|
|
height: videoElement.videoHeight || undefined
|
|
});
|
|
};
|
|
|
|
videoElement.onerror = () => {
|
|
cleanup();
|
|
resolve({ width: undefined, height: undefined });
|
|
};
|
|
|
|
videoElement.src = videoUrl;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get dimensions of media (either image or video).
|
|
*
|
|
* @param {Object} mediaData - Media data object with data/blobUrl and isVideo flag
|
|
* @returns {Promise<{width: number|undefined, height: number|undefined}>} Media dimensions
|
|
*/
|
|
async getMediaDimensions(mediaData) {
|
|
try {
|
|
if (mediaData?.isVideo) {
|
|
return await this.getVideoDimensions(mediaData.data || mediaData.blobUrl);
|
|
}
|
|
return await this.getImageDimensions(mediaData?.data || mediaData?.dataUrl);
|
|
} catch (error) {
|
|
console.warn(`${MODULE_LOG_PREFIX} | Could not read media dimensions:`, error);
|
|
return { width: undefined, height: undefined };
|
|
}
|
|
}
|
|
}
|