Proper data types

[skip ci]
This commit is contained in:
shamoon
2026-06-28 12:33:18 -07:00
parent c3a3939387
commit 20a855444b
3 changed files with 130 additions and 67 deletions
@@ -258,14 +258,14 @@
</select>
</div>
@if (zone.transform === 'date') {
@if (zone.transform === dateTransform) {
<div class="mb-3">
<label class="form-label" i18n>Date format</label>
<select class="form-select" [ngModel]="dateFormatChoice(zone)" (ngModelChange)="setDateFormatChoice(zone, $event)">
@for (opt of dateFormatOptions; track opt.id) {
<option [ngValue]="opt.id">{{ opt.name }}</option>
}
<option [ngValue]="'custom'" i18n>Custom...</option>
<option [ngValue]="customDateFormatChoice" i18n>Custom...</option>
</select>
@if (usesCustomDateFormat(zone)) {
<div class="input-group mt-2">
@@ -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
+53 -13
View File
@@ -1,11 +1,51 @@
import { ObjectWithId } from './object-with-id'
export type OcrZoneTarget = 'custom_field' | 'title' | 'asn' | 'created'
export type OcrBuiltinTarget = Exclude<OcrZoneTarget, 'custom_field'>
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<string, OcrZoneTarget>
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<string, OcrZoneTransform>
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