Add print size presets, 18x24 frame, and Prepare for Print workflow
- Add 18x24" to FRAME_SIZES in backend and frontend (frame_fit.js) - Add 200 DPI option to frame_fit dialog (adequate for large-format prints) - Add 18x24 portrait/landscape at 200 and 300 DPI to Canvas Size presets (size.js) - New /api/print/prepare endpoint: chains AI upscale to target DPI then frame-fit in one server-side call (avoids round-tripping a large upscaled image) - New print_prepare.js module: "Prepare for Print" dialog with per-frame quality assessment (current effective DPI, needed upscale factor, AI vs Lanczos note) - Add "Prepare for Print..." to Image menu above "Fit to Frame..." https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM
This commit is contained in:
@@ -21,6 +21,7 @@ FRAME_SIZES = {
|
|||||||
"8x10": (8, 10),
|
"8x10": (8, 10),
|
||||||
"11x14": (11, 14),
|
"11x14": (11, 14),
|
||||||
"16x20": (16, 20),
|
"16x20": (16, 20),
|
||||||
|
"18x24": (18, 24),
|
||||||
"20x24": (20, 24),
|
"20x24": (20, 24),
|
||||||
"24x36": (24, 36),
|
"24x36": (24, 36),
|
||||||
# Square
|
# Square
|
||||||
@@ -65,6 +66,16 @@ class UpscaleRequest(BaseModel):
|
|||||||
method: str = "auto"
|
method: str = "auto"
|
||||||
|
|
||||||
|
|
||||||
|
class PrepareRequest(BaseModel):
|
||||||
|
image: str # base64
|
||||||
|
frame: str # e.g. "8x10"
|
||||||
|
orientation: Literal["auto", "portrait", "landscape"] = "auto"
|
||||||
|
target_dpi: int = 300
|
||||||
|
upscale_method: str = "auto" # auto / realesrgan_pytorch / realesrgan_ncnn / lanczos
|
||||||
|
mode: Literal["crop", "extend", "smart"] = "smart"
|
||||||
|
prompt: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
# ── Frame sizes endpoint ───────────────────────────────────────────────────
|
# ── Frame sizes endpoint ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.get("/frame-sizes")
|
@router.get("/frame-sizes")
|
||||||
@@ -335,6 +346,105 @@ def upscale_install_status():
|
|||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/prepare")
|
||||||
|
async def prepare_for_print(req: PrepareRequest):
|
||||||
|
"""
|
||||||
|
One-shot Prepare for Print: AI upscale to reach target DPI, then fit to frame.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Resolve target pixel dimensions (frame × target_dpi, orientation-adjusted)
|
||||||
|
2. Calculate needed upscale factor so the image meets the target resolution
|
||||||
|
3. Run Real-ESRGAN if scale > 1.05 (else skip — already large enough)
|
||||||
|
4. Run frame-fit (crop / extend / smart) to exact target dimensions
|
||||||
|
5. Return the print-ready image and a quality report
|
||||||
|
"""
|
||||||
|
if req.frame not in FRAME_SIZES:
|
||||||
|
raise HTTPException(status_code=400,
|
||||||
|
detail=f"Unknown frame '{req.frame}'. Valid: {list(FRAME_SIZES.keys())}")
|
||||||
|
if not (72 <= req.target_dpi <= 600):
|
||||||
|
raise HTTPException(status_code=400, detail="target_dpi must be 72–600")
|
||||||
|
|
||||||
|
try:
|
||||||
|
image = Image.open(BytesIO(_decode(req.image))).convert("RGB")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Could not decode image: {e}")
|
||||||
|
|
||||||
|
fw, fh = FRAME_SIZES[req.frame]
|
||||||
|
img_w, img_h = image.size
|
||||||
|
|
||||||
|
# Resolve orientation (same logic as frame_fit)
|
||||||
|
img_landscape = img_w >= img_h
|
||||||
|
frame_landscape = fw >= fh
|
||||||
|
if req.orientation == "landscape":
|
||||||
|
fw, fh = max(fw, fh), min(fw, fh)
|
||||||
|
elif req.orientation == "portrait":
|
||||||
|
fw, fh = min(fw, fh), max(fw, fh)
|
||||||
|
else:
|
||||||
|
if img_landscape and not frame_landscape:
|
||||||
|
fw, fh = fh, fw
|
||||||
|
elif not img_landscape and frame_landscape:
|
||||||
|
fw, fh = fh, fw
|
||||||
|
|
||||||
|
target_w = fw * req.target_dpi
|
||||||
|
target_h = fh * req.target_dpi
|
||||||
|
|
||||||
|
# Scale factor needed so the shorter dimension fills the frame
|
||||||
|
scale_w = target_w / img_w
|
||||||
|
scale_h = target_h / img_h
|
||||||
|
needed_scale = min(scale_w, scale_h) # fill-to-fit (extend) baseline
|
||||||
|
# For crop mode we need max; use the larger to be safe and let frame-fit crop
|
||||||
|
needed_scale_crop = max(scale_w, scale_h)
|
||||||
|
|
||||||
|
# Use the smaller (extend) scale as the upscale target; frame-fit handles the rest
|
||||||
|
upscale_factor = max(1.0, needed_scale)
|
||||||
|
upscale_applied = False
|
||||||
|
method_used = "none"
|
||||||
|
|
||||||
|
upscaled = image
|
||||||
|
if upscale_factor > 1.05:
|
||||||
|
# Cap per-pass at 4× (Real-ESRGAN works best at 2–4×)
|
||||||
|
remaining = upscale_factor
|
||||||
|
while remaining > 1.05:
|
||||||
|
pass_scale = min(remaining, 4.0)
|
||||||
|
# Round to one decimal to keep scale in 1.1–8.0 range accepted by upscale service
|
||||||
|
pass_scale = round(pass_scale, 1)
|
||||||
|
if pass_scale < 1.1:
|
||||||
|
break
|
||||||
|
from app.services.upscale import upscale_image
|
||||||
|
result_bytes, method_used = await upscale_image(upscaled, pass_scale, req.upscale_method)
|
||||||
|
upscaled = Image.open(BytesIO(result_bytes)).convert("RGB")
|
||||||
|
remaining /= pass_scale
|
||||||
|
upscale_applied = True
|
||||||
|
|
||||||
|
# Encode upscaled image and run frame-fit
|
||||||
|
upscaled_b64 = _encode(_to_png(upscaled))
|
||||||
|
|
||||||
|
fit_req = FrameFitRequest(
|
||||||
|
image=upscaled_b64,
|
||||||
|
frame=req.frame,
|
||||||
|
orientation=req.orientation,
|
||||||
|
mode=req.mode,
|
||||||
|
dpi=req.target_dpi,
|
||||||
|
prompt=req.prompt or "",
|
||||||
|
)
|
||||||
|
# Re-use the existing frame_fit logic inline
|
||||||
|
fit_response = await frame_fit(fit_req)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"result": fit_response["result"],
|
||||||
|
"frame": req.frame,
|
||||||
|
"orientation": fit_response["orientation"],
|
||||||
|
"output_pixels": fit_response["output_pixels"],
|
||||||
|
"output_inches": fit_response["output_inches"],
|
||||||
|
"dpi": req.target_dpi,
|
||||||
|
"mode_used": fit_response["mode_used"],
|
||||||
|
"upscale_applied": upscale_applied,
|
||||||
|
"upscale_factor": round(upscale_factor, 2),
|
||||||
|
"upscale_method": method_used,
|
||||||
|
"summary": fit_response["summary"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upscale")
|
@router.post("/upscale")
|
||||||
async def upscale(req: UpscaleRequest):
|
async def upscale(req: UpscaleRequest):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -344,6 +344,11 @@ const menuDefinition = [
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
target: 'image/remove_background.remove_background'
|
target: 'image/remove_background.remove_background'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Prepare for Print...',
|
||||||
|
ellipsis: true,
|
||||||
|
target: 'image/print_prepare.print_prepare'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Fit to Frame...',
|
name: 'Fit to Frame...',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { getCapabilities } from './../../api/capabilities.js';
|
|||||||
var instance = null;
|
var instance = null;
|
||||||
|
|
||||||
const FRAME_SIZES = [
|
const FRAME_SIZES = [
|
||||||
'4x6', '5x7', '8x10', '11x14', '16x20', '20x24', '24x36',
|
'4x6', '5x7', '8x10', '11x14', '16x20', '18x24', '20x24', '24x36',
|
||||||
'4x4', '8x8', '12x12',
|
'4x4', '8x8', '12x12',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ const FRAME_SIZES = [
|
|||||||
const FRAME_PX = {
|
const FRAME_PX = {
|
||||||
'4x6': [1200, 1800], '5x7': [1500, 2100],
|
'4x6': [1200, 1800], '5x7': [1500, 2100],
|
||||||
'8x10': [2400, 3000], '11x14': [3300, 4200],
|
'8x10': [2400, 3000], '11x14': [3300, 4200],
|
||||||
'16x20': [4800, 6000], '20x24': [6000, 7200],
|
'16x20': [4800, 6000], '18x24': [5400, 7200],
|
||||||
'24x36': [7200, 10800],
|
'20x24': [6000, 7200], '24x36': [7200, 10800],
|
||||||
'4x4': [1200, 1200], '8x8': [2400, 2400], '12x12': [3600, 3600],
|
'4x4': [1200, 1200], '8x8': [2400, 2400], '12x12': [3600, 3600],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class Image_frame_fit_class {
|
|||||||
name: 'dpi',
|
name: 'dpi',
|
||||||
title: 'Output DPI:',
|
title: 'Output DPI:',
|
||||||
value: '300',
|
value: '300',
|
||||||
values: ['72', '150', '300'],
|
values: ['72', '150', '200', '300'],
|
||||||
type: 'select',
|
type: 'select',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* Prepare for Print — one-click AI upscale + frame fit.
|
||||||
|
*
|
||||||
|
* Shows a quality assessment (current effective DPI, needed upscale factor,
|
||||||
|
* AI vs Lanczos note) then chains AI upscale → frame-fit in a single backend call.
|
||||||
|
*
|
||||||
|
* Menu target: image/print_prepare.print_prepare
|
||||||
|
*/
|
||||||
|
|
||||||
|
import app from './../../app.js';
|
||||||
|
import config from './../../config.js';
|
||||||
|
import Base_layers_class from './../../core/base-layers.js';
|
||||||
|
import Dialog_class from './../../libs/popup.js';
|
||||||
|
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||||
|
import { getCapabilities } from './../../api/capabilities.js';
|
||||||
|
|
||||||
|
const FRAME_SIZES = [
|
||||||
|
'5x7', '8x10', '11x14', '18x24', '16x20', '20x24', '24x36',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Portrait pixels at 300 DPI (label use only)
|
||||||
|
const FRAME_PX = {
|
||||||
|
'5x7': [1500, 2100], '8x10': [2400, 3000],
|
||||||
|
'11x14': [3300, 4200], '18x24': [5400, 7200],
|
||||||
|
'16x20': [4800, 6000], '20x24': [6000, 7200],
|
||||||
|
'24x36': [7200, 10800],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actual frame inches (portrait w, h)
|
||||||
|
const FRAME_IN = {
|
||||||
|
'5x7': [5, 7], '8x10': [8, 10], '11x14': [11, 14],
|
||||||
|
'18x24': [18, 24], '16x20': [16, 20], '20x24': [20, 24],
|
||||||
|
'24x36': [24, 36],
|
||||||
|
};
|
||||||
|
|
||||||
|
var instance = null;
|
||||||
|
|
||||||
|
class Image_print_prepare_class {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (instance) return instance;
|
||||||
|
instance = this;
|
||||||
|
this.Base_layers = new Base_layers_class();
|
||||||
|
this.Dialog = new Dialog_class();
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async print_prepare() {
|
||||||
|
if (!config.layer || config.layer.type !== 'image') {
|
||||||
|
alertify.error('Select an image layer first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var caps = await getCapabilities();
|
||||||
|
var hasAI = (caps.remote && caps.remote.healthy) || (caps.local && caps.local.local_gpu_available);
|
||||||
|
|
||||||
|
var W = config.layer.width_original;
|
||||||
|
var H = config.layer.height_original;
|
||||||
|
|
||||||
|
var qualityHtml = _buildQualityHtml(W, H, hasAI);
|
||||||
|
|
||||||
|
var frameLabels = FRAME_SIZES.map(s => {
|
||||||
|
var px = FRAME_PX[s] || [0, 0];
|
||||||
|
return `${s}" (${px[0]}×${px[1]}px @ 300dpi)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
var _this = this;
|
||||||
|
this.Dialog.show({
|
||||||
|
title: 'Prepare for Print',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
html: qualityHtml,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'frame',
|
||||||
|
title: 'Target frame size:',
|
||||||
|
value: frameLabels[0],
|
||||||
|
values: frameLabels,
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'orientation',
|
||||||
|
title: 'Orientation:',
|
||||||
|
value: 'auto',
|
||||||
|
values: ['auto', 'portrait', 'landscape'],
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'target_dpi',
|
||||||
|
title: 'Target DPI:',
|
||||||
|
value: '300',
|
||||||
|
values: ['200', '300'],
|
||||||
|
type: 'select',
|
||||||
|
comment: '200 dpi is fine for 18×24" and larger (viewed from a distance)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mode',
|
||||||
|
title: 'Fit mode:',
|
||||||
|
value: 'smart',
|
||||||
|
values: ['smart', 'crop', 'extend'],
|
||||||
|
type: 'select',
|
||||||
|
comment: 'smart = extend if gap <15%, else crop',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'upscale_method',
|
||||||
|
title: 'Upscale engine:',
|
||||||
|
value: 'auto',
|
||||||
|
values: ['auto', 'realesrgan_pytorch', 'realesrgan_ncnn', 'lanczos'],
|
||||||
|
type: 'select',
|
||||||
|
comment: hasAI ? 'auto picks Real-ESRGAN — genuinely adds detail' : 'auto picks Real-ESRGAN if available, else Lanczos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'prompt',
|
||||||
|
title: 'Extend prompt (optional):',
|
||||||
|
value: '',
|
||||||
|
placeholder: 'e.g. "natural background continuation" — blank works well',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'new_layer',
|
||||||
|
title: 'Result as new layer (keep original):',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
on_finish: async function (params) {
|
||||||
|
var frameKey = params.frame.split('"')[0];
|
||||||
|
await _this._run(frameKey, params, W, H);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _run(frameKey, params, origW, origH) {
|
||||||
|
if (this.isProcessing) return;
|
||||||
|
this.isProcessing = true;
|
||||||
|
|
||||||
|
var dpi = parseInt(params.target_dpi) || 300;
|
||||||
|
var inches = FRAME_IN[frameKey] || [8, 10];
|
||||||
|
var targetW = inches[0] * dpi;
|
||||||
|
var targetH = inches[1] * dpi;
|
||||||
|
|
||||||
|
// Orientation swap for display
|
||||||
|
var orient = params.orientation || 'auto';
|
||||||
|
var imgLandscape = origW >= origH;
|
||||||
|
var frameLandscape = inches[0] >= inches[1];
|
||||||
|
if (orient === 'landscape' || (orient === 'auto' && imgLandscape && !frameLandscape)) {
|
||||||
|
targetW = Math.max(inches[0], inches[1]) * dpi;
|
||||||
|
targetH = Math.min(inches[0], inches[1]) * dpi;
|
||||||
|
} else if (orient === 'portrait' || (orient === 'auto' && !imgLandscape && frameLandscape)) {
|
||||||
|
targetW = Math.min(inches[0], inches[1]) * dpi;
|
||||||
|
targetH = Math.max(inches[0], inches[1]) * dpi;
|
||||||
|
}
|
||||||
|
|
||||||
|
var neededScale = Math.max(targetW / origW, targetH / origH);
|
||||||
|
var willUpscale = neededScale > 1.05;
|
||||||
|
|
||||||
|
alertify.message(
|
||||||
|
willUpscale
|
||||||
|
? `Upscaling ${neededScale.toFixed(1)}× with AI, then fitting to frame… this may take a minute`
|
||||||
|
: 'Fitting to frame…',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var layerCanvas = document.createElement('canvas');
|
||||||
|
layerCanvas.width = origW;
|
||||||
|
layerCanvas.height = origH;
|
||||||
|
layerCanvas.getContext('2d').drawImage(config.layer.link, 0, 0);
|
||||||
|
var imageB64 = layerCanvas.toDataURL('image/png').split(',')[1];
|
||||||
|
|
||||||
|
var base = window.API_BASE_URL || '';
|
||||||
|
var r = await fetch(`${base}/api/print/prepare`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
image: imageB64,
|
||||||
|
frame: frameKey,
|
||||||
|
orientation: orient,
|
||||||
|
target_dpi: dpi,
|
||||||
|
upscale_method: params.upscale_method || 'auto',
|
||||||
|
mode: params.mode || 'smart',
|
||||||
|
prompt: params.prompt || '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
var err = await r.json().catch(() => ({ detail: 'Server error' }));
|
||||||
|
throw new Error(err.detail || 'Prepare failed');
|
||||||
|
}
|
||||||
|
var result = await r.json();
|
||||||
|
|
||||||
|
var img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
var resultCanvas = document.createElement('canvas');
|
||||||
|
resultCanvas.width = img.naturalWidth;
|
||||||
|
resultCanvas.height = img.naturalHeight;
|
||||||
|
resultCanvas.getContext('2d').drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
var fitW = img.naturalWidth;
|
||||||
|
var fitH = img.naturalHeight;
|
||||||
|
|
||||||
|
if (params.new_layer) {
|
||||||
|
app.State.do_action(
|
||||||
|
new app.Actions.Bundle_action('print_prepare_layer', 'Prepare for Print', [
|
||||||
|
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} ${dpi}dpi`,
|
||||||
|
type: 'image',
|
||||||
|
data: img.src,
|
||||||
|
x: 0, y: 0,
|
||||||
|
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('print_prepare', 'Prepare for Print', [
|
||||||
|
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'),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
alertify.dismissAll();
|
||||||
|
var upscaleNote = result.upscale_applied
|
||||||
|
? ` · ${result.upscale_factor}× ${result.upscale_method}`
|
||||||
|
: ' · no upscale needed';
|
||||||
|
alertify.success(
|
||||||
|
`Print-ready! ${fitW}×${fitH}px @ ${dpi} DPI (${frameKey}")${upscaleNote}`
|
||||||
|
);
|
||||||
|
this.isProcessing = false;
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
alertify.dismissAll();
|
||||||
|
alertify.error('Failed to load result.');
|
||||||
|
this.isProcessing = false;
|
||||||
|
};
|
||||||
|
img.src = 'data:image/png;base64,' + result.result;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alertify.dismissAll();
|
||||||
|
alertify.error('Prepare for Print failed: ' + (err.message || err));
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildQualityHtml(W, H, hasAI) {
|
||||||
|
var rows = FRAME_SIZES.map(key => {
|
||||||
|
var inches = FRAME_IN[key];
|
||||||
|
// Effective DPI: smaller of the two dimensions (limiting factor)
|
||||||
|
var effDpi = Math.round(Math.min(W / inches[0], H / inches[1]));
|
||||||
|
var quality = effDpi >= 300 ? '✓ excellent'
|
||||||
|
: effDpi >= 200 ? '✓ good for large format'
|
||||||
|
: effDpi >= 150 ? '~ acceptable'
|
||||||
|
: '✗ needs upscaling';
|
||||||
|
var color = effDpi >= 300 ? '#44cc44'
|
||||||
|
: effDpi >= 200 ? '#88cc44'
|
||||||
|
: effDpi >= 150 ? '#ffaa44'
|
||||||
|
: '#ff6644';
|
||||||
|
var neededScale = Math.max(1, Math.ceil((300 / effDpi) * 10) / 10);
|
||||||
|
var scaleNote = effDpi >= 300 ? '' : ` → need ~${neededScale.toFixed(1)}× upscale`;
|
||||||
|
return `<tr>
|
||||||
|
<td style="color:#aaa;padding:2px 10px 2px 0;white-space:nowrap">${key}"</td>
|
||||||
|
<td style="color:#ddd;white-space:nowrap">${effDpi} DPI</td>
|
||||||
|
<td style="color:${color};padding-left:8px">${quality}${scaleNote}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
var aiNote = hasAI
|
||||||
|
? '<span style="color:#44cc44">Real-ESRGAN available — will add genuine sharpness (AI reconstructs detail)</span>'
|
||||||
|
: '<span style="color:#ffaa44">No AI provider — will use Lanczos (resizes but doesn\'t add detail)</span>';
|
||||||
|
|
||||||
|
return `<div style="font-size:11px;margin:0 0 8px">
|
||||||
|
<div style="color:#aaa;margin-bottom:6px">Current image: <span style="color:#ddd">${W}×${H}px</span> · ${aiNote}</div>
|
||||||
|
<table style="width:100%;border-collapse:collapse;margin-bottom:6px">${rows}</table>
|
||||||
|
<div style="color:#888">200 DPI is fine for 18×24" and larger prints viewed from 2+ feet.</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Image_print_prepare_class;
|
||||||
@@ -16,6 +16,10 @@ const PRINT_SIZES = [
|
|||||||
[3000, 2400, '8x10" Landscape'],
|
[3000, 2400, '8x10" Landscape'],
|
||||||
[3300, 4200, '11x14" Portrait'],
|
[3300, 4200, '11x14" Portrait'],
|
||||||
[4200, 3300, '11x14" Landscape'],
|
[4200, 3300, '11x14" Landscape'],
|
||||||
|
[3600, 4800, '18x24" Portrait 200dpi'],
|
||||||
|
[4800, 3600, '18x24" Landscape 200dpi'],
|
||||||
|
[5400, 7200, '18x24" Portrait 300dpi'],
|
||||||
|
[7200, 5400, '18x24" Landscape 300dpi'],
|
||||||
];
|
];
|
||||||
|
|
||||||
class Image_size_class {
|
class Image_size_class {
|
||||||
|
|||||||
Reference in New Issue
Block a user