You are viewing limited content. For full access, please sign in.

Question

Question

Forms: Select region of image

asked on May 2

Hello,

I have an end user who is trying to make a form in which has an image where the form visitor needs to select a region of the image. The form is for an accident reporting form and the intent is to select where on the body the injury occured. The desired end result would be something like the mockup below.

Is this possible with LF Forms? I was thinking maybe something with custom html image map and some javascripting... Has anyone done such a form yet?

0 0

Answer

SELECTED ANSWER
replied on May 2 โ€ข Show version history

Okay, let me see how I can make a guide for you based on what I was able to adapt for this scenario and how it came to me. Perhaps someone with much more advanced knowledge than mine can improve it. 

* I used ChatGPT to rewrite the code. Sorry if you find any errors. But the way I shared it with you works for me.

  • Interactive Drawing Canvas:

    • Displays a base image of the human body (front and back).

    • User can click to mark injury points (each click draws a red circle).

    • Includes Undo and Clear buttons to remove selections.

  • Image Generation:

    • A "Save as Image" button converts the canvas (with background and marks) into a PNG image using canvas.toDataURL().

    • The image is:

      • Hidden from the editing area

      • Previewed in a separate HTML block using <img src="...">

  • Download & Reset:

    • A Download Image button is available so the user can save the generated image locally. Then use this image to upload it intro the form Upload Field. 

    • A Start Over button allows the user to clear the drawing and generate a new image if needed. This action includes a confirmation dialog and scrolls back to the canvas.

  • Auto-Hide on Form Resume:

    • If the form is reopened in view or edit mode and an image already exists, the canvas section is automatically hidden and the saved image is shown in preview.

  • Final Review with Radio Button Confirmation:

    • A radio button field labeled: “The Injury Image is Correct?” was added.

    • When the user selects “Yes”, the entire canvas section—including drawing tools and preview—is hidden.

    • Only the final uploaded image is shown, using a File Upload field with image thumbnail display enabled, for clean final submission.

 

IMPLEMENTING

I do this using Classic Form:

1- Create a Classic Form and insert a Custom HTML Object with the follow Html code: 

<div class="body-map-container">
  <h3>Select injured areas on the body</h3>
  <canvas id="bodyCanvas" width="700" height="500"></canvas>

  <div class="buttons">
    <button type="button" id="undoBtn">โคบ Undo</button>
    <button type="button" id="clearBtn">๐Ÿ—‘๏ธ Clear</button>
    <button type="button" id="saveImageBtn">๐Ÿ“ธ Generate Image</button>
  </div>
</div>

* In the advanced options put this CSS class name: bodyCanvasCss

2- Insert a second Custom HTML with the follow code: 

<div id="imagePreviewContainer" style="text-align:center; margin-top: 20px;">
  <h4>Saved Image</h4>
  <div id="imagePreview"></div>
  <div id="imageActions" style="margin-top: 15px; display: none;">
    <a id="downloadImage" href="#" download="injury-diagram.png" class="action-btn">๐Ÿ’พ Download Image</a>
    <button type="button" id="startOverBtn" class="action-btn">๐Ÿ” Start Over</button>
  </div>
</div>

3- Insert a File Upload object (Make it required)

4- Insert a Radio Button for make the questions: The Injury Image is Correct?. In my example I use this filed to hide the Canvas Html section and the Save Image Html section. Because went you submit the form will showing both in the form file you will save into the Repository. 

The logic of this is to have only the file upload field showing with the Show uploaded image option active with X-Large view option selected. 

5- Put this CSS code in the CSS and JavaScript Tab. 

.body-map-container {
  text-align: center;
  padding: 10px;
}

canvas#bodyCanvas {
  border: 1px solid #ccc;
  cursor: crosshair;
  display: block;
  margin: auto;
  width: 700px;
  height: 500px;
}

.buttons {
  margin-top: 15px;
}

.buttons button {
  margin: 0 10px;
  padding: 10px 20px;
  font-weight: bold;
  font-size: 14px;
  border: 1px solid #444;
  border-radius: 5px;
  background-color: #f0f0f0;
  cursor: pointer;
}

#imagePreviewContainer img {
  max-width: 100%;
  border: 1px solid #ccc;
  margin-top: 10px;
}

.action-btn {
  margin: 0 10px;
  padding: 10px 20px;
  font-weight: bold;
  font-size: 14px;
  border: 1px solid #444;
  border-radius: 5px;
  background-color: #e6f0ff;
  cursor: pointer;
  display: inline-block;
  text-decoration: none;
  color: black;
}

6- Use this JavaScript: 

window.addEventListener("load", function () {
  const canvas = document.getElementById("bodyCanvas");
  const ctx = canvas.getContext("2d");
  const container = document.querySelector(".body-map-container");
  const extraSection = document.querySelector(".bodyCanvasCss");
  let clicks = [];

  const background = new Image();
  background.crossOrigin = "anonymous";
  background.src = "https://yourdomain.com/path/body-diagram.png"; //Put your Body Diagram Image URL Here

  function drawAll() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(background, 0, 0, canvas.width, canvas.height);
    clicks.forEach(({ x, y }) => {
      ctx.beginPath();
      ctx.arc(x, y, 15, 0, 2 * Math.PI);
      ctx.strokeStyle = "red";
      ctx.lineWidth = 2;
      ctx.stroke();
    });
  }

  canvas.addEventListener("click", function (e) {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    clicks.push({ x, y });
    drawAll();
    updateHiddenFields();
  });

  document.getElementById("undoBtn").addEventListener("click", function () {
    clicks.pop();
    drawAll();
    updateHiddenFields();
  });

  document.getElementById("clearBtn").addEventListener("click", function () {
    clicks = [];
    drawAll();
    updateHiddenFields();
  });

  document.getElementById("saveImageBtn").addEventListener("click", function () {
    const dataURL = canvas.toDataURL("image/png");

    // Show preview
    const previewTarget = document.getElementById("imagePreview");
    if (previewTarget) {
      previewTarget.innerHTML = `<img src="${dataURL}" alt="Saved Image" />`;
    }

    // Enable download and start over
    document.getElementById("downloadImage").href = dataURL;
    document.getElementById("imageActions").style.display = "block";

    // Hide canvas and extra section
    container.style.display = "none";
    if (extraSection) extraSection.style.display = "none";

    // Save to hidden field (Workflow)
    const fieldCanvas = document.querySelector("#canvasImageData input");
    if (fieldCanvas) fieldCanvas.value = dataURL;

    // Save to optional .ImageDir field
    const fieldByClass = document.querySelector(".ImageDir input, .ImageDir textarea");
    if (fieldByClass) fieldByClass.value = dataURL;

    alert("Image has been generated and is ready for download or submission.");
  });

  document.getElementById("startOverBtn").addEventListener("click", function () {
    const confirmReset = confirm("Are you sure you want to start over? This will delete the current image.");
    if (!confirmReset) return;

    clicks = [];
    drawAll();

    // Show canvas and extra section again
    container.style.display = "block";
    if (extraSection) extraSection.style.display = "block";

    // Clear preview and controls
    document.getElementById("imagePreview").innerHTML = "";
    document.getElementById("imageActions").style.display = "none";
    document.getElementById("downloadImage").href = "#";

    // Clear stored values
    const fieldCanvas = document.querySelector("#canvasImageData input");
    if (fieldCanvas) fieldCanvas.value = "";

    const fieldByClass = document.querySelector(".ImageDir input, .ImageDir textarea");
    if (fieldByClass) fieldByClass.value = "";

    // Scroll to canvas
    container.scrollIntoView({ behavior: "smooth" });
  });

  function updateHiddenFields() {
    const coordField = document.querySelector("#selectedPoints input");
    if (coordField) coordField.value = JSON.stringify(clicks);
  }

  background.onload = () => drawAll();
  if (background.complete) drawAll();

  // โœ… Auto-load saved image if present
  const fieldCanvas = document.querySelector("#canvasImageData input");
  if (fieldCanvas && fieldCanvas.value.startsWith("data:image/")) {
    container.style.display = "none";
    if (extraSection) extraSection.style.display = "none";

    const previewTarget = document.getElementById("imagePreview");
    if (previewTarget) {
      previewTarget.innerHTML = `<img src="${fieldCanvas.value}" alt="Saved Image" />`;
    }

    const actions = document.getElementById("imageActions");
    if (actions) {
      document.getElementById("downloadImage").href = fieldCanvas.value;
      actions.style.display = "block";
    }
  }
});

The human body image used as the background for the canvas must be hosted at a public HTTPS-accessible URL (e.g., https://yourdomain.com/path/body-diagram.png).
If the image is hosted locally or on an intranet, it will not render in the canvas for external users or during form rendering.

Why? HTML5 <canvas> cannot draw cross-origin images unless served from a public domain with proper CORS permissions.

 

7- Use a Process Diagram Service Task or Workflow Service Task. to save the form to the Repository.

8 - Almost forgot: Create a Field Rule where you will hide both Custom HTML if the Radio Button: The Injury Image is Correct? is Yes. 

 

I hope this can help you. If you have any questions let me know. 

https://i.postimg.cc/qM7X5wD2/req-img2.gif 

 

req-img.gif
req-img.gif (3.34 MB)
1 0

Replies

replied on May 5

This is great!

 

I had some things come up, but I’m going to try this as soon as I can get back to the office. I really appreciate it. 
 

Thank you!

2 0
replied two days ago

I'm so glad it was a good starting point for you. It's been a pleasure helping.

0 0
replied on May 5

Here is an example of a similar function done entirely in the modern designer:

https://answers.laserfiche.com/questions/216183/Draw-on-image-modern-designer#216210

1 0
replied on May 5 โ€ข Show version history

My concern was how to save the generated base64 image, so that it would be displayed in the form when saved (which is why I included the download and upload option), and at the same time, it would be saved as a file in the repository. I'm going to try the alternative you propose.

1 0
replied on May 5

I never took it beyond testing - because I saw it as an interesting thing to try to do from LFAnswers, it wasn't something I needed for my environment.  But it did seem to be working when populated from a prior task.

0 0
replied two days ago

I also found it interesting to implement something similar in Laserfiche. I have a project outside of Laserfiche where I use it to select regions in images, and I took part of this project and modified it to try to implement it here.
The best part is that all the suggestions always help create better solutions.

1 0
replied two days ago

This is interesting too. I did do a search before posting and I didn't find the post you linked to here. Thanks for that. I want to try the solution that Luis has recommended, but also ideally if there's a means to not have to download the generated image and have the image with the selected regions still attached to the form in LF, that would be awesome. 

I'm actually surprised that this is not an out of the box option.

0 0
replied two days ago

I'm trying to save the resulting image to a file in the repository using Workflow, but I'm still not sure if it can be embedded in the form, because even when I save the HTML with the generated image, it isn't saved. Perhaps I could do a variant of creating the PDF or image file in the repository, then storing the path to the image in the repository in a field, then displaying that field in the form, and finally saving the form with that image previously saved with the workflow. I don't know, it's just an idea I've had, I don't know if it's possible yet. If I found the way I will update this. 

1 0
replied two days ago

You're solution put me lightyears ahead of where I'd be on my own. I appriciate it greately!

0 0
replied two days ago

I'm still not sure if it can be embedded in the form, because even when I save the HTML with the generated image, it isn't saved.

 

That's why the way I handled it is by saving the actual image data as base64 data in a multi-line field, instead of trying to attach it as an image file.  You can't automate attaching a file (security-focused browser limitation, not a forms limitation), so to fully automate the image, this was the best way I could find.  Every time the form is loaded, the first thing it does is generate the image in the body of the form from the saved base64 data so that it shows from prior submissions.

1 0
replied two days ago

Hi Matthew & Keith, 

I been working around Matthew (thank you Matthew) post recommendation with the save data as base64 in the form. and made few minimal modifications, this is what I got, and is better solutions as my first approach. 

Using Modern Design Form: 

1- Insert a Custom HTML object with the CSS class: canvasContainer and the next HTML code inside: 
 

<div class="canvasContainer"></div>

2- Go to internet and convert your png or jpg file to be used as based of the selection. or you can download the dataImage64.txt I attached here and used the content as the image. You will need it to copy and paste that base64 code in the default value of the Multiline Field mentioned next (default_image_as_base64)

3- Insert a Multiline Field with the follow variable name: default_image_as_base64 and paste in the default value the result of your image 64 convert or paste the content of the file I mentioned before. 

4- Create a second Multiline Field with the variable name: image_as_base64.

5- In the Field Rules: You will hide always both Multiline Fields, but only will set "Save data when is hidden" to the Multiline Field: image_as_base64, the other Multiline leave in Ignore data when is hidden.

7- Insert this JavaScript code in your JS Section: 
 

var imgWidth = 500;
var imgHeight = 333;
var initialImage = LFForm.getFieldValues({variableName: "image_as_base64"});

// Load the default base64 image into the working field
async function CopyDefaultImage() {
  var defaultImage = await LFForm.getFieldValues({variableName: "default_image_as_base64"});
  await LFForm.setFieldValues({variableName: "image_as_base64"}, defaultImage);
  await CreateAndLoadVisibleCanvasAndImage();
  initialImage = await LFForm.getFieldValues({variableName: "image_as_base64"});
  await LoadImageToHiddenCanvas();
}

// Create the visible image + canvas
async function CreateAndLoadVisibleCanvasAndImage() {
  var currentImage = LFForm.getFieldValues({variableName: "image_as_base64"});
  var customHTMLField = LFForm.findFieldsByClassName("canvasContainer")[0];
  var newContent = '<div style="display: grid;">';
  newContent += '<img width="' + imgWidth + '" height="' + imgHeight + '" style="grid-column: 1; grid-row: 1;" src="' + currentImage + '">';
  newContent += '<canvas width="' + imgWidth + '" height="' + imgHeight + '" style="border:black 2px solid; grid-column: 1; grid-row: 1;" onclick="canvasClick(event)"></canvas>';
  newContent += '</div><br><button onclick="resetClick(event)">Reset/Clear the Image</button>';
  await LFForm.changeFieldSettings(customHTMLField, {content: newContent});
}

// Run on load
var formLoadTasks = async function() {
  if (initialImage == '') {
    await CopyDefaultImage();
  } else {
    await CreateAndLoadVisibleCanvasAndImage();
  }
  await LoadImageToHiddenCanvas();
}
formLoadTasks();

// Hidden canvas to handle modifications
const hiddenCanvas = document.createElement('canvas');
const ctx = hiddenCanvas.getContext('2d');
const hiddenImage = new Image();
async function LoadImageToHiddenCanvas() {
  hiddenImage.crossOrigin = "";
  hiddenImage.src = initialImage;
  hiddenImage.onload = () => {
    hiddenCanvas.width = imgWidth;
    hiddenCanvas.height = imgHeight;
    ctx.drawImage(hiddenImage, 0, 0);
  };
  document.body.appendChild(hiddenCanvas);
}

// Draw on click and update saved image
window.canvasClick = async function(event) {
  DrawCircleOnCanvas(event.offsetX, event.offsetY);
  await updateImage();
  await CreateAndLoadVisibleCanvasAndImage();
}

// Reset to default image
window.resetClick = async function(event) {
  await CopyDefaultImage();
}

// Save current canvas state to LFForm variable
async function updateImage() {
  await LFForm.setFieldValues({variableName: "image_as_base64"}, hiddenCanvas.toDataURL());
}

// Draw red circle
function DrawCircleOnCanvas(x, y) {
  ctx.beginPath();
  ctx.arc(x, y, 8, 0, 2 * Math.PI);
  ctx.strokeStyle = "red";
  ctx.lineWidth = 2;
  ctx.stroke();
}

8- Insert this CSS code in the CSS section: 
 

.canvasContainer {
  margin: 0 auto;
  width: fit-content;
}
.canvasContainer button {
  background-color: #dc3545; /* Bootstrap red */
  color: white;
  padding: 10px 20px;
  font-size: 14px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 0.2s ease;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
  display: block;
  margin: 20px auto;
}

.canvasContainer button:hover {
  background-color: #c82333;
}

This will allow you to also save the image with the previously made selections when you save the form, thus avoiding the step I suggested: generating the image, saving it, and then uploading it again. This solution, following Matthew's suggestion, simplifies many things and is much better. I hope this helps. Let me know if you have any questions.

dataImage64.txt (114.54 KB)
2 0
replied on May 2

Do you need this for Laserfiche Cloud or Self-Hosted? 

0 0
replied on May 2

Hi Luis,

 

We are self hosted. 

0 0
replied on May 2

I have a solution variant that I'm applying to something similar. I'm making some adjustments to share it with you here. I hope it helps you at least get closer to what you need.

1 0
You are not allowed to follow up in this post.

Sign in to reply to this post.