Skip to content

Commit

Permalink
Creating configurable SVG icon #587 #331 #325
Browse files Browse the repository at this point in the history
  • Loading branch information
bpatrik committed Aug 8, 2023
1 parent 63236c3 commit d1ed607
Show file tree
Hide file tree
Showing 26 changed files with 256 additions and 99 deletions.
2 changes: 0 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@
"tsConfig": "src/frontend/tsconfig.app.json",
"polyfills": "src/frontend/polyfills.ts",
"assets": [
"src/frontend/assets",
"src/frontend/robots.txt",
{
"glob": "**/*",
Expand Down Expand Up @@ -199,7 +198,6 @@
"src/frontend/styles.css"
],
"assets": [
"src/frontend/assets",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images/",
Expand Down
47 changes: 43 additions & 4 deletions src/backend/model/fileprocessing/PhotoProcessing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import * as os from 'os';
import * as crypto from 'crypto';
import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config';
import {PhotoWorker, RendererInput, ThumbnailSourceType,} from '../threading/PhotoWorker';
import {MediaRendererInput, PhotoWorker, SvgRendererInput, ThumbnailSourceType,} from '../threading/PhotoWorker';
import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter';
import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO';
import {SupportedFormats} from '../../../common/SupportedFormats';
import {PersonEntry} from '../database/enitites/PersonEntry';
import {SVGIconConfig} from '../../../common/config/public/ClientConfig';

export class PhotoProcessing {
private static initDone = false;
private static taskQue: ITaskExecuter<RendererInput, void> = null;
private static taskQue: ITaskExecuter<MediaRendererInput | SvgRendererInput, void> = null;
private static readonly CONVERTED_EXTENSION = '.webp';

public static init(): void {
Expand Down Expand Up @@ -101,7 +102,7 @@ export class PhotoProcessing {
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as RendererInput;
} as MediaRendererInput;
input.cut.width = Math.min(
input.cut.width,
photo.metadata.size.width - input.cut.left
Expand Down Expand Up @@ -240,6 +241,7 @@ export class PhotoProcessing {
return false;
}


public static async generateThumbnail(
mediaPath: string,
size: number,
Expand Down Expand Up @@ -267,7 +269,7 @@ export class PhotoProcessing {
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as RendererInput;
} as MediaRendererInput;

const outDir = path.dirname(input.outPath);

Expand All @@ -280,5 +282,42 @@ export class PhotoProcessing {
const extension = path.extname(fullPath).toLowerCase();
return SupportedFormats.WithDots.Photos.indexOf(extension) !== -1;
}

public static async renderSVG(
svgString: SVGIconConfig,
outPath: string,
color = '#000'
): Promise<string> {

// check if file already exist
try {
await fsp.access(outPath, fsConstants.R_OK);
return outPath;
} catch (e) {
// ignoring errors
}

const size = 256;
// run on other thread
const input = {
type: ThumbnailSourceType.Photo,
svgString: `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg"
viewBox="${Config.Server.svgIcon.viewBox || '0 0 512 512'}">
<path fill="${color}" d="${Config.Server.svgIcon.path}"/></svg>`,
size: size,
outPath,
makeSquare: false,
useLanczos3: Config.Media.Thumbnail.useLanczos3,
quality: Config.Media.Thumbnail.quality,
smartSubsample: Config.Media.Thumbnail.smartSubsample,
} as SvgRendererInput;

const outDir = path.dirname(input.outPath);

await fsp.mkdir(outDir, {recursive: true});
await this.taskQue.execute(input);
return outPath;
}

}

67 changes: 45 additions & 22 deletions src/backend/model/threading/PhotoWorker.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import * as sharp from 'sharp';
import {Metadata, Sharp} from 'sharp';
import {Logger} from '../../Logger';
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
import {FFmpegFactory} from '../FFmpegFactory';
const path = require('path');
import * as path from 'path';

const sharp = require('sharp');

sharp.cache(false);

export class PhotoWorker {
private static videoRenderer: (input: RendererInput) => Promise<void> = null;
private static videoRenderer: (input: MediaRendererInput) => Promise<void> = null;

public static render(input: RendererInput): Promise<void> {
public static render(input: SvgRendererInput | MediaRendererInput): Promise<void> {
if (input.type === ThumbnailSourceType.Photo) {
return this.renderFromImage(input);
}
if (input.type === ThumbnailSourceType.Video) {
return this.renderFromVideo(input);
return this.renderFromVideo(input as MediaRendererInput);
}
throw new Error('Unsupported media type to render thumbnail:' + input.type);
}

public static renderFromImage(input: RendererInput): Promise<void> {
public static renderFromImage(input: SvgRendererInput | MediaRendererInput): Promise<void> {
return ImageRendererFactory.render(input);
}

public static renderFromVideo(input: RendererInput): Promise<void> {
public static renderFromVideo(input: MediaRendererInput): Promise<void> {
if (PhotoWorker.videoRenderer === null) {
PhotoWorker.videoRenderer = VideoRendererFactory.build();
}
Expand All @@ -39,15 +39,13 @@ export enum ThumbnailSourceType {
Video = 2,
}

export interface RendererInput {
interface RendererInput {
type: ThumbnailSourceType;
mediaPath: string;
size: number;
makeSquare: boolean;
outPath: string;
quality: number;
useLanczos3: boolean;
smartSubsample: boolean;
cut?: {
left: number;
top: number;
Expand All @@ -56,10 +54,19 @@ export interface RendererInput {
};
}

export interface MediaRendererInput extends RendererInput {
mediaPath: string;
smartSubsample: boolean;
}

export interface SvgRendererInput extends RendererInput {
svgString: string;
}

export class VideoRendererFactory {
public static build(): (input: RendererInput) => Promise<void> {
public static build(): (input: MediaRendererInput) => Promise<void> {
const ffmpeg = FFmpegFactory.get();
return (input: RendererInput): Promise<void> => {
return (input: MediaRendererInput): Promise<void> => {
return new Promise((resolve, reject): void => {
Logger.silly('[FFmpeg] rendering thumbnail: ' + input.mediaPath);

Expand Down Expand Up @@ -122,16 +129,22 @@ export class VideoRendererFactory {

export class ImageRendererFactory {

public static async render(input: RendererInput): Promise<void> {
Logger.silly(
'[SharpRenderer] rendering photo:' +
input.mediaPath +
', size:' +
input.size
);
const image: Sharp = sharp(input.mediaPath, {failOnError: false});
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {

let image: Sharp;
if ((input as MediaRendererInput).mediaPath) {
Logger.silly(
'[SharpRenderer] rendering photo:' +
(input as MediaRendererInput).mediaPath +
', size:' +
input.size
);
image = sharp((input as MediaRendererInput).mediaPath, {failOnError: false});
} else {
const svg_buffer = Buffer.from((input as SvgRendererInput).svgString);
image = sharp(svg_buffer, { density: 450 });
}
const metadata: Metadata = await image.metadata();

const kernel =
input.useLanczos3 === true
? sharp.kernel.lanczos3
Expand All @@ -157,7 +170,17 @@ export class ImageRendererFactory {
fit: 'cover',
});
}
await image.rotate().webp({effort: 6, quality: input.quality, smartSubsample: input.smartSubsample}).toFile(input.outPath);
if ((input as MediaRendererInput).mediaPath) {
await image.rotate().webp({
effort: 6,
quality: input.quality,
smartSubsample: (input as MediaRendererInput).smartSubsample
}).toFile(input.outPath);
} else {
if ((input as SvgRendererInput).svgString) {
await image.rotate().png({effort: 6, quality: input.quality}).toFile(input.outPath);
}
}

}
}
50 changes: 48 additions & 2 deletions src/backend/routes/PublicRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {UserDTO} from '../../common/entities/UserDTO';
import {ServerTimeEntry} from '../middlewares/ServerTimingMWs';
import {ClientConfig, TAGS} from '../../common/config/public/ClientConfig';
import {QueryParams} from '../../common/QueryParams';
import {PhotoProcessing} from '../model/fileprocessing/PhotoProcessing';

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
Expand Down Expand Up @@ -101,7 +102,7 @@ export class PublicRouter {
.replace(/'/g, '&#039;');
res.tpl.Config = confCopy;
res.tpl.customHTMLHead = Config.Server.customHTMLHead;
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th=>th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th => th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
res.tpl.usedTheme = selectedTheme;

return next();
Expand All @@ -118,7 +119,11 @@ export class PublicRouter {
name: Config.Server.applicationTitle,
icons: [
{
src: 'assets/icon_inv.png',
src: 'icon_inv.svg',
sizes: 'any',
},
{
src: 'icon_inv.png',
sizes: '48x48 72x72 96x96 128x128 256x256',
},
],
Expand All @@ -133,6 +138,47 @@ export class PublicRouter {
});
});

app.get('/icon.svg', (req: Request, res: Response) => {
res.set('Cache-control', 'public, max-age=31536000');
res.send('<svg xmlns="http://www.w3.org/2000/svg"' +
' viewBox="' + (Config.Server.svgIcon.viewBox || '0 0 512 512') + '">' +
'<path d="' + Config.Server.svgIcon.path + '"/></svg>');
});

app.get('/icon_inv.svg', (req: Request, res: Response) => {
res.set('Cache-control', 'public, max-age=31536000');
res.send('<svg xmlns="http://www.w3.org/2000/svg"' +
' viewBox="' + (Config.Server.svgIcon.viewBox || '0 0 512 512') + '">' +
'<path fill="#FFF" d="' + Config.Server.svgIcon.path + '"/></svg>');
});


app.get('/icon.png', async (req: Request, res: Response, next: NextFunction) => {
try {
const p = path.join(ProjectPath.TempFolder, '/icon.png');
await PhotoProcessing.renderSVG(Config.Server.svgIcon, p);
res.sendFile(p, {
maxAge: 31536000,
dotfiles: 'allow',
});
} catch (e) {
return next(e);
}
});

app.get('/icon_inv.png', async (req: Request, res: Response, next: NextFunction) => {
try {
const p = path.join(ProjectPath.TempFolder, '/icon_inv.png');
await PhotoProcessing.renderSVG(Config.Server.svgIcon, p, '#FFF');
res.sendFile(p, {
maxAge: 31536000,
dotfiles: 'allow',
});
} catch (e) {
return next(e);
}
});

app.get(
[
'/',
Expand Down
13 changes: 13 additions & 0 deletions src/common/config/public/ClientConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,19 @@ export class ClientServiceConfig {
}
})
customHTMLHead: string = '';


@ConfigProperty({
type: SVGIconConfig,
tags: {
name: $localize`Svg Icon`,
uiType: 'SVGIconConfig',
priority: ConfigPriority.advanced
} as TAGS,
description: $localize`Sets the icon of the app`,
})
// Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
svgIcon: SVGIconConfig = new SVGIconConfig(`0 0 407 512`, 'M372 232.5l-3.7-6.5c.1-46.4-21.4-65.3-46.5-79.7 7.6-2 15.4-3.6 17.6-13.2 13.1-3.3 15.8-9.4 17.1-15.8 3.4-2.3 14.8-8.7 13.6-19.7 6.4-4.4 10-10.1 8.1-18.1 6.9-7.5 8.7-13.7 5.8-19.4 8.3-10.3 4.6-15.6 1.1-20.9 6.2-11.2.7-23.2-16.6-21.2-6.9-10.1-21.9-7.8-24.2-7.8-2.6-3.2-6-6-16.5-4.7-6.8-6.1-14.4-5-22.3-2.1-9.3-7.3-15.5-1.4-22.6.8C271.6.6 269 5.5 263.5 7.6c-12.3-2.6-16.1 3-22 8.9l-6.9-.1c-18.6 10.8-27.8 32.8-31.1 44.1-3.3-11.3-12.5-33.3-31.1-44.1l-6.9.1c-5.9-5.9-9.7-11.5-22-8.9-5.6-2-8.1-7-19.4-3.4-4.6-1.4-8.9-4.4-13.9-4.3-2.6.1-5.5 1-8.7 3.5-7.9-3-15.5-4-22.3 2.1-10.5-1.3-14 1.4-16.5 4.7-2.3 0-17.3-2.3-24.2 7.8C21.2 16 15.8 28 22 39.2c-3.5 5.4-7.2 10.7 1.1 20.9-2.9 5.7-1.1 11.9 5.8 19.4-1.8 8 1.8 13.7 8.1 18.1-1.2 11 10.2 17.4 13.6 19.7 1.3 6.4 4 12.4 17.1 15.8 2.2 9.5 10 11.2 17.6 13.2-25.1 14.4-46.6 33.3-46.5 79.7l-3.7 6.5c-28.8 17.2-54.7 72.7-14.2 117.7 2.6 14.1 7.1 24.2 11 35.4 5.9 45.2 44.5 66.3 54.6 68.8 14.9 11.2 30.8 21.8 52.2 29.2C159 504.2 181 512 203 512h1c22.1 0 44-7.8 64.2-28.4 21.5-7.4 37.3-18 52.2-29.2 10.2-2.5 48.7-23.6 54.6-68.8 3.9-11.2 8.4-21.3 11-35.4 40.6-45.1 14.7-100.5-14-117.7zm-22.2-8c-1.5 18.7-98.9-65.1-82.1-67.9 45.7-7.5 83.6 19.2 82.1 67.9zm-43 93.1c-24.5 15.8-59.8 5.6-78.8-22.8s-14.6-64.2 9.9-80c24.5-15.8 59.8-5.6 78.8 22.8s14.6 64.2-9.9 80zM238.9 29.3c.8 4.2 1.8 6.8 2.9 7.6 5.4-5.8 9.8-11.7 16.8-17.3 0 3.3-1.7 6.8 2.5 9.4 3.7-5 8.8-9.5 15.5-13.3-3.2 5.6-.6 7.3 1.2 9.6 5.1-4.4 10-8.8 19.4-12.3-2.6 3.1-6.2 6.2-2.4 9.8 5.3-3.3 10.6-6.6 23.1-8.9-2.8 3.1-8.7 6.3-5.1 9.4 6.6-2.5 14-4.4 22.1-5.4-3.9 3.2-7.1 6.3-3.9 8.8 7.1-2.2 16.9-5.1 26.4-2.6l-6 6.1c-.7.8 14.1.6 23.9.8-3.6 5-7.2 9.7-9.3 18.2 1 1 5.8.4 10.4 0-4.7 9.9-12.8 12.3-14.7 16.6 2.9 2.2 6.8 1.6 11.2.1-3.4 6.9-10.4 11.7-16 17.3 1.4 1 3.9 1.6 9.7.9-5.2 5.5-11.4 10.5-18.8 15 1.3 1.5 5.8 1.5 10 1.6-6.7 6.5-15.3 9.9-23.4 14.2 4 2.7 6.9 2.1 10 2.1-5.7 4.7-15.4 7.1-24.4 10 1.7 2.7 3.4 3.4 7.1 4.1-9.5 5.3-23.2 2.9-27 5.6.9 2.7 3.6 4.4 6.7 5.8-15.4.9-57.3-.6-65.4-32.3 15.7-17.3 44.4-37.5 93.7-62.6-38.4 12.8-73 30-102 53.5-34.3-15.9-10.8-55.9 5.8-71.8zm-34.4 114.6c24.2-.3 54.1 17.8 54 34.7-.1 15-21 27.1-53.8 26.9-32.1-.4-53.7-15.2-53.6-29.8 0-11.9 26.2-32.5 53.4-31.8zm-123-12.8c3.7-.7 5.4-1.5 7.1-4.1-9-2.8-18.7-5.3-24.4-10 3.1 0 6 .7 10-2.1-8.1-4.3-16.7-7.7-23.4-14.2 4.2-.1 8.7 0 10-1.6-7.4-4.5-13.6-9.5-18.8-15 5.8.7 8.3.1 9.7-.9-5.6-5.6-12.7-10.4-16-17.3 4.3 1.5 8.3 2 11.2-.1-1.9-4.2-10-6.7-14.7-16.6 4.6.4 9.4 1 10.4 0-2.1-8.5-5.8-13.3-9.3-18.2 9.8-.1 24.6 0 23.9-.8l-6-6.1c9.5-2.5 19.3.4 26.4 2.6 3.2-2.5-.1-5.6-3.9-8.8 8.1 1.1 15.4 2.9 22.1 5.4 3.5-3.1-2.3-6.3-5.1-9.4 12.5 2.3 17.8 5.6 23.1 8.9 3.8-3.6.2-6.7-2.4-9.8 9.4 3.4 14.3 7.9 19.4 12.3 1.7-2.3 4.4-4 1.2-9.6 6.7 3.8 11.8 8.3 15.5 13.3 4.1-2.6 2.5-6.2 2.5-9.4 7 5.6 11.4 11.5 16.8 17.3 1.1-.8 2-3.4 2.9-7.6 16.6 15.9 40.1 55.9 6 71.8-29-23.5-63.6-40.7-102-53.5 49.3 25 78 45.3 93.7 62.6-8 31.8-50 33.2-65.4 32.3 3.1-1.4 5.8-3.2 6.7-5.8-4-2.8-17.6-.4-27.2-5.6zm60.1 24.1c16.8 2.8-80.6 86.5-82.1 67.9-1.5-48.7 36.5-75.5 82.1-67.9zM38.2 342c-23.7-18.8-31.3-73.7 12.6-98.3 26.5-7 9 107.8-12.6 98.3zm91 98.2c-13.3 7.9-45.8 4.7-68.8-27.9-15.5-27.4-13.5-55.2-2.6-63.4 16.3-9.8 41.5 3.4 60.9 25.6 16.9 20 24.6 55.3 10.5 65.7zm-26.4-119.7c-24.5-15.8-28.9-51.6-9.9-80s54.3-38.6 78.8-22.8 28.9 51.6 9.9 80c-19.1 28.4-54.4 38.6-78.8 22.8zM205 496c-29.4 1.2-58.2-23.7-57.8-32.3-.4-12.7 35.8-22.6 59.3-22 23.7-1 55.6 7.5 55.7 18.9.5 11-28.8 35.9-57.2 35.4zm58.9-124.9c.2 29.7-26.2 53.8-58.8 54-32.6.2-59.2-23.8-59.4-53.4v-.6c-.2-29.7 26.2-53.8 58.8-54 32.6-.2 59.2 23.8 59.4 53.4v.6zm82.2 42.7c-25.3 34.6-59.6 35.9-72.3 26.3-13.3-12.4-3.2-50.9 15.1-72 20.9-23.3 43.3-38.5 58.9-26.6 10.5 10.3 16.7 49.1-1.7 72.3zm22.9-73.2c-21.5 9.4-39-105.3-12.6-98.3 43.9 24.7 36.3 79.6 12.6 98.3z');
}

@SubConfigClass({tags: {client: true}, softReadonly: true})
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import {GallerySearchFieldBaseComponent} from './ui/gallery/search/search-field-
import {AppRoutingModule} from './app.routing';
import {CookieService} from 'ngx-cookie-service';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
import {icon, Marker} from 'leaflet';
import {Marker} from 'leaflet';
import {AlbumsComponent} from './ui/albums/albums.component';
import {AlbumComponent} from './ui/albums/album/album.component';
import {AlbumsService} from './ui/albums/albums.service';
Expand Down Expand Up @@ -108,6 +108,7 @@ import {ThemeService} from './model/theme.service';
import {StringifyEnum} from './pipes/StringifyEnum';
import {StringifySearchType} from './pipes/StringifySearchType';
import {MarkerFactory} from './ui/gallery/map/MarkerFactory';
import {IconComponent} from './icon.component';

@Injectable()
export class MyHammerConfig extends HammerGestureConfig {
Expand Down Expand Up @@ -165,6 +166,7 @@ Marker.prototype.options.icon = MarkerFactory.defIcon;
],
declarations: [
AppComponent,
IconComponent,
LoginComponent,
ShareLoginComponent,
GalleryComponent,
Expand Down
24 changes: 24 additions & 0 deletions src/frontend/app/icon.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Component, Input} from '@angular/core';
import {Config} from '../../common/config/public/Config';

@Component({
selector: 'app-icon',
template: `
<svg xmlns="http://www.w3.org/2000/svg"
[attr.width]="width"
[attr.height]="height"
fill="currentcolor"
[attr.viewBox]="Config.Server.svgIcon.viewBox || '0 0 512 512'">
<path [attr.d]="Config.Server.svgIcon.path"/>
</svg>`,
})
export class IconComponent {

@Input() width: number;
@Input() height: number;

protected readonly Config = Config;

constructor() {
}
}
Loading

0 comments on commit d1ed607

Please sign in to comment.