@jrmc/adonis-attachment 3.3.0-beta.1 → 3.3.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ declare const _default: {
2
+ encode(pixels: Uint8ClampedArray, width: number, height: number, componentX: number, componentY: number): Promise<string>;
3
+ };
4
+ export default _default;
@@ -0,0 +1,6 @@
1
+ import { encode } from 'blurhash';
2
+ export default {
3
+ async encode(pixels, width, height, componentX, componentY) {
4
+ return encode(pixels, width, height, componentX, componentY);
5
+ },
6
+ };
@@ -129,6 +129,7 @@ export class Attachment extends AttachmentBase {
129
129
  mimeType: v.mimeType,
130
130
  meta: v.meta,
131
131
  size: v.size,
132
+ blurhash: v.blurhash,
132
133
  };
133
134
  });
134
135
  }
@@ -11,6 +11,7 @@ import { AttachmentBase } from './attachment_base.js';
11
11
  export declare class Variant extends AttachmentBase implements VariantInterface {
12
12
  #private;
13
13
  key: string;
14
+ blurhash?: string;
14
15
  constructor(drive: DriveService, attributes: VariantAttributes, input?: Input);
15
16
  /**
16
17
  * Getters
@@ -8,10 +8,12 @@ import { AttachmentBase } from './attachment_base.js';
8
8
  export class Variant extends AttachmentBase {
9
9
  key;
10
10
  #folder;
11
+ blurhash;
11
12
  constructor(drive, attributes, input) {
12
13
  super(drive, attributes, input);
13
14
  this.key = attributes.key;
14
15
  this.#folder = attributes.folder;
16
+ this.blurhash = attributes.blurhash;
15
17
  }
16
18
  /**
17
19
  * Getters
@@ -27,6 +29,7 @@ export class Variant extends AttachmentBase {
27
29
  key: this.key,
28
30
  folder: this.folder,
29
31
  name: this.name,
32
+ blurhash: this.blurhash,
30
33
  ...super.toObject(),
31
34
  };
32
35
  }
@@ -1,7 +1,9 @@
1
+ import logger from '@adonisjs/core/services/logger';
1
2
  import string from '@adonisjs/core/helpers/string';
2
3
  import db from '@adonisjs/lucid/services/db';
3
4
  import attachmentManager from '../services/main.js';
4
5
  import * as errors from './errors.js';
6
+ import { imageToBlurhash } from './utils/helpers.js';
5
7
  export class ConverterManager {
6
8
  #record;
7
9
  #attributeName;
@@ -19,11 +21,11 @@ export class ConverterManager {
19
21
  const id = this.#record.model.$attributes['id'];
20
22
  const data = {};
21
23
  if (this.#options.variants) {
22
- for (const option of this.#options.variants) {
24
+ for await (const option of this.#options.variants) {
23
25
  const converter = (await attachmentManager.getConverter(option));
24
26
  if (attachments && converter) {
25
- for (let i = 0; i < attachments.length; i++) {
26
- const input = attachments[i].input;
27
+ for await (const attachment of attachments) {
28
+ const input = attachment.input;
27
29
  const output = await converter.handle({
28
30
  input,
29
31
  options: converter.options,
@@ -31,7 +33,22 @@ export class ConverterManager {
31
33
  if (output === undefined) {
32
34
  throw new errors.E_CANNOT_PATH_BY_CONVERTER();
33
35
  }
34
- const variant = await attachments[i].createVariant(option, output);
36
+ const variant = await attachment.createVariant(option, output);
37
+ if (converter.options.blurhash) {
38
+ if ((typeof converter.options.blurhash !== 'boolean' &&
39
+ converter.options.blurhash.enabled === true) ||
40
+ converter.options.blurhash === true) {
41
+ try {
42
+ const options = typeof converter.options.blurhash !== 'boolean'
43
+ ? converter.options.blurhash
44
+ : undefined;
45
+ variant.blurhash = await imageToBlurhash(variant.input, options);
46
+ }
47
+ catch (error) {
48
+ logger.error(error.message);
49
+ }
50
+ }
51
+ }
35
52
  await attachmentManager.save(variant);
36
53
  }
37
54
  }
@@ -53,10 +70,10 @@ export class ConverterManager {
53
70
  });
54
71
  try {
55
72
  await trx.query().from(Model.table).where('id', id).update(data);
56
- return await trx.commit();
73
+ return trx.commit();
57
74
  }
58
75
  catch (error) {
59
- return await trx.rollback();
76
+ return trx.rollback();
60
77
  }
61
78
  }
62
79
  }
@@ -106,7 +106,7 @@ export default class Record {
106
106
  * For all properties Attachment
107
107
  * Launch async generation variants
108
108
  */
109
- await Promise.allSettled(attachmentAttributeNames.map((name) => {
109
+ for await (const name of attachmentAttributeNames) {
110
110
  const record = this;
111
111
  attachmentManager.queue.push({
112
112
  name: `${this.#model.constructor.name}-${name}`,
@@ -124,7 +124,7 @@ export default class Record {
124
124
  }
125
125
  },
126
126
  });
127
- }));
127
+ }
128
128
  }
129
129
  async detach() {
130
130
  const attachmentAttributeNames = this.#getDirtyAttributeNamesOfAttachment();
@@ -214,7 +214,9 @@ export default class Record {
214
214
  const dirtyValue = this.#model.$dirty[name];
215
215
  const originalValue = this.#model.$original[name]; // if dirtyValue is null, check original type
216
216
  const isDirtyAttachment = dirtyValue instanceof Attachment ||
217
- (Array.isArray(dirtyValue) && dirtyValue.every((item) => item instanceof Attachment));
217
+ (Array.isArray(dirtyValue) &&
218
+ dirtyValue.every((item) => item instanceof Attachment) &&
219
+ dirtyValue.length);
218
220
  const isOriginalAttachment = originalValue instanceof Attachment ||
219
221
  (Array.isArray(originalValue) && originalValue.every((item) => item instanceof Attachment));
220
222
  return isDirtyAttachment || isOriginalAttachment;
@@ -41,6 +41,7 @@ export type Attachment = AttachmentBase & {
41
41
  export type Variant = AttachmentBase & {
42
42
  key: string;
43
43
  folder: string;
44
+ blurhash?: string;
44
45
  toObject(): VariantAttributes;
45
46
  };
46
47
  export type LucidOptions = {
@@ -67,4 +68,5 @@ export type AttachmentAttributes = AttachmentBaseAttributes & {
67
68
  export type VariantAttributes = AttachmentBaseAttributes & {
68
69
  key: string;
69
70
  folder: string;
71
+ blurhash?: string;
70
72
  };
@@ -22,6 +22,11 @@ export type ConverterAttributes = {
22
22
  input: Input;
23
23
  options: ConverterOptions;
24
24
  };
25
+ export type BlurhashOptions = {
26
+ enabled: boolean;
27
+ componentX: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
28
+ componentY: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
29
+ };
25
30
  type jpeg = {
26
31
  format: 'jpeg';
27
32
  options: {
@@ -127,5 +132,6 @@ export type ConverterOptions = {
127
132
  fastShrinkOnLoad?: Boolean;
128
133
  };
129
134
  format?: 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp' | 'avif' | 'heif' | 'tiff' | 'raw' | jpeg | png | gif | webp | avif | heif;
135
+ blurhash?: boolean | BlurhashOptions;
130
136
  };
131
137
  export {};
@@ -4,9 +4,12 @@
4
4
  * @license MIT
5
5
  * @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
6
6
  */
7
+ import type { Input } from '../types/input.js';
8
+ import type { BlurhashOptions } from '../types/converter.js';
7
9
  export declare function cleanObject(obj: any): any;
8
10
  export declare function use(module: string): Promise<any>;
9
11
  export declare function bufferToTempFile(input: Buffer): Promise<string>;
10
12
  export declare function streamToTempFile(input: NodeJS.ReadableStream): Promise<string>;
11
13
  export declare function downloadToTempFile(input: URL): Promise<string>;
12
14
  export declare function isBase64(str: string): boolean;
15
+ export declare function imageToBlurhash(input: Input, options?: BlurhashOptions): Promise<string>;
@@ -11,6 +11,7 @@ import fs from 'node:fs/promises';
11
11
  import { pipeline } from 'node:stream';
12
12
  import { promisify } from 'node:util';
13
13
  import { createWriteStream } from 'node:fs';
14
+ import BlurhashAdapter from '../adapters/blurhash.js';
14
15
  import * as errors from '../errors.js';
15
16
  const streamPipeline = promisify(pipeline);
16
17
  export function cleanObject(obj) {
@@ -91,3 +92,21 @@ export function isBase64(str) {
91
92
  return false;
92
93
  }
93
94
  }
95
+ export function imageToBlurhash(input, options) {
96
+ const { componentX, componentY } = options || { componentX: 4, componentY: 4 };
97
+ return new Promise(async (resolve, reject) => {
98
+ try {
99
+ const sharp = await use('sharp');
100
+ // Convert input to pixels
101
+ const { data: pixels, info: metadata } = await sharp(input)
102
+ .raw()
103
+ .ensureAlpha()
104
+ .toBuffer({ resolveWithObject: true });
105
+ const blurhash = BlurhashAdapter.encode(new Uint8ClampedArray(pixels), metadata.width, metadata.height, componentX, componentY);
106
+ return resolve(blurhash);
107
+ }
108
+ catch (error) {
109
+ return reject(error);
110
+ }
111
+ });
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrmc/adonis-attachment",
3
- "version": "3.3.0-beta.1",
3
+ "version": "3.3.0-beta.3",
4
4
  "type": "module",
5
5
  "description": "Turn any field on your Lucid model to an attachment data type",
6
6
  "engines": {
@@ -65,6 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@poppinss/defer": "^1.1.1",
68
+ "blurhash": "^2.0.5",
68
69
  "exifreader": "^4.26.0",
69
70
  "file-type": "^19.6.0",
70
71
  "mime-types": "^2.1.35"
@@ -94,6 +95,8 @@
94
95
  "flydrive": "^1.1.0",
95
96
  "luxon": "^3.5.0",
96
97
  "prettier": "^3.4.2",
98
+ "sharp": "^0.33.5",
99
+ "sinon": "^19.0.2",
97
100
  "ts-node": "^10.9.2",
98
101
  "typescript": "^5.7.3",
99
102
  "vitepress": "^1.5.0"