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

bpatrik / pigallery2 / 7793107835

06 Feb 2024 01:04AM UTC coverage: 64.377% (-0.6%) from 65.017%
7793107835

Pull #827

github

web-flow
Merge da9c79a53 into 649e7a9a7
Pull Request #827: Fix video not loading #808

1396 of 2504 branches covered (0.0%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

46 existing lines in 3 files now uncovered.

4237 of 6246 relevant lines covered (67.84%)

13788.03 hits per line

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

53.17
/src/backend/model/fileaccess/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 '../PhotoWorker';
1✔
8
import {ITaskExecuter, TaskExecuter} from '../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
    Config.Media.Photo.concurrentThumbnailGenerations = Math.max(
1✔
25
      1,
26
      os.cpus().length - 1
27
    );
28

29
    this.taskQue = new TaskExecuter(
1✔
30
      Config.Media.Photo.concurrentThumbnailGenerations,
UNCOV
31
      (input): Promise<void> => PhotoWorker.render(input)
×
32
    );
33

34
    this.initDone = true;
1✔
35
  }
36

37
  public static async generatePersonThumbnail(
38
    person: PersonEntry
39
  ): Promise<string> {
40
    // load parameters
41
    const photo: PhotoDTO = person.sampleRegion.media;
×
42
    const mediaPath = path.join(
×
43
      ProjectPath.ImageFolder,
44
      photo.directory.path,
45
      photo.directory.name,
46
      photo.name
47
    );
48
    const size: number = Config.Media.Photo.personThumbnailSize;
×
49
    const faceRegion = person.sampleRegion.media.metadata.faces.find(f => f.name === person.name);
×
50
    // generate thumbnail path
51
    const thPath = PhotoProcessing.generatePersonThumbnailPath(
×
52
      mediaPath,
53
      faceRegion,
54
      size
55
    );
56

57
    // check if thumbnail already exist
58
    try {
×
59
      await fsp.access(thPath, fsConstants.R_OK);
×
60
      return thPath;
×
61
    } catch (e) {
62
      // ignoring errors
63
    }
64

65
    const margin = {
×
66
      x: Math.round(
67
        faceRegion.box.width *
68
        Config.Media.Photo.personFaceMargin
69
      ),
70
      y: Math.round(
71
        faceRegion.box.height *
72
        Config.Media.Photo.personFaceMargin
73
      ),
74
    };
75

76
    // run on other thread
77
    const input = {
×
78
      type: ThumbnailSourceType.Photo,
79
      mediaPath,
80
      size,
81
      outPath: thPath,
82
      makeSquare: false,
83
      cut: {
84
        left: Math.round(
85
          Math.max(0, faceRegion.box.left - margin.x / 2)
86
        ),
87
        top: Math.round(
88
          Math.max(0, faceRegion.box.top - margin.y / 2)
89
        ),
90
        width: faceRegion.box.width + margin.x,
91
        height: faceRegion.box.height + margin.y,
92
      },
93
      useLanczos3: Config.Media.Photo.useLanczos3,
94
      quality: Config.Media.Photo.quality,
95
      smartSubsample: Config.Media.Photo.smartSubsample,
96
    } as MediaRendererInput;
97
    input.cut.width = Math.min(
×
98
      input.cut.width,
99
      photo.metadata.size.width - input.cut.left
100
    );
101
    input.cut.height = Math.min(
×
102
      input.cut.height,
103
      photo.metadata.size.height - input.cut.top
104
    );
105

106
    await fsp.mkdir(ProjectPath.FacesFolder, {recursive: true});
×
107
    await PhotoProcessing.taskQue.execute(input);
×
108
    return thPath;
×
109
  }
110

111
  public static generateConvertedPath(mediaPath: string, size: number): string {
112
    const file = path.basename(mediaPath);
435✔
113
    const animated = Config.Media.Photo.animateGif && path.extname(mediaPath).toLowerCase() == '.gif';
435✔
114
    return path.join(
435✔
115
      ProjectPath.TranscodedFolder,
116
      ProjectPath.getRelativePathToImages(path.dirname(mediaPath)),
117
      file + '_' + size + 'q' + Config.Media.Photo.quality +
118
      (animated ? 'anim' : '') +
435✔
119
      (Config.Media.Photo.smartSubsample ? 'cs' : '') +
435!
120
      PhotoProcessing.CONVERTED_EXTENSION
121
    );
122
  }
123

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

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

165
    if (path.extname(convertedPath) !== PhotoProcessing.CONVERTED_EXTENSION) {
5!
166
      return false;
×
167
    }
168
    let nextIndex = convertedPath.lastIndexOf('_') + 1;
5✔
169

170
    const sizeStr = convertedPath.substring(
5✔
171
      nextIndex,
172
      convertedPath.lastIndexOf('q')
173
    );
174
    nextIndex = convertedPath.lastIndexOf('q') + 1;
5✔
175

176
    const size = parseInt(sizeStr, 10);
5✔
177

178
    if (
5✔
179
      (size + '').length !== sizeStr.length ||
10✔
180
      (Config.Media.Photo.thumbnailSizes.indexOf(size) === -1)
181
    ) {
182
      return false;
1✔
183
    }
184

185
    const qualityStr =convertedPath.substring(nextIndex,
4✔
186
      nextIndex+convertedPath.substring(nextIndex).search(/[A-Za-z]/)); // end of quality string
187

188
    const quality = parseInt(qualityStr, 10);
4✔
189

190
    if ((quality + '').length !== qualityStr.length ||
4!
191
      quality !== Config.Media.Photo.quality) {
192
      return false;
×
193
    }
194

195

196
    nextIndex += qualityStr.length;
4✔
197

198

199
    const lowerExt = path.extname(origFilePath).toLowerCase();
4✔
200
    const shouldBeAnimated = Config.Media.Photo.animateGif && lowerExt == '.gif';
4✔
201
    if (shouldBeAnimated) {
4!
202
      if (convertedPath.substring(
×
203
        nextIndex,
204
        nextIndex + 'anim'.length
205
      ) != 'anim') {
206
        return false;
×
207
      }
208
      nextIndex += 'anim'.length;
×
209
    }
210

211

212
    if (Config.Media.Photo.smartSubsample) {
4!
213
      if (convertedPath.substring(
4!
214
        nextIndex,
215
        nextIndex + 2
216
      ) != 'cs') {
217
        return false;
×
218
      }
219
      nextIndex+=2;
4✔
220
    }
221

222
    if(convertedPath.substring(
4!
223
      nextIndex
224
    ).toLowerCase() !== path.extname(convertedPath)){
225
      return false;
×
226
    }
227

228

229
    try {
4✔
230
      await fsp.access(origFilePath, fsConstants.R_OK);
4✔
231
    } catch (e) {
232
      return false;
2✔
233
    }
234

235
    return true;
2✔
236
  }
237

238

239
  static async convertedPhotoExist(
240
    mediaPath: string,
241
    size: number
242
  ): Promise<boolean> {
243
    // generate thumbnail path
244
    const outPath = PhotoProcessing.generateConvertedPath(mediaPath, size);
×
245

246
    // check if file already exist
247
    try {
×
248
      await fsp.access(outPath, fsConstants.R_OK);
×
249
      return true;
×
250
    } catch (e) {
251
      // ignoring errors
252
    }
253
    return false;
×
254
  }
255

256

257
  public static async generateThumbnail(
258
    mediaPath: string,
259
    size: number,
260
    sourceType: ThumbnailSourceType,
261
    makeSquare: boolean
262
  ): Promise<string> {
263
    // generate thumbnail path
UNCOV
264
    const outPath = PhotoProcessing.generateConvertedPath(mediaPath, size);
×
265

266
    // check if file already exist
UNCOV
267
    try {
×
UNCOV
268
      await fsp.access(outPath, fsConstants.R_OK);
×
269
      return outPath;
×
270
    } catch (e) {
271
      // ignoring errors
272
    }
273

274
    // run on other thread
UNCOV
275
    const input = {
×
276
      type: sourceType,
277
      mediaPath,
278
      size,
279
      outPath,
280
      makeSquare,
281
      useLanczos3: Config.Media.Photo.useLanczos3,
282
      quality: Config.Media.Photo.quality,
283
      smartSubsample: Config.Media.Photo.smartSubsample,
284
    } as MediaRendererInput;
285

UNCOV
286
    const outDir = path.dirname(input.outPath);
×
287

UNCOV
288
    await fsp.mkdir(outDir, {recursive: true});
×
UNCOV
289
    await this.taskQue.execute(input);
×
UNCOV
290
    return outPath;
×
291
  }
292

293
  public static isPhoto(fullPath: string): boolean {
294
    const extension = path.extname(fullPath).toLowerCase();
299✔
295
    return SupportedFormats.WithDots.Photos.indexOf(extension) !== -1;
299✔
296
  }
297

298
  public static async renderSVG(
299
    svgIcon: SVGIconConfig,
300
    outPath: string,
301
    color = '#000'
×
302
  ): Promise<string> {
303

304
    // check if file already exist
305
    try {
×
306
      await fsp.access(outPath, fsConstants.R_OK);
×
307
      return outPath;
×
308
    } catch (e) {
309
      // ignoring errors
310
    }
311

312
    const size = 256;
×
313
    // run on other thread
314
    const input = {
×
315
      type: ThumbnailSourceType.Photo,
316
      svgString: `<svg fill="${color}" width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg"
317
viewBox="${svgIcon.viewBox || '0 0 512 512'}">d="${svgIcon.items}</svg>`,
×
318
      size: size,
319
      outPath,
320
      makeSquare: false,
321
      animate: false,
322
      useLanczos3: Config.Media.Photo.useLanczos3,
323
      quality: Config.Media.Photo.quality,
324
      smartSubsample: Config.Media.Photo.smartSubsample,
325
    } as SvgRendererInput;
326

327
    const outDir = path.dirname(input.outPath);
×
328

329
    await fsp.mkdir(outDir, {recursive: true});
×
330
    await this.taskQue.execute(input);
×
331
    return outPath;
×
332
  }
333

334
}
335

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