mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-06-30 17:24:22 +00:00
Extract the geometry stuff a bit
This commit is contained in:
+73
-137
@@ -52,15 +52,21 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { OcrTemplateService } from 'src/app/services/rest/ocr-template.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
interface DrawingRect {
|
||||
startX: number
|
||||
startY: number
|
||||
endX: number
|
||||
endY: number
|
||||
}
|
||||
|
||||
type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
|
||||
import {
|
||||
DisplayRect,
|
||||
DrawingRect,
|
||||
findHandleAt,
|
||||
findZoneAt,
|
||||
getZoneDisplayRect,
|
||||
getZonePage,
|
||||
HANDLE_SIZE,
|
||||
isZoneOnPage,
|
||||
MoveStart,
|
||||
moveZone,
|
||||
ResizeHandle,
|
||||
resizeZone,
|
||||
sourceRectFromDrawing,
|
||||
} from './zone-geometry'
|
||||
|
||||
type ActiveTab = 'settings' | 'zones' | 'zone'
|
||||
|
||||
@@ -147,11 +153,10 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
isResizing = false
|
||||
resizeHandle: ResizeHandle | null = null
|
||||
resizeZoneIndex: number | null = null
|
||||
private readonly HANDLE_SIZE = 8
|
||||
|
||||
isMoving = false
|
||||
moveZoneIndex: number | null = null
|
||||
private moveStart = { mouseX: 0, mouseY: 0, zoneX: 0, zoneY: 0 }
|
||||
private moveStart: MoveStart = { mouseX: 0, mouseY: 0, zoneX: 0, zoneY: 0 }
|
||||
|
||||
zoneTestResult: OcrZoneTestResult | null = null
|
||||
zoneTesting = false
|
||||
@@ -340,13 +345,11 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
zonePage(zone: OcrTemplateZone): number {
|
||||
const v = zone.page ?? 1
|
||||
if (v === -1) return this.previewPageCount ?? this.previewPage + 1
|
||||
return v >= 1 ? v : 1
|
||||
return getZonePage(zone, this.previewPage, this.previewPageCount)
|
||||
}
|
||||
|
||||
private isOnCurrentPage(zone: OcrTemplateZone): boolean {
|
||||
return this.zonePage(zone) === this.previewPage + 1
|
||||
return isZoneOnPage(zone, this.previewPage, this.previewPageCount)
|
||||
}
|
||||
|
||||
onImageLoad() {
|
||||
@@ -364,7 +367,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
const y = event.clientY - rect.top
|
||||
|
||||
if (this.selectedZoneIndex !== null) {
|
||||
const handle = this.findHandleAt(x, y, this.selectedZoneIndex)
|
||||
const handle = this.findHandleAt({ x, y }, this.selectedZoneIndex)
|
||||
if (handle) {
|
||||
this.isResizing = true
|
||||
this.resizeHandle = handle
|
||||
@@ -373,7 +376,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
const clickedIdx = this.findZoneAt(x, y)
|
||||
const clickedIdx = this.findZoneAt({ x, y })
|
||||
if (clickedIdx !== null && !event.shiftKey) {
|
||||
this.selectZone(clickedIdx)
|
||||
const zone = this.template.zones[clickedIdx]
|
||||
@@ -401,22 +404,12 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (this.isMoving && this.moveZoneIndex !== null) {
|
||||
const zone = this.template.zones[this.moveZoneIndex]
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
const img = this.imageRef.nativeElement
|
||||
const srcW = zone.zone_source_width || img.naturalWidth
|
||||
const srcH = zone.zone_source_height || img.naturalHeight
|
||||
const scaleX = srcW / canvas.width
|
||||
const scaleY = srcH / canvas.height
|
||||
const dx = Math.round((mx - this.moveStart.mouseX) * scaleX)
|
||||
const dy = Math.round((my - this.moveStart.mouseY) * scaleY)
|
||||
zone.x = Math.max(
|
||||
0,
|
||||
Math.min(this.moveStart.zoneX + dx, srcW - zone.width)
|
||||
)
|
||||
zone.y = Math.max(
|
||||
0,
|
||||
Math.min(this.moveStart.zoneY + dy, srcH - zone.height)
|
||||
moveZone(
|
||||
this.template.zones[this.moveZoneIndex],
|
||||
{ x: mx, y: my },
|
||||
this.moveStart,
|
||||
this.canvasSize(),
|
||||
this.imageNaturalSize()
|
||||
)
|
||||
this.redrawCanvas()
|
||||
return
|
||||
@@ -432,7 +425,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
// Cursor feedback: resize handle > move (over a zone) > crosshair.
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
if (this.selectedZoneIndex !== null) {
|
||||
const handle = this.findHandleAt(mx, my, this.selectedZoneIndex)
|
||||
const handle = this.findHandleAt({ x: mx, y: my }, this.selectedZoneIndex)
|
||||
if (handle) {
|
||||
const cursorMap: Record<ResizeHandle, string> = {
|
||||
nw: 'nw-resize',
|
||||
@@ -449,7 +442,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
canvas.style.cursor =
|
||||
this.findZoneAt(mx, my) !== null ? 'move' : 'crosshair'
|
||||
this.findZoneAt({ x: mx, y: my }) !== null ? 'move' : 'crosshair'
|
||||
}
|
||||
|
||||
onCanvasMouseUp(event: MouseEvent) {
|
||||
@@ -466,27 +459,15 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
if (!this.isDrawing || !this.currentRect) return
|
||||
this.isDrawing = false
|
||||
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
const img = this.imageRef.nativeElement
|
||||
|
||||
const scaleX = img.naturalWidth / canvas.width
|
||||
const scaleY = img.naturalHeight / canvas.height
|
||||
|
||||
const x = Math.round(
|
||||
Math.min(this.currentRect.startX, this.currentRect.endX) * scaleX
|
||||
)
|
||||
const y = Math.round(
|
||||
Math.min(this.currentRect.startY, this.currentRect.endY) * scaleY
|
||||
)
|
||||
const w = Math.round(
|
||||
Math.abs(this.currentRect.endX - this.currentRect.startX) * scaleX
|
||||
)
|
||||
const h = Math.round(
|
||||
Math.abs(this.currentRect.endY - this.currentRect.startY) * scaleY
|
||||
const rect = sourceRectFromDrawing(
|
||||
this.currentRect,
|
||||
this.canvasSize(),
|
||||
this.imageNaturalSize()
|
||||
)
|
||||
|
||||
// Ignore tiny accidental clicks.
|
||||
if (w < 10 || h < 10) {
|
||||
if (rect.w < 10 || rect.h < 10) {
|
||||
this.currentRect = null
|
||||
this.redrawCanvas()
|
||||
return
|
||||
@@ -497,10 +478,10 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
target: 'custom_field',
|
||||
custom_field:
|
||||
this.customFields.length > 0 ? this.customFields[0].id : null,
|
||||
x,
|
||||
y,
|
||||
width: w,
|
||||
height: h,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.w,
|
||||
height: rect.h,
|
||||
page: this.previewPage + 1,
|
||||
ocr_language: 'deu+eng',
|
||||
transform: 'strip',
|
||||
@@ -533,110 +514,51 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
this.currentRect = null
|
||||
}
|
||||
|
||||
private getZoneDisplayRect(
|
||||
zoneIdx: number
|
||||
): { x: number; y: number; w: number; h: number } | null {
|
||||
private getZoneDisplayRect(zoneIdx: number): DisplayRect | null {
|
||||
const canvas = this.canvasRef?.nativeElement
|
||||
const img = this.imageRef?.nativeElement
|
||||
if (!canvas || !img || !img.naturalWidth) return null
|
||||
const zone = this.template.zones[zoneIdx]
|
||||
if (!zone) return null
|
||||
if (!this.isOnCurrentPage(zone)) return null
|
||||
const srcW = zone.zone_source_width || img.naturalWidth
|
||||
const srcH = zone.zone_source_height || img.naturalHeight
|
||||
const scaleX = canvas.width / srcW
|
||||
const scaleY = canvas.height / srcH
|
||||
return {
|
||||
x: zone.x * scaleX,
|
||||
y: zone.y * scaleY,
|
||||
w: zone.width * scaleX,
|
||||
h: zone.height * scaleY,
|
||||
}
|
||||
return getZoneDisplayRect(zone, this.canvasSize(), this.imageNaturalSize())
|
||||
}
|
||||
|
||||
private findHandleAt(
|
||||
mx: number,
|
||||
my: number,
|
||||
point: { x: number; y: number },
|
||||
zoneIdx: number
|
||||
): ResizeHandle | null {
|
||||
const r = this.getZoneDisplayRect(zoneIdx)
|
||||
if (!r) return null
|
||||
const hs = this.HANDLE_SIZE
|
||||
const handles: [ResizeHandle, number, number][] = [
|
||||
['nw', r.x, r.y],
|
||||
['n', r.x + r.w / 2, r.y],
|
||||
['ne', r.x + r.w, r.y],
|
||||
['w', r.x, r.y + r.h / 2],
|
||||
['e', r.x + r.w, r.y + r.h / 2],
|
||||
['sw', r.x, r.y + r.h],
|
||||
['s', r.x + r.w / 2, r.y + r.h],
|
||||
['se', r.x + r.w, r.y + r.h],
|
||||
]
|
||||
for (const [name, hx, hy] of handles) {
|
||||
if (Math.abs(mx - hx) <= hs && Math.abs(my - hy) <= hs) return name
|
||||
}
|
||||
return null
|
||||
return findHandleAt(point, r)
|
||||
}
|
||||
|
||||
private applyResize(mx: number, my: number) {
|
||||
if (this.resizeZoneIndex === null || !this.resizeHandle) return
|
||||
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
const img = this.imageRef.nativeElement
|
||||
const zone = this.template.zones[this.resizeZoneIndex]
|
||||
if (!zone) return
|
||||
const srcW = zone.zone_source_width || img.naturalWidth
|
||||
const srcH = zone.zone_source_height || img.naturalHeight
|
||||
const scaleX = srcW / canvas.width
|
||||
const scaleY = srcH / canvas.height
|
||||
const imgX = Math.max(0, Math.min(srcW, Math.round(mx * scaleX)))
|
||||
const imgY = Math.max(0, Math.min(srcH, Math.round(my * scaleY)))
|
||||
const handle = this.resizeHandle
|
||||
|
||||
if (handle.includes('w')) {
|
||||
const right = Math.min(zone.x + zone.width, srcW)
|
||||
zone.x = Math.max(0, Math.min(imgX, right - 10))
|
||||
zone.width = right - zone.x
|
||||
}
|
||||
if (handle.includes('e')) {
|
||||
zone.width = Math.max(10, imgX - zone.x)
|
||||
}
|
||||
if (handle.includes('n')) {
|
||||
const bottom = Math.min(zone.y + zone.height, srcH)
|
||||
zone.y = Math.max(0, Math.min(imgY, bottom - 10))
|
||||
zone.height = bottom - zone.y
|
||||
}
|
||||
if (handle.includes('s')) {
|
||||
zone.height = Math.max(10, imgY - zone.y)
|
||||
}
|
||||
resizeZone(
|
||||
zone,
|
||||
this.resizeHandle,
|
||||
{ x: mx, y: my },
|
||||
this.canvasSize(),
|
||||
this.imageNaturalSize()
|
||||
)
|
||||
}
|
||||
|
||||
private findZoneAt(displayX: number, displayY: number): number | null {
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
private findZoneAt(point: { x: number; y: number }): number | null {
|
||||
const img = this.imageRef.nativeElement
|
||||
if (!img.naturalWidth) return null
|
||||
|
||||
for (let i = this.template.zones.length - 1; i >= 0; i--) {
|
||||
const z = this.template.zones[i]
|
||||
if (!this.isOnCurrentPage(z)) continue
|
||||
const srcW = z.zone_source_width || img.naturalWidth
|
||||
const srcH = z.zone_source_height || img.naturalHeight
|
||||
const scaleX = canvas.width / srcW
|
||||
const scaleY = canvas.height / srcH
|
||||
const zx = z.x * scaleX
|
||||
const zy = z.y * scaleY
|
||||
const zw = z.width * scaleX
|
||||
const zh = z.height * scaleY
|
||||
if (
|
||||
displayX >= zx &&
|
||||
displayX <= zx + zw &&
|
||||
displayY >= zy &&
|
||||
displayY <= zy + zh
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
return findZoneAt(
|
||||
point,
|
||||
this.template.zones,
|
||||
this.previewPage,
|
||||
this.previewPageCount,
|
||||
this.canvasSize(),
|
||||
this.imageNaturalSize()
|
||||
)
|
||||
}
|
||||
|
||||
redrawCanvas() {
|
||||
@@ -703,7 +625,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
ctx.textBaseline = 'alphabetic'
|
||||
|
||||
if (idx === this.selectedZoneIndex) {
|
||||
const hs = this.HANDLE_SIZE
|
||||
ctx.fillStyle = color
|
||||
const handles = [
|
||||
[x, y],
|
||||
@@ -716,7 +637,12 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
[x + w, y + h],
|
||||
]
|
||||
for (const [hx, hy] of handles) {
|
||||
ctx.fillRect(hx - hs / 2, hy - hs / 2, hs, hs)
|
||||
ctx.fillRect(
|
||||
hx - HANDLE_SIZE / 2,
|
||||
hy - HANDLE_SIZE / 2,
|
||||
HANDLE_SIZE,
|
||||
HANDLE_SIZE
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -734,6 +660,16 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private canvasSize() {
|
||||
const canvas = this.canvasRef.nativeElement
|
||||
return { width: canvas.width, height: canvas.height }
|
||||
}
|
||||
|
||||
private imageNaturalSize() {
|
||||
const img = this.imageRef.nativeElement
|
||||
return { width: img.naturalWidth, height: img.naturalHeight }
|
||||
}
|
||||
|
||||
removeZone(index: number) {
|
||||
this.template.zones.splice(index, 1)
|
||||
if (this.selectedZoneIndex === index) {
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { OcrTemplateZone } from 'src/app/data/ocr-template'
|
||||
import {
|
||||
findHandleAt,
|
||||
findZoneAt,
|
||||
getZoneDisplayRect,
|
||||
getZonePage,
|
||||
isZoneOnPage,
|
||||
moveZone,
|
||||
resizeZone,
|
||||
sourceRectFromDrawing,
|
||||
} from './zone-geometry'
|
||||
|
||||
function zone(overrides: Partial<OcrTemplateZone> = {}): OcrTemplateZone {
|
||||
return {
|
||||
name: 'Zone',
|
||||
target: 'custom_field',
|
||||
custom_field: 1,
|
||||
x: 100,
|
||||
y: 200,
|
||||
width: 300,
|
||||
height: 400,
|
||||
page: 1,
|
||||
ocr_language: 'eng',
|
||||
transform: 'strip',
|
||||
validation_regex: '',
|
||||
order: 0,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('OCR template editor geometry', () => {
|
||||
it('normalizes zone pages', () => {
|
||||
expect(getZonePage(zone({ page: 2 }), 0, 5)).toBe(2)
|
||||
expect(getZonePage(zone({ page: -1 }), 0, 5)).toBe(5)
|
||||
expect(getZonePage(zone({ page: -1 }), 2, null)).toBe(3)
|
||||
expect(getZonePage(zone({ page: 0 }), 0, 5)).toBe(1)
|
||||
expect(getZonePage(zone({ page: undefined }), 0, 5)).toBe(1)
|
||||
})
|
||||
|
||||
it('checks whether a zone is on the current preview page', () => {
|
||||
expect(isZoneOnPage(zone({ page: 2 }), 1, 5)).toBe(true)
|
||||
expect(isZoneOnPage(zone({ page: 2 }), 0, 5)).toBe(false)
|
||||
expect(isZoneOnPage(zone({ page: -1 }), 4, 5)).toBe(true)
|
||||
})
|
||||
|
||||
it('scales source coordinates to canvas display coordinates', () => {
|
||||
expect(
|
||||
getZoneDisplayRect(
|
||||
zone({ x: 100, y: 200, width: 300, height: 400 }),
|
||||
{ width: 500, height: 1000 },
|
||||
{ width: 1000, height: 2000 }
|
||||
)
|
||||
).toEqual({ x: 50, y: 100, w: 150, h: 200 })
|
||||
})
|
||||
|
||||
it('uses per-zone source dimensions when present', () => {
|
||||
expect(
|
||||
getZoneDisplayRect(
|
||||
zone({
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
zone_source_width: 1000,
|
||||
zone_source_height: 1000,
|
||||
}),
|
||||
{ width: 500, height: 500 },
|
||||
{ width: 2000, height: 2000 }
|
||||
)
|
||||
).toEqual({ x: 50, y: 50, w: 50, h: 50 })
|
||||
})
|
||||
|
||||
it('finds zones from topmost to bottommost on the current page', () => {
|
||||
const zones = [
|
||||
zone({ name: 'first', x: 0, y: 0, width: 100, height: 100, page: 1 }),
|
||||
zone({ name: 'second', x: 0, y: 0, width: 50, height: 50, page: 1 }),
|
||||
zone({ name: 'third', x: 0, y: 0, width: 50, height: 50, page: 2 }),
|
||||
]
|
||||
|
||||
expect(
|
||||
findZoneAt(
|
||||
{ x: 25, y: 25 },
|
||||
zones,
|
||||
0,
|
||||
2,
|
||||
{ width: 100, height: 100 },
|
||||
{ width: 100, height: 100 }
|
||||
)
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('finds resize handles around a display rect', () => {
|
||||
const rect = { x: 10, y: 20, w: 100, h: 200 }
|
||||
|
||||
expect(findHandleAt({ x: 10, y: 20 }, rect)).toBe('nw')
|
||||
expect(findHandleAt({ x: 110, y: 220 }, rect)).toBe('se')
|
||||
expect(findHandleAt({ x: 60, y: 20 }, rect)).toBe('n')
|
||||
expect(findHandleAt({ x: 90, y: 160 }, rect)).toBeNull()
|
||||
})
|
||||
|
||||
it('moves zones without leaving source image bounds', () => {
|
||||
const z = zone({ x: 50, y: 50, width: 100, height: 100 })
|
||||
|
||||
moveZone(
|
||||
z,
|
||||
{ x: 500, y: 500 },
|
||||
{ mouseX: 50, mouseY: 50, zoneX: 50, zoneY: 50 },
|
||||
{ width: 500, height: 500 },
|
||||
{ width: 500, height: 500 }
|
||||
)
|
||||
|
||||
expect(z.x).toBe(400)
|
||||
expect(z.y).toBe(400)
|
||||
})
|
||||
|
||||
it('resizes zones without leaving source image bounds', () => {
|
||||
const z = zone({ x: 50, y: 50, width: 100, height: 100 })
|
||||
|
||||
resizeZone(
|
||||
z,
|
||||
'se',
|
||||
{ x: 500, y: 500 },
|
||||
{ width: 500, height: 500 },
|
||||
{ width: 200, height: 200 }
|
||||
)
|
||||
|
||||
expect(z.width).toBe(150)
|
||||
expect(z.height).toBe(150)
|
||||
})
|
||||
|
||||
it('converts drawn canvas rectangles to source rectangles', () => {
|
||||
expect(
|
||||
sourceRectFromDrawing(
|
||||
{ startX: 100, startY: 200, endX: 50, endY: 100 },
|
||||
{ width: 500, height: 1000 },
|
||||
{ width: 1000, height: 2000 }
|
||||
)
|
||||
).toEqual({ x: 100, y: 200, w: 100, h: 200 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,201 @@
|
||||
import { OcrTemplateZone } from 'src/app/data/ocr-template'
|
||||
|
||||
export interface DrawingRect {
|
||||
startX: number
|
||||
startY: number
|
||||
endX: number
|
||||
endY: number
|
||||
}
|
||||
|
||||
export interface Dimensions {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface DisplayRect {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export interface MoveStart {
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
zoneX: number
|
||||
zoneY: number
|
||||
}
|
||||
|
||||
export type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
|
||||
|
||||
export const HANDLE_SIZE = 8
|
||||
export const MIN_ZONE_SIZE = 10
|
||||
|
||||
export function getZonePage(
|
||||
zone: OcrTemplateZone,
|
||||
previewPage: number,
|
||||
previewPageCount: number | null
|
||||
): number {
|
||||
const page = zone.page ?? 1
|
||||
if (page === -1) return previewPageCount ?? previewPage + 1
|
||||
return page >= 1 ? page : 1
|
||||
}
|
||||
|
||||
export function isZoneOnPage(
|
||||
zone: OcrTemplateZone,
|
||||
previewPage: number,
|
||||
previewPageCount: number | null
|
||||
): boolean {
|
||||
return getZonePage(zone, previewPage, previewPageCount) === previewPage + 1
|
||||
}
|
||||
|
||||
export function getZoneSourceSize(
|
||||
zone: OcrTemplateZone,
|
||||
imageSize: Dimensions
|
||||
): Dimensions {
|
||||
return {
|
||||
width: zone.zone_source_width || imageSize.width,
|
||||
height: zone.zone_source_height || imageSize.height,
|
||||
}
|
||||
}
|
||||
|
||||
export function getZoneDisplayRect(
|
||||
zone: OcrTemplateZone,
|
||||
canvasSize: Dimensions,
|
||||
imageSize: Dimensions
|
||||
): DisplayRect {
|
||||
const sourceSize = getZoneSourceSize(zone, imageSize)
|
||||
const scaleX = canvasSize.width / sourceSize.width
|
||||
const scaleY = canvasSize.height / sourceSize.height
|
||||
|
||||
return {
|
||||
x: zone.x * scaleX,
|
||||
y: zone.y * scaleY,
|
||||
w: zone.width * scaleX,
|
||||
h: zone.height * scaleY,
|
||||
}
|
||||
}
|
||||
|
||||
export function findHandleAt(
|
||||
point: Point,
|
||||
rect: DisplayRect,
|
||||
handleSize = HANDLE_SIZE
|
||||
): ResizeHandle | null {
|
||||
const handles: [ResizeHandle, number, number][] = [
|
||||
['nw', rect.x, rect.y],
|
||||
['n', rect.x + rect.w / 2, rect.y],
|
||||
['ne', rect.x + rect.w, rect.y],
|
||||
['w', rect.x, rect.y + rect.h / 2],
|
||||
['e', rect.x + rect.w, rect.y + rect.h / 2],
|
||||
['sw', rect.x, rect.y + rect.h],
|
||||
['s', rect.x + rect.w / 2, rect.y + rect.h],
|
||||
['se', rect.x + rect.w, rect.y + rect.h],
|
||||
]
|
||||
|
||||
return (
|
||||
handles.find(
|
||||
([, x, y]) =>
|
||||
Math.abs(point.x - x) <= handleSize &&
|
||||
Math.abs(point.y - y) <= handleSize
|
||||
)?.[0] ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export function findZoneAt(
|
||||
point: Point,
|
||||
zones: OcrTemplateZone[],
|
||||
previewPage: number,
|
||||
previewPageCount: number | null,
|
||||
canvasSize: Dimensions,
|
||||
imageSize: Dimensions
|
||||
): number | null {
|
||||
for (let i = zones.length - 1; i >= 0; i--) {
|
||||
const zone = zones[i]
|
||||
if (!isZoneOnPage(zone, previewPage, previewPageCount)) continue
|
||||
const rect = getZoneDisplayRect(zone, canvasSize, imageSize)
|
||||
|
||||
if (
|
||||
point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.w &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.h
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function moveZone(
|
||||
zone: OcrTemplateZone,
|
||||
point: Point,
|
||||
moveStart: MoveStart,
|
||||
canvasSize: Dimensions,
|
||||
imageSize: Dimensions
|
||||
) {
|
||||
const sourceSize = getZoneSourceSize(zone, imageSize)
|
||||
const scaleX = sourceSize.width / canvasSize.width
|
||||
const scaleY = sourceSize.height / canvasSize.height
|
||||
const dx = Math.round((point.x - moveStart.mouseX) * scaleX)
|
||||
const dy = Math.round((point.y - moveStart.mouseY) * scaleY)
|
||||
|
||||
zone.x = clamp(moveStart.zoneX + dx, 0, sourceSize.width - zone.width)
|
||||
zone.y = clamp(moveStart.zoneY + dy, 0, sourceSize.height - zone.height)
|
||||
}
|
||||
|
||||
export function resizeZone(
|
||||
zone: OcrTemplateZone,
|
||||
handle: ResizeHandle,
|
||||
point: Point,
|
||||
canvasSize: Dimensions,
|
||||
imageSize: Dimensions
|
||||
) {
|
||||
const sourceSize = getZoneSourceSize(zone, imageSize)
|
||||
const scaleX = sourceSize.width / canvasSize.width
|
||||
const scaleY = sourceSize.height / canvasSize.height
|
||||
const imageX = clamp(Math.round(point.x * scaleX), 0, sourceSize.width)
|
||||
const imageY = clamp(Math.round(point.y * scaleY), 0, sourceSize.height)
|
||||
|
||||
if (handle.includes('w')) {
|
||||
const right = Math.min(zone.x + zone.width, sourceSize.width)
|
||||
zone.x = clamp(imageX, 0, right - MIN_ZONE_SIZE)
|
||||
zone.width = right - zone.x
|
||||
}
|
||||
if (handle.includes('e')) {
|
||||
zone.width = Math.max(MIN_ZONE_SIZE, imageX - zone.x)
|
||||
}
|
||||
if (handle.includes('n')) {
|
||||
const bottom = Math.min(zone.y + zone.height, sourceSize.height)
|
||||
zone.y = clamp(imageY, 0, bottom - MIN_ZONE_SIZE)
|
||||
zone.height = bottom - zone.y
|
||||
}
|
||||
if (handle.includes('s')) {
|
||||
zone.height = Math.max(MIN_ZONE_SIZE, imageY - zone.y)
|
||||
}
|
||||
}
|
||||
|
||||
export function sourceRectFromDrawing(
|
||||
rect: DrawingRect,
|
||||
canvasSize: Dimensions,
|
||||
imageSize: Dimensions
|
||||
): DisplayRect {
|
||||
const scaleX = imageSize.width / canvasSize.width
|
||||
const scaleY = imageSize.height / canvasSize.height
|
||||
|
||||
return {
|
||||
x: Math.round(Math.min(rect.startX, rect.endX) * scaleX),
|
||||
y: Math.round(Math.min(rect.startY, rect.endY) * scaleY),
|
||||
w: Math.round(Math.abs(rect.endX - rect.startX) * scaleX),
|
||||
h: Math.round(Math.abs(rect.endY - rect.startY) * scaleY),
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(value, max))
|
||||
}
|
||||
Reference in New Issue
Block a user