parent
678c46b15e
commit
967646572c
|
@ -298,7 +298,7 @@ class ImageController extends GenericApiController
|
||||||
string $extension,
|
string $extension,
|
||||||
array $state
|
array $state
|
||||||
): Http\Response {
|
): Http\Response {
|
||||||
return Util::guardEx(function () use ($id, $name, $quality, $extension, $state) {
|
return Util::guardEx(function () use ($id, $name, $width, $height, $quality, $extension, $state) {
|
||||||
// Get the file
|
// Get the file
|
||||||
$file = $this->fs->getUserFile($id);
|
$file = $this->fs->getUserFile($id);
|
||||||
|
|
||||||
|
@ -322,6 +322,11 @@ class ImageController extends GenericApiController
|
||||||
// Apply the edits
|
// Apply the edits
|
||||||
(new Service\FileRobotMagick($image, $state))->apply();
|
(new Service\FileRobotMagick($image, $state))->apply();
|
||||||
|
|
||||||
|
// Resize the image
|
||||||
|
if ($width > 0 && $height > 0 && ($width !== $image->getImageWidth() || $height !== $image->getImageHeight())) {
|
||||||
|
$image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Save the image
|
// Save the image
|
||||||
$image->setImageFormat($extension);
|
$image->setImageFormat($extension);
|
||||||
$image->setImageCompressionQuality((int) round(100 * $quality));
|
$image->setImageCompressionQuality((int) round(100 * $quality));
|
||||||
|
|
|
@ -8,9 +8,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { basename, dirname, extname, join } from "path";
|
|
||||||
import { emit } from "@nextcloud/event-bus";
|
import { emit } from "@nextcloud/event-bus";
|
||||||
import { showError, showSuccess } from "@nextcloud/dialogs";
|
import { showError, showSuccess } from "@nextcloud/dialogs";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
|
@ -20,6 +19,9 @@ import { FilerobotImageEditorConfig } from "react-filerobot-image-editor";
|
||||||
import translations from "./ImageEditorTranslations";
|
import translations from "./ImageEditorTranslations";
|
||||||
|
|
||||||
import { API } from "../../services/API";
|
import { API } from "../../services/API";
|
||||||
|
import { IPhoto } from "../../types";
|
||||||
|
import * as utils from "../../services/Utils";
|
||||||
|
import { fetchImage } from "../frame/XImgCache";
|
||||||
|
|
||||||
let TABS, TOOLS: any;
|
let TABS, TOOLS: any;
|
||||||
type FilerobotImageEditor = import("filerobot-image-editor").default;
|
type FilerobotImageEditor = import("filerobot-image-editor").default;
|
||||||
|
@ -36,16 +38,8 @@ async function loadFilerobot() {
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
fileid: {
|
photo: {
|
||||||
type: Number,
|
type: Object as PropType<IPhoto>,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
etag: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -57,10 +51,8 @@ export default defineComponent({
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
config(): FilerobotImageEditorConfig & { theme: any } {
|
config(): FilerobotImageEditorConfig & { theme: any } {
|
||||||
const src = API.IMAGE_DECODABLE(this.fileid, this.etag);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: src,
|
source: "nonexistent",
|
||||||
|
|
||||||
defaultSavedImageName: this.defaultSavedImageName,
|
defaultSavedImageName: this.defaultSavedImageName,
|
||||||
defaultSavedImageType: this.defaultSavedImageType,
|
defaultSavedImageType: this.defaultSavedImageType,
|
||||||
|
@ -74,9 +66,9 @@ export default defineComponent({
|
||||||
defaultTabId: TABS.ADJUST,
|
defaultTabId: TABS.ADJUST,
|
||||||
defaultToolId: TOOLS.CROP,
|
defaultToolId: TOOLS.CROP,
|
||||||
|
|
||||||
// Displayed tabs, disabling watermark
|
// Displayed tabs, disabling watermark and draw
|
||||||
tabsIds: Object.values(TABS)
|
tabsIds: Object.values(TABS)
|
||||||
.filter((tab) => tab !== TABS.WATERMARK)
|
.filter((tab) => ![TABS.WATERMARK, TABS.ANNOTATE].includes(tab))
|
||||||
.sort((a: string, b: string) => a.localeCompare(b)) as any[],
|
.sort((a: string, b: string) => a.localeCompare(b)) as any[],
|
||||||
|
|
||||||
// onBeforeSave: this.onBeforeSave,
|
// onBeforeSave: this.onBeforeSave,
|
||||||
|
@ -115,19 +107,20 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
savingPixelRatio: 8,
|
savingPixelRatio: window.devicePixelRatio,
|
||||||
previewPixelRatio: window.devicePixelRatio,
|
previewPixelRatio: window.devicePixelRatio,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultSavedImageName(): string {
|
defaultSavedImageName(): string {
|
||||||
return basename(this.src, extname(this.src));
|
return this.photo.basename;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultSavedImageType(): "jpeg" | "png" | "webp" {
|
defaultSavedImageType(): "jpeg" | "png" | "webp" {
|
||||||
const mime = extname(this.src).slice(1);
|
if (
|
||||||
if (["jpeg", "png", "webp"].includes(mime)) {
|
["image/jpeg", "image/png", "image/webp"].includes(this.photo.mimetype)
|
||||||
return mime as any;
|
) {
|
||||||
|
return this.photo.mimetype.split("/")[1] as any;
|
||||||
}
|
}
|
||||||
return "jpeg";
|
return "jpeg";
|
||||||
},
|
},
|
||||||
|
@ -151,32 +144,16 @@ export default defineComponent({
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await loadFilerobot();
|
await loadFilerobot();
|
||||||
this.imageEditor = new FilerobotImageEditor(
|
|
||||||
<any>this.$refs.editor,
|
const div = <HTMLElement>this.$refs.editor;
|
||||||
<any>this.config
|
const config = this.config;
|
||||||
);
|
config.source = await this.getImg();
|
||||||
|
|
||||||
|
this.imageEditor = new FilerobotImageEditor(div, this.config);
|
||||||
this.imageEditor.render();
|
this.imageEditor.render();
|
||||||
|
|
||||||
|
// Handle keyboard
|
||||||
window.addEventListener("keydown", this.handleKeydown, true);
|
window.addEventListener("keydown", this.handleKeydown, true);
|
||||||
|
|
||||||
// Get latest exif data
|
|
||||||
try {
|
|
||||||
const res = await axios.get(
|
|
||||||
API.Q(API.IMAGE_INFO(this.fileid), {
|
|
||||||
basic: "1",
|
|
||||||
current: "1",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.exif = res.data?.current;
|
|
||||||
if (!this.exif) {
|
|
||||||
throw new Error("No exif data");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
alert(
|
|
||||||
this.t("memories", "Failed to get Exif data. Metadata may be lost!")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -187,6 +164,27 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async getImg(): Promise<any> {
|
||||||
|
if (this.photo.w && this.photo.h) {
|
||||||
|
// Fetch the image to a blob
|
||||||
|
const preview = utils.getPreviewUrl(this.photo, false, 2048);
|
||||||
|
|
||||||
|
// Fetch preview image
|
||||||
|
const img = new Image();
|
||||||
|
img.height = this.photo.h;
|
||||||
|
img.width = this.photo.w;
|
||||||
|
await new Promise(async (resolve) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.src = await fetchImage(preview);
|
||||||
|
});
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have the size, we need to use the original image
|
||||||
|
return API.IMAGE_DECODABLE(this.photo.fileid, this.photo.etag);
|
||||||
|
},
|
||||||
|
|
||||||
onClose(closingReason, haveNotSavedChanges) {
|
onClose(closingReason, haveNotSavedChanges) {
|
||||||
if (haveNotSavedChanges) {
|
if (haveNotSavedChanges) {
|
||||||
this.onExitWithoutSaving();
|
this.onExitWithoutSaving();
|
||||||
|
@ -201,67 +199,61 @@ export default defineComponent({
|
||||||
*
|
*
|
||||||
* @see https://github.com/scaleflex/filerobot-image-editor#onsave
|
* @see https://github.com/scaleflex/filerobot-image-editor#onsave
|
||||||
*/
|
*/
|
||||||
async onSave({
|
async onSave(
|
||||||
fullName,
|
data: {
|
||||||
imageBase64,
|
name?: string;
|
||||||
}: {
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
quality?: number;
|
||||||
|
extension?: string;
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
imageBase64?: string;
|
imageBase64?: string;
|
||||||
}): Promise<void> {
|
},
|
||||||
if (!imageBase64) {
|
state: any
|
||||||
throw new Error("No image data");
|
): Promise<void> {
|
||||||
}
|
// Copy state
|
||||||
|
state = JSON.parse(JSON.stringify(state));
|
||||||
|
|
||||||
const { origin, pathname } = new URL(this.src);
|
// Convert crop to relative values
|
||||||
const putUrl = origin + join(dirname(pathname), fullName);
|
if (state?.adjustments?.crop) {
|
||||||
|
const iw = state.shownImageDimensions.width;
|
||||||
if (
|
const ih = state.shownImageDimensions.height;
|
||||||
!this.exif &&
|
const { x, y, width, height } = state.adjustments.crop;
|
||||||
!confirm(this.t("memories", "No Exif data found! Continue?"))
|
state.adjustments.crop = {
|
||||||
) {
|
x: x / iw,
|
||||||
return;
|
y: y / ih,
|
||||||
|
width: width / iw,
|
||||||
|
height: height / ih,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await fetch(imageBase64).then((res) => res.blob());
|
const res = await axios.put(API.IMAGE_EDIT(this.photo.fileid), {
|
||||||
const response = await axios.put(putUrl, new File([blob], fullName));
|
name: data.name,
|
||||||
const fileid =
|
width: data.width,
|
||||||
parseInt(response?.headers?.["oc-fileid"]?.split("oc")[0]) || null;
|
height: data.height,
|
||||||
if (response.status >= 400) {
|
quality: data.quality,
|
||||||
throw new Error("Failed to save image");
|
extension: data.extension,
|
||||||
}
|
state: state,
|
||||||
|
|
||||||
// Strip old and incorrect exif data
|
|
||||||
const exif = this.exif;
|
|
||||||
delete exif.Orientation;
|
|
||||||
delete exif.Rotation;
|
|
||||||
delete exif.ImageHeight;
|
|
||||||
delete exif.ImageWidth;
|
|
||||||
delete exif.ImageSize;
|
|
||||||
delete exif.ModifyDate;
|
|
||||||
delete exif.ExifImageHeight;
|
|
||||||
delete exif.ExifImageWidth;
|
|
||||||
delete exif.ExifImageSize;
|
|
||||||
delete exif.CompatibleBrands;
|
|
||||||
delete exif.FileType;
|
|
||||||
delete exif.FileTypeExtension;
|
|
||||||
delete exif.MIMEType;
|
|
||||||
delete exif.MajorBrand;
|
|
||||||
|
|
||||||
// Update exif data
|
|
||||||
await axios.patch(API.IMAGE_SETEXIF(fileid), {
|
|
||||||
raw: exif,
|
|
||||||
});
|
});
|
||||||
|
const fileid = res.data.fileid;
|
||||||
|
|
||||||
|
// Success, emit an appropriate event
|
||||||
showSuccess(this.t("memories", "Image saved successfully"));
|
showSuccess(this.t("memories", "Image saved successfully"));
|
||||||
if (fileid !== this.fileid) {
|
|
||||||
|
console.log(state);
|
||||||
|
|
||||||
|
if (fileid !== this.photo.fileid) {
|
||||||
|
console.log("Fileid changed", fileid);
|
||||||
emit("files:file:created", { fileid });
|
emit("files:file:created", { fileid });
|
||||||
} else {
|
} else {
|
||||||
|
console.log("Fileid unchanged", fileid);
|
||||||
emit("files:file:updated", { fileid });
|
emit("files:file:updated", { fileid });
|
||||||
}
|
}
|
||||||
this.onClose(undefined, false);
|
this.onClose(undefined, false);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
showError(this.t("memories", "Error saving image"));
|
showError(this.t("memories", "Error saving image"));
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue