Skip to content

Commit

Permalink
feat: draw on canvas and sync with backend and pdf
Browse files Browse the repository at this point in the history
  • Loading branch information
ledouxm committed Nov 12, 2024
1 parent 0c47394 commit 518d6e3
Show file tree
Hide file tree
Showing 28 changed files with 505 additions and 68 deletions.
1 change: 1 addition & 0 deletions db/migrations/910-add_picture_final_url.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE pictures ADD COLUMN "finalUrl" TEXT;
2 changes: 1 addition & 1 deletion packages/backend/openapi.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/upload/image":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}}
{"openapi":"3.1.0","info":{"title":"CR VIF API","description":"CR VIF API Documentation","version":"1.0"},"components":{"schemas":{}},"paths":{"/api/create-user":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"udap_id":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}},"required":["name","udap_id","email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/login":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"password":{"type":"string"}},"required":["email","password"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/refresh-token":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":true},{"schema":{"type":"string"},"in":"query","name":"refreshToken","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"udap_id":{"type":"string"},"udap":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}},"required":["id","name","udap_id","udap"]},"token":{"type":"string"},"expiresAt":{"type":"string"},"refreshToken":{"type":"string"}},"required":["token","expiresAt","refreshToken"]}}}}}}},"/api/send-reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"}},"required":["email"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/reset-password":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"temporaryLink":{"type":"string"},"newPassword":{"type":"string"}},"required":["temporaryLink","newPassword"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}}}}},"/api/udaps":{"get":{"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"department":{"type":"string"},"completeCoords":{"type":"string"},"visible":{"type":"boolean"},"name":{"type":"string"},"address":{"type":"string"},"zipCode":{"type":"string"},"city":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"},"marianne_text":{"type":"string"},"drac_text":{"type":"string"},"udap_text":{"type":"string"}},"required":["id","department"]}}}}}}}},"/api/upload/image":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/upload/picture":{"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true},{"schema":{"type":"string"},"in":"query","name":"pictureId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}},"/api/upload/picture/{pictureId}/lines":{"post":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true},{"schema":{"type":"string"},"in":"path","name":"pictureId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}}},"/api/pdf/report":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"htmlString":{"type":"string"},"reportId":{"type":"string"},"recipients":{"type":"string"}},"required":["htmlString","reportId","recipients"]}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"string"}}}}}},"get":{"parameters":[{"schema":{"type":"string"},"in":"query","name":"reportId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}}}}}}
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@react-pdf/renderer": "^3.4.2",
"@sentry/node": "^7.70.0",
"@sinclair/typebox": "^0.32.20",
"canvas": "^2.11.2",
"date-fns": "^3.6.0",
"debug": "^4.3.4",
"dotenv": "^16.4.5",
Expand Down
61 changes: 61 additions & 0 deletions packages/backend/src/features/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { db } from "../db/db";
import canvas, { createCanvas, loadImage } from "canvas";
import fs from "fs/promises";

export const getPictureWithLines = async ({ pictureId }: { pictureId: string }) => {
const picture = await db.pictures.findFirst({
where: { id: pictureId },
});

const pictureLines = await db.picture_lines.findFirst({
where: { pictureId },
});

const lines = JSON.parse(pictureLines?.lines || "[]");
const buffer = await applyLinesToPicture({ pictureUrl: picture!.url!, lines });

await fs.writeFile("./test.png", buffer);

return buffer;
};

export const applyLinesToPicture = async ({
pictureUrl,
lines,
}: {
pictureUrl: string;
lines: Array<{ points: { x: number; y: number }[]; color: string }>;
}) => {
try {
const image = await loadImage(pictureUrl);

const canvas = createCanvas(image.width, image.height);
const ctx = canvas.getContext("2d");

ctx.drawImage(image, 0, 0);

ctx.lineWidth = 5;
ctx.lineCap = "round";
ctx.lineJoin = "round";

lines.forEach((line) => {
ctx.strokeStyle = line.color;
if (line.points.length > 0) {
ctx.beginPath();

ctx.moveTo(line.points[0]!.x, line.points[0]!.y);

for (let i = 1; i < line.points.length; i++) {
ctx.lineTo(line.points[i]!.x, line.points[i]!.y);
}

ctx.stroke();
}
});

return canvas.toBuffer("image/png");
} catch (error) {
console.error("Error processing image:", error);
throw error;
}
};
26 changes: 25 additions & 1 deletion packages/backend/src/routes/uploadRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {

debug("adding url to pic", id, "for report", reportId);

await db.pictures.create({ data: { id, url, reportId, createdAt: new Date() } });
await db.pictures.create({ data: { id, url, reportId, createdAt: new Date(), finalUrl: url } });
// try {
// await db.tmp_pictures.delete({ where: { id } });
// } catch (e) {}
Expand Down Expand Up @@ -74,6 +74,30 @@ export const uploadPlugin: FastifyPluginAsyncTypebox = async (fastify, _) => {
return buffer.toString("base64");
},
);

fastify.post(
"/picture/:pictureId/lines",
{
schema: {
params: Type.Object({ pictureId: Type.String() }),
body: Type.Object({
lines: Type.Array(
Type.Object({
points: Type.Array(Type.Object({ x: Type.Number(), y: Type.Number() })),
color: Type.String(),
}),
),
}),
response: { 200: Type.String() },
},
},
async (request) => {
const { pictureId } = request.params;
const { lines } = request.body;

return request.services.upload.handleNotifyPictureLines({ pictureId, lines });
},
);
};

const getFileName = (file: MultipartFile) => {
Expand Down
43 changes: 42 additions & 1 deletion packages/backend/src/services/uploadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ENV } from "../envVars";
import { makeDebug } from "../features/debug";
import { AppError } from "../features/errors";
import { S3 } from "@aws-sdk/client-s3";
import { applyLinesToPicture, getPictureWithLines } from "../features/image";
import { db } from "../db/db";

const client = new S3Client({ endpoint: ENV.AWS_ENDPOINT, region: ENV.AWS_REGION });
const debug = makeDebug("upload");
Expand Down Expand Up @@ -87,7 +89,46 @@ export class UploadService {

return Buffer.from(buffer);
}

async handleNotifyPictureLines({
pictureId,
lines,
}: {
pictureId: string;
lines: Array<{ points: { x: number; y: number }[]; color: string }>;
}) {
debug("Handling picture lines", pictureId);
const picture = await db.pictures.findFirst({
where: { id: pictureId },
});
if (!picture) throw new AppError(404, "Picture not found");

const buffer = await applyLinesToPicture({ pictureUrl: picture.url!, lines });

const name = getPictureName(pictureId, pictureId, Math.round(Date.now() / 1000));

const bucketUrl = `${ENV.MINIO_URL}/${ENV.MINIO_BUCKET}`;

debug("Uploading picture to S3", pictureId);
await imageClient.putObject({
Bucket: bucketUrl,
Key: name,
Body: buffer,
ACL: "public-read",
ContentType: "image/png",
});

debug("Picture uploaded", pictureId);

const url = `${bucketUrl}/${name}`;

await db.pictures.update({ where: { id: pictureId }, data: { finalUrl: url } });

debug(url);
return url;
}
}

export const getPDFName = (reportId: string) => `${reportId}/compte_rendu.pdf`;
export const getPictureName = (reportId: string, pictureId: string) => `${reportId}/pictures/${pictureId}.png`;
export const getPictureName = (reportId: string, pictureId: string, snapshot?: number) =>
`${reportId}/pictures/${pictureId}${snapshot ? `_${snapshot}` : ""}.png`;
Binary file added packages/backend/test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 14 additions & 4 deletions packages/electric-client/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,26 @@ model pdf_snapshot {
}

model pictures {
id String @id
reportId String?
url String?
createdAt DateTime? @db.Timestamp(6)
finalUrl String?
picture_lines picture_lines[]
report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model tmp_pictures {
id String @id
reportId String?
url String?
createdAt DateTime? @db.Timestamp(6)
report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

model tmp_pictures {
model picture_lines {
id String @id
reportId String?
pictureId String?
lines String
createdAt DateTime? @db.Timestamp(6)
report report? @relation(fields: [reportId], references: [id], onDelete: Cascade, onUpdate: NoAction)
pictures pictures? @relation(fields: [pictureId], references: [id], onDelete: Cascade, onUpdate: NoAction)
}
Loading

0 comments on commit 518d6e3

Please sign in to comment.