File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 103 KiB |
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,14 +60,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import QRCodeStyling from 'qr-code-styling'
|
import QRCodeStyling from 'qr-code-styling'
|
||||||
import { optimize } from 'svgo'
|
import { optimize } from 'svgo'
|
||||||
import { getQrCodeDocument } from '@/api/qrCode'
|
import { getQrCodeDocument } from '@/api/qrCode'
|
||||||
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
|
||||||
|
import { calculateFontSizeForText } from '@/utils/images/calculateFontSizeForText'
|
||||||
|
import { downloadImage } from '@/utils/images/downloadImage'
|
||||||
|
|
||||||
const { qr_code_uri } = defineProps<{ qr_code_uri: QRCodeDocument['uri'] }>()
|
const { qr_code_uri } = defineProps<{ qr_code_uri: QRCodeDocument['uri'] }>()
|
||||||
|
const color = ref('#f43f5e')
|
||||||
|
const label = ref('Сообщите о проблеме или задайте вопрос')
|
||||||
|
const width = ref(256 * 4)
|
||||||
|
const height = ref(width.value)
|
||||||
|
|
||||||
// Хук для загрузки данных QR‑кода
|
// Хук для загрузки данных QR‑кода
|
||||||
const {
|
const {
|
||||||
@@ -81,12 +87,28 @@ const {
|
|||||||
|
|
||||||
// Инициализация QRCodeStyling
|
// Инициализация QRCodeStyling
|
||||||
const qrCode = new QRCodeStyling({
|
const qrCode = new QRCodeStyling({
|
||||||
width: 256 * 4,
|
width: width.value,
|
||||||
height: 256 * 4,
|
height: height.value,
|
||||||
data: '',
|
data: '',
|
||||||
|
image: '/images/logo.svg',
|
||||||
|
imageOptions: {
|
||||||
|
margin: 20,
|
||||||
|
// hideBackgroundDots: false,
|
||||||
|
imageSize: 0.45,
|
||||||
|
},
|
||||||
type: 'svg',
|
type: 'svg',
|
||||||
backgroundOptions: {
|
backgroundOptions: {
|
||||||
color: '#ffffff',
|
color: 'transparent',
|
||||||
|
},
|
||||||
|
cornersSquareOptions: {
|
||||||
|
type: 'extra-rounded',
|
||||||
|
},
|
||||||
|
cornersDotOptions: {
|
||||||
|
type: 'dot',
|
||||||
|
},
|
||||||
|
dotsOptions: {
|
||||||
|
color: color.value,
|
||||||
|
type: 'classy-rounded',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -126,7 +148,48 @@ const {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (optimizedSvg.data) {
|
if (optimizedSvg.data) {
|
||||||
return URL.createObjectURL(new Blob([optimizedSvg.data], { type: 'image/svg+xml' }))
|
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 {
|
} else {
|
||||||
throw new Error('Ошибка оптимизации SVG')
|
throw new Error('Ошибка оптимизации SVG')
|
||||||
}
|
}
|
||||||
@@ -144,9 +207,14 @@ const {
|
|||||||
// Скачивание PNG
|
// Скачивание PNG
|
||||||
async function downloadPng() {
|
async function downloadPng() {
|
||||||
try {
|
try {
|
||||||
await qrCode.download({
|
if (!svgQueryData.value) {
|
||||||
name: sanitizedFileName.value,
|
throw new Error('SVG не загружен')
|
||||||
extension: 'png',
|
}
|
||||||
|
await downloadImage({
|
||||||
|
type: 'image/png',
|
||||||
|
filename: `${sanitizedFileName.value}.png`,
|
||||||
|
width: width.value,
|
||||||
|
url: svgQueryData.value,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка скачивания PNG:', err)
|
console.error('Ошибка скачивания PNG:', err)
|
||||||
|
|||||||
Reference in New Issue
Block a user