image-validation.ts 5.1 KB

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