mirror of
https://github.com/Polaris-Entertainment/bytefy.git
synced 2026-04-09 09:29:39 +00:00
Finish image converter
This commit is contained in:
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md.
|
||||||
|
"name": ".NET Core Launch (web)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/services/bytefy.image/bytefy.image/bin/Debug/net8.0/bytefy.image.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/services/bytefy.image/bytefy.image",
|
||||||
|
"stopAtEntry": false,
|
||||||
|
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
|
||||||
|
"serverReadyAction": {
|
||||||
|
"action": "openExternally",
|
||||||
|
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"sourceFileMap": {
|
||||||
|
"/Views": "${workspaceFolder}/Views"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
41
.vscode/tasks.json
vendored
Normal file
41
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/services/bytefy.image/bytefy.image.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/services/bytefy.image/bytefy.image.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/services/bytefy.image/bytefy.image.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -36,13 +36,24 @@ public class ConversionQueueService : BackgroundService
|
|||||||
|
|
||||||
private Task<(byte[], string)> ProcessConversionAsync(ConversionTask task)
|
private Task<(byte[], string)> ProcessConversionAsync(ConversionTask task)
|
||||||
{
|
{
|
||||||
using var magickImage = new MagickImage(task.ImageData);
|
try
|
||||||
magickImage.Format = task.Format;
|
{
|
||||||
var resultStream = new MemoryStream();
|
using var magickImage = new MagickImage(task.ImageData);
|
||||||
magickImage.Write(resultStream);
|
magickImage.Format = task.Format;
|
||||||
resultStream.Position = 0;
|
var resultStream = new MemoryStream();
|
||||||
|
magickImage.Write(resultStream);
|
||||||
|
resultStream.Position = 0;
|
||||||
|
|
||||||
var mimeType = MimeTypes.MimeTypeMap.GetMimeType($"image/{task.Format.ToString().ToLower()}");
|
var mimeType = MimeTypes.MimeTypeMap.GetMimeType($"image/{task.Format.ToString().ToLower()}");
|
||||||
return Task.FromResult((resultStream.ToArray(), mimeType));
|
return Task.FromResult((resultStream.ToArray(), mimeType));
|
||||||
|
}
|
||||||
|
catch (MagickImageErrorException ex)
|
||||||
|
{
|
||||||
|
// Log the error message
|
||||||
|
Console.WriteLine($"Image conversion failed: {ex.Message}");
|
||||||
|
|
||||||
|
// Return a default value or handle the error as appropriate for your application
|
||||||
|
return Task.FromResult<(byte[], string)>((null, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,54 +6,81 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddAntiforgery(options => options.HeaderName = "2311d8d8-607d-4747-8939-1bde65643254");
|
builder.Services.AddAntiforgery(options => options.HeaderName = "2311d8d8-607d-4747-8939-1bde65643254");
|
||||||
builder.Services.AddSingleton<ConversionQueueService>();
|
builder.Services.AddSingleton<ConversionQueueService>();
|
||||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<ConversionQueueService>());
|
builder.Services.AddHostedService(provider => provider.GetRequiredService<ConversionQueueService>());
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowSpecificOrigin",
|
||||||
|
builder => builder.WithOrigins("http://localhost:4200")
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowCredentials());
|
||||||
|
});
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
app.UseCors("AllowSpecificOrigin"); // Use the CORS policy
|
||||||
var conversionQueue = app.Services.GetRequiredService<ConversionQueueService>();
|
var conversionQueue = app.Services.GetRequiredService<ConversionQueueService>();
|
||||||
|
|
||||||
app.MapPost("/convert/{format}", async (IFormFile image, string format) =>
|
app.MapPost("/convert/{format}", async (IFormFile file, string format) =>
|
||||||
{
|
{
|
||||||
if (!Enum.TryParse(format, true, out MagickFormat magickFormat) || magickFormat == MagickFormat.Unknown)
|
try
|
||||||
return Results.BadRequest("Invalid format");
|
|
||||||
|
|
||||||
if (image == null || image.Length == 0)
|
|
||||||
return Results.BadRequest("No image provided");
|
|
||||||
|
|
||||||
if (image.Length > 20 * 1024 * 1024)
|
|
||||||
throw new Exception("Image size too large");
|
|
||||||
|
|
||||||
using var memoryStream = new MemoryStream();
|
|
||||||
await image.CopyToAsync(memoryStream);
|
|
||||||
|
|
||||||
var conversionTask = new ConversionTask
|
|
||||||
{
|
{
|
||||||
ImageData = memoryStream.ToArray(),
|
if (!Enum.TryParse(format, true, out MagickFormat magickFormat) || magickFormat == MagickFormat.Unknown)
|
||||||
Format = magickFormat
|
return Results.BadRequest("Invalid format");
|
||||||
};
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<(byte[], string)>();
|
var formatInfo = MagickNET.SupportedFormats.FirstOrDefault(f => f.Format == magickFormat);
|
||||||
conversionQueue.QueueConversion(conversionTask, tcs);
|
if (formatInfo == null || !formatInfo.SupportsReading || !formatInfo.SupportsWriting)
|
||||||
|
return Results.BadRequest("Unsupported format");
|
||||||
|
|
||||||
var (imageData, mimeType) = await tcs.Task;
|
if (file == null || file.Length == 0)
|
||||||
|
return Results.BadRequest("No image provided");
|
||||||
|
|
||||||
return Results.File(new MemoryStream(imageData), mimeType, $"{Path.GetFileNameWithoutExtension(image.FileName)}.{magickFormat.ToString().ToLower()}");
|
if (file.Length > 20 * 1024 * 1024)
|
||||||
});
|
throw new Exception("Image size too large");
|
||||||
|
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await file.CopyToAsync(memoryStream);
|
||||||
|
|
||||||
|
var conversionTask = new ConversionTask
|
||||||
|
{
|
||||||
|
ImageData = memoryStream.ToArray(),
|
||||||
|
Format = magickFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<(byte[], string)>();
|
||||||
|
conversionQueue.QueueConversion(conversionTask, tcs);
|
||||||
|
|
||||||
|
var (imageData, mimeType) = await tcs.Task;
|
||||||
|
|
||||||
|
return Results.File(new MemoryStream(imageData), mimeType, $"{Path.GetFileNameWithoutExtension(file.FileName)}.{magickFormat.ToString().ToLower()}");
|
||||||
|
}
|
||||||
|
catch (ImageMagick.MagickImageErrorException e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
return Results.BadRequest("Invalid image");
|
||||||
|
}
|
||||||
|
}).DisableAntiforgery(); // should get this removed by getting antiforgery working with angular. Doesn't find Cookie.
|
||||||
|
|
||||||
app.MapGet("/antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
|
app.MapGet("/antiforgery/token", (IAntiforgery forgeryService, HttpContext context) =>
|
||||||
{
|
{
|
||||||
var tokens = forgeryService.GetAndStoreTokens(context);
|
var tokens = forgeryService.GetAndStoreTokens(context);
|
||||||
var xsrfToken = tokens.RequestToken!;
|
var xsrfToken = tokens.RequestToken!;
|
||||||
return TypedResults.Content(xsrfToken, "text/plain");
|
return TypedResults.Content(xsrfToken, "text/plain");
|
||||||
});
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
app.MapGet("/formats", () =>
|
app.MapGet("/formats", () =>
|
||||||
{
|
{
|
||||||
var formats = Enum.GetNames<MagickFormat>().ToList();
|
var formats = MagickNET.SupportedFormats
|
||||||
|
.Where(f => f.SupportsReading && f.SupportsWriting)
|
||||||
formats.Remove("Unknown");
|
.Select(f => f.Format.ToString())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return Results.Ok(formats);
|
return Results.Ok(formats);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
app.MapGet("/mimetype/{format}", (string format) =>
|
||||||
|
{
|
||||||
|
var mimeType = MimeTypes.MimeTypeMap.GetMimeType($".{format.ToLower()}");
|
||||||
|
return Results.Ok(mimeType);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
// .main-content {
|
|
||||||
// display: flex;
|
|
||||||
// flex-direction: row;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// router-outlet {
|
|
||||||
// flex: 1;
|
|
||||||
// padding: 20px;
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ApplicationConfig } from '@angular/core';
|
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import { provideNgIconsConfig } from '@ng-icons/core';
|
import { provideNgIconsConfig } from '@ng-icons/core';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { HttpClientXsrfModule, provideHttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -13,6 +13,10 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideNgIconsConfig({
|
provideNgIconsConfig({
|
||||||
size: '1.5em',
|
size: '1.5em',
|
||||||
}),
|
}),
|
||||||
provideHttpClient()
|
provideHttpClient(),
|
||||||
|
importProvidersFrom(HttpClientXsrfModule.withOptions({
|
||||||
|
cookieName: 'X-XSRF-TOKEN',
|
||||||
|
headerName: '2311d8d8-607d-4747-8939-1bde65643254',
|
||||||
|
}))
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Base64ConverterComponent } from '../tools/client-side/base64-converter/
|
|||||||
import { JwtToJsonComponent } from '../tools/client-side/jwt-to-json/jwt-to-json.component';
|
import { JwtToJsonComponent } from '../tools/client-side/jwt-to-json/jwt-to-json.component';
|
||||||
import { TextToCronComponent } from '../tools/client-side/text-to-cron/text-to-cron.component';
|
import { TextToCronComponent } from '../tools/client-side/text-to-cron/text-to-cron.component';
|
||||||
import { DdsToPngComponent } from '../tools/client-side/dds-to-png/dds-to-png.component';
|
import { DdsToPngComponent } from '../tools/client-side/dds-to-png/dds-to-png.component';
|
||||||
|
import { ImageConverterComponent } from '../tools/server-side/image-converter/image-converter.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -36,6 +37,11 @@ export const routes: Routes = [
|
|||||||
path: 'dds-to-png',
|
path: 'dds-to-png',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
component: DdsToPngComponent
|
component: DdsToPngComponent
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'image-converter',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: ImageConverterComponent
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ export class HeaderComponent implements OnInit {
|
|||||||
label: 'DDS to PNG',
|
label: 'DDS to PNG',
|
||||||
routerLink: 'dds-to-png',
|
routerLink: 'dds-to-png',
|
||||||
routerLinkActiveOptions: { exact: true }
|
routerLinkActiveOptions: { exact: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Image Converter',
|
||||||
|
routerLink: 'image-converter',
|
||||||
|
routerLinkActiveOptions: { exact: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
12
tools/src/app/models/conversion.model.ts
Normal file
12
tools/src/app/models/conversion.model.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Contains all models used for conversion
|
||||||
|
|
||||||
|
export interface ProcessedFile {
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
format: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<div class="card flex justify-center">
|
<div class="card flex justify-center">
|
||||||
<p-panel [header]="title">
|
<p-panel [header]="title">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<p-tag *ngIf="isBeta" severity="warn" value="Beta"></p-tag>
|
||||||
|
</ng-template>
|
||||||
<textarea
|
<textarea
|
||||||
(keyup)="onTopChange($event)"
|
(keyup)="onTopChange($event)"
|
||||||
[disabled]="topDisabled"
|
[disabled]="topDisabled"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
margin-top: 20px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 98vw;
|
width: 98vw;
|
||||||
|
|
||||||
@@ -39,4 +39,13 @@
|
|||||||
background-color: var(--primary-contrast);
|
background-color: var(--primary-contrast);
|
||||||
color: var(--text-color)
|
color: var(--text-color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::ng-deep .p-panel-header {
|
||||||
|
justify-content: unset !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
||||||
import { InputTextareaModule } from 'primeng/inputtextarea';
|
import { InputTextareaModule } from 'primeng/inputtextarea';
|
||||||
import { PanelModule } from 'primeng/panel';
|
import { PanelModule } from 'primeng/panel';
|
||||||
|
import { TagModule } from 'primeng/tag';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dual-textarea',
|
selector: 'app-dual-textarea',
|
||||||
@@ -15,7 +16,8 @@ import { PanelModule } from 'primeng/panel';
|
|||||||
InputTextareaModule,
|
InputTextareaModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
PanelModule,
|
PanelModule,
|
||||||
CommonModule
|
CommonModule,
|
||||||
|
TagModule
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -27,6 +29,7 @@ export class DualTextareaComponent {
|
|||||||
@Input() bottomPlaceholder: string = 'Right Textarea';
|
@Input() bottomPlaceholder: string = 'Right Textarea';
|
||||||
@Input() topValue: string = '';
|
@Input() topValue: string = '';
|
||||||
@Input() bottomValue: string = '';
|
@Input() bottomValue: string = '';
|
||||||
|
@Input() isBeta: boolean = false;
|
||||||
@Output() topChange = new EventEmitter<string>();
|
@Output() topChange = new EventEmitter<string>();
|
||||||
@Output() bottomChange = new EventEmitter<string>();
|
@Output() bottomChange = new EventEmitter<string>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,60 @@
|
|||||||
<div class="card flex justify-center">
|
<div class="card flex justify-center">
|
||||||
<p-panel [header]="title">
|
<p-panel [header]="title">
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<p-tag *ngIf="isBeta" severity="warn" value="Beta"></p-tag>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<p-fileUpload
|
<p-fileUpload
|
||||||
name="file"
|
name="file"
|
||||||
url="./upload"
|
|
||||||
(onSelect)="onFileSelect($event)"
|
(onSelect)="onFileSelect($event)"
|
||||||
[auto]="true"
|
[auto]="true"
|
||||||
[accept]="accept"
|
[accept]="accept"
|
||||||
[previewWidth]="isPreview ? '50px' : '0px'"
|
[previewWidth]="isPreview ? '50px' : '0px'"
|
||||||
|
mode="advanced"
|
||||||
|
[url]="url"
|
||||||
|
[withCredentials]="true"
|
||||||
|
[method]="method"
|
||||||
|
[headers]="requestHeaders"
|
||||||
>
|
>
|
||||||
|
<ng-template
|
||||||
|
*ngIf="fileTypeSelector"
|
||||||
|
pTemplate="header"
|
||||||
|
let-files
|
||||||
|
let-chooseCallback="chooseCallback"
|
||||||
|
let-clearCallback="clearCallback"
|
||||||
|
let-uploadCallback="uploadCallback"
|
||||||
|
>
|
||||||
|
<p-button
|
||||||
|
(onClick)="choose($event, chooseCallback)"
|
||||||
|
icon="pi pi-images"
|
||||||
|
[rounded]="true"
|
||||||
|
[outlined]="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p-autoComplete
|
||||||
|
*ngIf="fileTypeSelector"
|
||||||
|
(onSelect)="onAutoCompleteDropdownClick($event)"
|
||||||
|
[virtualScroll]="true"
|
||||||
|
[suggestions]="filteredFiles"
|
||||||
|
[virtualScrollItemSize]="34"
|
||||||
|
(completeMethod)="onAutoComplete($event)"
|
||||||
|
optionLabel="name"
|
||||||
|
[dropdown]="true"
|
||||||
|
placeholder="Select a output format"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p-button
|
||||||
|
(onClick)="onUploadEvent()"
|
||||||
|
icon="pi pi-file-arrow-up"
|
||||||
|
[rounded]="true"
|
||||||
|
[outlined]="true"
|
||||||
|
/>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template *ngIf="fileTypeSelector" pTemplate="empty">
|
||||||
|
<div>Drag and drop files to here to upload.</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
</p-fileUpload>
|
</p-fileUpload>
|
||||||
<p-table [value]="processedFiles" *ngIf="processedFiles.length != 0">
|
<p-table [value]="processedFiles" *ngIf="processedFiles.length != 0">
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
@@ -21,7 +68,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{file.name}}</td>
|
<td>{{file.name}}</td>
|
||||||
<td>{{file.format}}</td>
|
<td>{{file.format}}</td>
|
||||||
<td><a [href]="file.link" download>{{file.name}}</a></td>
|
<td><a [href]="file.link" [download]="file.name">{{file.name}}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-table>
|
</p-table>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
margin-top: 20px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 98vw;
|
width: 98vw;
|
||||||
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -17,4 +16,12 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
::ng-deep .p-panel-header {
|
||||||
|
justify-content: unset !important;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { FileSelectEvent, FileUploadModule } from 'primeng/fileupload';
|
import { FileSelectEvent, FileUploadEvent, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
import { PanelModule } from 'primeng/panel';
|
import { PanelModule } from 'primeng/panel';
|
||||||
import { TableModule } from 'primeng/table';
|
import { TableModule } from 'primeng/table';
|
||||||
|
import { AutoCompleteCompleteEvent, AutoCompleteModule, AutoCompleteSelectEvent } from 'primeng/autocomplete';
|
||||||
|
import { BadgeModule } from 'primeng/badge';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { TagModule } from 'primeng/tag';
|
||||||
|
|
||||||
interface ProcessedFile {
|
interface ProcessedFile {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,16 +27,26 @@ interface ProcessedFile {
|
|||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
PanelModule,
|
PanelModule,
|
||||||
TableModule
|
TableModule,
|
||||||
|
AutoCompleteModule,
|
||||||
|
BadgeModule,
|
||||||
|
TagModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class FileConverterComponent {
|
export class FileConverterComponent implements OnInit {
|
||||||
_fileFormats: string[] = [];
|
_fileFormats: string[] = [];
|
||||||
accept: string = '';
|
accept: string = '';
|
||||||
|
selected = '';
|
||||||
|
invalidFileTypeMessageSummary: string = '';
|
||||||
|
url: string = '';
|
||||||
|
requestHeaders: any;
|
||||||
|
selectedFile: File[] | null = null;
|
||||||
|
|
||||||
@Output() fileSelected = new EventEmitter<File[]>();
|
@Output() fileSelected = new EventEmitter<File[]>();
|
||||||
|
@Input() isBeta: boolean = false;
|
||||||
|
@Input() filteredFiles: string[] = [];
|
||||||
@Input() isPreview: boolean = true;
|
@Input() isPreview: boolean = true;
|
||||||
@Input () title: string = 'File Converter';
|
@Input() title: string = 'File Converter';
|
||||||
@Input() processedFiles: ProcessedFile[] = [];
|
@Input() processedFiles: ProcessedFile[] = [];
|
||||||
@Input()
|
@Input()
|
||||||
set fileFormats(formats: string[]) {
|
set fileFormats(formats: string[]) {
|
||||||
@@ -40,14 +54,44 @@ export class FileConverterComponent {
|
|||||||
this.accept = formats.join(',');
|
this.accept = formats.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File type selector
|
||||||
|
@Output() autoComplete = new EventEmitter<AutoCompleteCompleteEvent>();
|
||||||
|
@Output() selectedFormat = new EventEmitter<string>();
|
||||||
|
@Input() fileTypeSelector: boolean = false;
|
||||||
|
|
||||||
|
// Upload file to server
|
||||||
|
@Input() baseUrl = '';
|
||||||
|
@Input() method : 'post' | 'put' = 'post';
|
||||||
|
@Input() headers: HttpHeaders = new HttpHeaders();
|
||||||
|
@Output() upload = new EventEmitter<FileUploadEvent>();
|
||||||
|
|
||||||
get fileFormats(): string[] {
|
get fileFormats(): string[] {
|
||||||
return this._fileFormats;
|
return this._fileFormats;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedFile: File[] | null = null;
|
ngOnInit(): void {
|
||||||
|
this.requestHeaders = this.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
choose(_: any, callback: () => void) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
onFileSelect(event: FileSelectEvent): void {
|
onFileSelect(event: FileSelectEvent): void {
|
||||||
this.selectedFile = event.files;
|
this.selectedFile = event.currentFiles;
|
||||||
this.fileSelected.emit(this.selectedFile!);
|
this.fileSelected.emit(this.selectedFile!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAutoComplete(event: AutoCompleteCompleteEvent): void {
|
||||||
|
this.autoComplete.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onAutoCompleteDropdownClick(event: AutoCompleteSelectEvent): void {
|
||||||
|
this.selectedFormat.emit(event.value.name);
|
||||||
|
this.selected = event.value.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadEvent() {
|
||||||
|
this.upload.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
4
tools/src/environments/environment.ts
Normal file
4
tools/src/environments/environment.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
uploadServiceBaseUrl: 'http://localhost:1337'
|
||||||
|
};
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@304&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
|||||||
@@ -1,2 +1,11 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@import "primeicons/primeicons.css";
|
@import "primeicons/primeicons.css";
|
||||||
|
body,
|
||||||
|
body .p-component
|
||||||
|
{
|
||||||
|
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 304;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { FileConverterComponent } from '../../../app/shared/upload/file-converter.component';
|
import { FileConverterComponent } from '../../../app/shared/upload/file-converter.component';
|
||||||
import { DdsToPngService } from './dds-to-png.service';
|
import { DdsToPngService } from './dds-to-png.service';
|
||||||
|
import { ProcessedFile } from '../../../app/models/conversion.model';
|
||||||
interface ProcessedFile {
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
format: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dds-to-png',
|
selector: 'app-dds-to-png',
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { Injectable } from '@angular/core';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DdsToPngService {
|
export class DdsToPngService {
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
parseHeaders(arrayBuffer: ArrayBuffer) {
|
parseHeaders(arrayBuffer: ArrayBuffer) {
|
||||||
const header = new DataView(arrayBuffer, 0, 128);
|
const header = new DataView(arrayBuffer, 0, 128);
|
||||||
const height = header.getUint32(12, true);
|
const height = header.getUint32(12, true);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
bottomPlaceholder="*/5 * * * *"
|
bottomPlaceholder="*/5 * * * *"
|
||||||
[bottomDisabled]="true"
|
[bottomDisabled]="true"
|
||||||
[bottomValue]="cronExpression"
|
[bottomValue]="cronExpression"
|
||||||
|
[isBeta]="true"
|
||||||
(topChange)="getCronExpression($event)">
|
(topChange)="getCronExpression($event)">
|
||||||
</app-dual-textarea>
|
</app-dual-textarea>
|
||||||
|
|
||||||
<p>Still in beta, don't rely on this tool!</p>
|
|
||||||
@@ -9,7 +9,6 @@ import { DualTextareaComponent } from '../../../app/shared/dual-textarea/dual-te
|
|||||||
imports: [DualTextareaComponent]
|
imports: [DualTextareaComponent]
|
||||||
})
|
})
|
||||||
export class TextToCronComponent {
|
export class TextToCronComponent {
|
||||||
|
|
||||||
cronExpression: string = '';
|
cronExpression: string = '';
|
||||||
|
|
||||||
getCronExpression(description: string): string {
|
getCronExpression(description: string): string {
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Observable, tap } from 'rxjs';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class ImageService {
|
|
||||||
private baseUrl = 'http://localhost:1337'; // replace with your API base URL
|
|
||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
|
||||||
|
|
||||||
convertImage(image: File, format: string): Observable<any> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', image);
|
|
||||||
|
|
||||||
let imgToken = localStorage.getItem('imgToken');
|
|
||||||
|
|
||||||
const headers = new HttpHeaders({
|
|
||||||
'2311d8d8-607d-4747-8939-1bde65643254': imgToken!
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.http.post(`${this.baseUrl}/convert/${format}`, formData, { headers });
|
|
||||||
}
|
|
||||||
|
|
||||||
seteAntiforgeryToken(): void {
|
|
||||||
this.http.get<string>(`${this.baseUrl}/antiforgery/token`, { responseType: 'text' as 'json' }).pipe(
|
|
||||||
tap(token => localStorage.setItem('imgToken', token))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
<p>
|
<app-file-converter
|
||||||
image-converter works!
|
title="Image converter"
|
||||||
</p>
|
method="post"
|
||||||
|
[isPreview]="false"
|
||||||
|
[fileTypeSelector]="true"
|
||||||
|
[processedFiles]="processedFiles"
|
||||||
|
[fileFormats]="fileFormats"
|
||||||
|
[filteredFiles]="filteredFormats"
|
||||||
|
[baseUrl]="url"
|
||||||
|
[headers]="headers"
|
||||||
|
[isBeta]="true"
|
||||||
|
(autoComplete)="filterFormats($event)"
|
||||||
|
(fileSelected)="onFileSelected($event)"
|
||||||
|
(selectedFormat)="onFormatSelected($event)"
|
||||||
|
(upload)="onUploadClicked()"
|
||||||
|
>
|
||||||
|
</app-file-converter>
|
||||||
@@ -1,10 +1,101 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { map, Subscription } from 'rxjs';
|
||||||
|
import { DropdownModule } from 'primeng/dropdown';
|
||||||
|
import { AutoCompleteCompleteEvent, AutoCompleteModule } from 'primeng/autocomplete';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ImageService } from './image-converter.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FileConverterComponent } from "../../../app/shared/upload/file-converter.component";
|
||||||
|
import { Format, ProcessedFile } from '../../../app/models/conversion.model';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-image-converter',
|
selector: 'app-image-converter',
|
||||||
templateUrl: 'image-converter.component.html',
|
templateUrl: 'image-converter.component.html',
|
||||||
styleUrls: ['image-converter.component.scss']
|
styleUrls: ['image-converter.component.scss'],
|
||||||
|
standalone: true,
|
||||||
|
imports: [DropdownModule, AutoCompleteModule, FormsModule, CommonModule, FileConverterComponent]
|
||||||
})
|
})
|
||||||
export class ImageConverterComponent {
|
export class ImageConverterComponent implements OnInit, OnDestroy {
|
||||||
|
constructor(private ImageService: ImageService) { }
|
||||||
|
|
||||||
}
|
url = 'http://localhost:1337/convert';
|
||||||
|
filteredFormats: string[] = [];
|
||||||
|
formats: Format[] = [];
|
||||||
|
selectedFormat: string | undefined;
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
selected = '';
|
||||||
|
headers = new HttpHeaders();
|
||||||
|
processedFiles: ProcessedFile[] = [];
|
||||||
|
fileFormats: string[] = ["image/*"];
|
||||||
|
selectedFile: File[] | null = null;
|
||||||
|
|
||||||
|
filterFormats(event: AutoCompleteCompleteEvent) {
|
||||||
|
let filtered: any[] = [];
|
||||||
|
let query = event.query;
|
||||||
|
|
||||||
|
for (let index = 0; index < (this.formats as any[]).length; index++) {
|
||||||
|
let format = (this.formats as any[])[index];
|
||||||
|
if (format.name.toLowerCase().indexOf(query.toLowerCase()) == 0) {
|
||||||
|
filtered.push(format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filteredFormats = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadClicked() {
|
||||||
|
if (this.selectedFormat && this.selectedFile) {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.ImageService.getMimeType(this.selectedFormat).subscribe((typeResponse) => {
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.ImageService.convertImage(this.selectedFile![0], this.selectedFormat!)
|
||||||
|
.pipe(map((response: any) => {
|
||||||
|
const blob = new Blob([response], { type: typeResponse });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const processedFile = {
|
||||||
|
name: this.selectedFile![0].name.replace(/\.[^/.]+$/, `.${this.selectedFormat?.toLowerCase()}`),
|
||||||
|
link: blobUrl,
|
||||||
|
format: this.selectedFormat
|
||||||
|
} as ProcessedFile;
|
||||||
|
this.processedFiles.push(processedFile);
|
||||||
|
}))
|
||||||
|
.subscribe()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onFormatSelected(format: string) {
|
||||||
|
this.selectedFormat = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFileSelected(input: File[]): void {
|
||||||
|
this.selectedFile = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.subscriptions.push(this.ImageService.setAntiforgeryToken().subscribe());
|
||||||
|
|
||||||
|
this.subscriptions.push(this.ImageService.getFormats()
|
||||||
|
.pipe(map(formats => {
|
||||||
|
this.formats = formats.map(format => {
|
||||||
|
return {
|
||||||
|
name: format,
|
||||||
|
code: format.toLowerCase()
|
||||||
|
} as Format;
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.subscribe());
|
||||||
|
|
||||||
|
this.headers = new HttpHeaders({
|
||||||
|
'2311d8d8-607d-4747-8939-1bde65643254': localStorage.getItem('imgToken')!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ImageService {
|
||||||
|
private baseUrl = environment.uploadServiceBaseUrl;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
convertImage(image: File, format: string): Observable<any> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', image);
|
||||||
|
|
||||||
|
let imgToken = localStorage.getItem('imgToken');
|
||||||
|
|
||||||
|
const headers = new HttpHeaders({
|
||||||
|
'2311d8d8-607d-4747-8939-1bde65643254': imgToken!
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.http.post(`${this.baseUrl}/convert/${format}`, formData, { headers, responseType: 'blob' }); }
|
||||||
|
|
||||||
|
setAntiforgeryToken(): Observable<string> {
|
||||||
|
return this.http.get<string>(`${this.baseUrl}/antiforgery/token`, { responseType: 'text' as 'json' }).pipe(
|
||||||
|
map((token) => {
|
||||||
|
localStorage.setItem('imgToken', token.replace('"', ''));
|
||||||
|
return token;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMimeType(simpleType: string): Observable<string> {
|
||||||
|
return this.http.get<string>(`${this.baseUrl}/mimetype/${simpleType}`, { responseType: 'text' as 'json' });
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormats(): Observable<string[]> {
|
||||||
|
return this.http.get<string[]>(`${this.baseUrl}/formats`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user