@@ -0,0 +1,24 @@
|
||||
export function calculateFontSizeForText(text: string, maxWidth: number): number {
|
||||
// Создаем временный элемент <span> для измерения текста
|
||||
const span = document.createElement('span')
|
||||
span.style.position = 'absolute'
|
||||
span.style.whiteSpace = 'nowrap'
|
||||
span.style.fontFamily = 'Arial, sans-serif' // Задаем нужный шрифт
|
||||
span.style.visibility = 'hidden'
|
||||
span.textContent = text
|
||||
document.body.appendChild(span)
|
||||
|
||||
// Начальный размер шрифта
|
||||
let fontSize = maxWidth
|
||||
span.style.fontSize = `${fontSize}px`
|
||||
|
||||
// Уменьшаем размер шрифта, пока ширина текста не впишется в maxWidth
|
||||
while (span.offsetWidth > maxWidth) {
|
||||
fontSize -= 1
|
||||
span.style.fontSize = `${fontSize}px`
|
||||
}
|
||||
|
||||
// Удаляем временный элемент
|
||||
document.body.removeChild(span)
|
||||
return fontSize
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export async function convertImageToBlob({
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
type,
|
||||
}: {
|
||||
url: string
|
||||
width?: number // Ширина необязательна
|
||||
height?: number // Высота необязательна
|
||||
type: string
|
||||
}): Promise<Blob> {
|
||||
if (!width && !height) {
|
||||
throw new Error('Необходимо указать хотя бы одно значение: width или height.')
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
// Вычисляем размеры, сохраняя пропорции
|
||||
const aspectRatio = img.naturalWidth / img.naturalHeight
|
||||
const targetWidth = width || height! * aspectRatio
|
||||
const targetHeight = height || width! / aspectRatio
|
||||
|
||||
// Создаем canvas и рисуем изображение
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = Math.round(targetWidth)
|
||||
canvas.height = Math.round(targetHeight)
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
if (!context) {
|
||||
return reject(new Error('Ошибка создания контекста'))
|
||||
}
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Конвертируем canvas в Blob
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Ошибка создания изображения'))
|
||||
}
|
||||
},
|
||||
type,
|
||||
1.0,
|
||||
)
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Ошибка загрузки изображения'))
|
||||
}
|
||||
|
||||
img.src = url
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { convertImageToBlob } from './convertImageToBlob'
|
||||
|
||||
export async function downloadImage({
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
filename,
|
||||
type,
|
||||
}: {
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
filename: string
|
||||
type: string
|
||||
}) {
|
||||
try {
|
||||
const blob = await convertImageToBlob({ url, width, height, type })
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(link.href) // Очищаем URL после загрузки
|
||||
} catch (err) {
|
||||
console.error('Ошибка преобразования SVG в PNG:', err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { calculateFontSizeForText } from '@/utils/calculateFontSizeForText'
|
||||
|
||||
/**
|
||||
* Добавление текста в SVG, прибитого к нижнему краю.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param label Надпись для добавления.
|
||||
* @param color Цвет текста.
|
||||
* @param marginMultiplier Множитель относительного отступа.
|
||||
* @returns SVG с добавленным текстом.
|
||||
*/
|
||||
export function addLabelToSvg(
|
||||
svgText: string,
|
||||
label: string,
|
||||
color: string,
|
||||
marginMultiplier: number = 0.1,
|
||||
): string {
|
||||
if (!label) {
|
||||
return svgText
|
||||
}
|
||||
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml')
|
||||
const svgElement = svgDoc.querySelector('svg')
|
||||
|
||||
if (svgElement) {
|
||||
// Увеличиваем viewBox для текста
|
||||
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number)
|
||||
if (viewBox) {
|
||||
const fontSize = calculateFontSizeForText(label, viewBox[2])
|
||||
const textMargin = Math.min(fontSize, viewBox[3] * marginMultiplier)
|
||||
|
||||
viewBox[3] += fontSize + textMargin // Увеличиваем высоту
|
||||
svgElement.setAttribute('viewBox', viewBox.join(' '))
|
||||
|
||||
// Добавляем текст
|
||||
const textElement = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'text')
|
||||
textElement.setAttribute('x', '50%')
|
||||
textElement.setAttribute('y', '100%') // Устанавливаем Y на 100% высоты
|
||||
textElement.setAttribute('text-anchor', 'middle')
|
||||
textElement.setAttribute('alignment-baseline', 'baseline') // Привязываем к нижнему краю
|
||||
textElement.setAttribute('dominant-baseline', 'text-after-edge') // Альтернативный способ
|
||||
textElement.setAttribute('fill', color)
|
||||
textElement.setAttribute('font-size', fontSize.toString())
|
||||
textElement.setAttribute('font-family', 'Arial, sans-serif')
|
||||
textElement.textContent = label
|
||||
|
||||
svgElement.appendChild(textElement)
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
return serializer.serializeToString(svgDoc)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { optimize } from 'svgo'
|
||||
|
||||
/**
|
||||
* Оптимизация SVG с использованием SVGO.
|
||||
* @param svgText string изображения SVG.
|
||||
* @returns Строка оптимизированного SVG.
|
||||
*/
|
||||
export function 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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Заменяет атрибут `fill` в SVG-коде.
|
||||
* @param svgText Исходный SVG в виде строки.
|
||||
* @param fillColor Новый цвет для fill.
|
||||
* @returns Измененный SVG-код.
|
||||
*/
|
||||
export function replaceFillInSvg(svgText: string, fillColor: string): string {
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml')
|
||||
const paths = svgDoc.querySelectorAll('path, rect, circle, polygon, ellipse')
|
||||
|
||||
paths.forEach((path) => {
|
||||
if (path.hasAttribute('fill')) {
|
||||
path.setAttribute('fill', fillColor)
|
||||
}
|
||||
})
|
||||
|
||||
const serializer = new XMLSerializer()
|
||||
return serializer.serializeToString(svgDoc)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const sanitizeFileName = (fileName: string) => {
|
||||
return fileName
|
||||
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') // Убираем запрещённые символы
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
Reference in New Issue
Block a user