A biblioteca vue-picture-bd-marker oferece uma solução robusta para desenvolvedores que precisam implementar funcionalidades de marcação e anotação visual em imagens dentro do ecossistema Vue.js. Com ela, é possível desenhar áreas de interesse, atribuir etiquetas e exportar o resultado final.
Estrutura do Componenet
Para iniciar, integramos o componente de marcação em um template Vue, utilizando reeferências para manipular os dados e eventos para capturar as interações do usuário.
<template>
<div v-loading="estaCarregando" class="container-marcador">
<div class="wrapper-imagem" ref="areaCaptura">
<ui-marker
ref="instanciaMarcador"
class="marcador-visual"
:uniqueKey="idUnico"
:ratio="16 / 9"
@vmarker:onAnnoSelected="aoSelecionarAnotacao"
@vmarker:onAnnoAdded="aoAdicionarAnotacao"
@vmarker:onUpdated="aoSincronizarDados"
@vmarker:onReady="aoEstarPronto"
@vmarker:onImageLoad="aoCarregarImagem"
:readOnly="apenasLeitura"
:imgUrl="urlImagemAtual"
>
</ui-marker>
</div>
<div class="controles">
<el-button @click="limparMarcacoes">Limpar</el-button>
<el-button @click="exportarEEnviar" type="primary">Salvar Anotações</el-button>
</div>
<!-- Menu flutuante para seleção de etiquetas -->
<div v-if="exibirSeletor" class="menu-etiqueta" :style="{ left: coordenadaX, top: coordenadaY }">
<el-select
v-model="etiquetaSelecionada"
filterable
placeholder="Selecione a etiqueta"
@change="confirmarEtiqueta"
size="small"
>
<el-option
v-for="item in listaCategorias"
:key="item.id"
:label="item.nome"
:value="item.nome">
</el-option>
</el-select>
</div>
</div>
</template>
Lógica de Manipulação e Exportação
Abaixo, detalhamos a lógica necessária para gerenciar o estado das anotações, limitar a quantidade de marcações e converter o resultado visual em um arquivo pronto para upload via html2canvas.
import { AIMarker } from "Vue-Picture-BD-Marker";
import html2canvas from "html2canvas";
export default {
components: { "ui-marker": AIMarker },
data() {
return {
idUnico: "uuid-imagem-001",
apenasLeitura: false,
urlImagemAtual: 'caminho/para/imagem.jpg',
registrosAnotacoes: [],
etiquetaSelecionada: '',
exibirSeletor: false,
coordenadaX: '0px',
coordenadaY: '0px',
estaCarregando: false,
listaCategorias: [
{ id: 1, nome: 'Defeito' },
{ id: 2, nome: 'Observação' }
]
};
},
methods: {
aoSelecionarAnotacao(evento) {
this.idUnico = evento.uuid;
},
aoAdicionarAnotacao(evento) {
this.idUnico = evento.uuid;
this.exibirSeletor = true;
// Cálculo de posicionamento do seletor baseado na posição do marcador
const offsetX = parseFloat(evento.position.x.replace('%', '')) / 100;
const offsetY = parseFloat(evento.position.y1.replace('%', '')) / 100;
const container = this.$refs.areaCaptura;
this.coordenadaX = (offsetX * container.offsetWidth) + 'px';
this.coordenadaY = (offsetY * container.offsetHeight + 15) + 'px';
},
aoSincronizarDados(dados) {
this.validarLimiteMarcacoes(dados);
},
validarLimiteMarcacoes(dados) {
const LIMITE_MAXIMO = 5;
let anotacoesAtivas = dados.filter(item => item.tagName);
if (anotacoesAtivas.length > LIMITE_MAXIMO) {
this.$message.warning(`O limite de ${LIMITE_MAXIMO} marcações foi atingido.`);
// Lógica para remover o excesso ou impedir adição
}
this.registrosAnotacoes = anotacoesAtivas;
},
confirmarEtiqueta(valor) {
const marcador = this.$refs['instanciaMarcador'].getMarker();
marcador.setTag({
tagName: valor,
tag: "ID_INTERNO_001"
});
this.exibirSeletor = false;
},
async exportarEEnviar() {
if (this.registrosAnotacoes.length === 0) {
return this.$message.warning("Adicione ao menos uma marcação.");
}
this.estaCarregando = true;
try {
const canvas = await html2canvas(this.$refs.areaCaptura, {
useCORS: true,
backgroundColor: null
});
const base64 = canvas.toDataURL("image/jpeg", 0.8);
const arquivo = this.converterBase64ParaFile(base64, 'imagem-anotada');
await this.enviarParaServidor(arquivo);
} catch (erro) {
console.error("Erro no processamento:", erro);
} finally {
this.estaCarregando = false;
}
},
converterBase64ParaFile(dataurl, nomeArquivo) {
const partes = dataurl.split(',');
const mime = partes[0].match(/:(.*?);/)[1];
const bstr = atob(partes[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const extensao = mime.split('/')[1];
return new File([u8arr], `${nomeArquivo}.${extensao}`, { type: mime });
},
limparMarcacoes() {
this.$refs['instanciaMarcador'].clearData();
this.registrosAnotacoes = [];
}
}
};
Considerações Técnicas
- Sincronização: O evento
@vmarker:onUpdatedé essencial para manter o estado local da aplicação alinhado com as modificações visuais feitas pelo usuário (redimensionamento ou exclusão de caixas). - Exportação Visual: O uso do
html2canvaspermite que as marcações renderizadas via SVG/DOM sejam "achatadas" em uma imagem estática, facilitando o armazenamento e visualização em outros sistemas. - Conversão de Dados: A função
converterBase64ParaFileé um utilitário necessário para transformar a saída do Canvas em um objetoFilecompatível com envios viaFormData.