import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, isDevMode, OnChanges, OnInit, Output, SimpleChanges, ViewChild, Optional } from '@angular/core'; import { DomSanitizer, SafeStyle, SafeUrl } from '@angular/platform-browser'; import { HttpClient } from '@angular/common/http'; import { CropperPosition, Dimensions, ImageCroppedEvent, MoveStart, ImageTransform } from './interfaces'; import { getTransformationsFromExifData } from './utils/exif.utils'; import { resizeCanvas } from './utils/resize.utils'; import { ExifTransform } from './interfaces/exif-transform.interface'; import { MoveTypes } from './interfaces/move-start.interface'; import { debounce } from './utils/debounce.utils'; import { NotifyService } from '@farris/ui-notify'; @Component({ selector: 'image-cropper', templateUrl: './image-cropper.component.html', styleUrls: ['./image-cropper.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class ImageCropperComponent implements OnChanges, OnInit { private originalImage: HTMLImageElement | null; private transformedImage: HTMLImageElement; private originalBase64: string; private transformedBase64: string; private moveStart: MoveStart; //原始尺寸 private originalSize: Dimensions; //改变后尺寸 private transformedSize: Dimensions; //加载图片已用时间 private setImageMaxSizeRetries = 0; // private cropperScaledMinWidth = 20; private cropperScaledMinHeight = 20; // private exifTransform: ExifTransform = {rotate: 0, flip: false}; safeImgDataUrl: SafeUrl | string; safeTransformStyle: SafeStyle | string; marginLeft: SafeStyle | string = '0px'; maxSize: Dimensions; imageVisible = false; moveTypes = MoveTypes; uploadError:boolean = false; public isLoading: boolean = true; @ViewChild('wrapper') wrapper: ElementRef; @ViewChild('sourceImage') sourceImage: ElementRef; @Input() set imageChangedEvent(event: any) { this.initCropper(); if (event && event.target && event.target.files && event.target.files.length > 0) { this.isLoading = true; this.loadImageFile(event.target.files[0]); } // else{ // this.notifyService.error("文件为空"); // this.isLoading = false; // this.uploadError = true; // } } @Input() set imageURL(url: string) { if (url) { this.initCropper(); this.isLoading = true; this.loadImageFromURL(url); } else{ this.notifyService.error("图片路径为空"); this.isLoading = false; this.uploadError = true; } } @Input() set imageBase64(imageBase64: string) { this.initCropper(); this.isLoading = true; this.checkExifAndLoadBase64Image(imageBase64); } @Input() set imageFile(file: File) { this.initCropper(); if (file) { this.isLoading = true; this.loadImageFile(file); } // else{ // this.notifyService.error("文件为空"); // this.isLoading = false; // this.uploadError = true; // } } //输出图片格式 @Input() format: 'png' | 'jpeg' | 'bmp' | 'webp' | 'ico' = 'png'; //保持长宽比例 @Input() maintainAspectRatio = true; //scale缩放 rotate旋转 flipH横向反转 flipV垂直翻转 执行变化 @Input() transform: ImageTransform = {}; //宽高比例 width/height @Input() aspectRatio = 1; //裁剪后图片被调整为的宽高 @Input() resizeToWidth = 0; @Input() resizeToHeight = 0; //裁剪最小宽高 @Input() cropperMinWidth = 0; @Input() cropperMinHeight = 0; //旋转画布 1 = 90deg @Input() canvasRotation = 0; //裁剪形状是否为圆形 @Input() roundCropper = true; //启用这个选项将确保较小的图像不会被放大 @Input() onlyScaleDown = false; //适用于使用jpeg或webp作为输出格式时。输入0到100之间的数字将决定输出图像的质量 @Input() imageQuality = 92; //是否每次修改cropper的位置或大小时,cropper都会发出一个图像 @Input() autoCrop = true; //背景色 @Input() backgroundColor: string; //是否在图像周围添加填充以使其适合长宽比 @Input() containWithinAspectRatio = false; // @Input() hideResizeSquares = false; // @Input() cropper: CropperPosition = { x1: -100, y1: -100, x2: 10000, y2: 10000 }; @Input() loadImageErrorText: string = "图片加载错误"; @HostBinding('style.text-align') @Input() alignImage: 'left' | 'center' = 'center'; @HostBinding('class.farris-image-cropper-disabled') @Input() disabled = false; //图片每次裁切结果 @Output() imageCropped = new EventEmitter(); //开始裁切 @Output() startCropImage = new EventEmitter(); //图片加载完成 @Output() imageLoaded = new EventEmitter(); //裁剪区域初始化完成 @Output() cropperReady = new EventEmitter(); //加载图片失败 @Output() loadImageFailed = new EventEmitter(); constructor(private sanitizer: DomSanitizer, private cd: ChangeDetectorRef, @Optional() private http: HttpClient, private notifyService: NotifyService) { this.initCropper(); } ngOnChanges(changes: SimpleChanges): void { if (this.originalImage && this.originalImage.complete && this.exifTransform && (changes.containWithinAspectRatio || changes.canvasRotation)) { this.transformOriginalImage(); } if (changes.cropper) { this.setMaxSize(); this.setCropperScaledMinSize(); this.checkCropperPosition(false); this.doAutoCrop(); this.cd.markForCheck(); } if (changes.aspectRatio && this.imageVisible) { this.resetCropperPosition(); } if (changes.transform) { this.transform = this.transform || {}; this.setCssTransform(); this.doAutoCrop(); } } //设置图片transform private setCssTransform() { this.safeTransformStyle = this.sanitizer.bypassSecurityTrustStyle( 'scaleX(' + (this.transform.scale || 1) * (this.transform.flipH ? -1 : 1) + ')' + 'scaleY(' + (this.transform.scale || 1) * (this.transform.flipV ? -1 : 1) + ')' + 'rotate(' + (this.transform.rotate || 0) + 'deg)' ); } ngOnInit(): void { } //初始化 private initCropper(): void { this.imageVisible = false; this.transformedImage = null; this.safeImgDataUrl = 'data:image/png;base64,iVBORw0KGg' + 'oAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAU' + 'AAarVyFEAAAAASUVORK5CYII='; this.moveStart = { active: false, type: null, position: null, x1: 0, y1: 0, x2: 0, y2: 0, clientX: 0, clientY: 0 }; this.maxSize = { width: 0, height: 0 }; this.originalSize = { width: 0, height: 0 }; this.transformedSize = { width: 0, height: 0 }; this.cropper.x1 = -100; this.cropper.y1 = -100; this.cropper.x2 = 10000; this.cropper.y2 = 10000; } //处理图片 private loadImage(imageBase64: string, imageType: string) { if (this.isValidImageType(imageType)) { this.uploadError = false; this.checkExifAndLoadBase64Image(imageBase64); } else { this.notifyService.error("图片类型不正确,请重试"); this.isLoading = false; this.uploadError = true; this.loadImageFailed.emit(); } } private loadImageFile(file: File): void { const fileReader = new FileReader(); fileReader.onload = (event: any) => this.loadImage(event.target.result, file.type); fileReader.readAsDataURL(file); } //判断图片格式是否正确 private isValidImageType(type: string): boolean { return /image\/(png|jpg|jpeg|bmp|gif|tiff|webp)/.test(type); } //判断文件信息,转换成base64 private checkExifAndLoadBase64Image(imageBase64: string): void { const fail = (error) => { this.notifyService.error("图片上传错误,请重试"); this.isLoading = false; this.uploadError = true; this.loadImageFailed.emit(); this.originalImage = null; this.originalBase64 = null; // 这里执行错误提示 }; this.originalImage = new Image(); this.originalImage.onload = () => { this.originalBase64 = imageBase64; this.exifTransform = getTransformationsFromExifData(imageBase64); this.originalSize.width = this.originalImage.naturalWidth; this.originalSize.height = this.originalImage.naturalHeight; this.transformOriginalImage() .then(() => { this.uploadError = false; }) .catch(fail); }; this.originalImage.onerror = fail; this.originalImage.src = imageBase64; } //从url获取文件 private loadImageFromURL(url: string): void { const img = new Image(); img.onerror = () => { this.notifyService.error("图片加载错误,请重试"); this.isLoading = false; this.uploadError = true; this.loadImageFailed.emit(); }; img.onload = () => { this.uploadError = false; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; context.drawImage(img, 0, 0); this.checkExifAndLoadBase64Image(canvas.toDataURL()); }; img.crossOrigin = 'anonymous'; img.src = url; } private getTransformedSize(): Dimensions { const canvasRotation = this.canvasRotation + this.exifTransform.rotate; if (this.containWithinAspectRatio) {//填充空余 if (canvasRotation % 2) { const minWidthToContain = this.originalSize.width * this.aspectRatio; const minHeightToContain = this.originalSize.height / this.aspectRatio; return { width: Math.max(this.originalSize.height, minWidthToContain), height: Math.max(this.originalSize.width, minHeightToContain), }; } else { const minWidthToContain = this.originalSize.height * this.aspectRatio; const minHeightToContain = this.originalSize.width / this.aspectRatio; return { width: Math.max(this.originalSize.width, minWidthToContain), height: Math.max(this.originalSize.height, minHeightToContain), }; } } if (canvasRotation % 2) { return { height: this.originalSize.width, width: this.originalSize.height, }; } return { width: this.originalSize.width, height: this.originalSize.height, }; } private transformOriginalImage(): Promise { if (!this.originalImage || !this.originalImage.complete || !this.exifTransform) { return Promise.reject(new Error('No image loaded')); } const transformedBase64 = this.transformImageBase64(); return this.setTransformedImage(transformedBase64); } //改变base64图片 private transformImageBase64(): string { const canvasRotation = this.canvasRotation + this.exifTransform.rotate; const transformedSize = this.getTransformedSize(); const canvas = document.createElement('canvas'); canvas.width = transformedSize.width; canvas.height = transformedSize.height; const ctx = canvas.getContext('2d'); ctx.setTransform( this.exifTransform.flip ? -1 : 1, 0, 0, 1, canvas.width / 2, canvas.height / 2 ); ctx.rotate(Math.PI * (canvasRotation / 2)); ctx.drawImage( this.originalImage, -this.originalSize.width / 2, -this.originalSize.height / 2 ); return canvas.toDataURL(); } //更新显示图片文件 private setTransformedImage(transformedBase64): Promise { return new Promise((resolve) => { this.transformedBase64 = transformedBase64; this.safeImgDataUrl = this.sanitizer.bypassSecurityTrustResourceUrl(transformedBase64); this.transformedImage = new Image(); this.transformedImage.onload = () => { this.transformedSize.width = this.transformedImage.naturalWidth; this.transformedSize.height = this.transformedImage.naturalHeight; this.cd.markForCheck(); resolve(); }; this.transformedImage.src = this.transformedBase64; }); } //加载图片 加载20s没成功会抛出加载错误 imageLoadedInView(): void { if (this.transformedImage != null) { this.isLoading = false; this.imageLoaded.emit(); this.setImageMaxSizeRetries = 0; setTimeout(() => this.checkImageMaxSizeRecursively()); } } private checkImageMaxSizeRecursively(): void { if (this.setImageMaxSizeRetries > 40) { this.notifyService.error("图片加载超时,请重试"); this.uploadError = true; this.isLoading = false; this.loadImageFailed.emit(); } else if (this.sourceImageLoaded()) { this.uploadError = false; this.setMaxSize(); this.setCropperScaledMinSize(); this.resetCropperPosition(); this.cropperReady.emit({...this.maxSize}); this.cd.markForCheck(); } else { this.setImageMaxSizeRetries++; setTimeout(() => this.checkImageMaxSizeRecursively(), 50); } } private sourceImageLoaded(): boolean { return this.sourceImage && this.sourceImage.nativeElement && this.sourceImage.nativeElement.offsetWidth > 0; } @HostListener('window:resize') @debounce() onResize(): void { this.resizeCropperPosition(); this.setMaxSize(); this.setCropperScaledMinSize(); } private resizeCropperPosition(): void { const sourceImageElement = this.sourceImage.nativeElement; if (this.maxSize.width !== sourceImageElement.offsetWidth || this.maxSize.height !== sourceImageElement.offsetHeight) { this.cropper.x1 = this.cropper.x1 * sourceImageElement.offsetWidth / this.maxSize.width; this.cropper.x2 = this.cropper.x2 * sourceImageElement.offsetWidth / this.maxSize.width; this.cropper.y1 = this.cropper.y1 * sourceImageElement.offsetHeight / this.maxSize.height; this.cropper.y2 = this.cropper.y2 * sourceImageElement.offsetHeight / this.maxSize.height; } } //恢复裁剪区位置 resetCropperPosition(): void { const sourceImageElement = this.sourceImage.nativeElement; if (!this.maintainAspectRatio) { this.cropper.x1 = 0; this.cropper.x2 = sourceImageElement.offsetWidth; this.cropper.y1 = 0; this.cropper.y2 = sourceImageElement.offsetHeight; } else if (sourceImageElement.offsetWidth / this.aspectRatio < sourceImageElement.offsetHeight) { this.cropper.x1 = 0; this.cropper.x2 = sourceImageElement.offsetWidth; const cropperHeight = sourceImageElement.offsetWidth / this.aspectRatio; this.cropper.y1 = (sourceImageElement.offsetHeight - cropperHeight) / 2; this.cropper.y2 = this.cropper.y1 + cropperHeight; } else { this.cropper.y1 = 0; this.cropper.y2 = sourceImageElement.offsetHeight; const cropperWidth = sourceImageElement.offsetHeight * this.aspectRatio; this.cropper.x1 = (sourceImageElement.offsetWidth - cropperWidth) / 2; this.cropper.x2 = this.cropper.x1 + cropperWidth; } this.doAutoCrop(); this.imageVisible = true; } //开始移动轮廓 startMove(event: any, moveType: MoveTypes, position: string | null = null): void { if (this.moveStart && this.moveStart.active) { return; } if (event.preventDefault) { event.preventDefault(); } this.moveStart = { active: true, type: moveType, position, clientX: this.getClientX(event), clientY: this.getClientY(event), ...this.cropper }; } @HostListener('document:mousemove', ['$event']) @HostListener('document:touchmove', ['$event']) moveImg(event: any): void { if (this.moveStart.active) { if (event.stopPropagation) { event.stopPropagation(); } if (event.preventDefault) { event.preventDefault(); } if (this.moveStart.type === MoveTypes.Move) { this.move(event); this.checkCropperPosition(true); } else if (this.moveStart.type === MoveTypes.Resize) { this.resize(event); this.checkCropperPosition(false); } this.cd.detectChanges(); } } //设置最大尺寸 private setMaxSize(): void { if (this.sourceImage) { const sourceImageElement = this.sourceImage.nativeElement; this.maxSize.width = sourceImageElement.offsetWidth; this.maxSize.height = sourceImageElement.offsetHeight; this.marginLeft = this.sanitizer.bypassSecurityTrustStyle('calc(50% - ' + this.maxSize.width / 2 + 'px)'); } } //设置裁剪区最小尺寸 private setCropperScaledMinSize(): void { if (this.transformedImage) { this.setCropperScaledMinWidth(); this.setCropperScaledMinHeight(); } else { this.cropperScaledMinWidth = 20; this.cropperScaledMinHeight = 20; } } private setCropperScaledMinWidth(): void { this.cropperScaledMinWidth = this.cropperMinWidth > 0 ? Math.max(20, this.cropperMinWidth / this.transformedImage.width * this.maxSize.width) : 20; } private setCropperScaledMinHeight(): void { if (this.maintainAspectRatio) { this.cropperScaledMinHeight = Math.max(20, this.cropperScaledMinWidth / this.aspectRatio); } else if (this.cropperMinHeight > 0) { this.cropperScaledMinHeight = Math.max(20, this.cropperMinHeight / this.transformedImage.height * this.maxSize.height); } else { this.cropperScaledMinHeight = 20; } } private checkCropperPosition(maintainSize = false): void { if (this.cropper.x1 < 0) { this.cropper.x2 -= maintainSize ? this.cropper.x1 : 0; this.cropper.x1 = 0; } if (this.cropper.y1 < 0) { this.cropper.y2 -= maintainSize ? this.cropper.y1 : 0; this.cropper.y1 = 0; } if (this.cropper.x2 > this.maxSize.width) { this.cropper.x1 -= maintainSize ? (this.cropper.x2 - this.maxSize.width) : 0; this.cropper.x2 = this.maxSize.width; } if (this.cropper.y2 > this.maxSize.height) { this.cropper.y1 -= maintainSize ? (this.cropper.y2 - this.maxSize.height) : 0; this.cropper.y2 = this.maxSize.height; } } @HostListener('document:mouseup') @HostListener('document:touchend') moveStop(): void { if (this.moveStart.active) { this.moveStart.active = false; this.doAutoCrop(); } } private move(event: any) { const diffX = this.getClientX(event) - this.moveStart.clientX; const diffY = this.getClientY(event) - this.moveStart.clientY; this.cropper.x1 = this.moveStart.x1 + diffX; this.cropper.y1 = this.moveStart.y1 + diffY; this.cropper.x2 = this.moveStart.x2 + diffX; this.cropper.y2 = this.moveStart.y2 + diffY; } private resize(event: any): void { const diffX = this.getClientX(event) - this.moveStart.clientX; const diffY = this.getClientY(event) - this.moveStart.clientY; switch (this.moveStart.position) { case 'left': this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - this.cropperScaledMinWidth); break; case 'topleft': this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - this.cropperScaledMinWidth); this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - this.cropperScaledMinHeight); break; case 'top': this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - this.cropperScaledMinHeight); break; case 'topright': this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + this.cropperScaledMinWidth); this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - this.cropperScaledMinHeight); break; case 'right': this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + this.cropperScaledMinWidth); break; case 'bottomright': this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + this.cropperScaledMinWidth); this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + this.cropperScaledMinHeight); break; case 'bottom': this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + this.cropperScaledMinHeight); break; case 'bottomleft': this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - this.cropperScaledMinWidth); this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + this.cropperScaledMinHeight); break; case 'center': const scale = event.scale; const newWidth = (Math.abs(this.moveStart.x2 - this.moveStart.x1)) * scale; const newHeight = (Math.abs(this.moveStart.y2 - this.moveStart.y1)) * scale; const x1 = this.cropper.x1; const y1 = this.cropper.y1; this.cropper.x1 = Math.min(this.moveStart.clientX - (newWidth / 2), this.cropper.x2 - this.cropperScaledMinWidth); this.cropper.y1 = Math.min(this.moveStart.clientY - (newHeight / 2), this.cropper.y2 - this.cropperScaledMinHeight); this.cropper.x2 = Math.max(this.moveStart.clientX + (newWidth / 2), x1 + this.cropperScaledMinWidth); this.cropper.y2 = Math.max(this.moveStart.clientY + (newHeight / 2), y1 + this.cropperScaledMinHeight); break; } if (this.maintainAspectRatio) { this.checkAspectRatio(); } } private checkAspectRatio(): void { let overflowX = 0; let overflowY = 0; switch (this.moveStart.position) { case 'top': this.cropper.x2 = this.cropper.x1 + (this.cropper.y2 - this.cropper.y1) * this.aspectRatio; overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0); overflowY = Math.max(0 - this.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio; } break; case 'bottom': this.cropper.x2 = this.cropper.x1 + (this.cropper.y2 - this.cropper.y1) * this.aspectRatio; overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0); overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : (overflowX / this.aspectRatio); } break; case 'topleft': this.cropper.y1 = this.cropper.y2 - (this.cropper.x2 - this.cropper.x1) / this.aspectRatio; overflowX = Math.max(0 - this.cropper.x1, 0); overflowY = Math.max(0 - this.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x1 += (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio; } break; case 'topright': this.cropper.y1 = this.cropper.y2 - (this.cropper.x2 - this.cropper.x1) / this.aspectRatio; overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0); overflowY = Math.max(0 - this.cropper.y1, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio; } break; case 'right': case 'bottomright': this.cropper.y2 = this.cropper.y1 + (this.cropper.x2 - this.cropper.x1) / this.aspectRatio; overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0); overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio; } break; case 'left': case 'bottomleft': this.cropper.y2 = this.cropper.y1 + (this.cropper.x2 - this.cropper.x1) / this.aspectRatio; overflowX = Math.max(0 - this.cropper.x1, 0); overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0); if (overflowX > 0 || overflowY > 0) { this.cropper.x1 += (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX; this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio; } break; case 'center': this.cropper.x2 = this.cropper.x1 + (this.cropper.y2 - this.cropper.y1) * this.aspectRatio; this.cropper.y2 = this.cropper.y1 + (this.cropper.x2 - this.cropper.x1) / this.aspectRatio; const overflowX1 = Math.max(0 - this.cropper.x1, 0); const overflowX2 = Math.max(this.cropper.x2 - this.maxSize.width, 0); const overflowY1 = Math.max(this.cropper.y2 - this.maxSize.height, 0); const overflowY2 = Math.max(0 - this.cropper.y1, 0); if (overflowX1 > 0 || overflowX2 > 0 || overflowY1 > 0 || overflowY2 > 0) { this.cropper.x1 += (overflowY1 * this.aspectRatio) > overflowX1 ? (overflowY1 * this.aspectRatio) : overflowX1; this.cropper.x2 -= (overflowY2 * this.aspectRatio) > overflowX2 ? (overflowY2 * this.aspectRatio) : overflowX2; this.cropper.y1 += (overflowY2 * this.aspectRatio) > overflowX2 ? overflowY2 : overflowX2 / this.aspectRatio; this.cropper.y2 -= (overflowY1 * this.aspectRatio) > overflowX1 ? overflowY1 : overflowX1 / this.aspectRatio; } break; } } //判断是否裁切后发出图像 private doAutoCrop(): void { if (this.autoCrop) { this.crop(); } } /** * 裁剪动作 */ crop(): ImageCroppedEvent | null { if (this.sourceImage && this.sourceImage.nativeElement && this.transformedImage != null) { //开始裁剪 this.startCropImage.emit(); const imagePosition = this.getImagePosition(); const width = imagePosition.x2 - imagePosition.x1; const height = imagePosition.y2 - imagePosition.y1; const cropCanvas = document.createElement('canvas') as HTMLCanvasElement; cropCanvas.width = width; cropCanvas.height = height; const ctx = cropCanvas.getContext('2d'); if (ctx) { if (this.backgroundColor != null) { ctx.fillStyle = this.backgroundColor; ctx.fillRect(0, 0, width, height); } const scaleX = (this.transform.scale || 1) * (this.transform.flipH ? -1 : 1); const scaleY = (this.transform.scale || 1) * (this.transform.flipV ? -1 : 1); ctx.setTransform(scaleX, 0, 0, scaleY, this.transformedSize.width / 2, this.transformedSize.height / 2); ctx.translate(-imagePosition.x1 / scaleX, -imagePosition.y1 / scaleY); ctx.rotate((this.transform.rotate || 0) * Math.PI / 180); ctx.drawImage(this.transformedImage, -this.transformedSize.width / 2, -this.transformedSize.height / 2); const output: ImageCroppedEvent = { width, height, imagePosition, cropperPosition: {...this.cropper} }; if (this.containWithinAspectRatio) { output.offsetImagePosition = this.getOffsetImagePosition(); } const resizeRatio = this.getResizeRatio(width, height); if (resizeRatio !== 1) { output.width = Math.round(width * resizeRatio); output.height = this.maintainAspectRatio ? Math.round(output.width / this.aspectRatio) : Math.round(height * resizeRatio); resizeCanvas(cropCanvas, output.width, output.height); } output.base64 = this.cropToBase64(cropCanvas); this.imageCropped.emit(output); return output; } } return null; } /** * 获得图片位置坐标 */ private getImagePosition(): CropperPosition { const sourceImageElement = this.sourceImage.nativeElement; const ratio = this.transformedSize.width / sourceImageElement.offsetWidth; const out: CropperPosition = { x1: Math.round(this.cropper.x1 * ratio), y1: Math.round(this.cropper.y1 * ratio), x2: Math.round(this.cropper.x2 * ratio), y2: Math.round(this.cropper.y2 * ratio) }; if (!this.containWithinAspectRatio) { out.x1 = Math.max(out.x1, 0); out.y1 = Math.max(out.y1, 0); out.x2 = Math.min(out.x2, this.transformedSize.width); out.y2 = Math.min(out.y2, this.transformedSize.height); } return out; } //带填充的位置 private getOffsetImagePosition(): CropperPosition { const canvasRotation = this.canvasRotation + this.exifTransform.rotate; const sourceImageElement = this.sourceImage.nativeElement; const ratio = this.transformedSize.width / sourceImageElement.offsetWidth; let offsetX: number; let offsetY: number; if (canvasRotation % 2) { offsetX = (this.transformedSize.width - this.originalSize.height) / 2; offsetY = (this.transformedSize.height - this.originalSize.width) / 2; } else { offsetX = (this.transformedSize.width - this.originalSize.width) / 2; offsetY = (this.transformedSize.height - this.originalSize.height) / 2; } const out: CropperPosition = { x1: Math.round(this.cropper.x1 * ratio) - offsetX, y1: Math.round(this.cropper.y1 * ratio) - offsetY, x2: Math.round(this.cropper.x2 * ratio) - offsetX, y2: Math.round(this.cropper.y2 * ratio) - offsetY }; if (!this.containWithinAspectRatio) { out.x1 = Math.max(out.x1, 0); out.y1 = Math.max(out.y1, 0); out.x2 = Math.min(out.x2, this.transformedSize.width); out.y2 = Math.min(out.y2, this.transformedSize.height); } return out; } //转换为base64 private cropToBase64(cropCanvas: HTMLCanvasElement): string { return cropCanvas.toDataURL('image/' + this.format, this.getQuality()); } private getQuality(): number { return Math.min(1, Math.max(0, this.imageQuality / 100)); } private getResizeRatio(width: number, height: number): number { if (this.resizeToWidth > 0) { if (!this.onlyScaleDown || width > this.resizeToWidth) { return this.resizeToWidth / width; } } else if (this.resizeToHeight > 0) { if (!this.onlyScaleDown || height > this.resizeToHeight) { return this.resizeToHeight / height; } } return 1; } private getClientX(event: any): number { return (event.touches && event.touches[0] ? event.touches[0].clientX : event.clientX) || 0; } private getClientY(event: any): number { return (event.touches && event.touches[0] ? event.touches[0].clientY : event.clientY) || 0; } }