Compare commits
1
Commits
main
...
feat/load-image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4657e2b58 |
@@ -4,6 +4,15 @@ All notable changes to ComfyUI-AusBoss are documented here.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **New node: Load Image.** Loads a picture and sets its size in the same
|
||||
node, so a workflow needs no resize node after its loader. Size can be a
|
||||
megapixel count, a longest side, a scale, a box the picture fits inside, or
|
||||
a box it fills by cropping, rounded to a multiple you choose. Arrows step
|
||||
through the pictures in the input folder, and the node shows the size
|
||||
going in and coming out before you run. It also gives the file's name and
|
||||
its original size. With Size off the picture comes out exactly as the file
|
||||
has it.
|
||||
|
||||
- **Mask by Touch: type a name to take it out of the mask.** The name field
|
||||
under the picture now has a sign in front. Click it to switch between **+**
|
||||
(add by name) and **−** (take away by name), then type "face" or "hair" and
|
||||
|
||||
@@ -79,6 +79,12 @@ It returns the `image`, a `mask` of everything the model paints (padding, see-th
|
||||
|
||||
Krea 2 Rotate + Outpaint turns a photo, pads it and paints the new area in one pass. [Try the Krea 2 Rotate + Outpaint example →](example_workflows/Krea%202%20Rotate%20%2B%20Outpaint%20%28AusBoss%29.json)
|
||||
|
||||
### Load Image
|
||||
|
||||
Loads a picture and sets its size in one node. Pick a file, upload one, or step through the input folder with the arrows. **Size** can stay off, or bring the picture to a megapixel count, a longest side, a scale, a box it must fit inside, or a box it fills by cropping. **Round to** keeps the result on a multiple of 8, 16 or whatever the model wants. The node shows the size going in and the size coming out before you run, such as `912 × 1632 → 1144 × 2048`.
|
||||
|
||||
It returns the `image`, a `mask` of its see-through parts, the new `width` and `height`, the `filename` for a save prefix, and the file's `original_width` and `original_height`.
|
||||
|
||||
### Load Image + Pad
|
||||
|
||||
Makes a canvas for outpainting: drag the edges out to add room around a picture. The Source list shows the picture under the pointer. The new area can be a solid color, the average color of the nearest edge, the edge pixels smeared outward, a blurred copy of the picture, or a mirror of it. The card also has a seam feather, a canvas multiple and a megapixel budget, and **Exact padding** opens one control per side. A budget of 0 keeps the picture's size, and anything higher resizes the picture before padding. A picture wired into `source_image` is padded instead of the chosen file.
|
||||
|
||||
@@ -35,6 +35,7 @@ NODE_MODULES = [
|
||||
"node_lama_inpaint",
|
||||
"node_layout_boxes",
|
||||
"node_literals",
|
||||
"node_load_image",
|
||||
"node_load_image_pad",
|
||||
"node_load_audio",
|
||||
"node_load_video",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Load Image
|
||||
|
||||
Loads a picture and sets its size in the same node, so a workflow does not
|
||||
need a resize node and a size node after its loader.
|
||||
|
||||
Pick a file in **Source**, press **Load** to upload one, or use the arrows
|
||||
under it to step through the pictures in ComfyUI's input folder. The line
|
||||
beside the arrows shows the size of the file and, when the size changes, the
|
||||
size that comes out:
|
||||
|
||||
`912 × 1632 → 1144 × 2048`
|
||||
|
||||
That is the real output size, worked out before you run.
|
||||
|
||||
## Size
|
||||
|
||||
**Size** (`size`) picks how the picture is resized. Each choice shows only
|
||||
the numbers it uses.
|
||||
|
||||
| Choice | What it does | Its number |
|
||||
|---|---|---|
|
||||
| **off** | The picture comes out exactly as the file has it. | none |
|
||||
| **megapixels** | Brings the whole picture to a total size and keeps its shape. | **Megapixels** (`megapixels`): 1.0 is about 1024 × 1024 |
|
||||
| **longest side** | Sets the longer edge; the other edge follows. | **Longest side** (`longest_side`), in pixels |
|
||||
| **scale** | Multiplies both edges. | **Scale** (`scale`): 0.5 is half, 2 is double |
|
||||
| **fit inside** | Makes the picture as big as it can be inside a box, with nothing cut off. | **Width** and **Height** (`width`, `height`) |
|
||||
| **crop to fill** | Covers the box completely and cuts off what sticks out, evenly from both sides. | **Width** and **Height** |
|
||||
|
||||
**Round to** (`multiple`) rounds the new width and height to a multiple of
|
||||
that number. Most models want 8 or 16. Set it to 1 to keep the numbers as
|
||||
they are. It does nothing while Size is off.
|
||||
|
||||
## More
|
||||
|
||||
- **Method** (`method`): how pixels are resampled. `lanczos` is the sharpest
|
||||
for photos. `nearest` keeps pixel art crisp.
|
||||
- **Enlarge** (`enlarge`): on, a small picture is made bigger to reach the
|
||||
size you asked for. Off, a picture is only ever made smaller: one that is
|
||||
already below the target comes out at its own size, and crop to fill cuts
|
||||
the box's shape out of it without growing it.
|
||||
|
||||
## The picture on the node
|
||||
|
||||
The picture under the settings is ComfyUI's own preview, the same one the
|
||||
stock Load Image shows. Drop a file on the node or paste one to upload it,
|
||||
and right-click the node for the mask editor and for copy and paste.
|
||||
|
||||
## Outputs
|
||||
|
||||
- `image`: the picture at its new size.
|
||||
- `mask`: white where the picture is see-through and black where it is
|
||||
solid, as the stock loader gives it. A picture with no transparency gives
|
||||
an all black mask. It is always the same size as `image`.
|
||||
- `width`, `height`: the size that comes out.
|
||||
- `filename`: the file's name without its folder or its ending
|
||||
(`shots/hero.png` gives `hero`), ready for a save node's prefix.
|
||||
- `original_width`, `original_height`: the size of the file before any
|
||||
resize. Use them to bring a result back to the size you started from.
|
||||
|
||||
## Which loader to use
|
||||
|
||||
- **Load Image** (this node): load a picture and set its size.
|
||||
- **Load Image + Pad**: add empty space around a picture for outpainting.
|
||||
- **Image Crop + Rotate + Pad**: turn, crop and pad a picture by hand.
|
||||
|
||||
## Technical details
|
||||
|
||||
- Every frame of an animated file comes through as a batch, all at the same
|
||||
new size. A phone photo is turned the way its EXIF data says.
|
||||
- With Size off the pixels are the file's own, bit for bit, and Round to is
|
||||
not applied.
|
||||
- Rounding moves each edge to the nearest multiple, so the shape can change
|
||||
by up to half a step. Fit inside rounds down instead, so the result never
|
||||
leaves its box.
|
||||
- Crop to fill resizes the picture until it covers the box, then cuts the
|
||||
middle out. The mask goes through the same resize and the same cut.
|
||||
- With Enlarge off, a rounded size is rounded down so it never passes the
|
||||
file's own size.
|
||||
- The size line on the node uses the same arithmetic as the run. The pack's
|
||||
tests hold the two to the same answers over a table of cases.
|
||||
- Files are read only from ComfyUI's input, output and temp folders. A name
|
||||
such as `photo.png [output]` loads a picture ComfyUI made.
|
||||
- The node runs again when the file changes on disk or a setting changes.
|
||||
@@ -17,6 +17,7 @@ help panel.
|
||||
| Pick a picture size by shape and megapixels, in place of Empty Latent | Latent Size (`AUSBOSS_NODES_Resolution`) |
|
||||
| Resize a picture, or snap its size to a multiple | Image Resize (`AUSBOSS_NODES_ImageResize`), Align Image (`AUSBOSS_NODES_AlignImage`) |
|
||||
| Read a picture's width and height | Image Size (`AUSBOSS_NODES_ImageSize`) |
|
||||
| Load a picture and set its size (megapixels, longest side, a box) in one node | Load Image (`AUSBOSS_NODES_LoadImage`) |
|
||||
| Outpaint: load a picture and add empty space around it | Load Image + Pad (`AUSBOSS_NODES_LoadImagePad`) |
|
||||
| Turn, crop and pad a picture in one node | Image Crop + Rotate + Pad (`AUSBOSS_NODES_ImageCropRotatePad`) |
|
||||
| Inpaint only the masked part, then put it back | Crop For Inpaint (`AUSBOSS_NODES_CropForInpaint`) then Stitch Inpaint (`AUSBOSS_NODES_StitchInpaint`) |
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// The row under the file picker on Load Image: arrows that step through the
|
||||
// input folder, where the file is in the list ("14 / 212"), and the size the
|
||||
// picture goes in at and comes out at ("912 × 1632 → 1144 × 2048"). The
|
||||
// widgets underneath stay the source of truth: an arrow only changes the
|
||||
// image widget's value, the way picking from the list does.
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { placeText, planLoadSize, sizeText, stepFile } from "./load_size.mjs";
|
||||
import { mediaViewQuery } from "./media_list.mjs";
|
||||
import { FONT_UI, RADIUS_CONTROL, THEME as T } from "./theme.mjs";
|
||||
import { commitWidgetValue, comboValues } from "./widget_card_math.mjs";
|
||||
|
||||
const CSS_ID = "ausboss-load-strip-css";
|
||||
// Sizes already read, by file: stepping back to a picture shows its size at once.
|
||||
const SIZES_KEPT = 200;
|
||||
const sizes = new Map();
|
||||
|
||||
function ensureCss() {
|
||||
if (document.getElementById(CSS_ID)) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = CSS_ID;
|
||||
style.textContent = `
|
||||
.ausboss-load-strip{display:flex;align-items:center;gap:6px;height:100%;min-width:0;font:500 11px/1 ${FONT_UI};color:${T.label}}
|
||||
.ausboss-load-step{flex:none;width:26px;height:100%;padding:0;border:1px solid transparent;border-radius:${RADIUS_CONTROL}px;background:${T.fill};color:${T.inkSoft};font:600 11px/1 ${FONT_UI};cursor:pointer}
|
||||
.ausboss-load-step:hover{background:${T.fillHover};color:${T.inkHover}}
|
||||
.ausboss-load-step:focus-visible{outline:2px solid ${T.brand};outline-offset:1px}
|
||||
.ausboss-load-step:disabled{opacity:.4;cursor:default}
|
||||
.ausboss-load-place{flex:none;color:${T.faint};font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}
|
||||
.ausboss-load-size{flex:1 1 0;min-width:0;overflow:hidden;text-align:right;white-space:nowrap;text-overflow:ellipsis;color:${T.inkSoft};font-weight:600;font-variant-numeric:tabular-nums}
|
||||
.ausboss-load-size b{color:${T.brandInk};font-weight:700}
|
||||
.ausboss-load-size i{color:${T.faint};font-style:normal;margin:0 5px}
|
||||
`;
|
||||
document.head.append(style);
|
||||
}
|
||||
|
||||
const viewUrl = (value) => api.apiURL(`/view?${mediaViewQuery(value)}`);
|
||||
|
||||
// The picture's own size, read from the file the way the browser shows it.
|
||||
function readSize(file, done) {
|
||||
const known = sizes.get(file);
|
||||
if (known) return known;
|
||||
const picture = new Image();
|
||||
picture.onload = () => {
|
||||
if (sizes.size >= SIZES_KEPT) sizes.delete(sizes.keys().next().value);
|
||||
sizes.set(file, { width: picture.naturalWidth, height: picture.naturalHeight });
|
||||
done();
|
||||
};
|
||||
picture.src = viewUrl(file);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadImageStrip(node) {
|
||||
ensureCss();
|
||||
const root = document.createElement("div");
|
||||
root.className = "ausboss-load-strip";
|
||||
const widget = () => node.widgets?.find((item) => item.name === "image") ?? null;
|
||||
const linked = () => {
|
||||
const slot = node.inputs?.find((input) => input?.widget?.name === "image" || input?.name === "image");
|
||||
return slot?.link != null;
|
||||
};
|
||||
let last = {};
|
||||
|
||||
const step = (delta, label, title) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "ausboss-load-step";
|
||||
button.textContent = label;
|
||||
button.title = title;
|
||||
button.addEventListener("click", () => {
|
||||
const image = widget();
|
||||
if (!image) return;
|
||||
const next = stepFile(comboValues(image), image.value, delta);
|
||||
if (next === null || next === image.value) return;
|
||||
commitWidgetValue(node, image, next, app.canvas);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
});
|
||||
root.append(button);
|
||||
return button;
|
||||
};
|
||||
const back = step(-1, "◀", "The picture before this one in the input folder");
|
||||
const on = step(1, "▶", "The next picture in the input folder");
|
||||
const place = document.createElement("span");
|
||||
place.className = "ausboss-load-place";
|
||||
place.title = "Where this picture is in the input folder's list";
|
||||
const size = document.createElement("span");
|
||||
size.className = "ausboss-load-size";
|
||||
root.append(place, size);
|
||||
|
||||
const sync = (values = last) => {
|
||||
last = values;
|
||||
const image = widget();
|
||||
const files = comboValues(image);
|
||||
const file = String(values.image ?? "");
|
||||
place.textContent = placeText(files, file);
|
||||
const stuck = linked() || stepFile(files, file, 1) === null;
|
||||
back.disabled = stuck;
|
||||
on.disabled = stuck;
|
||||
const source = file ? readSize(file, () => { if (String(last.image ?? "") === file) sync(); }) : null;
|
||||
size.replaceChildren();
|
||||
if (!source) {
|
||||
size.title = "";
|
||||
return;
|
||||
}
|
||||
const plan = planLoadSize(source.width, source.height, values);
|
||||
const text = sizeText(source.width, source.height, plan);
|
||||
const [from, to] = text.split(" → ");
|
||||
size.append(from);
|
||||
if (to) {
|
||||
const arrow = document.createElement("i");
|
||||
arrow.textContent = "→";
|
||||
const out = document.createElement("b");
|
||||
out.textContent = to;
|
||||
size.append(arrow, out);
|
||||
}
|
||||
size.title = to
|
||||
? `The file is ${from}. The picture comes out at ${to}.`
|
||||
: `The picture comes out at its own size, ${from}.`;
|
||||
};
|
||||
return { element: root, sync };
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Load Image: the size a picture comes out at, worked out in the browser so
|
||||
// the node can show it before a run. This repeats plan_load_size in
|
||||
// nodes/_load_image_helpers.py; tests/fixtures/load_size_cases.json holds
|
||||
// both to the same answers. No DOM in here.
|
||||
|
||||
export const SIZE_MODES = Object.freeze(["off", "megapixels", "longest side", "scale", "fit inside", "crop to fill"]);
|
||||
|
||||
const roundHalfUp = (value) => Math.floor(value + 0.5);
|
||||
const whole = (value, fallback) => (Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback);
|
||||
// Nearest multiple, never below one step.
|
||||
const nearest = (value, step) => Math.max(step, roundHalfUp(value / step) * step);
|
||||
// The largest multiple that is not above the value, never below one step.
|
||||
const below = (value, step) => Math.max(step, Math.floor(value / step) * step);
|
||||
|
||||
function target(sourceW, sourceH, size, { megapixels, longest_side: longestSide, scale, width, height }) {
|
||||
const scaled = (factor) => [Math.max(1, roundHalfUp(sourceW * factor)), Math.max(1, roundHalfUp(sourceH * factor))];
|
||||
if (size === "megapixels") {
|
||||
const amount = Number(megapixels);
|
||||
return amount > 0 ? scaled(Math.sqrt((amount * 1e6) / (sourceW * sourceH))) : [sourceW, sourceH];
|
||||
}
|
||||
if (size === "longest side") {
|
||||
const edge = whole(longestSide, 0);
|
||||
if (edge <= 0) return [sourceW, sourceH];
|
||||
const [w, h] = scaled(edge / Math.max(sourceW, sourceH));
|
||||
return sourceW >= sourceH ? [edge, h] : [w, edge];
|
||||
}
|
||||
if (size === "scale") {
|
||||
const factor = Number(scale);
|
||||
return factor > 0 ? scaled(factor) : [sourceW, sourceH];
|
||||
}
|
||||
const fit = Math.min(Math.max(1, whole(width, 1)) / sourceW, Math.max(1, whole(height, 1)) / sourceH);
|
||||
return scaled(fit);
|
||||
}
|
||||
|
||||
// { resizeWidth, resizeHeight, cropX, cropY, width, height } for a picture of
|
||||
// sourceWidth x sourceHeight and the node's values, or null when the picture
|
||||
// has no size yet or the choice is not one the node knows.
|
||||
export function planLoadSize(sourceWidth, sourceHeight, values = {}) {
|
||||
const sourceW = whole(sourceWidth, 0);
|
||||
const sourceH = whole(sourceHeight, 0);
|
||||
const size = values.size ?? "off";
|
||||
if (sourceW <= 0 || sourceH <= 0 || !SIZE_MODES.includes(size)) return null;
|
||||
const same = { resizeWidth: sourceW, resizeHeight: sourceH, cropX: 0, cropY: 0, width: sourceW, height: sourceH };
|
||||
if (size === "off") return same;
|
||||
const step = Math.max(1, whole(values.multiple, 1));
|
||||
const enlarge = values.enlarge !== false;
|
||||
const boxW = Math.max(1, whole(values.width, 1024));
|
||||
const boxH = Math.max(1, whole(values.height, 1024));
|
||||
|
||||
if (size === "crop to fill") {
|
||||
let width = boxW;
|
||||
let height = boxH;
|
||||
if (enlarge) {
|
||||
width = nearest(width, step);
|
||||
height = nearest(height, step);
|
||||
} else {
|
||||
const grow = Math.max(width / sourceW, height / sourceH);
|
||||
if (grow > 1) {
|
||||
width = Math.max(1, Math.floor(width / grow + 1e-6));
|
||||
height = Math.max(1, Math.floor(height / grow + 1e-6));
|
||||
}
|
||||
width = Math.min(below(width, step), Math.max(step, sourceW));
|
||||
height = Math.min(below(height, step), Math.max(step, sourceH));
|
||||
}
|
||||
const cover = Math.max(width / sourceW, height / sourceH);
|
||||
const resizeWidth = Math.max(width, Math.ceil(sourceW * cover - 1e-6));
|
||||
const resizeHeight = Math.max(height, Math.ceil(sourceH * cover - 1e-6));
|
||||
return {
|
||||
resizeWidth, resizeHeight,
|
||||
cropX: Math.floor((resizeWidth - width) / 2), cropY: Math.floor((resizeHeight - height) / 2),
|
||||
width, height,
|
||||
};
|
||||
}
|
||||
|
||||
let [targetW, targetH] = target(sourceW, sourceH, size, { ...values, width: boxW, height: boxH });
|
||||
if (!enlarge && (targetW > sourceW || targetH > sourceH)) [targetW, targetH] = [sourceW, sourceH];
|
||||
const rounded = (wanted, source, box) => {
|
||||
let value = nearest(wanted, step);
|
||||
if (size === "fit inside" && value > box) value = below(box, step);
|
||||
if (!enlarge && value > source) value = below(source, step);
|
||||
return value;
|
||||
};
|
||||
const width = rounded(targetW, sourceW, boxW);
|
||||
const height = rounded(targetH, sourceH, boxH);
|
||||
return { resizeWidth: width, resizeHeight: height, cropX: 0, cropY: 0, width, height };
|
||||
}
|
||||
|
||||
// "912 × 1632" for a picture that comes out as it went in, "912 × 1632 →
|
||||
// 1144 × 2048" when its size changes, "" while the picture's size is unknown.
|
||||
export function sizeText(sourceWidth, sourceHeight, plan) {
|
||||
if (!(sourceWidth > 0 && sourceHeight > 0)) return "";
|
||||
const from = `${sourceWidth} × ${sourceHeight}`;
|
||||
if (!plan || (plan.width === sourceWidth && plan.height === sourceHeight)) return from;
|
||||
return `${from} → ${plan.width} × ${plan.height}`;
|
||||
}
|
||||
|
||||
// The file one step before or after the current one in the list, going
|
||||
// round at the ends. A file that is not in the list steps to the first or
|
||||
// the last. null when the list has nothing to step to.
|
||||
export function stepFile(files, current, delta) {
|
||||
const list = (Array.isArray(files) ? files : []).filter((name) => name !== "" && name != null);
|
||||
if (!list.length) return null;
|
||||
const at = list.indexOf(current);
|
||||
if (at < 0) return delta < 0 ? list[list.length - 1] : list[0];
|
||||
if (list.length === 1) return null;
|
||||
return list[(at + (delta < 0 ? -1 : 1) + list.length) % list.length];
|
||||
}
|
||||
|
||||
// "14 / 212": where the current file is in the list. "" for an empty list,
|
||||
// "– / 212" for a file that is not in it.
|
||||
export function placeText(files, current) {
|
||||
const list = (Array.isArray(files) ? files : []).filter((name) => name !== "" && name != null);
|
||||
if (!list.length) return "";
|
||||
const at = list.indexOf(current);
|
||||
return `${at < 0 ? "–" : at + 1} / ${list.length}`;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export const SOURCE_LOADERS = Object.freeze({
|
||||
LoadImageOutput: picture("image"),
|
||||
LoadVideo: video("file"),
|
||||
AUSBOSS_NODES_ImageCropRotatePad: picture("image"),
|
||||
AUSBOSS_NODES_LoadImage: picture("image"),
|
||||
// A wired source_image replaces the file, so the file may stay blank.
|
||||
AUSBOSS_NODES_LoadImagePad: (inputs) => (linked(inputs?.source_image) ? null : picture("image")(inputs)),
|
||||
AUSBOSS_NODES_LoadAudio: sound("audio"),
|
||||
|
||||
@@ -57,9 +57,9 @@ export function goesBehind(flying, ask, epoch) {
|
||||
|
||||
// The pack's own picture loaders. One of them wired straight into Mask by
|
||||
// Touch is run by the node itself, the way a press on Show picture runs it:
|
||||
// it may crop, turn or pad its file, so the file alone is not the picture,
|
||||
// it may crop, turn, pad or resize its file, so the file alone is not the picture,
|
||||
// but running it costs next to nothing.
|
||||
export const OWN_LOADERS = new Set(["AUSBOSS_NODES_ImageCropRotatePad", "AUSBOSS_NODES_LoadImagePad"]);
|
||||
export const OWN_LOADERS = new Set(["AUSBOSS_NODES_ImageCropRotatePad", "AUSBOSS_NODES_LoadImage", "AUSBOSS_NODES_LoadImagePad"]);
|
||||
|
||||
// What a loader will hand on, as far as the page can tell: its file and its
|
||||
// settings, as one text. Another text means another picture. Widgets that
|
||||
|
||||
@@ -12,6 +12,7 @@ import { formatWidgetVisibility } from "../shared/save_video_formats.mjs";
|
||||
import { hideInputsInDef } from "../shared/widget_visibility.mjs";
|
||||
import { castChips } from "../shared/h3_cast_chips.mjs";
|
||||
import { clipSetsDuration } from "../shared/h3_duration.mjs";
|
||||
import { loadImageStrip } from "../shared/load_image_strip.mjs";
|
||||
import { momentStrip } from "../shared/moment_strip.mjs";
|
||||
import { mountWidgetCard } from "../shared/widget_card.mjs";
|
||||
import { FONT_UI, THEME } from "../shared/theme.mjs";
|
||||
@@ -400,6 +401,28 @@ const CARDS = {
|
||||
title: "The longest the sound may be. Anything past it is cut off and shows in red. 0 means no limit." },
|
||||
],
|
||||
},
|
||||
// Load Image: the file, then where it is in the folder and the size it
|
||||
// goes in and comes out at, then the size settings. ComfyUI's own picture
|
||||
// preview sits under the card, so its mask editor and copy and paste work
|
||||
// as they do on the stock loader.
|
||||
AUSBOSS_NODES_LoadImage: {
|
||||
minWidth: 320, first: true, hide: ["upload"],
|
||||
rows: [
|
||||
{ widget: "image", label: "Source", kind: "select", preview: { kind: "image", url: viewUrl },
|
||||
button: { text: PANEL_LABELS.load, title: "Upload a picture into ComfyUI's input folder", onClick: (node) => node.widgets?.find((w) => w.name === "upload")?.callback?.() } },
|
||||
{ panel: loadImageStrip, height: 26 },
|
||||
{ widget: "size", label: "Size", kind: "select" },
|
||||
{ widget: "megapixels", label: "Megapixels", suffix: "MP", when: isMode("size", "megapixels") },
|
||||
{ widget: "longest_side", label: "Longest side", suffix: "px", when: isMode("size", "longest side") },
|
||||
{ widget: "scale", label: "Scale", suffix: "\u00d7", when: isMode("size", "scale") },
|
||||
{ widget: "width", label: "Width", suffix: "px", when: isMode("size", "fit inside", "crop to fill") },
|
||||
{ widget: "height", label: "Height", suffix: "px", when: isMode("size", "fit inside", "crop to fill") },
|
||||
{ widget: "multiple", label: "Round to", snap: true, step: 8, when: (values) => values.size !== "off" },
|
||||
{ group: "more", label: "More" },
|
||||
{ widget: "method", label: "Method", kind: "select", group: "more" },
|
||||
{ widget: "enlarge", label: "Enlarge", group: "more" },
|
||||
],
|
||||
},
|
||||
AUSBOSS_NODES_LoadImagePad: {
|
||||
minWidth: 340, first: true, hide: ["upload"],
|
||||
rows: [
|
||||
|
||||
@@ -593,6 +593,65 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AUSBOSS_NODES_LoadImage": {
|
||||
"display_name": "加载图像 🆎",
|
||||
"description": "在同一个节点里加载图片并设定尺寸。可以选择文件、上传,或用箭头逐张浏览输入文件夹。尺寸可以关闭,也可以按百万像素、最长边、缩放倍数、放入一个框内,或裁剪填满一个框来调整。运行之前,节点就会显示输入尺寸和输出尺寸。",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "图像"
|
||||
},
|
||||
"size": {
|
||||
"name": "尺寸"
|
||||
},
|
||||
"megapixels": {
|
||||
"name": "百万像素"
|
||||
},
|
||||
"longest_side": {
|
||||
"name": "最长边"
|
||||
},
|
||||
"scale": {
|
||||
"name": "缩放倍数"
|
||||
},
|
||||
"width": {
|
||||
"name": "宽度"
|
||||
},
|
||||
"height": {
|
||||
"name": "高度"
|
||||
},
|
||||
"multiple": {
|
||||
"name": "取整倍数"
|
||||
},
|
||||
"method": {
|
||||
"name": "插值"
|
||||
},
|
||||
"enlarge": {
|
||||
"name": "放大"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "图像"
|
||||
},
|
||||
"1": {
|
||||
"name": "遮罩"
|
||||
},
|
||||
"2": {
|
||||
"name": "宽度"
|
||||
},
|
||||
"3": {
|
||||
"name": "高度"
|
||||
},
|
||||
"4": {
|
||||
"name": "文件名"
|
||||
},
|
||||
"5": {
|
||||
"name": "原始宽度"
|
||||
},
|
||||
"6": {
|
||||
"name": "原始高度"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AUSBOSS_NODES_LoadImagePad": {
|
||||
"display_name": "加载图像 + 扩边 🆎",
|
||||
"description": "在一个节点里加载图像并扩边成扩图画布。拖动节点上画布的任意一边,直观地设置每边的扩边量;遮罩正好覆盖新扩出的部分,可选择向内羽化跨过接缝。画布会取整到整数倍,但不会给你没扩的边加条带;设置百万像素目标时会先缩放原图,让遮罩接缝保持清晰。",
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Load Image: the size a picture comes out at, as plain numbers.
|
||||
|
||||
``plan_load_size`` turns the node's size choice into what the picture is
|
||||
resized to and what is cut from it. It holds no tensors, so it is tested as
|
||||
numbers, and ``js/shared/load_size.mjs`` repeats it for the size the node
|
||||
shows before a run (``tests/fixtures/load_size_cases.json`` holds both to the
|
||||
same answers). ``apply_load_size`` runs a plan on a picture and its mask.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from ._resize_helpers import plan_resize, resample_batch, resolve_target, snap_to_multiple
|
||||
|
||||
SIZE_MODES = (
|
||||
"off",
|
||||
"megapixels",
|
||||
"longest side",
|
||||
"scale",
|
||||
"fit inside",
|
||||
"crop to fill",
|
||||
)
|
||||
|
||||
|
||||
def _round_half_up(value: float) -> int:
|
||||
return int(math.floor(value + 0.5))
|
||||
|
||||
|
||||
def _multiple_below(value: int, multiple: int) -> int:
|
||||
"""The largest multiple that is not above ``value``, never below one step."""
|
||||
step = max(1, int(multiple))
|
||||
return max(step, (int(value) // step) * step)
|
||||
|
||||
|
||||
def plan_load_size(
|
||||
source_width: int,
|
||||
source_height: int,
|
||||
size: str = "off",
|
||||
megapixels: float = 1.0,
|
||||
longest_side: int = 1024,
|
||||
scale: float = 1.0,
|
||||
width: int = 1024,
|
||||
height: int = 1024,
|
||||
multiple: int = 1,
|
||||
enlarge: bool = True,
|
||||
) -> dict[str, int]:
|
||||
"""What one picture is resized to (``resize_width``/``resize_height``),
|
||||
where a crop starts in the resized picture (``crop_x``/``crop_y``) and
|
||||
the size that comes out (``width``/``height``).
|
||||
|
||||
``off`` gives the picture back as it is; ``multiple`` rounds only a size
|
||||
that is being changed. With ``enlarge`` off nothing comes out larger than
|
||||
the picture: a target above it leaves the picture at its own size, and
|
||||
``crop to fill`` cuts the box's shape out of the picture without growing
|
||||
it. ``fit inside`` rounds down, so the result never leaves its box.
|
||||
"""
|
||||
source_w, source_h = int(source_width), int(source_height)
|
||||
if source_w <= 0 or source_h <= 0:
|
||||
raise ValueError("Load Image needs a picture larger than 0x0.")
|
||||
if size not in SIZE_MODES:
|
||||
raise ValueError(f"size must be one of {SIZE_MODES}, got '{size}'.")
|
||||
if size == "off":
|
||||
return {
|
||||
"resize_width": source_w,
|
||||
"resize_height": source_h,
|
||||
"crop_x": 0,
|
||||
"crop_y": 0,
|
||||
"width": source_w,
|
||||
"height": source_h,
|
||||
}
|
||||
step = max(1, int(multiple))
|
||||
|
||||
if size == "crop to fill":
|
||||
box_w, box_h = max(1, int(width)), max(1, int(height))
|
||||
if not enlarge:
|
||||
grow = max(box_w / source_w, box_h / source_h)
|
||||
if grow > 1.0:
|
||||
# The box keeps its shape and shrinks to what the picture holds.
|
||||
box_w = max(1, int(math.floor(box_w / grow + 1e-6)))
|
||||
box_h = max(1, int(math.floor(box_h / grow + 1e-6)))
|
||||
canvas_w = min(_multiple_below(box_w, step), max(step, source_w))
|
||||
canvas_h = min(_multiple_below(box_h, step), max(step, source_h))
|
||||
else:
|
||||
canvas_w = snap_to_multiple(box_w, step)
|
||||
canvas_h = snap_to_multiple(box_h, step)
|
||||
plan = plan_resize(source_w, source_h, canvas_w, canvas_h, "cover_crop", 1)
|
||||
return {
|
||||
"resize_width": plan["resize_width"],
|
||||
"resize_height": plan["resize_height"],
|
||||
"crop_x": plan["crop_x"],
|
||||
"crop_y": plan["crop_y"],
|
||||
"width": canvas_w,
|
||||
"height": canvas_h,
|
||||
}
|
||||
|
||||
if size == "megapixels":
|
||||
target_w, target_h = resolve_target(source_w, source_h, "megapixels", megapixels=megapixels)
|
||||
elif size == "longest side":
|
||||
target_w, target_h = resolve_target(source_w, source_h, "longest_edge", edge_length=longest_side)
|
||||
elif size == "scale":
|
||||
target_w, target_h = resolve_target(source_w, source_h, "scale_factor", scale_factor=scale)
|
||||
else: # fit inside
|
||||
fit = min(max(1, int(width)) / source_w, max(1, int(height)) / source_h)
|
||||
target_w = max(1, _round_half_up(source_w * fit))
|
||||
target_h = max(1, _round_half_up(source_h * fit))
|
||||
if not enlarge and (target_w > source_w or target_h > source_h):
|
||||
target_w, target_h = source_w, source_h
|
||||
|
||||
def rounded(target: int, source: int, box: int) -> int:
|
||||
value = snap_to_multiple(target, step)
|
||||
if size == "fit inside" and value > box:
|
||||
value = _multiple_below(box, step)
|
||||
if not enlarge and value > source:
|
||||
value = _multiple_below(source, step)
|
||||
return value
|
||||
|
||||
out_w = rounded(target_w, source_w, max(1, int(width)))
|
||||
out_h = rounded(target_h, source_h, max(1, int(height)))
|
||||
return {
|
||||
"resize_width": out_w,
|
||||
"resize_height": out_h,
|
||||
"crop_x": 0,
|
||||
"crop_y": 0,
|
||||
"width": out_w,
|
||||
"height": out_h,
|
||||
}
|
||||
|
||||
|
||||
def frames_to_image_and_mask(frames) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""RGBA PIL frames to a BHWC RGB batch and a BHW mask.
|
||||
|
||||
The mask is what ComfyUI's own Load Image gives: white where the picture
|
||||
is see-through, black where it is solid, and all black for a picture
|
||||
with no transparency (here at the picture's own size).
|
||||
"""
|
||||
images, masks = [], []
|
||||
for frame in frames:
|
||||
array = np.asarray(frame.convert("RGBA"), dtype=np.float32) / 255.0
|
||||
images.append(torch.from_numpy(array[..., :3].copy()))
|
||||
masks.append(torch.from_numpy(1.0 - array[..., 3]))
|
||||
return torch.stack(images, dim=0), torch.stack(masks, dim=0)
|
||||
|
||||
|
||||
def apply_load_size(
|
||||
image: torch.Tensor, mask: torch.Tensor, plan: dict[str, int], method: str
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Run a plan on a BHWC picture and its BHW mask. A plan that changes
|
||||
nothing hands both back untouched."""
|
||||
resize_w, resize_h = int(plan["resize_width"]), int(plan["resize_height"])
|
||||
out_w, out_h = int(plan["width"]), int(plan["height"])
|
||||
if (int(image.shape[2]), int(image.shape[1])) != (resize_w, resize_h):
|
||||
image = resample_batch(image, resize_w, resize_h, method)
|
||||
mask = resample_batch(mask.unsqueeze(-1), resize_w, resize_h, method).squeeze(-1).clamp(0.0, 1.0)
|
||||
if (resize_w, resize_h) != (out_w, out_h):
|
||||
x0, y0 = int(plan["crop_x"]), int(plan["crop_y"])
|
||||
image = image[:, y0 : y0 + out_h, x0 : x0 + out_w, :].contiguous()
|
||||
mask = mask[:, y0 : y0 + out_h, x0 : x0 + out_w].contiguous()
|
||||
return image, mask
|
||||
|
||||
|
||||
__all__ = ["SIZE_MODES", "apply_load_size", "frames_to_image_and_mask", "plan_load_size"]
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Load Image 🆎."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._execution_helpers import ignore_extra_inputs
|
||||
from ._load_image_helpers import (
|
||||
SIZE_MODES,
|
||||
apply_load_size,
|
||||
frames_to_image_and_mask,
|
||||
plan_load_size,
|
||||
)
|
||||
from ._media_helpers import list_input_images, load_image_frames, resolve_input_path
|
||||
from ._resize_helpers import INTERPOLATION_MODES
|
||||
from ._transform_engine import stable_file_fingerprint
|
||||
|
||||
|
||||
class AusBossLoadImage:
|
||||
CATEGORY = "🆎 AusBoss/Image"
|
||||
DESCRIPTION = (
|
||||
"Loads a picture and sets its size in the same node. Pick a file, "
|
||||
"upload one, or step through the input folder with the arrows. "
|
||||
"Size can stay off, or bring the picture to a megapixel count, a "
|
||||
"longest side, a scale, a box it must fit inside, or a box it fills "
|
||||
"by cropping. The node shows the size going in and the size coming "
|
||||
"out before you run."
|
||||
)
|
||||
SEARCH_ALIASES = [
|
||||
"load image",
|
||||
"load picture",
|
||||
"image loader",
|
||||
"resize on load",
|
||||
"megapixels",
|
||||
"longest side",
|
||||
"ausboss",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": (
|
||||
list_input_images(),
|
||||
{
|
||||
"image_upload": True,
|
||||
"tooltip": "The picture to load. Pick one from ComfyUI's input folder or upload a new one.",
|
||||
},
|
||||
),
|
||||
"size": (
|
||||
list(SIZE_MODES),
|
||||
{
|
||||
"default": "off",
|
||||
"tooltip": (
|
||||
"How the picture is resized. off keeps it as it "
|
||||
"is. megapixels, longest side and scale keep its "
|
||||
"shape. fit inside shrinks or grows it to sit in "
|
||||
"a box. crop to fill covers the box and cuts off "
|
||||
"what sticks out."
|
||||
),
|
||||
},
|
||||
),
|
||||
"megapixels": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.01,
|
||||
"max": 64.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "For megapixels: the picture's total size. 1.0 is about 1024 x 1024.",
|
||||
},
|
||||
),
|
||||
"longest_side": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 16,
|
||||
"max": 16384,
|
||||
"step": 1,
|
||||
"tooltip": "For longest side: the length of the longer edge, in pixels. The other edge follows.",
|
||||
},
|
||||
),
|
||||
"scale": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 1.0,
|
||||
"min": 0.05,
|
||||
"max": 8.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "For scale: 0.5 is half the size, 2 is double.",
|
||||
},
|
||||
),
|
||||
"width": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 16,
|
||||
"max": 16384,
|
||||
"step": 1,
|
||||
"tooltip": "For fit inside and crop to fill: the box's width in pixels.",
|
||||
},
|
||||
),
|
||||
"height": (
|
||||
"INT",
|
||||
{
|
||||
"default": 1024,
|
||||
"min": 16,
|
||||
"max": 16384,
|
||||
"step": 1,
|
||||
"tooltip": "For fit inside and crop to fill: the box's height in pixels.",
|
||||
},
|
||||
),
|
||||
"multiple": (
|
||||
"INT",
|
||||
{
|
||||
"default": 8,
|
||||
"min": 1,
|
||||
"max": 256,
|
||||
"step": 1,
|
||||
"tooltip": (
|
||||
"Rounds the new width and height to a multiple of "
|
||||
"this. Most models want 8 or 16. 1 leaves the "
|
||||
"numbers as they are. Not used while size is off."
|
||||
),
|
||||
},
|
||||
),
|
||||
"method": (
|
||||
list(INTERPOLATION_MODES),
|
||||
{
|
||||
"default": "lanczos",
|
||||
"tooltip": "How pixels are resampled. lanczos is the sharpest for photos; nearest keeps pixel art crisp.",
|
||||
},
|
||||
),
|
||||
"enlarge": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": True,
|
||||
"tooltip": (
|
||||
"On: a small picture is made bigger to reach the "
|
||||
"size you asked for. Off: a picture is only ever "
|
||||
"made smaller, and a small one comes out at its "
|
||||
"own size."
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT", "STRING", "INT", "INT")
|
||||
RETURN_NAMES = (
|
||||
"image",
|
||||
"mask",
|
||||
"width",
|
||||
"height",
|
||||
"filename",
|
||||
"original_width",
|
||||
"original_height",
|
||||
)
|
||||
OUTPUT_TOOLTIPS = (
|
||||
"The picture, at its new size.",
|
||||
"White where the picture is see-through, black where it is solid. "
|
||||
"All black for a picture with no transparency. Same size as the picture.",
|
||||
"Width of the picture that comes out, in pixels.",
|
||||
"Height of the picture that comes out, in pixels.",
|
||||
"The file's name without its folder or its ending, ready for a save "
|
||||
"node's prefix.",
|
||||
"Width of the file before any resize.",
|
||||
"Height of the file before any resize.",
|
||||
)
|
||||
FUNCTION = "load"
|
||||
|
||||
def load(
|
||||
self,
|
||||
image,
|
||||
size="off",
|
||||
megapixels=1.0,
|
||||
longest_side=1024,
|
||||
scale=1.0,
|
||||
width=1024,
|
||||
height=1024,
|
||||
multiple=8,
|
||||
method="lanczos",
|
||||
enlarge=True,
|
||||
):
|
||||
path = resolve_input_path(image)
|
||||
picture, mask = frames_to_image_and_mask(load_image_frames(path))
|
||||
source_w, source_h = int(picture.shape[2]), int(picture.shape[1])
|
||||
plan = plan_load_size(
|
||||
source_w,
|
||||
source_h,
|
||||
str(size),
|
||||
float(megapixels),
|
||||
int(longest_side),
|
||||
float(scale),
|
||||
int(width),
|
||||
int(height),
|
||||
int(multiple),
|
||||
bool(enlarge),
|
||||
)
|
||||
picture, mask = apply_load_size(picture, mask, plan, str(method))
|
||||
return (
|
||||
picture,
|
||||
mask,
|
||||
int(plan["width"]),
|
||||
int(plan["height"]),
|
||||
path.stem,
|
||||
source_w,
|
||||
source_h,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ignore_extra_inputs
|
||||
def VALIDATE_INPUTS(cls, image):
|
||||
try:
|
||||
resolve_input_path(image)
|
||||
except Exception as exc:
|
||||
return f"Load Image: {exc}"
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, image, **values):
|
||||
try:
|
||||
path = resolve_input_path(image)
|
||||
except Exception:
|
||||
path = image or ""
|
||||
return stable_file_fingerprint(path, {"image": image, **values})
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"AUSBOSS_NODES_LoadImage": AusBossLoadImage}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"AUSBOSS_NODES_LoadImage": "Load Image 🆎"}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -70,6 +70,7 @@ PUBLIC_NODE_IDS = {
|
||||
"AUSBOSS_NODES_LaMaInpaint",
|
||||
"AUSBOSS_NODES_LayoutBoxes",
|
||||
"AUSBOSS_NODES_LoadAudio",
|
||||
"AUSBOSS_NODES_LoadImage",
|
||||
"AUSBOSS_NODES_LoadImagePad",
|
||||
"AUSBOSS_NODES_LoadVideo",
|
||||
"AUSBOSS_NODES_LoraLoader",
|
||||
|
||||
Vendored
+256
@@ -0,0 +1,256 @@
|
||||
{"about":"Load Image size plans. nodes/_load_image_helpers.py made these; js/shared/load_size.mjs must give the same.",
|
||||
"columns":["source_width", "source_height", "size", "megapixels", "longest_side", "scale", "width", "height", "multiple", "enlarge", "resize_width", "resize_height", "crop_x", "crop_y", "out_width", "out_height"],
|
||||
"rows":[
|
||||
[912,1632,"off",1.0,1024,1.0,1024,1024,1,true,912,1632,0,0,912,1632],
|
||||
[912,1632,"off",1.0,1024,1.0,1024,1024,16,true,912,1632,0,0,912,1632],
|
||||
[912,1632,"megapixels",0.25,1024,1.0,1024,1024,1,true,374,669,0,0,374,669],
|
||||
[912,1632,"megapixels",2.0,1024,1.0,1024,1024,1,true,1057,1892,0,0,1057,1892],
|
||||
[912,1632,"longest side",1.0,1000,1.0,1024,1024,1,true,559,1000,0,0,559,1000],
|
||||
[912,1632,"longest side",1.0,2048,1.0,1024,1024,1,true,1144,2048,0,0,1144,2048],
|
||||
[912,1632,"scale",1.0,1024,0.33,1024,1024,1,true,301,539,0,0,301,539],
|
||||
[912,1632,"scale",1.0,1024,1.5,1024,1024,1,true,1368,2448,0,0,1368,2448],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1024,1024,1,true,572,1024,0,0,572,1024],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1024,1024,1,true,1024,1833,0,404,1024,1024],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1280,720,1,true,402,720,0,0,402,720],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1280,720,1,true,1280,2291,0,785,1280,720],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1000,600,1,true,335,600,0,0,335,600],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1000,600,1,true,1000,1790,0,595,1000,600],
|
||||
[912,1632,"megapixels",0.25,1024,1.0,1024,1024,1,false,374,669,0,0,374,669],
|
||||
[912,1632,"megapixels",2.0,1024,1.0,1024,1024,1,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"longest side",1.0,1000,1.0,1024,1024,1,false,559,1000,0,0,559,1000],
|
||||
[912,1632,"longest side",1.0,2048,1.0,1024,1024,1,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"scale",1.0,1024,0.33,1024,1024,1,false,301,539,0,0,301,539],
|
||||
[912,1632,"scale",1.0,1024,1.5,1024,1024,1,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1024,1024,1,false,572,1024,0,0,572,1024],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1024,1024,1,false,912,1632,0,360,912,912],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1280,720,1,false,402,720,0,0,402,720],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1280,720,1,false,912,1632,0,559,912,513],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1000,600,1,false,335,600,0,0,335,600],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1000,600,1,false,912,1632,0,542,912,547],
|
||||
[912,1632,"megapixels",0.25,1024,1.0,1024,1024,16,true,368,672,0,0,368,672],
|
||||
[912,1632,"megapixels",2.0,1024,1.0,1024,1024,16,true,1056,1888,0,0,1056,1888],
|
||||
[912,1632,"longest side",1.0,1000,1.0,1024,1024,16,true,560,1008,0,0,560,1008],
|
||||
[912,1632,"longest side",1.0,2048,1.0,1024,1024,16,true,1152,2048,0,0,1152,2048],
|
||||
[912,1632,"scale",1.0,1024,0.33,1024,1024,16,true,304,544,0,0,304,544],
|
||||
[912,1632,"scale",1.0,1024,1.5,1024,1024,16,true,1376,2448,0,0,1376,2448],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1024,1024,16,true,576,1024,0,0,576,1024],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1024,1024,16,true,1024,1833,0,404,1024,1024],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1280,720,16,true,400,720,0,0,400,720],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1280,720,16,true,1280,2291,0,785,1280,720],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1000,600,16,true,336,592,0,0,336,592],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1000,600,16,true,1008,1804,0,598,1008,608],
|
||||
[912,1632,"megapixels",0.25,1024,1.0,1024,1024,16,false,368,672,0,0,368,672],
|
||||
[912,1632,"megapixels",2.0,1024,1.0,1024,1024,16,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"longest side",1.0,1000,1.0,1024,1024,16,false,560,1008,0,0,560,1008],
|
||||
[912,1632,"longest side",1.0,2048,1.0,1024,1024,16,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"scale",1.0,1024,0.33,1024,1024,16,false,304,544,0,0,304,544],
|
||||
[912,1632,"scale",1.0,1024,1.5,1024,1024,16,false,912,1632,0,0,912,1632],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1024,1024,16,false,576,1024,0,0,576,1024],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1024,1024,16,false,912,1632,0,360,912,912],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1280,720,16,false,400,720,0,0,400,720],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1280,720,16,false,912,1632,0,560,912,512],
|
||||
[912,1632,"fit inside",1.0,1024,1.0,1000,600,16,false,336,592,0,0,336,592],
|
||||
[912,1632,"crop to fill",1.0,1024,1.0,1000,600,16,false,912,1632,0,544,912,544],
|
||||
[1920,1080,"off",1.0,1024,1.0,1024,1024,1,true,1920,1080,0,0,1920,1080],
|
||||
[1920,1080,"off",1.0,1024,1.0,1024,1024,16,true,1920,1080,0,0,1920,1080],
|
||||
[1920,1080,"megapixels",0.25,1024,1.0,1024,1024,1,true,667,375,0,0,667,375],
|
||||
[1920,1080,"megapixels",2.0,1024,1.0,1024,1024,1,true,1886,1061,0,0,1886,1061],
|
||||
[1920,1080,"longest side",1.0,1000,1.0,1024,1024,1,true,1000,563,0,0,1000,563],
|
||||
[1920,1080,"longest side",1.0,2048,1.0,1024,1024,1,true,2048,1152,0,0,2048,1152],
|
||||
[1920,1080,"scale",1.0,1024,0.33,1024,1024,1,true,634,356,0,0,634,356],
|
||||
[1920,1080,"scale",1.0,1024,1.5,1024,1024,1,true,2880,1620,0,0,2880,1620],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1024,1024,1,true,1024,576,0,0,1024,576],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1024,1024,1,true,1821,1024,398,0,1024,1024],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1280,720,1,true,1280,720,0,0,1280,720],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1280,720,1,true,1280,720,0,0,1280,720],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1000,600,1,true,1000,563,0,0,1000,563],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1000,600,1,true,1067,600,33,0,1000,600],
|
||||
[1920,1080,"megapixels",0.25,1024,1.0,1024,1024,1,false,667,375,0,0,667,375],
|
||||
[1920,1080,"megapixels",2.0,1024,1.0,1024,1024,1,false,1886,1061,0,0,1886,1061],
|
||||
[1920,1080,"longest side",1.0,1000,1.0,1024,1024,1,false,1000,563,0,0,1000,563],
|
||||
[1920,1080,"longest side",1.0,2048,1.0,1024,1024,1,false,1920,1080,0,0,1920,1080],
|
||||
[1920,1080,"scale",1.0,1024,0.33,1024,1024,1,false,634,356,0,0,634,356],
|
||||
[1920,1080,"scale",1.0,1024,1.5,1024,1024,1,false,1920,1080,0,0,1920,1080],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1024,1024,1,false,1024,576,0,0,1024,576],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1024,1024,1,false,1821,1024,398,0,1024,1024],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1280,720,1,false,1280,720,0,0,1280,720],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1280,720,1,false,1280,720,0,0,1280,720],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1000,600,1,false,1000,563,0,0,1000,563],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1000,600,1,false,1067,600,33,0,1000,600],
|
||||
[1920,1080,"megapixels",0.25,1024,1.0,1024,1024,16,true,672,368,0,0,672,368],
|
||||
[1920,1080,"megapixels",2.0,1024,1.0,1024,1024,16,true,1888,1056,0,0,1888,1056],
|
||||
[1920,1080,"longest side",1.0,1000,1.0,1024,1024,16,true,1008,560,0,0,1008,560],
|
||||
[1920,1080,"longest side",1.0,2048,1.0,1024,1024,16,true,2048,1152,0,0,2048,1152],
|
||||
[1920,1080,"scale",1.0,1024,0.33,1024,1024,16,true,640,352,0,0,640,352],
|
||||
[1920,1080,"scale",1.0,1024,1.5,1024,1024,16,true,2880,1616,0,0,2880,1616],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1024,1024,16,true,1024,576,0,0,1024,576],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1024,1024,16,true,1821,1024,398,0,1024,1024],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1280,720,16,true,1280,720,0,0,1280,720],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1280,720,16,true,1280,720,0,0,1280,720],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1000,600,16,true,992,560,0,0,992,560],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1000,600,16,true,1081,608,36,0,1008,608],
|
||||
[1920,1080,"megapixels",0.25,1024,1.0,1024,1024,16,false,672,368,0,0,672,368],
|
||||
[1920,1080,"megapixels",2.0,1024,1.0,1024,1024,16,false,1888,1056,0,0,1888,1056],
|
||||
[1920,1080,"longest side",1.0,1000,1.0,1024,1024,16,false,1008,560,0,0,1008,560],
|
||||
[1920,1080,"longest side",1.0,2048,1.0,1024,1024,16,false,1920,1072,0,0,1920,1072],
|
||||
[1920,1080,"scale",1.0,1024,0.33,1024,1024,16,false,640,352,0,0,640,352],
|
||||
[1920,1080,"scale",1.0,1024,1.5,1024,1024,16,false,1920,1072,0,0,1920,1072],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1024,1024,16,false,1024,576,0,0,1024,576],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1024,1024,16,false,1821,1024,398,0,1024,1024],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1280,720,16,false,1280,720,0,0,1280,720],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1280,720,16,false,1280,720,0,0,1280,720],
|
||||
[1920,1080,"fit inside",1.0,1024,1.0,1000,600,16,false,992,560,0,0,992,560],
|
||||
[1920,1080,"crop to fill",1.0,1024,1.0,1000,600,16,false,1053,592,30,0,992,592],
|
||||
[640,480,"off",1.0,1024,1.0,1024,1024,1,true,640,480,0,0,640,480],
|
||||
[640,480,"off",1.0,1024,1.0,1024,1024,16,true,640,480,0,0,640,480],
|
||||
[640,480,"megapixels",0.25,1024,1.0,1024,1024,1,true,577,433,0,0,577,433],
|
||||
[640,480,"megapixels",2.0,1024,1.0,1024,1024,1,true,1633,1225,0,0,1633,1225],
|
||||
[640,480,"longest side",1.0,1000,1.0,1024,1024,1,true,1000,750,0,0,1000,750],
|
||||
[640,480,"longest side",1.0,2048,1.0,1024,1024,1,true,2048,1536,0,0,2048,1536],
|
||||
[640,480,"scale",1.0,1024,0.33,1024,1024,1,true,211,158,0,0,211,158],
|
||||
[640,480,"scale",1.0,1024,1.5,1024,1024,1,true,960,720,0,0,960,720],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1024,1024,1,true,1024,768,0,0,1024,768],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1024,1024,1,true,1366,1024,171,0,1024,1024],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1280,720,1,true,960,720,0,0,960,720],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1280,720,1,true,1280,960,0,120,1280,720],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1000,600,1,true,800,600,0,0,800,600],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1000,600,1,true,1000,750,0,75,1000,600],
|
||||
[640,480,"megapixels",0.25,1024,1.0,1024,1024,1,false,577,433,0,0,577,433],
|
||||
[640,480,"megapixels",2.0,1024,1.0,1024,1024,1,false,640,480,0,0,640,480],
|
||||
[640,480,"longest side",1.0,1000,1.0,1024,1024,1,false,640,480,0,0,640,480],
|
||||
[640,480,"longest side",1.0,2048,1.0,1024,1024,1,false,640,480,0,0,640,480],
|
||||
[640,480,"scale",1.0,1024,0.33,1024,1024,1,false,211,158,0,0,211,158],
|
||||
[640,480,"scale",1.0,1024,1.5,1024,1024,1,false,640,480,0,0,640,480],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1024,1024,1,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1024,1024,1,false,640,480,80,0,480,480],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1280,720,1,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1280,720,1,false,640,480,0,60,640,360],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1000,600,1,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1000,600,1,false,640,480,0,48,640,384],
|
||||
[640,480,"megapixels",0.25,1024,1.0,1024,1024,16,true,576,432,0,0,576,432],
|
||||
[640,480,"megapixels",2.0,1024,1.0,1024,1024,16,true,1632,1232,0,0,1632,1232],
|
||||
[640,480,"longest side",1.0,1000,1.0,1024,1024,16,true,1008,752,0,0,1008,752],
|
||||
[640,480,"longest side",1.0,2048,1.0,1024,1024,16,true,2048,1536,0,0,2048,1536],
|
||||
[640,480,"scale",1.0,1024,0.33,1024,1024,16,true,208,160,0,0,208,160],
|
||||
[640,480,"scale",1.0,1024,1.5,1024,1024,16,true,960,720,0,0,960,720],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1024,1024,16,true,1024,768,0,0,1024,768],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1024,1024,16,true,1366,1024,171,0,1024,1024],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1280,720,16,true,960,720,0,0,960,720],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1280,720,16,true,1280,960,0,120,1280,720],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1000,600,16,true,800,592,0,0,800,592],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1000,600,16,true,1008,756,0,74,1008,608],
|
||||
[640,480,"megapixels",0.25,1024,1.0,1024,1024,16,false,576,432,0,0,576,432],
|
||||
[640,480,"megapixels",2.0,1024,1.0,1024,1024,16,false,640,480,0,0,640,480],
|
||||
[640,480,"longest side",1.0,1000,1.0,1024,1024,16,false,640,480,0,0,640,480],
|
||||
[640,480,"longest side",1.0,2048,1.0,1024,1024,16,false,640,480,0,0,640,480],
|
||||
[640,480,"scale",1.0,1024,0.33,1024,1024,16,false,208,160,0,0,208,160],
|
||||
[640,480,"scale",1.0,1024,1.5,1024,1024,16,false,640,480,0,0,640,480],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1024,1024,16,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1024,1024,16,false,640,480,80,0,480,480],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1280,720,16,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1280,720,16,false,640,480,0,64,640,352],
|
||||
[640,480,"fit inside",1.0,1024,1.0,1000,600,16,false,640,480,0,0,640,480],
|
||||
[640,480,"crop to fill",1.0,1024,1.0,1000,600,16,false,640,480,0,48,640,384],
|
||||
[333,777,"off",1.0,1024,1.0,1024,1024,1,true,333,777,0,0,333,777],
|
||||
[333,777,"off",1.0,1024,1.0,1024,1024,16,true,333,777,0,0,333,777],
|
||||
[333,777,"megapixels",0.25,1024,1.0,1024,1024,1,true,327,764,0,0,327,764],
|
||||
[333,777,"megapixels",2.0,1024,1.0,1024,1024,1,true,926,2160,0,0,926,2160],
|
||||
[333,777,"longest side",1.0,1000,1.0,1024,1024,1,true,429,1000,0,0,429,1000],
|
||||
[333,777,"longest side",1.0,2048,1.0,1024,1024,1,true,878,2048,0,0,878,2048],
|
||||
[333,777,"scale",1.0,1024,0.33,1024,1024,1,true,110,256,0,0,110,256],
|
||||
[333,777,"scale",1.0,1024,1.5,1024,1024,1,true,500,1166,0,0,500,1166],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1024,1024,1,true,439,1024,0,0,439,1024],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1024,1024,1,true,1024,2390,0,683,1024,1024],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1280,720,1,true,309,720,0,0,309,720],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1280,720,1,true,1280,2987,0,1133,1280,720],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1000,600,1,true,257,600,0,0,257,600],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1000,600,1,true,1000,2334,0,867,1000,600],
|
||||
[333,777,"megapixels",0.25,1024,1.0,1024,1024,1,false,327,764,0,0,327,764],
|
||||
[333,777,"megapixels",2.0,1024,1.0,1024,1024,1,false,333,777,0,0,333,777],
|
||||
[333,777,"longest side",1.0,1000,1.0,1024,1024,1,false,333,777,0,0,333,777],
|
||||
[333,777,"longest side",1.0,2048,1.0,1024,1024,1,false,333,777,0,0,333,777],
|
||||
[333,777,"scale",1.0,1024,0.33,1024,1024,1,false,110,256,0,0,110,256],
|
||||
[333,777,"scale",1.0,1024,1.5,1024,1024,1,false,333,777,0,0,333,777],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1024,1024,1,false,333,777,0,0,333,777],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1024,1024,1,false,333,777,0,222,333,333],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1280,720,1,false,309,720,0,0,309,720],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1280,720,1,false,333,777,0,295,333,187],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1000,600,1,false,257,600,0,0,257,600],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1000,600,1,false,333,777,0,289,333,199],
|
||||
[333,777,"megapixels",0.25,1024,1.0,1024,1024,16,true,320,768,0,0,320,768],
|
||||
[333,777,"megapixels",2.0,1024,1.0,1024,1024,16,true,928,2160,0,0,928,2160],
|
||||
[333,777,"longest side",1.0,1000,1.0,1024,1024,16,true,432,1008,0,0,432,1008],
|
||||
[333,777,"longest side",1.0,2048,1.0,1024,1024,16,true,880,2048,0,0,880,2048],
|
||||
[333,777,"scale",1.0,1024,0.33,1024,1024,16,true,112,256,0,0,112,256],
|
||||
[333,777,"scale",1.0,1024,1.5,1024,1024,16,true,496,1168,0,0,496,1168],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1024,1024,16,true,432,1024,0,0,432,1024],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1024,1024,16,true,1024,2390,0,683,1024,1024],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1280,720,16,true,304,720,0,0,304,720],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1280,720,16,true,1280,2987,0,1133,1280,720],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1000,600,16,true,256,592,0,0,256,592],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1000,600,16,true,1008,2352,0,872,1008,608],
|
||||
[333,777,"megapixels",0.25,1024,1.0,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"megapixels",2.0,1024,1.0,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"longest side",1.0,1000,1.0,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"longest side",1.0,2048,1.0,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"scale",1.0,1024,0.33,1024,1024,16,false,112,256,0,0,112,256],
|
||||
[333,777,"scale",1.0,1024,1.5,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1024,1024,16,false,320,768,0,0,320,768],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1024,1024,16,false,320,747,0,213,320,320],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1280,720,16,false,304,720,0,0,304,720],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1280,720,16,false,320,747,0,285,320,176],
|
||||
[333,777,"fit inside",1.0,1024,1.0,1000,600,16,false,256,592,0,0,256,592],
|
||||
[333,777,"crop to fill",1.0,1024,1.0,1000,600,16,false,320,747,0,277,320,192],
|
||||
[8192,5464,"off",1.0,1024,1.0,1024,1024,1,true,8192,5464,0,0,8192,5464],
|
||||
[8192,5464,"off",1.0,1024,1.0,1024,1024,16,true,8192,5464,0,0,8192,5464],
|
||||
[8192,5464,"megapixels",0.25,1024,1.0,1024,1024,1,true,612,408,0,0,612,408],
|
||||
[8192,5464,"megapixels",2.0,1024,1.0,1024,1024,1,true,1732,1155,0,0,1732,1155],
|
||||
[8192,5464,"longest side",1.0,1000,1.0,1024,1024,1,true,1000,667,0,0,1000,667],
|
||||
[8192,5464,"longest side",1.0,2048,1.0,1024,1024,1,true,2048,1366,0,0,2048,1366],
|
||||
[8192,5464,"scale",1.0,1024,0.33,1024,1024,1,true,2703,1803,0,0,2703,1803],
|
||||
[8192,5464,"scale",1.0,1024,1.5,1024,1024,1,true,12288,8196,0,0,12288,8196],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1024,1024,1,true,1024,683,0,0,1024,683],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1024,1024,1,true,1536,1024,256,0,1024,1024],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1280,720,1,true,1079,720,0,0,1079,720],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1280,720,1,true,1280,854,0,67,1280,720],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1000,600,1,true,900,600,0,0,900,600],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1000,600,1,true,1000,667,0,33,1000,600],
|
||||
[8192,5464,"megapixels",0.25,1024,1.0,1024,1024,1,false,612,408,0,0,612,408],
|
||||
[8192,5464,"megapixels",2.0,1024,1.0,1024,1024,1,false,1732,1155,0,0,1732,1155],
|
||||
[8192,5464,"longest side",1.0,1000,1.0,1024,1024,1,false,1000,667,0,0,1000,667],
|
||||
[8192,5464,"longest side",1.0,2048,1.0,1024,1024,1,false,2048,1366,0,0,2048,1366],
|
||||
[8192,5464,"scale",1.0,1024,0.33,1024,1024,1,false,2703,1803,0,0,2703,1803],
|
||||
[8192,5464,"scale",1.0,1024,1.5,1024,1024,1,false,8192,5464,0,0,8192,5464],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1024,1024,1,false,1024,683,0,0,1024,683],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1024,1024,1,false,1536,1024,256,0,1024,1024],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1280,720,1,false,1079,720,0,0,1079,720],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1280,720,1,false,1280,854,0,67,1280,720],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1000,600,1,false,900,600,0,0,900,600],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1000,600,1,false,1000,667,0,33,1000,600],
|
||||
[8192,5464,"megapixels",0.25,1024,1.0,1024,1024,16,true,608,416,0,0,608,416],
|
||||
[8192,5464,"megapixels",2.0,1024,1.0,1024,1024,16,true,1728,1152,0,0,1728,1152],
|
||||
[8192,5464,"longest side",1.0,1000,1.0,1024,1024,16,true,1008,672,0,0,1008,672],
|
||||
[8192,5464,"longest side",1.0,2048,1.0,1024,1024,16,true,2048,1360,0,0,2048,1360],
|
||||
[8192,5464,"scale",1.0,1024,0.33,1024,1024,16,true,2704,1808,0,0,2704,1808],
|
||||
[8192,5464,"scale",1.0,1024,1.5,1024,1024,16,true,12288,8192,0,0,12288,8192],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1024,1024,16,true,1024,688,0,0,1024,688],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1024,1024,16,true,1536,1024,256,0,1024,1024],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1280,720,16,true,1072,720,0,0,1072,720],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1280,720,16,true,1280,854,0,67,1280,720],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1000,600,16,true,896,592,0,0,896,592],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1000,600,16,true,1008,673,0,32,1008,608],
|
||||
[8192,5464,"megapixels",0.25,1024,1.0,1024,1024,16,false,608,416,0,0,608,416],
|
||||
[8192,5464,"megapixels",2.0,1024,1.0,1024,1024,16,false,1728,1152,0,0,1728,1152],
|
||||
[8192,5464,"longest side",1.0,1000,1.0,1024,1024,16,false,1008,672,0,0,1008,672],
|
||||
[8192,5464,"longest side",1.0,2048,1.0,1024,1024,16,false,2048,1360,0,0,2048,1360],
|
||||
[8192,5464,"scale",1.0,1024,0.33,1024,1024,16,false,2704,1808,0,0,2704,1808],
|
||||
[8192,5464,"scale",1.0,1024,1.5,1024,1024,16,false,8192,5456,0,0,8192,5456],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1024,1024,16,false,1024,688,0,0,1024,688],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1024,1024,16,false,1536,1024,256,0,1024,1024],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1280,720,16,false,1072,720,0,0,1072,720],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1280,720,16,false,1280,854,0,67,1280,720],
|
||||
[8192,5464,"fit inside",1.0,1024,1.0,1000,600,16,false,896,592,0,0,896,592],
|
||||
[8192,5464,"crop to fill",1.0,1024,1.0,1000,600,16,false,992,662,0,35,992,592],
|
||||
[1000,1000,"longest side",1.0,1000,1.0,1024,1024,64,true,1024,1024,0,0,1024,1024],
|
||||
[30,20,"megapixels",1.0,1024,1.0,1024,1024,64,false,64,64,0,0,64,64]
|
||||
]}
|
||||
Vendored
+45
@@ -522,6 +522,51 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"AUSBOSS_NODES_LoadImage": {
|
||||
"required": [
|
||||
"image",
|
||||
"size",
|
||||
"megapixels",
|
||||
"longest_side",
|
||||
"scale",
|
||||
"width",
|
||||
"height",
|
||||
"multiple",
|
||||
"method",
|
||||
"enlarge"
|
||||
],
|
||||
"optional": [],
|
||||
"outputs": [
|
||||
[
|
||||
"IMAGE",
|
||||
"image"
|
||||
],
|
||||
[
|
||||
"MASK",
|
||||
"mask"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"width"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"height"
|
||||
],
|
||||
[
|
||||
"STRING",
|
||||
"filename"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"original_width"
|
||||
],
|
||||
[
|
||||
"INT",
|
||||
"original_height"
|
||||
]
|
||||
]
|
||||
},
|
||||
"AUSBOSS_NODES_LoadImagePad": {
|
||||
"required": [
|
||||
"image",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { SIZE_MODES, placeText, planLoadSize, sizeText, stepFile } from "../js/shared/load_size.mjs";
|
||||
|
||||
const table = JSON.parse(readFileSync(new URL("./fixtures/load_size_cases.json", import.meta.url), "utf8"));
|
||||
|
||||
test("the size the node shows is the size the run gives, case by case", () => {
|
||||
assert.ok(table.rows.length > 200);
|
||||
const seen = new Set();
|
||||
for (const row of table.rows) {
|
||||
const [sourceWidth, sourceHeight, size, megapixels, longest_side, scale, width, height, multiple, enlarge, ...wanted] = row;
|
||||
seen.add(size);
|
||||
const plan = planLoadSize(sourceWidth, sourceHeight, { size, megapixels, longest_side, scale, width, height, multiple, enlarge });
|
||||
assert.deepEqual(
|
||||
[plan.resizeWidth, plan.resizeHeight, plan.cropX, plan.cropY, plan.width, plan.height],
|
||||
wanted,
|
||||
JSON.stringify(row.slice(0, 10)),
|
||||
);
|
||||
}
|
||||
assert.deepEqual([...seen].sort(), [...SIZE_MODES].sort(), "every size choice is in the table");
|
||||
});
|
||||
|
||||
test("off gives the picture back whatever the other settings say", () => {
|
||||
const plan = planLoadSize(913, 1631, { size: "off", multiple: 64, megapixels: 4, enlarge: false });
|
||||
assert.deepEqual([plan.width, plan.height, plan.cropX, plan.cropY], [913, 1631, 0, 0]);
|
||||
});
|
||||
|
||||
test("longest side lands on the number asked for, and round to moves it to a multiple", () => {
|
||||
assert.deepEqual(planLoadSize(912, 1632, { size: "longest side", longest_side: 2048, multiple: 1 }).height, 2048);
|
||||
const rounded = planLoadSize(912, 1632, { size: "longest side", longest_side: 2000, multiple: 16 });
|
||||
assert.equal(rounded.height, 2000);
|
||||
assert.equal(rounded.width % 16, 0);
|
||||
});
|
||||
|
||||
test("with enlarge off a small picture keeps its size, and crop to fill only cuts", () => {
|
||||
const kept = planLoadSize(640, 480, { size: "megapixels", megapixels: 4, multiple: 1, enlarge: false });
|
||||
assert.deepEqual([kept.width, kept.height], [640, 480]);
|
||||
const cut = planLoadSize(800, 600, { size: "crop to fill", width: 1024, height: 1024, multiple: 1, enlarge: false });
|
||||
assert.deepEqual([cut.width, cut.height, cut.resizeWidth, cut.resizeHeight, cut.cropX, cut.cropY], [600, 600, 800, 600, 100, 0]);
|
||||
});
|
||||
|
||||
test("fit inside never leaves its box, rounding included", () => {
|
||||
const plan = planLoadSize(1920, 1080, { size: "fit inside", width: 1000, height: 1000, multiple: 64 });
|
||||
assert.ok(plan.width <= 1000 && plan.height <= 1000);
|
||||
assert.equal(plan.width % 64, 0);
|
||||
});
|
||||
|
||||
test("a picture with no size yet, or a choice the node does not know, has no plan", () => {
|
||||
assert.equal(planLoadSize(0, 0, { size: "scale" }), null);
|
||||
assert.equal(planLoadSize(100, 100, { size: "" }), null);
|
||||
assert.equal(planLoadSize(100, 100, { size: "stretch" }), null);
|
||||
});
|
||||
|
||||
test("the size line shows one size when nothing changes and both when it does", () => {
|
||||
assert.equal(sizeText(912, 1632, planLoadSize(912, 1632, { size: "off" })), "912 × 1632");
|
||||
assert.equal(
|
||||
sizeText(912, 1632, planLoadSize(912, 1632, { size: "longest side", longest_side: 2048, multiple: 8 })),
|
||||
"912 × 1632 → 1144 × 2048",
|
||||
);
|
||||
assert.equal(sizeText(0, 0, null), "");
|
||||
assert.equal(sizeText(912, 1632, null), "912 × 1632");
|
||||
});
|
||||
|
||||
test("the arrows go round the list, and a file that is not listed steps onto it", () => {
|
||||
const files = ["a.png", "b.png", "sub/c.png"];
|
||||
assert.equal(stepFile(files, "a.png", 1), "b.png");
|
||||
assert.equal(stepFile(files, "sub/c.png", 1), "a.png");
|
||||
assert.equal(stepFile(files, "a.png", -1), "sub/c.png");
|
||||
assert.equal(stepFile(files, "gone.png", 1), "a.png");
|
||||
assert.equal(stepFile(files, "gone.png", -1), "sub/c.png");
|
||||
assert.equal(stepFile(["only.png"], "only.png", 1), null);
|
||||
assert.equal(stepFile([""], "", 1), null, "an empty input folder lists one blank");
|
||||
assert.equal(stepFile(undefined, "a.png", 1), null);
|
||||
});
|
||||
|
||||
test("the place in the list counts from one", () => {
|
||||
const files = ["a.png", "b.png", "sub/c.png"];
|
||||
assert.equal(placeText(files, "b.png"), "2 / 3");
|
||||
assert.equal(placeText(files, "gone.png"), "– / 3");
|
||||
assert.equal(placeText([""], ""), "");
|
||||
});
|
||||
|
||||
test("Load Image is one of the loaders checked for a picked file before a run", async () => {
|
||||
const { SOURCE_LOADERS } = await import("../js/shared/run_check.mjs");
|
||||
assert.equal(typeof SOURCE_LOADERS.AUSBOSS_NODES_LoadImage, "function");
|
||||
assert.ok(SOURCE_LOADERS.AUSBOSS_NODES_LoadImage({ image: "" }));
|
||||
assert.equal(SOURCE_LOADERS.AUSBOSS_NODES_LoadImage({ image: "pic.png" }), null);
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
"""Load Image: the size plan as numbers, and the node on real files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"):
|
||||
del sys.modules["nodes"]
|
||||
|
||||
from nodes import _media_helpers # noqa: E402
|
||||
from nodes._load_image_helpers import ( # noqa: E402
|
||||
SIZE_MODES,
|
||||
apply_load_size,
|
||||
plan_load_size,
|
||||
)
|
||||
from nodes.node_load_image import ( # noqa: E402
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS,
|
||||
AusBossLoadImage,
|
||||
)
|
||||
|
||||
TABLE = ROOT / "tests" / "fixtures" / "load_size_cases.json"
|
||||
|
||||
|
||||
class PlanTests(unittest.TestCase):
|
||||
def test_the_table_the_browser_is_held_to_is_what_the_planner_gives(self):
|
||||
table = json.loads(TABLE.read_text(encoding="utf-8"))
|
||||
self.assertGreater(len(table["rows"]), 200)
|
||||
for row in table["rows"]:
|
||||
source_w, source_h, size, megapixels, longest_side, scale, width, height, multiple, enlarge = row[:10]
|
||||
plan = plan_load_size(source_w, source_h, size, megapixels, longest_side, scale, width, height, multiple, enlarge)
|
||||
got = [plan["resize_width"], plan["resize_height"], plan["crop_x"], plan["crop_y"], plan["width"], plan["height"]]
|
||||
self.assertEqual(got, row[10:], row[:10])
|
||||
|
||||
def test_off_gives_the_picture_back_whatever_else_is_set(self):
|
||||
plan = plan_load_size(913, 1631, "off", megapixels=4.0, multiple=64, enlarge=False)
|
||||
self.assertEqual((plan["width"], plan["height"], plan["crop_x"], plan["crop_y"]), (913, 1631, 0, 0))
|
||||
|
||||
def test_each_choice_keeps_its_promise(self):
|
||||
plan = plan_load_size(912, 1632, "megapixels", megapixels=2.0, multiple=1)
|
||||
self.assertAlmostEqual(plan["width"] * plan["height"] / 1e6, 2.0, delta=0.01)
|
||||
self.assertAlmostEqual(plan["width"] / plan["height"], 912 / 1632, delta=0.002)
|
||||
plan = plan_load_size(912, 1632, "longest side", longest_side=2048, multiple=1)
|
||||
self.assertEqual((plan["width"], plan["height"]), (1144, 2048))
|
||||
plan = plan_load_size(1920, 1080, "longest side", longest_side=1280, multiple=1)
|
||||
self.assertEqual((plan["width"], plan["height"]), (1280, 720))
|
||||
plan = plan_load_size(1920, 1080, "scale", scale=0.5, multiple=1)
|
||||
self.assertEqual((plan["width"], plan["height"]), (960, 540))
|
||||
plan = plan_load_size(1920, 1080, "fit inside", width=1024, height=1024, multiple=1)
|
||||
self.assertEqual((plan["width"], plan["height"]), (1024, 576))
|
||||
plan = plan_load_size(1920, 1080, "crop to fill", width=1024, height=1024, multiple=1)
|
||||
self.assertEqual((plan["width"], plan["height"]), (1024, 1024))
|
||||
self.assertEqual((plan["resize_width"], plan["resize_height"]), (1821, 1024))
|
||||
self.assertEqual((plan["crop_x"], plan["crop_y"]), (398, 0))
|
||||
|
||||
def test_round_to_gives_multiples_and_fit_inside_stays_in_its_box(self):
|
||||
for size in SIZE_MODES[1:]:
|
||||
plan = plan_load_size(1000, 777, size, megapixels=1.3, longest_side=1111, scale=1.37, width=900, height=700, multiple=64)
|
||||
self.assertEqual((plan["width"] % 64, plan["height"] % 64), (0, 0), size)
|
||||
plan = plan_load_size(1920, 1080, "fit inside", width=1000, height=1000, multiple=64)
|
||||
self.assertLessEqual(plan["width"], 1000)
|
||||
self.assertLessEqual(plan["height"], 1000)
|
||||
|
||||
def test_with_enlarge_off_nothing_comes_out_bigger_than_the_file(self):
|
||||
for size in SIZE_MODES[1:]:
|
||||
for multiple in (1, 8, 16):
|
||||
plan = plan_load_size(640, 480, size, megapixels=4.0, longest_side=4096, scale=3.0, width=2048, height=2048, multiple=multiple, enlarge=False)
|
||||
self.assertLessEqual(plan["width"], 640, (size, multiple))
|
||||
self.assertLessEqual(plan["height"], 480, (size, multiple))
|
||||
self.assertLessEqual(plan["resize_width"], 640, (size, multiple))
|
||||
# Crop to fill still gives the box's shape, cut from the file as it is.
|
||||
plan = plan_load_size(800, 600, "crop to fill", width=1024, height=1024, multiple=1, enlarge=False)
|
||||
self.assertEqual((plan["width"], plan["height"], plan["resize_width"], plan["resize_height"], plan["crop_x"]), (600, 600, 800, 600, 100))
|
||||
# A picture that is big enough is resized as usual.
|
||||
plan = plan_load_size(4000, 3000, "longest side", longest_side=1024, multiple=1, enlarge=False)
|
||||
self.assertEqual((plan["width"], plan["height"]), (1024, 768))
|
||||
|
||||
def test_bad_input_is_named(self):
|
||||
with self.assertRaisesRegex(ValueError, "size must be one of"):
|
||||
plan_load_size(100, 100, "stretch")
|
||||
with self.assertRaisesRegex(ValueError, "larger than 0x0"):
|
||||
plan_load_size(0, 100, "off")
|
||||
|
||||
def test_a_plan_that_changes_nothing_hands_the_same_tensors_back(self):
|
||||
image, mask = torch.rand(1, 20, 30, 3), torch.zeros(1, 20, 30)
|
||||
out_image, out_mask = apply_load_size(image, mask, plan_load_size(30, 20, "off"), "lanczos")
|
||||
self.assertIs(out_image, image)
|
||||
self.assertIs(out_mask, mask)
|
||||
|
||||
|
||||
class NodeTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# The temp folder stands in for ComfyUI's input folder.
|
||||
self.folder = tempfile.mkdtemp(prefix="ausboss_load_image_")
|
||||
self.addCleanup(shutil.rmtree, self.folder, True)
|
||||
stand_in = SimpleNamespace(
|
||||
get_input_directory=lambda: self.folder,
|
||||
get_annotated_filepath=lambda name: os.path.join(self.folder, name),
|
||||
)
|
||||
patcher = patch.object(_media_helpers, "folder_paths", stand_in)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def write(self, name="pic.png", width=96, height=64, mode="RGB"):
|
||||
path = Path(self.folder) / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
values = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
values[..., 0] = np.linspace(0, 255, width, dtype=np.uint8)[None, :]
|
||||
values[..., 1] = np.linspace(0, 255, height, dtype=np.uint8)[:, None]
|
||||
values[..., 2] = 90
|
||||
values[..., 3] = 255
|
||||
values[: height // 2, : width // 2, 3] = 0 # the top left quarter is see-through
|
||||
Image.fromarray(values).convert(mode).save(path)
|
||||
return name
|
||||
|
||||
def run_node(self, name, **values):
|
||||
return AusBossLoadImage().load(name, **values)
|
||||
|
||||
def test_the_node_is_registered_under_its_permanent_key(self):
|
||||
self.assertIs(NODE_CLASS_MAPPINGS["AUSBOSS_NODES_LoadImage"], AusBossLoadImage)
|
||||
self.assertEqual(NODE_DISPLAY_NAME_MAPPINGS["AUSBOSS_NODES_LoadImage"], "Load Image \U0001f18e")
|
||||
spec = AusBossLoadImage.INPUT_TYPES()
|
||||
self.assertEqual(
|
||||
list(spec["required"]),
|
||||
["image", "size", "megapixels", "longest_side", "scale", "width", "height", "multiple", "method", "enlarge"],
|
||||
)
|
||||
self.assertEqual(spec["required"]["size"][0], list(SIZE_MODES))
|
||||
self.assertEqual(spec["required"]["size"][1]["default"], "off")
|
||||
self.assertEqual(len(AusBossLoadImage.RETURN_TYPES), len(AusBossLoadImage.RETURN_NAMES))
|
||||
self.assertEqual(len(AusBossLoadImage.RETURN_TYPES), len(AusBossLoadImage.OUTPUT_TOOLTIPS))
|
||||
for name, (_, options) in spec["required"].items():
|
||||
self.assertTrue(options.get("tooltip"), name)
|
||||
|
||||
def test_with_size_off_the_file_comes_out_as_it_is(self):
|
||||
name = self.write()
|
||||
image, mask, width, height, filename, original_w, original_h = self.run_node(name)
|
||||
self.assertEqual(tuple(image.shape), (1, 64, 96, 3))
|
||||
self.assertEqual(tuple(mask.shape), (1, 64, 96))
|
||||
self.assertEqual((width, height, original_w, original_h), (96, 64, 96, 64))
|
||||
self.assertEqual(filename, "pic")
|
||||
wanted = np.asarray(Image.open(Path(self.folder) / name).convert("RGB"), dtype=np.float32) / 255.0
|
||||
self.assertTrue(np.array_equal(image[0].numpy(), wanted), "every pixel as the file has it")
|
||||
self.assertEqual(float(mask.max()), 0.0, "no transparency: an all black mask")
|
||||
|
||||
def test_a_see_through_picture_gives_its_mask_the_way_the_stock_loader_does(self):
|
||||
name = self.write("cut.png", mode="RGBA")
|
||||
_, mask, *_ = self.run_node(name)
|
||||
self.assertEqual(float(mask[0, 5, 5]), 1.0, "see-through is white")
|
||||
self.assertEqual(float(mask[0, 60, 90]), 0.0, "solid is black")
|
||||
|
||||
def test_every_size_choice_gives_the_planned_size_for_picture_and_mask(self):
|
||||
name = self.write("cut.png", mode="RGBA")
|
||||
for size in SIZE_MODES[1:]:
|
||||
with self.subTest(size=size):
|
||||
values = {"megapixels": 0.02, "longest_side": 160, "scale": 1.5, "width": 80, "height": 80, "multiple": 8}
|
||||
image, mask, width, height, _, original_w, original_h = self.run_node(name, size=size, **values)
|
||||
plan = plan_load_size(96, 64, size, **values)
|
||||
self.assertEqual((width, height), (plan["width"], plan["height"]))
|
||||
self.assertEqual(tuple(image.shape), (1, height, width, 3))
|
||||
self.assertEqual(tuple(mask.shape), (1, height, width))
|
||||
self.assertEqual((original_w, original_h), (96, 64))
|
||||
self.assertGreaterEqual(float(image.min()), 0.0)
|
||||
self.assertLessEqual(float(image.max()), 1.0)
|
||||
self.assertGreaterEqual(float(mask.min()), 0.0)
|
||||
self.assertLessEqual(float(mask.max()), 1.0)
|
||||
|
||||
def test_crop_to_fill_cuts_the_middle(self):
|
||||
name = self.write("wide.png", width=200, height=100)
|
||||
image, _, width, height, *_ = self.run_node(name, size="crop to fill", width=100, height=100, multiple=1, method="nearest")
|
||||
self.assertEqual((width, height), (100, 100))
|
||||
whole, *_ = self.run_node(name)
|
||||
self.assertTrue(torch.equal(image, whole[:, :, 50:150, :]), "the middle of the file, untouched")
|
||||
|
||||
def test_enlarge_off_leaves_a_small_file_alone(self):
|
||||
name = self.write()
|
||||
image, _, width, height, *_ = self.run_node(name, size="longest side", longest_side=1024, multiple=1, enlarge=False)
|
||||
self.assertEqual((width, height), (96, 64))
|
||||
whole, *_ = self.run_node(name)
|
||||
self.assertTrue(torch.equal(image, whole))
|
||||
|
||||
def test_a_file_in_a_subfolder_gives_its_bare_name(self):
|
||||
name = self.write("shots/day one/hero.final.png")
|
||||
*_, filename, _, _ = self.run_node(name)
|
||||
self.assertEqual(filename, "hero.final")
|
||||
|
||||
def test_every_frame_of_an_animation_comes_through(self):
|
||||
frames = [Image.new("RGB", (40, 30), (index * 60, 20, 20)) for index in range(3)]
|
||||
path = Path(self.folder) / "turn.gif"
|
||||
frames[0].save(path, save_all=True, append_images=frames[1:], duration=80, loop=0)
|
||||
image, mask, width, height, *_ = self.run_node("turn.gif", size="scale", scale=2.0, multiple=1)
|
||||
self.assertEqual(tuple(image.shape), (3, 60, 80, 3))
|
||||
self.assertEqual(tuple(mask.shape), (3, 60, 80))
|
||||
self.assertEqual((width, height), (80, 60))
|
||||
|
||||
def test_a_missing_or_outside_file_stops_before_the_run_with_the_nodes_name(self):
|
||||
self.assertIs(AusBossLoadImage.VALIDATE_INPUTS(image=self.write()), True)
|
||||
for name in ("", "gone.png", "../secret.png", "//fileserver/share/pic.png"):
|
||||
verdict = AusBossLoadImage.VALIDATE_INPUTS(image=name)
|
||||
self.assertIsInstance(verdict, str, name)
|
||||
self.assertTrue(verdict.startswith("Load Image: "), verdict)
|
||||
|
||||
def test_a_changed_file_or_setting_runs_the_node_again(self):
|
||||
name = self.write()
|
||||
first = AusBossLoadImage.IS_CHANGED(name, size="off")
|
||||
self.assertEqual(first, AusBossLoadImage.IS_CHANGED(name, size="off"))
|
||||
self.assertNotEqual(first, AusBossLoadImage.IS_CHANGED(name, size="scale"))
|
||||
path = Path(self.folder) / name
|
||||
Image.new("RGB", (10, 10), (1, 2, 3)).save(path)
|
||||
os.utime(path, ns=(1, 1))
|
||||
self.assertNotEqual(first, AusBossLoadImage.IS_CHANGED(name, size="off"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -436,10 +436,11 @@ class LoaderValidationTests(TempFolders):
|
||||
"""The loaders refuse the same paths before a run, with one message each."""
|
||||
|
||||
def test_image_loaders_take_what_core_load_image_takes_and_nothing_else(self):
|
||||
from nodes import node_image_crop_rotate_pad, node_load_image_pad
|
||||
from nodes import node_image_crop_rotate_pad, node_load_image, node_load_image_pad
|
||||
|
||||
for module, cls, label in (
|
||||
(node_image_crop_rotate_pad, node_image_crop_rotate_pad.AusBossImageCropRotatePad, "Image Crop + Rotate + Pad"),
|
||||
(node_load_image, node_load_image.AusBossLoadImage, "Load Image"),
|
||||
(node_load_image_pad, node_load_image_pad.AusBossLoadImagePad, "Load Image + Pad"),
|
||||
):
|
||||
with self.subTest(node=label):
|
||||
|
||||
@@ -44,6 +44,7 @@ NAMED = {
|
||||
"AusBossImageFolder": ["source", "folder"],
|
||||
"AusBossLaMaInpaint": ["model"],
|
||||
"AusBossLoadAudio": ["audio"],
|
||||
"AusBossLoadImage": ["image"],
|
||||
"AusBossLoadImagePad": ["image", "source_image"],
|
||||
"AusBossLoadVideo": ["video"],
|
||||
"AusBossLoraLoader": ["loras", "on_missing"],
|
||||
|
||||
@@ -943,7 +943,7 @@ test("a name is shown only when SAM 3 is sure of it, and surer still for a big t
|
||||
});
|
||||
|
||||
test("the pack's own picture loaders are the ones the node runs by itself", () => {
|
||||
assert.deepEqual([...OWN_LOADERS].sort(), ["AUSBOSS_NODES_ImageCropRotatePad", "AUSBOSS_NODES_LoadImagePad"]);
|
||||
assert.deepEqual([...OWN_LOADERS].sort(), ["AUSBOSS_NODES_ImageCropRotatePad", "AUSBOSS_NODES_LoadImage", "AUSBOSS_NODES_LoadImagePad"]);
|
||||
// A core Load Image is not in the list: its file is the picture, and the node shows it without a run.
|
||||
assert.equal(OWN_LOADERS.has("LoadImage"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user