image-validation.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. // Image URL validation utilities for the ImageInput component
  2. import { apiClient } from './api';
  3. export interface ImageValidationResult {
  4. isValid: boolean;
  5. error?: string;
  6. detectedType?: string;
  7. filename?: string;
  8. isCorsBlocked?: boolean;
  9. base64Data?: string;
  10. tempUrl?: string;
  11. tempFilename?: string;
  12. }
  13. export interface ImageInputMode {
  14. type: 'file' | 'url' | null;
  15. value?: File | string;
  16. }
  17. // Supported image MIME types and extensions
  18. export const IMAGE_MIME_TYPES = [
  19. 'image/jpeg',
  20. 'image/jpg',
  21. 'image/png',
  22. 'image/gif',
  23. 'image/webp',
  24. 'image/bmp',
  25. 'image/svg+xml',
  26. 'image/tiff'
  27. ] as const;
  28. export const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tiff'] as const;
  29. export type ImageExtension = typeof IMAGE_EXTENSIONS[number];
  30. // Extract extension from filename or URL
  31. export function extractExtension(filenameOrUrl: string): string | null {
  32. // Remove query parameters and fragments
  33. const cleanUrl = filenameOrUrl.split('?')[0].split('#')[0];
  34. // Get the last part after dot
  35. const extension = cleanUrl.split('.').pop()?.toLowerCase();
  36. return extension || null;
  37. }
  38. // Check if URL contains image extension (more flexible than just checking the end)
  39. export function hasImageExtension(url: string): boolean {
  40. const extension = extractExtension(url);
  41. return extension ? isValidImageExtension(extension) : false;
  42. }
  43. // Check if extension is a valid image extension
  44. export function isValidImageExtension(extension: string): extension is ImageExtension {
  45. return IMAGE_EXTENSIONS.includes(extension as ImageExtension);
  46. }
  47. // Get MIME type from extension
  48. export function getMimeTypeFromExtension(extension: string): string {
  49. const extToMime: Record<ImageExtension, string> = {
  50. 'jpg': 'image/jpeg',
  51. 'jpeg': 'image/jpeg',
  52. 'png': 'image/png',
  53. 'gif': 'image/gif',
  54. 'webp': 'image/webp',
  55. 'bmp': 'image/bmp',
  56. 'svg': 'image/svg+xml',
  57. 'tiff': 'image/tiff'
  58. };
  59. return extToMime[extension as ImageExtension] || 'image/unknown';
  60. }
  61. // Validate file object
  62. export function validateFile(file: File): ImageValidationResult {
  63. // Check if file is empty
  64. if (!file || file.size === 0) {
  65. return {
  66. isValid: false,
  67. error: 'File is empty or invalid'
  68. };
  69. }
  70. // Check file size (max 10MB)
  71. const maxSize = 10 * 1024 * 1024; // 10MB
  72. if (file.size > maxSize) {
  73. return {
  74. isValid: false,
  75. error: `File size (${(file.size / 1024 / 1024).toFixed(1)}MB) exceeds 10MB limit`
  76. };
  77. }
  78. // Check MIME type
  79. if (!IMAGE_MIME_TYPES.includes(file.type as typeof IMAGE_MIME_TYPES[number])) {
  80. return {
  81. isValid: false,
  82. error: `File type '${file.type}' is not supported. Supported formats: ${IMAGE_EXTENSIONS.join(', ')}`
  83. };
  84. }
  85. return {
  86. isValid: true,
  87. detectedType: file.type,
  88. filename: file.name
  89. };
  90. }
  91. // Basic URL validation without network requests
  92. export function validateUrlFormat(url: string): ImageValidationResult {
  93. try {
  94. new URL(url);
  95. } catch {
  96. return {
  97. isValid: false,
  98. error: 'Invalid URL format'
  99. };
  100. }
  101. // Check if it has a valid image extension
  102. if (!hasImageExtension(url)) {
  103. return {
  104. isValid: false,
  105. error: 'URL does not point to a supported image format. Supported formats: ' + IMAGE_EXTENSIONS.join(', ')
  106. };
  107. }
  108. // Basic validation passed - return the extension-based result
  109. const extension = extractExtension(url);
  110. return {
  111. isValid: true,
  112. detectedType: extension ? getMimeTypeFromExtension(extension) : undefined,
  113. filename: url.split('/').pop() || url
  114. };
  115. }
  116. // Validate image URL using server-side download endpoint
  117. export async function validateImageUrlWithBase64(url: string): Promise<ImageValidationResult> {
  118. // First validate URL format and extension
  119. const formatValidation = validateUrlFormat(url);
  120. if (!formatValidation.isValid) {
  121. return formatValidation;
  122. }
  123. try {
  124. // Use server-side download endpoint to avoid CORS issues
  125. const result = await apiClient.downloadImageFromUrl(url);
  126. return {
  127. isValid: true,
  128. detectedType: result.mimeType,
  129. filename: result.filename,
  130. base64Data: result.base64Data,
  131. tempUrl: result.tempUrl,
  132. tempFilename: result.tempFilename
  133. };
  134. } catch (error) {
  135. return {
  136. isValid: false,
  137. error: `Failed to download image from URL: ${error instanceof Error ? error.message : 'Unknown error'}`
  138. };
  139. }
  140. }
  141. // Validate image input (file or URL) with base64 conversion
  142. export async function validateImageInput(
  143. input: File | string
  144. ): Promise<ImageValidationResult> {
  145. if (input instanceof File) {
  146. return validateFile(input);
  147. } else {
  148. return await validateImageUrlWithBase64(input);
  149. }
  150. }
  151. // Get display name for image input
  152. export function getImageDisplayName(input: File | string): string {
  153. if (input instanceof File) {
  154. return input.name;
  155. } else {
  156. const url = new URL(input);
  157. return url.pathname.split('/').pop() || input;
  158. }
  159. }
  160. // Convert file to data URL for preview
  161. export function fileToDataURL(file: File): Promise<string> {
  162. return new Promise((resolve, reject) => {
  163. const reader = new FileReader();
  164. reader.onload = () => resolve(reader.result as string);
  165. reader.onerror = reject;
  166. reader.readAsDataURL(file);
  167. });
  168. }