- More than one zone writes to this field. Build the combined
- value below: click a zone to insert its token, and type any
- separators or literal text between tokens.
-
-
- @for (z of zonesForField(zone); track $index) {
-
- }
-
-
-
- Tokens are matched by zone name. An empty zone leaves its
- token blank and the stray separator is trimmed. Leave empty
- to just join the zones in order with a space.
-
-
+
+
+ @for (z of zonesForField(zone); track z) {
+
+ }
}
diff --git a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts
index 132faa3ee..04d7abeef 100644
--- a/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts
+++ b/src-ui/src/app/components/manage/ocr-templates/ocr-template-editor/ocr-template-editor.component.spec.ts
@@ -39,6 +39,7 @@ describe('OcrTemplateEditorComponent', () => {
}
const templates = {
testZone: jest.fn(),
+ create: jest.fn(),
getPageImageUrl: (id: number, page: number) => `/preview/${id}/${page}`,
}
const modal = { open: jest.fn() }
@@ -268,6 +269,28 @@ describe('OcrTemplateEditorComponent', () => {
])
})
+ it('builds a combine format for zones sharing a field and prunes stale ones', () => {
+ const first = { ...zone(), name: 'First' }
+ const second = { ...zone(), name: 'Second' }
+ const other = { ...zone(), name: 'Other', custom_field: 2 }
+ component.template.zones = [first, second, other]
+ component.template.combine_formats = { '2': '{Other}' }
+
+ expect(component.isFieldShared(first)).toBe(true)
+ expect(component.isFieldShared(other)).toBe(false)
+ expect(component.defaultCombineFormat(first)).toBe('{First} {Second}')
+
+ component.insertCombineToken(first, second)
+ component.insertCombineToken(first, first)
+ expect(component.getCombineFormat(second)).toBe('{Second} {First}')
+
+ templates.create.mockReturnValue(of(component.template))
+ component.save()
+ expect(component.template.combine_formats).toEqual({
+ '1': '{Second} {First}',
+ })
+ })
+
it('hides results when extraction settings or geometry change', () => {
const selected = zone()
component.template.zones = [selected]
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 213432cdc..bf43f4700 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
@@ -47,6 +47,7 @@ import {
DEFAULT_OCR_ZONE_LANGUAGE,
DEFAULT_OCR_ZONE_TARGET,
DEFAULT_OCR_ZONE_TRANSFORM,
+ getZoneFieldKey,
isOcrBuiltinTarget,
OCR_BUILTIN_TARGETS,
OCR_LANGUAGE_OPTIONS,
@@ -309,7 +310,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe((t) => {
this.template = t
- this.template.combine_formats ??= {}
this.changeDetector.markForCheck()
if (t.sample_document) {
this.previewDocId = t.sample_document
@@ -827,7 +827,6 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
this.clearZoneTest()
const zone = this.template.zones[index]
if (zone) {
- this.seedCombineDefault(zone)
this.goToPage(this.zonePage(zone) - 1)
}
}
@@ -965,18 +964,12 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
zone.target = OCR_ZONE_TARGET.CustomField
zone.custom_field = typeof value === 'number' ? value : null
}
- this.seedCombineDefault(zone)
- }
-
- fieldKeyFor(zone: OcrTemplateZone): string | null {
- const v = this.zoneFieldValue(zone)
- return v === null || v === undefined ? null : String(v)
}
zonesForField(zone: OcrTemplateZone): OcrTemplateZone[] {
- const key = this.fieldKeyFor(zone)
+ const key = getZoneFieldKey(zone)
if (!key) return []
- return this.template.zones.filter((z) => this.fieldKeyFor(z) === key)
+ return this.template.zones.filter((z) => getZoneFieldKey(z) === key)
}
isFieldShared(zone: OcrTemplateZone): boolean {
@@ -984,47 +977,37 @@ export class OcrTemplateEditorComponent implements OnInit, OnDestroy {
}
getCombineFormat(zone: OcrTemplateZone): string {
- const key = this.fieldKeyFor(zone)
- return (key && this.template.combine_formats?.[key]) || ''
+ return this.template.combine_formats?.[getZoneFieldKey(zone)] ?? ''
}
setCombineFormat(zone: OcrTemplateZone, value: string) {
- const key = this.fieldKeyFor(zone)
+ const key = getZoneFieldKey(zone)
if (!key) return
this.template.combine_formats ??= {}
this.template.combine_formats[key] = value
}
+ defaultCombineFormat(zone: OcrTemplateZone): string {
+ return this.zonesForField(zone)
+ .map((z) => `{${z.name}}`)
+ .join(' ')
+ }
+
insertCombineToken(zone: OcrTemplateZone, tokenZone: OcrTemplateZone) {
- const token = `{${tokenZone.name}}`
const current = this.getCombineFormat(zone)
const sep = current && !current.endsWith(' ') ? ' ' : ''
- this.setCombineFormat(zone, `${current}${sep}${token}`)
- }
-
- private seedCombineDefault(zone: OcrTemplateZone) {
- const key = this.fieldKeyFor(zone)
- if (!key) return
- const shared = this.zonesForField(zone)
- if (shared.length <= 1) return
- this.template.combine_formats ??= {}
- if (!this.template.combine_formats[key]) {
- this.template.combine_formats[key] = shared
- .map((z) => `{${z.name}}`)
- .join(' ')
- }
+ this.setCombineFormat(zone, `${current}${sep}{${tokenZone.name}}`)
}
+ /** Drop formats that are empty or whose field no longer has several zones. */
private pruneCombineFormats() {
const formats = this.template.combine_formats
if (!formats) return
- const counts = new Map()
- for (const z of this.template.zones) {
- const key = this.fieldKeyFor(z)
- if (key) counts.set(key, (counts.get(key) ?? 0) + 1)
- }
- for (const key of Object.keys(formats)) {
- if ((counts.get(key) ?? 0) <= 1) delete formats[key]
+ const keys = this.template.zones.map(getZoneFieldKey)
+ for (const [key, format] of Object.entries(formats)) {
+ if (!format || keys.filter((k) => k === key).length < 2) {
+ delete formats[key]
+ }
}
}
diff --git a/src-ui/src/app/data/ocr-template.ts b/src-ui/src/app/data/ocr-template.ts
index 6f2b9f1ab..5bc9cb8bb 100644
--- a/src-ui/src/app/data/ocr-template.ts
+++ b/src-ui/src/app/data/ocr-template.ts
@@ -43,6 +43,15 @@ export function isOcrBuiltinTarget(value: unknown): value is OcrBuiltinTarget {
)
}
+/** The field a zone writes to, keyed by OcrTemplate.combine_formats. */
+export function getZoneFieldKey(zone: OcrTemplateZone): string | null {
+ const target = zone.target || DEFAULT_OCR_ZONE_TARGET
+ if (target !== OCR_ZONE_TARGET.CustomField) return target
+ return zone.custom_field === null || zone.custom_field === undefined
+ ? null
+ : String(zone.custom_field)
+}
+
/** Custom field types zone OCR can write to. See models.py OCR_SUPPORTED_FIELD_TYPES. */
export const OCR_SUPPORTED_FIELD_TYPES: CustomFieldDataType[] = [
CustomFieldDataType.String,