From 603e797694b3a143b7f2d6abe91b0d325e6be262 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Sun, 18 Jan 2026 23:35:48 +0300 Subject: [PATCH 1/2] added IF handler for PDF form items processing, metanorma/mn-samples-oiml#24 --- Makefile | 2 +- README.adoc | 12 +- pom.xml | 2 +- .../java/org/metanorma/fop/PDFGenerator.java | 32 +- .../org/metanorma/fop/SourceXMLDocument.java | 8 + .../java/org/metanorma/fop/form/FormItem.java | 34 ++ .../org/metanorma/fop/form/FormItemType.java | 7 + .../fop/ifhandler/FOPIFFormsHandler.java | 295 ++++++++++++++++++ src/main/resources/META-INF/MANIFEST.MF | 2 +- 9 files changed, 380 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/metanorma/fop/form/FormItem.java create mode 100644 src/main/java/org/metanorma/fop/form/FormItemType.java create mode 100644 src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java diff --git a/Makefile b/Makefile index 0a2c8b8..2b0a5d9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ SHELL ?= /bin/bash endif #JAR_VERSION := $(shell mvn -q -Dexec.executable="echo" -Dexec.args='$${project.version}' --non-recursive exec:exec -DforceStdout) -JAR_VERSION := 2.38 +JAR_VERSION := 2.39 JAR_FILE := mn2pdf-$(JAR_VERSION).jar all: target/$(JAR_FILE) diff --git a/README.adoc b/README.adoc index 972754b..6327a83 100644 --- a/README.adoc +++ b/README.adoc @@ -17,14 +17,14 @@ You will need the Java Development Kit (JDK) version 8, Update 241 (8u241) or hi [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-2.38.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] +java -Xss5m -Xmx2048m -jar target/mn2pdf-2.39.jar --xml-file --xsl-file --pdf-file [--syntax-highlight] ---- e.g. [source,sh] ---- -java -Xss5m -Xmx2048m -jar target/mn2pdf-2.38.jar --xml-file tests/G.191.xml --xsl-file tests/itu.recommendation.xsl --pdf-file tests/G.191.pdf +java -Xss5m -Xmx2048m -jar target/mn2pdf-2.39.jar --xml-file tests/G.191.xml --xsl-file tests/itu.recommendation.xsl --pdf-file tests/G.191.pdf ---- === PDF encryption features @@ -108,7 +108,7 @@ Update version in `pom.xml`, e.g.: ---- org.metanorma.fop mn2pdf -2.38 +2.39 Metanorma XML to PDF converter ---- @@ -116,7 +116,7 @@ and in `src/main/resources/META-INF/MANIFEST.MF`: [source] ---- -Implementation-Version: 2.38 +Implementation-Version: 2.39 ---- @@ -127,8 +127,8 @@ Tag the same version in Git: [source,xml] ---- -git tag v2.38 -git push origin v2.38 +git tag v2.39 +git push origin v2.39 ---- Then the corresponding GitHub release will be automatically created at: diff --git a/pom.xml b/pom.xml index 482c6ef..ecd101e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.metanorma.fop mn2pdf - 2.38 + 2.39 Metanorma XML to PDF converter jar https://www.metanorma.org diff --git a/src/main/java/org/metanorma/fop/PDFGenerator.java b/src/main/java/org/metanorma/fop/PDFGenerator.java index a1f8d44..795192b 100644 --- a/src/main/java/org/metanorma/fop/PDFGenerator.java +++ b/src/main/java/org/metanorma/fop/PDFGenerator.java @@ -50,10 +50,8 @@ import org.metanorma.fop.annotations.FileAttachmentAnnotation; import org.metanorma.fop.eventlistener.LoggingEventListener; import org.metanorma.fop.eventlistener.SecondPassSysOutEventListener; -import org.metanorma.fop.ifhandler.FOPIFFlatHandler; -import org.metanorma.fop.ifhandler.FOPIFHiddenMathHandler; -import org.metanorma.fop.ifhandler.FOPIFIndexHandler; -import org.metanorma.fop.ifhandler.FOPXMLPresentationHandler; +import org.metanorma.fop.form.FormItem; +import org.metanorma.fop.ifhandler.*; import org.metanorma.fop.portfolio.PDFMetainfo; import org.metanorma.fop.portfolio.PDFPortfolio; import org.metanorma.fop.portfolio.PDFPortfolioItem; @@ -109,6 +107,8 @@ public class PDFGenerator { private boolean isAddLineNumbers = false; private boolean isAddCommentaryPageNumbers = false; + + private boolean isAddForms = false; private boolean isAddMathAsAttachment = false; @@ -369,6 +369,7 @@ public boolean process() { isAddAnnotations = sourceXMLDocument.hasAnnotations(); isAddFileAttachmentAnnotations = sourceXMLDocument.hasFileAttachmentAnnotations(); isTableExists = sourceXMLDocument.hasTables(); + isAddForms = sourceXMLDocument.hasForms(); boolean isMathExists = sourceXMLDocument.hasMath(); XSLTconverter xsltConverter = new XSLTconverter(fXSL, fXSLoverride, sourceXMLDocument.getPreprocessXSLT(), fPDF.getAbsolutePath()); @@ -772,7 +773,11 @@ private void runFOP (fontConfig fontcfg, Source src, File pdf) throws IOExceptio String mime = MimeConstants.MIME_PDF; - boolean isPostprocessing = isAddMathAsText || isAddAnnotations || isAddLineNumbers || isAddCommentaryPageNumbers; + boolean isPostprocessing = isAddMathAsText || + isAddAnnotations || + isAddLineNumbers || + isAddCommentaryPageNumbers || + isAddForms; if (isPostprocessing) { logger.info("Starting post-processing..."); @@ -815,6 +820,23 @@ private void runFOP (fontConfig fontcfg, Source src, File pdf) throws IOExceptio debugSaveXML(xmlIF, pdf.getAbsolutePath() + ".if.commentarypagenumbers.xml"); } + if (isAddForms) { + logger.info("Read Forms information from Intermediate Format..."); + + + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser saxParser = factory.newSAXParser(); + FOPIFFormsHandler fopIFFormsHandler = new FOPIFFormsHandler(); + InputSource srcIntermediateXML = new InputSource(new StringReader(xmlIF)); + saxParser.parse(srcIntermediateXML, fopIFFormsHandler); + + //String xmlIFForm = applyXSLT("forms_if.xsl", xmlIF, true); + + xmlIF = fopIFFormsHandler.getResultedXML(); + List formItems = fopIFFormsHandler.getFormsItems(); + + debugSaveXML(xmlIF, pdf.getAbsolutePath() + ".if.forms.xml"); + } src = new StreamSource(new StringReader(xmlIF)); } diff --git a/src/main/java/org/metanorma/fop/SourceXMLDocument.java b/src/main/java/org/metanorma/fop/SourceXMLDocument.java index bd3aeca..64a44f5 100644 --- a/src/main/java/org/metanorma/fop/SourceXMLDocument.java +++ b/src/main/java/org/metanorma/fop/SourceXMLDocument.java @@ -49,6 +49,7 @@ public class SourceXMLDocument { private boolean hasAnnotations = false; private boolean hasFileAttachmentAnnotations = false; private boolean hasTables = false; + private boolean hasForms = false; private Map tablesCellsCountMap = new HashMap<>(); private boolean hasMath = false; @@ -108,6 +109,8 @@ private void readMetaInformation() { //hasTables = element_table.length() != 0; obtainTablesCellsCount(); hasTables = !tablesCellsCountMap.isEmpty(); + String element_form = readValue("//*[local-name() = 'form'][1]"); + hasForms = element_form.length() != 0; } private void obtainTablesCellsCount() { @@ -506,6 +509,11 @@ public boolean hasTables() { return hasTables; } + // find tag 'form' + public boolean hasForms() { + return hasForms; + } + public boolean hasMath() { return hasMath; } diff --git a/src/main/java/org/metanorma/fop/form/FormItem.java b/src/main/java/org/metanorma/fop/form/FormItem.java new file mode 100644 index 0000000..f7b06a1 --- /dev/null +++ b/src/main/java/org/metanorma/fop/form/FormItem.java @@ -0,0 +1,34 @@ +package org.metanorma.fop.form; + +import org.apache.pdfbox.pdmodel.common.PDRectangle; + +public class FormItem { + + PDRectangle rect ;// = new PDRectangle(50, 750, 200, 50); + + FormItemType formItemType; + + public FormItem(PDRectangle rect) { + this.rect = rect; + } + + public PDRectangle getRect() { + return rect; + } + + public FormItemType getFormItemType() { + return formItemType; + } + + public void setFormItemType(String formItemType) { + switch (formItemType) { + case "textfield": + this.formItemType = FormItemType.TextField; + break; + default: + this.formItemType = FormItemType.TextField; + break; + } + + } +} diff --git a/src/main/java/org/metanorma/fop/form/FormItemType.java b/src/main/java/org/metanorma/fop/form/FormItemType.java new file mode 100644 index 0000000..f9bd9f3 --- /dev/null +++ b/src/main/java/org/metanorma/fop/form/FormItemType.java @@ -0,0 +1,7 @@ +package org.metanorma.fop.form; + +public enum FormItemType { + TextField, + CheckBox, +} + diff --git a/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java b/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java new file mode 100644 index 0000000..3ec342d --- /dev/null +++ b/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java @@ -0,0 +1,295 @@ +package org.metanorma.fop.ifhandler; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.metanorma.fop.form.FormItem; +import org.metanorma.utils.LoggerHelper; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.*; +import java.util.logging.Logger; + +public class FOPIFFormsHandler extends DefaultHandler { + + protected static final Logger logger = Logger.getLogger(LoggerHelper.LOGGER_NAME); + + private final String XMLHEADER = ""; + + private final Character SIGN_GREATER = '>'; + + private List listResult = new ArrayList<>(); + + private String rootXMLNS = ""; + + private String previousElement = ""; + + boolean isBorderAroundElement = false; + + Stack stackViewports = new Stack<>(); + + FormItem currFormItem; + List formItems = new ArrayList<>(); + + boolean isViewportProcessing = false; + + StringBuilder sbViewport = new StringBuilder(); + + Stack stackElements = new Stack<>(); + + Stack stackChar = new Stack<>(); + + Stack skipElements = new Stack<>(); + + @Override + public void startDocument() { + //sbResult.append(XMLHEADER); + listResult.add(XMLHEADER); + } + + @Override + public void startElement(String uri, String lName, String qName, Attributes attr) throws SAXException { + + stackElements.push(qName); + + if (rootXMLNS.isEmpty()) { + rootXMLNS = copyAttributes(attr); + } + + // if next item is border around the form element + if (qName.equals("id") && attr.getValue("name").startsWith("_metanorma_form_item_border_")) { + // example: + isBorderAroundElement = true; + // skip + skipElements.push(true); + return; + } + // text element with hair space after + if (qName.equals("text") && isBorderAroundElement && !previousElement.equals("form_item_id")) { + // example: + skipElements.push(true); + return; + } + if (qName.equals("border-rect") && isBorderAroundElement) { + // example: + float border_x1 = stackViewports.peek().getX() + Integer.parseInt(attr.getValue("x")); + float border_y1 = stackViewports.peek().getY() + Integer.parseInt(attr.getValue("y")); + float border_x2 = border_x1 + Integer.parseInt(attr.getValue("width")); + float border_y2 = border_y1 + Integer.parseInt(attr.getValue("height")); + PDRectangle pdRectangle = new PDRectangle(); + pdRectangle.setLowerLeftX(border_x1); + pdRectangle.setLowerLeftY(border_y1); + pdRectangle.setUpperRightX(border_x2); + pdRectangle.setUpperRightY(border_y2); + currFormItem = new FormItem(new PDRectangle()); + skipElements.push(true); + return; + } + if (qName.equals("id") && attr.getValue("name").startsWith("_metanorma_form_item_")) { + // example: + + String attname = attr.getValue("name"); + String value = attname.substring("_metanorma_form_item_".length()); + String field_type = value.substring(0, value.indexOf("_")); + currFormItem.setFormItemType(field_type); + formItems.add(currFormItem); + + previousElement = "form_item_id"; + skipElements.push(false); + copyStartElement(qName, attr); + return; + } + + if (qName.equals("text") && isBorderAroundElement && previousElement.equals("form_item_id")) { + // example: __________ + //skipElements.push(true); + // Todo: replace to hair space - for previous ID work + skipElements.push(false); + copyStartElement(qName, attr); + return; + } + + isBorderAroundElement = false; + + skipElements.push(false); + + switch (qName) { + case "viewport": + case "g": + + int x = 0; + int y = 0; + + if (!stackViewports.isEmpty()) { + x = stackViewports.peek().getX(); + y = stackViewports.peek().getY(); + } + + int value_x = 0; + int value_y = 0; + + String attr_transform = attr.getValue("transform"); + if (attr_transform != null && attr_transform.startsWith("translate")) { + String translate_value = attr_transform.substring(attr_transform.indexOf("(") + 1, attr_transform.indexOf(")")); + + if (translate_value.contains(",")) { // Example: transform="translate(70866,36000)" + value_x = Integer.valueOf(translate_value.substring(0, translate_value.indexOf(","))); + } else { // Example: transform="translate(70866)" + value_x = Integer.valueOf(translate_value); + } + + if (translate_value.contains(",")) { // Example: transform="translate(70866,36000)" + value_y = Integer.valueOf(translate_value.substring(translate_value.indexOf(",") + 1)); + } else { // Example: transform="translate(70866)" + value_y = 0; + } + } + + int new_x = x + value_x; + int new_y = y + value_y; + + stackViewports.push(new Viewport(new_x, new_y)); + break; + } + + copyStartElement(qName, attr); + + previousElement = qName; + } + + private void copyStartElement(String qName, Attributes attr) { + StringBuilder sbTmp = new StringBuilder(); + + updateStackChar(sbTmp); + + sbTmp.append("<"); + sbTmp.append(qName); + sbTmp.append(copyAttributes(attr)); + + stackChar.push(SIGN_GREATER); + + if (isViewportProcessing) { + sbViewport.append(sbTmp.toString()); + } else { + //sbResult.append(sbTmp.toString()); + listResult.add(sbTmp.toString()); + } + } + + private String copyAttributes(Attributes attr) { + StringBuilder sbTmp = new StringBuilder(); + for (int i = 0; i < attr.getLength(); i++) { + sbTmp.append(" "); + String attName = attr.getLocalName(i); + sbTmp.append(attName); + sbTmp.append("=\""); + String value = StringEscapeUtils.escapeXml(attr.getValue(i)); + if (attName.equals("name") && value.startsWith("_metanorma_form_item_")) { + value = value.substring("_metanorma_form_item_".length()); + value = value.substring(value.indexOf("_") + 1); + } + sbTmp.append(value); + sbTmp.append("\""); + } + return sbTmp.toString(); + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + stackElements.pop(); + + if (skipElements.contains(true)) { + // skip + } else { + + switch (qName) { + case "viewport": + case "g": + stackViewports.pop(); + break; + default: + break; + } + + copyEndElement(qName, listResult); + + } + //previousElement = qName; + skipElements.pop(); + } + + private void copyEndElement(String qName, StringBuilder sb) { + if (!stackChar.isEmpty() && stackChar.peek().compareTo(SIGN_GREATER) == 0) { + sb.append("/>"); + } else { + sb.append(""); + } + stackChar.pop(); + } + private void copyEndElement(String qName, List list) { + if (!stackChar.isEmpty() && stackChar.peek().compareTo(SIGN_GREATER) == 0) { + list.add("/>"); + } else { + list.add(""); + } + stackChar.pop(); + } + + @Override + public void characters(char character[], int start, int length) throws SAXException { + if (skipElements.contains(true)) { + return; + } + String str = new String(character, start, length); + str = StringEscapeUtils.escapeXml(str); + if (!str.isEmpty()) { + if (isViewportProcessing) { + updateStackChar(sbViewport); + sbViewport.append(str); + } else { + updateStackChar(listResult); + listResult.add(str); + } + } + } + + private void updateStackChar(StringBuilder sb) { + if (!stackChar.isEmpty() && stackChar.peek().compareTo(SIGN_GREATER) == 0) { + sb.append(stackChar.pop()); + stackChar.push(Character.MIN_VALUE); + } + } + + private void updateStackChar(List list) { + if (!stackChar.isEmpty() && stackChar.peek().compareTo(SIGN_GREATER) == 0) { + list.add(stackChar.pop().toString()); + stackChar.push(Character.MIN_VALUE); + } + } + + public String getResultedXML() { + if (listResult.size() == 0) { + return ""; + } + int size = 0; + for (String item: listResult) { + size += item.length(); + } + StringBuilder sbResult = new StringBuilder(size); + for (String item: listResult) { + sbResult.append(item); + } + listResult.clear(); + return sbResult.toString(); + } + + public List getFormsItems() { + return formItems; + } + +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF index a21c40a..3467477 100644 --- a/src/main/resources/META-INF/MANIFEST.MF +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -1,5 +1,5 @@ Manifest-Version: 1.0 -Implementation-Version: 2.38 +Implementation-Version: 2.39 Build-Jdk-Spec: 1.8 Created-By: Maven JAR Plugin 3.3.0 Main-Class: org.metanorma.fop.mn2pdf From 950b27039bf5b003f413dc0f416bb568b642a0c8 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Tue, 20 Jan 2026 00:19:52 +0300 Subject: [PATCH 2/2] updated for PDF forms items processing, metanorma/mn-samples-oiml#24 --- .../java/org/metanorma/fop/PDFGenerator.java | 16 +++++++++++++++- .../java/org/metanorma/fop/form/FormItem.java | 9 ++++++++- .../fop/ifhandler/FOPIFFormsHandler.java | 8 ++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/metanorma/fop/PDFGenerator.java b/src/main/java/org/metanorma/fop/PDFGenerator.java index 795192b..83c4dde 100644 --- a/src/main/java/org/metanorma/fop/PDFGenerator.java +++ b/src/main/java/org/metanorma/fop/PDFGenerator.java @@ -51,6 +51,7 @@ import org.metanorma.fop.eventlistener.LoggingEventListener; import org.metanorma.fop.eventlistener.SecondPassSysOutEventListener; import org.metanorma.fop.form.FormItem; +import org.metanorma.fop.form.PDFForm; import org.metanorma.fop.ifhandler.*; import org.metanorma.fop.portfolio.PDFMetainfo; import org.metanorma.fop.portfolio.PDFPortfolio; @@ -109,6 +110,8 @@ public class PDFGenerator { private boolean isAddCommentaryPageNumbers = false; private boolean isAddForms = false; + + private List formsItems = new ArrayList<>(); private boolean isAddMathAsAttachment = false; @@ -833,7 +836,7 @@ private void runFOP (fontConfig fontcfg, Source src, File pdf) throws IOExceptio //String xmlIFForm = applyXSLT("forms_if.xsl", xmlIF, true); xmlIF = fopIFFormsHandler.getResultedXML(); - List formItems = fopIFFormsHandler.getFormsItems(); + formsItems = fopIFFormsHandler.getFormsItems(); debugSaveXML(xmlIF, pdf.getAbsolutePath() + ".if.forms.xml"); } @@ -994,6 +997,17 @@ private void runFOP (fontConfig fontcfg, Source src, File pdf) throws IOExceptio } } + if (isAddForms && !formsItems.isEmpty()) { + logger.log(Level.INFO, "[INFO] Forms processing..."); + try { + PDFForm forms = new PDFForm(); + forms.process(pdf, formsItems); + } catch (Exception ex) { + logger.severe("Can't process forms (" + ex.toString() + ")."); + ex.printStackTrace(); + } + } + Profiler.printProcessingTime(methodName, startMethodTime); Profiler.removeMethodCall(); } diff --git a/src/main/java/org/metanorma/fop/form/FormItem.java b/src/main/java/org/metanorma/fop/form/FormItem.java index f7b06a1..01dab20 100644 --- a/src/main/java/org/metanorma/fop/form/FormItem.java +++ b/src/main/java/org/metanorma/fop/form/FormItem.java @@ -6,10 +6,13 @@ public class FormItem { PDRectangle rect ;// = new PDRectangle(50, 750, 200, 50); + int page; + FormItemType formItemType; - public FormItem(PDRectangle rect) { + public FormItem(PDRectangle rect, int page) { this.rect = rect; + this.page = page; } public PDRectangle getRect() { @@ -20,6 +23,10 @@ public FormItemType getFormItemType() { return formItemType; } + public int getPage() { + return page; + } + public void setFormItemType(String formItemType) { switch (formItemType) { case "textfield": diff --git a/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java b/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java index 3ec342d..534ce6b 100644 --- a/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java +++ b/src/main/java/org/metanorma/fop/ifhandler/FOPIFFormsHandler.java @@ -29,6 +29,8 @@ public class FOPIFFormsHandler extends DefaultHandler { Stack stackViewports = new Stack<>(); + private int page; + FormItem currFormItem; List formItems = new ArrayList<>(); @@ -82,7 +84,7 @@ public void startElement(String uri, String lName, String qName, Attributes attr pdRectangle.setLowerLeftY(border_y1); pdRectangle.setUpperRightX(border_x2); pdRectangle.setUpperRightY(border_y2); - currFormItem = new FormItem(new PDRectangle()); + currFormItem = new FormItem(new PDRectangle(), page); skipElements.push(true); return; } @@ -115,6 +117,8 @@ public void startElement(String uri, String lName, String qName, Attributes attr skipElements.push(false); switch (qName) { + case "page": + page++; case "viewport": case "g": @@ -271,7 +275,7 @@ private void updateStackChar(List list) { stackChar.push(Character.MIN_VALUE); } } - + public String getResultedXML() { if (listResult.size() == 0) { return "";