mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Merge branch 'main' into custom_task_20260812
This commit is contained in:
@@ -148,7 +148,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: "25"
|
||||
distribution: "temurin"
|
||||
|
||||
@@ -527,27 +527,45 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-image",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-io",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-io",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-lang",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.common:common-lang",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-batik",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-bmp",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
@@ -557,9 +575,15 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-core",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-jpeg",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
@@ -569,9 +593,15 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-metadata",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-psd",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
@@ -581,12 +611,24 @@
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-webp",
|
||||
"moduleVersion": "3.13.1",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.twelvemonkeys.imageio:imageio-webp",
|
||||
"moduleVersion": "3.14.0",
|
||||
"moduleLicense": "The BSD License",
|
||||
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
|
||||
},
|
||||
{
|
||||
"moduleName": "com.vladsch.flexmark:flexmark",
|
||||
"moduleVersion": "0.64.8",
|
||||
@@ -2262,7 +2304,7 @@
|
||||
},
|
||||
{
|
||||
"moduleName": "org.simplejavamail:core-module",
|
||||
"moduleVersion": "9.2.0",
|
||||
"moduleVersion": "9.3.1",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
@@ -2275,13 +2317,13 @@
|
||||
},
|
||||
{
|
||||
"moduleName": "org.simplejavamail:outlook-module",
|
||||
"moduleVersion": "9.2.0",
|
||||
"moduleVersion": "9.3.1",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
{
|
||||
"moduleName": "org.simplejavamail:simple-java-mail",
|
||||
"moduleVersion": "9.2.0",
|
||||
"moduleVersion": "9.3.1",
|
||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-cache'
|
||||
api 'com.github.ben-manes.caffeine:caffeine'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46'
|
||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.53'
|
||||
implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}"
|
||||
// Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide
|
||||
// token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window
|
||||
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
package stirling.software.proprietary.service.ua;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
import stirling.software.proprietary.pdf.ua.DocumentStructure;
|
||||
import stirling.software.proprietary.pdf.ua.LayoutAnalyzer;
|
||||
import stirling.software.proprietary.pdf.ua.PageContent;
|
||||
import stirling.software.proprietary.pdf.ua.PdfUaProfile;
|
||||
import stirling.software.proprietary.pdf.ua.PdfUaTagger;
|
||||
import stirling.software.proprietary.pdf.ua.TaggedContentExtractor;
|
||||
import stirling.software.proprietary.pdf.ua.TaggingOptions;
|
||||
|
||||
/**
|
||||
* Measures where conversion time and memory go; meant to be read, not to gate CI. Assertions catch
|
||||
* only order-of-magnitude regressions - wall-clock numbers are no contract.
|
||||
*/
|
||||
class PdfUaBenchmarkTest {
|
||||
|
||||
private static PdfUaConversionService service;
|
||||
private static PdfUaValidationService validation;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
validation = new PdfUaValidationService();
|
||||
validation.initialise();
|
||||
service =
|
||||
new PdfUaConversionService(
|
||||
validation,
|
||||
new FontEmbeddingService(),
|
||||
new CustomPDFDocumentFactory(
|
||||
org.mockito.Mockito.mock(PdfMetadataService.class)));
|
||||
}
|
||||
|
||||
/** A realistic page: heading, prose, a small table, a bullet list. */
|
||||
private static byte[] document(int pages) throws IOException {
|
||||
try (PDDocument document = new PDDocument()) {
|
||||
PDFont font = null;
|
||||
for (int p = 0; p < pages; p++) {
|
||||
PDPage page = new PDPage(PDRectangle.A4);
|
||||
document.addPage(page);
|
||||
if (font == null) {
|
||||
font = PdfUaTestDocuments.font(document);
|
||||
}
|
||||
try (PDPageContentStream cs = new PDPageContentStream(document, page)) {
|
||||
float y = 790;
|
||||
write(cs, font, 9, 60, 810, "Benchmark Corpus Running Head");
|
||||
write(cs, font, 18, 60, y, "Section " + (p + 1));
|
||||
y -= 30;
|
||||
for (int line = 0; line < 22; line++) {
|
||||
write(
|
||||
cs,
|
||||
font,
|
||||
11,
|
||||
60,
|
||||
y,
|
||||
"Body line " + line + " of section " + (p + 1) + " with prose.");
|
||||
y -= 15;
|
||||
}
|
||||
for (int row = 0; row < 4; row++) {
|
||||
cs.beginText();
|
||||
cs.setFont(font, 11);
|
||||
cs.newLineAtOffset(60, y);
|
||||
cs.showText("Row " + row);
|
||||
cs.newLineAtOffset(160, 0);
|
||||
cs.showText(String.valueOf(row * 120));
|
||||
cs.newLineAtOffset(140, 0);
|
||||
cs.showText(String.valueOf(row * 480));
|
||||
cs.endText();
|
||||
y -= 16;
|
||||
}
|
||||
write(cs, font, 11, 60, y - 10, "• First bullet point");
|
||||
write(cs, font, 11, 60, y - 25, "• Second bullet point");
|
||||
write(cs, font, 9, 300, 30, "Page " + (p + 1));
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
document.save(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(
|
||||
PDPageContentStream cs, PDFont font, float size, float x, float y, String text)
|
||||
throws IOException {
|
||||
cs.beginText();
|
||||
cs.setFont(font, size);
|
||||
cs.newLineAtOffset(x, y);
|
||||
cs.showText(text);
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
private static TaggingOptions options() {
|
||||
return TaggingOptions.builder()
|
||||
.profile(PdfUaProfile.UA1)
|
||||
.language("en-GB")
|
||||
.title("Benchmark")
|
||||
.embedFonts(false)
|
||||
.existingTags(TaggingOptions.ExistingTags.REBUILD)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static long usedHeap() {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
System.gc();
|
||||
return runtime.totalMemory() - runtime.freeMemory();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("reports throughput and memory across document sizes")
|
||||
void throughputAcrossSizes() throws Exception {
|
||||
int[] sizes = {1, 10, 50, 150};
|
||||
StringBuilder report =
|
||||
new StringBuilder("\nPDF/UA conversion throughput\n")
|
||||
.append(
|
||||
String.format(
|
||||
" %-7s %-10s %-12s %-12s %-10s %s%n",
|
||||
"pages",
|
||||
"input",
|
||||
"convert ms",
|
||||
"ms/page",
|
||||
"pages/s",
|
||||
"heap MB"));
|
||||
|
||||
// Warm up so the first timed run is not measuring class loading and JIT.
|
||||
service.convert(document(5), options());
|
||||
|
||||
for (int pages : sizes) {
|
||||
byte[] input = document(pages);
|
||||
long heapBefore = usedHeap();
|
||||
long start = System.nanoTime();
|
||||
var outcome = service.convert(input, options());
|
||||
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
|
||||
long heapDelta = (usedHeap() - heapBefore) / (1024 * 1024);
|
||||
|
||||
assertTrue(outcome.pdfBytes().length > 0);
|
||||
report.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
" %-7d %-10s %-12d %-12.2f %-10.1f %d%n",
|
||||
pages,
|
||||
humanBytes(input.length),
|
||||
elapsedMs,
|
||||
elapsedMs / (double) pages,
|
||||
pages * 1000.0 / Math.max(elapsedMs, 1),
|
||||
Math.max(heapDelta, 0)));
|
||||
}
|
||||
System.out.println(report);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("breaks conversion down by phase so optimisation has a target")
|
||||
void phaseBreakdown() throws Exception {
|
||||
byte[] input = document(60);
|
||||
|
||||
// Warm up.
|
||||
try (PDDocument warm = Loader.loadPDF(input)) {
|
||||
new TaggedContentExtractor().extract(warm);
|
||||
}
|
||||
|
||||
long parseMs;
|
||||
long extractMs;
|
||||
long analyseMs;
|
||||
long tagMs;
|
||||
List<PageContent> pages;
|
||||
DocumentStructure structure;
|
||||
|
||||
long t0 = System.nanoTime();
|
||||
try (PDDocument document = Loader.loadPDF(input)) {
|
||||
parseMs = ms(t0);
|
||||
|
||||
long t1 = System.nanoTime();
|
||||
pages = new TaggedContentExtractor().extract(document);
|
||||
extractMs = ms(t1);
|
||||
|
||||
long t2 = System.nanoTime();
|
||||
structure = new LayoutAnalyzer().analyse(pages);
|
||||
analyseMs = ms(t2);
|
||||
}
|
||||
|
||||
long t3 = System.nanoTime();
|
||||
try (PDDocument document = Loader.loadPDF(input)) {
|
||||
new PdfUaTagger().tag(document, options());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
document.save(out);
|
||||
}
|
||||
tagMs = ms(t3);
|
||||
|
||||
long t4 = System.nanoTime();
|
||||
var outcome = service.convert(input, options());
|
||||
long totalMs = ms(t4);
|
||||
|
||||
long t5 = System.nanoTime();
|
||||
validation.validate(outcome.pdfBytes(), PdfUaProfile.UA1);
|
||||
long validateMs = ms(t5);
|
||||
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"%nPhase breakdown over %d pages (%d blocks)%n"
|
||||
+ " parse %5d ms%n"
|
||||
+ " extract %5d ms (text pass + token scan)%n"
|
||||
+ " analyse %5d ms%n"
|
||||
+ " tag end-to-end %5d ms (includes parse, extract, analyse, inject, write)%n"
|
||||
+ " validate %5d ms (veraPDF)%n"
|
||||
+ " full convert %5d ms (tag + declare + validate)%n",
|
||||
60,
|
||||
structure.getBlocks().size(),
|
||||
parseMs,
|
||||
extractMs,
|
||||
analyseMs,
|
||||
tagMs,
|
||||
validateMs,
|
||||
totalMs);
|
||||
|
||||
assertTrue(pages.size() == 60, "extractor lost pages");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("splits the tagging pass into its own sub-phases")
|
||||
void taggingSubPhases() throws Exception {
|
||||
byte[] input = document(60);
|
||||
try (PDDocument warm = Loader.loadPDF(input)) {
|
||||
new TaggedContentExtractor().extract(warm);
|
||||
}
|
||||
|
||||
long extractMs;
|
||||
long analyseMs;
|
||||
long injectMs;
|
||||
long treeMs;
|
||||
long saveMs;
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(input)) {
|
||||
long t = System.nanoTime();
|
||||
List<PageContent> pages = new TaggedContentExtractor().extract(document);
|
||||
extractMs = ms(t);
|
||||
|
||||
t = System.nanoTime();
|
||||
DocumentStructure structure = new LayoutAnalyzer().analyse(pages);
|
||||
analyseMs = ms(t);
|
||||
|
||||
t = System.nanoTime();
|
||||
var injector = new stirling.software.proprietary.pdf.ua.MarkedContentInjector();
|
||||
var byPage =
|
||||
new java.util.LinkedHashMap<
|
||||
Integer, List<stirling.software.proprietary.pdf.ua.StructBlock>>();
|
||||
structure
|
||||
.getBlocks()
|
||||
.forEach(
|
||||
b ->
|
||||
byPage.computeIfAbsent(b.getPageIndex(), k -> new ArrayList<>())
|
||||
.add(b));
|
||||
for (int p = 0; p < document.getNumberOfPages(); p++) {
|
||||
injector.inject(
|
||||
document, document.getPage(p), byPage.getOrDefault(p, List.of()), 0, true);
|
||||
}
|
||||
injectMs = ms(t);
|
||||
|
||||
t = System.nanoTime();
|
||||
new stirling.software.proprietary.pdf.ua.StructTreeWriter()
|
||||
.write(document, structure, PdfUaProfile.UA1);
|
||||
treeMs = ms(t);
|
||||
|
||||
t = System.nanoTime();
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
document.save(out);
|
||||
saveMs = ms(t);
|
||||
}
|
||||
|
||||
System.out.printf(
|
||||
Locale.ROOT,
|
||||
"%nTagging sub-phases over 60 pages%n"
|
||||
+ " extract %5d ms%n"
|
||||
+ " analyse %5d ms%n"
|
||||
+ " inject %5d ms%n"
|
||||
+ " struct tree %5d ms%n"
|
||||
+ " save %5d ms%n",
|
||||
extractMs,
|
||||
analyseMs,
|
||||
injectMs,
|
||||
treeMs,
|
||||
saveMs);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("memory stays proportional to document size, not quadratic")
|
||||
void memoryScales() throws Exception {
|
||||
List<String> rows = new ArrayList<>();
|
||||
long previousPerPage = 0;
|
||||
boolean blewUp = false;
|
||||
|
||||
for (int pages : new int[] {20, 80, 200}) {
|
||||
byte[] input = document(pages);
|
||||
long before = usedHeap();
|
||||
var outcome = service.convert(input, options());
|
||||
long after = usedHeap();
|
||||
long perPageKb = Math.max(after - before, 0) / 1024 / pages;
|
||||
rows.add(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
" %-6d pages in %-9s out %-9s ~%d KB/page retained",
|
||||
pages,
|
||||
humanBytes(input.length),
|
||||
humanBytes(outcome.pdfBytes().length),
|
||||
perPageKb));
|
||||
// Per-page cost should stay roughly flat; a big jump means something accumulates.
|
||||
if (previousPerPage > 0 && perPageKb > previousPerPage * 4 && perPageKb > 200) {
|
||||
blewUp = true;
|
||||
}
|
||||
previousPerPage = Math.max(perPageKb, 1);
|
||||
}
|
||||
System.out.println("\nMemory scaling\n" + String.join("\n", rows));
|
||||
assertTrue(!blewUp, "per-page memory grew superlinearly: " + rows);
|
||||
}
|
||||
|
||||
private static long ms(long startNanos) {
|
||||
return (System.nanoTime() - startNanos) / 1_000_000;
|
||||
}
|
||||
|
||||
private static String humanBytes(int bytes) {
|
||||
return bytes < 1024 * 1024
|
||||
? (bytes / 1024) + " KB"
|
||||
: String.format(Locale.ROOT, "%.1f MB", bytes / 1024.0 / 1024.0);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3
|
||||
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
||||
|
||||
# Stage 1: Build the Java application (backend only, no frontend)
|
||||
FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build
|
||||
FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build
|
||||
|
||||
# JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead
|
||||
ENV JDK_JAVA_OPTIONS="--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
|
||||
|
||||
@@ -5,7 +5,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3
|
||||
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
||||
|
||||
# Stage 1: Build the Java application and frontend
|
||||
FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build
|
||||
FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build
|
||||
|
||||
ARG TASK_VERSION=3.53.1
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -8,7 +8,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3
|
||||
ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION}
|
||||
|
||||
# Stage 1: Build the Java application and frontend
|
||||
FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build
|
||||
FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build
|
||||
|
||||
ARG TASK_VERSION=3.53.1
|
||||
RUN apt-get update \
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Single JAR contains both frontend and backend with minimal dependencies
|
||||
|
||||
# Stage 1: Build application with embedded frontend
|
||||
FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS build
|
||||
FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS build
|
||||
|
||||
# Install Node.js and npm for frontend build
|
||||
ARG TASK_VERSION=3.53.1
|
||||
|
||||
@@ -49,7 +49,7 @@ tools = [
|
||||
"deep-translator>=1.11.4",
|
||||
"defusedxml>=0.7.1",
|
||||
"fonttools>=4.63.0",
|
||||
"fpdf2>=2.8.7",
|
||||
"fpdf2>=2.8.8",
|
||||
"openai>=2.53.0",
|
||||
"requests>=2.34.2",
|
||||
"tomli-w>=1.2.0",
|
||||
|
||||
Generated
+4
-4
@@ -726,7 +726,7 @@ tools = [
|
||||
{ name = "deep-translator", specifier = ">=1.11.4" },
|
||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||
{ name = "fonttools", specifier = ">=4.63.0" },
|
||||
{ name = "fpdf2", specifier = ">=2.8.7" },
|
||||
{ name = "fpdf2", specifier = ">=2.8.8" },
|
||||
{ name = "openai", specifier = ">=2.53.0" },
|
||||
{ name = "requests", specifier = ">=2.34.2" },
|
||||
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||
@@ -898,16 +898,16 @@ woff = [
|
||||
|
||||
[[package]]
|
||||
name = "fpdf2"
|
||||
version = "2.8.7"
|
||||
version = "2.8.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
{ name = "fonttools" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/f2/72feae0b2827ed38013e4307b14f95bf0b3d124adfef4d38a7d57533f7be/fpdf2-2.8.7.tar.gz", hash = "sha256:7060ccee5a9c7ab0a271fb765a36a23639f83ef8996c34e3d46af0a17ede57f9", size = 362351, upload-time = "2026-02-28T05:39:16.456Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/bc/8fd4321aed40cadadddc8f311c65b6082346b252bca048f7b476d8f35d72/fpdf2-2.8.8.tar.gz", hash = "sha256:9e94e155e85e8053329a9a1fce8b566fd7a7c5bb79e98a1a3952d379b947c5b9", size = 374689, upload-time = "2026-08-09T23:32:45.334Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0a/cf50ecffa1e3747ed9380a3adfc829259f1f86b3fdbd9e505af789003141/fpdf2-2.8.7-py3-none-any.whl", hash = "sha256:d391fc508a3ce02fc43a577c830cda4fe6f37646f2d143d489839940932fbc19", size = 327056, upload-time = "2026-02-28T05:39:14.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/be/af012eda9507494f28b99b077423806c43a11573eb6225dd46f19ae2d263/fpdf2-2.8.8-py3-none-any.whl", hash = "sha256:3557a478fc577a929c94aace9666aed4dcc432b5ab6764232e6a59f1ccd75f17", size = 337000, upload-time = "2026-08-09T23:32:43.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { TextInput } from "@app/components/shared/TextInput";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
|
||||
function renderWithMantine(ui: React.ReactElement) {
|
||||
return render(<MantineProvider>{ui}</MantineProvider>);
|
||||
}
|
||||
|
||||
describe("TextInput", () => {
|
||||
it("renders input with value and placeholder", () => {
|
||||
renderWithMantine(
|
||||
<TextInput
|
||||
id="test-input"
|
||||
name="test-input"
|
||||
value="hello"
|
||||
onChange={vi.fn()}
|
||||
placeholder="Type here..."
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText("Type here...");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue("hello");
|
||||
});
|
||||
|
||||
it("shows clear button when there is a value and triggers clear on click", () => {
|
||||
const onChange = vi.fn();
|
||||
renderWithMantine(
|
||||
<TextInput
|
||||
id="test-input"
|
||||
name="test-input"
|
||||
value="search text"
|
||||
onChange={onChange}
|
||||
showClearButton={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const clearButton = screen.getByRole("button", {
|
||||
name: /(?:clear|textInput\.clear)/i,
|
||||
});
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(clearButton);
|
||||
expect(onChange).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("does not show clear button when value is empty", () => {
|
||||
renderWithMantine(
|
||||
<TextInput
|
||||
id="test-input"
|
||||
name="test-input"
|
||||
value=""
|
||||
onChange={vi.fn()}
|
||||
showClearButton={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /clear input/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show clear button when showClearButton is false", () => {
|
||||
renderWithMantine(
|
||||
<TextInput
|
||||
id="test-input"
|
||||
name="test-input"
|
||||
value="some text"
|
||||
onChange={vi.fn()}
|
||||
showClearButton={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /clear input/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -119,12 +119,25 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
/>
|
||||
{shouldShowClearButton && (
|
||||
<ActionIcon
|
||||
variant="tertiary"
|
||||
variant="quiet"
|
||||
accent="neutral"
|
||||
shape="circle"
|
||||
className={styles.clearButton}
|
||||
style={
|
||||
{
|
||||
"--ai-size": "1.25rem",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onClick={handleClear}
|
||||
aria-label={t("textInput.clear", "Clear input")}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
<LocalIcon
|
||||
icon="close-rounded"
|
||||
width="0.875rem"
|
||||
height="0.875rem"
|
||||
/>
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -209,9 +209,6 @@
|
||||
}
|
||||
|
||||
.super-search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border: none;
|
||||
@@ -227,6 +224,18 @@
|
||||
background: var(--c-hover);
|
||||
}
|
||||
|
||||
.super-search-item .mantine-Button-inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.super-search-item .mantine-Button-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.super-search-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -241,6 +250,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.super-search-item-title {
|
||||
|
||||
@@ -52,22 +52,29 @@
|
||||
|
||||
.clearButton {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
right: 6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
--ai-size: 1.35rem !important;
|
||||
width: 1.35rem !important;
|
||||
height: 1.35rem !important;
|
||||
min-width: 1.35rem !important;
|
||||
min-height: 1.35rem !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: var(--radius-full, 9999px);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--c-text-subtle);
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.clearButton:hover {
|
||||
background-color: var(--c-hover);
|
||||
color: var(--c-text);
|
||||
background-color: var(--c-hover) !important;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
Reference in New Issue
Block a user