add tg_bot_sessions, padding to qrcode
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
DOMParser,
|
||||
XMLSerializer,
|
||||
type Document as XmlDomDocument,
|
||||
type Element as XmlElement,
|
||||
} from "@xmldom/xmldom";
|
||||
import { optimize } from "svgo";
|
||||
import { calculateFontSizeForText } from "./calculateFontSizeForText";
|
||||
|
||||
export class SvgProcessor {
|
||||
private doc: XmlDomDocument;
|
||||
private svgElement: XmlElement;
|
||||
|
||||
constructor(
|
||||
svgText: string,
|
||||
private optimizeOutput: boolean = false,
|
||||
) {
|
||||
const parser = new DOMParser();
|
||||
this.doc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
const svgElement = this.doc.getElementsByTagName("svg").item(0);
|
||||
if (!svgElement) {
|
||||
throw new Error("SVG не найден");
|
||||
}
|
||||
this.svgElement = svgElement;
|
||||
}
|
||||
|
||||
addPadding(padding: number, backgroundColor = "transparent"): SvgProcessor {
|
||||
const viewBox = this.svgElement
|
||||
.getAttribute("viewBox")
|
||||
?.split(" ")
|
||||
.map(Number);
|
||||
if (!viewBox) {
|
||||
throw new Error("viewBox не найден");
|
||||
}
|
||||
const [x, y, width, height] = viewBox;
|
||||
|
||||
// Увеличиваем размеры viewBox с учётом отступов
|
||||
const newWidth = width + padding * 2;
|
||||
const newHeight = height + padding * 2;
|
||||
|
||||
const newViewBox = [x, y, newWidth, newHeight].join(" ");
|
||||
this.svgElement.setAttribute("viewBox", newViewBox);
|
||||
|
||||
// Добавляем прямоугольник для фона, если требуется
|
||||
if (backgroundColor !== "transparent") {
|
||||
const rect = this.doc.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"rect",
|
||||
);
|
||||
rect.setAttribute("x", x.toString());
|
||||
rect.setAttribute("y", y.toString());
|
||||
rect.setAttribute("width", newWidth.toString());
|
||||
rect.setAttribute("height", newHeight.toString());
|
||||
rect.setAttribute("fill", backgroundColor);
|
||||
this.svgElement.insertBefore(rect, this.svgElement.firstChild);
|
||||
}
|
||||
|
||||
// Сдвигаем все элементы
|
||||
const elements = Array.from(this.svgElement.childNodes).filter(
|
||||
(node) => node.nodeType === 1 && node !== this.svgElement.firstChild,
|
||||
) as XmlElement[];
|
||||
|
||||
elements.forEach((el) => {
|
||||
const isText = el.tagName === "text";
|
||||
|
||||
// Для текста добавляем дополнительное смещение
|
||||
if (isText) {
|
||||
const dy = el.getAttribute("dy") || "0";
|
||||
if (!dy.includes("%")) {
|
||||
el.setAttribute("dy", (parseFloat(dy) - padding).toString());
|
||||
}
|
||||
const dx = el.getAttribute("dx") || "0";
|
||||
if (!dx.includes("%") && !el.getAttribute("x")?.includes("%")) {
|
||||
el.setAttribute("dx", padding.toString());
|
||||
}
|
||||
} else {
|
||||
const transform = el.getAttribute("transform") || "";
|
||||
el.setAttribute(
|
||||
"transform",
|
||||
`translate(${padding}, ${padding}) ${transform}`.trim(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем атрибуты width и height, чтобы размеры SVG задавались только через viewBox
|
||||
this.svgElement.removeAttribute("width");
|
||||
this.svgElement.removeAttribute("height");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
addLabel(
|
||||
label: string,
|
||||
color: string,
|
||||
marginMultiplier: number = 0.1,
|
||||
): SvgProcessor {
|
||||
if (!label) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const viewBoxAttr = this.svgElement.getAttribute("viewBox");
|
||||
if (!viewBoxAttr) {
|
||||
throw new Error("viewBox не найден");
|
||||
}
|
||||
|
||||
const viewBox: readonly number[] = viewBoxAttr.split(" ").map(Number);
|
||||
const [x, y, width, height] = viewBox;
|
||||
if (viewBox.length === 4) {
|
||||
// Рассчитываем размер шрифта
|
||||
const fontSize = calculateFontSizeForText(label, width);
|
||||
const textMargin = Math.min(fontSize, height * marginMultiplier);
|
||||
const extraBottomPadding = fontSize * 0.3; // 30% дополнительного пространства для висячих символов
|
||||
|
||||
// Увеличиваем высоту viewBox
|
||||
const newHeight = height + textMargin + fontSize;
|
||||
this.svgElement.setAttribute(
|
||||
"viewBox",
|
||||
[x, y, width, newHeight].join(" "),
|
||||
);
|
||||
|
||||
// Создаем элемент текста
|
||||
const textElement = this.doc.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
"text",
|
||||
);
|
||||
|
||||
textElement.setAttribute("x", "50%");
|
||||
textElement.setAttribute("y", "100%");
|
||||
textElement.setAttribute("dy", `-${extraBottomPadding}px`);
|
||||
textElement.setAttribute("text-anchor", "middle");
|
||||
textElement.setAttribute("alignment-baseline", "baseline");
|
||||
textElement.setAttribute("dominant-baseline", "alphabetic");
|
||||
textElement.setAttribute("fill", color);
|
||||
textElement.setAttribute("font-size", fontSize.toString());
|
||||
textElement.setAttribute("font-family", "Arial, sans-serif");
|
||||
textElement.textContent = label;
|
||||
|
||||
this.svgElement.appendChild(textElement);
|
||||
}
|
||||
|
||||
// Удаляем атрибуты width и height, чтобы размеры SVG задавались только через viewBox
|
||||
this.svgElement.removeAttribute("width");
|
||||
this.svgElement.removeAttribute("height");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
replaceFill(fillColor: string): SvgProcessor {
|
||||
const elements = this.doc.getElementsByTagName("*");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const el = elements.item(i);
|
||||
if (el && el.hasAttribute("fill")) {
|
||||
el.setAttribute("fill", fillColor);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Оптимизация SVG с использованием SVGO.
|
||||
* @param svgText string изображения SVG.
|
||||
* @returns Строка оптимизированного SVG.
|
||||
*/
|
||||
private optimizeSvg(svgText: string): string {
|
||||
const optimizedSvg = optimize(svgText, {
|
||||
plugins: [
|
||||
{ name: "removeDoctype" },
|
||||
{ name: "removeComments" },
|
||||
{ name: "removeDimensions" },
|
||||
{ name: "convertShapeToPath" },
|
||||
{ name: "convertPathData" },
|
||||
{ name: "mergePaths" },
|
||||
],
|
||||
});
|
||||
|
||||
if (!optimizedSvg.data) {
|
||||
throw new Error("Ошибка оптимизации SVG");
|
||||
}
|
||||
|
||||
return optimizedSvg.data;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const serializer = new XMLSerializer();
|
||||
const result = serializer.serializeToString(this.doc);
|
||||
// console.log(result);
|
||||
if (this.optimizeOutput) {
|
||||
return this.optimizeSvg(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user