From c903f88447b4cc839f7e763c990f9fe449cce76b Mon Sep 17 00:00:00 2001 From: Myx Date: Fri, 11 Oct 2024 21:13:29 +0200 Subject: [PATCH] Add dds to png converter --- package-lock.json | 9 + src/app/app.config.ts | 8 +- src/app/app.routes.ts | 7 +- src/app/header/header.component.ts | 89 ++++--- .../dual-textarea.component.scss | 8 +- .../upload/file-converter.component.html | 29 +++ .../upload/file-converter.component.scss | 20 ++ .../shared/upload/file-converter.component.ts | 53 +++++ .../dds-to-png/dds-to-png.component.html | 7 + .../dds-to-png/dds-to-png.component.scss | 0 .../dds-to-png/dds-to-png.component.spec.ts | 28 +++ src/tools/dds-to-png/dds-to-png.component.ts | 52 ++++ src/tools/dds-to-png/dds-to-png.service.ts | 222 ++++++++++++++++++ src/tools/guid/guid.component.scss | 4 +- 14 files changed, 495 insertions(+), 41 deletions(-) create mode 100644 src/app/shared/upload/file-converter.component.html create mode 100644 src/app/shared/upload/file-converter.component.scss create mode 100644 src/app/shared/upload/file-converter.component.ts create mode 100644 src/tools/dds-to-png/dds-to-png.component.html create mode 100644 src/tools/dds-to-png/dds-to-png.component.scss create mode 100644 src/tools/dds-to-png/dds-to-png.component.spec.ts create mode 100644 src/tools/dds-to-png/dds-to-png.component.ts create mode 100644 src/tools/dds-to-png/dds-to-png.service.ts diff --git a/package-lock.json b/package-lock.json index 146660b..8e02dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@ng-icons/core": "^29.5.1", "@ng-icons/css.gg": "^29.5.1", "@ng-icons/heroicons": "^29.5.1", + "dds-parser": "^1.0.1", "primeicons": "^7.0.0", "primeng": "^18.0.0-beta.2", "rxjs": "~7.8.0", @@ -5477,6 +5478,14 @@ "node": ">=4.0" } }, + "node_modules/dds-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dds-parser/-/dds-parser-1.0.1.tgz", + "integrity": "sha512-T7/hvdtB96hZkm6vBkzXtyIR06kU4bHtpkn8S5v1d6cHkc4oJIgOLzWF/8XydXb27423H39q7CDbzLmOqJYmEQ==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/src/app/app.config.ts b/src/app/app.config.ts index c7c6d0d..e19affb 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,11 +4,15 @@ import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { provideNgIconsConfig } from '@ng-icons/core'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes), + providers: [ + provideRouter(routes), provideAnimationsAsync("animations"), provideNgIconsConfig({ size: '1.5em', - }),] + }), + provideHttpClient() + ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6b236ed..5c497a1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,8 +4,8 @@ import { GuidComponent } from '../tools/guid/guid.component'; import { Base64ConverterComponent } from '../tools/base64-converter/base64-converter.component'; import { JwtToJsonComponent } from '../tools/jwt-to-json/jwt-to-json.component'; import { TextToCronComponent } from '../tools/text-to-cron/text-to-cron.component'; +import { DdsToPngComponent } from '../tools/dds-to-png/dds-to-png.component'; -// create route to the ascii-to-text component export const routes: Routes = [ { path: 'ascii-to-text', @@ -31,6 +31,11 @@ export const routes: Routes = [ path: 'text-to-cron', pathMatch: 'full', component: TextToCronComponent + }, + { + path: 'dds-to-png', + pathMatch: 'full', + component: DdsToPngComponent } ]; diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 9ce2320..6795bf6 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -17,43 +17,60 @@ export class HeaderComponent implements OnInit { ngOnInit() { this.items = [ { - label: 'Text Tools', - icon: 'pi pi-box', - items: [ - [ - { - items: [ - { - label: 'Ascii to text', - routerLink: 'ascii-to-text', - routerLinkActiveOptions: { exact: true } - }, - { - label: 'Guid Generator', - routerLink: 'guid', - routerLinkActiveOptions: { exact: true } - }, - { - label: 'Base64 Converter', - routerLink: 'base64-converter', - routerLinkActiveOptions: { exact: true } - }, - { - label: 'Jwt decoder', - routerLink: 'jwt-decoder', - routerLinkActiveOptions: { exact: true } - }, - { - label: 'Text to Cron Expression', - routerLink: 'text-to-cron', - routerLinkActiveOptions: { exact: true } - }, - ], - - } - ] + label: 'Text Tools', + icon: 'pi pi-box', + items: [ + [ + { + items: [ + { + label: 'Ascii to text', + routerLink: 'ascii-to-text', + routerLinkActiveOptions: { exact: true } + }, + { + label: 'Guid Generator', + routerLink: 'guid', + routerLinkActiveOptions: { exact: true } + }, + { + label: 'Base64 Converter', + routerLink: 'base64-converter', + routerLinkActiveOptions: { exact: true } + }, + { + label: 'Jwt decoder', + routerLink: 'jwt-decoder', + routerLinkActiveOptions: { exact: true } + }, + { + label: 'Text to Cron Expression', + routerLink: 'text-to-cron', + routerLinkActiveOptions: { exact: true } + } + ], + + } + ] ] - } + }, + { + label: 'Conversion', + icon: 'pi pi-box', + items: [ + [ + { + items: [ + { + label: 'DDS to PNG', + routerLink: 'dds-to-png', + routerLinkActiveOptions: { exact: true } + } + ] + } + ] + ] + } ] } } \ No newline at end of file diff --git a/src/app/shared/dual-textarea/dual-textarea.component.scss b/src/app/shared/dual-textarea/dual-textarea.component.scss index 8e2fa27..923cd92 100644 --- a/src/app/shared/dual-textarea/dual-textarea.component.scss +++ b/src/app/shared/dual-textarea/dual-textarea.component.scss @@ -5,6 +5,12 @@ height: 100vh; width: 98vw; + .card { + display: flex; + flex-direction: column; + width: 1140px; + } + .wrapper { display: flex; flex-direction: column; @@ -23,7 +29,7 @@ } textarea { - width: 540px; + width: 100%; height: 175px; padding: 12px 20px; box-sizing: border-box; diff --git a/src/app/shared/upload/file-converter.component.html b/src/app/shared/upload/file-converter.component.html new file mode 100644 index 0000000..f8a4731 --- /dev/null +++ b/src/app/shared/upload/file-converter.component.html @@ -0,0 +1,29 @@ +
+ + + + + + + Name + Format + Download + + + + + {{file.name}} + {{file.format}} + {{file.name}} + + + + +
\ No newline at end of file diff --git a/src/app/shared/upload/file-converter.component.scss b/src/app/shared/upload/file-converter.component.scss new file mode 100644 index 0000000..f2ebb9a --- /dev/null +++ b/src/app/shared/upload/file-converter.component.scss @@ -0,0 +1,20 @@ +:host { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + width: 98vw; + + + .card { + display: flex; + flex-direction: column; + width: 1140px; + } + + .conversion { + display: flex; + justify-content: center; + margin-top: 1rem; + } + } \ No newline at end of file diff --git a/src/app/shared/upload/file-converter.component.ts b/src/app/shared/upload/file-converter.component.ts new file mode 100644 index 0000000..bafeef7 --- /dev/null +++ b/src/app/shared/upload/file-converter.component.ts @@ -0,0 +1,53 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { FileSelectEvent, FileUploadModule } from 'primeng/fileupload'; +import { ButtonModule } from 'primeng/button'; +import { PanelModule } from 'primeng/panel'; +import { TableModule } from 'primeng/table'; + +interface ProcessedFile { + name: string; + link: string; + format: string; +} + +@Component({ + selector: 'app-file-converter', + templateUrl: 'file-converter.component.html', + styleUrls: ['file-converter.component.scss'], + standalone: true, + imports: [ + CommonModule, + FormsModule, + FileUploadModule, + ButtonModule, + PanelModule, + TableModule + ] +}) +export class FileConverterComponent { + _fileFormats: string[] = []; + accept: string = ''; + + @Output() fileSelected = new EventEmitter(); + @Input() isPreview: boolean = true; + @Input () title: string = 'File Converter'; + @Input() processedFiles: ProcessedFile[] = []; + @Input() + set fileFormats(formats: string[]) { + this._fileFormats = formats; + this.accept = formats.join(','); + } + + get fileFormats(): string[] { + return this._fileFormats; + } + + selectedFile: File[] | null = null; + + onFileSelect(event: FileSelectEvent): void { + this.selectedFile = event.files; + this.fileSelected.emit(this.selectedFile!); + } +} \ No newline at end of file diff --git a/src/tools/dds-to-png/dds-to-png.component.html b/src/tools/dds-to-png/dds-to-png.component.html new file mode 100644 index 0000000..181233a --- /dev/null +++ b/src/tools/dds-to-png/dds-to-png.component.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/src/tools/dds-to-png/dds-to-png.component.scss b/src/tools/dds-to-png/dds-to-png.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/tools/dds-to-png/dds-to-png.component.spec.ts b/src/tools/dds-to-png/dds-to-png.component.spec.ts new file mode 100644 index 0000000..37da053 --- /dev/null +++ b/src/tools/dds-to-png/dds-to-png.component.spec.ts @@ -0,0 +1,28 @@ +/* tslint:disable:no-unused-variable */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { DdsToPngComponent } from './dds-to-png.component'; + +describe('DdsToPngComponent', () => { + let component: DdsToPngComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DdsToPngComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DdsToPngComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/tools/dds-to-png/dds-to-png.component.ts b/src/tools/dds-to-png/dds-to-png.component.ts new file mode 100644 index 0000000..a7b196e --- /dev/null +++ b/src/tools/dds-to-png/dds-to-png.component.ts @@ -0,0 +1,52 @@ +import { Component, OnInit } from '@angular/core'; +import { FileConverterComponent } from '../../app/shared/upload/file-converter.component'; +import { DdsToPngService } from './dds-to-png.service'; + +interface ProcessedFile { + name: string; + link: string; + format: string; +} + +@Component({ + selector: 'app-dds-to-png', + templateUrl: './dds-to-png.component.html', + styleUrls: ['./dds-to-png.component.scss'], + standalone: true, + imports: [FileConverterComponent] +}) +export class DdsToPngComponent { + + processedFiles: ProcessedFile[] = []; + fileFormats: string[] = [".dds"]; + + constructor(private ddsToPngService: DdsToPngService) { } + + onFileSelected(input: File[]): void { + if (input.length > 0) { + const file = input[0]; + const reader = new FileReader(); + reader.onload = async () => { + try { + const ddsArrayBuffer = reader.result as ArrayBuffer; + const pngDataUrl = await this.ddsToPngService.ddsToPng(ddsArrayBuffer); + + const blob = await (await fetch(pngDataUrl)).blob(); + const blobUrl = URL.createObjectURL(blob); + + const processedFile: ProcessedFile = { + name: file.name.replace('.dds', '.png'), + link: blobUrl, + format: 'png' + }; + this.processedFiles.push(processedFile); + + console.log('Processed Files:', this.processedFiles); + } catch (error) { + console.error('Error:', error); + } + }; + reader.readAsArrayBuffer(file); + } + } +} diff --git a/src/tools/dds-to-png/dds-to-png.service.ts b/src/tools/dds-to-png/dds-to-png.service.ts new file mode 100644 index 0000000..19caa1b --- /dev/null +++ b/src/tools/dds-to-png/dds-to-png.service.ts @@ -0,0 +1,222 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DdsToPngService { + constructor() {} + + parseHeaders(arrayBuffer: ArrayBuffer) { + const header = new DataView(arrayBuffer, 0, 128); + const height = header.getUint32(12, true); + const width = header.getUint32(16, true); + const fourCC = header.getUint32(84, true); + return { width, height, fourCC }; + } + + decodeDXT1(src: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(width * height * 4); + let srcIndex = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const c0 = src[srcIndex] | (src[srcIndex + 1] << 8); + const c1 = src[srcIndex + 2] | (src[srcIndex + 3] << 8); + const code = src[srcIndex + 4] | (src[srcIndex + 5] << 8) | (src[srcIndex + 6] << 16) | (src[srcIndex + 7] << 24); + srcIndex += 8; + + const colors = new Uint8Array(16); + this.decodeColors(c0, c1, colors); + + for (let blockY = 0; blockY < 4; blockY++) { + for (let blockX = 0; blockX < 4; blockX++) { + const pixelIndex = ((code >> (2 * (blockY * 4 + blockX))) & 0x03) * 4; + const dstPixelIndex = ((y + blockY) * width + (x + blockX)) * 4; + rgba[dstPixelIndex] = colors[pixelIndex]; + rgba[dstPixelIndex + 1] = colors[pixelIndex + 1]; + rgba[dstPixelIndex + 2] = colors[pixelIndex + 2]; + rgba[dstPixelIndex + 3] = colors[pixelIndex + 3]; + } + } + } + } + + return rgba; + } + + decodeDXT3(src: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(width * height * 4); + let srcIndex = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha = new Uint8Array(16); + for (let i = 0; i < 8; i++) { + const byte = src[srcIndex++]; + alpha[i * 2] = (byte & 0x0F) * 17; + alpha[i * 2 + 1] = (byte >> 4) * 17; + } + + const c0 = src[srcIndex] | (src[srcIndex + 1] << 8); + const c1 = src[srcIndex + 2] | (src[srcIndex + 3] << 8); + const code = src[srcIndex + 4] | (src[srcIndex + 5] << 8) | (src[srcIndex + 6] << 16) | (src[srcIndex + 7] << 24); + srcIndex += 8; + + const colors = new Uint8Array(16); + this.decodeColors(c0, c1, colors); + + for (let blockY = 0; blockY < 4; blockY++) { + for (let blockX = 0; blockX < 4; blockX++) { + const pixelIndex = ((code >> (2 * (blockY * 4 + blockX))) & 0x03) * 4; + const dstPixelIndex = ((y + blockY) * width + (x + blockX)) * 4; + rgba[dstPixelIndex] = colors[pixelIndex]; + rgba[dstPixelIndex + 1] = colors[pixelIndex + 1]; + rgba[dstPixelIndex + 2] = colors[pixelIndex + 2]; + rgba[dstPixelIndex + 3] = alpha[blockY * 4 + blockX]; + } + } + } + } + + return rgba; + } + + decodeDXT5(src: Uint8Array, width: number, height: number): Uint8Array { + const rgba = new Uint8Array(width * height * 4); + let srcIndex = 0; + + for (let y = 0; y < height; y += 4) { + for (let x = 0; x < width; x += 4) { + const alpha0 = src[srcIndex++]; + const alpha1 = src[srcIndex++]; + const alphaCode = src[srcIndex] | (src[srcIndex + 1] << 8) | (src[srcIndex + 2] << 16) | (src[srcIndex + 3] << 24) | (src[srcIndex + 4] << 32) | (src[srcIndex + 5] << 40); + srcIndex += 6; + + const alphas = new Uint8Array(8); + alphas[0] = alpha0; + alphas[1] = alpha1; + if (alpha0 > alpha1) { + for (let i = 1; i < 7; i++) { + alphas[i + 1] = ((7 - i) * alpha0 + i * alpha1) / 7; + } + } else { + for (let i = 1; i < 5; i++) { + alphas[i + 1] = ((5 - i) * alpha0 + i * alpha1) / 5; + } + alphas[6] = 0; + alphas[7] = 255; + } + + const c0 = src[srcIndex] | (src[srcIndex + 1] << 8); + const c1 = src[srcIndex + 2] | (src[srcIndex + 3] << 8); + const code = src[srcIndex + 4] | (src[srcIndex + 5] << 8) | (src[srcIndex + 6] << 16) | (src[srcIndex + 7] << 24); + srcIndex += 8; + + const colors = new Uint8Array(16); + this.decodeColors(c0, c1, colors); + + for (let blockY = 0; blockY < 4; blockY++) { + for (let blockX = 0; blockX < 4; blockX++) { + const pixelIndex = ((code >> (2 * (blockY * 4 + blockX))) & 0x03) * 4; + const alphaIndex = (alphaCode >> (3 * (blockY * 4 + blockX))) & 0x07; + const dstPixelIndex = ((y + blockY) * width + (x + blockX)) * 4; + rgba[dstPixelIndex] = colors[pixelIndex]; + rgba[dstPixelIndex + 1] = colors[pixelIndex + 1]; + rgba[dstPixelIndex + 2] = colors[pixelIndex + 2]; + rgba[dstPixelIndex + 3] = alphas[alphaIndex]; + } + } + } + } + + return rgba; + } + + decodeColors(c0: number, c1: number, colors: Uint8Array) { + const r0 = (c0 >> 11) & 0x1F; + const g0 = (c0 >> 5) & 0x3F; + const b0 = c0 & 0x1F; + const r1 = (c1 >> 11) & 0x1F; + const g1 = (c1 >> 5) & 0x3F; + const b1 = c1 & 0x1F; + + colors[0] = (r0 << 3) | (r0 >> 2); + colors[1] = (g0 << 2) | (g0 >> 4); + colors[2] = (b0 << 3) | (b0 >> 2); + colors[3] = 255; + + colors[4] = (r1 << 3) | (r1 >> 2); + colors[5] = (g1 << 2) | (g1 >> 4); + colors[6] = (b1 << 3) | (b1 >> 2); + colors[7] = 255; + + if (c0 > c1) { + for (let i = 0; i < 3; i++) { + colors[8 + i] = (2 * colors[i] + colors[4 + i]) / 3; + colors[12 + i] = (colors[i] + 2 * colors[4 + i]) / 3; + } + colors[11] = colors[15] = 255; + } else { + for (let i = 0; i < 3; i++) { + colors[8 + i] = (colors[i] + colors[4 + i]) / 2; + colors[12 + i] = 0; + } + colors[11] = 255; + colors[15] = 0; + } + } + + ddsToPng(arrayBuffer: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + try { + const { width, height, fourCC } = this.parseHeaders(arrayBuffer); + + let rgbaData: Uint8Array; + const src = new Uint8Array(arrayBuffer, 128); + + switch (fourCC) { + case 0x31545844: // 'DXT1' in ASCII + rgbaData = this.decodeDXT1(src, width, height); + break; + case 0x33545844: // 'DXT3' in ASCII + rgbaData = this.decodeDXT3(src, width, height); + break; + case 0x35545844: // 'DXT5' in ASCII + rgbaData = this.decodeDXT5(src, width, height); + break; + default: + throw new Error('Unsupported DDS format'); + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + + if (!context) { + reject('Failed to get canvas context'); + return; + } + + const imageData = context.createImageData(width, height); + imageData.data.set(rgbaData); + context.putImageData(imageData, 0, 0); + + canvas.toBlob((blob) => { + if (blob) { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + } else { + reject('Failed to create blob'); + } + }, 'image/png'); + } catch (error) { + reject(`Error converting DDS to PNG: ${error}`); + } + }); + } +} \ No newline at end of file diff --git a/src/tools/guid/guid.component.scss b/src/tools/guid/guid.component.scss index 4c761e2..d3f4e66 100644 --- a/src/tools/guid/guid.component.scss +++ b/src/tools/guid/guid.component.scss @@ -6,7 +6,9 @@ width: 98vw; p-panel { - width: 576px; + display: flex; + flex-direction: column; + width: 1140px; } .guid-row {