From 83cc652b525b94c42de901c94f3bdf048fbd6bb8 Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Thu, 1 Sep 2022 15:40:56 +0200 Subject: [PATCH] Add ability to tell image to shrink only Useful for thumbnails, no need to enlarge a small image --- .../src/managers/image/image-converter.service.ts | 7 +++++-- .../src/app/routes/images/images.component.ts | 2 +- .../customize-dialog.component.html | 4 ++++ .../customize-dialog.component.ts | 2 ++ frontend/src/app/routes/view/view.component.ts | 15 +++++++-------- frontend/src/app/services/api/image.service.ts | 14 ++++++++++++-- shared/src/dto/api/image.dto.ts | 11 ++++++----- shared/src/util/parse-simple.ts | 12 ++++++++---- 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/backend/src/managers/image/image-converter.service.ts b/backend/src/managers/image/image-converter.service.ts index 35dfe7e..97455f2 100644 --- a/backend/src/managers/image/image-converter.service.ts +++ b/backend/src/managers/image/image-converter.service.ts @@ -72,19 +72,22 @@ export class ImageConverterService { // Do modifications if (options.height || options.width) { - if (options.height && options.width) { + if ((options.height && options.width)) { sharpWrapper.operation('resize', { width: options.width, height: options.height, fit: 'fill', kernel: 'cubic', + withoutEnlargement: options.shrinkonly, }); } else { sharpWrapper.operation('resize', { width: options.width, height: options.height, - fit: 'contain', + fit: 'inside', kernel: 'cubic', + + withoutEnlargement: options.shrinkonly, }); } } diff --git a/frontend/src/app/routes/images/images.component.ts b/frontend/src/app/routes/images/images.component.ts index 93b0cd1..4b42b2a 100644 --- a/frontend/src/app/routes/images/images.component.ts +++ b/frontend/src/app/routes/images/images.component.ts @@ -71,7 +71,7 @@ export class ImagesComponent implements OnInit { getThumbnailUrl(image: EImage) { return ( - this.imageService.GetImageURL(image.id, ImageFileType.QOI) + '?height=480' + this.imageService.GetImageURL(image.id, ImageFileType.QOI) + '?height=480&shrinkonly=yes' ); } diff --git a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html index 7ca19e8..6c2f41e 100644 --- a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html +++ b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html @@ -68,6 +68,10 @@ +
+ Shrink only +
+
Greyscale
diff --git a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.ts b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.ts index b70047c..c7cd558 100644 --- a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.ts +++ b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.ts @@ -32,6 +32,7 @@ export class CustomizeDialogComponent implements OnInit { public rotate: number; public flipx: boolean; public flipy: boolean; + public shrinkonly: boolean; public greyscale: boolean; public noalpha: boolean; public negative: boolean; @@ -64,6 +65,7 @@ export class CustomizeDialogComponent implements OnInit { quality: this.quality ?? undefined, flipx: this.flipx, flipy: this.flipy, + shrinkonly: this.shrinkonly, greyscale: this.greyscale, noalpha: this.noalpha, negative: this.negative, diff --git a/frontend/src/app/routes/view/view.component.ts b/frontend/src/app/routes/view/view.component.ts index 608c1bc..19b8957 100644 --- a/frontend/src/app/routes/view/view.component.ts +++ b/frontend/src/app/routes/view/view.component.ts @@ -80,11 +80,13 @@ export class ViewComponent implements OnInit { if (HasFailed(metadata)) return this.utilService.quitError(metadata.getReason()); + // Get width of screen in pixels + const width = window.innerWidth * window.devicePixelRatio; + // Populate fields with metadata - this.previewLink = this.imageService.GetImageURL( - this.id, - metadata.fileTypes.master, - ); + this.previewLink = + this.imageService.GetImageURL(this.id, metadata.fileTypes.master) + + (width > 1 ? `?width=${width}&shrinkonly=yes` : ''); this.hasOriginal = metadata.fileTypes.original !== undefined; @@ -162,10 +164,7 @@ export class ViewComponent implements OnInit { ); } - this.utilService.showSnackBar( - 'Image deleted', - SnackBarType.Success, - ); + this.utilService.showSnackBar('Image deleted', SnackBarType.Success); this.router.navigate(['/']); } diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts index 58394d6..d3c0e4c 100644 --- a/frontend/src/app/services/api/image.service.ts +++ b/frontend/src/app/services/api/image.service.ts @@ -15,7 +15,13 @@ import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { AsyncFailable } from 'picsur-shared/dist/types'; -import { Fail, FT, HasFailed, HasSuccess, Open } from 'picsur-shared/dist/types/failable'; +import { + Fail, + FT, + HasFailed, + HasSuccess, + Open +} from 'picsur-shared/dist/types/failable'; import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto'; import { ApiService } from './api.service'; import { UserService } from './user.service'; @@ -107,7 +113,9 @@ export class ImageService { const baseURL = this.location.protocol + '//' + this.location.host; const extension = FileType2Ext(filetype ?? ''); - return `${baseURL}/i/${image}${HasSuccess(extension) ? '.' + extension : ''}`; + return `${baseURL}/i/${image}${ + HasSuccess(extension) ? '.' + extension : '' + }`; } public GetImageURLCustomized( @@ -129,6 +137,8 @@ export class ImageService { queryParams.push(`rotate=${options.rotate}`); if (options.flipx !== undefined) queryParams.push(`flipx=${options.flipx}`); if (options.flipy !== undefined) queryParams.push(`flipy=${options.flipy}`); + if (options.shrinkonly !== undefined) + queryParams.push(`shrinkonly=${options.shrinkonly}`); if (options.greyscale !== undefined) queryParams.push(`greyscale=${options.greyscale}`); if (options.noalpha !== undefined) diff --git a/shared/src/dto/api/image.dto.ts b/shared/src/dto/api/image.dto.ts index cb66797..058b876 100644 --- a/shared/src/dto/api/image.dto.ts +++ b/shared/src/dto/api/image.dto.ts @@ -2,15 +2,15 @@ import { z } from 'zod'; import { EImageSchema } from '../../entities/image.entity'; import { EUserSchema } from '../../entities/user.entity'; import { createZodDto } from '../../util/create-zod-dto'; -import { ParseBool } from '../../util/parse-simple'; +import { ParseBool, ParseInt } from '../../util/parse-simple'; import { ImageEntryVariant } from '../image-entry-variant.enum'; export const ImageRequestParamsSchema = z .object({ - height: z.preprocess(Number, z.number().int().min(1).max(32767)), - width: z.preprocess(Number, z.number().int().min(1).max(32767)), + height: z.preprocess(ParseInt, z.number().int().min(1).max(32767)), + width: z.preprocess(ParseInt, z.number().int().min(1).max(32767)), rotate: z.preprocess( - Number, + ParseInt, z.number().int().multipleOf(90).min(0).max(360), ), flipx: z.preprocess(ParseBool, z.boolean()), @@ -18,7 +18,8 @@ export const ImageRequestParamsSchema = z greyscale: z.preprocess(ParseBool, z.boolean()), noalpha: z.preprocess(ParseBool, z.boolean()), negative: z.preprocess(ParseBool, z.boolean()), - quality: z.preprocess(Number, z.number().int().min(1).max(100)), + shrinkonly: z.preprocess(ParseBool, z.boolean()), + quality: z.preprocess(ParseInt, z.number().int().min(1).max(100)), }) .partial(); diff --git a/shared/src/util/parse-simple.ts b/shared/src/util/parse-simple.ts index 01e59eb..45b9804 100644 --- a/shared/src/util/parse-simple.ts +++ b/shared/src/util/parse-simple.ts @@ -14,13 +14,17 @@ export const ParseInt = ( value: unknown, fallback?: T, ): number | T => { - if (typeof value === 'number') return value; + if (typeof value === 'number') return Math.round(value); if (typeof value === 'boolean') return value ? 1 : 0; if (typeof value === 'string') { - const parsed = parseInt(value); - if (!isNaN(parsed)) return parsed; + const parsed = Number(value); + if (!isNaN(parsed)) return Math.round(parsed); } - return fallback === undefined ? (null as T) : fallback; + return fallback === undefined + ? (null as T) + : fallback === null + ? fallback + : Math.round(fallback); }; export const ParseString = (