Files
kk_card_gen/lib/id-config.ts
T
2026-09-13 14:58:27 +05:30

278 lines
7.6 KiB
TypeScript

export type TextAlign = "left" | "center" | "right";
export type IdDrawStyle = {
fontFamily: string;
fontSize: number;
color: string;
bold: boolean;
italic: boolean;
align: TextAlign;
xPercent: number;
yPercent: number;
shadowEnabled: boolean;
shadowColor: string;
shadowOpacity: number;
shadowBlur: number;
shadowOffsetX: number;
shadowOffsetY: number;
outlineEnabled: boolean;
outlineColor: string;
outlineOpacity: number;
outlineWidth: number;
};
export type GenerateConfig = IdDrawStyle & {
prefix: string;
suffix: string;
start: number;
pad: number;
count: number;
useCustomFont: boolean;
};
export const MAX_COUNT = 1000;
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
export const MAX_FONT_BYTES = 5 * 1024 * 1024;
export const CUSTOM_FONT_FAMILY = "CustomUpload";
export const DEFAULT_CONFIG: GenerateConfig = {
prefix: "KK",
suffix: "",
start: 1,
pad: 4,
count: 1000,
fontFamily: "Inter",
fontSize: 48,
color: "#111111",
bold: false,
italic: false,
align: "center",
xPercent: 50,
yPercent: 50,
shadowEnabled: false,
shadowColor: "#000000",
shadowOpacity: 0.45,
shadowBlur: 8,
shadowOffsetX: 2,
shadowOffsetY: 3,
outlineEnabled: false,
outlineColor: "#ffffff",
outlineOpacity: 1,
outlineWidth: 3,
useCustomFont: false,
};
const COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
export function formatUserId(
prefix: string,
n: number,
pad: number,
suffix: string,
): string {
return `${prefix}${String(n).padStart(Math.max(0, pad), "0")}${suffix}`;
}
export function sanitizeFilename(id: string): string {
const cleaned = id
.replace(/[/\\:*?"<>|]/g, "")
.replace(/\s+/g, " ")
.trim();
return cleaned || "id";
}
export function buildCanvasFont(
style: Pick<IdDrawStyle, "fontFamily" | "fontSize" | "bold" | "italic">,
): string {
const italic = style.italic ? "italic " : "";
const weight = style.bold ? "700 " : "400 ";
return `${italic}${weight}${style.fontSize}px "${style.fontFamily}"`;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
function asNumber(value: unknown, fallback: number): number {
const n = typeof value === "number" ? value : Number(value);
return Number.isFinite(n) ? n : fallback;
}
function asString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function asBool(value: unknown): boolean {
return value === true;
}
export function parseGenerateConfig(raw: unknown): GenerateConfig {
if (!raw || typeof raw !== "object") {
throw new Error("Invalid config");
}
const input = raw as Record<string, unknown>;
const start = Math.trunc(asNumber(input.start, DEFAULT_CONFIG.start));
const pad = Math.trunc(asNumber(input.pad, DEFAULT_CONFIG.pad));
const count = Math.trunc(asNumber(input.count, DEFAULT_CONFIG.count));
const fontSize = asNumber(input.fontSize, DEFAULT_CONFIG.fontSize);
const xPercent = asNumber(input.xPercent, DEFAULT_CONFIG.xPercent);
const yPercent = asNumber(input.yPercent, DEFAULT_CONFIG.yPercent);
const shadowOpacity = asNumber(
input.shadowOpacity,
DEFAULT_CONFIG.shadowOpacity,
);
const shadowBlur = asNumber(input.shadowBlur, DEFAULT_CONFIG.shadowBlur);
const shadowOffsetX = asNumber(
input.shadowOffsetX,
DEFAULT_CONFIG.shadowOffsetX,
);
const shadowOffsetY = asNumber(
input.shadowOffsetY,
DEFAULT_CONFIG.shadowOffsetY,
);
const outlineOpacity = asNumber(
input.outlineOpacity,
DEFAULT_CONFIG.outlineOpacity,
);
const outlineWidth = asNumber(
input.outlineWidth,
DEFAULT_CONFIG.outlineWidth,
);
const color = asString(input.color, DEFAULT_CONFIG.color);
const shadowColor = asString(input.shadowColor, DEFAULT_CONFIG.shadowColor);
const outlineColor = asString(
input.outlineColor,
DEFAULT_CONFIG.outlineColor,
);
const align = asString(input.align, DEFAULT_CONFIG.align);
const prefix = asString(input.prefix).slice(0, 50);
const suffix = asString(input.suffix).slice(0, 50);
const fontFamily = asString(input.fontFamily, DEFAULT_CONFIG.fontFamily).slice(
0,
80,
);
if (start < 0 || start > 1_000_000) {
throw new Error("Start number must be between 0 and 1000000");
}
if (pad < 0 || pad > 6) {
throw new Error("Pad length must be between 0 and 6");
}
if (count < 1 || count > MAX_COUNT) {
throw new Error(`Count must be between 1 and ${MAX_COUNT}`);
}
if (fontSize < 8 || fontSize > 400) {
throw new Error("Font size must be between 8 and 400");
}
if (xPercent < 0 || xPercent > 100 || yPercent < 0 || yPercent > 100) {
throw new Error("Position must be between 0 and 100 percent");
}
if (!COLOR_RE.test(color)) {
throw new Error("Color must be a hex value like #111111");
}
if (!COLOR_RE.test(shadowColor)) {
throw new Error("Shadow color must be a hex value like #000000");
}
if (shadowOpacity < 0 || shadowOpacity > 1) {
throw new Error("Shadow opacity must be between 0 and 1");
}
if (shadowBlur < 0 || shadowBlur > 80) {
throw new Error("Shadow blur must be between 0 and 80");
}
if (
shadowOffsetX < -80 ||
shadowOffsetX > 80 ||
shadowOffsetY < -80 ||
shadowOffsetY > 80
) {
throw new Error("Shadow offset must be between -80 and 80");
}
if (!COLOR_RE.test(outlineColor)) {
throw new Error("Outline color must be a hex value like #ffffff");
}
if (outlineOpacity < 0 || outlineOpacity > 1) {
throw new Error("Outline opacity must be between 0 and 1");
}
if (outlineWidth < 0 || outlineWidth > 40) {
throw new Error("Outline width must be between 0 and 40");
}
if (align !== "left" && align !== "center" && align !== "right") {
throw new Error("Alignment must be left, center, or right");
}
return {
prefix,
suffix,
start,
pad,
count,
fontFamily,
fontSize,
color,
bold: asBool(input.bold),
italic: asBool(input.italic),
align,
xPercent,
yPercent,
shadowEnabled: asBool(input.shadowEnabled),
shadowColor,
shadowOpacity,
shadowBlur,
shadowOffsetX,
shadowOffsetY,
outlineEnabled: asBool(input.outlineEnabled),
outlineColor,
outlineOpacity,
outlineWidth,
useCustomFont: asBool(input.useCustomFont),
};
}
export function toIdDrawStyle(
config: GenerateConfig,
fontFamily = config.useCustomFont ? CUSTOM_FONT_FAMILY : config.fontFamily,
): IdDrawStyle {
return {
fontFamily,
fontSize: config.fontSize,
color: config.color,
bold: config.bold,
italic: config.italic,
align: config.align,
xPercent: config.xPercent,
yPercent: config.yPercent,
shadowEnabled: config.shadowEnabled,
shadowColor: config.shadowColor,
shadowOpacity: config.shadowOpacity,
shadowBlur: config.shadowBlur,
shadowOffsetX: config.shadowOffsetX,
shadowOffsetY: config.shadowOffsetY,
outlineEnabled: config.outlineEnabled,
outlineColor: config.outlineColor,
outlineOpacity: config.outlineOpacity,
outlineWidth: config.outlineWidth,
};
}
export function hexToRgba(hex: string, opacity: number): string {
let value = hex.replace("#", "");
if (value.length === 3) {
value = value
.split("")
.map((char) => char + char)
.join("");
}
let alpha = clamp(opacity, 0, 1);
if (value.length === 8) {
alpha *= parseInt(value.slice(6, 8), 16) / 255;
value = value.slice(0, 6);
}
const r = parseInt(value.slice(0, 2), 16);
const g = parseInt(value.slice(2, 4), 16);
const b = parseInt(value.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}