Add inches, crop-to-fill, and persistent units to Resize dialog

Resize dialog:
- Units selector (pixels / inches) at the top; switching updates the
  Width/Height placeholders and clears any partially entered values
- New "Crop to fill" checkbox: when both Width and Height are given,
  scales the image with cover-fit (fills target without letterboxing)
  then center-crops — subject looks the same at 5x7, 8x10, 11x14
- resize_layer/resize_gui now honour params.units instead of only
  reading the global default_units setting

Global units persistence:
- Switching units in either Resize or Canvas Size saves the choice to
  default_units so both dialogs open with the same unit next time

https://claude.ai/code/session_017wupXfpjuoSdAqVyak4fJf
This commit is contained in:
Claude
2026-06-12 01:50:19 +00:00
parent 0bf7697916
commit f28d8e44b6
2 changed files with 175 additions and 26 deletions
+155 -9
View File
@@ -30,6 +30,7 @@ class Image_resize_class {
this.Tools_settings = new Tools_settings_class(); this.Tools_settings = new Tools_settings_class();
this.pica = Pica(); this.pica = Pica();
this.Helper = new Helper_class(); this.Helper = new Helper_class();
this._lastUnits = 'pixels';
this.set_events(); this.set_events();
} }
@@ -50,25 +51,31 @@ class Image_resize_class {
resize() { resize() {
var _this = this; 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'); var resolution = this.Tools_settings.get_setting('resolution');
//convert units var displayUnits = (savedUnits === 'inches') ? 'inches' : 'pixels';
var width = this.Helper.get_user_unit(config.WIDTH, units, resolution); this._lastUnits = displayUnits;
var height = this.Helper.get_user_unit(config.HEIGHT, units, resolution);
var width = this.Helper.get_user_unit(config.WIDTH, displayUnits, resolution);
var height = this.Helper.get_user_unit(config.HEIGHT, displayUnits, resolution);
var settings = { var settings = {
title: 'Resize', title: 'Resize',
params: [ params: [
{name: "width", title: "Width:", value: '', placeholder: width, comment: units}, {name: "units", title: "Units:", value: displayUnits, values: ["pixels", "inches"]},
{name: "height", title: "Height:", value: '', placeholder: height, comment: units}, {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: "width_percent", title: "Width (%):", value: '', placeholder: 100, comment: "%"},
{name: "height_percent", title: "Height (%):", value: '', placeholder: 100, comment: "%"}, {name: "height_percent", title: "Height (%):", value: '', placeholder: 100, comment: "%"},
{name: "mode", title: "Mode:", values: ["Lanczos", "Hermite", "Basic"]}, {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: "sharpen", title: "Sharpen:", value: false},
{name: "layers", title: "Layers:", values: ["All", "Active"], value: "All"}, {name: "layers", title: "Layers:", values: ["All", "Active"], value: "All"},
], ],
on_change: function(params) {
_this.units_change_handler(params);
},
on_finish: function (params) { on_finish: function (params) {
_this.do_resize(params); _this.do_resize(params);
}, },
@@ -78,6 +85,40 @@ class Image_resize_class {
document.getElementById("pop_data_width").select(); 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
this.Tools_settings.save_setting('default_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) { async do_resize(params) {
//validate //validate
if (isNaN(params.width) && isNaN(params.height) && isNaN(params.width_percent) && isNaN(params.height_percent)) { if (isNaN(params.width) && isNaN(params.height) && isNaN(params.width_percent) && isNaN(params.height_percent)) {
@@ -85,6 +126,17 @@ class Image_resize_class {
return false; 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 // Build a list of actions to execute for resize
let actions = []; let actions = [];
@@ -112,6 +164,100 @@ 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. * Generates actions that will resize layer (image, text, vector), returns a promise that rejects on failure.
* *
@@ -120,7 +266,7 @@ class Image_resize_class {
* @returns {Promise<object>} Returns array of actions to perform * @returns {Promise<object>} Returns array of actions to perform
*/ */
async resize_layer(layer, params) { 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 resolution = this.Tools_settings.get_setting('resolution');
var mode = params.mode; var mode = params.mode;
var width = parseFloat(params.width); var width = parseFloat(params.width);
@@ -279,7 +425,7 @@ class Image_resize_class {
} }
resize_gui(params) { 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 resolution = this.Tools_settings.get_setting('resolution');
var width = parseFloat(params.width); var width = parseFloat(params.width);
+3
View File
@@ -82,6 +82,9 @@ class Image_size_class {
this._lastUnits = units; this._lastUnits = units;
var resolution = this.Tools_settings.get_setting('resolution'); var resolution = this.Tools_settings.get_setting('resolution');
// Persist so Resize and other dialogs open with the same units
this.Tools_settings.save_setting('default_units', units);
var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution); var newWidth = this.Helper.get_user_unit(config.WIDTH, units, resolution);
var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution); var newHeight = this.Helper.get_user_unit(config.HEIGHT, units, resolution);