skip to Main Content

Using JS (or jQuery) and CSS, I am trying to build a web page that enables the positioning of one image inside a very specific rectangular area of another image. Think of how Paste Into works in Photoshop; that is conceptually what I’m trying to achieve.

I was looking deeply into the excellent cropperjs library, thinking that would provide an efficient way to do this (without reinventing a bunch of wheels) and so I made this post within the cropperjs forum. But the developer of cropperjs posted this as a response:

I think you may need another library, this library may not fit your
purpose.

Now to describe my need in more detail:

Begin with IMAGE #1. This will be an image on the web page that doesn’t move, crop, or change in any way. BUT it will have a rectangular region within its boundaries (defined by me) inside which another image (IMAGE #2) must be placed. This placement needs to behave similarly to Photoshop’s Paste Into, in that IMAGE #2 will not be distorted in any way: it will be placed inside the predefined region of IMAGE #1 and able to drag/position around (by mouse on a PC or finger on a phone). Much of IMAGE #2 will be cropped away and invisible.

Only the portion inside the defined rectangular region will be visible. Within that region, IMAGE #2 will overlay/cover/obscure the region of IMAGE #1. So a portion of IMAGE #2 will be on top of IMAGE #1, but IMAGE #2 can only live within the predefined region of IMAGE #1. IMAGE #2 needs to be draggable within that region. When the desired positioning is complete, I need to enable the user (via js) to capture and save the entire composite image.

Sorry to belabor the point, but in case that’s not clear, let me describe it pictorially:

IMAGE #1 (fixed on the page):

enter image description here

Notice the blue border: that is my predefined region. Everything outside that region will remain the same. Everything inside that region will be covered by a portion of IMAGE #2.

IMAGE #2:

enter image description here

A cropped version of IMAGE #2 will appear inside the predefined (blue-border) region of IMAGE #1, and the user will be able to drag it around as desired until the position is satisfactory.

IMAGE #3 (the final composite):

enter image description here

Finally, I need the user to be able to click a button that (calling some JS) captures the composite image and enables it to be saved, or transmitted via ajax to my server. The final image needs to be high-res without any compression or degradation: just as high-res as the original images on the page.

I’m OK using a third-party library. I’m also OK doing this from scratch. But I need some guidance as to the simplest and most efficient way to do it. I’m very comfortable with HTML, CSS, JavaScript, and jQuery. Any pointers to a working example of something similar, or a library that does it, is all I’m looking for.

2

Answers


  1. Here’s a small standalone example of how to do something like this with the Canvas 2D Context API.

    In your example, your user would probably not choose the base image themselves but you’d get it from your server or whatnot.

    /**
     * Read an `Image` out of an user-selected file.
     */
    function getImageFromInput(input) {
      return new Promise((resolve, reject) => {
        const fr = new FileReader();
        fr.onload = () => {
          const img = new Image();
          img.onload = () => resolve(img);
          img.src = fr.result;
        };
        fr.onerror = reject;
        fr.readAsDataURL(input.files[0]);
      });
    }
    
    async function render() {
      let baseImg;
      let overlayImg;
      try {
        baseImg = await getImageFromInput(document.getElementById("base"));
        overlayImg = await getImageFromInput(document.getElementById("overlay"));
      } catch (e) {
        return;
      }
      const canvas = document.getElementById("canvas");
      const ctx = canvas.getContext("2d");
      canvas.width = baseImg.width;
      canvas.height = baseImg.height;
      ctx.drawImage(baseImg, 0, 0, canvas.width, canvas.height);
      // These would be hard-coded according to your base image.
      const x0 = canvas.width * 0.2;
      const y0 = canvas.height * 0.2;
      const x1 = canvas.width * 0.8;
      const y1 = canvas.height * 0.8;
      ctx.drawImage(overlayImg, 0, 0, overlayImg.width, overlayImg.height, x0, y0, x1 - x0, y1 - y0);
    }
    Choose base image:
    <input type="file" id="base" accept="image/*" onchange="render()">
    <br>
    Choose overlay image:
    <input type="file" id="overlay" accept="image/*" onchange="render()">
    <br>
    <canvas width="800" height="600" id="canvas"></canvas>
    Login or Signup to reply.
  2. As you mentioned in your comment, you could use draggable functionality from jQuery UI to enable positioning for the second image. You could also use html2canvas to capture your montage.

    Since I got a cross-origin issue with the built-in snippet on Stack Overflow, please take a look at this JSFiddle. I put the code below too:

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery-ui.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js"></script>
    
    <p>Copy a first image and paste it here (Ctrl-V):</p>
    <div id="frame"></div>
    <div id="container">
      <div></div>
      <div></div>
    </div>
    
    #frame,
    #container div,
    #container img,
    button {
      position: absolute;
    }
    
    #frame {
      width: 300px;
      height: 200px;
      border: solid 5px #22bbf6;
      background-color: transparent;
      z-index: 3;
      pointer-events: none;
    }
    
    #container div:first-child {
      z-index: 1;
    }
    
    #container div:last-child {
      width: 310px;
      height: 210px;
      overflow: hidden;
      z-index: 2;
      pointer-events: none;
    }
    
    #container img {
      z-index: -1;
      pointer-events: auto;
    }
    
    button {
      top: 270px;
      cursor: pointer;
    }
    
    $(document).ready(() => {
    
      let counter = 0,
          $body = $('body'),
          $p = $('p'),
          $container = $('#container'),
          frame = document.getElementById('frame');
    
      document.onpaste = e => {
        let fileReader = new FileReader(),
            clipItems = e.clipboardData.items;
    
        fileReader.onload = e => {
          counter++;
    
          let img = document.createElement('img');
    
          if (counter === 1 || counter === 2) {
            img.src = e.target.result;
            img.height = frame.offsetHeight;
          }
    
          if (counter === 1) {
            $p.text('Copy a second image and paste it here (Ctrl-V):');
            $container.children().first().append(img);
          } else if (counter === 2) {
            $p.text('You can adjust the position of your second image and capture your montage:');
            $container.children().last().append(img);
            $(img).draggable();
            $(img).css('cursor', 'grab');
            $body.append('<button type="button">Capture</button>');
          }
        };
    
        fileReader.readAsDataURL(clipItems[clipItems.length - 1].getAsFile());
      }
    
      $body.on('click', 'button', () => {
        $container.css({ width: '310px', height: '210px' });
    
        $(frame).css('border', 'none');
    
        html2canvas($container[0], { scale: 2 }).then(canvas => {
          $p.text('Result:');
          $('#container, button').hide();
          $body.append(canvas);
        });
      });
      
    });
    

    I used a bit of Vanilla JS in there, especially when I found it more convenient than jQuery.

    Notice that you can modify image quality through the scale option (see html2canvas options).


    EDIT 1

    As explained in my comment, I initially put a very restrictive height to avoid large images for my demo. Your test shows that your first image has been reduced too much, so you do not see that its width can actually be larger than the frame. This is because it depends on the aspect ratio…

    To give you a more insightful answer, I edited my JSFiddle. Now you can select the area (using draggable and resizable interactions from jQuery UI) before pasting the second image. I also defined a less restrictive height and added html2canvas options to make it work.

    Here is the updated code:

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/themes/base/jquery-ui.min.css">
    
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery-ui.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js"></script>
    
    <p>Copy a first image and paste it here (Ctrl-V):</p>
    <div id="frame"></div>
    <div id="container">
      <div></div>
      <div></div>
    </div>
    
    #frame,
    #container,
    #container div,
    #container img {
      position: absolute;
    }
    
    #frame {
      width: 300px;
      height: 200px;
      border: solid 5px #22bbf6;
      background-color: transparent;
      z-index: 3;
    }
    
    #container div:first-child {
      z-index: 1;
    }
    
    #container div:last-child {
      overflow: hidden;
      z-index: 2;
      pointer-events: none;
    }
    
    #container img {
      z-index: -1;
      pointer-events: auto;
    }
    
    button {
      margin-left: 5px;
      cursor: pointer;
    }
    
    $(document).ready(() => {
    
      let counter = 0,
          $body = $('body'),
          $p = $('p'),
          $container = $('#container'),
          $lastChild = $container.children().last(),
          frame = document.getElementById('frame');
          
      const verticalOffset = 3;
    
      function dragResizeCallback(event) {
        $lastChild.width(event.target.offsetWidth);
        $lastChild.height(event.target.clientHeight + verticalOffset);
        $lastChild.offset({
          top: event.target.offsetTop,
          left: event.target.offsetLeft
        });
      }
    
      document.onpaste = e => {
        let fileReader = new FileReader(),
            clipItems = e.clipboardData.items;
    
        fileReader.onload = e => {
          counter++;
    
          let img = document.createElement('img');
    
          if (counter === 1 || counter === 2) {
            img.src = e.target.result;
            img.height = 350;
          }
    
          if (counter === 1) {
            $p.text('Drag/resize the frame, then copy a second image and paste it here (Ctrl-V):');
            $container.children().first().append(img);
            $(frame).css('cursor', 'move');
            
            $(frame).draggable({
              stop: (e) => dragResizeCallback(e)
            });
            
            $(frame).resizable({
              stop: (e) => dragResizeCallback(e)
            });
          } else if (counter === 2) {
            $p.text('You can adjust the position of your second image and capture your montage:');
            $container.children().last().append(img);
            $(img).draggable();
            $(img).css('cursor', 'grab');
            $(frame).css('pointer-events', 'none');
            $p.append('<button type="button">Capture</button>');
          }
        };
    
        fileReader.readAsDataURL(clipItems[clipItems.length - 1].getAsFile());
      }
    
      $body.on('click', 'button', () => {
        $(frame).resizable('destroy');
        $(frame).css('border', 'none');
    
        html2canvas($container[0], {
          width: $lastChild.width(),
          height: $lastChild.height(),
          x: $lastChild.position().left,
          y: $lastChild.position().top,
          scale: 2
        }).then(canvas => {
          $p.text('Result:');
          $('#container, button').hide();
          $body.append(canvas);
        });
      });
      
    });
    

    EDIT 2

    If you do not want to ignore what is outside the crop area in your export, you can define a $firstChild variable containing $container.children().first() (above $lastChild declaration and initialization) then configure html2canvas like this:

    html2canvas($container[0], {
      width: $firstChild.find('img').width(),
      height: $firstChild.find('img').height(),
      x: $firstChild.position().left,
      y: $firstChild.position().top,
      scale: 2
    })
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search