Skip to content

Commit a0988e4

Browse files
committed
Report the fields actually written and close an unload race
1 parent 0215fa8 commit a0988e4

8 files changed

Lines changed: 60 additions & 66 deletions

File tree

app/common/src/main/java/stirling/software/common/util/FormUtils.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,11 +1014,12 @@ private PDAcroForm getAcroFormSafely(PDDocument document) {
10141014
* Create AcroForm fields from definitions, uniquifying names against existing fields. Creates
10151015
* the AcroForm with a Helvetica default resource when the document has none.
10161016
*/
1017-
public void addFields(PDDocument document, List<NewFormFieldDefinition> definitions)
1018-
throws IOException {
1017+
public List<CreatedField> addFields(
1018+
PDDocument document, List<NewFormFieldDefinition> definitions) throws IOException {
10191019
if (document == null || definitions == null || definitions.isEmpty()) {
1020-
return;
1020+
return List.of();
10211021
}
1022+
List<CreatedField> created = new ArrayList<>();
10221023
PDDocumentCatalog documentCatalog = document.getDocumentCatalog();
10231024
PDAcroForm acroForm = documentCatalog.getAcroForm();
10241025
boolean priorNeedAppearances =
@@ -1087,10 +1088,11 @@ public void addFields(PDDocument document, List<NewFormFieldDefinition> definiti
10871088
uniqueName,
10881089
definition,
10891090
definition.options());
1090-
PDField created = acroForm.getField(uniqueName);
1091-
if (created != null) {
1092-
createdFields.add(created);
1091+
PDField field = acroForm.getField(uniqueName);
1092+
if (field != null) {
1093+
createdFields.add(field);
10931094
createdButtons.add(Map.entry(uniqueName, definition));
1095+
created.add(new CreatedField(handler.typeName(), pageIndex));
10941096
}
10951097
} catch (Exception e) {
10961098
log.warn("Failed to create detected field '{}': {}", uniqueName, e.getMessage());
@@ -1100,8 +1102,12 @@ public void addFields(PDDocument document, List<NewFormFieldDefinition> definiti
11001102
applyButtonAppearances(document, acroForm, createdButtons);
11011103
// Refresh only what we added; regenerating pre-existing fields could alter their look.
11021104
ensureAppearances(acroForm, createdFields, priorNeedAppearances);
1105+
return List.copyOf(created);
11031106
}
11041107

1108+
/** A field that was actually written, with the type it ended up as after any coercion. */
1109+
public record CreatedField(String type, int pageIndex) {}
1110+
11051111
public String filterSingleChoiceSelection(
11061112
String selection, List<String> allowedOptions, String fieldName) {
11071113
if (selection == null || selection.trim().isEmpty()) return null;

app/proprietary/src/main/java/stirling/software/proprietary/formdetection/controller/FormDetectionController.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,13 @@ public ResponseEntity<?> detect(
109109
for (DetectedField f : detections) {
110110
defs.add(toDefinition(f));
111111
}
112-
FormUtils.addFields(document, defs);
112+
List<FormUtils.CreatedField> written = FormUtils.addFields(document, defs);
113113
ResponseEntity<Resource> pdf =
114114
WebResponseUtils.pdfDocToWebResponse(
115115
document, baseName(file) + ".pdf", tempFileManager);
116116
return ResponseEntity.status(pdf.getStatusCode())
117117
.headers(pdf.getHeaders())
118-
.header(SUMMARY_HEADER, summaryHeader(detections))
118+
.header(SUMMARY_HEADER, summaryHeader(written))
119119
.body(pdf.getBody());
120120
} catch (IOException e) {
121121
log.debug("Auto Form Detection could not apply fields: {}", e.getMessage());
@@ -155,17 +155,20 @@ private static NewFormFieldDefinition toDefinition(DetectedField f) {
155155
null);
156156
}
157157

158-
/** Compact JSON of what was added; a header keeps it to one request and one inference. */
159-
private String summaryHeader(List<DetectedField> detections) {
158+
/**
159+
* Compact JSON of the fields actually written, so the panel cannot over-report ones that
160+
* addFields skipped, and reports the type each ended up as rather than the detected one.
161+
*/
162+
private String summaryHeader(List<FormUtils.CreatedField> written) {
160163
Map<String, Integer> byType = new LinkedHashMap<>();
161164
TreeSet<Integer> pages = new TreeSet<>();
162-
for (DetectedField f : detections) {
165+
for (FormUtils.CreatedField f : written) {
163166
byType.merge(f.type(), 1, Integer::sum);
164-
pages.add(f.page());
167+
pages.add(f.pageIndex());
165168
}
166169
return objectMapper.writeValueAsString(
167170
Map.of(
168-
"total", detections.size(),
171+
"total", written.size(),
169172
"byType", byType,
170173
"pagesWithFields", pages.size()));
171174
}

app/proprietary/src/main/java/stirling/software/proprietary/formdetection/inference/OnnxFormDetector.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,18 @@ public Map<String, Yolo.RawOutput> infer(float[] chw, int inputSize) {
5454
concurrency.acquireUninterruptibly();
5555
lock.readLock().lock();
5656
try {
57+
// Re-read under the lock: an uninstall between ensureLoaded and here closes the
58+
// session, and dereferencing it then would be a 500 rather than the usual 503.
59+
OrtSession current = session;
60+
String input = inputName;
61+
if (current == null || input == null) {
62+
throw new IllegalStateException("Model was unloaded while the request was running");
63+
}
5764
OrtEnvironment env = OrtEnvironment.getEnvironment();
5865
long[] shape = {1, 3, inputSize, inputSize};
5966
try (OnnxTensor tensor = OnnxTensor.createTensor(env, FloatBuffer.wrap(chw), shape);
6067
OrtSession.Result results =
61-
session.run(Collections.singletonMap(inputName, tensor))) {
68+
current.run(Collections.singletonMap(input, tensor))) {
6269
Map<String, Yolo.RawOutput> outputs = new LinkedHashMap<>();
6370
for (Map.Entry<String, OnnxValue> entry : results) {
6471
outputs.put(entry.getKey(), toRawOutput(entry.getKey(), entry.getValue()));

frontend/editor/public/locales/en-US/translation.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,9 +2115,7 @@ text = "Text fields"
21152115
failed = "An error occurred while detecting form fields."
21162116

21172117
[autoFormDetection.progress]
2118-
applying = "Building fillable fields..."
2119-
starting = "Preparing detection..."
2120-
uploading = "Analyzing your document..."
2118+
detecting = "Analyzing your document..."
21212119

21222120
[autoFormDetection.results]
21232121
title = "Review fillable PDF"
Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useEffect, useState } from "react";
2-
import { Stack, Text } from "@mantine/core";
2+
import { Group, Loader, Stack, Text } from "@mantine/core";
33
import { useTranslation } from "react-i18next";
4-
import { ProgressBar } from "@app/ui/ProgressBar";
54
import { DetectionStage, onStage } from "@app/services/formDetection/progress";
65

76
export default function DetectionProgressPanel({
@@ -19,42 +18,19 @@ export default function DetectionProgressPanel({
1918

2019
if (!active || !stage || stage.kind === "done") return null;
2120

22-
let value = 0.05;
23-
let label = t(
24-
"autoFormDetection.progress.starting",
25-
"Preparing detection...",
26-
);
27-
28-
switch (stage.kind) {
29-
case "uploading":
30-
value = 0.35;
31-
label = t(
32-
"autoFormDetection.progress.uploading",
33-
"Analyzing your document...",
34-
);
35-
break;
36-
case "applying":
37-
value = 0.9;
38-
label = t(
39-
"autoFormDetection.progress.applying",
40-
"Building fillable fields...",
41-
);
42-
break;
43-
case "starting":
44-
value = 0.05;
45-
label = t(
46-
"autoFormDetection.progress.starting",
47-
"Preparing detection...",
48-
);
49-
break;
50-
}
51-
21+
// Detection is one request of unknown length, so a spinner is honest where a percentage
22+
// would have to be invented.
5223
return (
5324
<Stack gap={6} mx="md" mt="sm">
54-
<ProgressBar value={value} label={label} />
55-
<Text size="xs" c="dimmed">
56-
{label}
57-
</Text>
25+
<Group gap={8} wrap="nowrap">
26+
<Loader size="xs" />
27+
<Text size="xs" c="dimmed">
28+
{t(
29+
"autoFormDetection.progress.detecting",
30+
"Analyzing your document...",
31+
)}
32+
</Text>
33+
</Group>
5834
</Stack>
5935
);
6036
}

frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ vi.mock("react-i18next", () => ({
1313
}),
1414
}));
1515

16+
import { expectConsole } from "@app/tests/failOnConsole";
1617
import apiClient from "@app/services/apiClient";
1718
import { onSummary } from "@app/services/formDetection/progress";
1819
import { autoFormDetectionOperationConfig } from "@app/hooks/tools/autoFormDetection/useAutoFormDetectionOperation";
@@ -24,7 +25,12 @@ function pdfFile(): File {
2425
return new File(["%PDF-1.4 dummy"], "doc.pdf", { type: "application/pdf" });
2526
}
2627

27-
function respond(headers: Record<string, string> = {}) {
28+
const SUMMARY =
29+
'{"total":11,"byType":{"text":8,"checkbox":3},"pagesWithFields":1}';
30+
31+
function respond(
32+
headers: Record<string, string> = { "x-stirling-detected-fields": SUMMARY },
33+
) {
2834
(apiClient.post as Mock).mockResolvedValue({
2935
data: new Blob(["%PDF-1.4 applied"]),
3036
headers,
@@ -59,10 +65,7 @@ describe("processAutoFormDetection", () => {
5965
});
6066

6167
it("publishes the summary the server reported", async () => {
62-
respond({
63-
"x-stirling-detected-fields":
64-
'{"total":11,"byType":{"text":8,"checkbox":3},"pagesWithFields":1}',
65-
});
68+
respond({ "x-stirling-detected-fields": SUMMARY });
6669
const seen: unknown[] = [];
6770
const stop = onSummary((s) => seen.push(s));
6871

@@ -75,6 +78,7 @@ describe("processAutoFormDetection", () => {
7578
});
7679

7780
it("still returns the PDF when the summary header is missing or malformed", async () => {
81+
expectConsole.warn(/no X-Stirling-Detected-Fields header/);
7882
respond({ "x-stirling-detected-fields": "not json" });
7983
const seen: unknown[] = [];
8084
const stop = onSummary((s) => seen.push(s));

frontend/editor/src/core/hooks/tools/autoFormDetection/useAutoFormDetectionOperation.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,22 @@ async function processAutoFormDetection(
4747
const file = files[0];
4848

4949
try {
50-
emitStage({ kind: "starting" });
51-
emitStage({ kind: "uploading" });
50+
emitStage({ kind: "detecting" });
5251
const res = await apiClient.post(
5352
DETECT_ENDPOINT,
5453
buildAutoFormDetectionFormData(parameters, file),
5554
{ responseType: "blob" },
5655
);
5756

58-
emitStage({ kind: "applying" });
5957
const summary = parseSummary(res.headers?.["x-stirling-detected-fields"]);
6058
if (summary) {
6159
emitSummary(summary);
60+
} else {
61+
// Not fatal - the PDF is still correct - but the results panel needs the header, so a
62+
// proxy that drops it turns into a silently missing summary without this.
63+
console.warn(
64+
"[AutoFormDetection] no X-Stirling-Detected-Fields header; skipping the results summary",
65+
);
6266
}
6367
return { files: [asPdf(res.data as Blob, file)] };
6468
} finally {

frontend/editor/src/core/services/formDetection/progress.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
export type DetectionStage =
2-
| { kind: "starting" }
3-
| { kind: "uploading" }
4-
| { kind: "applying" }
5-
| { kind: "done" };
1+
export type DetectionStage = { kind: "detecting" } | { kind: "done" };
62

73
export interface DetectionSummary {
84
total: number;

0 commit comments

Comments
 (0)