mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
## Move editor under `frontend/editor/`
Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.
### Why
`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
lint config / Storybook.
### What moves
frontend/
├── editor/ ← NEW: everything editor-specific
│ ├── src/ ← was frontend/src/
│ ├── public/ ← was frontend/public/
│ ├── src-tauri/ ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
│ ├── tsconfig*.json, tailwind.config.js, postcss.config.js
│ ├── scripts/
│ ├── .env, .env.desktop, .env.saas
│ └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
├── .gitignore
└── README.md
### Wiring edits (40 files)
- `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
- `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
- `docker/frontend/Dockerfile`
- 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
`.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
- `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
`frontend/editor/DeveloperGuide.md`
Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
relative to script).
### Verification
| Check | Result |
|---|---|
| `task frontend:typecheck:all` (6 variants) | exit 0 |
| `task frontend:lint` (eslint + dpdm) | exit 0 |
| `task frontend:format:check` | exit 0 |
| `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
| `playwright test --list --project=stubbed` | 172 tests discovered |
`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
wrong it wouldn't have built.
### Test plan
- [ ] `frontend-validation.yml` green
- [ ] `e2e-stubbed.yml` green
- [ ] `tauri-build.yml` green on at least one platform
- [ ] `check_toml.yml` runs on a translation-touching PR
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
264 lines
7.1 KiB
JavaScript
264 lines
7.1 KiB
JavaScript
/*! jscanify v1.4.0 | (c) ColonelParrot and other contributors | MIT License */
|
|
|
|
(function (global, factory) {
|
|
typeof exports === "object" && typeof module !== "undefined"
|
|
? (module.exports = factory())
|
|
: typeof define === "function" && define.amd
|
|
? define(factory)
|
|
: (global.jscanify = factory());
|
|
})(this, function () {
|
|
"use strict";
|
|
|
|
/**
|
|
* Calculates distance between two points. Each point must have `x` and `y` property
|
|
* @param {*} p1 point 1
|
|
* @param {*} p2 point 2
|
|
* @returns distance between two points
|
|
*/
|
|
function distance(p1, p2) {
|
|
return Math.hypot(p1.x - p2.x, p1.y - p2.y);
|
|
}
|
|
|
|
class jscanify {
|
|
constructor() { }
|
|
|
|
/**
|
|
* Finds the contour of the paper within the image
|
|
* @param {*} img image to process (cv.Mat)
|
|
* @returns the biggest contour inside the image
|
|
*/
|
|
findPaperContour(img) {
|
|
const imgGray = new cv.Mat();
|
|
cv.Canny(img, imgGray, 50, 200);
|
|
|
|
const imgBlur = new cv.Mat();
|
|
cv.GaussianBlur(
|
|
imgGray,
|
|
imgBlur,
|
|
new cv.Size(3, 3),
|
|
0,
|
|
0,
|
|
cv.BORDER_DEFAULT
|
|
);
|
|
|
|
const imgThresh = new cv.Mat();
|
|
cv.threshold(
|
|
imgBlur,
|
|
imgThresh,
|
|
0,
|
|
255,
|
|
cv.THRESH_OTSU
|
|
);
|
|
|
|
let contours = new cv.MatVector();
|
|
let hierarchy = new cv.Mat();
|
|
|
|
cv.findContours(
|
|
imgThresh,
|
|
contours,
|
|
hierarchy,
|
|
cv.RETR_CCOMP,
|
|
cv.CHAIN_APPROX_SIMPLE
|
|
);
|
|
|
|
let maxArea = 0;
|
|
let maxContourIndex = -1;
|
|
for (let i = 0; i < contours.size(); ++i) {
|
|
let contourArea = cv.contourArea(contours.get(i));
|
|
if (contourArea > maxArea) {
|
|
maxArea = contourArea;
|
|
maxContourIndex = i;
|
|
}
|
|
}
|
|
|
|
const maxContour =
|
|
maxContourIndex >= 0 ?
|
|
contours.get(maxContourIndex) :
|
|
null;
|
|
|
|
imgGray.delete();
|
|
imgBlur.delete();
|
|
imgThresh.delete();
|
|
contours.delete();
|
|
hierarchy.delete();
|
|
return maxContour;
|
|
}
|
|
|
|
/**
|
|
* Highlights the paper detected inside the image.
|
|
* @param {*} image image to process
|
|
* @param {*} options options for highlighting. Accepts `color` and `thickness` parameter
|
|
* @returns `HTMLCanvasElement` with original image and paper highlighted
|
|
*/
|
|
highlightPaper(image, options) {
|
|
options = options || {};
|
|
options.color = options.color || "orange";
|
|
options.thickness = options.thickness || 10;
|
|
const canvas = document.createElement("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
const img = cv.imread(image);
|
|
|
|
const maxContour = this.findPaperContour(img);
|
|
cv.imshow(canvas, img);
|
|
if (maxContour) {
|
|
const {
|
|
topLeftCorner,
|
|
topRightCorner,
|
|
bottomLeftCorner,
|
|
bottomRightCorner,
|
|
} = this.getCornerPoints(maxContour, img);
|
|
|
|
if (
|
|
topLeftCorner &&
|
|
topRightCorner &&
|
|
bottomLeftCorner &&
|
|
bottomRightCorner
|
|
) {
|
|
ctx.strokeStyle = options.color;
|
|
ctx.lineWidth = options.thickness;
|
|
ctx.beginPath();
|
|
ctx.moveTo(...Object.values(topLeftCorner));
|
|
ctx.lineTo(...Object.values(topRightCorner));
|
|
ctx.lineTo(...Object.values(bottomRightCorner));
|
|
ctx.lineTo(...Object.values(bottomLeftCorner));
|
|
ctx.lineTo(...Object.values(topLeftCorner));
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
img.delete();
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* Extracts and undistorts the image detected within the frame.
|
|
*
|
|
* Returns `null` if no paper is detected.
|
|
*
|
|
* @param {*} image image to process
|
|
* @param {*} resultWidth desired result paper width
|
|
* @param {*} resultHeight desired result paper height
|
|
* @param {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect
|
|
* @returns `HTMLCanvasElement` containing undistorted image
|
|
*/
|
|
extractPaper(image, resultWidth, resultHeight, cornerPoints) {
|
|
const canvas = document.createElement("canvas");
|
|
const img = cv.imread(image);
|
|
const maxContour = cornerPoints ? null : this.findPaperContour(img);
|
|
|
|
if(maxContour == null && cornerPoints === undefined){
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
topLeftCorner,
|
|
topRightCorner,
|
|
bottomLeftCorner,
|
|
bottomRightCorner,
|
|
} = cornerPoints || this.getCornerPoints(maxContour, img);
|
|
let warpedDst = new cv.Mat();
|
|
|
|
let dsize = new cv.Size(resultWidth, resultHeight);
|
|
let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
topLeftCorner.x,
|
|
topLeftCorner.y,
|
|
topRightCorner.x,
|
|
topRightCorner.y,
|
|
bottomLeftCorner.x,
|
|
bottomLeftCorner.y,
|
|
bottomRightCorner.x,
|
|
bottomRightCorner.y,
|
|
]);
|
|
|
|
let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
0,
|
|
0,
|
|
resultWidth,
|
|
0,
|
|
0,
|
|
resultHeight,
|
|
resultWidth,
|
|
resultHeight,
|
|
]);
|
|
|
|
let M = cv.getPerspectiveTransform(srcTri, dstTri);
|
|
cv.warpPerspective(
|
|
img,
|
|
warpedDst,
|
|
M,
|
|
dsize,
|
|
cv.INTER_LINEAR,
|
|
cv.BORDER_CONSTANT,
|
|
new cv.Scalar()
|
|
);
|
|
|
|
cv.imshow(canvas, warpedDst);
|
|
|
|
img.delete()
|
|
warpedDst.delete()
|
|
return canvas;
|
|
}
|
|
|
|
/**
|
|
* Calculates the corner points of a contour.
|
|
* @param {*} contour contour from {@link findPaperContour}
|
|
* @returns object with properties `topLeftCorner`, `topRightCorner`, `bottomLeftCorner`, `bottomRightCorner`, each with `x` and `y` property
|
|
*/
|
|
getCornerPoints(contour) {
|
|
let rect = cv.minAreaRect(contour);
|
|
const center = rect.center;
|
|
|
|
let topLeftCorner;
|
|
let topLeftCornerDist = 0;
|
|
|
|
let topRightCorner;
|
|
let topRightCornerDist = 0;
|
|
|
|
let bottomLeftCorner;
|
|
let bottomLeftCornerDist = 0;
|
|
|
|
let bottomRightCorner;
|
|
let bottomRightCornerDist = 0;
|
|
|
|
for (let i = 0; i < contour.data32S.length; i += 2) {
|
|
const point = { x: contour.data32S[i], y: contour.data32S[i + 1] };
|
|
const dist = distance(point, center);
|
|
if (point.x < center.x && point.y < center.y) {
|
|
// top left
|
|
if (dist > topLeftCornerDist) {
|
|
topLeftCorner = point;
|
|
topLeftCornerDist = dist;
|
|
}
|
|
} else if (point.x > center.x && point.y < center.y) {
|
|
// top right
|
|
if (dist > topRightCornerDist) {
|
|
topRightCorner = point;
|
|
topRightCornerDist = dist;
|
|
}
|
|
} else if (point.x < center.x && point.y > center.y) {
|
|
// bottom left
|
|
if (dist > bottomLeftCornerDist) {
|
|
bottomLeftCorner = point;
|
|
bottomLeftCornerDist = dist;
|
|
}
|
|
} else if (point.x > center.x && point.y > center.y) {
|
|
// bottom right
|
|
if (dist > bottomRightCornerDist) {
|
|
bottomRightCorner = point;
|
|
bottomRightCornerDist = dist;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
topLeftCorner,
|
|
topRightCorner,
|
|
bottomLeftCorner,
|
|
bottomRightCorner,
|
|
};
|
|
}
|
|
}
|
|
|
|
return jscanify;
|
|
});
|