Skip to content

Commit 88adad4

Browse files
committed
feat(store): add w=original support to fileSendTransformedImage, keep track of original image width and height
1 parent 5479066 commit 88adad4

File tree

12 files changed

+625
-133
lines changed

12 files changed

+625
-133
lines changed

examples/file-handling/test/e2e.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ test("e2e", async (t) => {
9595
t.ok(blob.size > 5);
9696
});
9797

98+
t.test("retrieve original image", async (t) => {
99+
const { posts } = await apiPostList(fetchFn);
100+
101+
const postWithImage = posts.find((it) => it.headerImage);
102+
103+
t.equal(postWithImage.headerImage.contentType, "image/jpeg");
104+
105+
const blob = await apiPostHeaderImage(
106+
fetchFn,
107+
{
108+
id: postWithImage.id,
109+
},
110+
{
111+
w: "original",
112+
},
113+
);
114+
115+
t.ok(blob.size > 5);
116+
});
117+
98118
t.test("teardown", async (t) => {
99119
server.close();
100120
await cleanupTestPostgresDatabase(sql);

gen/store.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function applyStoreStructure(generator) {
1313
)
1414
.keys({
1515
q: T.number().min(1).max(100).default(75),
16-
w: T.number().min(1).max(99999),
16+
w: T.anyOf().values(T.number().min(1).max(99999), "original"),
1717
})
1818
.loose(),
1919

@@ -24,14 +24,16 @@ export function applyStoreStructure(generator) {
2424
.keys({
2525
accessToken: T.string(),
2626
q: T.number().min(1).max(100).default(75),
27-
w: T.number().min(1).max(99999),
27+
w: T.anyOf().values(T.number().min(1).max(99999), "original"),
2828
})
2929
.loose(),
3030

3131
T.object("fileResponse").keys({
3232
id: T.uuid(),
3333
name: T.string(),
3434
contentType: T.string(),
35+
originalWidth: T.number().optional(),
36+
originalHeight: T.number().optional(),
3537
url: T.string(),
3638
placeholderImage: T.string().optional(),
3739
altText: T.string().optional(),
@@ -48,6 +50,8 @@ export function applyStoreStructure(generator) {
4850
.keys({
4951
transforms: T.any().optional(),
5052
transformedFromOriginal: T.string().optional(),
53+
originalWidth: T.number().optional(),
54+
originalHeight: T.number().optional(),
5155
placeholderImage: T.string().optional(),
5256
altText: T.string().optional(),
5357
})

packages/store/src/file-send.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ export async function fileSendTransformedImageResponse(
157157
acceptsWebp ? "webp" : acceptsAvif ? "avif" : "none"
158158
}-w${w}-q${q}`;
159159

160-
const loadedFile = file.meta?.transforms?.[transformKey];
160+
const loadedFile =
161+
w === "original" ? file.id : file.meta?.transforms?.[transformKey];
161162

162163
if (!loadedFile) {
163164
await queueWorkerAddJob(sql, {

packages/store/src/file-send.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,31 @@ test("store/file-send", async (t) => {
160160
}
161161
});
162162

163+
t.test("original", async (t) => {
164+
await reloadFile();
165+
const ctx = createKoaContext();
166+
167+
ctx.validatedQuery = {
168+
w: "original",
169+
q: 75,
170+
};
171+
ctx.req.headers["accept"] = "*/*";
172+
173+
await fileSendTransformedImageResponse(sql, s3Client, ctx, file);
174+
await execFileJobs();
175+
176+
t.equal(ctx.res.getHeader("Content-Type"), "image/png");
177+
t.equal(
178+
Object.keys(file.meta.transforms ?? {}).length,
179+
0,
180+
"No transform is added.",
181+
);
182+
183+
// Refetch should use the original again.
184+
await fileSendTransformedImageResponse(sql, s3Client, ctx, file);
185+
t.equal(ctx.res.getHeader("Content-Type"), "image/png");
186+
});
187+
163188
t.test("accept webp", async (t) => {
164189
await reloadFile();
165190
const ctx = createKoaContext();
@@ -175,6 +200,8 @@ test("store/file-send", async (t) => {
175200

176201
t.equal(ctx.res.getHeader("Content-Type"), "image/png");
177202
t.ok(file.meta.transforms["compas-image-transform-webp-w10-q75"]);
203+
t.equal(file.meta.originalWidth, 16);
204+
t.equal(file.meta.originalHeight, 16);
178205

179206
// Refetch should use the newly transformed image
180207
await fileSendTransformedImageResponse(sql, s3Client, ctx, file);

packages/store/src/file.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ export function fileFormatMetadata(file, options) {
288288
id: file.id,
289289
name: file.name,
290290
contentType: file.contentType,
291+
originalWidth: file.meta?.originalWidth,
292+
originalHeight: file.meta?.originalHeight,
291293
altText: file.meta?.altText,
292294
placeholderImage: file.meta?.placeholderImage,
293295
url: options.url,

packages/store/src/files-jobs.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ export function jobFileCleanup(
1414
bucketName: string,
1515
): import("./queue-worker.js").QueueWorkerHandler;
1616
/**
17-
* Returns a {@link QueueWorkerHandler} that generates a `meta.placeholderImage` for the
18-
* provided `fileId`. The `compas.file.generatePlaceholderImage` job is inserted when
17+
* Returns a {@link QueueWorkerHandler} that populates `meta.width` and `meta.height`,
18+
* and also generates a `meta.placeholderImage` for the provided `fileId`. The
19+
* `compas.file.generatePlaceholderImage` job is inserted when
1920
* `fileCreateOrUpdate` is provided with the `schedulePlaceholderImageJob` option.
2021
*
2122
* @param {import("@aws-sdk/client-s3").S3Client} s3Client
@@ -28,7 +29,9 @@ export function jobFileGeneratePlaceholderImage(
2829
): import("./queue-worker.js").QueueWorkerHandler;
2930
/**
3031
* Returns a {@link QueueWorkerHandler} that generates a trasnformed image for the
31-
* provided `fileId` and other settings. This job is inserted by {@link fileSendTransformedImageResponse} when it encounters an not yet transformed option combination.
32+
* provided `fileId` and other settings. This job is inserted by
33+
* {@link fileSendTransformedImageResponse} when it encounters an not yet transformed
34+
* option combination.
3235
*
3336
* @param {import("@aws-sdk/client-s3").S3Client} s3Client
3437
* @returns {import("./queue-worker.js").QueueWorkerHandler}

packages/store/src/files-jobs.js

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ export function jobFileCleanup(s3Client, bucketName) {
4545
}
4646

4747
/**
48-
* Returns a {@link QueueWorkerHandler} that generates a `meta.placeholderImage` for the
49-
* provided `fileId`. The `compas.file.generatePlaceholderImage` job is inserted when
48+
* Returns a {@link QueueWorkerHandler} that populates `meta.width` and `meta.height`,
49+
* and also generates a `meta.placeholderImage` for the provided `fileId`. The
50+
* `compas.file.generatePlaceholderImage` job is inserted when
5051
* `fileCreateOrUpdate` is provided with the `schedulePlaceholderImageJob` option.
5152
*
5253
* @param {import("@aws-sdk/client-s3").S3Client} s3Client
@@ -91,35 +92,37 @@ export function jobFileGeneratePlaceholderImage(s3Client, bucketName) {
9192
return;
9293
}
9394

94-
const placeholderImageBuffer = await sharp(buffer)
95-
.rotate()
95+
const sharpInstance = sharp(buffer).rotate();
96+
const metadata = await sharpInstance.metadata();
97+
98+
const placeholderImageBuffer = await sharpInstance
9699
.resize(10)
97100
.jpeg()
98101
.toBuffer();
99102

100-
await queries.fileUpdate(sql, {
101-
update: {
102-
meta: {
103-
$set: {
104-
path: ["placeholderImage"],
105-
value: `data:image/jpeg;base64,${placeholderImageBuffer.toString(
106-
"base64",
107-
)}`,
108-
},
109-
},
110-
},
111-
where: {
112-
id: file.id,
113-
},
114-
});
103+
// Set placeholderImage, originalWidth and originalHeight atomically.
104+
await query`UPDATE "file"
105+
SET
106+
"meta" = jsonb_set(jsonb_set(
107+
jsonb_set("meta", '{placeholderImage}', ${`"data:image/jpeg;base64,${placeholderImageBuffer.toString(
108+
"base64",
109+
)}"`}::jsonb), '{originalHeight}',
110+
${String(metadata.height)}::jsonb),
111+
'{originalWidth}', ${String(
112+
metadata.width,
113+
)}::jsonb)
114+
WHERE
115+
id = ${file.id}`.exec(sql);
115116

116117
eventStop(event);
117118
};
118119
}
119120

120121
/**
121122
* Returns a {@link QueueWorkerHandler} that generates a trasnformed image for the
122-
* provided `fileId` and other settings. This job is inserted by {@link fileSendTransformedImageResponse} when it encounters an not yet transformed option combination.
123+
* provided `fileId` and other settings. This job is inserted by
124+
* {@link fileSendTransformedImageResponse} when it encounters an not yet transformed
125+
* option combination.
123126
*
124127
* @param {import("@aws-sdk/client-s3").S3Client} s3Client
125128
* @returns {import("./queue-worker.js").QueueWorkerHandler}
@@ -197,9 +200,14 @@ export function jobFileTransformImage(s3Client) {
197200
const sharpInstance = sharp(buffer);
198201
sharpInstance.rotate();
199202

200-
const { width: currentWidth } = await sharpInstance.metadata();
203+
const metadataPromise = sharpInstance.metadata();
204+
205+
const originalWidth =
206+
file.meta?.originalWidth ?? (await metadataPromise).width;
207+
const originalHeight =
208+
file.meta?.originalHeight ?? (await metadataPromise).height;
201209

202-
if (!isNil(currentWidth) && currentWidth > options.w) {
210+
if (!isNil(originalWidth) && originalWidth > options.w) {
203211
// Only resize if width is greater than the needed with, so we don't upscale
204212
sharpInstance.resize(options.w);
205213
}
@@ -239,6 +247,22 @@ export function jobFileTransformImage(s3Client) {
239247
// @ts-expect-error
240248
await atomicSetTransformKey(sql, file.id, transformKey, image.id);
241249

250+
if (
251+
(isNil(file.meta?.originalWidth) || isNil(file.meta?.originalHeight)) &&
252+
!isNil(originalWidth) &&
253+
!isNil(originalHeight)
254+
) {
255+
// Update the original image to include the width and height.
256+
await query`UPDATE "file"
257+
SET
258+
"meta" = jsonb_set(jsonb_set("meta", '{originalHeight}', ${String(
259+
originalHeight,
260+
)}::jsonb), '{originalWidth}',
261+
${String(originalWidth)}::jsonb)
262+
WHERE
263+
id = ${file.id}`.exec(sql);
264+
}
265+
242266
eventStop(event);
243267
};
244268

@@ -257,10 +281,10 @@ export function jobFileTransformImage(s3Client) {
257281
"meta" = jsonb_set(CASE
258282
WHEN coalesce("meta", '{}'::jsonb) ? 'transforms'
259283
THEN "meta"
260-
ELSE jsonb_set(coalesce("meta", '{}'::jsonb), '{transforms}', '{}'::jsonb) END,
261-
${`{transforms,${transformKey}}`}, ${JSON.stringify(
262-
newFileId,
263-
)}),
284+
ELSE jsonb_set(coalesce("meta", '{}'::jsonb),
285+
'{transforms}', '{}'::jsonb) END,
286+
${`{transforms,${transformKey}}`},
287+
${JSON.stringify(newFileId)}),
264288
"updatedAt" = now()
265289
WHERE
266290
id = ${fileId}`.exec(sql);

packages/store/src/files-jobs.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ test("store/files-jobs", (t) => {
6767
},
6868
}).exec(sql);
6969

70+
t.ok(reloadedFile.meta.originalWidth);
71+
t.ok(reloadedFile.meta.originalHeight);
7072
t.ok(reloadedFile.meta.placeholderImage);
7173
t.ok(
7274
reloadedFile.meta.placeholderImage.startsWith("data:image/jpeg;base64,"),

0 commit comments

Comments
 (0)