QR Code em AdvPL – Parte 01

Introdução

Alguém me comentou algo sobre QR Code no meu Facebook, e como eu já tinha ouvido falar nesse tipo de código, resolvi pesquisar um pouco. O resultado da pesquisa acabou saindo em um código em AdvPL para gerar um QR Code “na unha”. Vamos ver passo a passo o que e como isso foi feito a partir deste post.

O que é o QR Code

Visto de uma forma muito, muito simplista, um QR Code é um código de barras quadriculado. Trata-se de uma imagem quadriculada formando uma matriz de bits, onde cada bit “1” é um quadradinho preto, e cada bit “0” é um quadradinho branco. Visto como apenas um mapa de bits quadriculado, e encontrando muitos geradores de QR Code online na Internet, eu não imaginava a complexidade escondida por trás da estrutura dessa imagem. A definição detalhada e completa você encontra na Wikipedia.

Tudo em AdvPL

A imagem acima, por exemplo, é um QR Code que contém o endereço deste blog — “https://siga0984.wordpress.com/”. Um QR Code pode conter apenas números, caracteres alfanuméricos (com alfabeto limitado), caracteres UNICODE (UTF8) e até mesmo Kanji — este código inclusive foi desenvolvido para a Industria Automobilística Japonesa em 1994.

Embora sua utilização inicial fosse apenas para etiquetar caixas, atualmente ele é utilizado das mais diversas formas. Com a possibilidade de leitura da imagem por um Smartphone, ele pode ser usado para identificar um website, um evento, códigos promocionais, conter dados criptografados — como o verso da versão impressa da nova CNH Brasileira  — enfim, o céu é o limite.

Características Gerais

Cada “quadradinho” de um QR Code é chamado de “módulo”. Existem 40 versões de QR Code, relacionadas diretamente ao tamanho da matriz de módulos. A versão 1 inicia com 21×21 módulos, e a 40 com 177×177 módulos. Cada versão incrementa o tamanho da matriz em 4 unidades. A codificação dos dados pode usar 4 níveis de recuperação de erro, o que permite inclusive ler com sucesso um QRCode parcialmente danificado. A imagem contém os dados, a correção de erros, áreas reservadas para informações sobre a imagem — como os dado foram codificados na imagem — e alguns padrões de imagens fixas para auxiliar no alinhamento óptico do leitor. A partir de determinadas versões os dados são divididos em grupos, e o método de disposição da matriz de bits intercala dados e correção de erro — não fica tão complexo, apenas mais trabalhoso.

Mergulhando nos bits

Implementar um gerador de QR Code não está sendo tão simples, cada etapa possui suas características e regras. A maioria é relativamente simples, apenas requer vários passos — exceto pelo cálculo da correção de erro… Passei três dias mastigando exemplos e pesquisando o algoritmo de Reed-Solomon, só consegui implementar os cálculos depois que eu realmente entendi — ou quase — a multiplicação e divisão de polinômios. Neurônios colocados para escanteio ha quase 30 anos foram “ressuscitados” lembrando das regras matemáticas ligadas a equações, potências e afins.

Boa parte da implementação envolve a geração de sequências binárias e diversas conversões — as funções criadas para isso foram incorporadas na ZLIB, e já foram posadas no Blog, e a última parte seria justamente a geração da imagem, já praticamente pronta com a classe ZBITMAP — publicada em posts anteriores.

Fontes de Pesquisa

Nas minhas buscas, o que salvou a pátria e trouxe informações indispensáveis para começar a implementação em AdvPL e esclarecer “o que é e como funciona” foi o site https://www.thonky.com/qr-code-tutorial/ !! Além de explicar como funciona e detalhar os cálculos, o conteúdo publicado engloba algumas tabelas com os resultados pré-calculados, uma mão na roda. Wikipedia e outros sites complementaram as informações.

Conclusão

Para este momento, concluo apenas que a jornada do QR Code está apenas começando, mas seu objetivo não é apenas mostrar por dentro como a mágica foi feita, mas também abordar as escolhas feitas na codificação e ter mais um exemplo prático de orientação a objetos em AdvPL 😀

Agradeço novamente pela audiência e desejo a todos TERABYTES DE SUCESSO !!!

Referências

 

Algoritmos – Validação de CNPJ

Introdução

Continuando a sequência de funções de validação e dígitos verificadores, vamos ver o cálculo do dígito do CNPJ – Cadastro Nacional da Pessoa Jurídica.

Formato

O CNPJ é formado por 14 números, dos quais os 12 primeiros são usados para o cálculo do primeiro digito verificador, e o segundo dígito é calculado a partir dos 12 primeiros números mais o primeiro dígito verificador. Destes 12 números de dados, o último bloco de quatro números é usado para representar a matriz e/ou filiais da mesma empresa.O CNPJ é representado no formato “99.999.999/9999-99”.

Fonte AdvPL

O fonte abaixo mostra um exemplo de uso e o fonte do cálculo do dígito verificador do CNPJ, buscando o melhor nível de otimização.

#include 'protheus.ch'  

// -----------------------------------
// Referências
// https://www.geradorcnpj.com/algoritmo_do_cnpj.htm

User Function TSTCNPJ()
Local cCNPJ 
cCNPJ := '121213450001' // 12.121.345/0001-xx
conout(cCNPJ)
conout(DV_CNPJ(cCNPJ))
Return


/*  ------------------------------------------
Dígito verificador de CNPJ
Recebe o CNPJ como string de 12 números
Retorna os 2 digitos verificadores como string
------------------------------------------- */

STATIC Function DV_CNPJ(cCNPJ)
Local nI , nVL  
Local nM1 := 5, nM2 := 6
Local nDV1 := 0, nDV2 := 0

// Calculo dos valores dos digitos baseado 
// no fator multiplicativo de acordo com a posição 
// de cada dígito. 

For nI := 1 to 12
	nVL := val( substr(cCNPJ,nI,1) )
	nDV1 += (  nVL * nM1 )
	nDV2 += (  nVL * nM2 )
	nM1--           
	IF nM1 < 2 
		nM1 := 9
	Endif
	nM2--           
	IF nM2 < 2 
		nM2 := 9
	Endif
Next

// Cálculo do primeiro dígito
nDV1 := nDV1 % 11
IF nDV1 < 2 
	nDV1 := 0 
Else
	nDV1 := 11 - nDV1
Endif

// Cálculo do segundo digito 
nDV2 += ( nDV1 * 2 )  
nDV2 := nDV2 % 11
IF nDV2 < 2 
	nDV2 := 0 
Else
	nDV2 := 11 - nDV2
Endif

// Retorna os dígitos verificadors como String
Return chr(48+nDV1)+chr(48+nDV2)

Funcionamento

De forma muito parecida com todas as demais funções de validação, o cálculo principal se faz com o módulo 11 da somatória da multiplicação de cada dígito por um fator de peso. Você encontra a explicação completa e exemplificada nas referências no final do post.

Conclusão

Desejo a todos um bom proveito das funções publicadas, e TERABYTES DE SUCESSO 😀

Referências

 

Algoritmos – Validação de RG

Introdução

Continuando os algoritmos de validação e cálculo de dígitos verificadores, vamos ver agora o dígito verificador do RG – Documento de Identidade ou Registro Geral Brasileiro.

Cálculo

De forma similar aos demais, trata-se do complemento do módulo 11 com fator crescente iniciado em 2 no primeiro dígito (lado esquerdo) aplicado aos 8 dígitos do RG. O diferencial é que, caso o cálculo retorne 10 (dez), o dígito verificador é o caractere “x” (xis). Segue abaixo fonte de testes e implementação.

#include 'protheus.ch'

User Function TSTRG()
Local cRG := '24678131'
conout(cRG)                      
conout(DV_RG(cRG))
Return

// Validação RG ( 8 digitos + 1 verificador ) 
// Recebe string com 8 numeros do RG, sem pontuação
// Retorna o digito verificador como string
// O dígito pode ser de 0 a 9 ou "X" 
          
STATIC Function DV_RG(cRG)
Local nM := 2
Local nDV := 0 
Local nI,nVal

// Calcula o digito verificador baseado na soma
// dos digitos multiplicados pelo fator de acordo 
// com a posical do digito 
For nI := 1 to 8 
	nVal := val(substr(cRG,nI,1))
	nDV += ( nVal * nM )
	nM++	
Next

// Módulo 11 e complemento 
nDV := nDV % 11
nDV := 11 - nDV

// Regra final 
IF nDV = 10 
	Return 'X'
ElseIf nDV == 11
	nDV := 0
Endif

Return chr(48+nDV)

Conclusão

Depois de analisar algumas validações, vemos que todas são muito parecidas, algumas até idênticas, mas cada uma pode ter a sua particularidade ou comportamento diferenciado.

Espero que tenham gostado, e desejo a todos TERABYTES DE SUCESSO !!!

Referências

 

Bitmaps em AdvPL – Parte 04

Introdução

No post anterior, vimos como desenhar uma reta. Que tal ver por dentro como funciona um algoritmo de pintura ?

Pintura

A pintura de uma determinada área de um Bitmap parte do ponto que desejamos substituir uma determinada cor a partir de um ponto, onde todos os pontos adjacentes também tenham a sua cor substituída, se os pontos adjacentes ainda não estão da cor desejada, e são da mesma cor que o ponto inicial.

Por exemplo: Na imagem abaixo, eu quero pintar a meia-lua ou semi-círculo azul do lado esquerdo da cor branca. A imagem em azul é delimitada pelo losango em cor amarela. Logo, a cor azul somente deve ser trocada para branco dentro desta imagem, sem alterar demais pontos.

paint01

Quando usamos o Paint, e a ferramenta de pintura, selecionamos a cor branca e acionamos a pintura dentro da meia-lua azul do lado esquerdo, obtemos o seguinte resultado:

paint02

O mesmo resultado é obtido usando o método Paint() da classe ZBITMAP, informando a coordenada de um ponto qualquer dentro da área do semi-círculo, e setando a cor de pintura para branco.

Método Paint()

Sem maiores delongas, vamos ver como o algoritmo descobre o que pode e até onde pode ser mexido na imagem ao executar a pintura.

METHOD Paint(nL,nC,nColor)  CLASS ZBITMAP
Local nPColor 
Local aPaint := {}                   
Local nCurrent
Local nLNext, nCNext
Local nPL, nPC, nPColor

IF nColor = NIL 
	nColor := ::nFRColor
Endif

// Pega a cor do ponto atual 
nPColor := ::GetPixel(nL,nC)

aadd(aPaint,{nL,nC})

While len(aPaint) > 0 

	// Pega o primeiro ponto pendente
	nPL := aPaint[1][1]
	nPC := aPaint[1][2]

	// Remove das pendencias
	aDel(aPaint,1)
	aSize(aPaint,len(aPaint)-1)
	
	// Pega a cor desta coordenada
	nCurrent := ::GetPixel(nPL,nPC)
	
	If nCurrent <> nColor .and. nCurrent == nPColor

		// Se o ponto nao tem a cor final, e é da cor 
		// do ponto original, pinta ele 
		::SetPixel(nPL,nPC,nColor,1)      
		
		// ao pintar um ponto, seta os pontos adjacentes
		// como pendencias caso eles tambem precisem ser pintados
 
		// Ponto superior
		nLNext := nPL-1
		nCNext := nPC
		If nLNext >= 0 
			nCurrent := ::GetPixel(nLNext,nCNext)
			If nCurrent <> nColor .and. nCurrent == nPColor
				aadd(aPAint,{nLNext,nCNext}) 
			Endif
		Endif
		
		// Ponto inferior
		nLNext := nPL+1
		nCNext := nPC
		If nLNext < ::nHeight 
			nCurrent := ::GetPixel(nLNext,nCNext)
			If nCurrent <> nColor .and. nCurrent == nPColor
				aadd(aPaint,{nLNext,nCNext}) 
			Endif
		Endif

		// Ponto a direita
		nLNext := nPL
		nCNext := nPC+1
		If nLNext < ::nWidth
			nCurrent := ::GetPixel(nLNext,nCNext)
			If nCurrent <> nColor .and. nCurrent == nPColor
				aadd(aPaint,{nLNext,nCNext}) 
			Endif
		Endif

		// Ponto a esquerda
		nLNext := nPL
		nCNext := nPC-1
		If nLNext >= 0
			nCurrent := ::GetPixel(nLNext,nCNext)
			If nCurrent <> nColor .and. nCurrent == nPColor
				aadd(aPaint,{nLNext,nCNext}) 
			Endif
		Endif
		
	Endif

Enddo

Return

Observações

O método de pintura trabalha com uma lista de pontos pendentes de verificação. Cada ponto setado acrescenta os pontos adjacentes horizontais e verticais que são candidatos a serem setados, e cada iteração remove um ponto da pendência, pinta se necessário, e acrescenta os demais candidatos. Dessa forma, ele não ultrapassa o limite da parte da imagem a ser pintada, mesmo que a cor dos pontos desse limite sejam da mesma cor que está sendo usada na pintura.

Depois de criar o algoritmo e usá-lo, eu foi buscar e encontrei uma referência a esta técnica, chamada de “Flood Fill” — e descobri que eu implementei exatamente um pseudo-código alternativo usando lista de pendências — sem usar recursão. O pseudo-código é praticamente o que eu escrevi em AdvPL 😀 Para mais informações, veja as referências no final deste post.

Conclusão

Apenas setando um ponto, traçando retas e círculos, já podemos fazer coisas interessantes. Vai ficar mais interessante ainda quando eu acrescentar o que falta … 😀

Agradeço a todos novamente pela audiência, e desejo a todos TERABYTES DE SUCESSO !!!

Referências

 

 

Bitmaps em AdvPL – Parte 03

Introdução

No post anterior vimos como desenhar um círculo ponto a ponto. Agora, vamos ver como fazer uma reta. Horizontal e vertical é fácil, porém uma reta que ligue quaisquer pontos do gráfico exige alguns truques. Vamos ver a mágica por dentro.

Uma reta

Conceitualmente, o que nós vamos traçar no Bitmap é um segmento de reta – que liga suas coordenadas dentro da área do gráfico. Uma vez que representamos a imagem em pontos, devemos alterar a cor dos pontos que compõe esta reta. No mínimo precisamos saber as coordenadas dos pontos que serão conectados pelo segmento de reta, ou as suas “extremidades”. Como as coordenadas podem estar em linhas e colunas diferentes, calcular os pontos de plotagem da reta exige uma abordagem particular.

Exemplo

Queremos traçar uma reta entre as coordenadas 10,10 até a 110,60 — coordenadas expressas em linha,coluna. Partindo de uma imagem de 320 colunas x 200 linhas, esperamos o resultado abaixo:

reta01

Passos:

  • Determinar a maior distância entre os pontos. No caso, a distância entre as linhas (diferença da linha da coordenada final para a linha da coordenada inicial) são apenas 50 pontos (linha 10 até a linha 60), já a distância entre as colunas é de 100 pontos (coluna final menos inicial). Logo, serão necessários 100 pontos para traçar a reta.
  • Cada ponto a ser plotado deve partir da coordenada inicial, e a cada iteração do programa ele deve calcular e plotar o próximo ponto da sequência, até que o último ponto seja plotado na coordenada final do segmento de reta. Neste exemplo, serão necessárias 100 iterações.
  • Como cada novo ponto tem que estar mais para a direita do original, e mais para baixo, partimos de um loop de 100 iterações, onde a maior distância (horizontal) incrementará a coluna de plotagem a partir da origem em uma unidade.
  • Como a distância entre os pontos verticais (50) é menor que a distância maior (100), qual seria o meu “fator de incremento” para atualizar a coordenada da linhao ponto de origem para, após 100 iterações, ele seja igual ao ponto de destino?
Basta dividir a distância menor pela maior:
nPassoV := 50 / 100  // = 0.5
  • Logo, a cada iteração da plotagem, a próxima coluna de plotagem será a coluna atual mais uma unidade, e a linha será incrementada em apenas 0.5 . Desse modo, a cada dois pontos para a direita plotados, o próximo ponto desce uma unidade na vertical.
  • Reparem que os pontos de origem  e destino da reta formam um triângulo retângulo quando cruzamos a coluna do primeiro ponto com a linha do segundo ponto:

reta02

  • Sabemos que neste cenário a distância física entre os pontos da reta é maior que o tamanho horizontal. Aplicando o teorema de Pitágoras — “ Em qualquer triângulo retângulo, o quadrado do comprimento da hipotenusa é igual à soma dos quadrados dos comprimentos dos catetos. ” — descobrimos que a distancia entre os pontos é 111.80 (praticamente 112). Porém, como traçamos uma reta ponto a ponto, colocando pontos adjacentes mais para baixo conforme desenhamos a reta, precisamos de apenas 100 pontos.

Vamos ver agora em ZOOM a plotagem da reta:

reta03

Se contarmos os pontos, vamos ver que existem apenas 100 pontos ligando as extremidades deste segmento de reta.

Vamos ao fonte

METHOD Line(L1,C1,L2,C2,nColor,nPen) CLASS ZBITMAP
Local nDH , nDV 
Local nStepH , nStepV 
Local nPoints 
Local nRow,nCol

If nPen = NIL
  nPen := ::nPenSize
Endif

// Calcula as distancias entre os pontos
nDH := C2 - C1 
nDV := L2 - L1

nStepH := 1
nStepV := 1

// Calcula a maior distancia e o passo
// decimal
If abs(nDH) > abs(nDV)
  nStepV := nDV / nDH
ElseIf abs(nDV) > abs(nDH)
  nStepH := nDH / nDV
Endif

// Pontos que vao compor a reta
nPoints := Max(abs(nDV),abs(nDH))

// Traça a reta ponto a ponto , menos o ultimo
nRow := L1 
nCol := C1 
For nX := 0 to nPoints-1
  ::SetPixel(Round(nRow,1),round(nCol,1),nColor,nPen)
  nRow += nStepV
  nCol += nStepH
Next

// O ultimo ponto seta com as coordenadas informadas
// Pode haver perda de precisão na aritmética dos passos
::SetPixel(L2,C2,nColor)

Return

Conclusão

Não importa se o segmento de reta começa mais pra cima ou para baixo, esquerda ou direita. Os cálculos usam o valor absoluto (sempre positivos) para calcular as distâncias, e as diferenças reais das coordenadas para calcular os passos. A reta sempre será traçada do ponto da coordenada de origem ao ponto da coordenada de destino.

Espero que tenham gostado, e desejo a todos TERABYTES DE SUCESSO !!!

 

Bitmaps em AdvPL – Parte 02

Introdução

No post anterior, vimos a primeira versão de uma implementação para ler e modificar um arquivo Bitmap monocromático. Agora, vamos ver alguns detalhes de como a implementação foi projetada, e alguns aspectos de “como desenhar” no Bitmap em memória.

O array aMatrix

Criado como uma propriedade da classe ZBITMAP, ele é um array bidimensional de números inteiros. Ele é acessado por dois índices, o primeiro pelo número da linha, e o segundo pela coluna, e seu conteúdo é o número da cor correspondente daquele ponto (pixel) na imagem. Como a convenção adotada para a imagem começa na linha 0 e coluna 0, e os Arrays em AdvPL são “base 1” — o primeiro elemento é 1, e não 0 — os métodos que encapsulam o acesso somam 1 na linha e coluna.

// -------------------------------------------------
// Retorna a cor de um pixel 
// linha e coluna, base 0 
METHOD GETPIXEL(nRow,nCol) CLASS ZBITMAP
Return ::aMatrix[nRow+1][nCol+1]

Cada número corresponde a uma cor. Em um Bitmap monocromático, 0 (zero) corresponde a cor preta, e 1 (um) a cor branca. Já em um Bitmap de 16 cores — 4 bits per pixel — a cor branca é 15 (quinze), e as demais cores usadas estão em uma tabela gravada após o header do arquivo. Veja abaixo a representação da tabela de cores para um Bitmap

Desenhando um círculo

Já vamos começar do exemplo um pouco mais complexo — e ver que não é tão complexo assim. Para desenhar um círculo, é necessário saber no mínimo quais são as coordenadas do centro do círculo, e a medida do raio do círculo a ser usada. Com estas informações, e usando a constante PI ( 3.14159265 ), descobrir o tamanho da circunferência a ser traçada.

Uma vez que o raio é informado em pontos (ou pixels), a medida da circunferência também será em pixels. Por exemplo, para desenhar um círculo de raio 20 serão necessários pelo menos 126 pontos (125,663694 arredondado para cima), lembrando que a fórmula de cálculo da circunferência é 2 x Pi x R , onde R é o raio da circunferência, e Pi é a constante 3.14159265 — usada na implementação com 8 casas decimais de precisão.

Seno e Cosseno

Em um círculo, a partir de seu centro, ao traçar duas retas perpendiculares cruzando o centro do círculo, vamos obter quatro “quadrantes”. Em cada reta, que vamos chanar de eixos (x e y), usando as funções de Seno e Cosseno de um ângulo — representado em radianos (frações ou múltiplos de PI), obtemos em cada função ( Seno e Cosseno ) um número decimal entre 0 e 1 , que corresponde a posição de um ponto relativo no eixo entre o centro (0,0) e uma das extremidades — formadas com o cruzamento do círculo com o eixo. Veja o exemplo abaixo:

Seno-Cosseno

Para um ângulo a formado entre uma semi-reta — traçada a partir do centro do círculo — e o eixo X do lado direito do círculo trigonométrico — que corresponde ao ângulo 0 (Zero), em um determinado ponto esta semi-reta cruza com um ponto do círculo ( realçado na cor roxa). Para descobrirmos exatamente em que posição este ponto se encontra em relação ao ponto do centro do círculo, a função trigonométrica de Cosseno() — ou  COS() em AdvPL — retorna um número decimal entre 0 e 1, onde 0 é o centro do círculo e 1 é o ponto de cruzamento do eixo X com o círculo. A distância entre estes dois pontos é o raio da circunferência. Para descobrir o ponto relativo no eixo Y (vertical), usamos a função Seno() — ou SIN() em AdvPL.

Números e números

Um ângulo pode ser representado em graus, onde uma circunferência pode ser dividida em 360 graus e suas frações. Os ângulos também podem ser representados em radianos, que representam frações de PI. Uma circunferência possui 2 x PI ângulos. Logo, podemos converter ângulos entre graus e radianos, partindo da proporção que 360 graus está para 2 x PI como n Graus está para r Radianos.

Vamos por exemplo calcular a posição relativa de um ponto no círculo, que corresponde ao cruzamento de uma semi-reta em um círculo de 20 pontos de raio, formando um angulo de 30 graus entre a semi-reta e o eixo X. Primeiro, as funções SIN() e COS() — seno  e cosseno — em AdvPL recebem como parâmetro um ângulo em radianos. Logo:

20 graus = r radianos
360 graus = 2 x PI 

( multiplica em cruz ) 
360 x r = 2 x PI x 20 
r = ( 2 x 20 x PI) / 360
r = 0.34906585   // 30 graus em radianos

Cos(r) = 0.93969262 // posição no eixo X 
Sin(r) = 0.34202014 // posição no eixo Y

// Lembrando que a posição relativa parte que o centro é a posição 0
// e a extremidade é a posição 1, então precisamos multiplicar os 
// resultados pelo raio (20)

nX = 20 x 0.93969262 = 18.7938524 ( arredonda para 19 ) 
nY = 20 x 0.34202014 = 6.8404028  ( arredonda para 7 )  

// Logo, o ponto correspondente no círculo está na coordenada 7,19 
// em relação ao centro do círculo ( coordenada 0,0 ). Se o círculo está 
// traçado com o centro nas coordenadas 80,80 (linha,coluna), o ponto 
// está nas coordenadas 80-7 (73) e 80+19 (99)

Agora fica fácil

Determinado o comprimento do círculo em pontos, sabemos quantos “cruzamentos” serão necessários para desenhar os pontos que compõe a circunferência. Pegamos o valor de 2 x PI ( correspondendo aos ângulos de um circulo inteiro) e dividimos a quantidade de pontos a traçar por este valor, e chegamos a uma fração de ângulo, que ao ser usada como incremento a partir do ângulo zero, para cada ponto a ser traçado, corresponde ao ângulo deste ponto. Dessa forma, o método de traçar um círculo plotando ponto a ponto fica assim:

METHOD Circle(nL , nC , nRadius , nColor, nPen ) CLASS ZBITMAP
Local nRow , nCol
Local nAngle
Local nPoints
Local nStep
Local nI,nR

If nPen = NIL
	nPen := ::nPenSize
Endif

For nR := 1 to nPen
	// Seno e cosseno em Radianos
	// Faz o loop de 0 a 2*PI para calcular as coordenadas
	// dos pontos para desenhar o círculo
	nAngle := 0
	nPoints := 2 * PI * nRadius
	nStep   := (2*PI) / nPoints
	For nI := 0 to nPoints
		nRow := round(Sin(nAngle) * nRadius,1)
		nCol := round(Cos(nAngle) * nRadius,1)
		::SetPixel(nL-nRow,nC+nCol,nColor)
		nAngle += nStep
	Next
	nRadius--
Next

Return

Observações

As funções de Seno e Cosseno retornam também números negativos, cobrindo todos os quadrantes. Um ângulo entre 90 e 180 graus resulta em um ponto no quadrante superior esquerdo, entre 180 e 270 graus no quadrante inferior esquerdo, e entre 270 e 360 graus, no quadrante inferior direito.

Conclusão

Pode parecer um pouco complicado, mas depois que você entende as regras de uso das funções e como elas funcionam, o resultado não é complicado — apenas parece para quem não conhece.

Agradeço novamente a todos pela audiência e desejo a todos TERABYTES DE SUCESSO 😀

Referências

Bitmaps em AdvPL – Parte 01

Introdução

Um “bitmap”, como o próprio nome sugere, é um “mapa de bits”, uma foma relativamente simples de representar uma imagem bi-dimensional, desde uma imagem monocromática (pontos pretos e brancos) ate imagens coloridas com milhões de cores. Vamos ver algumas formas de manipular e mostrar estas imagens em AdvPL, e ver um pouco mais “por dentro” deste formato.

Funções e Classes AdvPL

Existem classes visuais do AdvPL que são capazes de mostrar uma imagem no formato BMP, entre outros formatos, no SmartClient, e funções dedicadas a converter imagens entre formatos, a seguir:

  • BMPTOJPG() – Função que permite converter uma imagem BMP para o formato JPG (ou JPEG).
  • Classe TBITMAP() – Cria um componente visual para visualização de imagens BMP — entre outros formatos — na interface do SmartClient.
  • Classe TBTNBMP() – Cria um botão com imagem em uma barra de botões
  • Classe TDRAWER() – Permite abrir e manipular visualmente uma imagem no SmartClient nos formatos BMP, JPG e PNG.
  • Classe TPAINTPANEL() – Permite a criação de um painel com sub-painéis e shapes para construção de imagens

Além das funções e classes acima, existem outras classes da interface visual e de impressão que podem se utilizar de imagens.

Estrutura interna do arquivo

Em linhas gerais, um arquivo no formato BMP possui um header (ou cabeçalho) com identificador de formato de tamanho variável para diferentes plataformas, e uma área de dados representada em um array de bits, onde cada linha deste array possui um formato determinado pela quantidade de cores da imagem, e adicionalmente pode conter um mapa de cores, entre outras informações adicionais.

Embora existam algumas formas de representação de Bitmaps que permitam compactação, este recurso é opcional, e imagens com representação de cores em 16 ou 32 bits são armazenadas sem compactação — o que torna arquivos com resolução relativamente baixa bem grandes. Por exemplo, um Bitmap com resolução de 800×600 pontos (ou pixels) True-Color (24 bits para cores) tem o tamanho aproximado de 1.37 MB, por isso outros formatos de arquivo de imagem usando algoritmos de compressão de imagem são mais usados que o formato BMP, principalmente para a WEB — como JPEG e PNG. Para mais referências e informações sobre o formato do arquivo, consulte as referências no final do post.

O aplicativo PaintBrush, da Microsoft, permite por exemplo criar e salvar uma imagem com uma cor (monocromática), 16 cores, 256 cores e TrueColor (24 bits, mais de 16 milhões de cores por ponto).

Lendo e interpretando um Bitmap

Devido ao formato relativamente simples de representação de uma imagem, é possível usar as funções de baixo nível de arquivos do AdvPL (FOpen,FRead,FSeek…) para abrir um arquivo neste formato e interpretar seu conteúdo. Isto pode ser muito útil inclusive quando surge a necessidade de representar a imagem de uma outra forma para, por exemplo, ser impressa em um periférico específico — como impressoras térmicas e/ou etiquetadoras.

ZLIB BITMAP

Para fins de estudo e posterior utilização prática, acabei criando uma classe em AdvPL para criar, ler e alterar arquivos no formato BMP. Usando as funções de baixo nível de arquivo do AdvPL, a classe ZBITMAP() por hora manipula apenas imagens monocromáticas. Ela permite abrir um arquivo Bitmap para Windows, identificar suas propriedades, representá-lo em um array bidimensional AdvPL — onde um determinado ponto (pixel) do arquivo pode ser acessado por sua coordenada [linha,coluna] e é representado em memória por um valor numérico — 0 para um pixel preto, e 1 para um pixel branco, onde as coordenadas 0,0 (linha,coluna) aponta para o ponto à esquerda da primeira linha de pontos da imagem.

Por hora os métodos disponíveis permitem criar uma imagem vazia, carregar uma imagem do disco, consultar e alterar a imagem ponto a ponto, inverter as cores da imagem ( efeito “negativo” – trocar preto por branco e vice-versa), setar ou consultar a cor padrão de fundo (default=1, branco), e salvar a imagem em arquivo. Vamos dar uma olhada na versão 1.0 do fonte.

#include 'protheus.ch'
#include 'zlib.ch'

/* ===========================================================================
Classe		ZBITMAP
Autor		Júlio Wittwer
Data		05/2019

Leitura e edição de Bitmap monocromático em AdvPL 

Referencia do formato do arquivo 
https://en.wikipedia.org/wiki/BMP_file_format
=========================================================================== */

CLASS ZBITMAP FROM LONGNAMECLASS

    DATA cFileName     // Nome do arquivo de imagem 
    DATA nFileSize     // Tamanho em bytes total do arquivo
    DATA nHeight       // Altura da imagem em pixels
    DATA nWidth        // largura da imagem em pixels
    DATA nBPP          // Bits por Pixel ( 1,4,...)
    DATA cERROR        // String com ultima ocorrência de erro da classe         
    DATA aMatrix       // Matrix de pontos do Bitmap ( cores ) 
    DATA cFormat       // Identificador do formato do arquivo 
    DATA nOffSet       // Offset de inicio dos dados 
    DATA nRawData      // Tamanho dos dados 
    DATA nRowSize      // Tamanho em bytes de uma linha da imagem 
    DATA nHeadSize     // Tamanho do Info Header 
    DATA nCompress     // Nível de compressão 
    DATA nColorPlanes  // Numero de planos de cor ( 1 ) 
    DATA nHRes         // Resolução horizontal 
    DATA nVRes         // Resolução vertical 
    DATA nColorPal     // Paleta de cores
    DATA nImpColors    // Cores importantes
    DATA nBgColor      // Cor de fundo ( default = branco ) 
    DATA nMargin       // Margem da imagem 
    
    METHOD New()           // Cria uma imagem vazia 
    METHOD LoadFromFile()  // Le imagem de arquivo 
    METHOD GetErrorStr()   // Recupera ultimo erro da classe
    METHOD Clear()         // Limpa a imagem ( preenche com a cor de fundo ) 
    METHOD SetPixel()      // Seta a cor de um ponto 
    METHOD GetPixel()      // Recupera a cor de um ponto 
    METHOD BgColor()       // Seta ou Recupera a cor de fundo 
    METHOD Negative()      // Inverte as cores da imagem ( = Negativo ) 
    METHOD SaveToFile()    // Salva a imagem em disco 

ENDCLASS


// Cria um objeto BMP
// Pode ser criado sem parâmetros, para carregar uma imagem do disco 
// Por default, cria uma imagem de 32x32

METHOD NEW(nWidth,nHeight,nBPP) CLASS ZBITMAP
Local nL
Local nC
Local aRow

IF nWidth = NIL
	::nWidth := 32
Else
	::nWidth := nWidth
Endif

If nHeight = NIL 
	::nHeight := 32
Else
	::nHeight := nHeight
Endif

If nBPP = NIL
	::nBPP := 1
Else
	::nBPP := nBPP
Endif

::cFileName := ''

If ::nBPP = 1
	::nBgColor := 1
Endif

aRow := {}

// Inicializa matrix
::aMatrix := {}
For nC := 1 to ::nWidth
	aadd(aRow,1)
Next
For nL := 1 to ::nHeight
	aadd(::aMatrix,aClone(aRow))
Next

::nOffSet    := 62
::nHeadSize  := 40
::nColorPlanes := 1
::nCompress  := 0
::nHRes      := 0
::nVRes      := 0
::nColorPal  := 0
::nImpColors := 0

// Tamanho calculado de cada linha em bytes
::nRowSize   := int( ( (::nBPP*::nWidth) + 31 ) / 32 ) * 4 

// Tamanho da area de dados da imagem 
::nRawData   := ::nRowSize  * ::nHeight

// Tamanho final da imagem 
::nFileSize :=   ::nRawData + ::nOffSet

Return self


// Carrega uma imagem BMP do disco 

METHOD LOADFROMFILE(cFile)  CLASS ZBITMAP
Local nH
Local cBuffer
Local cBinSize  
Local nBmpSize
Local nL , nC, nI
Local nByte
Local nReadOffset 
Local aRow := {}
Local cBits

::cFileName := cFile

nH := Fopen(cFile)

If  nH < 0 
	UserException("Fopen error ("+cValToChar(Ferror())+")")
Endif

::nFileSize := fSeek(nH,0,2)

fSeek(nH,0)

If ::nFileSize < 54
	::cError := "Invalid BMP -- file too small"
	Return .F.
Endif

// Aloca o Buffer e lê o arquivo intero 
// para arquivos maiores que 1MB, aumentar o tamanho 
// máximo de string no AdvPL -- MaxStringSize

cBuffer := space(::nFileSize) 
fRead(nH,@cBuffer,::nFileSize)
fClose(nH)

::cFormat := substr(cBuffer,0,2)

If ::cFormat <> "BM"
	::cError := "Unknow BMP Format ["+::cFormat+"]"
	Return .F.
Endif

cBinSize := substr(cBuffer,3,4)
nBmpSize := bin2l(cBinSize)

If ::nFileSize <> nBmpSize
	::cError := "BMP Incorrect Format -- File size mismatch"
	Return .F.
Endif


/*
                                                
Windows BITMAPINFOHEADER[1]

Offset (hex)	Offset (dec)	Size (bytes)	Information 
0E	14	4	the size of this header (40 bytes)
12	18	4	the bitmap width in pixels (signed integer)
16	22	4	the bitmap height in pixels (signed integer)
1A	26	2	the number of color planes (must be 1)
1C	28	2	the number of bits per pixel, which is the color 
                        depth of the image. 
                        Typical values are 1, 4, 8, 16, 24 and 32.
1E	30	4	the compression method being used. 
                        See the next table for a list of possible values
22	34	4	the image size. This is the size of the 
                        raw bitmap data; 
                        a dummy 0 can be given for BI_RGB bitmaps.
26	38	4	the horizontal resolution of the image. 
                        (pixel per metre, signed integer)
2A	42	4	the vertical resolution of the image. 
                        (pixel per metre, signed integer)
2E	46	4	the number of colors in the color palette, 
                        or 0 to default to 2n
32	50	4	the number of important colors used, or 0 
                        when every color is important; generally ignored

*/

::nOffSet    := bin2l(substr(cBuffer,11,4))
::nHeadSize  := bin2l(substr(cBuffer,15,4))
::nWidth     := bin2l(substr(cBuffer,19,4))
::nHeight    := bin2l(substr(cBuffer,23,4))
::nColorPlanes := bin2i(substr(cBuffer,27,2))
::nBPP       := bin2i(substr(cBuffer,29,2))
::nCompress  := bin2l(substr(cBuffer,31,4))
::nRawData   := bin2l(substr(cBuffer,35,4))
::nHRes      := bin2l(substr(cBuffer,39,4))
::nVRes      := bin2l(substr(cBuffer,43,4))
::nColorPal  := bin2l(substr(cBuffer,47,4))
::nImpColors := bin2l(substr(cBuffer,51,4))

// Tamanho calculado de cada linha em bytes
::nRowSize   := int( ( (::nBPP*::nWidth) + 31 ) / 32 ) * 4 

// Leitura dos dados na matriz

::aMatrix := {}


For nL := 0 to ::nHeight-1
	nReadOffset := ::nOffSet + ( ::nRowSize * nL ) + 1
	For nC := 0 to ::nWidth-1
		// Le o byte e "inverte", o BMP monocromatico 
		// usa "0" para cor preta e "1" para branco  
		nByte := asc(substr(cBuffer,nReadOffset,1))
		If ::nBPP == 1 
			// Bitmap monocromatico 
			// um bit por pixel, converte para binario direto 
			// 0 = preto, 1 = branco
			cBits := NTOBIT8(nByte) 
			For nI := 1 to 8
				aadd(aRow,IIF(substr(cBits,nI,1)=='0',0,1))
			Next
		Else	 
			UserException("Unsupported ("+cValToChar(::nBPP)+/
") Bytes per Pixel")
		Endif
		nReadOffset++
	Next
	aSize(aRow,::nWidth)
	aadd(::aMatrix,aClone(aRow))
	aSize(aRow,0)
Next

Return .T.

// Recupera a última informação de erro da Classe

METHOD GetErrorStr()  CLASS ZBITMAP 
Return ::cError

// Limpa a imagem preenchendo os pontos com 
// a cor de fundo 

METHOD Clear() CLASS ZBITMAP
Local nL, nC
If ::aMatrix != NIL
	For nL := 1 to ::nHeight
		For nC := 1 to ::nWidth
			::aMatrix[nL][nC] := ::nBGColor 
		Next
	Next
Endif
Return

// -------------------------------------------------
// Seta um pixel com uma cor em uma coordenada 
// linha e coluna, base 0 
METHOD SETPIXEL(nRow,nCol,nColor) CLASS ZBITMAP
::aMatrix[nRow+1][nCol+1] := nColor
Return
              
// -------------------------------------------------
// Retorna a cor de um pixel 
// linha e coluna, base 0 
METHOD GETPIXEL(nRow,nCol) CLASS ZBITMAP
Return ::aMatrix[nRow+1][nCol+1]

// -------------------------------------------------
// Retorna a cor de fundo da imagem 
// se informado um parametro, seta a cor de fundo 
METHOD BgColor(nSet) CLASS ZBITMAP
If pCount()>0
	::nBgColor := nSet
Endif
Return ::nBgColor

// --------------------------------------------------------
// Faz o "negativo" da imagem 
// Por hora, imagem preta e branca, apenas inverte as cores

METHOD Negative()  CLASS ZBITMAP
Local nL, nC
If ::aMatrix != NIL            
	IF ::nBPP = 1	
		// Imagem monocromatica
		For nL := 1 to ::nHeight
			For nC := 1 to ::nWidth
				::aMatrix[nL][nC] := 1-::aMatrix[nL][nC]
			Next
		Next
	Endif
Endif
Return


// 
// Salva o arquivo em disco 
METHOD SaveToFile(cFile)  CLASS ZBITMAP
Local nH
Local cHeader := ''
Local cHeadInfo := ''

If ::nBPP <> 1	
	UserException("Format not implemented (yet) to save")
Endif

nH := fCreate(cFile)
           
cHeader := "BM"               // 2 bytes
cHeader += L2bin(::nFileSize) // 4 bytes 
cHeader += chr(0)+chr(0)      // 2 bytes
cHeader += chr(0)+chr(0)      // 2 bytes
cHeader += L2bin(::nOffset)   // 4 bytes

// Grava o primeiro Header
fWrite(nH,cHeader,len(cHEader)) // 14 bytes 

cHeadInfo += L2bin(40)           // This Header Size
cHeadInfo += L2bin(::nWidth)     // 
cHeadInfo += L2bin(::nHeight)    // 
cHeadInfo += I2Bin(1)            // Color planes
cHeadInfo += I2Bin(::nBPP)       // Bits Per Pixel
cHeadInfo += L2bin(0)            // Compression Method ( 0 = No Compression ) 
cHeadInfo += L2bin(::nRawData)   // RAW Data Size 
cHeadInfo += L2bin(::nHRes)      // Resolucao Horizontal
cHeadInfo += L2bin(::nVRes)      // Resolucao vertical
cHeadInfo += L2bin(::nColorPal)  // Color Pallete
cHeadInfo += L2bin(::nImpColors) // Important Colors

// Ultimos 8 bytes do header para BMP Monocromatico

cHeadInfo += chr(0)+chr(0)+chr(0)+chr(0)
cHeadInfo += chr(255)+chr(255)+chr(255)+chr(0)

fWrite(nH,cHeadInfo,len(cHeadInfo))

For nL := 1 to ::nHeight
	cBinRow := ''
	cBitRow := ''
	For nC := 1 to ::nWidth
		cBitRow += chr( 48 + ::aMatrix[nL][nC])
	Next
	While len(cBitRow)%32 > 0
		// Padding Bits to 32 ( 4 bytes ) 
		cBitRow += '1'
	Enddo            
	For nC := 0 to len(cBitRow)-1 STEP 8
		cByteBit := substr(cBitRow,nC+1,8)
		nByte := BitsToN(cByteBit)
		cBinRow += chr(nByte)
	Next
	while len(cBinRow) < ::nRowSize
		// Padding Bytes ( ASCII 0 )
		cBinRow += chr(0)
	Enddo
	fWrite(nH,cBinRow)
Next

fClose(nH)

Return .T.

Conclusão

Ainda estou acrescentando alguns recursos interessantes na classe, assim que ela chevar na versão 1.1, no próximo post vamos ver um exemplo de uso !

Agradeço desde já a audiência, e desejo a todos TERABYTES DE SUCESSO !!!

Referências