diff --git a/frontend/src/js/modules/image/frame_fit.js b/frontend/src/js/modules/image/frame_fit.js index cfcd1fa..81ac049 100644 --- a/frontend/src/js/modules/image/frame_fit.js +++ b/frontend/src/js/modules/image/frame_fit.js @@ -164,26 +164,41 @@ class Image_frame_fit_class { resultCanvas.height = img.naturalHeight; resultCanvas.getContext('2d').drawImage(img, 0, 0); + var fitW = img.naturalWidth; + var fitH = img.naturalHeight; + if (params.new_layer) { var dataURL = img.src; app.State.do_action( new app.Actions.Bundle_action('frame_fit_layer', 'Fit to Frame', [ + new app.Actions.Prepare_canvas_action('undo'), + new app.Actions.Update_config_action({ + WIDTH: fitW, + HEIGHT: fitH, + }), new app.Actions.Insert_layer_action({ name: `${frameKey} fit`, type: 'image', data: dataURL, x: 0, y: 0, - width: img.naturalWidth, - height: img.naturalHeight, - width_original: img.naturalWidth, - height_original: img.naturalHeight, - }) + width: fitW, + height: fitH, + width_original: fitW, + height_original: fitH, + }), + new app.Actions.Prepare_canvas_action('do'), ]) ); } else { app.State.do_action( new app.Actions.Bundle_action('frame_fit', 'Fit to Frame', [ - new app.Actions.Update_layer_image_action(resultCanvas) + new app.Actions.Prepare_canvas_action('undo'), + new app.Actions.Update_config_action({ + WIDTH: fitW, + HEIGHT: fitH, + }), + new app.Actions.Update_layer_image_action(resultCanvas), + new app.Actions.Prepare_canvas_action('do'), ]) ); } diff --git a/frontend/src/js/modules/image/resize.js b/frontend/src/js/modules/image/resize.js index 61a5f61..4385bb7 100644 --- a/frontend/src/js/modules/image/resize.js +++ b/frontend/src/js/modules/image/resize.js @@ -30,6 +30,7 @@ class Image_resize_class { this.Tools_settings = new Tools_settings_class(); this.pica = Pica(); this.Helper = new Helper_class(); + this._lastUnits = 'pixels'; this.set_events(); } @@ -50,25 +51,31 @@ class Image_resize_class { resize() { var _this = this; - var units = this.Tools_settings.get_setting('default_units'); + var savedUnits = this.Tools_settings.get_setting('default_units'); var resolution = this.Tools_settings.get_setting('resolution'); - //convert units - var width = this.Helper.get_user_unit(config.WIDTH, units, resolution); - var height = this.Helper.get_user_unit(config.HEIGHT, units, resolution); + var displayUnits = (savedUnits === 'inches') ? 'inches' : 'pixels'; + this._lastUnits = displayUnits; + + var width = this.Helper.get_user_unit(config.WIDTH, displayUnits, resolution); + var height = this.Helper.get_user_unit(config.HEIGHT, displayUnits, resolution); var settings = { title: 'Resize', params: [ - {name: "width", title: "Width:", value: '', placeholder: width, comment: units}, - {name: "height", title: "Height:", value: '', placeholder: height, comment: units}, + {name: "units", title: "Units:", value: displayUnits, values: ["pixels", "inches"]}, + {name: "width", title: "Width:", value: '', placeholder: width, comment: displayUnits}, + {name: "height", title: "Height:", value: '', placeholder: height, comment: displayUnits}, {name: "width_percent", title: "Width (%):", value: '', placeholder: 100, comment: "%"}, {name: "height_percent", title: "Height (%):", value: '', placeholder: 100, comment: "%"}, {name: "mode", title: "Mode:", values: ["Lanczos", "Hermite", "Basic"]}, - + {name: "crop_to_fill", title: "Crop to fill:", value: false}, {name: "sharpen", title: "Sharpen:", value: false}, {name: "layers", title: "Layers:", values: ["All", "Active"], value: "All"}, ], + on_change: function(params) { + _this.units_change_handler(params); + }, on_finish: function (params) { _this.do_resize(params); }, @@ -78,16 +85,63 @@ class Image_resize_class { document.getElementById("pop_data_width").select(); } + /** + * Called on any dialog field change; reacts only when the units radio switches. + * Updates width/height placeholders and labels, and persists the choice globally. + */ + units_change_handler(params) { + var units = params.units; + if (units === this._lastUnits) return; + + this._lastUnits = units; + var resolution = this.Tools_settings.get_setting('resolution'); + + // Persist so Canvas Size and other dialogs open with the same units + const unitShort = {pixels: 'px', inches: '"', centimeters: 'cm', millimetres: 'mm'}; + this.Tools_settings.save_setting('default_units', units); + this.Tools_settings.save_setting('default_units_short', unitShort[units] || units); + + var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution); + var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution); + + var widthInput = document.getElementById('pop_data_width'); + var heightInput = document.getElementById('pop_data_height'); + if (widthInput) { + widthInput.placeholder = newWidth; + widthInput.value = ''; + } + if (heightInput) { + heightInput.placeholder = newHeight; + heightInput.value = ''; + } + + var wComment = widthInput ? widthInput.nextElementSibling : null; + var hComment = heightInput ? heightInput.nextElementSibling : null; + if (wComment && wComment.classList.contains('field_comment')) wComment.textContent = units; + if (hComment && hComment.classList.contains('field_comment')) hComment.textContent = units; + } + async do_resize(params) { //validate if (isNaN(params.width) && isNaN(params.height) && isNaN(params.width_percent) && isNaN(params.height_percent)) { alertify.error('Missing at least 1 size parameter.'); return false; } - + + // Crop-to-fill: scale to cover then center-crop; requires both dimensions + if (params.crop_to_fill == true) { + if (isNaN(params.width) || isNaN(params.height)) { + alertify.error('Crop to fill requires both Width and Height.'); + return false; + } + if (params.layers == 'All') { + return this.do_resize_crop_fill(params); + } + } + // Build a list of actions to execute for resize let actions = []; - + if (params.layers == 'All') { //resize all layers var skips = 0; @@ -112,15 +166,109 @@ class Image_resize_class { ); } + /** + * Resize all image layers using cover-scale then center-crop so the subject + * looks the same regardless of target aspect ratio (no stretching). + */ + async do_resize_crop_fill(params) { + var units = params.units || this.Tools_settings.get_setting('default_units'); + var resolution = this.Tools_settings.get_setting('resolution'); + + var targetWidth = this.Helper.get_internal_unit(parseFloat(params.width), units, resolution); + var targetHeight = this.Helper.get_internal_unit(parseFloat(params.height), units, resolution); + targetWidth = parseInt(targetWidth); + targetHeight = parseInt(targetHeight); + + if (!targetWidth || !targetHeight || targetWidth < 1 || targetHeight < 1) { + alertify.error('Invalid dimensions for crop to fill.'); + return; + } + + var srcWidth = config.WIDTH; + var srcHeight = config.HEIGHT; + + // Cover scale: image fills target, excess is cropped from center + var scale = Math.max(targetWidth / srcWidth, targetHeight / srcHeight); + var scaledW = Math.round(srcWidth * scale); + var scaledH = Math.round(srcHeight * scale); + var cropX = Math.round((scaledW - targetWidth) / 2); + var cropY = Math.round((scaledH - targetHeight) / 2); + + var mode = params.mode; + var sharpen = params.sharpen; + let actions = []; + + for (var i in config.layers) { + var layer = config.layers[i]; + if (layer.type !== 'image') continue; + if (layer.width == null || layer.height == null) continue; + + var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false); + var newLayerW = Math.round(layer.width * scale); + var newLayerH = Math.round(layer.height * scale); + + var useMode = mode; + if (useMode == "Hermite" && (newLayerW > canvas.width || newLayerH > canvas.height)) { + useMode = "Lanczos"; + } + + var tmp = document.createElement('canvas'); + tmp.width = newLayerW; + tmp.height = newLayerH; + + if (useMode == "Lanczos") { + await this.pica.resize(canvas, tmp, {alpha: true}); + } else if (useMode == "Hermite") { + tmp.getContext('2d').drawImage(canvas, 0, 0); + this.Hermite.resample_single(tmp, newLayerW, newLayerH, true); + } else { + tmp.getContext('2d').drawImage(canvas, 0, 0, newLayerW, newLayerH); + } + + if (sharpen == true) { + var ctx = tmp.getContext('2d'); + var imageData = ctx.getImageData(0, 0, tmp.width, tmp.height); + ctx.putImageData(this.ImageFilters.Sharpen(imageData, 1), 0, 0); + } + + var newX = Math.round(layer.x * scale) - cropX; + var newY = Math.round(layer.y * scale) - cropY; + + actions.push(new app.Actions.Update_layer_image_action(tmp, layer.id)); + actions.push(new app.Actions.Update_layer_action(layer.id, { + x: newX, + y: newY, + width: newLayerW, + height: newLayerH, + width_original: newLayerW, + height_original: newLayerH, + })); + } + + // Update canvas dimensions to exact target + actions = actions.concat([ + new app.Actions.Prepare_canvas_action('undo'), + new app.Actions.Update_config_action({ + WIDTH: targetWidth, + HEIGHT: targetHeight, + }), + new app.Actions.Prepare_canvas_action('do'), + ]); + + return app.State.do_action( + new app.Actions.Bundle_action('resize_layers', 'Resize Layers', actions) + ); + } + /** * Generates actions that will resize layer (image, text, vector), returns a promise that rejects on failure. - * + * * @param {object} layer * @param {object} params * @returns {Promise} Returns array of actions to perform */ async resize_layer(layer, params) { - var units = this.Tools_settings.get_setting('default_units'); + var units = params.units || this.Tools_settings.get_setting('default_units'); var resolution = this.Tools_settings.get_setting('resolution'); var mode = params.mode; var width = parseFloat(params.width); @@ -168,7 +316,7 @@ class Image_resize_class { let new_y = params.layers == 'All' ? Math.round(layer.y * height / config.HEIGHT) : layer.y; let xratio = width / config.WIDTH; let yratio = height / config.HEIGHT; - + //is text if (layer.type == 'text') { let data = JSON.parse(JSON.stringify(layer.data)); @@ -183,7 +331,7 @@ class Image_resize_class { // Return actions return [ new app.Actions.Update_layer_action(layer.id, { - x: new_x, + x: new_x, y: new_y, data, width: layer.width * xratio, @@ -191,27 +339,27 @@ class Image_resize_class { }) ]; } - + //is vector else if (layer.is_vector == true && layer.width != null && layer.height != null) { // Return actions return [ new app.Actions.Update_layer_action(layer.id, { - x: new_x, + x: new_x, y: new_y, width: layer.width * xratio, height: layer.height * yratio }) ]; } - + //only images supported at this point else if (layer.type != 'image') { //error - no support alertify.error('Layer must be vector or image (convert it to raster).'); throw new Error('Layer is not compatible with resize'); } - + //get canvas from layer var canvas = this.Base_layers.convert_layer_to_canvas(layer.id, true, false); var ctx = canvas.getContext("2d"); @@ -221,15 +369,15 @@ class Image_resize_class { alertify.warning('Scaling up is not supported in Hermite, using Lanczos.'); mode = "Lanczos"; } - + //resize if (mode == "Lanczos") { //Pica resize with max quality - + var tmp_data = document.createElement("canvas"); tmp_data.width = width; tmp_data.height = height; - + await this.pica.resize(canvas, tmp_data, { alpha: true, }) @@ -250,11 +398,11 @@ class Image_resize_class { tmp_data.width = canvas.width; tmp_data.height = canvas.height; tmp_data.getContext("2d").drawImage(canvas, 0, 0); - + ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = width; canvas.height = height; - + ctx.drawImage(tmp_data, 0, 0, width, height); } @@ -268,7 +416,7 @@ class Image_resize_class { return [ new app.Actions.Update_layer_image_action(canvas, layer.id), new app.Actions.Update_layer_action(layer.id, { - x: new_x, + x: new_x, y: new_y, width: canvas.width, height: canvas.height, @@ -277,9 +425,9 @@ class Image_resize_class { }) ]; } - + resize_gui(params) { - var units = this.Tools_settings.get_setting('default_units'); + var units = params.units || this.Tools_settings.get_setting('default_units'); var resolution = this.Tools_settings.get_setting('resolution'); var width = parseFloat(params.width); @@ -326,4 +474,4 @@ class Image_resize_class { } -export default Image_resize_class; \ No newline at end of file +export default Image_resize_class; diff --git a/frontend/src/js/modules/image/size.js b/frontend/src/js/modules/image/size.js index 9f24979..39cbc34 100644 --- a/frontend/src/js/modules/image/size.js +++ b/frontend/src/js/modules/image/size.js @@ -82,6 +82,11 @@ class Image_size_class { this._lastUnits = units; var resolution = this.Tools_settings.get_setting('resolution'); + // Persist so Resize and other dialogs open with the same units + const unitShort = {pixels: 'px', inches: '"', centimeters: 'cm', millimetres: 'mm'}; + this.Tools_settings.save_setting('default_units', units); + this.Tools_settings.save_setting('default_units_short', unitShort[units] || units); + var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution); var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution);