Pasting images from clipboard

I consider building an application with Wappler using SQLite 3 as the database.

However, one feature I strongly want is for users to paste images from their clipboards into a field/container in the application. These images have never been saved to disk. Think of it as a BLOB, where the end user submits the content on the clipboard to the application without saving it to an image file on disk.

Is this feasible?

Hello @GAM and welcome to the community..

I don't think there's a Wappler way to do that, but you can check this example:

https://codepen.io/Lolopicker/pen/qBqBXvK

So using some javascript could be possible :slight_smile:

1 Like

Thanks, I was starting to suspect that JavaScript may be needed, particularly after checking out this one. The images are typically small and could be encoded as BASE64 and stored in the database. I probably need to try this out with just JavaScript first, but it could be the solution that will work for me.

You can also use this extension if you're using nodejs:

Then you can save it as a png/jpg file

Have you considered saving the image and placing a link in the database. Its a much simpler and effective solution.

O'mighty Deepseek! I seek your wisdom. I'd like some solution to allow a User to paste the contents of their clipboard, an image for this purpose, to a web form, and have the value encoded to base 64 for saving to a database by echoing the value to a hidden form input within the form. So would need an area where the user could paste the contents in to and a corresponding hidden field in base 64. Can you think of a stylish and intuitive way of accomplishing this in a cross browser way?

Clipboard Image to Base64 Solution

Here's a stylish, cross-browser solution that allows users to paste images from their clipboard into a web form and converts them to Base64 for database storage:

HTML Structure

<div class="image-paste-container">
  <div class="paste-area" id="pasteArea">
    <div class="paste-instructions">
      <i class="paste-icon">📋</i>
      <p>Click here and paste (Ctrl+V) an image</p>
    </div>
    <img id="previewImage" style="display: none; max-width: 100%; max-height: 300px;">
  </div>
  <input type="hidden" name="image_data" id="imageData">
  <div class="paste-actions">
    <button type="button" id="clearButton" disabled>Clear Image</button>
  </div>
</div>

CSS Styling

.image-paste-container {
  max-width: 600px;
  margin: 20px auto;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.paste-area {
  border: 2px dashed #ccc;
  border-radius: 8px;
  padding: 20px;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
  background-color: #f9f9f9;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.paste-area:hover {
  border-color: #999;
  background-color: #f0f0f0;
}

.paste-area.highlight {
  border-color: #4CAF50;
  background-color: #e8f5e9;
}

.paste-instructions {
  color: #666;
}

.paste-icon {
  font-size: 48px;
  margin-bottom: 10px;
}

.paste-actions {
  margin-top: 10px;
  text-align: right;
}

#clearButton {
  padding: 8px 16px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

#clearButton:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

JavaScript Implementation

document.addEventListener('DOMContentLoaded', function() {
  const pasteArea = document.getElementById('pasteArea');
  const previewImage = document.getElementById('previewImage');
  const imageData = document.getElementById('imageData');
  const clearButton = document.getElementById('clearButton');
  
  // Handle paste event
  pasteArea.addEventListener('click', function() {
    this.focus();
  });
  
  pasteArea.addEventListener('paste', function(e) {
    e.preventDefault();
    
    // Check if clipboard data contains files
    const items = (e.clipboardData || window.clipboardData).items;
    
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf('image') !== -1) {
        const blob = items[i].getAsFile();
        const reader = new FileReader();
        
        reader.onload = function(event) {
          previewImage.src = event.target.result;
          previewImage.style.display = 'block';
          imageData.value = event.target.result;
          pasteArea.classList.add('highlight');
          clearButton.disabled = false;
          
          // Hide instructions
          document.querySelector('.paste-instructions').style.display = 'none';
        };
        
        reader.readAsDataURL(blob);
        return;
      }
    }
    
    alert('No image found in clipboard. Please copy an image first.');
  });
  
  // Handle drag over
  pasteArea.addEventListener('dragover', function(e) {
    e.preventDefault();
    pasteArea.classList.add('highlight');
  });
  
  // Handle drag leave
  pasteArea.addEventListener('dragleave', function() {
    pasteArea.classList.remove('highlight');
  });
  
  // Handle drop
  pasteArea.addEventListener('drop', function(e) {
    e.preventDefault();
    pasteArea.classList.remove('highlight');
    
    const files = e.dataTransfer.files;
    if (files.length) {
      handleImageFile(files[0]);
    }
  });
  
  // Clear button
  clearButton.addEventListener('click', function() {
    previewImage.src = '';
    previewImage.style.display = 'none';
    imageData.value = '';
    pasteArea.classList.remove('highlight');
    clearButton.disabled = true;
    document.querySelector('.paste-instructions').style.display = 'block';
  });
  
  function handleImageFile(file) {
    if (!file.type.match('image.*')) {
      alert('Please select an image file.');
      return;
    }
    
    const reader = new FileReader();
    reader.onload = function(event) {
      previewImage.src = event.target.result;
      previewImage.style.display = 'block';
      imageData.value = event.target.result;
      clearButton.disabled = false;
      document.querySelector('.paste-instructions').style.display = 'none';
    };
    reader.readAsDataURL(file);
  }
});

Features

  1. Intuitive UI:

    • Clear instructions with visual icon
    • Drag-and-drop support in addition to paste
    • Visual feedback when active/hovered
  2. Cross-browser Compatibility:

    • Works with both modern clipboard API and older window.clipboardData
    • Graceful fallbacks
  3. User Experience:

    • Immediate image preview
    • Clear button to remove the image
    • Visual feedback during interactions
  4. Technical Implementation:

    • Converts image to Base64 automatically
    • Stores result in hidden input for form submission
    • Handles both direct paste and file drops

Server-side Consideration

Remember that Base64 images can be quite large. You may want to:

  1. Validate the image size on the client before submission
  2. Consider compressing the image before storing
  3. Set appropriate size limits in your server-side validation

Haven't got time to test it right now but DeepSeek seems to get things right first time unlike other Models which enjoy wasting thousands of tokens (am very sure this is by design)... Cough cough, Claude, cough cough...

Yes, but that would kill the concept. It would be too much overhead.
The images would be small notices from typically old online newspapers, normally taking up only a few percent of a newspaper page.

I do plan to offer the option of adding image files, though, but if I can't make it work with just a paste from the clipboard, then it would be too cumbersome to work effectively.

Interesting, I've found some similar examples online. Need to check into this and try it out. It seems that the work needs to be done mainly with JavaScript, though, so not so much a Wappler issue.

Definitely something that is outside the scope of Wappler natively and would require a custom implementation such as the one outlined above. Good luck with whatever way to select to implement it. Be sure to update the Topic when you have a solution as am sure others would be interested.

if you have them paste a link to an image rather than the actual image then this would be compatible with the image upload component

It is not feasible to paste a link to an image, as the main source is a site that publishes content with links to whole pages, and only a tiny fraction of a page is of interest. To make things "worse", these pages are actually tiles of small images presented together as one big image (to facilitate zooming in and out, as they have several sizes of these tiles on the server side).

So the easy way to handle this is to take a snapshot of the relevant part of the page.

Can you.not just right click on the required image and select "copy link"?

No, that won't work. Only a fraction of the single or double page is wanted, probably 1% - 5% of a newspaper page.

Here is an ugly but working example using the codepen code example:

Server connect is using the base64tofile extension (https://community.wappler.io/t/base64tofile-custom-module-nodejs/45949):

Then on the client side:
image

Where text2 is a hidden input with: {{pasted_image}} value like:
dmx-bind:value="pasted_image" type="hidden"
(pasted_image will be set on our javascript code)

This is the content page:

<div class="container">
    <form is="dmx-serverconnect-form" id="serverconnectform1" method="post" action="/api/wapplercommunity/imagefromclipboard">
        <input id="text1" autocomplete="off" name="name" type="text" class="form-control">
        <input id="text2" name="image" class="form-control" dmx-bind:value="pasted_image" type="hidden">
        <div class="containerimage">
            <div class="message">
                <div class="shortcut">
                    <span class="shortcut__key">Ctrl</span>
                    <span class="shortcut__sign">+</span>
                    <span class="shortcut__key">V</span>
                </div>
                <div class="message__text">
                    to paste image from clipboard
                </div>
            </div>
            <img class="image" />
        </div>
        <button id="btn1" class="btn" type="submit">Submit</button>

    </form>
</div>
<style>
    @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500&display=swap");

    :root {
        --color-light: #fff;
        --color-dark: #222;
        --color-border: rgba(255, 255, 255, 0.8);
    }

    * {
        margin: 0;
        padding: 0;
        border: none;
        box-sizing: border-box;
    }

    /* body {
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        background: var(--color-dark);
    } */

    .containerimage {
        width: 600px;
        height: 300px;
        max-width: 100%;
        max-height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 6px;
        border: 6px dashed var(--color-border);
        border-radius: 10px;
        overflow: hidden;
    }

    .message {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-family: "Roboto Mono", monospace;
        font-size: 20px;
        color: var(--color-light);
    }

    .message__text {
        margin-top: 12px;
        text-align: center;
    }

    .shortcut {
        display: flex;
        align-items: center;
    }

    .shortcut__key {
        padding: 6px 12px;
        border: 2px solid var(--color-light);
        border-radius: 6px;
    }

    .shortcut__sign {
        margin: 0 6px;
    }

    .image {
        max-width: 100%;
        max-height: 100%;
    }

    .hidden {
        display: none;
    }
</style>




<script>
    const imageElement = document.querySelector(".image");
const messageElement = document.querySelector(".message");

function setImage(src) {
	imageElement.src = src;
}


function hideMessage() {
	messageElement.classList.add("hidden");
}

function loadBlobImageSrc(blob) {
	return new Promise((resolve) => {
		const reader = new FileReader();
		reader.onload = (data) => {
			resolve(data.target.result);
		};
		reader.readAsDataURL(blob);
	});
}

document.body.addEventListener("paste", (event) => {
	const clipboardData = event.clipboardData || event.originalEvent.clipboardData;
	const imageItem = [...clipboardData.items].find((item) =>
		item.type.includes("image/")
	);

	if (!imageItem) {
		console.log("No image items in clipboard");
		return;
	}

	const blob = imageItem.getAsFile();

	if (blob === null) {
		console.log("Can not get image data from clipboard item");
		return;
	}

	loadBlobImageSrc(blob).then((src) => {
		hideMessage();
		setImage(src);
        
        const base64Data = src.replace(/^data:image\/[a-zA-Z]+;base64,/, "");
        dmx.global.set('pasted_image', base64Data);
	});
});
</script>

You can see I added a few things to the original code:

  1. const base64Data = src.replace(/^data:image\/[a-zA-Z]+;base64,/, ""); because the base64tofile extension need the string withouth the header.
  2. dmx.global.set('pasted_image', base64Data); that will set the pasted_image used on the hidden text input.

Also changed class container to containerimage because we don't want to fight with the original one..

Hope it helps :slight_smile: