Implementando Anotações em Imagens com Vue-Picture-BD-Marker

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 html2canvas permite 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 objeto File compatível com envios via FormData.

Tags: Vue.js Vue-Picture-BD-Marker html2canvas frontend javascript

Publicado em 10-2 23:29