The idea behind this proof of concept is to find a way to use a picture field to enter information such as errors, defects, deviations, or - since we are a construction company - pipe or cable routes.
I already had this idea back in 2022.
My solution is to use the signature field as a drawing board and then copy the result back into a picture field.
Perhaps this will also serve as a starting point for others to explore new solutions in a similar way.
My PoC has the following limitations:
The original image should be in landscape format and is reduced to a height of 500px during processing (aspect ratio preserved). If the width is narrower or wider then 889px (this corresponds to a 16:9 ratio) when the height is reduced to 500px, a right margin is created or it is cropped (hence the recommendation for landscape format).
__
The original idea goes back to my post from 2022-09-26:
SUGGESTION: Mix between "Picture" and "Handwritten signature" form field [https://community.webcon.com/forum/thread/2230?messageid=2230]
and the answer to my question by @Markus Jenni on 2023-08-23:
SOLVED -> Q: Is it possible to change the pencil color in "HANDWRITTEN SIGNATURE" FORM FIELD [https://community.webcon.com/forum/thread/3409?messageid=3409]
Helpful for this extended version were:
* the mentioned website by @Markus Jenni: [https://codepen.io/zsolt555/pen/rpPXOB]
* F12 in Chrome 😉
* <script src="https://unpkg.com/pica@8.0.0/dist/pica.min.js"></script> [older version of the Pica library by Vitaly Puzrin, which is available under the MIT license]
* ChatGPT for Vibe Coding - with a little help from me - the "JAVASCRIPT Edit menu for canvas" (Give credit where credit is due - Use at your own risk ! 😜)







<script src="https://unpkg.com/pica@8.0.0/dist/pica.min.js"></script>
<script>
setTimeout(() => {
const copyButton = document.getElementById("copyResize500");
if (copyButton) {
copyButton.addEventListener("click", async function () {
// Get the base64 string from the source field
const rawBase64 = GetValue("#{FLD:12525}#");
if (!rawBase64 || rawBase64.length < 50) {
alert("No valid image data found in the source field.");
return;
}
const img = new Image();
img.src = rawBase64;
img.onload = async () => {
const originalWidth = img.width;
const originalHeight = img.height;
// Target height: exactly 500px, width adjusted proportionally
const targetHeight = 500;
const scaleFactor = targetHeight / originalHeight;
const targetWidth = Math.round(originalWidth * scaleFactor);
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
try {
const pica = window.pica();
await pica.resize(img, canvas);
const resizedBase64 = canvas.toDataURL("image/jpeg", 0.95);
// Set the resized image in the target field
SetValue("#{FLD:12530}#", resizedBase64);
alert(`Image resized and copied as ${targetWidth}×${targetHeight}.`);
} catch (err) {
console.error("Error during image processing:", err);
alert("An error occurred while resizing the image.");
}
};
img.onerror = () => {
alert("The image could not be loaded.");
};
});
} else {
console.error("Button with ID 'copyResize500' not found.");
}
}, 500);
</script>
<div style="text-align: right;">
<button id="copyResize500" style="background-color: #00C0FF; font-weight: bold; border-radius: 10px; font-size:16px;">
Resize image to height 500px (aspect ratio preserved)
</button>
</div>
<script>
setTimeout(() => {
const canvas = document.querySelector("canvas.editable-canvas__canvas");
if (canvas) {
canvas.width = 889; // native width – important for signature data
canvas.height = 500; // optional: new height
canvas.style.width = "889px";
canvas.style.height = "500px";
canvas.style.backgroundColor = "#FFF4C4"; // optional
} else {
console.warn("Canvas not found.");
}
}, 500);
</script>
<!-- === Signature Controls + Engine Override (contrast UI + undo + max undo) === -->
<style>
/* compact, high-contrast panel */
#signature-tools {
display:flex; gap:1rem; align-items:center; flex-wrap:wrap;
padding:.75rem 1rem; margin:.5rem 0;
background:#111827; color:#F9FAFB; border:1px solid #374151; border-radius:.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,.25);
}
#signature-tools label { display:flex; align-items:center; gap:.5rem; }
#signature-tools select,
#signature-tools input[type="range"] {
background:#1F2937; color:#F9FAFB; border:1px solid #4B5563; border-radius:.5rem;
padding:.35rem .5rem;
}
#signature-tools select:focus,
#signature-tools input[type="range"]:focus {
outline:2px solid #10B981; outline-offset:1px;
}
#signature-tools button {
background:#10B981; color:#06281B; border:none; border-radius:.5rem;
padding:.45rem .75rem; font-weight:600; cursor:pointer;
}
#signature-tools button[disabled] { opacity:.5; cursor:not-allowed; }
#signature-tools .pill {
display:inline-block; padding:.2rem .5rem; border:1px solid #4B5563; border-radius:999px; color:#D1D5DB;
}
#signature-tools .btn-row { display:flex; gap:.5rem; align-items:center; }
</style>
<div id="signature-tools">
<label>
<span>Pen color:</span>
<select id="penColor" aria-label="Pen color">
<option value="#000000">Black</option>
<option value="#ff0000">Red</option>
<option value="#0000ff">Blue</option>
<option value="#00aa00">Green</option>
<option value="#ff00ff" selected>Magenta</option>
<option value="#ffa500">Orange</option>
<option value="#ffffff">White</option>
<option value="#ffff00">Yellow</option>
</select>
</label>
<label>
<span>Line width:</span>
<input type="range" id="penWidth" min="1" max="16" value="2" />
<span id="penWidthVal" class="pill">2 px</span>
</label>
<div class="btn-row">
<button id="undoBtn" title="Undo last stroke (Ctrl+Z)" disabled>Undo</button>
<button id="maxUndoBtn" title="Undo all strokes" disabled>Max undo</button>
</div>
</div>
<script>
(function(){
// --- persisted preferences ---
const LS_KEY = 'signature-tools-prefs';
const defaults = { color: '#ff00ff', width: 2 };
function loadPrefs() {
try { return { ...defaults, ...(JSON.parse(localStorage.getItem(LS_KEY))||{}) }; }
catch{ return { ...defaults }; }
}
function savePrefs(p){ try { localStorage.setItem(LS_KEY, JSON.stringify(p)); } catch{} }
const prefs = loadPrefs();
// --- UI wiring ---
const colorSel = document.getElementById('penColor');
const widthInp = document.getElementById('penWidth');
const widthVal = document.getElementById('penWidthVal');
const undoBtn = document.getElementById('undoBtn');
const maxUndoBtn = document.getElementById('maxUndoBtn');
if (colorSel) colorSel.value = prefs.color;
if (widthInp) widthInp.value = String(prefs.width);
if (widthVal) widthVal.textContent = prefs.width + ' px';
colorSel?.addEventListener('change', () => { prefs.color = colorSel.value; savePrefs(prefs); });
widthInp?.addEventListener('input', () => {
prefs.width = Math.min(16, Math.max(1, parseInt(widthInp.value,10) || 1));
widthVal.textContent = prefs.width + ' px';
savePrefs(prefs);
});
// --- helpers ---
function isValidHex(hex) { return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex); }
// Track last active signature canvas so Undo knows where to apply
let lastActiveCanvas = null;
// Per-canvas undo stacks using ImageData snapshots
const undoStacks = new WeakMap(); // canvas -> ImageData[]
const MAX_UNDO = 200;
function getStack(canvas){
let s = undoStacks.get(canvas);
if(!s){ s = []; undoStacks.set(canvas, s); }
return s;
}
function pushUndo(ctx){
try {
const c = ctx.canvas;
if(!c) return;
const stack = getStack(c);
const img = ctx.getImageData(0, 0, c.width, c.height);
stack.push(img);
if(stack.length > MAX_UNDO) stack.shift();
updateUndoState();
} catch(e){ /* ignore */ }
}
function canUndo(canvas){
if(!canvas) return false;
const s = undoStacks.get(canvas);
return !!(s && s.length);
}
function doUndo(canvas){
try {
if(!canvas) return;
const ctx = canvas.getContext('2d');
const stack = getStack(canvas);
const img = stack.pop();
if(img){ ctx.putImageData(img, 0, 0); }
updateUndoState();
} catch(e){ /* ignore */ }
}
// Undo everything currently stored (fast loop)
function doMaxUndo(canvas){
if(!canvas) return;
const stack = getStack(canvas);
if(!stack.length) return;
const ctx = canvas.getContext('2d');
// Put the earliest snapshot to clear all strokes since then
const first = stack[0];
stack.length = 0; // clear stack
if(first){ ctx.putImageData(first, 0, 0); }
updateUndoState();
}
function updateUndoState(){
const hasUndo = canUndo(lastActiveCanvas);
if(undoBtn) undoBtn.disabled = !hasUndo;
if(maxUndoBtn) maxUndoBtn.disabled = !hasUndo;
}
undoBtn?.addEventListener('click', () => doUndo(lastActiveCanvas));
maxUndoBtn?.addEventListener('click', () => doMaxUndo(lastActiveCanvas));
// Keyboard shortcut: Ctrl/Cmd + Z
window.addEventListener('keydown', (e) => {
const isUndo = (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
if(isUndo){
e.preventDefault();
doUndo(lastActiveCanvas);
}
});
// --- prevent double injection ---
if (window.__signaturePatchedV3) {
console.log('[signature-tools] already patched');
return;
}
window.__signaturePatchedV3 = true;
// --- enforce color+width and capture undo before each stroke ---
const origStroke = CanvasRenderingContext2D.prototype.stroke;
CanvasRenderingContext2D.prototype.stroke = function(){
try {
const canvas = this.canvas;
if (canvas && canvas.classList && canvas.classList.contains('editable-canvas__canvas')) {
lastActiveCanvas = canvas;
// Snapshot BEFORE drawing for undo
pushUndo(this);
// Apply desired pen settings just-in-time
const prevStroke = this.strokeStyle;
const prevWidth = this.lineWidth;
const useColor = isValidHex(prefs.color) ? prefs.color : defaults.color;
const useWidth = Math.min(16, Math.max(1, Number(prefs.width)||defaults.width));
this.strokeStyle = useColor;
this.lineWidth = useWidth;
const res = origStroke.apply(this, arguments);
// Restore previous values
this.strokeStyle = prevStroke;
this.lineWidth = prevWidth;
return res;
}
} catch(e) {}
return origStroke.apply(this, arguments);
};
// Keep fill() aligned with chosen color
const origFill = CanvasRenderingContext2D.prototype.fill;
CanvasRenderingContext2D.prototype.fill = function(){
try {
const canvas = this.canvas;
if (canvas && canvas.classList && canvas.classList.contains('editable-canvas__canvas')) {
lastActiveCanvas = canvas;
const prevFill = this.fillStyle;
const useColor = isValidHex(prefs.color) ? prefs.color : defaults.color;
this.fillStyle = useColor;
const res = origFill.apply(this, arguments);
this.fillStyle = prevFill;
return res;
}
} catch(e){}
return origFill.apply(this, arguments);
};
// Detect canvases appearing later
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
m.addedNodes && m.addedNodes.forEach(node => {
if (node.nodeType===1) {
const target = node.matches?.('canvas.editable-canvas__canvas')
? node
: node.querySelector?.('canvas.editable-canvas__canvas');
if (target) {
console.log('[signature-tools] signature canvas detected – patch active');
lastActiveCanvas = target;
updateUndoState();
}
}
});
}
});
observer.observe(document.documentElement || document.body, { childList: true, subtree: true });
console.log('[signature-tools] ready. color:', prefs.color, 'width:', prefs.width, 'MAX_UNDO:', MAX_UNDO);
})();
</script>