docs: add common tools and components
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
import ParamsCard from './tool/ParamsCard.svelte';
|
import ParamsCard from './tool/ParamsCard.svelte';
|
||||||
import ResultCard from './tool/ResultCard.svelte';
|
import ResultCard from './tool/ResultCard.svelte';
|
||||||
import SourceCard from './tool/SourceCard.svelte';
|
import SourceCard from './tool/SourceCard.svelte';
|
||||||
|
import TextInputCard from './tool/TextInputCard.svelte';
|
||||||
|
|
||||||
let { tool }: { tool: ToolEntry } = $props();
|
let { tool }: { tool: ToolEntry } = $props();
|
||||||
|
|
||||||
@@ -15,15 +16,19 @@
|
|||||||
let source = $state<PixelImage | null>(null);
|
let source = $state<PixelImage | null>(null);
|
||||||
let result = $state<PixelImage | null>(null);
|
let result = $state<PixelImage | null>(null);
|
||||||
let previewResult = $state<PixelImage | null>(null);
|
let previewResult = $state<PixelImage | null>(null);
|
||||||
|
let textResult = $state<string | null>(null);
|
||||||
let showMask = $state(false);
|
let showMask = $state(false);
|
||||||
let info = $state<ImageInfo | null>(null);
|
let info = $state<ImageInfo | null>(null);
|
||||||
let errorText = $state('');
|
let errorText = $state('');
|
||||||
let values = $state<Record<string, any>>({});
|
let values = $state<Record<string, any>>({});
|
||||||
|
|
||||||
const isInfo = $derived(tool.resultType === 'info');
|
const isInfo = $derived(tool.resultType === 'info');
|
||||||
|
const isSourceless = $derived(tool.sourceMode === 'none');
|
||||||
|
const isTextSource = $derived(tool.sourceMode === 'text');
|
||||||
const sanitized = $derived(sanitizeParams(tool, values));
|
const sanitized = $derived(sanitizeParams(tool, values));
|
||||||
|
|
||||||
let runToken = 0;
|
let runToken = 0;
|
||||||
|
let hasLastRun = false;
|
||||||
let lastRunSource: PixelImage | null = null;
|
let lastRunSource: PixelImage | null = null;
|
||||||
let lastRunValuesJson = '';
|
let lastRunValuesJson = '';
|
||||||
let pipetteTargetId = $state<string | null>(null);
|
let pipetteTargetId = $state<string | null>(null);
|
||||||
@@ -59,24 +64,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTextSubmit(text: string) {
|
||||||
|
if (!isTextSource || !tool.runFromText) return;
|
||||||
|
errorText = '';
|
||||||
|
status = 'processing';
|
||||||
|
try {
|
||||||
|
source = await tool.runFromText(text, defaultParams(tool));
|
||||||
|
values = defaultParams(tool);
|
||||||
|
result = null;
|
||||||
|
textResult = null;
|
||||||
|
previewResult = null;
|
||||||
|
showMask = false;
|
||||||
|
pipetteTargetId = null;
|
||||||
|
info = null;
|
||||||
|
await runTool();
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runTool() {
|
async function runTool() {
|
||||||
if (!source || isInfo) return;
|
if (isInfo) return;
|
||||||
|
if (!source && !isSourceless) return;
|
||||||
const token = ++runToken;
|
const token = ++runToken;
|
||||||
|
hasLastRun = true;
|
||||||
lastRunSource = source;
|
lastRunSource = source;
|
||||||
lastRunValuesJson = JSON.stringify(sanitized);
|
lastRunValuesJson = JSON.stringify(sanitized);
|
||||||
try {
|
try {
|
||||||
const next = await tool.run(source, sanitized);
|
let next: PixelImage;
|
||||||
|
if (isSourceless) {
|
||||||
|
next = await tool.generate!(sanitized);
|
||||||
|
} else {
|
||||||
|
next = await tool.run(source!, sanitized);
|
||||||
|
}
|
||||||
let nextPreview: PixelImage | null = null;
|
let nextPreview: PixelImage | null = null;
|
||||||
if (tool.preview) {
|
if (tool.preview && source) {
|
||||||
try {
|
try {
|
||||||
nextPreview = await tool.preview(source, sanitized);
|
nextPreview = await tool.preview(source, sanitized);
|
||||||
} catch {
|
} catch {
|
||||||
nextPreview = null;
|
nextPreview = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let nextText: string | null = null;
|
||||||
|
if (tool.toText && source) {
|
||||||
|
nextText = await tool.toText(source, sanitized);
|
||||||
|
}
|
||||||
if (token !== runToken) return;
|
if (token !== runToken) return;
|
||||||
result = next;
|
result = next;
|
||||||
previewResult = nextPreview;
|
previewResult = nextPreview;
|
||||||
|
textResult = nextText;
|
||||||
status = 'loaded';
|
status = 'loaded';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (token !== runToken) return;
|
if (token !== runToken) return;
|
||||||
@@ -86,8 +122,9 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const valuesJson = JSON.stringify(sanitized);
|
const valuesJson = JSON.stringify(sanitized);
|
||||||
if (source === lastRunSource && valuesJson === lastRunValuesJson) return;
|
if (hasLastRun && source === lastRunSource && valuesJson === lastRunValuesJson) return;
|
||||||
if (!source || isInfo) return;
|
if (!source && !isSourceless) return;
|
||||||
|
if (isInfo) return;
|
||||||
const timer = setTimeout(() => void runTool(), 300);
|
const timer = setTimeout(() => void runTool(), 300);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
});
|
});
|
||||||
@@ -101,6 +138,7 @@
|
|||||||
source = null;
|
source = null;
|
||||||
result = null;
|
result = null;
|
||||||
previewResult = null;
|
previewResult = null;
|
||||||
|
textResult = null;
|
||||||
showMask = false;
|
showMask = false;
|
||||||
pipetteTargetId = null;
|
pipetteTargetId = null;
|
||||||
info = null;
|
info = null;
|
||||||
@@ -138,21 +176,27 @@
|
|||||||
<div class="error-banner" role="alert">{errorText}</div>
|
<div class="error-banner" role="alert">{errorText}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="stage panel">
|
<div class="stage panel" class:single={isSourceless}>
|
||||||
<div class="cell">
|
{#if !isSourceless}
|
||||||
<SourceCard
|
<div class="cell">
|
||||||
{source}
|
{#if isTextSource && !source}
|
||||||
onFile={handleFile}
|
<TextInputCard onSubmit={handleTextSubmit} />
|
||||||
onError={(message) => (errorText = message)}
|
{:else}
|
||||||
onReset={reset}
|
<SourceCard
|
||||||
pipetteActive={!!pipetteTargetId}
|
{source}
|
||||||
onPickColor={handlePickColor}
|
onFile={handleFile}
|
||||||
/>
|
onError={(message) => (errorText = message)}
|
||||||
</div>
|
onReset={reset}
|
||||||
|
pipetteActive={!!pipetteTargetId}
|
||||||
|
onPickColor={handlePickColor}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
<ResultCard
|
<ResultCard
|
||||||
{tool}
|
{tool}
|
||||||
sourceLoaded={!!source}
|
sourceLoaded={isSourceless ? true : !!source}
|
||||||
{status}
|
{status}
|
||||||
{result}
|
{result}
|
||||||
{previewResult}
|
{previewResult}
|
||||||
@@ -160,12 +204,13 @@
|
|||||||
{info}
|
{info}
|
||||||
{isInfo}
|
{isInfo}
|
||||||
params={sanitized}
|
params={sanitized}
|
||||||
|
{textResult}
|
||||||
onDownloadError={showError}
|
onDownloadError={showError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if source && !isInfo}
|
{#if (source || isSourceless) && !isInfo}
|
||||||
<ParamsCard
|
<ParamsCard
|
||||||
params={tool.params}
|
params={tool.params}
|
||||||
bind:values
|
bind:values
|
||||||
@@ -196,6 +241,10 @@
|
|||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stage.single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import EmptyState from '../ui/EmptyState.svelte';
|
import EmptyState from '../ui/EmptyState.svelte';
|
||||||
import InfoPanel from '../InfoPanel.svelte';
|
import InfoPanel from '../InfoPanel.svelte';
|
||||||
import Preview from '../Preview.svelte';
|
import Preview from '../Preview.svelte';
|
||||||
|
import TextResult from './TextResult.svelte';
|
||||||
import type { ImageInfo } from '$lib/core/analyze';
|
import type { ImageInfo } from '$lib/core/analyze';
|
||||||
import type { PixelImage } from '$lib/core/types';
|
import type { PixelImage } from '$lib/core/types';
|
||||||
import { outputOf, type ToolEntry } from '$lib/registry';
|
import { outputOf, type ToolEntry } from '$lib/registry';
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
info: ImageInfo | null;
|
info: ImageInfo | null;
|
||||||
isInfo: boolean;
|
isInfo: boolean;
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
|
textResult: string | null;
|
||||||
onDownloadError: (e: unknown) => void;
|
onDownloadError: (e: unknown) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@
|
|||||||
info,
|
info,
|
||||||
isInfo,
|
isInfo,
|
||||||
params,
|
params,
|
||||||
|
textResult,
|
||||||
onDownloadError
|
onDownloadError
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -62,6 +65,12 @@
|
|||||||
<InfoPanel {info} />
|
<InfoPanel {info} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if tool.resultType === 'text'}
|
||||||
|
<div class="media">
|
||||||
|
{#if textResult !== null}
|
||||||
|
<TextResult text={textResult} filename={tool.id} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if hasPreview}
|
{#if hasPreview}
|
||||||
<CheckboxField id="show-mask" label="Показать маску" bind:checked={showMask} />
|
<CheckboxField id="show-mask" label="Показать маску" bind:checked={showMask} />
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Button from '../ui/Button.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSubmit: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onSubmit }: Props = $props();
|
||||||
|
|
||||||
|
let text = $state('');
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (text.trim().length === 0) return;
|
||||||
|
onSubmit(text);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="heading-section">Текст</h2>
|
||||||
|
<textarea
|
||||||
|
class="input"
|
||||||
|
rows="8"
|
||||||
|
bind:value={text}
|
||||||
|
placeholder="Вставьте данные сюда"
|
||||||
|
aria-label="Текстовые данные"
|
||||||
|
></textarea>
|
||||||
|
<Button variant="secondary" onclick={submit} disabled={text.trim().length === 0}>
|
||||||
|
Декодировать
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: var(--surface);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-s);
|
||||||
|
min-height: 12rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { downloadBlob } from '$lib/core/io';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { text, filename }: Props = $props();
|
||||||
|
|
||||||
|
let copied = $state(false);
|
||||||
|
|
||||||
|
async function copy() {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copied = true;
|
||||||
|
setTimeout(() => (copied = false), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
downloadBlob(new Blob([text], { type: 'text/plain' }), `${filename}.txt`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="panel text-result">
|
||||||
|
<textarea class="output" rows="10" readonly value={text} aria-label="Текстовый результат"></textarea>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="secondary" onclick={copy}>
|
||||||
|
{copied ? 'Скопировано' : 'Копировать'}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="primary" onclick={download}>Скачать .txt</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-result {
|
||||||
|
padding: var(--space-3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.output {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: var(--bg);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--text-m);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .secondary {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .secondary:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { encodeBmpBytes } from './bmp';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('encodeBmpBytes', () => {
|
||||||
|
it('пишет заголовки BM и размеры для 2x1 без паддинга-неопределённости', () => {
|
||||||
|
const bytes = encodeBmpBytes(
|
||||||
|
makeImage(2, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[0, 255, 0, 255]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect([bytes[0], bytes[1]]).toEqual([0x42, 0x4d]);
|
||||||
|
const view = new DataView(bytes.buffer);
|
||||||
|
expect(view.getUint32(2, true)).toBe(54 + 8);
|
||||||
|
expect(view.getInt32(18, true)).toBe(2);
|
||||||
|
expect(view.getInt32(22, true)).toBe(1);
|
||||||
|
expect(view.getUint16(28, true)).toBe(24);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('хранит пиксели в BGR снизу-вверх с паддингом строки', () => {
|
||||||
|
const bytes = encodeBmpBytes(
|
||||||
|
makeImage(2, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[0, 255, 0, 255]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect([...bytes.slice(54, 60)]).toEqual([
|
||||||
|
0, 0, 255,
|
||||||
|
0, 255, 0
|
||||||
|
]);
|
||||||
|
expect(bytes[60]).toBe(0);
|
||||||
|
expect(bytes[61]).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('нижняя строка изображения идёт первой в файле', () => {
|
||||||
|
const bytes = encodeBmpBytes(
|
||||||
|
makeImage(4, 2, [
|
||||||
|
[10, 10, 10, 255],
|
||||||
|
[20, 20, 20, 255],
|
||||||
|
[30, 30, 30, 255],
|
||||||
|
[40, 40, 40, 255],
|
||||||
|
[50, 50, 50, 255],
|
||||||
|
[60, 60, 60, 255],
|
||||||
|
[70, 70, 70, 255],
|
||||||
|
[80, 80, 80, 255]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(bytes[54]).toBe(50);
|
||||||
|
expect(bytes[54 + 12]).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { PixelImage } from './types';
|
||||||
|
|
||||||
|
export function encodeBmpBytes(img: PixelImage): Uint8Array<ArrayBuffer> {
|
||||||
|
const rowSize = Math.ceil((img.width * 3) / 4) * 4;
|
||||||
|
const pixelArraySize = rowSize * img.height;
|
||||||
|
const fileSize = 54 + pixelArraySize;
|
||||||
|
|
||||||
|
const buf = new Uint8Array(fileSize);
|
||||||
|
const view = new DataView(buf.buffer);
|
||||||
|
|
||||||
|
buf[0] = 0x42;
|
||||||
|
buf[1] = 0x4d;
|
||||||
|
view.setUint32(2, fileSize, true);
|
||||||
|
view.setUint32(10, 54, true);
|
||||||
|
view.setUint32(14, 40, true);
|
||||||
|
view.setInt32(18, img.width, true);
|
||||||
|
view.setInt32(22, img.height, true);
|
||||||
|
view.setUint16(26, 1, true);
|
||||||
|
view.setUint16(28, 24, true);
|
||||||
|
view.setUint32(30, 0, true);
|
||||||
|
view.setUint32(34, pixelArraySize, true);
|
||||||
|
view.setInt32(38, 2835, true);
|
||||||
|
view.setInt32(42, 2835, true);
|
||||||
|
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
const srcY = img.height - 1 - y;
|
||||||
|
let off = 54 + y * rowSize;
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
const i = (srcY * img.width + x) * 4;
|
||||||
|
buf[off++] = img.data[i + 2];
|
||||||
|
buf[off++] = img.data[i + 1];
|
||||||
|
buf[off++] = img.data[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { encodeBmpBytes } from './bmp';
|
||||||
import { type PixelImage } from './types';
|
import { type PixelImage } from './types';
|
||||||
|
|
||||||
export type OutputMime = 'image/png' | 'image/jpeg' | 'image/webp';
|
export type OutputMime = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/bmp';
|
||||||
|
|
||||||
export const ACCEPTED_IMAGE_TYPES =
|
export const ACCEPTED_IMAGE_TYPES =
|
||||||
'image/png,image/jpeg,image/webp,image/gif,image/bmp,image/x-icon';
|
'image/png,image/jpeg,image/webp,image/gif,image/bmp,image/x-icon';
|
||||||
@@ -38,6 +39,9 @@ export async function encode(
|
|||||||
mime: OutputMime = 'image/png',
|
mime: OutputMime = 'image/png',
|
||||||
quality?: number
|
quality?: number
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
|
if (mime === 'image/bmp') {
|
||||||
|
return new Blob([encodeBmpBytes(img)], { type: mime });
|
||||||
|
}
|
||||||
if (mime === 'image/jpeg' || mime === 'image/webp') {
|
if (mime === 'image/jpeg' || mime === 'image/webp') {
|
||||||
if (quality !== undefined && (quality < 0 || quality > 1)) {
|
if (quality !== undefined && (quality < 0 || quality > 1)) {
|
||||||
throw new RangeError('quality должен быть в диапазоне 0..1');
|
throw new RangeError('quality должен быть в диапазоне 0..1');
|
||||||
|
|||||||
@@ -40,18 +40,24 @@ export type OutputFormat = {
|
|||||||
qualityParamId?: string;
|
qualityParamId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SourceMode = 'file' | 'none' | 'text';
|
||||||
|
|
||||||
export type ToolEntry = {
|
export type ToolEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
category: CategoryId;
|
category: CategoryId;
|
||||||
|
sourceMode?: SourceMode;
|
||||||
params: ParamDef[];
|
params: ParamDef[];
|
||||||
run: (img: PixelImage, params: Record<string, unknown>) => Promise<PixelImage> | PixelImage;
|
run: (img: PixelImage, params: Record<string, unknown>) => Promise<PixelImage> | PixelImage;
|
||||||
|
generate?: (params: Record<string, unknown>) => Promise<PixelImage> | PixelImage;
|
||||||
|
toText?: (img: PixelImage, params: Record<string, unknown>) => Promise<string> | string;
|
||||||
|
runFromText?: (text: string, params: Record<string, unknown>) => Promise<PixelImage> | PixelImage;
|
||||||
preview?: (
|
preview?: (
|
||||||
img: PixelImage,
|
img: PixelImage,
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>
|
||||||
) => Promise<PixelImage> | PixelImage;
|
) => Promise<PixelImage> | PixelImage;
|
||||||
resultType?: 'image' | 'info';
|
resultType?: 'image' | 'info' | 'text';
|
||||||
output?: OutputFormat;
|
output?: OutputFormat;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user