Search code examples
javascriptimagerastercoordinate-systemspaperjs

Paper.js Subraster Selecting Wrong Area


I'm working in a Paper.js project where we're essentially doing image editing. There is one large Raster. I'm attempting to use the getSubRaster method to copy a section of the image (raster) that the user can then move around.

After the raster to edit is loaded, selectArea is called to register these listeners:

var selectArea = function() {
  if(paper.project != null) {
    var startDragPoint;

    paper.project.layers[0].on('mousedown', function(event) { // TODO should be layer 0 in long run? // Capture start of drag selection
      if(event.event.ctrlKey && event.event.altKey) {
        startDragPoint = new paper.Point(event.point.x + imageWidth/2, (event.point.y + imageHeight/2));
        //topLeftPointOfSelectionRectangleCanvasCoordinates = new paper.Point(event.point.x, event.point.y);
      }
    });
    paper.project.layers[0].on('mouseup', function(event) { // TODO should be layer 0 in long run? // Capture end of drag selection
      if(event.event.ctrlKey && event.event.altKey) {
        var endDragPoint = new paper.Point(event.point.x + imageWidth/2, event.point.y + imageHeight/2);

        // Don't know which corner user started dragging from, aggregate the data we have into the leftmost and topmost points for constructing a rectangle
        var leftmostX;
        if(startDragPoint.x < endDragPoint.x) {
          leftmostX = startDragPoint.x;
        } else {
          leftmostX = endDragPoint.x;
        }
        var width = Math.abs(startDragPoint.x - endDragPoint.x);

        var topmostY;
        if(startDragPoint.y < endDragPoint.y) {
          topmostY = startDragPoint.y;
        } else {
          topmostY = endDragPoint.y;
        }
        var height = Math.abs(startDragPoint.y - endDragPoint.y);

        var boundingRectangle = new paper.Rectangle(leftmostX, topmostY, width, height);
        console.log(boundingRectangle);
        console.log(paper.view.center);
        var selectedArea = raster.getSubRaster(boundingRectangle);

        var selectedAreaAsDataUrl = selectedArea.toDataURL();
        var subImage = new Image(width, height);
        subImage.src = selectedAreaAsDataUrl;

        subImage.onload = function(event) {
          var subRaster = new paper.Raster(subImage);

          // Make movable
          subRaster.onMouseEnter = movableEvents.showSelected;
          subRaster.onMouseDrag = movableEvents.dragItem;
          subRaster.onMouseLeave = movableEvents.hideSelected;
        };        
      }
    });
  }
};

The methods are triggered at the right time and the selection box seems to be the right size. It does indeed render a new raster for me that I can move around, but the contents of the raster are not what I selected. They are close to what I selected but not what I selected. Selecting different areas does not seem to yield different results. The content of the generated subraster always seems to be down and to the right of the actual selection.

Note that as I build the points for the bounding selection rectangle I do some translations. This is because of differences in coordinate systems. The coordinate system where I've drawn the rectangle selection has (0,0) in the center of the image and x increases rightward and y increases downward. But for getSubRaster, we are required to provide the pixel coordinates, per the documentation, which start at (0,0) at the top left of the image and increase going rightward and downward. Consequently, as I build the points, I translate the points to the raster/pixel coordinates by adding imageWidth/2 and imageHeight/2`.

So why does this code select the wrong area? Thanks in advance.

EDIT:

Unfortunately I can't share the image I'm working with because it is sensitive company data. But here is some metadata:

  • Image Width: 4250 pixels
  • Image Height: 5500 pixels
  • Canvas Width: 591 pixels
  • Canvas Height: 766 pixels

My canvas size varies by the size of the browser window, but those are the parameters I've been testing in. I don't think the canvas dimensions are particularly relevant because I'm doing everything in terms of image pixels. When I capture the event.point.x and event.point.y to the best of my knowledge these are image scaled coordinates, but from a different origin - the center rather than the top left. Unfortunately I can't find any documentation on this. Observe how the coordinates work in this sketch.

I've also been working on a sketch to illustrate the problem of this question. To use it, hold Ctrl + Alt and drag a box on the image. This should trigger some logging data and attempt to get a subraster, but I get an operation insecure error, which I think is because of security settings in the image request header. Using the base 64 string instead of the URL doesn't give the security error, but doesn't do anything. Using that string in the sketch produces a super long URL I can't paste here. But to get that you can download the image (or any image) and convert it here, and put that as the img.src.


Solution

  • The problem is that the mouse events all return points relative to 0, 0 of the canvas. And getSubRaster expects the coordinates to be relative to the 0, 0 of the raster item it is extracting from.

    The adjustment needs to be eventpoint - raster.bounds.topLeft. It doesn't really have anything to do with the image width or height. You want to adjust the event points so they are relative to 0, 0 of the raster, and 0, 0 is raster.bounds.topLeft.

    When you adjust the event points by 1/2 the image size that causes event points to be offset incorrectly. For the Mona Lisa example, the raster size (image size) is w: 320, h: 491; divided by two they are w: 160, h: 245.5. But bounds.topLeft of the image (when I ran my sketch) was x: 252.5, y: 155.5.

    Here's a sketch that shows it working. I've added a little red square highlighting the selected area just to make it easier to compare when it's done. I also didn't include the toDataURL logic as that creates the security issues you mentioned.

    Here you go: Sketch

    Here's code I put into an HTML file; I noticed that the sketch I put together links to a previous version of the code that doesn't completely work.

    <!doctype html>
    <html lang="en">
    
    <head>
    <meta charset="utf-8">
    <title>Rasters</title>
    <script src="./vendor/jquery-2.1.3.js"></script>
    <script src="./vendor/paper-0.9.25.js"></script>
    </head>
    <body>
    <main>
        <h3>Raster Bug</h3>
        <div>
            <canvas id="canvas"></canvas>
        </div>
        <div id="position">
    
        </div>
    </main>
    
    <script>
     // initialization code
     $(function() {
         // setup paper
         $("#canvas").attr({width: 600, height: 600});
         var canvas = document.getElementById("canvas");
         paper.setup(canvas);
    
         // show a border to make range of canvas clear
         var border = new paper.Path.Rectangle({
             rectangle: {point: [0, 0], size: paper.view.size},
             strokeColor: 'black',
             strokeWidth: 2
         });
    
         var tool = new paper.Tool();
    
         // setup mouse position tracking
         tool.on('mousemove', function(e) {
             $("#position").text("mouse: " + e.point);
         });
    
         // load the image from a dataURL to avoid CORS issues
         var raster = new paper.Raster(dataURL);
         raster.position = paper.view.center;
    
         var lt = raster.bounds.topLeft;
         var startDrag, endDrag;
    
         console.log('rb', raster.bounds);
         console.log('lt', lt);
    
         // setup mouse handling
         tool.on('mousedown', function(e) {
             startDrag = new paper.Point(e.point);
             console.log('sd', startDrag);
         });
         tool.on('mousedrag', function(e) {
             var show = new paper.Path.Rectangle({
                 from: startDrag,
                 to: e.point,
                 strokeColor: 'red',
                 strokeWidth: 1
             });
             show.removeOn({
                 drag: true,
                 up: true
             });
         });
         tool.on('mouseup', function(e) {
             endDrag = new paper.Point(e.point);
             console.log('ed', endDrag);
    
             var bounds = new paper.Rectangle({
                 from: startDrag.subtract(lt),
                 to: endDrag.subtract(lt)
             });
             console.log('bounds', bounds);
    
             var sub = raster.getSubRaster(bounds);
             sub.bringToFront();
    
             var subData = sub.toDataURL();
             sub.remove();
    
             var subRaster = new paper.Raster(subData);
             subRaster.position = paper.view.center;
    
         });
    
     });
    
     var dataURL = ; // insert data or real URL here
    
     </script>
    </body>
    </html>