From 20a855444bcc4e85d1937bb849984b88fd969abf Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:33:18 -0700 Subject: [PATCH] Proper data types [skip ci] --- .../ocr-template-editor.component.html | 4 +- .../ocr-template-editor.component.ts | 127 +++++++++++------- src-ui/src/app/data/ocr-template.ts | 66 +++++++-- 3 files changed, 130 insertions(+), 67 deletions(-) diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html index b7792c073..c3bd61a82 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.html @@ -258,14 +258,14 @@ - @if (zone.transform === 'date') { + @if (zone.transform === dateTransform) {
@if (usesCustomDateFormat(zone)) {
diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts index f862c0186..ed8fb3b1a 100644 --- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts +++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.ts @@ -33,13 +33,20 @@ import { SelectComponent } from 'src/app/components/common/input/select/select.c import { SwitchComponent } from 'src/app/components/common/input/switch/switch.component' import { TextComponent } from 'src/app/components/common/input/text/text.component' import { PageHeaderComponent } from 'src/app/components/common/page-header/page-header.component' -import { CustomField } from 'src/app/data/custom-field' +import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { Document } from 'src/app/data/document' import { DocumentType } from 'src/app/data/document-type' import { DATE_FORMAT_OPTIONS, + DEFAULT_OCR_ZONE_LANGUAGE, + DEFAULT_OCR_ZONE_TARGET, + DEFAULT_OCR_ZONE_TRANSFORM, + isOcrBuiltinTarget, OCR_BUILTIN_TARGETS, OCR_LANGUAGE_OPTIONS, + OCR_ZONE_TARGET, + OCR_ZONE_TRANSFORM, + OcrBuiltinTarget, OcrTemplate, OcrTemplateZone, OcrZoneTestResult, @@ -70,6 +77,10 @@ import { } from './zone-geometry' type ActiveTab = 'settings' | 'zones' | 'zone' +type ZoneFieldSelection = OcrBuiltinTarget | number | null + +const CUSTOM_DATE_FORMAT_CHOICE = 'custom' +const MIN_DRAWN_ZONE_SIZE = 10 @Component({ selector: 'pngx-ocr-template-editor', @@ -125,6 +136,8 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { builtinTargets = OCR_BUILTIN_TARGETS dateFormatOptions = DATE_FORMAT_OPTIONS ocrLanguageOptions = OCR_LANGUAGE_OPTIONS + dateTransform = OCR_ZONE_TRANSFORM.Date + customDateFormatChoice = CUSTOM_DATE_FORMAT_CHOICE isNew = true saving = false @@ -165,17 +178,17 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { showQuickCreate = false quickCreateName = '' - quickCreateType = 'string' + quickCreateType = CustomFieldDataType.String quickCreateForZoneIndex: number | null = null quickCreateTypes = [ - { id: 'string', name: $localize`String` }, - { id: 'integer', name: $localize`Integer` }, - { id: 'float', name: $localize`Float` }, - { id: 'date', name: $localize`Date` }, - { id: 'monetary', name: $localize`Monetary` }, - { id: 'boolean', name: $localize`Boolean` }, - { id: 'url', name: $localize`URL` }, - { id: 'longtext', name: $localize`Long Text` }, + { id: CustomFieldDataType.String, name: $localize`String` }, + { id: CustomFieldDataType.Integer, name: $localize`Integer` }, + { id: CustomFieldDataType.Float, name: $localize`Float` }, + { id: CustomFieldDataType.Date, name: $localize`Date` }, + { id: CustomFieldDataType.Monetary, name: $localize`Monetary` }, + { id: CustomFieldDataType.Boolean, name: $localize`Boolean` }, + { id: CustomFieldDataType.Url, name: $localize`URL` }, + { id: CustomFieldDataType.LongText, name: $localize`Long Text` }, ] get selectedZone(): OcrTemplateZone | null { @@ -461,7 +474,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { if (!this.isDrawing || !this.currentRect) return this.isDrawing = false - const img = this.imageRef.nativeElement const rect = sourceRectFromDrawing( this.currentRect, this.canvasSize(), @@ -469,34 +481,40 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { ) // Ignore tiny accidental clicks. - if (rect.w < 10 || rect.h < 10) { + if (rect.w < MIN_DRAWN_ZONE_SIZE || rect.h < MIN_DRAWN_ZONE_SIZE) { this.currentRect = null this.redrawCanvas() return } - const zone: OcrTemplateZone = { + this.template.zones.push(this.createZoneFromRect(rect)) + this.currentRect = null + this.selectZone(this.template.zones.length - 1) + } + + private createZoneFromRect(rect: DisplayRect): OcrTemplateZone { + const imageSize = this.imageNaturalSize() + return { name: `Zone ${this.template.zones.length + 1}`, - target: 'custom_field', - custom_field: - this.customFields.length > 0 ? this.customFields[0].id : null, + target: DEFAULT_OCR_ZONE_TARGET, + custom_field: this.defaultCustomFieldId(), x: rect.x, y: rect.y, width: rect.w, height: rect.h, - page: this.previewPage + 1, - ocr_language: 'deu+eng', - transform: 'strip', + page: this.previewPageDisplay, + ocr_language: DEFAULT_OCR_ZONE_LANGUAGE, + transform: DEFAULT_OCR_ZONE_TRANSFORM, date_format: '', validation_regex: '', order: this.template.zones.length, - zone_source_width: img.naturalWidth, - zone_source_height: img.naturalHeight, + zone_source_width: imageSize.width, + zone_source_height: imageSize.height, } + } - this.template.zones.push(zone) - this.currentRect = null - this.selectZone(this.template.zones.length - 1) + private defaultCustomFieldId(): number | null { + return this.customFields[0]?.id ?? null } @HostListener('document:mouseup') @@ -699,22 +717,8 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { if (!zone || !this.previewDocId) return this.zoneTesting = true this.zoneTestResult = null - const payload: ZoneTestRequest = { - name: zone.name, - x: zone.x, - y: zone.y, - width: zone.width, - height: zone.height, - page: zone.page ?? 1, - ocr_language: zone.ocr_language, - transform: zone.transform, - date_format: zone.date_format, - validation_regex: zone.validation_regex, - zone_source_width: zone.zone_source_width, - zone_source_height: zone.zone_source_height, - } this.templateService - .testZone(this.previewDocId, payload) + .testZone(this.previewDocId, this.zoneTestRequest(zone)) .pipe(takeUntil(this.destroy$)) .subscribe({ next: (res) => { @@ -730,6 +734,23 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { }) } + private zoneTestRequest(zone: OcrTemplateZone): ZoneTestRequest { + return { + name: zone.name, + x: zone.x, + y: zone.y, + width: zone.width, + height: zone.height, + page: zone.page ?? 1, + ocr_language: zone.ocr_language, + transform: zone.transform, + date_format: zone.date_format, + validation_regex: zone.validation_regex, + zone_source_width: zone.zone_source_width, + zone_source_height: zone.zone_source_height, + } + } + deleteSelectedZone() { if (this.selectedZoneIndex === null) return this.removeZone(this.selectedZoneIndex) @@ -789,17 +810,17 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { } /** Value bound to the field select: a built-in id string or a custom-field id. */ - zoneFieldValue(zone: OcrTemplateZone): number | string | null { - const target = zone.target || 'custom_field' - return target === 'custom_field' ? zone.custom_field : target + zoneFieldValue(zone: OcrTemplateZone): ZoneFieldSelection { + const target = zone.target || DEFAULT_OCR_ZONE_TARGET + return target === OCR_ZONE_TARGET.CustomField ? zone.custom_field : target } - setZoneField(zone: OcrTemplateZone, value: number | string) { - if (value === 'title' || value === 'asn' || value === 'created') { + setZoneField(zone: OcrTemplateZone, value: ZoneFieldSelection) { + if (isOcrBuiltinTarget(value)) { zone.target = value zone.custom_field = null } else { - zone.target = 'custom_field' + zone.target = OCR_ZONE_TARGET.CustomField zone.custom_field = typeof value === 'number' ? value : null } this.seedCombineDefault(zone) @@ -807,7 +828,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { fieldKeyFor(zone: OcrTemplateZone): string | null { const v = this.zoneFieldValue(zone) - return v === null || v === undefined || v === '' ? null : String(v) + return v === null || v === undefined ? null : String(v) } zonesForField(zone: OcrTemplateZone): OcrTemplateZone[] { @@ -867,11 +888,13 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { /** Value bound to the date-format select: a preset, '' (auto), or 'custom'. */ dateFormatChoice(zone: OcrTemplateZone): string { - return this.usesCustomDateFormat(zone) ? 'custom' : zone.date_format || '' + return this.usesCustomDateFormat(zone) + ? CUSTOM_DATE_FORMAT_CHOICE + : zone.date_format || '' } setDateFormatChoice(zone: OcrTemplateZone, value: string) { - if (value === 'custom') { + if (value === CUSTOM_DATE_FORMAT_CHOICE) { this.customDateFormatZones.add(zone) zone.date_format ||= '' } else { @@ -891,8 +914,8 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { } getZoneTargetName(zone: OcrTemplateZone): string { - const target = zone.target || 'custom_field' - if (target === 'custom_field') { + const target = zone.target || DEFAULT_OCR_ZONE_TARGET + if (target === OCR_ZONE_TARGET.CustomField) { return zone.custom_field ? this.getCustomFieldName(zone.custom_field) : $localize`(no field)` @@ -909,7 +932,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { if (zoneIndex === null) return this.quickCreateForZoneIndex = zoneIndex this.quickCreateName = this.template.zones[zoneIndex]?.name || '' - this.quickCreateType = 'string' + this.quickCreateType = CustomFieldDataType.String this.showQuickCreate = true } @@ -936,7 +959,7 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy { this.template.zones[this.quickCreateForZoneIndex].custom_field = result.id this.template.zones[this.quickCreateForZoneIndex].target = - 'custom_field' + OCR_ZONE_TARGET.CustomField } this.showQuickCreate = false this.quickCreateForZoneIndex = null diff --git a/src-ui/src/app/data/ocr-template.ts b/src-ui/src/app/data/ocr-template.ts index 94f925d6b..1e0fdef70 100644 --- a/src-ui/src/app/data/ocr-template.ts +++ b/src-ui/src/app/data/ocr-template.ts @@ -1,11 +1,51 @@ import { ObjectWithId } from './object-with-id' export type OcrZoneTarget = 'custom_field' | 'title' | 'asn' | 'created' +export type OcrBuiltinTarget = Exclude +export type OcrZoneTransform = + | 'none' + | 'strip' + | 'uppercase' + | 'lowercase' + | 'numeric' + | 'strip_punctuation' + | 'date' + | 'qr_code' + +export const OCR_ZONE_TARGET = { + CustomField: 'custom_field', + Title: 'title', + Asn: 'asn', + Created: 'created', +} as const satisfies Record + +export const OCR_ZONE_TRANSFORM = { + None: 'none', + Strip: 'strip', + Uppercase: 'uppercase', + Lowercase: 'lowercase', + Numeric: 'numeric', + StripPunctuation: 'strip_punctuation', + Date: 'date', + QrCode: 'qr_code', +} as const satisfies Record + +export const DEFAULT_OCR_ZONE_TARGET = OCR_ZONE_TARGET.CustomField +export const DEFAULT_OCR_ZONE_TRANSFORM = OCR_ZONE_TRANSFORM.Strip +export const DEFAULT_OCR_ZONE_LANGUAGE = 'deu+eng' + +export function isOcrBuiltinTarget(value: unknown): value is OcrBuiltinTarget { + return ( + value === OCR_ZONE_TARGET.Title || + value === OCR_ZONE_TARGET.Asn || + value === OCR_ZONE_TARGET.Created + ) +} export const OCR_BUILTIN_TARGETS = [ - { id: 'title', name: $localize`Title` }, - { id: 'asn', name: $localize`Archive serial number` }, - { id: 'created', name: $localize`Date created` }, + { id: OCR_ZONE_TARGET.Title, name: $localize`Title` }, + { id: OCR_ZONE_TARGET.Asn, name: $localize`Archive serial number` }, + { id: OCR_ZONE_TARGET.Created, name: $localize`Date created` }, ] export interface OcrTemplateZone { @@ -19,7 +59,7 @@ export interface OcrTemplateZone { width: number height: number ocr_language: string - transform: string + transform: OcrZoneTransform date_format?: string validation_regex: string order: number @@ -28,17 +68,17 @@ export interface OcrTemplateZone { } export const TRANSFORM_OPTIONS = [ - { id: 'none', name: $localize`None` }, - { id: 'strip', name: $localize`Strip whitespace` }, - { id: 'uppercase', name: $localize`Uppercase` }, - { id: 'lowercase', name: $localize`Lowercase` }, - { id: 'numeric', name: $localize`Numeric only` }, + { id: OCR_ZONE_TRANSFORM.None, name: $localize`None` }, + { id: OCR_ZONE_TRANSFORM.Strip, name: $localize`Strip whitespace` }, + { id: OCR_ZONE_TRANSFORM.Uppercase, name: $localize`Uppercase` }, + { id: OCR_ZONE_TRANSFORM.Lowercase, name: $localize`Lowercase` }, + { id: OCR_ZONE_TRANSFORM.Numeric, name: $localize`Numeric only` }, { - id: 'strip_punctuation', + id: OCR_ZONE_TRANSFORM.StripPunctuation, name: $localize`Remove leading/trailing punctuation`, }, - { id: 'date', name: $localize`Parse date` }, - { id: 'qr_code', name: $localize`Read QR/barcode` }, + { id: OCR_ZONE_TRANSFORM.Date, name: $localize`Parse date` }, + { id: OCR_ZONE_TRANSFORM.QrCode, name: $localize`Read QR/barcode` }, ] export const OCR_LANGUAGE_OPTIONS = [ @@ -79,7 +119,7 @@ export interface ZoneTestRequest { height: number page: number ocr_language: string - transform: string + transform: OcrZoneTransform date_format?: string validation_regex: string zone_source_width?: number