How to Build an Image Cropper Web Component
Image cropping is a common image processing task where we only need a potion of an image.
In most cases, we make a rectangular selection box for cropping.
But for cases like document scanning, we need to make a polygonal selection box. Perspective transformation is needed to normalize the document.
In this article, we are going to use Stencil.js to create an image cropper web component. We can use it to make a rectangular or polygonal selection box and get the cropped image.
Dynamsoft Document Normalizer is used to detect document borders and normalize images.
New Project
Run the following command to create a new Stencil Component project:
npm init stencil
Generate a New Component
Generate a new component named image-cropper
.
npx stencil g image-cropper
To test the component, we can modify the src\index.html
file:
- <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>
+ <image-cropper>
+ <p>Inner elements</p>
+ </image-cropper>
Then run the following command to test it:
npm run start
Add an SVG Element
In the JSX, add an SVG element. We are going to use it to display the image and allow users to adjust the selection.
svgElement:SVGElement;
render() {
return (
<Host>
<svg
version="1.1"
ref={(el) => this.svgElement = el as SVGElement}
class="cropper-svg"
xmlns="http://www.w3.org/2000/svg"
>
</svg>
<slot></slot>
</Host>
);
}
Display a Local Image in the SVG Element
-
Add an
img
prop to the component.@Prop() img?: HTMLImageElement;
-
Add an
image
element to the SVG element.<svg version="1.1" ref={(el) => this.svgElement = el as SVGElement} class="cropper-svg" xmlns="http://www.w3.org/2000/svg" > <image href={this.img ? this.img.src : ""}></image> </svg>
-
Watch the
img
prop and update theviewBox
state used by the SVG element.@State() viewBox:string = "0 0 1280 720"; @Watch('img') watchImgPropHandler(newValue: HTMLImageElement) { if (newValue) { this.viewBox = "0 0 "+newValue.naturalWidth+" "+newValue.naturalHeight; } } render() { return ( <Host> <svg version="1.1" ref={(el) => this.svgElement = el as SVGElement} class="cropper-svg" xmlns="http://www.w3.org/2000/svg" > <image href={this.img ? this.img.src : ""}></image> </svg> <slot></slot> </Host> ); }
-
In
index.html
, load an image file to a new image element, pass it to the cropper and display the cropper.<!DOCTYPE html> <html dir="ltr" lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" /> <title>Stencil Component Starter</title> <script type="module" src="/build/image-cropper-component.esm.js"></script> <script nomodule src="/build/image-cropper-component.js"></script> <style> img { max-width: 100%; max-height: 350px; } #cropper { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } </style> </head> <body> <div> Load local image: <input type="file" id="file" onchange="loadImageFromFile();" accept=".jpg,.jpeg,.png,.bmp" /> <div> <button onclick="openCropper()">Open Cropper</button> </div> </div> <div> <div>Original:</div> <div class="orginalImageContainer"></div> <div>Cropped:</div> <div class="croppedImageContainer"> <img id="cropped" /> </div> </div> <div id="cropper" style="display:none;"> <image-cropper></image-cropper> </div> </body> <script> function loadImageFromFile() { let files = document.getElementById('file').files; if (files.length == 0) { return; } let file = files[0]; fileReader = new FileReader(); fileReader.onload = function(e){ let container = document.getElementsByClassName("orginalImageContainer")[0]; container.innerHTML = ""; let img = document.createElement("img"); img.src = e.target.result; img.id = "original"; container.appendChild(img); }; fileReader.onerror = function () { console.warn('oops, something went wrong.'); }; fileReader.readAsDataURL(file); } async function openCropper(){ if (!document.getElementById("original")) { return; } let cropper = document.querySelector("image-cropper"); cropper.img = document.getElementById("original"); document.getElementById("cropper").style.display = ""; } </script> </html>
Display a Selection Box
Next, we need to use SVG to draw a selection box like the following:
It is a polygon with eight handlers on its edges. Its SVG code is the following:
<svg version="1.1" class="cropper-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="320">
<defs>
<mask id="myMask">
<rect x="0" y="0" width="512" height="512" fill="white"></rect>
<rect x="72" y="46" width="14" height="14" fill="black"></rect>
<rect x="172" y="46" width="14" height="14" fill="black"></rect>
<rect x="272" y="46" width="14" height="14" fill="black"></rect>
<rect x="272" y="146" width="14" height="14" fill="black"></rect>
<rect x="272" y="246" width="14" height="14" fill="black"></rect>
<rect x="172" y="246" width="14" height="14" fill="black"></rect><rect x="72" y="246" width="14" height="14" fill="black"></rect>
<rect x="72" y="146" width="14" height="14" fill="black"></rect>
</mask>
</defs>
<polygon mask="url(#myMask)" points="79,53 279,53 279,253 79,253" stroke="orange" stroke-width="2" fill="transparent"></polygon>
<rect x="72" y="46" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="172" y="46" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="272" y="46" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="272" y="146" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="272" y="246" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="172" y="246" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="72" y="246" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent"></rect>
<rect x="72" y="146" width="14" height="14" stroke="orange" stroke-width="2" fill="transparent">
</rect>
</svg>
Here, we use SVG’s mask feature to mask out the polygon under the rectangular handlers.
Let’s implement this in the component’s class.
-
Define several geometry interfaces: Quad, Rect and Point.
export interface Quad{ points:[Point,Point,Point,Point]; } export interface Point{ x:number; y:number; } export interface Rect{ x:number; y:number; width:number; height:number; }
-
Add a state named points:
@State() points:[Point,Point,Point,Point] = undefined;
-
Add a
rect
prop and aquad
prop so that the users can pass initial values for the selection box.@Prop() rect?: Rect; @Prop() quad?: Quad; usingQuad = false; @Watch('rect') watchRectPropHandler(newValue: Rect) { if (newValue) { this.usingQuad = false; const point1:Point = {x:newValue.x,y:newValue.y}; const point2:Point = {x:newValue.x+newValue.width,y:newValue.y}; const point3:Point = {x:newValue.x+newValue.width,y:newValue.y+newValue.height}; const point4:Point = {x:newValue.x,y:newValue.y+newValue.height}; this.points = [point1,point2,point3,point4]; } } @Watch('quad') watchQuadPropHandler(newValue: Quad) { if (newValue) { this.usingQuad = true; this.points = newValue.points; } }
-
Update the SVG element to draw the polygon and rectangles.
<svg version="1.1" ref={(el) => this.svgElement = el as SVGElement} class="cropper-svg" xmlns="http://www.w3.org/2000/svg" viewBox={this.viewBox} > {this.renderHandlersMaskDefs()} <image href={this.img ? this.img.src : ""}></image> <polygon mask="url(#myMask)" points={this.getPointsData()} class="cropper-controls" stroke-width="2" fill="transparent" > </polygon> {this.renderHandlers()} </svg>
The
renderHandlers
function:handlers:number[] = [0,1,2,3,4,5,6,7]; @State() selectedHandlerIndex:number = -1; renderHandlers(){ if (!this.points) { return (<div></div>) } return ( <Fragment> {this.handlers.map(index => ( <rect x={this.getHandlerPos(index,"x")} y={this.getHandlerPos(index,"y")} width={this.getHandlerSize()} height={this.getHandlerSize()} class="cropper-controls" stroke-width={index === this.selectedHandlerIndex ? 4 * this.getRatio() : 2 * this.getRatio()} fill="transparent" onMouseDown={(e:MouseEvent)=>this.onHandlerMouseDown(e,index)} onMouseUp={(e:MouseEvent)=>this.onHandlerMouseUp(e)} onTouchStart={(e:TouchEvent)=>this.onHandlerTouchStart(e,index)} /> ))} </Fragment> ) }
The
renderHandlersMaskDefs
function:renderHandlersMaskDefs(){ if (!this.points) { return (<div></div>) } return ( <defs> <mask id="myMask"> <rect x="0" y="0" width={this.img ? this.img.naturalWidth : "0"} height={this.img ? this.img.naturalHeight : "0"} fill="white" /> {this.handlers.map(index => ( <rect x={this.getHandlerPos(index,"x")} y={this.getHandlerPos(index,"y")} width={this.getHandlerSize()} height={this.getHandlerSize()} fill="black" /> ))} </mask> </defs> ) }
Below are some helper functions. Please note that the 8 handlers’ positions are calculated based on the four points.
getHandlerPos(index:number,key:string) { let pos = 0; let size = this.getHandlerSize(); if (index === 0){ pos = this.points[0][key]; }else if (index === 1) { pos = this.points[0][key] + (this.points[1][key] - this.points[0][key])/2; }else if (index === 2) { pos = this.points[1][key]; }else if (index === 3) { pos = this.points[1][key] + (this.points[2][key] - this.points[1][key])/2; }else if (index === 4) { pos = this.points[2][key]; }else if (index === 5) { pos = this.points[3][key] + (this.points[2][key] - this.points[3][key])/2; }else if (index === 6) { pos = this.points[3][key]; }else if (index === 7) { pos = this.points[0][key] + (this.points[3][key] - this.points[0][key])/2; } pos = pos - size/2; return pos; } getHandlerSize() { let ratio = this.getRatio(); let size:number = 20; if (this.handlersize) { try { size = parseInt(this.handlersize); } catch (error) { console.log(error); } } return Math.ceil(size*ratio); }
-
In
index.html
, pass a rectangle for providing the initial coordinates.let cropper = document.querySelector("image-cropper"); cropper.rect = {x:50,y:50,width:200,height:200};
Add Mouse Events to Adjust the Selection
There are two ways to adjust the selection. Dragging the polygon to move the entire selection or dragging the handlers to adjust the points.
Drag the Polygon to Move the Entire Selection
-
Add
MouseDown
andMouseUp
events for the polygon.<polygon onMouseDown={(e:MouseEvent)=>this.onPolygonMouseDown(e)} onMouseUp={(e:MouseEvent)=>this.onPolygonMouseUp(e)} > </polygon>
-
In the mouse down event, record the clicked point and the points for the polygon as
originalPoints
when clicked.originalPoints:[Point,Point,Point,Point] = undefined; polygonMouseDown:boolean = false; polygonMouseDownPoint:Point = {x:0,y:0}; onPolygonMouseDown(e:MouseEvent){ e.stopPropagation(); this.originalPoints = JSON.parse(JSON.stringify(this.points)); this.polygonMouseDown = true; let coord = this.getMousePosition(e,this.svgElement); this.polygonMouseDownPoint.x = coord.x; this.polygonMouseDownPoint.y = coord.y; }
The mouse’s coordinates are converted to the coordinates in SVG with the following function:
//Convert the screen coordinates to the SVG's coordinates from https://www.petercollingridge.co.uk/tutorials/svg/interactive/dragging/ getMousePosition(event:any,svg:any) { let CTM = svg.getScreenCTM(); if (event.targetTouches) { //if it is a touch event let x = event.targetTouches[0].clientX; let y = event.targetTouches[0].clientY; return { x: (x - CTM.e) / CTM.a, y: (y - CTM.f) / CTM.d }; }else{ return { x: (event.clientX - CTM.e) / CTM.a, y: (event.clientY - CTM.f) / CTM.d }; } }
-
In the mouse up event, set the mouse down status to false and reset the selected handler’s index.
onPolygonMouseUp(e:MouseEvent){ e.stopPropagation(); this.selectedHandlerIndex = -1; this.polygonMouseDown = false; }
-
Add
MouseMove
andMouseUp
events for the SVG element.<svg onMouseUp={()=>this.onSVGMouseUp()} onMouseMove={(e:MouseEvent)=>this.onSVGMouseMove(e)} > </svg>
-
In the mouse up event for the SVG, set the mouse down status to false and reset the selected handler’s index as in step 2.
onSVGMouseUp(){ this.selectedHandlerIndex = -1; this.polygonMouseDown = false; }
-
In the mouse move event for the SVG, get the current coordinates of the mouse and calculate the points after moving to move the polygon.
onSVGMouseMove(e:MouseEvent){ if (this.polygonMouseDown) { let coord = this.getMousePosition(e,this.svgElement); let offsetX = coord.x - this.polygonMouseDownPoint.x; let offsetY = coord.y - this.polygonMouseDownPoint.y; let newPoints = JSON.parse(JSON.stringify(this.originalPoints)); for (const point of newPoints) { point.x = point.x + offsetX; point.y = point.y + offsetY; if (point.x < 0 || point.y < 0 || point.x > this.img.naturalWidth || point.y > this.img.naturalHeight){ console.log("reach bounds"); return; } } this.points = newPoints; } }
Drag a Selected Handler to Adjust the Selection
-
Add
MouseDown
andMouseUp
events for the rectangular handlers.{this.handlers.map(index => ( <rect onMouseDown={(e:MouseEvent)=>this.onHandlerMouseDown(e,index)} onMouseUp={(e:MouseEvent)=>this.onHandlerMouseUp(e)} /> ))}
-
In the mouse down event, record the clicked point and the points for the polygon as
originalPoints
when clicked. Record the clicked handler’s index, as well.onHandlerMouseDown(e:MouseEvent,index:number){ e.stopPropagation(); let coord = this.getMousePosition(e,this.svgElement); this.originalPoints = JSON.parse(JSON.stringify(this.points)); this.handlerMouseDownPoint.x = coord.x; this.handlerMouseDownPoint.y = coord.y; this.selectedHandlerIndex = index; }
-
In the mouse up event, set the selected handler’s index to -1.
onHandlerMouseUp(e:MouseEvent){ e.stopPropagation(); this.selectedHandlerIndex = -1; }
-
In the mouse move event for the SVG, update the points if a handler is selected. If the user passes a rectangle for cropping instead of a quadrilateral, then keep the selection rectangular.
onSVGMouseMove(e:MouseEvent){ if (this.polygonMouseDown) { let coord = this.getMousePosition(e,this.svgElement); let offsetX = coord.x - this.polygonMouseDownPoint.x; let offsetY = coord.y - this.polygonMouseDownPoint.y; let newPoints = JSON.parse(JSON.stringify(this.originalPoints)); for (const point of newPoints) { point.x = point.x + offsetX; point.y = point.y + offsetY; if (point.x < 0 || point.y < 0 || point.x > this.img.naturalWidth || point.y > this.img.naturalHeight){ console.log("reach bounds"); return; } } this.points = newPoints; } if (this.selectedHandlerIndex >= 0) { let pointIndex = this.getPointIndexFromHandlerIndex(this.selectedHandlerIndex); let coord = this.getMousePosition(e,this.svgElement); let offsetX = coord.x - this.handlerMouseDownPoint.x; let offsetY = coord.y - this.handlerMouseDownPoint.y; let newPoints = JSON.parse(JSON.stringify(this.originalPoints)); if (pointIndex != -1) { let selectedPoint = newPoints[pointIndex]; selectedPoint.x = this.originalPoints[pointIndex].x + offsetX; selectedPoint.y = this.originalPoints[pointIndex].y + offsetY; if (this.usingQuad === false) { //rect mode if (pointIndex === 0) { newPoints[1].y = selectedPoint.y; newPoints[3].x = selectedPoint.x; }else if (pointIndex === 1) { newPoints[0].y = selectedPoint.y; newPoints[2].x = selectedPoint.x; }else if (pointIndex === 2) { newPoints[1].x = selectedPoint.x; newPoints[3].y = selectedPoint.y; }else if (pointIndex === 3) { newPoints[0].x = selectedPoint.x; newPoints[2].y = selectedPoint.y; } } }else{ //mid-point handlers if (this.selectedHandlerIndex === 1) { newPoints[0].y = this.originalPoints[0].y + offsetY; newPoints[1].y = this.originalPoints[1].y + offsetY; }else if (this.selectedHandlerIndex === 3) { newPoints[1].x = this.originalPoints[1].x + offsetX; newPoints[2].x = this.originalPoints[2].x + offsetX; }else if (this.selectedHandlerIndex === 5) { newPoints[2].y = this.originalPoints[2].y + offsetY; newPoints[3].y = this.originalPoints[3].y + offsetY; }else if (this.selectedHandlerIndex === 7) { newPoints[0].x = this.originalPoints[0].x + offsetX; newPoints[3].x = this.originalPoints[3].x + offsetX; } } this.points = newPoints; } }
Add Touch Events to Adjust the Selection
For devices with a touch screen, we also need to add touch events. Basically, we need to do the following things:
- Do not trigger mouse events if a touch event is triggered.
TouchMove
andMouseMove
can share the same logic.TouchStart
is equivalent toMouseDown
andTouchEnd
is equivalent toMouseUp
. For touch devices, we can keep the touched handler as selected even if the touch ends and drag the SVG element to adjust it as it may be difficult to see the content under a handler when our finger is above it.
Add a Confirmation Footer
Create an optional footer for the users to cancel the operation or confirm the operation:
@Prop() hidefooter?: string;
@Event() confirmed?: EventEmitter<void>;
@Event() canceled?: EventEmitter<void>;
onCanceled(){
if (this.canceled){
console.log("emit");
this.canceled.emit();
}
}
onConfirmed(){
if (this.confirmed){
this.confirmed.emit();
}
}
renderFooter(){
if (this.hidefooter === "") {
return "";
}
return (
<div class="footer">
<section class="items">
<div class="item accept-cancel" onClick={() => this.onCanceled()}>
<img src="data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3E%3Ccircle fill='%23727A87' cx='256' cy='256' r='256'/%3E%3Cg id='Icon_5_'%3E%3Cg%3E%3Cpath fill='%23FFFFFF' d='M394.2,142L370,117.8c-1.6-1.6-4.1-1.6-5.7,0L258.8,223.4c-1.6,1.6-4.1,1.6-5.7,0L147.6,117.8 c-1.6-1.6-4.1-1.6-5.7,0L117.8,142c-1.6,1.6-1.6,4.1,0,5.7l105.5,105.5c1.6,1.6,1.6,4.1,0,5.7L117.8,364.4c-1.6,1.6-1.6,4.1,0,5.7 l24.1,24.1c1.6,1.6,4.1,1.6,5.7,0l105.5-105.5c1.6-1.6,4.1-1.6,5.7,0l105.5,105.5c1.6,1.6,4.1,1.6,5.7,0l24.1-24.1 c1.6-1.6,1.6-4.1,0-5.7L288.6,258.8c-1.6-1.6-1.6-4.1,0-5.7l105.5-105.5C395.7,146.1,395.7,143.5,394.2,142z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E" />
</div>
<div class="item accept-use" onClick={() => this.onConfirmed()}>
<img src="data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 512 512' enable-background='new 0 0 512 512' xml:space='preserve'%3E%3Ccircle fill='%232CD865' cx='256' cy='256' r='256'/%3E%3Cg id='Icon_1_'%3E%3Cg%3E%3Cg%3E%3Cpath fill='%23FFFFFF' d='M208,301.4l-55.4-55.5c-1.5-1.5-4-1.6-5.6-0.1l-23.4,22.3c-1.6,1.6-1.7,4.1-0.1,5.7l81.6,81.4 c3.1,3.1,8.2,3.1,11.3,0l171.8-171.7c1.6-1.6,1.6-4.2-0.1-5.7l-23.4-22.3c-1.6-1.5-4.1-1.5-5.6,0.1L213.7,301.4 C212.1,303,209.6,303,208,301.4z'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E" />
</div>
</section>
</div>
)
}
Get the Cropping Results
Add several methods to get the adjusted coordinates or the cropped image.
-
Getting the cropped image:
@Prop() license?: string; @Method() async getCroppedImage(perspectiveTransform?:boolean,colorMode?:"binary"|"gray"|"color"):Promise<string> { if (perspectiveTransform && window["Dynamsoft"]) { if (!this.cvr) { await this.initCVR(); } if (colorMode) { let templateName = "NormalizeDocument_Color"; if (colorMode === "binary") { templateName = "NormalizeDocument_Binary"; } else if (colorMode === "gray") { templateName = "NormalizeDocument_Gray"; } else { templateName = "NormalizeDocument_Color"; } } let quad = await this.getQuad(); let settings = await this.cvr.getSimplifiedSettings(templateName); settings.roi = quad; settings.roiMeasuredInPercentage = false; await this.cvr.updateSettings(templateName, settings); let normalizedImagesResult:CapturedResult = await this.cvr.capture(img,templateName); let normalizedImageResultItem:NormalizedImageResultItem = (normalizedImagesResult.items[0] as NormalizedImageResultItem); return normalizedImageResultItem.image.toCanvas().toDataURL(); }else{ let ctx = this.canvasElement.getContext("2d"); let rect = await this.getRect(); this.canvasElement.width = rect.width; this.canvasElement.height = rect.height; ctx.drawImage(this.img, rect.x, rect.y, rect.width, rect.height, 0, 0, rect.width, rect.height); return this.canvasElement.toDataURL(); } } async initCVR(){ window["Dynamsoft"]["License"]["LicenseManager"].initLicense(this.license); window["Dynamsoft"]["CVR"]["CaptureVisionRouter"].preloadModule(["DDN"]); this.cvr = await window["Dynamsoft"]["CVR"]["CaptureVisionRouter"].createInstance(); this.cvr.initSettings("{\"CaptureVisionTemplates\": [{\"Name\": \"Default\"},{\"Name\": \"DetectDocumentBoundaries_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-document-boundaries\"]},{\"Name\": \"DetectAndNormalizeDocument_Default\",\"ImageROIProcessingNameArray\": [\"roi-detect-and-normalize-document\"]},{\"Name\": \"NormalizeDocument_Binary\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-binary\"]}, {\"Name\": \"NormalizeDocument_Gray\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-gray\"]}, {\"Name\": \"NormalizeDocument_Color\",\"ImageROIProcessingNameArray\": [\"roi-normalize-document-color\"]}],\"TargetROIDefOptions\": [{\"Name\": \"roi-detect-document-boundaries\",\"TaskSettingNameArray\": [\"task-detect-document-boundaries\"]},{\"Name\": \"roi-detect-and-normalize-document\",\"TaskSettingNameArray\": [\"task-detect-and-normalize-document\"]},{\"Name\": \"roi-normalize-document-binary\",\"TaskSettingNameArray\": [\"task-normalize-document-binary\"]}, {\"Name\": \"roi-normalize-document-gray\",\"TaskSettingNameArray\": [\"task-normalize-document-gray\"]}, {\"Name\": \"roi-normalize-document-color\",\"TaskSettingNameArray\": [\"task-normalize-document-color\"]}],\"DocumentNormalizerTaskSettingOptions\": [{\"Name\": \"task-detect-and-normalize-document\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect-and-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect-and-normalize\"}]},{\"Name\": \"task-detect-document-boundaries\",\"TerminateSetting\": {\"Section\": \"ST_DOCUMENT_DETECTION\"},\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-detect\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-detect\"}]},{\"Name\": \"task-normalize-document-binary\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\", \"ColourMode\": \"ICM_BINARY\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-gray\", \"ColourMode\": \"ICM_GRAYSCALE\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}, {\"Name\": \"task-normalize-document-color\", \"ColourMode\": \"ICM_COLOUR\",\"StartSection\": \"ST_DOCUMENT_NORMALIZATION\",\"SectionImageParameterArray\": [{\"Section\": \"ST_REGION_PREDETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_DETECTION\",\"ImageParameterName\": \"ip-normalize\"},{\"Section\": \"ST_DOCUMENT_NORMALIZATION\",\"ImageParameterName\": \"ip-normalize\"}]}],\"ImageParameterOptions\": [{\"Name\": \"ip-detect-and-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}},{\"Name\": \"ip-detect\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0,\"ThresholdCompensation\" : 7}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7},\"ScaleDownThreshold\" : 512},{\"Name\": \"ip-normalize\",\"BinarizationModes\": [{\"Mode\": \"BM_LOCAL_BLOCK\",\"BlockSizeX\": 0,\"BlockSizeY\": 0,\"EnableFillBinaryVacancy\": 0}],\"TextDetectionMode\": {\"Mode\": \"TTDM_WORD\",\"Direction\": \"HORIZONTAL\",\"Sensitivity\": 7}}]}"); this.cvr.maxCvsSideLength = 99999; }
If perspective transformation is enabled, use Dynamsoft Document Normalizer to get the normalized image, otherwise, use Canvas to crop the image. When using Dynamsoft Document Normalizer, we can also set the color mode for the result: black & white, gray or color. To use Dynamsoft Document Normalizer, we need to include it via CDN and set a license for it.
Include the library in HTML:
<script src="https://cdn.jsdelivr.net/npm/dynamsoft-core@3.0.10/dist/core.js"></script> <script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-normalizer@2.0.11/dist/ddn.js"></script> <script src="https://cdn.jsdelivr.net/npm/dynamsoft-capture-vision-router@2.0.11/dist/cvr.js"></script>
Set the license:
<image-cropper license="your license"></image-cropper>
You can apply for a license here.
-
Getting the coordinates:
@Method() async getPoints():Promise<[Point,Point,Point,Point]> { return this.points; } @Method() async getQuad():Promise<Quad> { return {points:this.points}; } @Method() async getRect():Promise<Rect> { let minX:number; let minY:number; let maxX:number; let maxY:number; for (const point of this.points) { if (!minX) { minX = point.x; maxX = point.x; minY = point.y; maxY = point.y; }else{ minX = Math.min(point.x,minX); minY = Math.min(point.y,minY); maxX = Math.max(point.x,maxX); maxY = Math.max(point.y,maxY); } } minX = Math.floor(minX); maxX = Math.floor(maxX); minY = Math.floor(minY); maxY = Math.floor(maxY); return {x:minX,y:minY,width:maxX - minX,height:maxY - minY}; }
Add a Detect Method
Dynamsoft Document Normalizer can detect the borders of documents. We can also add a detect
method for the component so that we can automatically get a suitable selection box.
@Method()
async detect(source: string | HTMLImageElement | Blob | HTMLCanvasElement):Promise<DetectedQuadResult[]>
{
if (window["Dynamsoft"]) {
if (!this.cvr) {
await this.initCVR();
}
let result:CapturedResult = await this.cvr.capture(source,"DetectDocumentBoundaries_Default");
let results:DetectedQuadResultItem[] = [];
for (let index = 0; index < result.items.length; index++) {
const item = (result.items[index] as DetectedQuadResultItem);
results.push(item);
}
return results;
}else{
throw "Dynamsoft Document Normalizer not found";
}
}
Source Code
All right, we’ve now finished the web component. Get the source code of the image cropper web component to have a try: