• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bpatrik / pigallery2 / 6488973310

11 Oct 2023 11:05PM UTC coverage: 65.19% (+1.3%) from 63.854%
6488973310

push

github

bpatrik
tweak tests

1360 of 2403 branches covered (0.0%)

Branch coverage included in aggregate %.

4056 of 5905 relevant lines covered (68.69%)

14577.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.21
/src/backend/model/fileprocessing/PhotoProcessing.ts
1
import * as path from 'path';
1✔
2
import {constants as fsConstants, promises as fsp} from 'fs';
1✔
3
import * as os from 'os';
1✔
4
import * as crypto from 'crypto';
1✔
5
import {ProjectPath} from '../../ProjectPath';
1✔
6
import {Config} from '../../../common/config/private/Config';
1✔
7
import {MediaRendererInput, PhotoWorker, SvgRendererInput, ThumbnailSourceType,} from '../threading/PhotoWorker';
1✔
8
import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter';
1✔
9
import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO';
10
import {SupportedFormats} from '../../../common/SupportedFormats';
1✔
11
import {PersonEntry} from '../database/enitites/PersonEntry';
12
import {SVGIconConfig} from '../../../common/config/public/ClientConfig';
13

14
export class PhotoProcessing {
1✔
15
  private static initDone = false;
1✔
16
  private static taskQue: ITaskExecuter<MediaRendererInput | SvgRendererInput, void> = null;
1✔
17
  private static readonly CONVERTED_EXTENSION = '.webp';
1✔
18

19
  public static init(): void {
20
    if (this.initDone === true) {
23✔
21
      return;
22✔
22
    }
23

24
    if (Config.Server.Threading.enabled === true) {
1!
25
      if (Config.Server.Threading.thumbnailThreads > 0) {
×
26
        Config.Media.Thumbnail.concurrentThumbnailGenerations =
×
27
          Config.Server.Threading.thumbnailThreads;
28
      } else {
29
        Config.Media.Thumbnail.concurrentThumbnailGenerations = Math.max(
×
30
          1,
31
          os.cpus().length - 1
32
        );
33
      }
34
    } else {
35
      Config.Media.Thumbnail.concurrentThumbnailGenerations = 1;
1✔
36
    }
37

38
    this.taskQue = new TaskExecuter(
1✔
39
      Config.Media.Thumbnail.concurrentThumbnailGenerations,
40
      (input): Promise<void> => PhotoWorker.render(input)
×
41
    );
42

43
    this.initDone = true;
1✔
44
  }
45

46
  public static async generatePersonThumbnail(
47
    person: PersonEntry
48
  ): Promise<string> {
49
    // load parameters
50
    const photo: PhotoDTO = person.sampleRegion.media;
×
51
    const mediaPath = path.join(
×
52
      ProjectPath.ImageFolder,
53
      photo.directory.path,
54
      photo.directory.name,
55
      photo.name
56
    );
57
    const size: number = Config.Media.Thumbnail.personThumbnailSize;
×
58
    const faceRegion = person.sampleRegion.media.metadata.faces.find(f => f.name === person.name);
×
59
    // generate thumbnail path
60
    const thPath = PhotoProcessing.generatePersonThumbnailPath(
×
61
      mediaPath,
62
      faceRegion,
63
      size
64
    );
65

66
    // check if thumbnail already exist
67
    try {
×
68
      await fsp.access(thPath, fsConstants.R_OK);
×
69
      return thPath;
×
70
    } catch (e) {
71
      // ignoring errors
72
    }
73

74
    const margin = {
×
75
      x: Math.round(
76
        faceRegion.box.width *
77
        Config.Media.Thumbnail.personFaceMargin
78
      ),
79
      y: Math.round(
80
        faceRegion.box.height *
81
        Config.Media.Thumbnail.personFaceMargin
82
      ),
83
    };
84

85
    // run on other thread
86
    const input = {
×
87
      type: ThumbnailSourceType.Photo,
88
      mediaPath,
89
      size,
90
      outPath: thPath,
91
      makeSquare: false,
92
      cut: {
93
        left: Math.round(
94
          Math.max(0, faceRegion.box.left - margin.x / 2)
95
        ),
96
        top: Math.round(
97
          Math.max(0, faceRegion.box.top - margin.y / 2)
98
        ),
99
        width: faceRegion.box.width + margin.x,
100
        height: faceRegion.box.height + margin.y,
101
      },
102
      useLanczos3: Config.Media.Thumbnail.useLanczos3,
103
      quality: Config.Media.Thumbnail.quality,
104
      smartSubsample: Config.Media.Thumbnail.smartSubsample,
105
    } as MediaRendererInput;
106
    input.cut.width = Math.min(
×
107
      input.cut.width,
108
      photo.metadata.size.width - input.cut.left
109
    );
110
    input.cut.height = Math.min(
×
111
      input.cut.height,
112
      photo.metadata.size.height - input.cut.top
113
    );
114

115
    await fsp.mkdir(ProjectPath.FacesFolder, {recursive: true});
×
116
    await PhotoProcessing.taskQue.execute(input);
×
117
    return thPath;
×
118
  }
119

120
  public static generateConvertedPath(mediaPath: string, size: number): string {
121
    const file = path.basename(mediaPath);
260✔
122
    return path.join(
260✔
123
      ProjectPath.TranscodedFolder,
124
      ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
125
      file + '_' + size + 'q' + Config.Media.Thumbnail.quality + (Config.Media.Thumbnail.smartSubsample ? 'cs' : '') + PhotoProcessing.CONVERTED_EXTENSION
260!
126
    );
127
  }
128

129
  public static generatePersonThumbnailPath(
130
    mediaPath: string,
131
    faceRegion: FaceRegion,
132
    size: number
133
  ): string {
134
    return path.join(
×
135
      ProjectPath.FacesFolder,
136
      crypto
137
        .createHash('md5')
138
        .update(
139
          mediaPath +
140
          '_' +
141
          faceRegion.name +
142
          '_' +
143
          faceRegion.box.left +
144
          '_' +
145
          faceRegion.box.top
146
        )
147
        .digest('hex') +
148
      '_' +
149
      size +
150
      '_' + Config.Media.Thumbnail.personFaceMargin +
151
      PhotoProcessing.CONVERTED_EXTENSION
152
    );
153
  }
154

155
  /**
156
   * Tells if the path is valid with the current config
157
   * @param convertedPath
158
   */
159
  public static async isValidConvertedPath(
160
    convertedPath: string
161
  ): Promise<boolean> {
162
    const origFilePath = path.join(
8✔
163
      ProjectPath.ImageFolder,
164
      path.relative(
165
        ProjectPath.TranscodedFolder,
166
        convertedPath.substring(0, convertedPath.lastIndexOf('_'))
167
      )
168
    );
169

170
    if (path.extname(convertedPath) !== PhotoProcessing.CONVERTED_EXTENSION) {
8!
171
      return false;
×
172
    }
173

174
    const sizeStr = convertedPath.substring(
8✔
175
      convertedPath.lastIndexOf('_') + 1,
176
      convertedPath.lastIndexOf('q')
177
    );
178

179
    const size = parseInt(sizeStr, 10);
8✔
180

181
    if (
8✔
182
      (size + '').length !== sizeStr.length ||
20✔
183
      (Config.Media.Thumbnail.thumbnailSizes.indexOf(size) === -1 &&
184
        Config.Media.Photo.Converting.resolution !== size)
185
    ) {
186
      return false;
2✔
187
    }
188

189

190
    let qualityStr = convertedPath.substring(
6✔
191
      convertedPath.lastIndexOf('q') + 1,
192
      convertedPath.length - path.extname(convertedPath).length
193
    );
194

195

196
    if (Config.Media.Thumbnail.smartSubsample) {
6!
197
      if (!qualityStr.endsWith('cs')) { // remove chromatic subsampling flag if exists
6!
198
        return false;
×
199
      }
200
      qualityStr = qualityStr.slice(0, -2);
6✔
201
    }
202

203
    const quality = parseInt(qualityStr, 10);
6✔
204

205
    if ((quality + '').length !== qualityStr.length ||
6!
206
      quality !== Config.Media.Thumbnail.quality) {
207
      return false;
×
208
    }
209

210
    try {
6✔
211
      await fsp.access(origFilePath, fsConstants.R_OK);
6✔
212
    } catch (e) {
213
      return false;
3✔
214
    }
215

216
    return true;
3✔
217
  }
218

219
  public static async convertPhoto(mediaPath: string): Promise<string> {
220
    return this.generateThumbnail(
×
221
      mediaPath,
222
      Config.Media.Photo.Converting.resolution,
223
      ThumbnailSourceType.Photo,
224
      false
225
    );
226
  }
227

228
  static async convertedPhotoExist(
229
    mediaPath: string,
230
    size: number
231
  ): Promise<boolean> {
232
    // generate thumbnail path
233
    const outPath = PhotoProcessing.generateConvertedPath(mediaPath, size);
×
234

235
    // check if file already exist
236
    try {
×
237
      await fsp.access(outPath, fsConstants.R_OK);
×
238
      return true;
×
239
    } catch (e) {
240
      // ignoring errors
241
    }
242
    return false;
×
243
  }
244

245

246
  public static async generateThumbnail(
247
    mediaPath: string,
248
    size: number,
249
    sourceType: ThumbnailSourceType,
250
    makeSquare: boolean
251
  ): Promise<string> {
252
    // generate thumbnail path
253
    const outPath = PhotoProcessing.generateConvertedPath(mediaPath, size);
×
254

255
    // check if file already exist
256
    try {
×
257
      await fsp.access(outPath, fsConstants.R_OK);
×
258
      return outPath;
×
259
    } catch (e) {
260
      // ignoring errors
261
    }
262

263
    // run on other thread
264
    const input = {
×
265
      type: sourceType,
266
      mediaPath,
267
      size,
268
      outPath,
269
      makeSquare,
270
      useLanczos3: Config.Media.Thumbnail.useLanczos3,
271
      quality: Config.Media.Thumbnail.quality,
272
      smartSubsample: Config.Media.Thumbnail.smartSubsample,
273
    } as MediaRendererInput;
274

275
    const outDir = path.dirname(input.outPath);
×
276

277
    await fsp.mkdir(outDir, {recursive: true});
×
278
    await this.taskQue.execute(input);
×
279
    return outPath;
×
280
  }
281

282
  public static isPhoto(fullPath: string): boolean {
283
    const extension = path.extname(fullPath).toLowerCase();
290✔
284
    return SupportedFormats.WithDots.Photos.indexOf(extension) !== -1;
290✔
285
  }
286

287
  public static async renderSVG(
288
    svgIcon: SVGIconConfig,
289
    outPath: string,
290
    color = '#000'
×
291
  ): Promise<string> {
292

293
    // check if file already exist
294
    try {
×
295
      await fsp.access(outPath, fsConstants.R_OK);
×
296
      return outPath;
×
297
    } catch (e) {
298
      // ignoring errors
299
    }
300

301
    const size = 256;
×
302
    // run on other thread
303
    const input = {
×
304
      type: ThumbnailSourceType.Photo,
305
      svgString: `<svg fill="${color}" width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg"
306
viewBox="${svgIcon.viewBox || '0 0 512 512'}">d="${svgIcon.items}</svg>`,
×
307
      size: size,
308
      outPath,
309
      makeSquare: false,
310
      useLanczos3: Config.Media.Thumbnail.useLanczos3,
311
      quality: Config.Media.Thumbnail.quality,
312
      smartSubsample: Config.Media.Thumbnail.smartSubsample,
313
    } as SvgRendererInput;
314

315
    const outDir = path.dirname(input.outPath);
×
316

317
    await fsp.mkdir(outDir, {recursive: true});
×
318
    await this.taskQue.execute(input);
×
319
    return outPath;
×
320
  }
321

322
}
323

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc