package net.gepafin.tendermanagement.dao; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.itextpdf.kernel.colors.ColorConstants; import com.itextpdf.kernel.colors.DeviceRgb; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.layout.properties.UnitValue; import com.itextpdf.layout.renderer.CellRenderer; import com.itextpdf.layout.renderer.DrawContext; import com.itextpdf.text.*; import com.itextpdf.text.Element; import com.itextpdf.text.Font; import com.itextpdf.text.Image; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.*; import jakarta.servlet.http.HttpServletRequest; import net.gepafin.tendermanagement.config.Translator; import net.gepafin.tendermanagement.constants.GepafinConstant; import net.gepafin.tendermanagement.entities.*; import net.gepafin.tendermanagement.model.request.CustomPageEvent; import net.gepafin.tendermanagement.model.request.FieldLabelValuePairRequest; import net.gepafin.tendermanagement.model.response.*; import net.gepafin.tendermanagement.repositories.ApplicationRepository; import net.gepafin.tendermanagement.service.CallService; import net.gepafin.tendermanagement.util.Validator; import net.gepafin.tendermanagement.web.rest.api.errors.ResourceNotFoundException; import net.gepafin.tendermanagement.web.rest.api.errors.Status; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.itextpdf.layout.element.Table; import com.itextpdf.layout.element.Cell; //import com.itextpdf.layout.element. import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.*; import java.util.List; import java.util.stream.Collectors; @Component public class PdfDao { @Autowired private CallService callService; @Autowired private ApplicationDao applicationDao; @Autowired private Validator validator; public byte[] generatePdf(HttpServletRequest request,Long applicationId) { try { UserEntity userEntity = validator.validateUser(request); ApplicationEntity applicationEntity = applicationDao.validateApplication(applicationId); validator.validateUserWithCompany(request, applicationEntity.getCompany().getId()); CallEntity call=callService.validateCall(applicationEntity.getCall().getId()); // Create a byte stream to hold the PDF ByteArrayOutputStream out = new ByteArrayOutputStream(); float leftMargin = 50f; // Adjust this for the left margin Document document = new Document(PageSize.A4, leftMargin, 36f, 50f, 35); PdfWriter writer = PdfWriter.getInstance(document, out); // CustomPageEvent pageEvent = new CustomPageEvent(call.getName(), 0); // writer.setPageEvent(pageEvent); document.open(); // pageEvent.setTotalPages(writer.getPageNumber()); addLogo(document, "https://mementoresources.s3.eu-west-1.amazonaws.com/gepafin/logo.jpg"); // Add your image path here BaseColor customColor = new BaseColor(0, 128, 0); // Adjust RGB values as needed // Define fonts and styles BaseColor greenColor = new BaseColor(0, 128, 0); // Adjust RGB values as needed BaseColor darkGreenColor = new BaseColor(1, 50, 32); // Adjust RGB values as needed Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, customColor); Font sectionFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12,darkGreenColor); Font labelFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12,new BaseColor(113,121,126)); // Light grey); Font smallFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 8,new BaseColor(105, 105, 105)); Font valueFont=FontFactory.getFont(FontFactory.HELVETICA_BOLD,10,new BaseColor(178, 190, 181)); Paragraph title = new Paragraph(call.getName(), titleFont); title.setAlignment(Element.ALIGN_LEFT); document.add(title); BaseColor greyColor=new BaseColor(178, 190, 181); // Very light grey color addColoredLines(writer,document,greyColor); document.add(new Paragraph(" ")); // Application ID section (Centered) // pageEvent.setTotalPages(writer.getPageNumber()); String protocolNumber="XX00"; if(applicationEntity.getProtocol()!=null) { protocolNumber= String.valueOf(applicationEntity.getProtocol().getProtocolNumber()); } Paragraph appId = new Paragraph("ID domanda :" +protocolNumber); appId.setAlignment(Element.ALIGN_RIGHT); document.add(appId); document.add(new Paragraph(" ")); addColoredLines(writer,document,greenColor); document.add(new Paragraph(" ")); document.add(new Paragraph("\n")); // Add line break // String companyName= companyEntity.getCompanyName(); // String vatNumber=companyEntity.getVatNumber(); // String address=companyEntity.getAddress(); // // Section: Dati Anagrafici Azienda // document.add(new Paragraph("Dati Anagrafici Azienda", sectionFont)); // addLabelValuePair(document, "Codice ATECO", "SEZIONE C “ATTIVITÀ MANUFATTURIERE”", regularFont); // addLabelValuePair(document, "Ragione Sociale", companyName, regularFont); // addLabelValuePair(document, "Partita IVA", vatNumber, regularFont); // addLabelValuePair(document, "Indirizzo sede Legale", address, regularFont); // // document.add(new Paragraph("\n")); // Add line break // // // Section: Domanda presentata da // document.add(new Paragraph("Domanda presentata da:", sectionFont)); // addLabelValuePair(document, "Nome e cognome", userEntity.getBeneficiary().getFirstName()+" "+userEntity.getBeneficiary().getLastName(), regularFont); // addLabelValuePair(document, "Codice fiscale", userEntity.getBeneficiary().getCodiceFiscale(), regularFont); // addLabelValuePair(document, "Telefono", userEntity.getBeneficiary().getPhoneNumber(), regularFont); // addLabelValuePair(document, "Email", userEntity.getBeneficiary().getEmail(), regularFont); // addLabelValuePair(document, "Con il titolo di", "Rappresentante legale", regularFont); document.add(new Paragraph(" ")); ApplicationGetResponseBean applicationGetResponseBean=applicationDao.getApplicationByFormId(applicationId,null, userEntity); for(FormApplicationResponse formApplicationResponse: applicationGetResponseBean.getForm()) { document.add(new Paragraph(formApplicationResponse.getLabel(),sectionFont)); document.add(new Paragraph(" ")); // Add line break List fieldLabelValuePairRequests = getFormFieldsToLabels(formApplicationResponse); for (FieldLabelValuePairRequest pair : fieldLabelValuePairRequests) { String label = pair.getLabel(); Object value = pair.getValue(); Integer pages=0; pages=addLabelValuePair(writer,document, label, value, labelFont,valueFont,call.getName(),pages); if(pages !=0 ){ // pageEvent.setTotalPages(writer.getPageNumber()); } } addColoredLines(writer,document,greenColor); document.add(new Paragraph(" ")); // Add line break } document.add(new Paragraph("\n")); // Add line break Font boldSmallFont = new Font(Font.FontFamily.HELVETICA, 10, Font.BOLD,new BaseColor(105, 105, 105)); // Adding the "Documenti Allegati" section title document.add(new Paragraph(" ")); // pageEvent.setTotalPages(writer.getPageNumber()); document.newPage(); // pageEvent.setTotalPages(writer.getPageNumber()); document.add(new Paragraph("Documenti Allegati", sectionFont)); document.add(new Paragraph(" ")); // 1. Autocertificazione possesso Requisiti Paragraph p1 = new Paragraph(); p1.add(new Chunk("1. ", boldSmallFont)); p1.add(new Chunk("Autocertificazione possesso Requisiti ", boldSmallFont)); p1.add(new Chunk("ai sensi degli artt. 46 e 47 del DPR 445/2000", smallFont)); document.add(p1); document.add(new Paragraph(" ")); // 2. Informativa Privacy relativa al trattamento dei dati personali Paragraph p2 = new Paragraph(); p2.add(new Chunk("2. ", boldSmallFont)); p2.add(new Chunk("Informativa Privacy relativa al trattamento dei dati personali", boldSmallFont)); document.add(p2); document.add(new Paragraph(" ")); // 3. Dati richiesti per la valutazione dell’adeguatezza dei flussi finanziari Paragraph p3 = new Paragraph(); p3.add(new Chunk("3. ", boldSmallFont)); p3.add(new Chunk("Dati richiesti per la valutazione dell’adeguatezza dei flussi finanziari prospettici come da tabella di cui all’Appendice 9", boldSmallFont)); document.add(p3); document.add(new Paragraph(" ")); // 4. Rilevazione Centrale dei Rischi Paragraph p4 = new Paragraph(); p4.add(new Chunk("4. ", boldSmallFont)); p4.add(new Chunk("Rilevazione Centrale dei Rischi riferita agli ultimi 36 mesi disponibili alla data di presentazione della Domanda", boldSmallFont)); document.add(p4); document.add(new Paragraph(" ")); // 5. Schema di presentazione dei dati di bilancio Paragraph p5 = new Paragraph(); p5.add(new Chunk("5. ", boldSmallFont)); p5.add(new Chunk("Schema di presentazione dei dati di bilancio", boldSmallFont)); document.add(p5); document.add(new Paragraph(" ")); // 6. Dettagli bilanci in forma abbreviata Paragraph p6 = new Paragraph(); p6.add(new Chunk("6. ", boldSmallFont)); p6.add(new Chunk("Dettagli bilanci in forma abbreviata", boldSmallFont)); document.add(p6); document.add(new Paragraph(" ")); // 7. Relazione aziendale illustrativa Paragraph p7 = new Paragraph(); p7.add(new Chunk("7. ", boldSmallFont)); p7.add(new Chunk("Relazione aziendale illustrativa", boldSmallFont)); document.add(p7); document.add(new Paragraph(" ")); addColoredLines(writer,document,greenColor); // System.out.println(writer.getPageSize()); // System.out.println(document.getPageSize()); // System.out.println(document.getPageNumber()); // System.out.println(writer.getPageNumber()); // document.setPageCount(100); // document.setPageCount(writer.getPageNumber()); // System.out.println(document.getPageNumber()); // Close the document document.close(); // Convert to byte array for response byte[] pdfBytes =PdfPageNumberInserter.addPageNumbers(out.toByteArray()); return pdfBytes; } catch (Exception e) { e.printStackTrace(); } return null; } private Integer addLabelValuePair(PdfWriter writer,Document document, String label, Object value, Font labelFont,Font valueFont,String title,Integer totalPages) throws DocumentException { // Add label Paragraph labelParagraph = new Paragraph(label, labelFont); document.add(labelParagraph); float leftMargin = 20f; PdfContentByte canvas = writer.getDirectContent(); // Setting the color and width of the line float lineWidth = 1.0f; // Thickness of the line canvas.setLineWidth(lineWidth); // Get the current vertical position in the document float yPos = writer.getVerticalPosition(true) - 10f; // Adjust this to move line slightly below current content // Define start and end points for the line (relative to the page size and margins) if (yPos <= 140) { // If xEnd is less than or equal to 200, generate a new page totalPages++; document.newPage(); } // Add a gap between the label and value document.add(new Paragraph(" ")); // Adding an empty paragraph for spacing // Create value cell with rounded corners PdfPTable valueTable = new PdfPTable(1); valueTable.setWidthPercentage(100); if (value instanceof List) { // Further check if the list contains Strings List list = (List) value; if (!list.isEmpty() && list.get(0) instanceof String) { // Cast to List List values = (List) value; // Loop through the list of strings and create a cell for each string for (String item : values) { PdfPCell valueCell = new PdfPCell(new Phrase(item, valueFont)); valueCell.setPadding(5f); // Increase padding for better spacing valueCell.setPaddingLeft(leftMargin); // Increase left margin for value valueCell.setBorder(Rectangle.NO_BORDER); // Remove border for value cell valueCell.setMinimumHeight(30f); valueCell.setVerticalAlignment(Element.ALIGN_MIDDLE); valueCell.setCellEvent(new RoundedCorners()); // Apply rounded corners // Add the cell to the table valueTable.addCell(valueCell); } // Finally, add the table to the document document.add(valueTable); } else { boolean containsThreeValues = false; // Variable to track if any map contains three keys List> dataList = (List>) value; // Cast Object to List of Maps for (Map entry : dataList) { if (entry.size() == 3) { // Check if the current map has three keys containsThreeValues = true; // If found, set the variable to true break; // No need to check further, exit loop } } List> extractedData = new ArrayList<>(); // To hold extracted data for (Map entry : dataList) { Map extractedMap = new HashMap<>(); // To hold the current extracted row of data List keys = new ArrayList<>(entry.keySet()); // Get all keys in the current map // Handle based on the number of keys in the map if (Boolean.FALSE.equals(containsThreeValues) && keys.size() == 2) { // Treat the first key as the "key" and second key as the "value" String heading = (String) entry.get(keys.get(0)); // Get value of first key String value1 = (String) entry.get(keys.get(1)); // Get value of second key extractedMap.put(heading,value1); // Store the first key's value as "heading" } if (Boolean.TRUE.equals(containsThreeValues) ) { String amount=""; // Treat the first as number, second as description, third as amount if(keys.size()==3){ amount = (String) entry.get(keys.get(2)); // Third key's value } String number = (String) entry.get(keys.get(0)); // First key's value String description = (String) entry.get(keys.get(1)); // Second key's value // Store the combined result as a value in the map, with a suitable key String combinedValue = number + "; " + description + "; " + amount; // Concatenate them as a single value extractedMap.put("combined", combinedValue); // Store as a single entry, key as "combined" } extractedData.add(extractedMap); // Add each extracted map to the list } document=createPdfTable(extractedData,document); } } else { PdfPCell valueCell = new PdfPCell(new Phrase(String.valueOf(value), valueFont)); valueCell.setPadding(5f); // Increase padding for better spacing valueCell.setPaddingLeft(leftMargin); // Increase left margin for value valueCell.setBorder(Rectangle.NO_BORDER); // Remove border for value cell valueCell.setMinimumHeight(30f); valueCell.setVerticalAlignment(Element.ALIGN_MIDDLE); valueCell.setCellEvent(new RoundedCorners()); // Apply rounded corners valueTable.addCell(valueCell); document.add(valueTable); } document.add(new Paragraph("\n")); // Add line break after each value return totalPages; } private Document createPdfTable(List> extractedData,Document document) throws DocumentException { // Create a PdfPTable with 2 columns PdfPTable table = new PdfPTable(2); // Initial assumption for 2 columns table.setWidthPercentage(100); // Set table width to 100% table.setTableEvent(new RoundedBorderEvent()); Font textFont = FontFactory.getFont(FontFactory.HELVETICA, 12, Font.NORMAL, new BaseColor(105, 105, 105)); // Gray text boolean combinedHeaderAdded = false; // Flag to track if headers for combined have been added float rowHeight = 50f; // Example row height, adjust as necessary float maxTableHeight = 700f; // Maximum height of the table before a page break float[] columnWidths = {0.7f, 0.3f}; table.setWidths(columnWidths); // Add table header // Populate the table with extracted data and style rows for (Map row : extractedData) { for (Map.Entry entry : row.entrySet()) { String key = entry.getKey(); // This will give you the key String value = entry.getValue(); // This will give you the value // Check if the current entry is for the combined section if ("combined".equals(key)) { // Ensure the combined header is added only once if (!combinedHeaderAdded) { // Create a new table for combined entries table = new PdfPTable(3); // 3 columns for combined entries PdfPCell headerCell1 = new PdfPCell(new Phrase("Number")); headerCell1.setHorizontalAlignment(Element.ALIGN_CENTER); // Center align headerCell1.setVerticalAlignment(Element.ALIGN_MIDDLE); headerCell1.setBackgroundColor(new BaseColor(178, 190, 181)); // Light gray background for header table.addCell(headerCell1); PdfPCell headerCell2 = new PdfPCell(new Phrase("Details")); headerCell2.setHorizontalAlignment(Element.ALIGN_CENTER); // Center align headerCell2.setVerticalAlignment(Element.ALIGN_MIDDLE); headerCell2.setBackgroundColor(new BaseColor(178, 190, 181)); // Light gray background for header table.addCell(headerCell2); PdfPCell headerCell3 = new PdfPCell(new Phrase("Amount")); headerCell3.setHorizontalAlignment(Element.ALIGN_CENTER); // Center align headerCell3.setVerticalAlignment(Element.ALIGN_MIDDLE); headerCell3.setBackgroundColor(new BaseColor(178, 190, 181)); // Light gray background for header table.addCell(headerCell3); combinedHeaderAdded = true; // Mark header as added } // Split the value for "combined" into separate parts String[] combinedValues = value.split("; "); // Check if we have 3 parts (number, description, amount) String number = combinedValues[0]; // 1st part (number) String description = combinedValues[1]; // 2nd part (description) String amount = ""; if (combinedValues.length == 3) { amount = combinedValues[2]; // 3rd part (amount) } // Create PDF cells using the split values PdfPCell cellNumber = new PdfPCell(new Phrase(number, textFont)); // Cell for number PdfPCell cellDescription = new PdfPCell(new Phrase(description, textFont)); // Cell for description PdfPCell cellAmount = new PdfPCell(new Phrase(amount, textFont)); // Cell for amount // Set row background color for combined values cellNumber.setBackgroundColor(new BaseColor(239, 243, 248)); // Light blue for combined rows cellDescription.setBackgroundColor(new BaseColor(239, 243, 248)); cellAmount.setBackgroundColor(new BaseColor(239, 243, 248)); // Set cell height and add rounded borders // cellNumber.setFixedHeight(rowHeight); // cellDescription.setFixedHeight(rowHeight); // cellAmount.setFixedHeight(rowHeight); cellNumber.setMinimumHeight(20f); // Set minimum height for better appearance cellDescription.setMinimumHeight(20f); // Set minimum height for better appearance cellAmount.setMinimumHeight(20f); // Set minimum height for better appearance cellNumber.setPadding(7f); cellDescription.setPadding(7f); cellAmount.setPadding(7f); // Add the cells to the table only once table.addCell(cellNumber); table.addCell(cellDescription); table.addCell(cellAmount); } else { if (!combinedHeaderAdded) { // Create a new table for combined entries table= new PdfPTable(2); // 3 columns for combined entries table.setWidthPercentage(100); PdfPCell headerCell1 = new PdfPCell(new Phrase("Details")); headerCell1.setHorizontalAlignment(Element.ALIGN_CENTER); // Center align headerCell1.setVerticalAlignment(Element.ALIGN_MIDDLE); headerCell1.setBackgroundColor(new BaseColor(178, 190, 181)); // Light gray background for header table.addCell(headerCell1); PdfPCell headerCell2 = new PdfPCell(new Phrase("Amount")); headerCell2.setHorizontalAlignment(Element.ALIGN_CENTER); // Center align headerCell2.setVerticalAlignment(Element.ALIGN_MIDDLE); headerCell2.setBackgroundColor(new BaseColor(178, 190, 181)); // Light gray background for header table.addCell(headerCell2); combinedHeaderAdded=true; } // Add cells for regular key-value pairs without headers PdfPCell cellKey = new PdfPCell(new Phrase(key, textFont)); PdfPCell cellValue = new PdfPCell(new Phrase(value, textFont)); // Set background color for both cells cellKey.setBackgroundColor(new BaseColor(239, 243, 248)); // Light blue for other rows cellValue.setBackgroundColor(new BaseColor(239, 243, 248)); cellKey.setPadding(7f); cellValue.setPadding(7f); // Set cell height and add rounded borders cellKey.setFixedHeight(rowHeight); cellValue.setFixedHeight(rowHeight); // Add the cells to the table table.addCell(cellKey); table.addCell(cellValue); } if (table.getTotalHeight() + rowHeight > maxTableHeight) { // Start a new page if needed document.add(table); table = new PdfPTable(2); // Reset table for new page table.setWidthPercentage(100); // Reset width percentage combinedHeaderAdded = false; // Reset header flag } } } document.add(table); // Add the last table before returning // Check if adding a new row would exceed the maximum height // Return the populated table return document; } public static class RoundedBorderEvent implements PdfPTableEvent { @Override public void tableLayout(PdfPTable table, float[][] widths, float[] heights, int headerRows, int rowStart, PdfContentByte[] canvases) { PdfContentByte canvas = canvases[PdfPTable.BASECANVAS]; // Get the table boundaries float left = widths[0][0]; float right = widths[0][widths[0].length - 1]; float top = heights[0]; float bottom = heights[heights.length - 1]; // Define the corner radius float radius = 20f; // Draw a rounded rectangle around the table canvas.roundRectangle(left, bottom, right - left, top - bottom, radius); canvas.stroke(); } } public List getFormFieldsToLabels(FormApplicationResponse responseBean) { List labelValuePairs = new ArrayList<>(); // Iterate through each form in the application response List formFields = responseBean.getFormFields(); List contents = responseBean.getContent(); // Iterate through each formField in the current form for (ApplicationFormFieldResponseBean formField : formFields) { String fieldId = formField.getFieldId(); Object fieldValue = formField.getFieldValue(); // Find the content in the form that matches the fieldId Optional matchingContent = contents.stream() .filter(content -> content.getId().equals(fieldId)) .findFirst(); // If the content with the matching fieldId is found, create a label-value pair if (matchingContent.isPresent()) { String name = matchingContent.get().getName(); if (name.equals("fileupload")) { // Step 1: Check if fieldValue is an instance of List if (fieldValue instanceof List && ((List) fieldValue).stream().allMatch(item -> item instanceof DocumentResponseBean)) { // Step 2: Safely cast to List List documentList = (List) fieldValue; // Step 3: Extract names from the document list List names = documentList.stream() .map(DocumentResponseBean::getName) // Extract the name from each DocumentResponseBean .collect(Collectors.toList()); fieldValue=names; } } if(name.equals("checkboxes")) { List check = (List) fieldValue; List settingResponseBeans = matchingContent.get().getSettings(); for (SettingResponseBean settingResponseBean : settingResponseBeans) { // Initialize a list to hold matched labels for each SettingResponseBean List matchedLabels = new ArrayList<>(); if (settingResponseBean.getValue() instanceof List) { List valueList = (List) settingResponseBean.getValue(); if (!valueList.isEmpty() && valueList.get(0) instanceof Map) { // Cast to List> List> options = (List>) valueList; for (Map field : options) { for (String val : check) { String name1=field.get("name"); if (val.equals(name1)) { // Check if the key exists in the current field map String label = field.get("label"); // Extract the label if (field != null) { // Check if the value is not null matchedLabels.add(label); // Add the value to the matchedValues list } } } } fieldValue = matchedLabels; } } } } String label = matchingContent.get().getLabel(); // Add the label-value pair to the list if (fieldValue != null && !fieldValue.toString().trim().isEmpty()) { fieldValue = findLabelInOptions(matchingContent.get().getSettings(), fieldValue); labelValuePairs.add(new FieldLabelValuePairRequest(label, fieldValue)); } } } return labelValuePairs; } public static Object findLabelInOptions(List settings, Object valueToFind) { ObjectMapper objectMapper = new ObjectMapper(); try { if (valueToFind instanceof String) { String searchValue = (String) valueToFind; for (SettingResponseBean setting : settings) { Object value = setting.getValue(); if (value instanceof List) { List options = (List) value; for (Object option : options) { JsonNode optionNode = objectMapper.convertValue(option, JsonNode.class); if (optionNode.get("name").asText().equals(searchValue)) { return optionNode.get("label").asText(); } } } } } } catch (Exception e) { } return valueToFind; } public void addLogo(Document document, String logoPath) throws Exception { Image logo = Image.getInstance(logoPath); logo.scaleToFit(document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin(), // Fit to document width document.getPageSize().getHeight() / 4); // Adjust the height as needed (1/4th of the page height) logo.setAlignment(Image.ALIGN_CENTER); // Align logo to center document.add(logo); // Add some space after logo document.add(new Paragraph("\n")); // Adding space after the logo } public void addColoredLines(PdfWriter writer, Document document, BaseColor color){ PdfContentByte canvas = writer.getDirectContent(); // Setting the color and width of the line canvas.setColorStroke(color); float lineWidth = 1.0f; // Thickness of the line canvas.setLineWidth(lineWidth); // Get the current vertical position in the document float yPos = writer.getVerticalPosition(true) - 10f; // Adjust this to move line slightly below current content // Define start and end points for the line (relative to the page size and margins) float xStart = document.leftMargin(); // Start from the left margin float xEnd = document.getPageSize().getWidth() - document.rightMargin(); // End at the right margin // Draw the line at the current Y position canvas.moveTo(xStart, yPos); // Move to the starting point canvas.lineTo(xEnd, yPos); // Draw the line to the end point canvas.stroke(); // Apply the stroke (line) } }