From 0d006e97f8f34ada469b741fddfd5adf210a53aa Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 14 Oct 2025 15:32:25 +0300 Subject: [PATCH 01/14] Fix logo and address not showing in Sticker --- .../PatientIdStickerXmlReportRenderer.java | 35 ++++++++++++------ .../resources/msfStickerFopStylesheet.xsl | 8 ++-- .../patientIdStickerFopStylesheet.xsl | 34 +++++++++++------ omod/pom.xml | 5 +++ .../main/webapp/resources/OpenMRS_logo.png | Bin 0 -> 29275 bytes 5 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 omod/src/main/webapp/resources/OpenMRS_logo.png diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index be60dd8..375acec 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -15,6 +15,9 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -46,6 +49,7 @@ import org.openmrs.module.reporting.report.renderer.RenderingException; import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; import org.openmrs.module.reporting.report.renderer.ReportRenderer; +import org.openmrs.util.OpenmrsClassLoader; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -239,16 +243,24 @@ private void configureHeader(Document doc, Element templatePIDElement) { } private void configureLogo(Document doc, Element header, String logoUrlPath) { - String logoPath; + String logoPath = ""; File logoFile = new File(logoUrlPath); boolean isValidFile = logoFile.exists() && logoFile.canRead() && logoFile.isAbsolute(); if (isValidFile) { logoPath = logoFile.getAbsolutePath(); } else { - throw new RenderingException("Logo file not found or not accessible: " + logoUrlPath); + try { + URL res = OpenmrsClassLoader.getInstance().getResource(logoUrlPath); + + if (res != null) { + logoPath = Paths.get(res.toURI()).toString(); + } + } + catch (URISyntaxException e) { + throw new RenderingException("Logo file not found or not accessible: " + logoUrlPath); + } } - Element branding = doc.createElement("branding"); Element image = doc.createElement("logo"); image.setTextContent(logoPath); @@ -365,15 +377,16 @@ && shouldIncludeColumn("patientdocuments.patientIdSticker.fields.secondaryIdenti // Process address if (shouldIncludeColumn("patientdocuments.patientIdSticker.fields.fulladdress")) { - Map addressData = (Map) patientData.get("preferredAddress"); - if (addressData != null) { + List> addressData = (List>) patientData.get("addresses"); + if (addressData != null && !addressData.isEmpty()) { + Map preferredAddress = addressData.get(0); StringBuilder address = new StringBuilder(); - appendIfNotNull(address, addressData.get("address1")); - appendIfNotNull(address, addressData.get("address2")); - appendIfNotNull(address, addressData.get("cityVillage")); - appendIfNotNull(address, addressData.get("stateProvince")); - appendIfNotNull(address, addressData.get("country")); - appendIfNotNull(address, addressData.get("postalCode")); + appendIfNotNull(address, preferredAddress.get("address1")); + appendIfNotNull(address, preferredAddress.get("address2")); + appendIfNotNull(address, preferredAddress.get("cityVillage")); + appendIfNotNull(address, preferredAddress.get("stateProvince")); + appendIfNotNull(address, preferredAddress.get("country")); + appendIfNotNull(address, preferredAddress.get("postalCode")); if (address.length() > 0) { addField(doc, fields, addressKey, address.toString().trim()); diff --git a/api/src/main/resources/msfStickerFopStylesheet.xsl b/api/src/main/resources/msfStickerFopStylesheet.xsl index 180ec2d..c563c4c 100644 --- a/api/src/main/resources/msfStickerFopStylesheet.xsl +++ b/api/src/main/resources/msfStickerFopStylesheet.xsl @@ -80,7 +80,7 @@ + margin="3mm"> @@ -227,7 +227,7 @@ - + @@ -238,7 +238,7 @@ - + @@ -255,7 +255,7 @@ - + diff --git a/api/src/main/resources/patientIdStickerFopStylesheet.xsl b/api/src/main/resources/patientIdStickerFopStylesheet.xsl index e9b2e05..fe05894 100644 --- a/api/src/main/resources/patientIdStickerFopStylesheet.xsl +++ b/api/src/main/resources/patientIdStickerFopStylesheet.xsl @@ -7,7 +7,7 @@ - + @@ -80,7 +80,7 @@ + margin="3mm"> @@ -211,6 +211,17 @@ + + + + + + + + + + + @@ -235,17 +246,16 @@ - - - - - - - - - - + + + + + + + + diff --git a/omod/pom.xml b/omod/pom.xml index d555a1c..2b808fd 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -136,6 +136,11 @@ **/*.xml + + ../omod/src/main/webapp/resources + web/module/resources/ + true + diff --git a/omod/src/main/webapp/resources/OpenMRS_logo.png b/omod/src/main/webapp/resources/OpenMRS_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b941a5c9a4f542de09399fafdbca9943b15d449b GIT binary patch literal 29275 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

$^-l?C*C**Id2xyp@ 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 Date: Tue, 28 Oct 2025 00:59:03 +0300 Subject: [PATCH 02/14] working non-clean logo --- .../PatientIdStickerXmlReportRenderer.java | 76 ++++++++++++++----- .../reports/PatientIdStickerPdfReport.java | 10 ++- ...tientIdStickerDataPdfExportController.java | 47 +++++++++++- ...tIdStickerDataPdfExportControllerTest.java | 6 +- 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index 375acec..c88d98d 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -50,6 +50,7 @@ import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; import org.openmrs.module.reporting.report.renderer.ReportRenderer; import org.openmrs.util.OpenmrsClassLoader; +import org.openmrs.util.OpenmrsUtil; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -64,6 +65,8 @@ @Localized("patientdocuments.patientIdStickerXmlReportRenderer") public class PatientIdStickerXmlReportRenderer extends ReportDesignRenderer { + private static final String DEFAULT_LOGO_CLASSPATH = "org/openmrs/ui/framework/images/openmrs-logo.png"; + private MessageSourceService mss; private InitializerService initializerService; @@ -117,6 +120,10 @@ protected String getStringValue(DataSetRow row, DataSetColumn column) { @Override public void render(ReportData results, String argument, OutputStream out) throws IOException, RenderingException { + render(results, argument, out, null); + } + + public void render(ReportData results, String argument, OutputStream out, byte[] defaultLogoBytes) throws IOException, RenderingException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { @@ -141,7 +148,7 @@ public void render(ReportData results, String argument, OutputStream out) throws Element templatePIDElement = createStickerTemplate(doc); // Handle header configuration - configureHeader(doc, templatePIDElement); + configureHeader(doc, templatePIDElement, defaultLogoBytes); // Process data set fields processDataSetFields(results, doc, templatePIDElement); @@ -214,12 +221,12 @@ private Element createStickerTemplate(Document doc) { return templatePIDElement; } - private void configureHeader(Document doc, Element templatePIDElement) { + private void configureHeader(Document doc, Element templatePIDElement, byte[] defaultLogoBytes) { Element header = doc.createElement("header"); // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); if (isNotBlank(logoUrlPath)) { - configureLogo(doc, header, logoUrlPath); + configureLogo(doc, header, logoUrlPath, defaultLogoBytes); } boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); @@ -242,25 +249,60 @@ private void configureHeader(Document doc, Element templatePIDElement) { templatePIDElement.appendChild(i18nStrings); } - private void configureLogo(Document doc, Element header, String logoUrlPath) { + /** + * Configures the logo for the sticker document. + * + * Logo resolution priority: + * 1. Custom logo from file system (absolute or relative path) + * 2. Default logo from webapp (if provided via byte array) + * 3. Default OpenMRS logo from classpath + * + * @param doc The XML document + * @param header The header element to append the logo to + * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) + * @param defaultLogoBytes Optional default logo from webapp (can be null) + * @throws RenderingException if no valid logo can be found + */ + private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { String logoPath = ""; - File logoFile = new File(logoUrlPath); - boolean isValidFile = logoFile.exists() && logoFile.canRead() && logoFile.isAbsolute(); + {System.out.println("Using custom logo from: "+ OpenmrsUtil.getApplicationDataDirectory());} + // Try to load custom logo from file system + if (logoUrlPath != null && !logoUrlPath.isEmpty()) { + File logoFile = new File(logoUrlPath); + + // Check if path is absolute first (cheap operation) + if (!logoFile.isAbsolute()) { + // Resolve relative paths against OPENMRS_APPLICATION_DIRECTORY + File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); + logoFile = new File(appDataDir, logoUrlPath); + } + + // Now perform expensive file system checks + if (logoFile.exists() && logoFile.canRead()) { + logoPath = logoFile.getAbsolutePath(); + {System.out.println("Using custom logo from: "+ logoPath);} + } else { + {System.out.println("Custom logo not found or not readable: " + logoUrlPath + " Falling back to default logo.");} + } + } - if (isValidFile) { - logoPath = logoFile.getAbsolutePath(); - } else { + // Fallback to default logo from webapp if provided + if (logoPath.isEmpty() && defaultLogoBytes != null && defaultLogoBytes.length > 0) { try { - URL res = OpenmrsClassLoader.getInstance().getResource(logoUrlPath); - - if (res != null) { - logoPath = Paths.get(res.toURI()).toString(); - } - } - catch (URISyntaxException e) { - throw new RenderingException("Logo file not found or not accessible: " + logoUrlPath); + // Write the byte array to a temporary file and use its path + File tempFile = File.createTempFile("openmrs-logo-", ".png"); + tempFile.deleteOnExit(); + java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile); + fos.write(defaultLogoBytes); + fos.close(); + logoPath = tempFile.getAbsolutePath(); + {System.out.println("Using default logo from webapp (" + defaultLogoBytes.length + " bytes)");} + } catch (IOException e) { + {System.out.println("Error creating temp file for webapp logo: " + e.getMessage());} } } + + // Create and append logo elements Element branding = doc.createElement("branding"); Element image = doc.createElement("logo"); image.setTextContent(logoPath); diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java index ece961f..621a452 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java @@ -59,11 +59,15 @@ public class PatientIdStickerPdfReport { private InitializerService initializerService; public byte[] generatePdf(Patient patient) throws RuntimeException { + return generatePdf(patient, null); + } + + public byte[] generatePdf(Patient patient, byte[] defaultLogoBytes) throws RuntimeException { validatePatientAndPrivileges(patient); try { ReportData reportData = createReportData(patient); - byte[] xmlBytes = renderReportToXml(reportData); + byte[] xmlBytes = renderReportToXml(reportData, defaultLogoBytes); return transformXmlToPdf(xmlBytes); } catch (Exception e) { @@ -95,10 +99,10 @@ private ReportData createReportData(Patient patient) throws EvaluationException return reportData; } - private byte[] renderReportToXml(ReportData reportData) throws IOException { + private byte[] renderReportToXml(ReportData reportData, byte[] defaultLogoBytes) throws IOException { PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); try (ByteArrayOutputStream xmlOutputStream = new ByteArrayOutputStream()) { - renderer.render(reportData, null, xmlOutputStream); + renderer.render(reportData, null, xmlOutputStream, defaultLogoBytes); return xmlOutputStream.toByteArray(); } } diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index be719e0..3de6d7e 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -12,7 +12,13 @@ import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.PATIENT_ID_STICKER_ID; import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletRequest; import org.openmrs.Patient; import org.openmrs.api.PatientService; @@ -49,9 +55,10 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati this.pdfReport = pdfReport; } - private ResponseEntity writeResponse(Patient patient, boolean inline) { + private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - byte[] pdfBytes = pdfReport.generatePdf(patient); + byte[] defaultLogoBytes = loadDefaultLogo(servletContext); + byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/pdf"); @@ -68,8 +75,41 @@ private ResponseEntity writeResponse(Patient patient, boolean inline) { } } + private byte[] loadDefaultLogo(ServletContext servletContext) { + byte[] logoBytes = null; + if (servletContext != null) { + try (InputStream logoStream = servletContext.getResourceAsStream("/images/openmrs_logo_white_large.png")) { + if (logoStream != null) { + logger.info("Logo file found using ServletContext.getResourceAsStream()"); + + // Read the stream into a byte array + logoBytes = inputStreamToByteArray(logoStream); + if (logoBytes != null) { + logger.info("Successfully read " + logoBytes.length + " bytes from logo file"); + } + } else { + logger.warn("Logo file not found from default path"); + } + } catch (IOException e) { + logger.warn("Error reading logo file: {}", e.getMessage()); + } + } + return logoBytes; + } + + private byte[] inputStreamToByteArray(InputStream inputStream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, bytesRead); + } + return buffer.toByteArray(); + } + @RequestMapping(method = RequestMethod.GET) public ResponseEntity getPatientIdSticker(HttpServletResponse response, + HttpServletRequest request, @RequestParam(value = "patientUuid") String patientUuid, @RequestParam(value = "inline", required = false, defaultValue = "true") boolean inline) { @@ -79,6 +119,7 @@ public ResponseEntity getPatientIdSticker(HttpServletResponse response, return null; } - return writeResponse(patient, inline); + ServletContext servletContext = request.getSession().getServletContext(); + return writeResponse(patient, inline, servletContext); } } diff --git a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java index 45d9b00..6601bf0 100644 --- a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java @@ -69,7 +69,7 @@ public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Ex Context.setLocale(Locale.ENGLISH); MockHttpServletResponse response = new MockHttpServletResponse(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, null, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -93,7 +93,7 @@ public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exc Context.setLocale(new Locale("ar", "AR")); MockHttpServletResponse response = new MockHttpServletResponse(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, null, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -116,7 +116,7 @@ public void getPatientIdSticker_shouldReturn404ForInvalidPatient() throws Except MockHttpServletResponse response = new MockHttpServletResponse(); String invalidUuid = "invalid-uuid"; - ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, invalidUuid, false); + ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, null, invalidUuid, false); assertNull("Response entity should be null", responseEntity); assertEquals("Should return HTTP 404 status", HttpStatus.NOT_FOUND.value(), response.getStatus()); From 6d6f03a3993a280ce8f9518b9496682973267dcc Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 28 Oct 2025 13:06:24 +0300 Subject: [PATCH 03/14] pull logo from servlet context as default --- .../PatientIdStickerXmlReportRenderer.java | 76 +++++++------------ .../reports/PatientIdStickerPdfReport.java | 10 +-- omod/pom.xml | 7 ++ ...tientIdStickerDataPdfExportController.java | 64 ++++++++-------- ...tIdStickerDataPdfExportControllerTest.java | 11 ++- 5 files changed, 78 insertions(+), 90 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index c88d98d..69a15f6 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -9,15 +9,13 @@ */ package org.openmrs.module.patientdocuments.renderer; +import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.openmrs.module.patientdocuments.reports.PatientIdStickerReportManager.DATASET_KEY_STICKER_FIELDS; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -49,7 +47,6 @@ import org.openmrs.module.reporting.report.renderer.RenderingException; import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; import org.openmrs.module.reporting.report.renderer.ReportRenderer; -import org.openmrs.util.OpenmrsClassLoader; import org.openmrs.util.OpenmrsUtil; import org.springframework.stereotype.Component; import org.w3c.dom.Document; @@ -65,8 +62,6 @@ @Localized("patientdocuments.patientIdStickerXmlReportRenderer") public class PatientIdStickerXmlReportRenderer extends ReportDesignRenderer { - private static final String DEFAULT_LOGO_CLASSPATH = "org/openmrs/ui/framework/images/openmrs-logo.png"; - private MessageSourceService mss; private InitializerService initializerService; @@ -120,10 +115,6 @@ protected String getStringValue(DataSetRow row, DataSetColumn column) { @Override public void render(ReportData results, String argument, OutputStream out) throws IOException, RenderingException { - render(results, argument, out, null); - } - - public void render(ReportData results, String argument, OutputStream out, byte[] defaultLogoBytes) throws IOException, RenderingException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { @@ -148,7 +139,7 @@ public void render(ReportData results, String argument, OutputStream out, byte[] Element templatePIDElement = createStickerTemplate(doc); // Handle header configuration - configureHeader(doc, templatePIDElement, defaultLogoBytes); + configureHeader(doc, templatePIDElement); // Process data set fields processDataSetFields(results, doc, templatePIDElement); @@ -221,12 +212,12 @@ private Element createStickerTemplate(Document doc) { return templatePIDElement; } - private void configureHeader(Document doc, Element templatePIDElement, byte[] defaultLogoBytes) { + private void configureHeader(Document doc, Element templatePIDElement) { Element header = doc.createElement("header"); // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); if (isNotBlank(logoUrlPath)) { - configureLogo(doc, header, logoUrlPath, defaultLogoBytes); + configureLogo(doc, header, logoUrlPath); } boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); @@ -254,52 +245,39 @@ private void configureHeader(Document doc, Element templatePIDElement, byte[] de * * Logo resolution priority: * 1. Custom logo from file system (absolute or relative path) - * 2. Default logo from webapp (if provided via byte array) - * 3. Default OpenMRS logo from classpath + * 2. Default OpenMRS logo from classpath * * @param doc The XML document * @param header The header element to append the logo to * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) - * @param defaultLogoBytes Optional default logo from webapp (can be null) * @throws RenderingException if no valid logo can be found */ - private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { + private void configureLogo(Document doc, Element header, String logoUrlPath) { String logoPath = ""; - {System.out.println("Using custom logo from: "+ OpenmrsUtil.getApplicationDataDirectory());} - // Try to load custom logo from file system - if (logoUrlPath != null && !logoUrlPath.isEmpty()) { - File logoFile = new File(logoUrlPath); - - // Check if path is absolute first (cheap operation) - if (!logoFile.isAbsolute()) { - // Resolve relative paths against OPENMRS_APPLICATION_DIRECTORY - File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); - logoFile = new File(appDataDir, logoUrlPath); - } - - // Now perform expensive file system checks - if (logoFile.exists() && logoFile.canRead()) { - logoPath = logoFile.getAbsolutePath(); - {System.out.println("Using custom logo from: "+ logoPath);} - } else { - {System.out.println("Custom logo not found or not readable: " + logoUrlPath + " Falling back to default logo.");} + + try { + // 1. Try custom logo + if (isNotBlank(logoUrlPath)) { + File logoFile = new File(logoUrlPath); + if (!logoFile.isAbsolute()) { + File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); + logoFile = new File(appDataDir, logoUrlPath); + } + if (logoFile.exists() && logoFile.canRead()) { + logoPath = logoFile.getAbsolutePath(); + } } - } - // Fallback to default logo from webapp if provided - if (logoPath.isEmpty() && defaultLogoBytes != null && defaultLogoBytes.length > 0) { - try { - // Write the byte array to a temporary file and use its path - File tempFile = File.createTempFile("openmrs-logo-", ".png"); - tempFile.deleteOnExit(); - java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile); - fos.write(defaultLogoBytes); - fos.close(); - logoPath = tempFile.getAbsolutePath(); - {System.out.println("Using default logo from webapp (" + defaultLogoBytes.length + " bytes)");} - } catch (IOException e) { - {System.out.println("Error creating temp file for webapp logo: " + e.getMessage());} + // 2. Fall back to cached logo + if (isBlank(logoPath)) { + File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); + File cachedLogo = new File(appDataDir, "patientdocuments_logo_cache.png"); + if (cachedLogo.exists() && cachedLogo.canRead()) { + logoPath = cachedLogo.getAbsolutePath(); + } } + } catch (Exception e) { + throw new RenderingException("Failed to configure logo", e); } // Create and append logo elements diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java index 621a452..ece961f 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java @@ -59,15 +59,11 @@ public class PatientIdStickerPdfReport { private InitializerService initializerService; public byte[] generatePdf(Patient patient) throws RuntimeException { - return generatePdf(patient, null); - } - - public byte[] generatePdf(Patient patient, byte[] defaultLogoBytes) throws RuntimeException { validatePatientAndPrivileges(patient); try { ReportData reportData = createReportData(patient); - byte[] xmlBytes = renderReportToXml(reportData, defaultLogoBytes); + byte[] xmlBytes = renderReportToXml(reportData); return transformXmlToPdf(xmlBytes); } catch (Exception e) { @@ -99,10 +95,10 @@ private ReportData createReportData(Patient patient) throws EvaluationException return reportData; } - private byte[] renderReportToXml(ReportData reportData, byte[] defaultLogoBytes) throws IOException { + private byte[] renderReportToXml(ReportData reportData) throws IOException { PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); try (ByteArrayOutputStream xmlOutputStream = new ByteArrayOutputStream()) { - renderer.render(reportData, null, xmlOutputStream, defaultLogoBytes); + renderer.render(reportData, null, xmlOutputStream); return xmlOutputStream.toByteArray(); } } diff --git a/omod/pom.xml b/omod/pom.xml index 2b808fd..0f7a416 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -17,6 +17,7 @@ 2.9 2.0.32 2.17.2 + 3.1.0 @@ -116,6 +117,12 @@ ${fasterxmlJacksonVersion} test + + javax.servlet + javax.servlet-api + ${javaxServletVersion} + test + diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 3de6d7e..7cfa832 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -37,12 +37,17 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.openmrs.util.OpenmrsUtil; +import java.io.File; +import java.io.FileOutputStream; @Controller @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/" + MODULE_ARTIFACT_ID + "/" + PATIENT_ID_STICKER_ID) public class PatientIdStickerDataPdfExportController extends BaseRestController { private static final Logger logger = LoggerFactory.getLogger(PatientIdStickerDataPdfExportController.class); + + private static final String DEFAULT_LOGO_CLASSPATH = "/images/openmrs_logo_white_large.png"; private PatientIdStickerPdfReport pdfReport; @@ -57,8 +62,9 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - byte[] defaultLogoBytes = loadDefaultLogo(servletContext); - byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); + loadAndCacheDefaultLogo(servletContext); + + byte[] pdfBytes = pdfReport.generatePdf(patient); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/pdf"); @@ -74,37 +80,33 @@ private ResponseEntity writeResponse(Patient patient, boolean inline, Se .body("Error generating PDF".getBytes()); } } - - private byte[] loadDefaultLogo(ServletContext servletContext) { - byte[] logoBytes = null; - if (servletContext != null) { - try (InputStream logoStream = servletContext.getResourceAsStream("/images/openmrs_logo_white_large.png")) { - if (logoStream != null) { - logger.info("Logo file found using ServletContext.getResourceAsStream()"); - - // Read the stream into a byte array - logoBytes = inputStreamToByteArray(logoStream); - if (logoBytes != null) { - logger.info("Successfully read " + logoBytes.length + " bytes from logo file"); - } - } else { - logger.warn("Logo file not found from default path"); - } - } catch (IOException e) { - logger.warn("Error reading logo file: {}", e.getMessage()); - } + + private void loadAndCacheDefaultLogo(ServletContext servletContext) { + if (servletContext == null) { + return; } - return logoBytes; - } - - private byte[] inputStreamToByteArray(InputStream inputStream) throws IOException { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - byte[] data = new byte[1024]; - int bytesRead; - while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, bytesRead); + + try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { + if (logoStream == null) { + logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); + return; + } + + File cacheFile = new File( + OpenmrsUtil.getDirectoryInApplicationDataDirectory(""), + "patientdocuments_logo_cache.png" + ); + + try (FileOutputStream fos = new FileOutputStream(cacheFile)) { + OpenmrsUtil.copyFile(logoStream, fos); + logger.info("Successfully cached logo to: {}", cacheFile.getAbsolutePath()); + } + + } catch (IOException e) { + logger.error("Failed to cache logo from: {}", DEFAULT_LOGO_CLASSPATH, e); + } catch (Exception e) { + logger.warn("Unable to load and cache default logo", e); } - return buffer.toByteArray(); } @RequestMapping(method = RequestMethod.GET) diff --git a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java index 6601bf0..4318114 100644 --- a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; @@ -68,8 +69,9 @@ public void setup() throws Exception { public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Exception { Context.setLocale(Locale.ENGLISH); MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, null, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -92,8 +94,9 @@ public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Ex public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exception { Context.setLocale(new Locale("ar", "AR")); MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, null, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -114,9 +117,11 @@ public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exc @Test public void getPatientIdSticker_shouldReturn404ForInvalidPatient() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + String invalidUuid = "invalid-uuid"; - ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, null, invalidUuid, false); + ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, request, invalidUuid, false); assertNull("Response entity should be null", responseEntity); assertEquals("Should return HTTP 404 status", HttpStatus.NOT_FOUND.value(), response.getStatus()); From 1fdce7f6ec8fdc54dce4e50950802ea055fd3948 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Tue, 28 Oct 2025 13:10:10 +0300 Subject: [PATCH 04/14] remove added openmrs logo --- omod/pom.xml | 5 ----- omod/src/main/webapp/resources/OpenMRS_logo.png | Bin 29275 -> 0 bytes 2 files changed, 5 deletions(-) delete mode 100644 omod/src/main/webapp/resources/OpenMRS_logo.png diff --git a/omod/pom.xml b/omod/pom.xml index 0f7a416..dc63196 100644 --- a/omod/pom.xml +++ b/omod/pom.xml @@ -143,11 +143,6 @@ **/*.xml - - ../omod/src/main/webapp/resources - web/module/resources/ - true - diff --git a/omod/src/main/webapp/resources/OpenMRS_logo.png b/omod/src/main/webapp/resources/OpenMRS_logo.png deleted file mode 100644 index b941a5c9a4f542de09399fafdbca9943b15d449b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29275 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

$^-l?C*C**Id2xyp@ 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 Date: Tue, 28 Oct 2025 14:36:18 +0300 Subject: [PATCH 05/14] update documentation --- readme/PatientIdSticker.md | 7 +++++-- readme/PatientIdStickerXSL.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/readme/PatientIdSticker.md b/readme/PatientIdSticker.md index 1831f21..5a77aea 100644 --- a/readme/PatientIdSticker.md +++ b/readme/PatientIdSticker.md @@ -28,7 +28,7 @@ These flags control which patient information is displayed on each sticker. | Key | Type | Description | |---------------------------------------------------------------|---------|--------------------------------------------------| | `report.patientIdSticker.stylesheet` | String | XSL stylesheet to use for rendering stickers | -| `report.patientIdSticker.logourl` | String | URL of the logo image displayed on the sticker | +| `report.patientIdSticker.logourl` | String | Logo path or URL displayed on the sticker (supports a path relative to `OPENMRS_APPLICATION_DATA_DIRECTORY`) | | `report.patientIdSticker.header` | Boolean | Show a header section on each sticker | | `report.patientIdSticker.barcode` | Boolean | Show a barcode section on each sticker | | `report.patientIdSticker.pages` | Number | Number of sticker pages to generate | @@ -92,5 +92,8 @@ The module supports internationalization through message properties. Field label - The default font family is IBM Plex Sans Arabic for labels and IBM Plex Sans Arabic Bold for values. - The secondary identifier type can be configured using `report.patientIdSticker.fields.identifier.secondary.type` if needed. - Available stylesheets include `patientIdStickerFopStylesheet.xsl` (default) and `msfStickerFopStylesheet.xsl` for MSF-specific layouts. -- Logo URLs must be HTTP URLs starting with "http" to be processed. +- Logo resolution rules: + - If `report.patientIdSticker.logourl` starts with `http` or `https`, the logo is downloaded once and cached. + - If `report.patientIdSticker.logourl` is a relative path, it is resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `/openmrs/data/my_custom_logo.png`). + - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context and cached into `OPENMRS_APPLICATION_DATA_DIRECTORY`. - The barcode is generated from the preferred patient identifier when barcode is enabled. \ No newline at end of file diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index c9cfcea..6433653 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -161,9 +161,12 @@ Key features: ### Header Section The optional header section can contain: -- An organizational logo on the left (loaded from HTTP URL) +- An organizational logo on the left (from HTTP(S) URL or from a file path under `OPENMRS_APPLICATION_DATA_DIRECTORY`) - Custom header text on the right -- Automatic logo downloading and caching for HTTP URLs +- Logo handling behavior: + - HTTP(S) logos are downloaded once and cached + - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` + - If none configured or missing, the default OpenMRS logo is loaded from the servlet context and cached under `OPENMRS_APPLICATION_DATA_DIRECTORY` ### Internationalization Section From f82dddc95e25cd808c47a8a493d2ff0abd863eb3 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Wed, 29 Oct 2025 09:42:16 +0300 Subject: [PATCH 06/14] load default logo if no logo config suplied --- .../renderer/PatientIdStickerXmlReportRenderer.java | 4 +--- readme/PatientIdStickerXSL.md | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index 69a15f6..e9b48d5 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -216,9 +216,7 @@ private void configureHeader(Document doc, Element templatePIDElement) { Element header = doc.createElement("header"); // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); - if (isNotBlank(logoUrlPath)) { - configureLogo(doc, header, logoUrlPath); - } + configureLogo(doc, header, logoUrlPath); boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); if (useHeader) { diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index 6433653..2cbd340 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -161,10 +161,9 @@ Key features: ### Header Section The optional header section can contain: -- An organizational logo on the left (from HTTP(S) URL or from a file path under `OPENMRS_APPLICATION_DATA_DIRECTORY`) +- An organizational logo on the left (from a file path under `OPENMRS_APPLICATION_DATA_DIRECTORY`) - Custom header text on the right - Logo handling behavior: - - HTTP(S) logos are downloaded once and cached - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` - If none configured or missing, the default OpenMRS logo is loaded from the servlet context and cached under `OPENMRS_APPLICATION_DATA_DIRECTORY` From bb58b1fc2ebdea4209a705d52fc77cff9f149298 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Thu, 30 Oct 2025 10:26:39 +0300 Subject: [PATCH 07/14] only cache if the file doesn't already exist. --- .../PatientIdStickerDataPdfExportController.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 7cfa832..b6921f0 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -86,20 +86,21 @@ private void loadAndCacheDefaultLogo(ServletContext servletContext) { return; } + File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); + File cachedLogo = new File(appDataDir, "patientdocuments_logo_cache.png"); + if (cachedLogo.exists() && cachedLogo.canRead()) { + return; + } + try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { if (logoStream == null) { logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); return; } - File cacheFile = new File( - OpenmrsUtil.getDirectoryInApplicationDataDirectory(""), - "patientdocuments_logo_cache.png" - ); - - try (FileOutputStream fos = new FileOutputStream(cacheFile)) { + try (FileOutputStream fos = new FileOutputStream(cachedLogo)) { OpenmrsUtil.copyFile(logoStream, fos); - logger.info("Successfully cached logo to: {}", cacheFile.getAbsolutePath()); + logger.info("Successfully cached logo to: {}", cachedLogo.getAbsolutePath()); } } catch (IOException e) { From 4a533f9f0ea0fd54f4610621528d39709a8e7629 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Thu, 30 Oct 2025 15:30:59 +0300 Subject: [PATCH 08/14] remove ability to cache logo --- .../PatientIdStickerXmlReportRenderer.java | 21 +++++------ .../reports/PatientIdStickerPdfReport.java | 8 ++--- ...PatientIdStickerXmlReportRendererTest.java | 2 +- ...tientIdStickerDataPdfExportController.java | 36 +++++++------------ 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index e9b48d5..d4ca626 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -115,6 +115,10 @@ protected String getStringValue(DataSetRow row, DataSetColumn column) { @Override public void render(ReportData results, String argument, OutputStream out) throws IOException, RenderingException { + render(results, argument, out, null); + } + + public void render(ReportData results, String argument, OutputStream out, byte[] defaultLogoBytes) throws IOException, RenderingException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { @@ -139,7 +143,7 @@ public void render(ReportData results, String argument, OutputStream out) throws Element templatePIDElement = createStickerTemplate(doc); // Handle header configuration - configureHeader(doc, templatePIDElement); + configureHeader(doc, templatePIDElement, defaultLogoBytes); // Process data set fields processDataSetFields(results, doc, templatePIDElement); @@ -212,11 +216,11 @@ private Element createStickerTemplate(Document doc) { return templatePIDElement; } - private void configureHeader(Document doc, Element templatePIDElement) { + private void configureHeader(Document doc, Element templatePIDElement, byte[] defaultLogoBytes) { Element header = doc.createElement("header"); // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); - configureLogo(doc, header, logoUrlPath); + configureLogo(doc, header, logoUrlPath, defaultLogoBytes); boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); if (useHeader) { @@ -250,7 +254,7 @@ private void configureHeader(Document doc, Element templatePIDElement) { * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) * @throws RenderingException if no valid logo can be found */ - private void configureLogo(Document doc, Element header, String logoUrlPath) { + private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { String logoPath = ""; try { @@ -267,12 +271,9 @@ private void configureLogo(Document doc, Element header, String logoUrlPath) { } // 2. Fall back to cached logo - if (isBlank(logoPath)) { - File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); - File cachedLogo = new File(appDataDir, "patientdocuments_logo_cache.png"); - if (cachedLogo.exists() && cachedLogo.canRead()) { - logoPath = cachedLogo.getAbsolutePath(); - } + if (isBlank(logoPath) && defaultLogoBytes != null && defaultLogoBytes.length > 0) { + String base64Image = java.util.Base64.getEncoder().encodeToString(defaultLogoBytes); + logoPath = "data:image/png;base64," + base64Image; } } catch (Exception e) { throw new RenderingException("Failed to configure logo", e); diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java index ece961f..fb3eb02 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java @@ -58,12 +58,12 @@ public class PatientIdStickerPdfReport { @Autowired private InitializerService initializerService; - public byte[] generatePdf(Patient patient) throws RuntimeException { + public byte[] generatePdf(Patient patient, byte[] defaultLogoBytes) throws RuntimeException { validatePatientAndPrivileges(patient); try { ReportData reportData = createReportData(patient); - byte[] xmlBytes = renderReportToXml(reportData); + byte[] xmlBytes = renderReportToXml(reportData, defaultLogoBytes); return transformXmlToPdf(xmlBytes); } catch (Exception e) { @@ -95,10 +95,10 @@ private ReportData createReportData(Patient patient) throws EvaluationException return reportData; } - private byte[] renderReportToXml(ReportData reportData) throws IOException { + private byte[] renderReportToXml(ReportData reportData, byte[] defaultLogoBytes) throws IOException { PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); try (ByteArrayOutputStream xmlOutputStream = new ByteArrayOutputStream()) { - renderer.render(reportData, null, xmlOutputStream); + renderer.render(reportData, null, xmlOutputStream, defaultLogoBytes); return xmlOutputStream.toByteArray(); } } diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java index 236f7fd..79dd159 100644 --- a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java +++ b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java @@ -34,7 +34,7 @@ public void setup() throws Exception { public void generatePdf_shouldThrowWhenPatientIsMissing() throws Exception { Patient badPatient = null; Assertions.assertThrows(IllegalArgumentException.class, () -> { - pdfReport.generatePdf(badPatient); + pdfReport.generatePdf(badPatient, null); }); } diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index b6921f0..2a8b37b 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -12,7 +12,6 @@ import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.PATIENT_ID_STICKER_ID; import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -20,6 +19,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.io.IOUtils; import org.openmrs.Patient; import org.openmrs.api.PatientService; import org.openmrs.module.patientdocuments.reports.PatientIdStickerPdfReport; @@ -37,9 +37,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.openmrs.util.OpenmrsUtil; -import java.io.File; -import java.io.FileOutputStream; @Controller @RequestMapping(value = "/rest/" + RestConstants.VERSION_1 + "/" + MODULE_ARTIFACT_ID + "/" + PATIENT_ID_STICKER_ID) @@ -48,7 +45,7 @@ public class PatientIdStickerDataPdfExportController extends BaseRestController private static final Logger logger = LoggerFactory.getLogger(PatientIdStickerDataPdfExportController.class); private static final String DEFAULT_LOGO_CLASSPATH = "/images/openmrs_logo_white_large.png"; - + private PatientIdStickerPdfReport pdfReport; private PatientService ps; @@ -62,9 +59,8 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - loadAndCacheDefaultLogo(servletContext); - - byte[] pdfBytes = pdfReport.generatePdf(patient); + byte[] defaultLogoBytes =loadAndCacheDefaultLogo(servletContext); + byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/pdf"); @@ -81,32 +77,24 @@ private ResponseEntity writeResponse(Patient patient, boolean inline, Se } } - private void loadAndCacheDefaultLogo(ServletContext servletContext) { + private byte[] loadAndCacheDefaultLogo(ServletContext servletContext) { if (servletContext == null) { - return; + return null; } - File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); - File cachedLogo = new File(appDataDir, "patientdocuments_logo_cache.png"); - if (cachedLogo.exists() && cachedLogo.canRead()) { - return; - } - try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { if (logoStream == null) { logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); - return; + return null; } - try (FileOutputStream fos = new FileOutputStream(cachedLogo)) { - OpenmrsUtil.copyFile(logoStream, fos); - logger.info("Successfully cached logo to: {}", cachedLogo.getAbsolutePath()); - } + byte[] logoBytes = IOUtils.toByteArray(logoStream); + logger.info("Successfully cached logo in memory ({} bytes)", logoBytes.length); + return logoBytes; } catch (IOException e) { - logger.error("Failed to cache logo from: {}", DEFAULT_LOGO_CLASSPATH, e); - } catch (Exception e) { - logger.warn("Unable to load and cache default logo", e); + logger.error("Failed to load logo from: {}", DEFAULT_LOGO_CLASSPATH, e); + return null; } } From 16233d6d7d1447cfc33f5276f5a34bb8fce73fb5 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Thu, 30 Oct 2025 15:36:10 +0300 Subject: [PATCH 09/14] remove ability to cache logo --- .../renderer/PatientIdStickerXmlReportRenderer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index d4ca626..d352a1a 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -272,7 +273,7 @@ private void configureLogo(Document doc, Element header, String logoUrlPath, byt // 2. Fall back to cached logo if (isBlank(logoPath) && defaultLogoBytes != null && defaultLogoBytes.length > 0) { - String base64Image = java.util.Base64.getEncoder().encodeToString(defaultLogoBytes); + String base64Image = Base64.getEncoder().encodeToString(defaultLogoBytes); logoPath = "data:image/png;base64," + base64Image; } } catch (Exception e) { From 317aef0f0945eb07272c503de284701d7817bf54 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Thu, 30 Oct 2025 16:35:36 +0300 Subject: [PATCH 10/14] update documentation to remove caching --- .../renderer/PatientIdStickerXmlReportRenderer.java | 2 +- .../controller/PatientIdStickerDataPdfExportController.java | 5 ++--- readme/PatientIdSticker.md | 3 +-- readme/PatientIdStickerXSL.md | 5 ++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index d352a1a..9b1869f 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -271,7 +271,7 @@ private void configureLogo(Document doc, Element header, String logoUrlPath, byt } } - // 2. Fall back to cached logo + // 2. Fall back to default logo if (isBlank(logoPath) && defaultLogoBytes != null && defaultLogoBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(defaultLogoBytes); logoPath = "data:image/png;base64," + base64Image; diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 2a8b37b..07f2e98 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -59,7 +59,7 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - byte[] defaultLogoBytes =loadAndCacheDefaultLogo(servletContext); + byte[] defaultLogoBytes =loadDefaultLogo(servletContext); byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); HttpHeaders headers = new HttpHeaders(); @@ -77,7 +77,7 @@ private ResponseEntity writeResponse(Patient patient, boolean inline, Se } } - private byte[] loadAndCacheDefaultLogo(ServletContext servletContext) { + private byte[] loadDefaultLogo(ServletContext servletContext) { if (servletContext == null) { return null; } @@ -89,7 +89,6 @@ private byte[] loadAndCacheDefaultLogo(ServletContext servletContext) { } byte[] logoBytes = IOUtils.toByteArray(logoStream); - logger.info("Successfully cached logo in memory ({} bytes)", logoBytes.length); return logoBytes; } catch (IOException e) { diff --git a/readme/PatientIdSticker.md b/readme/PatientIdSticker.md index 5a77aea..60171a7 100644 --- a/readme/PatientIdSticker.md +++ b/readme/PatientIdSticker.md @@ -93,7 +93,6 @@ The module supports internationalization through message properties. Field label - The secondary identifier type can be configured using `report.patientIdSticker.fields.identifier.secondary.type` if needed. - Available stylesheets include `patientIdStickerFopStylesheet.xsl` (default) and `msfStickerFopStylesheet.xsl` for MSF-specific layouts. - Logo resolution rules: - - If `report.patientIdSticker.logourl` starts with `http` or `https`, the logo is downloaded once and cached. - If `report.patientIdSticker.logourl` is a relative path, it is resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `/openmrs/data/my_custom_logo.png`). - - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context and cached into `OPENMRS_APPLICATION_DATA_DIRECTORY`. + - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context. - The barcode is generated from the preferred patient identifier when barcode is enabled. \ No newline at end of file diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index 2cbd340..ce70f02 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -165,7 +165,7 @@ The optional header section can contain: - Custom header text on the right - Logo handling behavior: - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` - - If none configured or missing, the default OpenMRS logo is loaded from the servlet context and cached under `OPENMRS_APPLICATION_DATA_DIRECTORY` + - If none configured or missing, the default OpenMRS logo is loaded from the servlet context. ### Internationalization Section @@ -207,14 +207,13 @@ The stylesheet includes several responsive design elements: - **Demographic Grouping**: In MSF layout, groups Gender, DOB, and Age fields in a single row - **Secondary Identifier**: Special handling for secondary patient identifiers - **Internationalization**: Support for translated field labels and messages -- **Logo Handling**: Automatic download and caching of logos from HTTP URLs +- **Logo Handling**: Pulled from the `OPENMRS_APPLICATION_DATA_DIRECTORY` or servlet context for the default OpenMRS logo. ## Technical Requirements - **XSL-FO Processor**: Compatible with Apache FOP or similar XSL-FO processors - **Barcode4J Library**: Required for barcode generation - **Fonts**: Requires IBM Plex Sans Arabic and IBM Plex Sans Arabic Bold (or configured alternatives) -- **Network Access**: Required for logo downloading from HTTP URLs ## Examples From 64276740c733583d5bada62642b54a8c53c2e280 Mon Sep 17 00:00:00 2001 From: jnsereko <58003327+jnsereko@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:16:52 +0300 Subject: [PATCH 11/14] Fix formatting of loadDefaultLogo call --- .../controller/PatientIdStickerDataPdfExportController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 07f2e98..2e867bd 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -59,7 +59,7 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { try { - byte[] defaultLogoBytes =loadDefaultLogo(servletContext); + byte[] defaultLogoBytes = loadDefaultLogo(servletContext); byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); HttpHeaders headers = new HttpHeaders(); From aa3aac8ffbe272f65849cb58d78cc36c27fea5c6 Mon Sep 17 00:00:00 2001 From: jnsereko <58003327+jnsereko@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:21:03 +0300 Subject: [PATCH 12/14] Simplify logo loading by removing unnecessary variable --- .../controller/PatientIdStickerDataPdfExportController.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 2e867bd..62db228 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -87,10 +87,7 @@ private byte[] loadDefaultLogo(ServletContext servletContext) { logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); return null; } - - byte[] logoBytes = IOUtils.toByteArray(logoStream); - return logoBytes; - + return IOUtils.toByteArray(logoStream); } catch (IOException e) { logger.error("Failed to load logo from: {}", DEFAULT_LOGO_CLASSPATH, e); return null; From 85f8aa80d6aaeb6a6068226f5a5819349a347164 Mon Sep 17 00:00:00 2001 From: jnsereko Date: Mon, 3 Nov 2025 17:26:46 +0300 Subject: [PATCH 13/14] update documentation to show support for both absolute paths and base64 data URI --- .../PatientIdStickerXmlReportRenderer.java | 24 +++++++++---------- readme/PatientIdSticker.md | 1 + readme/PatientIdStickerXSL.md | 3 ++- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index 9b1869f..2b543ef 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -243,18 +243,18 @@ private void configureHeader(Document doc, Element templatePIDElement, byte[] de templatePIDElement.appendChild(i18nStrings); } - /** - * Configures the logo for the sticker document. - * - * Logo resolution priority: - * 1. Custom logo from file system (absolute or relative path) - * 2. Default OpenMRS logo from classpath - * - * @param doc The XML document - * @param header The header element to append the logo to - * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) - * @throws RenderingException if no valid logo can be found - */ + /** + * Configures the logo for the sticker document. + * + * Logo resolution priority: + * 1. Custom logo from absolute filesystem path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} + * 2. Default OpenMRS logo as base64 data URI + * + * @param doc The XML document + * @param header The header element to append the logo to + * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) + * @throws RenderingException if no valid logo can be found + */ private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { String logoPath = ""; diff --git a/readme/PatientIdSticker.md b/readme/PatientIdSticker.md index 60171a7..5d43193 100644 --- a/readme/PatientIdSticker.md +++ b/readme/PatientIdSticker.md @@ -95,4 +95,5 @@ The module supports internationalization through message properties. Field label - Logo resolution rules: - If `report.patientIdSticker.logourl` is a relative path, it is resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `/openmrs/data/my_custom_logo.png`). - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context. + - Supported formats: an absolute filesystem path (for a custom logo) or a base64-encoded data URI (for the default OpenMRS logo), both of which are accepted by the renderer/XSL-FO processor. - The barcode is generated from the preferred patient identifier when barcode is enabled. \ No newline at end of file diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index ce70f02..4d9173f 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -166,6 +166,7 @@ The optional header section can contain: - Logo handling behavior: - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` - If none configured or missing, the default OpenMRS logo is loaded from the servlet context. + - Supported formats: absolute filesystem path (custom logo) or base64 data URI (default logo). The XSL-FO processor can handle both. ### Internationalization Section @@ -277,6 +278,6 @@ The stylesheet includes several responsive design elements: - Configuration is managed through the Initializer module - Field visibility is controlled by boolean configuration properties - Secondary identifier type is specified by UUID in configuration -- Logo URLs must be HTTP URLs starting with "http" to be processed +- Logo input is either an absolute filesystem path or a base64-encoded data URI (e.g., `data:image/png;base64,...`). Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY`. - Barcode generation uses the preferred patient identifier - Multiple stickers can be generated based on the `pages` configuration From 71ad294c757ca514d2603ee0a05810ac49314bc7 Mon Sep 17 00:00:00 2001 From: jnsereko <58003327+jnsereko@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:40:22 +0300 Subject: [PATCH 14/14] Add logo element creation conditionally --- .../PatientIdStickerXmlReportRenderer.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index 2b543ef..21a547c 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -280,12 +280,14 @@ private void configureLogo(Document doc, Element header, String logoUrlPath, byt throw new RenderingException("Failed to configure logo", e); } - // Create and append logo elements - Element branding = doc.createElement("branding"); - Element image = doc.createElement("logo"); - image.setTextContent(logoPath); - branding.appendChild(image); - header.appendChild(branding); + // Create and append logo elements if valid + if (isNotBlank(logoPath)) { + Element branding = doc.createElement("branding"); + Element image = doc.createElement("logo"); + image.setTextContent(logoPath); + branding.appendChild(image); + header.appendChild(branding); + } } private Map createConfigKeyMap() {