diff --git a/public/images/logo.svg b/public/images/logo.svg deleted file mode 100644 index b6ec863..0000000 --- a/public/images/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..c4771d6 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/utils/images/calculateFontSizeForText.ts b/src/utils/calculateFontSizeForText.ts similarity index 100% rename from src/utils/images/calculateFontSizeForText.ts rename to src/utils/calculateFontSizeForText.ts diff --git a/src/utils/images/svg/addLabelToSvg.ts b/src/utils/images/svg/addLabelToSvg.ts new file mode 100644 index 0000000..2cac45a --- /dev/null +++ b/src/utils/images/svg/addLabelToSvg.ts @@ -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) +} diff --git a/src/utils/images/svg/optimizeSvg.ts b/src/utils/images/svg/optimizeSvg.ts new file mode 100644 index 0000000..c0b3b01 --- /dev/null +++ b/src/utils/images/svg/optimizeSvg.ts @@ -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 +} diff --git a/src/utils/images/svg/replaceFillInSvg.ts b/src/utils/images/svg/replaceFillInSvg.ts new file mode 100644 index 0000000..03d3dc8 --- /dev/null +++ b/src/utils/images/svg/replaceFillInSvg.ts @@ -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) +} diff --git a/src/views/ManageQRCodeReadyView.vue b/src/views/ManageQRCodeReadyView.vue index 82e1400..03d8ce2 100644 --- a/src/views/ManageQRCodeReadyView.vue +++ b/src/views/ManageQRCodeReadyView.vue @@ -9,6 +9,9 @@
QR‑код
+

+ Ошибка генерации QR‑кода: {{ svgQueryError?.message }} +

@@ -63,14 +66,17 @@ import { computed, ref } from 'vue' import { useQuery } from '@tanstack/vue-query' import QRCodeStyling from 'qr-code-styling' -import { optimize } from 'svgo' +import { optimizeSvg } from '@/utils/images/svg/optimizeSvg' +import { replaceFillInSvg } from '@/utils/images/svg/replaceFillInSvg' import { getQrCodeDocument } from '@/api/qrCode' import { type QRCodeDocument } from '@/types/DBDocumentTypes' -import { calculateFontSizeForText } from '@/utils/images/calculateFontSizeForText' import { downloadImage } from '@/utils/images/downloadImage' +import { addLabelToSvg } from '@/utils/images/svg/addLabelToSvg' +import logo from '@/assets/logo.svg?raw' const { qr_code_uri } = defineProps<{ qr_code_uri: QRCodeDocument['uri'] }>() -const color = ref('#f43f5e') +const color = ref('#000') // Цвет для текста QR-кода +const logoColor = ref(color.value) // Цвет логотипа // #FB2500 const label = ref('Сообщите о проблеме или задайте вопрос') const width = ref(256 * 4) const height = ref(width.value) @@ -85,123 +91,75 @@ const { queryFn: () => getQrCodeDocument(qr_code_uri), }) -// Инициализация QRCodeStyling -const qrCode = new QRCodeStyling({ - width: width.value, - height: height.value, - data: '', - image: '/images/logo.svg', - imageOptions: { - margin: 20, - // hideBackgroundDots: false, - imageSize: 0.45, - }, - type: 'svg', - backgroundOptions: { - color: 'transparent', - }, - cornersSquareOptions: { - type: 'extra-rounded', - }, - cornersDotOptions: { - type: 'dot', - }, - dotsOptions: { - color: color.value, - type: 'classy-rounded', - }, -}) - // Получение имени файла const sanitizedFileName = computed(() => { const sanitized = qrCodeDoc.value?.name ?.replace(/[<>:"/\\|?*\x00-\x1F]/g, '') // Убираем запрещённые символы - .replace(/\s+/g, '_') // Заменяем пробелы на подчёркивания + .replace(/\s+/g, ' ') // Заменяем пробелы на подчёркивания .trim() - return sanitized ? `${sanitized}_QR-Code_HereConnect` : 'QR-Code_HereConnect' + return sanitized ? `${sanitized} QR-Code - НаСвязи` : 'QR-Code - НаСвязи' }) -// Хук для генерации и оптимизации SVG +// Генерация и оптимизация SVG const { isLoading: svgQueryIsLoading, + isError: svgQueryIsError, data: svgQueryData, error: svgQueryError, } = useQuery({ queryKey: ['svg', computed(() => qr_code_uri)], enabled: computed(() => !!qrCodeDoc.value?.url), + retry: false, queryFn: async () => { - qrCode.update({ data: qrCodeDoc.value!.url }) + // Заменяем цвет `fill` в логотипе + const updatedLogo = replaceFillInSvg(logo, logoColor.value) + + // Добавляем текст в логотип (если нужно) + const logoWithText = addLabelToSvg(updatedLogo, 'НаСвязи', logoColor.value, 0.01) + + const image = URL.createObjectURL(new Blob([logoWithText], { type: 'image/svg+xml' })) + + // Инициализация QRCodeStyling + const qrCode = new QRCodeStyling({ + width: width.value, + height: height.value, + data: qrCodeDoc.value!.url, + image, + imageOptions: { + margin: 20, + imageSize: 0.45, + }, + type: 'svg', + backgroundOptions: { + color: 'transparent', + }, + cornersSquareOptions: { + type: 'extra-rounded', + }, + cornersDotOptions: { + type: 'dot', + }, + dotsOptions: { + color: color.value, + type: 'classy-rounded', + }, + }) const blob = await qrCode.getRawData('svg') if (blob && blob instanceof Blob) { const text = await blob.text() - - const optimizedSvg = optimize(text, { - plugins: [ - { name: 'removeDoctype' }, - { name: 'removeComments' }, - { name: 'removeDimensions' }, - { name: 'convertShapeToPath' }, - { name: 'convertPathData' }, - { name: 'mergePaths' }, - ], - }) - - if (optimizedSvg.data) { - if (!label.value) { - // Если label пуст, возвращаем SVG без изменений - return URL.createObjectURL(new Blob([optimizedSvg.data], { type: 'image/svg+xml' })) - } - - // Добавляем надпись и увеличиваем viewBox - const parser = new DOMParser() - const svgDoc = parser.parseFromString(optimizedSvg.data, '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.value, viewBox[2]) - - // Рассчитываем вертикальное расстояние между QR-кодом и текстом - const textMargin = Math.min(fontSize, viewBox[3] * 0.1) // Максимум 10% от высоты QR-кода - - 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', viewBox[3] - fontSize / 2) // Уменьшаем нижний отступ - textElement.setAttribute('text-anchor', 'middle') - textElement.setAttribute('fill', color.value) - textElement.setAttribute('font-size', fontSize.toString()) - textElement.setAttribute('font-family', 'Arial, sans-serif') - textElement.textContent = label.value - - svgElement.appendChild(textElement) - } - } - - // Сериализуем обратно в строку - const serializer = new XMLSerializer() - const updatedSvg = serializer.serializeToString(svgDoc) - - return URL.createObjectURL(new Blob([updatedSvg], { type: 'image/svg+xml' })) - } else { + let resultSvg = optimizeSvg(text) + if (!resultSvg) { throw new Error('Ошибка оптимизации SVG') } + if (label.value) { + resultSvg = addLabelToSvg(resultSvg, label.value, color.value) + } + return URL.createObjectURL(new Blob([resultSvg], { type: 'image/svg+xml' })) } else { throw new Error('Ошибка генерации QR‑кода') } }, - onSettled(data) { - if (!data && svgQueryError) { - console.error('Ошибка загрузки SVG:', svgQueryError.message) - } - }, }) // Скачивание PNG