# Draw on image

**URL:** <https://community.glideapps.com/t/draw-on-image/65163>\
**Category:** Ask for Help\
**Created:** [August 18, 2023, 4:08pm UTC](https://community.glideapps.com/t/draw-on-image/65163 "2023-08-18T16:08:11Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![Jacklyn](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/jacklyn/32/60623_2.png) [@Jacklyn](https://community.glideapps.com/u/Jacklyn)\
**Post date:** [August 18, 2023, 4:08pm UTC](https://community.glideapps.com/t/draw-on-image/65163/1 "2023-08-18T16:08:11Z")

</div>

Here’s a wild one. Does anyone know how I can have a user annotate an image? Ideally they’d be able to draw a circle around an object.

So they’d see an image displayed → click it → annotate it → it saves to my database as the annotated version.

I suspect the answer is a hard no, but perhaps there’s someone out there that has an idea for me.

---

<div class="post-metadata">

**Author:** ![ThinhDinh](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/thinhdinh/32/49_2.png) [@ThinhDinh](https://community.glideapps.com/u/ThinhDinh)\
**Post date:** [August 19, 2023, 1:05am UTC](https://community.glideapps.com/t/draw-on-image/65163/2 "2023-08-19T01:05:51Z")

</div>

That is indeed a hard no with native components.

I don’t know if there are third party services that offer you anything like that, to be embedded in a web embed component. Even if there is, you would need to find a way to link the Glide image URL to that service, which is almost certainly tricky.

---

<div class="post-metadata">

**Author:** ![Agostino\_Mastroianni](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/agostino_mastroianni/32/66495_2.png) [@Agostino\_Mastroianni](https://community.glideapps.com/u/Agostino_Mastroianni)\
**Post date:** [July 15, 2024, 7:48pm UTC](https://community.glideapps.com/t/draw-on-image/65163/3 "2024-07-15T19:48:10Z")

</div>

Hi, has anyone found a solution for this one?

---

<div class="post-metadata">

**Author:** ![ThinhDinh](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/thinhdinh/32/49_2.png) [@ThinhDinh](https://community.glideapps.com/u/ThinhDinh)\
**Post date:** [July 15, 2024, 11:38pm UTC](https://community.glideapps.com/t/draw-on-image/65163/4 "2024-07-15T23:38:33Z")

</div>

![image](https://us1.discourse-cdn.com/flex002/uploads/glideapps/original/3X/4/f/4ffab1047fb0ed076459bf68b1a1d3bd809f559a.jpeg)

I have tried to do it just now, but I haven’t found a way to actually save the annotated image.

---

<div class="post-metadata">

**Author:** ![Agostino\_Mastroianni](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/agostino_mastroianni/32/66495_2.png) [@Agostino\_Mastroianni](https://community.glideapps.com/u/Agostino_Mastroianni)\
**Post date:** [July 16, 2024, 12:06am UTC](https://community.glideapps.com/t/draw-on-image/65163/5 "2024-07-16T00:06:51Z")

</div>

Cool. Do you think sending it to a 3rd party app to render, like URLBox would work?

---

<div class="post-metadata">

**Author:** ![ThinhDinh](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/thinhdinh/32/49_2.png) [@ThinhDinh](https://community.glideapps.com/u/ThinhDinh)\
**Post date:** [July 16, 2024, 12:13am UTC](https://community.glideapps.com/t/draw-on-image/65163/6 "2024-07-16T00:13:12Z")

</div>

It has to be in consideration. Usually for components that save directly to Glide’s storage (say recorder), they must have a way to upload the file. We must be able to somehow do the same thing with custom code.

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [October 30, 2024, 11:00am UTC](https://community.glideapps.com/t/draw-on-image/65163/7 "2024-10-30T11:00:12Z")

</div>

I’m guessing no new native solutions. @ThinhDinh can you share HTML?  
Or anyone found a way?

---

<div class="post-metadata">

**Author:** ![ThinhDinh](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/thinhdinh/32/49_2.png) [@ThinhDinh](https://community.glideapps.com/u/ThinhDinh)\
**Post date:** [October 30, 2024, 11:25pm UTC](https://community.glideapps.com/t/draw-on-image/65163/8 "2024-10-30T23:25:22Z")

</div>

It was my test with the Code Component, which is not working anymore.

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [October 31, 2024, 8:43am UTC](https://community.glideapps.com/t/draw-on-image/65163/9 "2024-10-31T08:43:51Z")

</div>

You can try using the URI in the JavaScript column. However, it’s true what @thinhdinh mentioned, that the issue lies with the storage. The image below is an example that I saved in the form of a data URI using the Glide API.

 ![Screenshot 2024-10-31 at 15.37.57](https://us1.discourse-cdn.com/flex002/uploads/glideapps/original/3X/6/c/6c8e09b03886cde2c1d3e852eecf1888283e3906.jpeg)

 ![Screenshot 2024-10-31 at 15.38.13](https://us1.discourse-cdn.com/flex002/uploads/glideapps/original/3X/f/2/f222cef84e13bddd771b5aed855b61fab2788a14.jpeg)

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [October 31, 2024, 10:34am UTC](https://community.glideapps.com/t/draw-on-image/65163/10 "2024-10-31T10:34:06Z")

</div>

With the AI Custom Component, you can also achieve the same result. Unfortunately, I couldn’t get the “save” feature to successfully upload/update in Glide. It might be achievable by using a third-party hosting service.

 ![Screenshot 2024-10-31 at 17.32.59](https://us1.discourse-cdn.com/flex002/uploads/glideapps/original/3X/1/1/11d18d409b5a60527a9caf980e79605e7094b13b.jpeg)

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [November 1, 2024, 2:38pm UTC](https://community.glideapps.com/t/draw-on-image/65163/11 "2024-11-01T14:38:48Z")

</div>

Thanks for examples, but generally what i’m hearing is:  
Open image in Glide = Fine  
Edit image in Glide = Doable via JavaScript column / AI column  
Save edited image = Problematic

No worries, answer to stakeholders is, that this is not at native Glide feature, and the else brilliant community support hasn’t found a workaround yet 🙂

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [November 2, 2024, 10:24am UTC](https://community.glideapps.com/t/draw-on-image/65163/12 "2024-11-02T10:24:04Z")

</div>

In the first thread(2nd img), I demonstrated that images can be saved as a URI or reused to add new annotations.  
Now I have succeeded and am currently trying to create a download button to download the URI as an image file. And this works without having to host it on a third party.

![Recording2024-11-02195613-ezgif.com-video-to-webp-converter](https://us1.discourse-cdn.com/flex002/uploads/glideapps/original/3X/f/1/f1a0ce087a8d5aef5c2e9e7e71f70da53eb3076c.webp)

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [November 5, 2024, 9:39am UTC](https://community.glideapps.com/t/draw-on-image/65163/13 "2024-11-05T09:39:34Z")

</div>

WOW! - How does this work / can you share script? 🙂

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [November 7, 2024, 10:44am UTC](https://community.glideapps.com/t/draw-on-image/65163/14 "2024-11-07T10:44:54Z")

</div>

**Here it is.**  
To save the annotated file, you need to add a basic column as an Image placeholder and a Row ID column. For the JavaScript column I provided to work, target p1 to the Image placeholder column and p2 to your Row ID column.  
Additionally, you need to insert your own API data into the code at the end of the provided script (read the comments in the script).

> **Script**
>
> ```auto
> function generateAnnotatedImageURI(imageSource, rowID) {
> const htmlContent = `
> <!DOCTYPE html>
> <html lang="en">
> <head>
> <meta charset="UTF-8">
> <meta name="viewport" content="width=device-width, initial-scale=1.0">
> <title>Annotated Image</title>
> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
> <style>
> :root {
> --text-color-light: #1f2937;
> --text-color-dark: #cccccc; 
> }
> 
> body {
> color: var(--text-color-light);
> transition: background-color 0.5s, color 0.5s;
> }
> 
> body.dark {
> color: var(--text-color-dark); 
> }
> 
> label, .color-option {
> color: inherit;
> transition: color 0.5s; 
> }
> 
> body.dark label, 
> body.dark .color-option {
> color: var(--text-color-dark);
> }
> 
> canvas { 
> border-radius: 0.5rem;
> display: block; 
> margin: 0 auto; 
> width: 100%; 
> height: auto; 
> }
> 
> .color-option {
> border: 3px solid transparent;
> border-radius: 50%;
> width: 24px;
> height: 24px;
> cursor: pointer;
> display: inline-block;
> }
> 
> .color-option.selected {
> border-color: white;
> }
> #save:hover {
> opacity: 0.9;
> }
> </style>
> 
> <body class="flex justify-center items-center min-h-screen m-0">
> <div class="relative text-center w-full">
> <div class="canvas-container" style="margin-bottom: 20px; width: 100%;">
> <canvas id="canvas" style="width: 100%; height: auto;"></canvas>
> </div>
> 
> <div class="controls" style="display: grid; grid-template-columns: 1fr auto; gap: 10px; width: 100%;">
> <div class="control-content">
> <div class="flex flex-col md:flex-row items-start mt-2 gap-2">
> <div class="flex items-center mb-2 md:mb-0">
> <label class="mr-2">Size:</label>
> <input type="range" id="sizeRange" min="1" max="50" value="5" class="w-32 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer" style="outline:none">
> <span id="sizeValue" class="ml-2" style="font-size: 0.825rem; width: 20px; text-align: center; display: inline-block;">5</span> 
> </div>
> <div class="flex items-center mb-2 md:mt-0">
> <label class="mr-2">Color:</label>
> <div class="color-option cursor-pointer" data-color="black" style="background-color: #323232;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="red" style="background-color: red;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="blue" style="background-color: blue;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="green" style="background-color: green;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="yellow" style="background-color: yellow;"></div>
> </div>
> </div>
> <div class="flex justify-start mb-2 mt-2 gap-2">
> <button id="clear" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500">Clear</button>
> <button id="save" class="border rounded px-3 py-1" style="background-color: orange; color: white;">Save</button>
> <button id="download" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300" title="Download">
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path d="M3 12V17C3 19.2091 4.79086 21 7 21H17C19.2091 21 21 19.2091 21 17V12M7 11L12 16L17 11M12 3L12 15.5" stroke="black" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
> </svg>
> </button>
> </div>
> </div>
> <div class="flex gap-2 justify-self-end align-self-start items-start" style="position: relative; top: 0;">
> <button id="undo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Undo" disabled>
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
> </svg>
> </button>
> <button id="redo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Redo" disabled>
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6"></path>
> </svg>
> </button>
> </div>
> </div>
> </div>
> 
> <script>
> // Detect and apply theme
> const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
> if (prefersDarkScheme.matches) {
> document.body.classList.add("dark");
> }
> 
> prefersDarkScheme.addEventListener("change", (e) => {
> if (e.matches) {
> document.body.classList.add("dark");
> } else {
> document.body.classList.remove("dark");
> }
> });
> 
> const canvas = document.getElementById("canvas");
> const ctx = canvas.getContext("2d");
> 
> const img = new Image();
> img.crossOrigin = "Anonymous"; 
> img.src = "${imageSource}";
> img.onload = () => {
> canvas.width = img.width;
> canvas.height = img.height;
> ctx.drawImage(img, 0, 0, img.width, img.height);
> saveState();
> };
> 
> let color = "black";
> let size = document.getElementById("sizeRange").value;
> let isDrawing = false;
> let history = [];
> let historyStep = -1;
> 
> document.getElementById("sizeRange").oninput = (e) => {
> size = e.target.value;
> document.getElementById("sizeValue").textContent = size;
> };
> 
> document.querySelectorAll(".color-option").forEach((colorOption) => {
> colorOption.onclick = () => {
> color = colorOption.getAttribute('data-color');
> document.querySelectorAll(".color-option").forEach(opt => opt.classList.remove('selected'));
> colorOption.classList.add('selected');
> };
> });
> 
> document.querySelectorAll(".color-option")[0].classList.add('selected');
> 
> document.getElementById("clear").onclick = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(img, 0, 0, img.width, img.height);
> saveState();
> };
> 
> function getMousePos(canvas, evt) {
> const rect = canvas.getBoundingClientRect();
> const scaleX = canvas.width / rect.width;
> const scaleY = canvas.height / rect.height;
> return {
> x: (evt.clientX - rect.left) * scaleX,
> y: (evt.clientY - rect.top) * scaleY
> };
> }
> 
> canvas.onmousedown = (e) => {
> isDrawing = true;
> ctx.beginPath();
> ctx.lineWidth = size;
> ctx.strokeStyle = color;
> ctx.lineJoin = "round";
> ctx.lineCap = "round";
> const pos = getMousePos(canvas, e);
> ctx.moveTo(pos.x, pos.y);
> };
> 
> canvas.onmousemove = (e) => {
> if (isDrawing) {
> const pos = getMousePos(canvas, e);
> ctx.lineTo(pos.x, pos.y);
> ctx.stroke();
> }
> };
> 
> canvas.onmouseup = () => {
> isDrawing = false;
> saveState();
> };
> 
> function saveState() {
> if (historyStep < history.length - 1) {
> history = history.slice(0, historyStep + 1);
> }
> history.push(canvas.toDataURL());
> historyStep++;
> updateUndoRedo();
> }
> 
> function updateUndoRedo() {
> document.getElementById("undo").disabled = historyStep <= 0;
> document.getElementById("redo").disabled = historyStep >= history.length - 1;
> }
> 
> document.getElementById("undo").onclick = () => {
> if (historyStep > 0) {
> historyStep--;
> const imgData = new Image();
> imgData.src = history[historyStep];
> imgData.onload = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
> };
> }
> updateUndoRedo();
> };
> 
> document.getElementById("redo").onclick = () => {
> if (historyStep < history.length - 1) {
> historyStep++;
> const imgData = new Image();
> imgData.src = history[historyStep];
> imgData.onload = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
> };
> }
> updateUndoRedo();
> };
> 
> document.getElementById("save").onclick = () => {
> const dataURL = canvas.toDataURL("image/png");
> document.getElementById("save").innerText = "Saving...";
> document.getElementById("save").disabled = true;
> 
> fetch('https://api.glideapp.io/api/function/mutateTables', {
> method: 'POST',
> headers: {
> 'Content-Type': 'application/json',
> 'Authorization': 'Bearer [YOUR_API_KEY]' // Replace this with your API key
> },
> body: JSON.stringify({
> appID: "[YOUR_APP_ID]", // Replace this with your Glide app ID
> mutations: [
> {
> kind: "set-columns-in-row",
> tableName: "[YOUR_TABLE_NAME]", // Replace this with the table name in your Glide app
> columnValues: {
> "[COLUMN_NAME]": dataURL // Replace [COLUMN_NAME] with the name of the column where you want to store the image URL
> },
> rowID: "${rowID}"
> }
> ]
> })
> })
> .then(response => response.json())
> .then(data => {
> console.log('Success:', data);
> document.getElementById("save").innerText = "Save";
> document.getElementById("save").disabled = false;
> })
> .catch((error) => {
> console.error('Error:', error);
> alert("Error saving image. Please try again."); 
> document.getElementById("save").innerText = "Save";
> document.getElementById("save").disabled = false;
> });
> };
> 
> document.getElementById("download").onclick = () => {
> const link = document.createElement("a");
> link.href = canvas.toDataURL();
> link.download = "image.png";
> link.click();
> };
> </script>
> </body>
> </html>
> `;
> 
> return `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`;
> }
> 
> const imageURI = generateAnnotatedImageURI(p1, p2);
> p1 = imageURI;
> return imageURI; 
> 
> ```

As an additional note, to make your web-embed component accept background transparency, use the following CSS with the class name `color-scheme`.

```auto
.color-scheme {
  color-scheme: auto;
}

```

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [November 7, 2024, 1:48pm UTC](https://community.glideapps.com/t/draw-on-image/65163/15 "2024-11-07T13:48:48Z")

</div>

You wizard! 😃

First of all, THANKS!.

I keep getting error when i save. I have quadruple-checked API\_KEY, APP-ID, TABLE\_NAME, COLUMN\_NAME and rowID. Can you see what im doing wrong?

> **Script**
>
> ```
> fetch('https://api.glideapp.io/api/function/mutateTables', {
> method: 'POST',
> headers: {
> 'Content-Type': 'application/json',
> 'Authorization': 'Bearer XXXX'
> },
> body: JSON.stringify({
> appID: "YYYY",
> mutations: [
> {
> kind: "set-columns-in-row",
> tableName: "ZZZZ",
> columnValues: {
> "AAAAAA": dataURL
> },
> rowID: "BBBB"
> }
> ]
> })
> })
> 
> ```

Also, can “${rowID}” be set to p2 directly? - or do i need to convert p2 to string first?

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [November 7, 2024, 1:54pm UTC](https://community.glideapps.com/t/draw-on-image/65163/16 "2024-11-07T13:54:33Z")

</div>

I have a few comments that are excessive. Try to retain the row ID by taking the value from p2. So the writing would be:

```auto
          rowID: "${rowID}" 

```

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [November 7, 2024, 3:11pm UTC](https://community.glideapps.com/t/draw-on-image/65163/17 "2024-11-07T15:11:13Z")

</div>

Hmm, still error?

> **Script**
>
> ```
> fetch('https://api.glideapp.io/api/function/mutateTables', {
> method: 'POST',
> headers: {
> 'Content-Type': 'application/json',
> 'Authorization': 'Bearer 11111111-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
> },
> body: JSON.stringify({
> appID: "XXXXXXXXXXXXXXXXXXXX",
> mutations: [
> {
> kind: "set-columns-in-row",
> tableName: "native-table-ZZZZZZZZZZZZZZZZZZZZ",
> columnValues: {
> "AAAAA": dataURL
> },
> rowID: "${rowID}"
> }
> ]
> })
> })
> 
> ```

Btw, Download works, so im guessing its something in the API. I have compared API to some of my other scripts, cant find diffence.

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [November 7, 2024, 3:27pm UTC](https://community.glideapps.com/t/draw-on-image/65163/18 "2024-11-07T15:27:23Z")

</div>

So you’re having save problems?

---

<div class="post-metadata">

**Author:** ![Danny\_Nielsen](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/danny_nielsen/32/61765_2.png) [@Danny\_Nielsen](https://community.glideapps.com/u/Danny_Nielsen)\
**Post date:** [November 7, 2024, 3:29pm UTC](https://community.glideapps.com/t/draw-on-image/65163/19 "2024-11-07T15:29:45Z")

</div>

Yeah, I keep getting error when i save

---

<div class="post-metadata">

**Author:** ![Himaladin](https://sea2.discourse-cdn.com/flex002/user_avatar/community.glideapps.com/himaladin/32/71023_2.png) [@Himaladin](https://community.glideapps.com/u/Himaladin)\
**Post date:** [November 7, 2024, 3:41pm UTC](https://community.glideapps.com/t/draw-on-image/65163/20 "2024-11-07T15:41:43Z")

</div>

I’ll try to provide the original script with the API I’ve manipulated so you can check where the error is.

> **Script**
>
> ```auto
> function generateAnnotatedImageURI(imageSource, rowID) {
> const htmlContent = `
> <!DOCTYPE html>
> <html lang="en">
> <head>
> <meta charset="UTF-8">
> <meta name="viewport" content="width=device-width, initial-scale=1.0">
> <title>Annotated Image</title>
> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
> <style>
> :root {
> --text-color-light: #1f2937;
> --text-color-dark: #cccccc; 
> }
> 
> body {
> color: var(--text-color-light);
> transition: background-color 0.5s, color 0.5s;
> }
> 
> body.dark {
> color: var(--text-color-dark); 
> }
> 
> label, .color-option {
> color: inherit;
> transition: color 0.5s; 
> }
> 
> body.dark label, 
> body.dark .color-option {
> color: var(--text-color-dark);
> }
> 
> canvas { 
> border-radius: 0.5rem;
> display: block; 
> margin: 0 auto; 
> width: 100%; 
> height: auto; 
> }
> 
> .color-option {
> border: 3px solid transparent;
> border-radius: 50%;
> width: 24px;
> height: 24px;
> cursor: pointer;
> display: inline-block;
> }
> 
> .color-option.selected {
> border-color: white;
> }
> #save:hover {
> opacity: 0.9;
> }
> </style>
> 
> <body class="flex justify-center items-center min-h-screen m-0">
> <div class="relative text-center w-full">
> <div class="canvas-container" style="margin-bottom: 20px; width: 100%;">
> <canvas id="canvas" style="width: 100%; height: auto;"></canvas>
> </div>
> 
> <div class="controls" style="display: grid; grid-template-columns: 1fr auto; gap: 10px; width: 100%;">
> <div class="control-content">
> <div class="flex flex-col md:flex-row items-start mt-2 gap-2">
> <div class="flex items-center mb-2 md:mb-0">
> <label class="mr-2">Size:</label>
> <input type="range" id="sizeRange" min="1" max="50" value="5" class="w-32 h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer" style="outline:none">
> <span id="sizeValue" class="ml-2" style="font-size: 0.825rem; width: 20px; text-align: center; display: inline-block;">5</span> 
> </div>
> <div class="flex items-center mb-2 md:mt-0">
> <label class="mr-2">Color:</label>
> <div class="color-option cursor-pointer" data-color="black" style="background-color: #323232;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="red" style="background-color: red;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="blue" style="background-color: blue;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="green" style="background-color: green;"></div>
> <div class="color-option cursor-pointer ml-1" data-color="yellow" style="background-color: yellow;"></div>
> </div>
> </div>
> <div class="flex justify-start mt-2 mb-2 gap-2">
> <button id="clear" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500">Clear</button>
> <button id="save" class="border rounded px-3 py-1" style="background-color: orange; color: white;">Save</button>
> <button id="download" class="border rounded px-3 py-1 bg-gray-200 hover:bg-gray-300" title="Download">
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path d="M3 12V17C3 19.2091 4.79086 21 7 21H17C19.2091 21 21 19.2091 21 17V12M7 11L12 16L17 11M12 3L12 15.5" stroke="black" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
> </svg>
> </button>
> </div>
> </div>
> <div class="flex gap-2 justify-self-end align-self-start items-start" style="position: relative; top: 0;">
> <button id="undo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Undo" disabled>
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
> </svg>
> </button>
> <button id="redo" class="icon-button p-2 bg-white rounded hover:opacity-70 text-gray-800 dark:text-white" title="Redo" disabled>
> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6"></path>
> </svg>
> </button>
> </div>
> </div>
> </div>
> 
> <script>
> // Detect and apply theme
> const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
> if (prefersDarkScheme.matches) {
> document.body.classList.add("dark");
> }
> 
> prefersDarkScheme.addEventListener("change", (e) => {
> if (e.matches) {
> document.body.classList.add("dark");
> } else {
> document.body.classList.remove("dark");
> }
> });
> 
> const canvas = document.getElementById("canvas");
> const ctx = canvas.getContext("2d");
> 
> const img = new Image();
> img.crossOrigin = "Anonymous"; 
> img.src = "${imageSource}";
> img.onload = () => {
> canvas.width = img.width;
> canvas.height = img.height;
> ctx.drawImage(img, 0, 0, img.width, img.height);
> saveState();
> };
> 
> let color = "black";
> let size = document.getElementById("sizeRange").value;
> let isDrawing = false;
> let history = [];
> let historyStep = -1;
> 
> document.getElementById("sizeRange").oninput = (e) => {
> size = e.target.value;
> document.getElementById("sizeValue").textContent = size;
> };
> 
> document.querySelectorAll(".color-option").forEach((colorOption) => {
> colorOption.onclick = () => {
> color = colorOption.getAttribute('data-color');
> document.querySelectorAll(".color-option").forEach(opt => opt.classList.remove('selected'));
> colorOption.classList.add('selected');
> };
> });
> 
> document.querySelectorAll(".color-option")[0].classList.add('selected');
> 
> document.getElementById("clear").onclick = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(img, 0, 0, img.width, img.height);
> saveState();
> };
> 
> function getMousePos(canvas, evt) {
> const rect = canvas.getBoundingClientRect();
> const scaleX = canvas.width / rect.width;
> const scaleY = canvas.height / rect.height;
> return {
> x: (evt.clientX - rect.left) * scaleX,
> y: (evt.clientY - rect.top) * scaleY
> };
> }
> 
> canvas.onmousedown = (e) => {
> isDrawing = true;
> ctx.beginPath();
> ctx.lineWidth = size;
> ctx.strokeStyle = color;
> ctx.lineJoin = "round";
> ctx.lineCap = "round";
> const pos = getMousePos(canvas, e);
> ctx.moveTo(pos.x, pos.y);
> };
> 
> canvas.onmousemove = (e) => {
> if (isDrawing) {
> const pos = getMousePos(canvas, e);
> ctx.lineTo(pos.x, pos.y);
> ctx.stroke();
> }
> };
> 
> canvas.onmouseup = () => {
> isDrawing = false;
> saveState();
> };
> 
> function saveState() {
> if (historyStep < history.length - 1) {
> history = history.slice(0, historyStep + 1);
> }
> history.push(canvas.toDataURL());
> historyStep++;
> updateUndoRedo();
> }
> 
> function updateUndoRedo() {
> document.getElementById("undo").disabled = historyStep <= 0;
> document.getElementById("redo").disabled = historyStep >= history.length - 1;
> }
> 
> document.getElementById("undo").onclick = () => {
> if (historyStep > 0) {
> historyStep--;
> const imgData = new Image();
> imgData.src = history[historyStep];
> imgData.onload = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
> };
> }
> updateUndoRedo();
> };
> 
> document.getElementById("redo").onclick = () => {
> if (historyStep < history.length - 1) {
> historyStep++;
> const imgData = new Image();
> imgData.src = history[historyStep];
> imgData.onload = () => {
> ctx.clearRect(0, 0, canvas.width, canvas.height);
> ctx.drawImage(imgData, 0, 0, imgData.width, imgData.height);
> };
> }
> updateUndoRedo();
> };
> 
> document.getElementById("save").onclick = () => {
> const dataURL = canvas.toDataURL("image/png");
> document.getElementById("save").innerText = "Saving...";
> document.getElementById("save").disabled = true;
> 
> fetch('https://api.glideapp.io/api/function/mutateTables', {
> method: 'POST',
> headers: {
> 'Content-Type': 'application/json',
> 'Authorization': 'Bearer d116a077-gce2-480a-bb88-78115353a629'
> },
> body: JSON.stringify({
> appID: "dKdkrSQrNQPy0SY4Ax6M",
> mutations: [
> {
> kind: "set-columns-in-row",
> tableName: "native-table-xedUieLHU43F8SR68jxa",
> columnValues: {
> "SXOYm": dataURL 
> },
> rowID: "${rowID}"
> }
> ]
> })
> })
> .then(response => response.json())
> .then(data => {
> console.log('Success:', data);
> document.getElementById("save").innerText = "Save";
> document.getElementById("save").disabled = false;
> })
> .catch((error) => {
> console.error('Error:', error);
> alert("Error saving image. Please try again.");
> document.getElementById("save").innerText = "Save";
> document.getElementById("save").disabled = false;
> });
> };
> 
> document.getElementById("download").onclick = () => {
> const link = document.createElement("a");
> link.href = canvas.toDataURL();
> link.download = "image.png";
> link.click();
> };
> </script>
> </body>
> </html>
> `;
> 
> return `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`;
> }
> 
> const imageURI = generateAnnotatedImageURI(p1, p2);
> p1 = imageURI;
> return imageURI;
> 
> ```

[Next page](https://community.glideapps.com/t/draw-on-image/65163.md?page=2)
