mirror of https://github.com/Squidex/squidex.git
Browse Source
* Improved image editor and focus point. * added missing files. * Folder renamed * Focus point improvement. * Focus point in API. * Tags improved. * Another style fix.pull/483/head
committed by
GitHub
68 changed files with 1117 additions and 833 deletions
@ -0,0 +1,20 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.Assets |
|||
{ |
|||
public enum ResizeMode |
|||
{ |
|||
Crop, |
|||
CropUpsize, |
|||
Pad, |
|||
BoxPad, |
|||
Max, |
|||
Min, |
|||
Stretch |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text; |
|||
|
|||
namespace Squidex.Infrastructure.Assets |
|||
{ |
|||
public sealed class ResizeOptions |
|||
{ |
|||
public int? Width { get; set; } |
|||
|
|||
public int? Height { get; set; } |
|||
|
|||
public int? Quality { get; set; } |
|||
|
|||
public float? FocusX { get; set; } |
|||
|
|||
public float? FocusY { get; set; } |
|||
|
|||
public ResizeMode Mode { get; set; } |
|||
|
|||
public override string ToString() |
|||
{ |
|||
var sb = new StringBuilder(); |
|||
|
|||
sb.Append(Width); |
|||
sb.Append("_"); |
|||
sb.Append(Height); |
|||
sb.Append("_"); |
|||
sb.Append(Mode); |
|||
|
|||
if (Quality.HasValue) |
|||
{ |
|||
sb.Append("_"); |
|||
sb.Append(Quality); |
|||
} |
|||
|
|||
if (FocusX.HasValue) |
|||
{ |
|||
sb.Append("_focusX_"); |
|||
sb.Append(FocusX); |
|||
} |
|||
|
|||
if (FocusY.HasValue) |
|||
{ |
|||
sb.Append("_focusY_"); |
|||
sb.Append(FocusY); |
|||
} |
|||
|
|||
return sb.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<div class="menu"> |
|||
<button type="button" class="btn" (click)="rotate(-90)" title="Rotate Left"> |
|||
<i class="icon-rotate_left"></i> |
|||
</button> |
|||
|
|||
<button type="button" class="btn" (click)="rotate(90)" title="Rotate Right"> |
|||
<i class="icon-rotate_right"></i> |
|||
</button> |
|||
|
|||
<span class="separator"></span> |
|||
|
|||
<button type="button" class="btn" (click)="flip(false)" title="Flip Vertically"> |
|||
<i class="icon-flip"></i> |
|||
</button> |
|||
|
|||
<button type="button" class="btn rotate 90" (click)="flip(true)" title="Flip Horizontally"> |
|||
<i class="icon-flip"></i> |
|||
</button> |
|||
|
|||
<span class="separator"></span> |
|||
|
|||
<button type="button" class="btn" (click)="reset()" title="Reset"> |
|||
<i class="icon-close"></i> |
|||
</button> |
|||
</div> |
|||
|
|||
<div class="editor"> |
|||
<canvas #editor></canvas> |
|||
</div> |
|||
@ -0,0 +1,46 @@ |
|||
:host ::ng-deep { |
|||
@import '~cropperjs/dist/cropper'; |
|||
|
|||
img { |
|||
background-image: $asset-background; |
|||
} |
|||
} |
|||
|
|||
:host { |
|||
background: $color-dark-black; |
|||
display: flex; |
|||
flex-direction: column; |
|||
flex-grow: 1; |
|||
height: 100%; |
|||
overflow-x: hidden; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.editor { |
|||
flex-grow: 1; |
|||
overflow-x: hidden; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.menu { |
|||
flex-shrink: 0; |
|||
padding: 1rem; |
|||
text-align: center; |
|||
text-decoration: none; |
|||
|
|||
.btn { |
|||
color: $color-dark-foreground; |
|||
} |
|||
} |
|||
|
|||
.rotate { |
|||
transform: rotate(90deg); |
|||
} |
|||
|
|||
.separator { |
|||
border-left: 1px solid $color-dark-foreground; |
|||
display: inline-block; |
|||
height: .75rem; |
|||
margin-left: 1rem; |
|||
margin-right: 1rem; |
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; |
|||
import Cropper from 'cropperjs'; |
|||
|
|||
import { Types } from '@app/framework'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-image-editor', |
|||
styleUrls: ['./image-cropper.component.scss'], |
|||
templateUrl: './image-cropper.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class ImageCropperComponent implements AfterViewInit, OnDestroy, OnChanges { |
|||
private cropper: Cropper | null = null; |
|||
private data: Cropper.Data; |
|||
|
|||
@Input() |
|||
public imageSource: string; |
|||
|
|||
@ViewChild('editor', { static: false }) |
|||
public editor: ElementRef<HTMLCanvasElement>; |
|||
|
|||
public ngOnDestroy() { |
|||
if (this.cropper) { |
|||
this.cropper.destroy(); |
|||
} |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
if (this.cropper) { |
|||
this.cropper.replace(this.imageSource); |
|||
} |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
this.cropper = new Cropper(this.editor.nativeElement, { |
|||
ready: () => { |
|||
if (this.cropper) { |
|||
this.data = this.cropper.getData(); |
|||
} |
|||
}, |
|||
autoCrop: false, |
|||
background: false, |
|||
minContainerHeight: 0, |
|||
minContainerWidth: 0, |
|||
movable: false, |
|||
zoomOnTouch: false, |
|||
zoomOnWheel: false, |
|||
viewMode: 0 |
|||
}); |
|||
|
|||
this.cropper.replace(this.imageSource); |
|||
} |
|||
|
|||
public rotate(value: number) { |
|||
if (this.cropper) { |
|||
this.cropper.rotate(-90); |
|||
|
|||
const canvasData = this.cropper.getCanvasData(); |
|||
const containerData = this.cropper.getContainerData(); |
|||
|
|||
const dx = containerData.width / canvasData.naturalWidth; |
|||
const dy = containerData.height / canvasData.naturalHeight; |
|||
|
|||
this.cropper.zoomTo(Math.min(dx, dy), { |
|||
x: containerData.width / 2, |
|||
y: containerData.height / 2 |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public flip(vertically: boolean) { |
|||
if (this.cropper) { |
|||
const { rotate, scaleX, scaleY } = this.cropper.getData(); |
|||
|
|||
if (rotate === 90 || rotate === 270) { |
|||
vertically = !vertically; |
|||
} |
|||
|
|||
if (vertically) { |
|||
this.cropper.scale(scaleX, -1 * scaleY); |
|||
} else { |
|||
this.cropper.scale(-1 * scaleX, scaleY); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public zoomIn() { |
|||
if (this.cropper) { |
|||
this.cropper.zoom(.1); |
|||
} |
|||
} |
|||
|
|||
public zoomOut() { |
|||
if (this.cropper) { |
|||
this.cropper.zoom(-.1); |
|||
} |
|||
} |
|||
|
|||
public reset() { |
|||
if (this.cropper) { |
|||
this.cropper.reset(); |
|||
this.cropper.clear(); |
|||
|
|||
this.data = this.cropper.getData(); |
|||
} |
|||
} |
|||
|
|||
public toFile(): Promise<Blob | null> { |
|||
return new Promise<Blob | null>(resolve => { |
|||
if (!this.cropper) { |
|||
return resolve(null); |
|||
} else { |
|||
const data = this.cropper.getData(); |
|||
|
|||
if (Types.equals(data, this.data)) { |
|||
resolve(null); |
|||
} else { |
|||
this.data = data; |
|||
|
|||
this.cropper.getCroppedCanvas().toBlob(blob => { |
|||
resolve(blob); |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
<div #editor></div> |
|||
@ -1,29 +0,0 @@ |
|||
:host { |
|||
height: 100%; |
|||
} |
|||
|
|||
:host /deep/ { |
|||
.tui-image-editor-header { |
|||
display: none; |
|||
} |
|||
|
|||
* { |
|||
box-sizing: content-box; |
|||
} |
|||
|
|||
.tie-btn-delete { |
|||
display: none !important; |
|||
} |
|||
|
|||
.tie-btn-delete-all { |
|||
display: none !important; |
|||
|
|||
& + li { |
|||
display: none !important; |
|||
} |
|||
} |
|||
|
|||
svg { |
|||
vertical-align: baseline; |
|||
} |
|||
} |
|||
@ -1,199 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, ViewChild } from '@angular/core'; |
|||
|
|||
import { ResourceLoaderService } from '@app/shared/internal'; |
|||
|
|||
declare var tui: any; |
|||
|
|||
const blackTheme = { |
|||
'common.bi.image': 'https://uicdn.toast.com/toastui/img/tui-image-editor-bi.png', |
|||
'common.bisize.width': '251px', |
|||
'common.bisize.height': '21px', |
|||
'common.backgroundImage': 'none', |
|||
'common.backgroundColor': '#000', |
|||
'common.border': '0px', |
|||
|
|||
// header
|
|||
'header.backgroundImage': 'none', |
|||
'header.backgroundColor': 'transparent', |
|||
'header.border': '0px', |
|||
|
|||
// load button
|
|||
'loadButton.backgroundColor': '#fff', |
|||
'loadButton.border': '1px solid #ddd', |
|||
'loadButton.color': '#222', |
|||
'loadButton.fontFamily': '\'Noto Sans\', sans-serif', |
|||
'loadButton.fontSize': '12px', |
|||
|
|||
// download button
|
|||
'downloadButton.backgroundColor': '#fdba3b', |
|||
'downloadButton.border': '1px solid #fdba3b', |
|||
'downloadButton.color': '#fff', |
|||
'downloadButton.fontFamily': '\'Noto Sans\', sans-serif', |
|||
'downloadButton.fontSize': '12px', |
|||
|
|||
// main icons
|
|||
'menu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg', |
|||
'menu.normalIcon.name': 'icon-d', |
|||
'menu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-b.svg', |
|||
'menu.activeIcon.name': 'icon-b', |
|||
'menu.disabledIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-a.svg', |
|||
'menu.disabledIcon.name': 'icon-a', |
|||
'menu.hoverIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg', |
|||
'menu.hoverIcon.name': 'icon-c', |
|||
'menu.iconSize.width': '24px', |
|||
'menu.iconSize.height': '24px', |
|||
|
|||
// submenu primary color
|
|||
'submenu.backgroundColor': '#000', |
|||
'submenu.partition.color': '#3c3c3c', |
|||
|
|||
// submenu icons
|
|||
'submenu.normalIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-d.svg', |
|||
'submenu.normalIcon.name': 'icon-d', |
|||
'submenu.activeIcon.path': 'https://unpkg.com/tui-image-editor@3.7.3/dist/svg/icon-c.svg', |
|||
'submenu.activeIcon.name': 'icon-c', |
|||
'submenu.iconSize.width': '32px', |
|||
'submenu.iconSize.height': '32px', |
|||
|
|||
// submenu labels
|
|||
'submenu.normalLabel.color': '#8a8a8a', |
|||
'submenu.normalLabel.fontWeight': 'normal', |
|||
'submenu.activeLabel.color': '#fff', |
|||
'submenu.activeLabel.fontWeight': 'normal', |
|||
|
|||
// checkbox style
|
|||
'checkbox.border': '0px', |
|||
'checkbox.backgroundColor': '#fff', |
|||
|
|||
// range style
|
|||
'range.pointer.color': '#fff', |
|||
'range.bar.color': '#666', |
|||
'range.subbar.color': '#d1d1d1', |
|||
|
|||
'range.disabledPointer.color': '#414141', |
|||
'range.disabledBar.color': '#282828', |
|||
'range.disabledSubbar.color': '#414141', |
|||
|
|||
'range.value.color': '#fff', |
|||
'range.value.fontWeight': 'normal', |
|||
'range.value.fontSize': '11px', |
|||
'range.value.border': '1px solid #353535', |
|||
'range.value.backgroundColor': '#151515', |
|||
'range.title.color': '#fff', |
|||
'range.title.fontWeight': 'normal', |
|||
|
|||
// colorpicker style
|
|||
'colorpicker.button.border': '1px solid #1e1e1e', |
|||
'colorpicker.title.color': '#fff' |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-image-editor', |
|||
styleUrls: ['./image-editor.component.scss'], |
|||
templateUrl: './image-editor.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class ImageEditorComponent implements AfterViewInit, OnChanges { |
|||
private imageEditor: any; |
|||
private isChanged = false; |
|||
private isChangedBefore = false; |
|||
|
|||
@Input() |
|||
public accessToken: string; |
|||
|
|||
@Input() |
|||
public imageUrl: string; |
|||
|
|||
@ViewChild('editor', { static: false }) |
|||
public editor: ElementRef; |
|||
|
|||
constructor( |
|||
private readonly resourceLoader: ResourceLoaderService |
|||
) { |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
if (this.imageEditor && this.imageUrl) { |
|||
this.imageEditor.loadImageFromURL(this.imageUrl); |
|||
} |
|||
} |
|||
|
|||
public toFile(): Blob | null { |
|||
if (!this.isChanged) { |
|||
return null; |
|||
} |
|||
|
|||
this.isChanged = false; |
|||
|
|||
const dataURI = this.imageEditor.toDataURL(); |
|||
|
|||
const byteString = atob(dataURI.split(',')[1]); |
|||
const byteBuffer = new ArrayBuffer(byteString.length); |
|||
|
|||
const type = dataURI.split(',')[0].split(':')[1].split(';')[0]; |
|||
|
|||
const array = new Uint8Array(byteBuffer); |
|||
|
|||
for (let i = 0; i < byteString.length; i++) { |
|||
array[i] = byteString.charCodeAt(i); |
|||
} |
|||
|
|||
return new Blob([array], { type }); |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
const styles = [ |
|||
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.css', |
|||
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.css' |
|||
]; |
|||
|
|||
const scripts = [ |
|||
'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js', |
|||
'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.3.2/fabric.js', |
|||
'https://uicdn.toast.com/tui.code-snippet/latest/tui-code-snippet.min.js', |
|||
'https://uicdn.toast.com/tui-color-picker/latest/tui-color-picker.js', |
|||
'https://uicdn.toast.com/tui-image-editor/latest/tui-image-editor.js' |
|||
]; |
|||
|
|||
let path = this.imageUrl; |
|||
|
|||
if (this.accessToken) { |
|||
path += `&access_token=${this.accessToken}`; |
|||
} |
|||
|
|||
styles.forEach(style => this.resourceLoader.loadStyle(style)); |
|||
Promise.all(scripts.map(script => this.resourceLoader.loadScript(script))).then(() => { |
|||
this.imageEditor = new tui.ImageEditor(this.editor.nativeElement, { |
|||
includeUI: { |
|||
loadImage: { |
|||
path, name: 'image' |
|||
}, |
|||
menu: [ |
|||
'crop', |
|||
'flip', |
|||
'mask', |
|||
'filter' |
|||
], |
|||
theme: blackTheme |
|||
}, |
|||
cssMaxWidth: 700, |
|||
cssMaxHeight: 500 |
|||
}); |
|||
|
|||
this.imageEditor.on('undoStackChanged', () => { |
|||
if (this.isChangedBefore) { |
|||
this.isChanged = true; |
|||
} else { |
|||
this.isChangedBefore = true; |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<div class="row"> |
|||
<div class="col-5"> |
|||
<Label>Select position of focus point</Label> |
|||
|
|||
<div class="image"> |
|||
<div class="image-container absolute align-items-center"> |
|||
<div> |
|||
<img #image [src]="imageSource" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-7"> |
|||
<Label>Preview for different sizes</Label> |
|||
|
|||
<div class="preview-wide"> |
|||
<img #previewWide [src]="imageSource" /> |
|||
</div> |
|||
|
|||
<div class="preview-line2"> |
|||
<div class="preview-small absolute"> |
|||
<img #previewSmall [src]="imageSource" /> |
|||
</div> |
|||
<div class="preview-normal absolute"> |
|||
<img #previewNormal [src]="imageSource" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,41 @@ |
|||
.image { |
|||
background: $color-dark-black; |
|||
padding: 0; |
|||
padding-top: 100%; |
|||
position: relative; |
|||
|
|||
&-container { |
|||
@include absolute(0, 0, 0, 0); |
|||
display: flex; |
|||
} |
|||
} |
|||
|
|||
.preview { |
|||
&-wide { |
|||
padding-top: 20%; |
|||
} |
|||
|
|||
&-line2 { |
|||
margin-top: 1rem; |
|||
max-height: 20rem; |
|||
min-height: 20rem; |
|||
position: relative; |
|||
} |
|||
|
|||
&-normal { |
|||
@include absolute(0, 9rem, 0, 0); |
|||
} |
|||
|
|||
&-small { |
|||
@include absolute(0, 0, 0, null); |
|||
width: 8rem; |
|||
} |
|||
} |
|||
|
|||
.absolute { |
|||
position: absolute !important; |
|||
} |
|||
|
|||
img { |
|||
background-image: $asset-background; |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
// tslint:disable: readonly-array
|
|||
|
|||
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; |
|||
import { FocusedImage, FocusPicker } from 'image-focus'; |
|||
|
|||
import { Types } from '@app/framework'; |
|||
|
|||
import { AnnotateAssetDto, AssetDto } from '@app/shared/services/assets.service'; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-image-focus-point', |
|||
styleUrls: ['./image-focus-point.component.scss'], |
|||
templateUrl: './image-focus-point.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush |
|||
}) |
|||
export class ImageFocusPointComponent implements AfterViewInit, OnDestroy, OnChanges { |
|||
private readonly previewImages: FocusedImage[] = []; |
|||
private focusPicker: FocusPicker | null = null; |
|||
private x = 0; |
|||
private y = 0; |
|||
|
|||
@Input() |
|||
public imageSource: string; |
|||
|
|||
@Input() |
|||
public focusPoint: any; |
|||
|
|||
@ViewChild('image', { static: false }) |
|||
public image: ElementRef<HTMLImageElement>; |
|||
|
|||
@ViewChild('previewWide', { static: false }) |
|||
public previewWide: ElementRef<HTMLImageElement>; |
|||
|
|||
@ViewChild('previewSmall', { static: false }) |
|||
public previewSmall: ElementRef<HTMLImageElement>; |
|||
|
|||
@ViewChild('previewNormal', { static: false }) |
|||
public previewNormal: ElementRef<HTMLImageElement>; |
|||
|
|||
public ngOnDestroy() { |
|||
if (this.focusPicker) { |
|||
this.focusPicker.stopListening(); |
|||
} |
|||
} |
|||
|
|||
public ngOnChanges() { |
|||
const { x, y } = getFocusPoint(this.focusPoint); |
|||
|
|||
this.x = x; |
|||
this.y = y; |
|||
} |
|||
|
|||
public ngAfterViewInit() { |
|||
const focus = { x: this.x, y: this.y }; |
|||
|
|||
const properties = { focus, debounceTime: 50 }; |
|||
|
|||
this.previewImages.push(new FocusedImage(this.previewWide.nativeElement, properties)); |
|||
this.previewImages.push(new FocusedImage(this.previewSmall.nativeElement, properties)); |
|||
this.previewImages.push(new FocusedImage(this.previewNormal.nativeElement, properties)); |
|||
|
|||
this.focusPicker = new FocusPicker(this.image.nativeElement, { |
|||
focus, |
|||
onChange: newFocus => { |
|||
this.x = newFocus.x; |
|||
this.y = newFocus.y; |
|||
|
|||
for (let preview of this.previewImages) { |
|||
preview.setFocus(newFocus); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public submit(asset: AssetDto): AnnotateAssetDto | null { |
|||
const previous = getFocusPoint(asset.metadata); |
|||
|
|||
if (previous.x === this.x && previous.y === this.y) { |
|||
return null; |
|||
} |
|||
|
|||
const metadata = { ...asset.metadata, focusX: this.x, focusY: this.y }; |
|||
|
|||
return { metadata }; |
|||
} |
|||
} |
|||
|
|||
function getFocusPoint(value: any): { x: number, y: number } { |
|||
let x = 0; |
|||
let y = 0; |
|||
|
|||
if (value && Types.isNumber(value.focusX)) { |
|||
x = value.focusX; |
|||
} |
|||
|
|||
if (value && Types.isNumber(value.focusY)) { |
|||
y = value.focusY; |
|||
} |
|||
|
|||
return { x, y }; |
|||
} |
|||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue