From 6bd9f067ed792e054f2b9e4309f367bd441b65c9 Mon Sep 17 00:00:00 2001 From: Jaydeep Solanki Date: Mon, 25 Aug 2025 13:21:32 +0530 Subject: [PATCH 1/2] feat(image-color-inverter): add image color inverter tool with upload and processing functionality --- components.d.ts | 23 +++- locales/de.yml | 4 + locales/en.yml | 4 + locales/fr.yml | 4 + locales/zh.yml | 4 + .../image-color-inverter.e2e.spec.ts | 117 ++++++++++++++++ .../image-color-inverter.service.test.ts | 111 +++++++++++++++ .../image-color-inverter.service.ts | 86 ++++++++++++ .../image-color-inverter.vue | 126 ++++++++++++++++++ src/tools/image-color-inverter/index.ts | 13 ++ src/tools/index.ts | 28 +++- 11 files changed, 514 insertions(+), 6 deletions(-) create mode 100644 src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts create mode 100644 src/tools/image-color-inverter/image-color-inverter.service.test.ts create mode 100644 src/tools/image-color-inverter/image-color-inverter.service.ts create mode 100644 src/tools/image-color-inverter/image-color-inverter.vue create mode 100644 src/tools/image-color-inverter/index.ts diff --git a/components.d.ts b/components.d.ts index 3e65c3cc..d944a45d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -91,16 +91,24 @@ declare module '@vue/runtime-core' { IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] 'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] 'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] + IconMdiCamera: typeof import('~icons/mdi/camera')['default'] IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] IconMdiClose: typeof import('~icons/mdi/close')['default'] IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] + IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] + IconMdiDownload: typeof import('~icons/mdi/download')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiHeart: typeof import('~icons/mdi/heart')['default'] + IconMdiPause: typeof import('~icons/mdi/pause')['default'] + IconMdiPlay: typeof import('~icons/mdi/play')['default'] + IconMdiRecord: typeof import('~icons/mdi/record')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] + IconMdiVideo: typeof import('~icons/mdi/video')['default'] + ImageColorInverter: typeof import('./src/tools/image-color-inverter/image-color-inverter.vue')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] @@ -129,21 +137,32 @@ declare module '@vue/runtime-core' { MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] - NCheckbox: typeof import('naive-ui')['NCheckbox'] + NButton: typeof import('naive-ui')['NButton'] + NButtonGroup: typeof import('naive-ui')['NButtonGroup'] + NCard: typeof import('naive-ui')['NCard'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] + NFormItem: typeof import('naive-ui')['NFormItem'] + NGi: typeof import('naive-ui')['NGi'] + NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] + NInputGroup: typeof import('naive-ui')['NInputGroup'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] + NP: typeof import('naive-ui')['NP'] NSpace: typeof import('naive-ui')['NSpace'] - NTable: typeof import('naive-ui')['NTable'] + NSpin: typeof import('naive-ui')['NSpin'] + NText: typeof import('naive-ui')['NText'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] + NUpload: typeof import('naive-ui')['NUpload'] + NUploadDragger: typeof import('naive-ui')['NUploadDragger'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default'] diff --git a/locales/de.yml b/locales/de.yml index 0ccdb71d..7fa16108 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -454,3 +454,7 @@ tools: text-to-binary: title: Text zu ASCII-Binär description: Konvertiere Text in seine ASCII-Binärrepräsentation und umgekehrt. + + image-color-inverter: + title: Bild-Farben-Inverter + description: Laden Sie ein Bild hoch und invertieren Sie seine Farben sofort in Ihrem Browser. Laden Sie das Ergebnis als PNG herunter. diff --git a/locales/en.yml b/locales/en.yml index d03d80d3..1bde4e77 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -392,3 +392,7 @@ tools: text-to-binary: title: Text to ASCII binary description: Convert text to its ASCII binary representation and vice-versa. + + image-color-inverter: + title: Image color inverter + description: Upload an image and invert its colors instantly in your browser. Download the result as PNG. diff --git a/locales/fr.yml b/locales/fr.yml index 86bb47d6..619c8ea0 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -80,3 +80,7 @@ tools: copied: Le token a été copié length: Longueur tokenPlaceholder: Le token... + + image-color-inverter: + title: Inverseur de couleurs d'image + description: Téléchargez une image et inversez ses couleurs instantanément dans votre navigateur. Téléchargez le résultat en PNG. diff --git a/locales/zh.yml b/locales/zh.yml index 97968eb5..503daeb3 100644 --- a/locales/zh.yml +++ b/locales/zh.yml @@ -388,3 +388,7 @@ tools: text-to-binary: title: 文本到 ASCII 二进制 description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。 + + image-color-inverter: + title: 图像颜色反转器 + description: 上传图像并在浏览器中立即反转其颜色。下载PNG格式的结果。 diff --git a/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts b/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts new file mode 100644 index 00000000..a73f9219 --- /dev/null +++ b/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from '@playwright/test'; +import { promises as fs } from 'fs'; +import path from 'path'; + +test.describe('Tool - Image color inverter', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/image-color-inverter'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('Image color inverter - IT Tools'); + }); + + test('Displays file upload area', async ({ page }) => { + // Check if the file upload component is visible + const uploadArea = page.locator('text=Drag and drop an image here, or click to select'); + await expect(uploadArea).toBeVisible(); + }); + + test('Shows error for invalid file type', async ({ page }) => { + // Create a temporary non-image file + const testFilePath = path.join(process.cwd(), 'test-file.txt'); + await fs.writeFile(testFilePath, 'This is not an image'); + + try { + // Upload the text file + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles(testFilePath); + + // Should show error message + const errorAlert = page.locator('.n-alert--error'); + await expect(errorAlert).toBeVisible(); + await expect(errorAlert).toContainText('File must be an image'); + } finally { + // Clean up + await fs.unlink(testFilePath).catch(() => {}); + } + }); + + test('Processes image upload successfully', async ({ page }) => { + // Create a simple test image (1x1 pixel PNG in base64) + const testImageDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + + // Create a blob from the data URL + const response = await fetch(testImageDataUrl); + const blob = await response.blob(); + + // Create a temporary file + const buffer = await blob.arrayBuffer(); + const testFilePath = path.join(process.cwd(), 'test-image.png'); + await fs.writeFile(testFilePath, new Uint8Array(buffer)); + + try { + // Upload the image + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles(testFilePath); + + // Wait for processing to complete + await page.waitForSelector('text=Original Image', { timeout: 5000 }); + await page.waitForSelector('text=Inverted Image', { timeout: 5000 }); + + // Check that both images are displayed + const originalImage = page.locator('text=Original Image'); + const invertedImage = page.locator('text=Inverted Image'); + + await expect(originalImage).toBeVisible(); + await expect(invertedImage).toBeVisible(); + + // Check that download button is available + const downloadButton = page.locator('text=Download PNG'); + await expect(downloadButton).toBeVisible(); + + // Check that copy button is available + const copyButton = page.locator('text=Copy Base64'); + await expect(copyButton).toBeVisible(); + } finally { + // Clean up + await fs.unlink(testFilePath).catch(() => {}); + } + }); + + test('Clear button resets the tool', async ({ page }) => { + // Create a simple test image + const testImageDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + + const response = await fetch(testImageDataUrl); + const blob = await response.blob(); + const buffer = await blob.arrayBuffer(); + const testFilePath = path.join(process.cwd(), 'test-image.png'); + await fs.writeFile(testFilePath, new Uint8Array(buffer)); + + try { + // Upload and process image + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles(testFilePath); + + await page.waitForSelector('text=Inverted Image', { timeout: 5000 }); + + // Click clear button + const clearButton = page.locator('text=Clear & Upload New'); + await clearButton.click(); + + // Check that images are cleared + const originalImage = page.locator('text=Original Image'); + const invertedImage = page.locator('text=Inverted Image'); + + await expect(originalImage).not.toBeVisible(); + await expect(invertedImage).not.toBeVisible(); + + // Check that upload area is visible again + const uploadArea = page.locator('text=Drag and drop an image here, or click to select'); + await expect(uploadArea).toBeVisible(); + } finally { + await fs.unlink(testFilePath).catch(() => {}); + } + }); +}); \ No newline at end of file diff --git a/src/tools/image-color-inverter/image-color-inverter.service.test.ts b/src/tools/image-color-inverter/image-color-inverter.service.test.ts new file mode 100644 index 00000000..96869978 --- /dev/null +++ b/src/tools/image-color-inverter/image-color-inverter.service.test.ts @@ -0,0 +1,111 @@ +import { expect, describe, it, beforeEach } from 'vitest'; +import { invertImageColors } from './image-color-inverter.service'; + +// Mock ImageData for Node.js environment +Object.defineProperty(global, 'ImageData', { + value: class ImageData { + data: Uint8ClampedArray; + width: number; + height: number; + + constructor(data: Uint8ClampedArray | number, width: number, height?: number) { + if (typeof data === 'number') { + // ImageData(width, height) + this.width = data; + this.height = width; + this.data = new Uint8ClampedArray(data * width * 4); + } else { + // ImageData(data, width, height) + this.data = data; + this.width = width; + this.height = height || data.length / (width * 4); + } + } + }, +}); + +// Mock other browser APIs for completeness +Object.defineProperty(global, 'HTMLCanvasElement', { + value: class { + width = 0; + height = 0; + getContext() { + return { + drawImage: () => {}, + getImageData: () => new (global as any).ImageData(new Uint8ClampedArray([255, 0, 128, 255]), 1, 1), + putImageData: () => {}, + }; + } + toDataURL() { return 'data:image/png;base64,mock'; } + }, +}); + +Object.defineProperty(global, 'Image', { + value: class { + onload: () => void = () => {}; + onerror: () => void = () => {}; + set src(value: string) { + setTimeout(() => this.onload(), 0); + } + width = 100; + height = 100; + }, +}); + +describe('image-color-inverter service', () => { + describe('invertImageColors', () => { + it('should invert RGB colors while preserving alpha', () => { + // Create test image data with known values + const originalData = new Uint8ClampedArray([ + 255, 0, 128, 255, // Red=255, Green=0, Blue=128, Alpha=255 + 0, 255, 64, 128, // Red=0, Green=255, Blue=64, Alpha=128 + ]); + const imageData = new (global as any).ImageData(originalData, 2, 1); + + const inverted = invertImageColors(imageData); + + // Check that colors are inverted correctly + expect(inverted.data[0]).toBe(0); // 255 - 255 = 0 + expect(inverted.data[1]).toBe(255); // 255 - 0 = 255 + expect(inverted.data[2]).toBe(127); // 255 - 128 = 127 + expect(inverted.data[3]).toBe(255); // Alpha unchanged + + expect(inverted.data[4]).toBe(255); // 255 - 0 = 255 + expect(inverted.data[5]).toBe(0); // 255 - 255 = 0 + expect(inverted.data[6]).toBe(191); // 255 - 64 = 191 + expect(inverted.data[7]).toBe(128); // Alpha unchanged + }); + + it('should preserve image dimensions', () => { + const originalData = new Uint8ClampedArray([255, 0, 128, 255]); + const imageData = new (global as any).ImageData(originalData, 1, 1); + + const inverted = invertImageColors(imageData); + + expect(inverted.width).toBe(1); + expect(inverted.height).toBe(1); + }); + + it('should handle pure black and white pixels', () => { + const originalData = new Uint8ClampedArray([ + 0, 0, 0, 255, // Pure black + 255, 255, 255, 255, // Pure white + ]); + const imageData = new (global as any).ImageData(originalData, 2, 1); + + const inverted = invertImageColors(imageData); + + // Black should become white + expect(inverted.data[0]).toBe(255); + expect(inverted.data[1]).toBe(255); + expect(inverted.data[2]).toBe(255); + expect(inverted.data[3]).toBe(255); + + // White should become black + expect(inverted.data[4]).toBe(0); + expect(inverted.data[5]).toBe(0); + expect(inverted.data[6]).toBe(0); + expect(inverted.data[7]).toBe(255); + }); + }); +}); \ No newline at end of file diff --git a/src/tools/image-color-inverter/image-color-inverter.service.ts b/src/tools/image-color-inverter/image-color-inverter.service.ts new file mode 100644 index 00000000..eac3e918 --- /dev/null +++ b/src/tools/image-color-inverter/image-color-inverter.service.ts @@ -0,0 +1,86 @@ +export function invertImageFile(file: File): Promise { + return new Promise((resolve, reject) => { + // Validate file type + if (!file.type.startsWith('image/')) { + reject(new Error('File must be an image')); + return; + } + + // Check file size (limit to 10MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + reject(new Error('Image file size must be less than 10MB')); + return; + } + + const reader = new FileReader(); + + reader.onload = () => { + const img = new Image(); + img.src = reader.result as string; + + img.onload = () => { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + + canvas.width = img.width; + canvas.height = img.height; + + // Draw the original image + ctx.drawImage(img, 0, 0); + + // Get image data + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + // Invert colors (RGB channels only, preserve alpha) + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; // Red + data[i + 1] = 255 - data[i + 1]; // Green + data[i + 2] = 255 - data[i + 2]; // Blue + // data[i + 3] is alpha - keep unchanged + } + + // Put the modified image data back + ctx.putImageData(imageData, 0, 0); + + // Convert to base64 PNG + const invertedDataUrl = canvas.toDataURL('image/png'); + resolve(invertedDataUrl); + } catch (err) { + reject(new Error(`Failed to process image: ${err}`)); + } + }; + + img.onerror = () => { + reject(new Error('Failed to load image')); + }; + }; + + reader.onerror = () => { + reject(new Error('Failed to read file')); + }; + + reader.readAsDataURL(file); + }); +} + +export function invertImageColors(imageData: ImageData): ImageData { + const data = new Uint8ClampedArray(imageData.data); + + // Invert RGB values while preserving alpha + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; // Red + data[i + 1] = 255 - data[i + 1]; // Green + data[i + 2] = 255 - data[i + 2]; // Blue + // data[i + 3] is alpha - keep unchanged + } + + return new ImageData(data, imageData.width, imageData.height); +} diff --git a/src/tools/image-color-inverter/image-color-inverter.vue b/src/tools/image-color-inverter/image-color-inverter.vue new file mode 100644 index 00000000..d0999e28 --- /dev/null +++ b/src/tools/image-color-inverter/image-color-inverter.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/tools/image-color-inverter/index.ts b/src/tools/image-color-inverter/index.ts new file mode 100644 index 00000000..c407d2a5 --- /dev/null +++ b/src/tools/image-color-inverter/index.ts @@ -0,0 +1,13 @@ +import { Palette } from '@vicons/tabler'; +import { defineTool } from '../tool'; +import { translate } from '@/plugins/i18n.plugin'; + +export const tool = defineTool({ + name: translate('tools.image-color-inverter.title'), + path: '/image-color-inverter', + description: translate('tools.image-color-inverter.description'), + keywords: ['image', 'color', 'invert', 'negative', 'photo', 'filter', 'effect', 'png', 'jpg', 'jpeg'], + component: () => import('./image-color-inverter.vue'), + icon: Palette, + createdAt: new Date('2025-08-25'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf4..d3f3d4b6 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as imageColorInverter } from './image-color-inverter'; import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -91,7 +92,19 @@ import { tool as yamlViewer } from './yaml-viewer'; export const toolsByCategory: ToolCategory[] = [ { name: 'Crypto', - components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker], + components: [ + tokenGenerator, + hashText, + bcrypt, + uuidGenerator, + ulidGenerator, + cypher, + bip39, + hmacGenerator, + rsaKeyPairGenerator, + passwordStrengthAnalyser, + pdfSignatureChecker, + ], }, { name: 'Converter', @@ -141,7 +154,7 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Images and videos', - components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], + components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, imageColorInverter], }, { name: 'Development', @@ -164,7 +177,14 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Network', - components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator], + components: [ + ipv4SubnetCalculator, + ipv4AddressConverter, + ipv4RangeExpander, + macAddressLookup, + macAddressGenerator, + ipv6UlaGenerator, + ], }, { name: 'Math', @@ -194,5 +214,5 @@ export const toolsByCategory: ToolCategory[] = [ export const tools = toolsByCategory.flatMap(({ components }) => components); export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) => - components.map(tool => ({ category: name, ...tool })), + components.map((tool) => ({ category: name, ...tool })), ); From 663fb318ad1b3c0bdc46ac18db370ab265fb6bc7 Mon Sep 17 00:00:00 2001 From: Jaydeep Solanki Date: Mon, 25 Aug 2025 13:33:04 +0530 Subject: [PATCH 2/2] refactor(command-palette): adjust class order for improved styling consistency refactor(image-color-inverter): update import statements for Node.js compatibility test(image-color-inverter): enhance tests with consistent globalThis usage fix(image-color-inverter.service): streamline image processing logic and error handling refactor(image-color-inverter.vue): simplify file upload handling and improve readability refactor(index): optimize tools mapping for better performance fix(c-select): correct input class order for consistent styling fix(c-table): adjust header class order for improved styling consistency --- .../command-palette/command-palette.vue | 2 +- .../image-color-inverter.e2e.spec.ts | 36 ++++++---- .../image-color-inverter.service.test.ts | 72 ++++++++++++------- .../image-color-inverter.service.ts | 27 +++---- .../image-color-inverter.vue | 61 ++++++++-------- src/tools/index.ts | 2 +- src/ui/c-select/c-select.vue | 2 +- src/ui/c-table/c-table.vue | 2 +- 8 files changed, 118 insertions(+), 86 deletions(-) diff --git a/src/modules/command-palette/command-palette.vue b/src/modules/command-palette/command-palette.vue index bceef5cd..5d953fee 100644 --- a/src/modules/command-palette/command-palette.vue +++ b/src/modules/command-palette/command-palette.vue @@ -128,7 +128,7 @@ function activateOption(option: PaletteOption) {
-
+
{{ category }}
diff --git a/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts b/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts index a73f9219..a27375e2 100644 --- a/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts +++ b/src/tools/image-color-inverter/image-color-inverter.e2e.spec.ts @@ -1,6 +1,7 @@ -import { test, expect } from '@playwright/test'; -import { promises as fs } from 'fs'; -import path from 'path'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { expect, test } from '@playwright/test'; test.describe('Tool - Image color inverter', () => { test.beforeEach(async ({ page }) => { @@ -31,7 +32,8 @@ test.describe('Tool - Image color inverter', () => { const errorAlert = page.locator('.n-alert--error'); await expect(errorAlert).toBeVisible(); await expect(errorAlert).toContainText('File must be an image'); - } finally { + } + finally { // Clean up await fs.unlink(testFilePath).catch(() => {}); } @@ -39,12 +41,13 @@ test.describe('Tool - Image color inverter', () => { test('Processes image upload successfully', async ({ page }) => { // Create a simple test image (1x1 pixel PNG in base64) - const testImageDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - + const testImageDataUrl + = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + // Create a blob from the data URL const response = await fetch(testImageDataUrl); const blob = await response.blob(); - + // Create a temporary file const buffer = await blob.arrayBuffer(); const testFilePath = path.join(process.cwd(), 'test-image.png'); @@ -62,7 +65,7 @@ test.describe('Tool - Image color inverter', () => { // Check that both images are displayed const originalImage = page.locator('text=Original Image'); const invertedImage = page.locator('text=Inverted Image'); - + await expect(originalImage).toBeVisible(); await expect(invertedImage).toBeVisible(); @@ -73,7 +76,8 @@ test.describe('Tool - Image color inverter', () => { // Check that copy button is available const copyButton = page.locator('text=Copy Base64'); await expect(copyButton).toBeVisible(); - } finally { + } + finally { // Clean up await fs.unlink(testFilePath).catch(() => {}); } @@ -81,8 +85,9 @@ test.describe('Tool - Image color inverter', () => { test('Clear button resets the tool', async ({ page }) => { // Create a simple test image - const testImageDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - + const testImageDataUrl + = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + const response = await fetch(testImageDataUrl); const blob = await response.blob(); const buffer = await blob.arrayBuffer(); @@ -93,7 +98,7 @@ test.describe('Tool - Image color inverter', () => { // Upload and process image const fileInput = page.locator('input[type="file"]'); await fileInput.setInputFiles(testFilePath); - + await page.waitForSelector('text=Inverted Image', { timeout: 5000 }); // Click clear button @@ -103,15 +108,16 @@ test.describe('Tool - Image color inverter', () => { // Check that images are cleared const originalImage = page.locator('text=Original Image'); const invertedImage = page.locator('text=Inverted Image'); - + await expect(originalImage).not.toBeVisible(); await expect(invertedImage).not.toBeVisible(); // Check that upload area is visible again const uploadArea = page.locator('text=Drag and drop an image here, or click to select'); await expect(uploadArea).toBeVisible(); - } finally { + } + finally { await fs.unlink(testFilePath).catch(() => {}); } }); -}); \ No newline at end of file +}); diff --git a/src/tools/image-color-inverter/image-color-inverter.service.test.ts b/src/tools/image-color-inverter/image-color-inverter.service.test.ts index 96869978..ac8c9269 100644 --- a/src/tools/image-color-inverter/image-color-inverter.service.test.ts +++ b/src/tools/image-color-inverter/image-color-inverter.service.test.ts @@ -1,8 +1,8 @@ -import { expect, describe, it, beforeEach } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { invertImageColors } from './image-color-inverter.service'; // Mock ImageData for Node.js environment -Object.defineProperty(global, 'ImageData', { +Object.defineProperty(globalThis, 'ImageData', { value: class ImageData { data: Uint8ClampedArray; width: number; @@ -14,7 +14,8 @@ Object.defineProperty(global, 'ImageData', { this.width = data; this.height = width; this.data = new Uint8ClampedArray(data * width * 4); - } else { + } + else { // ImageData(data, width, height) this.data = data; this.width = width; @@ -25,30 +26,39 @@ Object.defineProperty(global, 'ImageData', { }); // Mock other browser APIs for completeness -Object.defineProperty(global, 'HTMLCanvasElement', { +Object.defineProperty(globalThis, 'HTMLCanvasElement', { value: class { width = 0; height = 0; + getContext() { return { drawImage: () => {}, - getImageData: () => new (global as any).ImageData(new Uint8ClampedArray([255, 0, 128, 255]), 1, 1), + getImageData: () => new (globalThis as any).ImageData(new Uint8ClampedArray([255, 0, 128, 255]), 1, 1), putImageData: () => {}, }; } - toDataURL() { return 'data:image/png;base64,mock'; } + + toDataURL() { + return 'data:image/png;base64,mock'; + } }, }); -Object.defineProperty(global, 'Image', { +Object.defineProperty(globalThis, 'Image', { value: class { onload: () => void = () => {}; onerror: () => void = () => {}; + width = 100; + height = 100; + set src(value: string) { setTimeout(() => this.onload(), 0); } - width = 100; - height = 100; + + get src(): string { + return ''; + } }, }); @@ -57,28 +67,34 @@ describe('image-color-inverter service', () => { it('should invert RGB colors while preserving alpha', () => { // Create test image data with known values const originalData = new Uint8ClampedArray([ - 255, 0, 128, 255, // Red=255, Green=0, Blue=128, Alpha=255 - 0, 255, 64, 128, // Red=0, Green=255, Blue=64, Alpha=128 + 255, + 0, + 128, + 255, // Red=255, Green=0, Blue=128, Alpha=255 + 0, + 255, + 64, + 128, // Red=0, Green=255, Blue=64, Alpha=128 ]); - const imageData = new (global as any).ImageData(originalData, 2, 1); + const imageData = new (globalThis as any).ImageData(originalData, 2, 1); const inverted = invertImageColors(imageData); // Check that colors are inverted correctly - expect(inverted.data[0]).toBe(0); // 255 - 255 = 0 - expect(inverted.data[1]).toBe(255); // 255 - 0 = 255 - expect(inverted.data[2]).toBe(127); // 255 - 128 = 127 - expect(inverted.data[3]).toBe(255); // Alpha unchanged + expect(inverted.data[0]).toBe(0); // 255 - 255 = 0 + expect(inverted.data[1]).toBe(255); // 255 - 0 = 255 + expect(inverted.data[2]).toBe(127); // 255 - 128 = 127 + expect(inverted.data[3]).toBe(255); // Alpha unchanged - expect(inverted.data[4]).toBe(255); // 255 - 0 = 255 - expect(inverted.data[5]).toBe(0); // 255 - 255 = 0 - expect(inverted.data[6]).toBe(191); // 255 - 64 = 191 - expect(inverted.data[7]).toBe(128); // Alpha unchanged + expect(inverted.data[4]).toBe(255); // 255 - 0 = 255 + expect(inverted.data[5]).toBe(0); // 255 - 255 = 0 + expect(inverted.data[6]).toBe(191); // 255 - 64 = 191 + expect(inverted.data[7]).toBe(128); // Alpha unchanged }); it('should preserve image dimensions', () => { const originalData = new Uint8ClampedArray([255, 0, 128, 255]); - const imageData = new (global as any).ImageData(originalData, 1, 1); + const imageData = new (globalThis as any).ImageData(originalData, 1, 1); const inverted = invertImageColors(imageData); @@ -88,10 +104,16 @@ describe('image-color-inverter service', () => { it('should handle pure black and white pixels', () => { const originalData = new Uint8ClampedArray([ - 0, 0, 0, 255, // Pure black - 255, 255, 255, 255, // Pure white + 0, + 0, + 0, + 255, // Pure black + 255, + 255, + 255, + 255, // Pure white ]); - const imageData = new (global as any).ImageData(originalData, 2, 1); + const imageData = new (globalThis as any).ImageData(originalData, 2, 1); const inverted = invertImageColors(imageData); @@ -108,4 +130,4 @@ describe('image-color-inverter service', () => { expect(inverted.data[7]).toBe(255); }); }); -}); \ No newline at end of file +}); diff --git a/src/tools/image-color-inverter/image-color-inverter.service.ts b/src/tools/image-color-inverter/image-color-inverter.service.ts index eac3e918..04fb71dc 100644 --- a/src/tools/image-color-inverter/image-color-inverter.service.ts +++ b/src/tools/image-color-inverter/image-color-inverter.service.ts @@ -14,16 +14,16 @@ export function invertImageFile(file: File): Promise { } const reader = new FileReader(); - + reader.onload = () => { const img = new Image(); img.src = reader.result as string; - + img.onload = () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); - + if (!ctx) { reject(new Error('Failed to get canvas context')); return; @@ -31,7 +31,7 @@ export function invertImageFile(file: File): Promise { canvas.width = img.width; canvas.height = img.height; - + // Draw the original image ctx.drawImage(img, 0, 0); @@ -41,7 +41,7 @@ export function invertImageFile(file: File): Promise { // Invert colors (RGB channels only, preserve alpha) for (let i = 0; i < data.length; i += 4) { - data[i] = 255 - data[i]; // Red + data[i] = 255 - data[i]; // Red data[i + 1] = 255 - data[i + 1]; // Green data[i + 2] = 255 - data[i + 2]; // Blue // data[i + 3] is alpha - keep unchanged @@ -49,38 +49,39 @@ export function invertImageFile(file: File): Promise { // Put the modified image data back ctx.putImageData(imageData, 0, 0); - + // Convert to base64 PNG const invertedDataUrl = canvas.toDataURL('image/png'); resolve(invertedDataUrl); - } catch (err) { + } + catch (err) { reject(new Error(`Failed to process image: ${err}`)); } }; - + img.onerror = () => { reject(new Error('Failed to load image')); }; }; - + reader.onerror = () => { reject(new Error('Failed to read file')); }; - + reader.readAsDataURL(file); }); } export function invertImageColors(imageData: ImageData): ImageData { const data = new Uint8ClampedArray(imageData.data); - + // Invert RGB values while preserving alpha for (let i = 0; i < data.length; i += 4) { - data[i] = 255 - data[i]; // Red + data[i] = 255 - data[i]; // Red data[i + 1] = 255 - data[i + 1]; // Green data[i + 2] = 255 - data[i + 2]; // Blue // data[i + 3] is alpha - keep unchanged } - + return new ImageData(data, imageData.width, imageData.height); } diff --git a/src/tools/image-color-inverter/image-color-inverter.vue b/src/tools/image-color-inverter/image-color-inverter.vue index d0999e28..d6a783cc 100644 --- a/src/tools/image-color-inverter/image-color-inverter.vue +++ b/src/tools/image-color-inverter/image-color-inverter.vue @@ -1,17 +1,17 @@