Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
*/
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.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -46,6 +48,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.OpenmrsUtil;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
Expand Down Expand Up @@ -113,6 +116,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 {
Expand All @@ -137,7 +144,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);
Expand Down Expand Up @@ -210,13 +217,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");
if (isNotBlank(logoUrlPath)) {
configureLogo(doc, header, logoUrlPath);
}
configureLogo(doc, header, logoUrlPath, defaultLogoBytes);

boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header"));
if (useHeader) {
Expand All @@ -238,17 +243,44 @@ private void configureHeader(Document doc, Element templatePIDElement) {
templatePIDElement.appendChild(i18nStrings);
}

private void configureLogo(Document doc, Element header, String logoUrlPath) {
String logoPath;
File logoFile = new File(logoUrlPath);
boolean isValidFile = logoFile.exists() && logoFile.canRead() && logoFile.isAbsolute();
/**
* 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 = "";

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();
}
}

if (isValidFile) {
logoPath = logoFile.getAbsolutePath();
} else {
throw new RenderingException("Logo file not found or not accessible: " + logoUrlPath);
// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when the logo is not a png?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @dkayiwa. This is the default fallback logo, which we hardcoded to a PNG file at DEFAULT_LOGO_CLASSPATH = "/images/openmrs_logo_white_large.png"
Are we expecting the default logo format to change from PNG to something else? If so, I can make this more dynamic, but otherwise the hardcoded MIME type should match the hardcoded PNG file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when someone's config has a JPG?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user-supplied logo will be retrieved using its absolute path (step 1 in configureLogo()) before falling back to the default logo. So if someone configures a JPG in their report.patientIdSticker.logourl setting, the absolute file path to that JPG will be used directly. The MIME type image/png only applies to our hardcoded default fallback logo (openmrs_logo_white_large.png), not to user-supplied images.

}
} catch (Exception e) {
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is displayed for the image when logoPath is an empty string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have conditionally added logo creation.

Expand Down Expand Up @@ -365,15 +397,16 @@ && shouldIncludeColumn("patientdocuments.patientIdSticker.fields.secondaryIdenti

// Process address
if (shouldIncludeColumn("patientdocuments.patientIdSticker.fields.fulladdress")) {
Map<String, String> addressData = (Map<String, String>) patientData.get("preferredAddress");
if (addressData != null) {
List<Map<String, String>> addressData = (List<Map<String, String>>) patientData.get("addresses");
if (addressData != null && !addressData.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also related to the logo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No! This ensures that the address data is pulled from the right variable in the Patient Data object.
Since its a single line of change and related to intended field field not showing, i thought its better merging the work with the logo not showing.

Map<String, String> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
}
Expand Down
8 changes: 4 additions & 4 deletions api/src/main/resources/msfStickerFopStylesheet.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<fo:layout-master-set>
<fo:simple-page-master master-name="sticker"
page-width="{$sticker-width}" page-height="{$sticker-height}"
margin="1.5mm">
margin="3mm">
<fo:region-body margin="0"/>
</fo:simple-page-master>
</fo:layout-master-set>
Expand Down Expand Up @@ -227,7 +227,7 @@
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="fields/field[@label = $patientIdKey]/@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="fields/field[@label = $patientIdKey]"/>
</fo:block>
</fo:block-container>
Expand All @@ -238,7 +238,7 @@
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="fields/field[@label = $patientNameKey]/@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="fields/field[@label = $patientNameKey]"/>
</fo:block>
</fo:block-container>
Expand All @@ -255,7 +255,7 @@
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="."/>
</fo:block>
</fo:block-container>
Expand Down
34 changes: 22 additions & 12 deletions api/src/main/resources/patientIdStickerFopStylesheet.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- Attribute for dynamic page height and width -->
<xsl:variable name="sticker-height" select="/patientIdStickers/@sticker-height"/>
<xsl:variable name="sticker-width" select="/patientIdStickers/@sticker-width"/>

<!-- Calculate effective body height (page height minus margins) -->
<xsl:variable name="body-height" select="concat(number(substring-before($sticker-height, 'mm')) - 4, 'mm')"/>
<xsl:variable name="body-height-value" select="number(substring-before($sticker-height, 'mm')) - 4"/>
Expand Down Expand Up @@ -80,7 +80,7 @@
<fo:layout-master-set>
<fo:simple-page-master master-name="sticker"
page-width="{$sticker-width}" page-height="{$sticker-height}"
margin="1.5mm">
margin="3mm">
<fo:region-body margin="0"/>
</fo:simple-page-master>
</fo:layout-master-set>
Expand Down Expand Up @@ -211,6 +211,17 @@

<!-- Main data section -->
<fo:block-container height="{$table-height}" display-align="before">
<xsl:if test="fields/field[@label = $patientSecondaryIdKey]">
<fo:block-container margin-bottom="{$field-vertical-gap}">
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="fields/field[@label = $patientSecondaryIdKey]/@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="fields/field[@label = $patientSecondaryIdKey]"/>
</fo:block>
</fo:block-container>
</xsl:if>

<xsl:if test="fields/field[@label = $patientIdKey]">
<fo:block-container margin-bottom="{$field-vertical-gap}">
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
Expand All @@ -235,17 +246,16 @@

<xsl:for-each select="fields/field[
@label != $patientNameKey and
@label != $patientSecondaryIdKey and
@label != $patientIdKey]">
<xsl:if test="not(preceding-sibling::field[@label = current()/@label])">
<fo:block-container margin-bottom="{$field-vertical-gap}">
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="."/>
</fo:block>
</fo:block-container>
</xsl:if>
<fo:block-container margin-bottom="{$field-vertical-gap}">
<fo:block font-size="{$label-font-size}pt" font-weight="normal" font-family="{$label-font-family}" color="#444444">
<xsl:value-of select="@label"/>
</fo:block>
<fo:block font-size="{$title-font-size}pt" font-weight="bold" font-family="{$value-font-family}" margin-top="0.2mm">
<xsl:value-of select="."/>
</fo:block>
</fo:block-container>
</xsl:for-each>
</fo:block-container>
</fo:block-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand Down
7 changes: 7 additions & 0 deletions omod/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<xmlgraphicsVersion>2.9</xmlgraphicsVersion>
<pdfboxVersion>2.0.32</pdfboxVersion>
<fasterxmlJacksonVersion>2.17.2</fasterxmlJacksonVersion>
<javaxServletVersion>3.1.0</javaxServletVersion>
</properties>

<dependencies>
Expand Down Expand Up @@ -116,6 +117,12 @@
<version>${fasterxmlJacksonVersion}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javaxServletVersion}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
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.IOException;
import java.io.InputStream;

import javax.servlet.ServletContext;
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;
Expand All @@ -37,7 +43,9 @@
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;
Expand All @@ -49,9 +57,10 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati
this.pdfReport = pdfReport;
}

private ResponseEntity<byte[]> writeResponse(Patient patient, boolean inline) {
private ResponseEntity<byte[]> 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");
Expand All @@ -67,9 +76,27 @@ private ResponseEntity<byte[]> writeResponse(Patient patient, boolean inline) {
.body("Error generating PDF".getBytes());
}
}

private byte[] loadDefaultLogo(ServletContext servletContext) {
if (servletContext == null) {
return null;
}

try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) {
if (logoStream == null) {
logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH);
return null;
}
return IOUtils.toByteArray(logoStream);
} catch (IOException e) {
logger.error("Failed to load logo from: {}", DEFAULT_LOGO_CLASSPATH, e);
return null;
}
}

@RequestMapping(method = RequestMethod.GET)
public ResponseEntity<byte[]> getPatientIdSticker(HttpServletResponse response,
HttpServletRequest request,
@RequestParam(value = "patientUuid") String patientUuid,
@RequestParam(value = "inline", required = false, defaultValue = "true") boolean inline) {

Expand All @@ -79,6 +106,7 @@ public ResponseEntity<byte[]> getPatientIdSticker(HttpServletResponse response,
return null;
}

return writeResponse(patient, inline);
ServletContext servletContext = request.getSession().getServletContext();
return writeResponse(patient, inline, servletContext);
}
}
Loading