Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alexander.clausen/snip
  • irp/snip
2 results
Show changes
Commits on Source (12)
Showing
with 1438 additions and 51 deletions
......@@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## 1.12.2
- Hotfix: Fixed a small bug with newly inserted snippets being instantly rendered on a page
even tho they are not placed yet
## 1.12.1
### Fixed
- Hotfix: Fixed a small bug in the render backend which should prevent heavy cpu usage for pdf rendering
- Removed user error.message component for invitation problems as errors are stripped in production
- Collaborator remove modal now closes after removing a collaborator
## 1.12.0
### Fixed
......
......@@ -320,7 +320,10 @@ function RemoveCollaboratorModal({
</Button>
<Button
variant="danger"
onClick={() => removeCollaborator(collaborator)}
onClick={() => {
removeCollaborator(collaborator);
handleClose();
}}
>
Remove
</Button>
......
......@@ -17,11 +17,6 @@ export default function Error({
<>
<h1 className="text-center">Invitation error</h1>
<hr />
{error.message && (
<div className="text-center p-1">
<code>{error.message}</code>
</div>
)}
<p className="text-center text-dark">
The token provided is invalid or has already been used. Make
sure you are using the correct link we have sent you via email.
......
......@@ -178,7 +178,10 @@ export class Page extends LocalPage {
// check if a snip is relevant for this page
return (
this.data.id === snip.page_id ||
this.data.referenced_page_id === snip.page_id
(
this.data.referenced_page_id !== null &&
this.data.referenced_page_id === snip.page_id
)
);
}
}
import { NextFunction, Request, Response } from "express";
import { NotFoundError } from "@snip/database/errors";
import {
DataValidationError,
DataValidationErrorArray,
} from "@snip/snips/errors";
import { NotFoundError } from "@snip/database/errors";
export const errorHandler = (
err: Error,
......@@ -56,8 +56,8 @@ export const asyncHandler =
next: NextFunction,
) => Promise<unknown>,
) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
/** Extend the error class to create an auth failed error */
export class AuthFailedError extends Error {
......
......@@ -47,8 +47,11 @@ const get_bitmap = async function (this: any) {
ImageSnipLegacy.prototype.get_bitmap = get_bitmap;
ImageSnip.prototype.get_bitmap = get_bitmap;
import path from "path";
async function readFromAssetsAsResponse(p: string, ...args: unknown[]) {
const buffer = await readFile(path.join("/assets", p));
async function readFromAssetsAsResponse(
p: string | URL | Request,
..._args: unknown[]
) {
const buffer = await readFile(path.join("/assets", String(p)));
return new Response(buffer);
}
......
......@@ -2,14 +2,14 @@ import { PDFDocument } from "pdf-lib";
import { Readable } from "stream";
import stream2buffer from "@snip/common/stream2buffer";
import { BookDataRet, PageData } from "@snip/database/types";
import { BookDataRet, PageDataRet } from "@snip/database/types";
import renderPage from ".";
import { PageNumberLocation, SelectedPage } from "./renderParams";
export async function render_book_pdf(
bookData: BookDataRet,
pages: PageData[],
pages: PageDataRet[],
selectedPages?: SelectedPage[],
pageNumberLocation?: PageNumberLocation,
): Promise<[Readable, number]> {
......@@ -41,20 +41,16 @@ export async function render_book_pdf(
});
}
const pdfsToMerge = await Promise.all(
pages.map((page) => {
return renderPage(page.id, {
width: 720,
format: "pdf",
force_render: false,
encoding: "gzip",
return_stream: true,
}) as Promise<Readable>;
}),
);
const mergedPdf = await PDFDocument.create();
for (const pdfBytes of pdfsToMerge) {
for (const page of pages) {
const pdfBytes = (await renderPage(page.id, {
width: 720,
format: "pdf",
force_render: false,
encoding: "gzip",
return_stream: true,
})) as Readable;
const pdf = await PDFDocument.load(await stream2buffer(pdfBytes));
// Get the form containing all the fields
const form = pdf.getForm();
......
import express, { Response } from "express";
import { createServer } from "http";
import { AddressInfo } from "net";
import sharp from "sharp";
import { Readable } from "stream";
import { createGzip } from "zlib";
import { z } from "zod";
import { config } from "@snip/config/server";
import service from "@snip/database";
import { NotFoundError } from "@snip/database/errors";
import { LocalPage } from "@snip/render/page";
import { get_snip_from_data } from "@snip/snips";
import {
adminMiddleWare,
......@@ -17,27 +25,17 @@ import {
BadUsage,
errorHandler,
} from "./common/errorHandler";
import renderPage, { render } from "./render";
import { render_book_pdf } from "./render/pdf";
import {
parseSelectedPages,
RenderParams,
validateRenderParams,
} from "./render/renderParams";
import { renderSnip } from "./render/snip";
const app = express();
export const server = createServer(app);
import { AddressInfo } from "net";
import sharp from "sharp";
import { Readable } from "stream";
import { createGzip } from "zlib";
import { z } from "zod";
import { config } from "@snip/config/server";
import { LocalPage } from "@snip/render/page";
import { get_snip_from_data } from "@snip/snips";
import renderPage, { render } from "./render";
import { render_book_pdf } from "./render/pdf";
import { renderSnip } from "./render/snip";
const renderSchema = z.object({
page_id: z.coerce.number(),
......
{
"name": "snip",
"version": "1.12.0",
"version": "1.12.2",
"description": "our digital lab book",
"author": "Sebastian B. Mohr, Markus Osterhoff",
"repository": {
......
......@@ -29,6 +29,7 @@
"@types/sprintf-js": "^1.1.4",
"tree-sitter-cli": "^0.25.1",
"tree-sitter-python": "^0.23.6",
"skia-canvas": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
......@@ -39,7 +40,8 @@
"build": "tsup",
"watch": "tsc -w",
"test": "vitest",
"generate-grammars": "tree-sitter build --wasm node_modules/tree-sitter-python -o ../../assets/grammars/tree-sitter-python.wasm"
"generate-grammars": "tree-sitter build --wasm node_modules/tree-sitter-python -o ../../assets/grammars/tree-sitter-python.wasm",
"json_to_snip": "tsx ./src/json_to_snip.ts"
},
"type": "module"
}
......@@ -265,15 +265,15 @@ export class TextSnip extends BaseSnip {
lineWrap: validation.view?.wrap,
baseline: validation.view?.baseline,
//Base
id: data.id,
page_id: data.page_id,
book_id: data.book_id,
last_updated: data.last_updated,
created: data.created,
x: data.view?.x,
y: data.view?.y,
rot: data.view?.rot,
mirror: data.view?.mirror,
id: validation.id,
page_id: validation.page_id,
book_id: validation.book_id,
last_updated: validation.last_updated,
created: validation.created,
x: validation.view?.x,
y: validation.view?.y,
rot: validation.view?.rot,
mirror: validation.view?.mirror,
});
}
......
/** Debugging script to render a snippet
*
* Can be use to develop a new snippet type and test it easily.
*/
import fs from "fs";
import { Canvas } from "skia-canvas";
import { get_snip_from_data } from "./get_snip_from_data";
const file_name = process.argv[2];
let output_file = process.argv[3];
if (!file_name) {
console.error("Usage: tsx render_json.ts <file_name> [output_file]");
process.exit(1);
}
if (output_file && !output_file.endsWith(".png")) {
console.error("Output file must be a PNG file");
process.exit(1);
}
if (!fs.existsSync(file_name)) {
console.error("File does not exist");
process.exit(1);
}
if (!output_file) {
console.log("No output file specified, rendering to output.png");
output_file = "./output.png";
}
// Read file as JSON
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let data: any;
try {
const f = fs.readFileSync(file_name, "utf8");
data = JSON.parse(f);
} catch (e) {
console.error(`Error reading file: ${e}`);
process.exit(1);
}
const snip = get_snip_from_data(data);
const canvas = new Canvas(1400, 2100);
const ctx = canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, canvas.width, canvas.height);
snip.render(ctx as unknown as OffscreenCanvasRenderingContext2D);
// Save to file
canvas.saveAsSync(output_file);
import { RenderContext } from "@/general/base";
/** An abstract class for all parts.
*
* Parts may be rendered but have far less functionality than snips.
*/
export abstract class Part {
render(ctx: RenderContext): void {
ctx.save();
this._render(ctx);
ctx.restore();
}
/** The actual render function. This should be implemented by all parts.
*
* This function is called by the render function and should not be called
* directly.
*/
abstract _render(ctx: RenderContext): void;
abstract size(ctx: RenderContext): {
width: number;
height: number;
};
}
import { RenderContext } from "@/general/base";
import { Part } from "./abc";
interface TableStyling {
columnWidths?: number[];
borderStyle?: string;
textStyle?: {
fontSize?: number;
headerFontSize?: number;
fill?: string;
};
gap?: {
row?: number;
column?: number;
};
labelWidth?: number;
columnAlign?: ("left" | "right" | "center")[];
}
/** Minimal table with minimal styling.
*/
export class Table extends Part {
private style: TableStyling;
private data: string[][];
private headers?: string[]; // titles horizontal
private labels?: string[]; // titles vertical
private title?: string; // title of the table (placed top left)
constructor(
data: string[][],
styling?: TableStyling,
headers?: string[],
labels?: string[],
title?: string,
) {
super();
this.style = styling || {};
this.data = data;
this.headers = headers;
this.labels = labels;
this.title = title;
}
get columnWidths() {
return this.style.columnWidths || this.data[0]!.map(() => 100);
}
get fontSize() {
return this.style.textStyle?.fontSize || 24;
}
get headerFontSize() {
return this.style.textStyle?.headerFontSize || 24;
}
get labelWidth() {
return this.style.labelWidth || 100;
}
get width() {
let w = this.columnWidths.reduce((a, b) => a + b);
if (this.labels) {
w += this.labelWidth;
}
return w;
}
get height() {
const rowGap = this.style.gap?.row || 0;
let h = this.data.length * (this.fontSize + rowGap);
if (this.headers) {
h += this.headerFontSize + rowGap;
}
return h;
}
size(ctx: RenderContext) {
return {
width: this.width,
height: this.height,
};
}
_render(ctx: RenderContext) {
ctx.save();
// render rect around
ctx.textBaseline = "top";
ctx.strokeStyle = "#000";
ctx.lineWidth = 1;
// Render headers
if (this.headers) {
ctx.font = `bold ${this.headerFontSize}px Arial`;
ctx.fillStyle = this.style.textStyle?.fill || "#000";
if (this.title) {
this.renderRow(ctx, [this.title]);
}
if (this.labels || this.title) {
// skip title
ctx.translate(this.labelWidth, 0);
}
this.renderRow(ctx, this.headers);
ctx.translate(
this.labels ? -this.labelWidth : 0,
this.fontSize + (this.style.gap?.row || 0),
);
//Draw line
const m = ctx.measureText(this.headers[0]!);
console.log(m);
ctx.beginPath();
ctx.moveTo(0, -m.hangingBaseline - (this.style.gap?.row || 0) / 2);
ctx.lineTo(
this.width,
-m.hangingBaseline - (this.style.gap?.row || 0) / 2,
);
ctx.stroke();
}
// Set font
ctx.font = `${this.fontSize}px PlexMono`;
ctx.fillStyle = this.style.textStyle?.fill || "#000";
// Render data
this.renderRows(ctx, this.data);
ctx.restore();
}
private renderRows(ctx: RenderContext, data: string[][]) {
const rowGap = this.style.gap?.row || 0;
let height = 0;
for (let i = 0; i < data.length; i++) {
let offset = 0;
// resolve labels
if (this.labels && this.labels[i]) {
ctx.font = `bold ${this.fontSize}px PlexMono`;
offset = this.labelWidth;
const rowLabel = this.labels[i]!;
// center
ctx.fillText(
rowLabel,
(offset - ctx.measureText(rowLabel).width) / 2,
0,
);
}
ctx.font = `${this.fontSize}px PlexMono`;
ctx.translate(offset, 0);
height += this.renderRow(ctx, data[i]!) + rowGap;
ctx.translate(-offset, this.fontSize + rowGap);
}
// Draw line
if (this.labels) {
ctx.beginPath();
ctx.moveTo(this.labelWidth, this.fontSize / 3);
ctx.lineTo(this.labelWidth, -(height + this.fontSize / 1.5));
ctx.stroke();
}
}
private renderRow(ctx: RenderContext, row: string[]) {
let currentX = 0;
const colGap = this.style?.gap?.column || 0;
for (let i = 0; i < row.length; i++) {
const width = this.columnWidths[i]!;
const text = row[i]!;
const align = this.style.columnAlign?.[i] || "center";
const textWidth = ctx.measureText(text).width;
let x = 0;
switch (align) {
case "left":
x = currentX + colGap / 2;
break;
case "right":
x = currentX + width - textWidth - colGap / 2;
break;
case "center":
x = currentX + (width / 2 - textWidth / 2);
break;
}
console.log(text, x, currentX, width / 2, textWidth);
ctx.fillText(text, x, 0);
currentX += width;
}
return this.fontSize;
}
}
import { Part } from "./abc";
import { RenderContext } from "@/general/base";
export interface FontStyle {
fontSize: number;
fontFamily: string;
fontWeight: string;
textAlign: "left" | "right" | "center";
textBaseline: "top" | "middle" | "bottom";
fill: string;
}
export class TextPart extends Part {
protected text: string;
protected fontStyle: Required<FontStyle>;
constructor(text: string, fontStyle?: Partial<FontStyle>) {
super();
this.text = text;
this.fontStyle = {
fontSize: fontStyle?.fontSize || 24,
fontFamily: fontStyle?.fontFamily || "Arial",
fontWeight: fontStyle?.fontWeight || "normal",
textAlign: fontStyle?.textAlign || "left",
textBaseline: fontStyle?.textBaseline || "top",
fill: fontStyle?.fill || "black",
};
}
get fontString() {
return `${this.fontStyle.fontWeight} ${this.fontStyle.fontSize}px ${this.fontStyle.fontFamily}`;
}
get fontSize() {
return this.fontStyle.fontSize;
}
_render(ctx: RenderContext) {
ctx.font = this.fontString;
ctx.fillStyle = this.fontStyle.fill;
ctx.textAlign = this.fontStyle.textAlign;
ctx.textBaseline = this.fontStyle.textBaseline;
ctx.fillText(this.text, 0, 0);
}
size(ctx: RenderContext) {
return {
width: ctx.measureText(this.text).width,
height: this.fontSize,
};
}
}
export class KeyValuePart extends TextPart {
private value: string;
private fontStyleValue: Required<FontStyle>;
constructor(
key: string,
value: string,
fontStyleKey?: Partial<FontStyle>,
fontStyleValue?: Partial<FontStyle>,
) {
super(
key + ": ",
fontStyleKey || {
fontSize: 24,
fontFamily: "Arial",
fontWeight: "bold",
textAlign: "left",
textBaseline: "top",
fill: "black",
},
);
this.value = value;
this.fontStyleValue = {
fontSize: fontStyleValue?.fontSize || 24,
fontFamily: fontStyleValue?.fontFamily || "Arial",
fontWeight: fontStyleValue?.fontWeight || "normal",
textAlign: fontStyleValue?.textAlign || "left",
textBaseline: fontStyleValue?.textBaseline || "top",
fill: fontStyleValue?.fill || "black",
};
}
get fontStringValue() {
return `${this.fontStyleValue.fontWeight} ${this.fontStyleValue.fontSize}px ${this.fontStyleValue.fontFamily}`;
}
_render(ctx: RenderContext) {
// Render key
super._render(ctx);
const metrics = super.size(ctx);
ctx.font = this.fontStringValue;
ctx.fillStyle = this.fontStyleValue.fill;
ctx.textAlign = this.fontStyleValue.textAlign;
ctx.textBaseline = this.fontStyleValue.textBaseline;
ctx.fillText(this.value, metrics.width, 0);
}
size(ctx: RenderContext) {
const metrics = super.size(ctx);
const metricsValue = ctx.measureText(this.value);
return {
width: metrics.width + metricsValue.width,
height: Math.max(metrics.height, this.fontStyleValue.fontSize),
};
}
}
/** Text block with out wrapping */
export class TextBlock extends Part {
protected text: string;
protected fontStyle: Required<FontStyle>;
protected maxWidth: number;
constructor(
text: string,
maxWidth: number,
fontStyle?: Partial<FontStyle>,
) {
super();
this.text = text;
this.maxWidth = maxWidth;
this.fontStyle = {
fontSize: fontStyle?.fontSize || 24,
fontFamily: fontStyle?.fontFamily || "Arial",
fontWeight: fontStyle?.fontWeight || "normal",
textAlign: fontStyle?.textAlign || "left",
textBaseline: fontStyle?.textBaseline || "top",
fill: fontStyle?.fill || "black",
};
}
get fontString() {
return `${this.fontStyle.fontWeight} ${this.fontStyle.fontSize}px ${this.fontStyle.fontFamily}`;
}
_render(ctx: RenderContext): void {
ctx.font = this.fontString;
ctx.fillStyle = this.fontStyle.fill;
ctx.textAlign = this.fontStyle.textAlign;
ctx.textBaseline = this.fontStyle.textBaseline;
const block = this.text.split("\\n");
for (let i = 0; i < block.length; i++) {
const [, y] = this.__renderWrap(ctx, block[i]!);
ctx.translate(0, y);
}
}
__renderWrap(ctx: RenderContext, text: string): [number, number] {
const parts = text.match(/\S+|\s+/g) || [];
let currentX = 0;
let currentY = 0;
for (let part of parts) {
const metrics = ctx.measureText(part);
if (currentX + metrics.width > this.maxWidth) {
currentX = 0;
currentY +=
metrics.fontBoundingBoxAscent +
metrics.fontBoundingBoxDescent;
part = part.trimStart();
}
ctx.fillText(part, currentX, currentY);
currentX += ctx.measureText(part).width;
}
return [currentX, currentY + this.fontStyle.fontSize];
}
size(ctx: RenderContext) {
ctx.font = this.fontString;
const parts = this.text.match(/\S+|\s+/g) || [];
let currentX = 0;
let currentY = this.fontStyle.fontSize;
for (let part of parts) {
const metrics = ctx.measureText(part);
if (currentX + metrics.width > this.maxWidth) {
currentX = 0;
currentY +=
metrics.fontBoundingBoxAscent +
metrics.fontBoundingBoxDescent;
part = part.trimStart();
}
currentX += ctx.measureText(part).width;
}
return {
width: this.maxWidth,
height: currentY + this.fontStyle.fontSize,
};
}
}
......@@ -5,6 +5,7 @@ import { LogfileSnip } from "./spec/logfile";
import { MacroSpecSnip } from "./spec/macrospec";
import { MotorsSnip } from "./spec/motors";
import { TimestampSnip } from "./spec/timestamp";
import { StampySnip } from "./stampy";
const TYPE_TO_SNIP: Map<string, typeof BaseSnip> = new Map();
......@@ -25,4 +26,7 @@ TYPE_TO_SNIP.set("uprp/spec/matlab", ImageSnip);
// SPOC mapping
// TODO:@Markus
// Stampy
TYPE_TO_SNIP.set("uprp/stampy", StampySnip);
export default TYPE_TO_SNIP;
This diff is collapsed.
{
"type": "uprp/stampy",
"book_id": -1,
"data": {
"stampy": {
"meta": {
"version": 1,
"instrument": "GINIX",
"setup": "PB tomo",
"experiment": "run123",
"date": "2024-9-27"
},
"sample": {
"name": "sample01a",
"id": 0,
"comment": "NR_PREV\\nImaging of top part"
},
"measurement": {
"volume": {
"diameter": 1,
"height": 1,
"fovw": 1.4,
"fovh": 1.2,
"unit": "mm"
},
"stack": { "motor": "stz", "factorh": 1.1, "values": [0] },
"detector": {
"name": "PCO.edge",
"pixelSize": 0.65e-6,
"pixels": [2560, 2160]
},
"grid": {
"motors": ["cx", "cy"],
"method": "hexagonal",
"factorw": 1.1,
"prefill": 1,
"values": [[0, 0]]
},
"timing": {
"illumination": 0.035,
"interval": 0.015,
"unit": "second",
"mode": "continuous-1",
"topupSync": "no",
"topupParameter": [100.9, 100.8],
"trgLine": 2,
"trgDelay": 2000
},
"tomo": {
"motor": "stzrot",
"frames": 3000,
"commands": ["oslow; sleep(1);", "cslow;"],
"angles": {
"range": 360.014,
"safety": 20,
"unit": "degree"
},
"velocity": { "tomo": 11972, "moving": 240000 }
},
"empty": {
"frames": 500,
"commands": ["", ""],
"scheme": "pre-volume",
"pattern": "eted"
},
"dark": { "frames": 100, "commands": ["cslow; sleep(1);", ""] },
"range": "all"
},
"motors": {
"tomo": {
"stx": 180,
"cx": 2.50001,
"sty": -3.94607,
"cy": 1.60297,
"stz": -1,
"cz": 2.07352,
"stzrot": 0.000194
},
"empty": {
"stx": 180,
"cx": 2.50001,
"sty": -3.94607,
"cy": -4,
"stz": -1,
"cz": 2.07352,
"stzrot": 0.000194
},
"final": {
"stx": 180,
"cx": 2.50001,
"sty": -3.94607,
"cy": 1.60297,
"stz": -1,
"cz": 2.07352,
"stzrot": 0.000194
},
"detector": { "mdetx": 0, "mdety": 0, "mdetz": 0 },
"energy": { "mono": 8, "unit": "keV" },
"innerLoop": [
{ "x": 0, "y": 0, "z": 0 },
{ "x": 1, "y": 1, "z": 1 },
{ "x": 2, "y": 2, "z": 2 },
{ "x": 3, "y": 3, "z": 3 }
],
"outerLoop": [
{ "mdetx": 1.1, "mdety": 1.2, "mdetz": 1.3 },
{ "mdetx": 2.1, "mdety": 2.2, "mdetz": 2.3 },
{ "mdetx": 3.1, "mdety": 3.2, "mdetz": 3.3 },
{ "mdetx": 4.1, "mdety": 4.2, "mdetz": 4.3 }
]
}
}
},
"view": {
"x": 10,
"y": 0
}
}
......@@ -1013,6 +1013,9 @@ importers:
'@types/sprintf-js':
specifier: ^1.1.4
version: 1.1.4
skia-canvas:
specifier: 'catalog:'
version: 2.0.2
tree-sitter-cli:
specifier: ^0.25.1
version: 0.25.1
......