O objetivo deste trabalho é implementar, na linguagem C, duas funções
(utf8_16
e utf16_8
), que recebem
como entrada um arquivo contendo um texto codificado em um formato UNICODE
(UTF-8 ou UTF-16) e geram como saida um arquivo contendo o mesmo texto,
codificado no outro formato.
Leia com atenção o enunciado do trabalho e as instruções para a entrega.
Em caso de dúvidas, não invente. Pergunte!
Em computação, caracteres de texto são tipicamente representados por códigos especificados por algum padrão de codificação. Um padrão bastante conhecido é a codificação ASCII, que utiliza valores inteiros de 0 a 127 para representar letras, dígitos e alguns outros simbolos. Algumas extensões dessa codificação utilizam também a faixa de valores de 128 a 255 para representar, por exemplo, caracteres acentuados e alguns outros símbolos adicionais.
A codificação ASCII e outros padrões de codificação que utilizam códigos de um único byte são limitados à representação de apenas 256 símbolos diferentes. Para permitir a representação de um conjunto maior de caracteres foi criada, no final da década de 1980, a codificação UNICODE. A versão corrente dessa codificação é capaz de representar os caracteres utilizados por todos os idiomas conhecidos, além de diversos outros símbolos.
Cada caractere em UNICODE é associado a um código na faixa de 0 a 0x10FFFF, o que permite a representação de 1.114.112 símbolos diferentes. Na notação adotada pelo padrão UNICODE, U+xxxx identifica o código com valor hexadecimal xxxx. Por exemplo, o código U+00A9 (o código do símbolo ©) corresponde ao valor hexadecimal 0x00A9.
Existem algumas formas diferentes de codificação de caracteres UNICODE.
Para este trabalho, as codificações de interesse são a UTF-8
e a UTF-16.
Uma característica importante é que a codificação UTF-8 é compatível com o padrão ASCII, ou seja, os 128 caracteres associados aos códigos de 0 a 0x7F em ASCII tem a mesma representação (em um único byte) em UTF-8.
A tabela a seguir indica para cada faixa de valores de códigos UNICODE o número de bytes necessários para representá-los e a codificação usada para essa faixa.
Código UNICODE | Representação UTF-8 (byte a byte) |
---|---|
U+0000 a U+007F | 0xxxxxxx |
U+0080 a U+07FF | 110xxxxx 10xxxxxx |
U+0800 a U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
U+10000 a U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Note que:
11000010 10101001 = 0xC2 0xA9
O primeiro byte começa com 110, indicando que a sequência é composta por dois bytes. A seguir vêm os cinco primeiros bits do código UNICODE (note o preenchimento com zeros à esquerda para completar a porção do código do caractere colocada no primeiro byte da sequência).
O segundo byte começa com 10, indicando que é um byte de continuação. A seguir vêm os próximos seis bits do código UNICODE.
11100010 10001001 10100000 = 0xE2 0x89 0xA0
O primeiro byte começa com 1110, indicando que a sequência é composta por três bytes. A seguir vêm os quatro primeiros bits do código UNICODE
O segundo e o terceiro bytes começam com 10, indicando que são bytes de continuação. Cada um deles tem, em seguida, os próximos seis bits do código UNICODE.
Códigos UNICODE na faixa U+0000 a U+FFFF são representados em UTF-16 pelo seu próprio valor numérico (em 16 bits).
Exemplos:
Códigos UNICODE na faixa U+10000 a U+10FFFF são codificados em um par de code units, sendo cada elemento desse par um valor inteiro de 16 bits no intervalo [0xD800, 0xDFFF].
A codificação UTF-16 leva em consideração o fato de que não há caracteres UNICODE válidos com código no intervalo [0xD800, 0xDFFF], por isso não há risco de confundir os elementos de um par de code units com um código UNICODE válido da faixa U+0000 a U+FFFF.
Para encontrar o par de code units para um caractere UNICODE na faixa U+10000 a U+10FFFF é executado o seguinte algoritmo:
Podemos também ver a composição dos dois elementos do code unit a partir do resultado da subtração do passo 1 da seguinte forma:
Primeiro code unit | 1101 10xx xxxx xxxx |
Segundo code unit | 1101 11xx xxxx xxxx |
onde os "x" do primeiro code unit são os primeiros 10 bits do resultado da subtração, e os "x" do segundo code unit são os próximos 10 bits do resultado da subtração.
Exemplo:
Consideramos apenas 20 bits do resultado: 0000110100 0100011110.
Os 10 bits mais significativos (0000110100 = 0x0034), somados a 0xD800 fornecem o primeiro code unit: 0xD834.
Os 10 bits menos significativos (0100011110 = 0x011E), somados a 0xDC00 fornecem o segundo code unit: 0xDD1E.
O BOM funciona como uma assinatura do arquivo: além de identificar o conteúdo do arquivo como UNICODE, ele também define a ordem de armazenamento do arquivo (big-endian ou little-endian). Como a ordem de armazenamento não faz sentido para arquivos UTF-8, o BOM é raramente usado no início de arquivos UTF-8, e seu uso não é recomendado pelo padrão. Além disso, muitas aplicações que trabalham com UTF-8 não dão suporte a um BOM no inicio do arquivo.
A sequência exata de bytes que corresponde ao caractere BOM inserido no início do arquivo depende da codificação empregada e da ordem de armazenamento do arquivo. Para a codificação UTF-16, temos:
Tipo do Arquivo | Bytes |
---|---|
UTF-16, big-endian | FE FF |
UTF-16, little-endian | FF FE |
O objetivo deste trabalho é implementar, na linguagem C, as funções
utf8_16
e utf16_8
, que realizam
a conversão de um formato UNICODE
(UTF-8 ou UTF-16) para o outro formato.
utf8_16
deve ler o conteúdo de um arquivo de
entrada (um texto codificado em UTF-8) e gravar em um arquivo de saída esse
mesmo texto, codificado em UTF-16,
com ordenação BIG-ENDIAN .
O arquivo de entrada UTF-8 não terá um caracter BOM inicial; já o arquivo de saída, UTF-16, deverá necessariamente conter o BOM inicial.
O protótipo (cabeçalho) da função utf8_16
é o
seguinte:
int utf8_16(FILE *arq_entrada, FILE *arq_saida);
Os dois parâmetros da função são dois arquivos abertos em modo binário:
o arquivo de entrada (arq_entrada
) e o arquivo de
saída (arq_saida
).
O valor de retorno da função utf8_16
é 0, em caso de
sucesso e -1, em caso de erro de E/S.
Em caso de erro,
a função deve emitir, na saída de erro (stderr),
uma mensagem indicando qual o tipo de erro ocorrido (leitura ou gravação)
e retornar imediatamente.
Por simplicidade, você pode considerar que os arquivos de entrada sempre conterão um texto CORRETAMENTE CODIFICADO. Dessa forma, você não precisa implementar o tratamento de erros de codificação do arquivo de entrada.
utf16_8
deve ler o conteúdo de um arquivo de
entrada (um texto codificado em UTF-16
com ordenação BIG-ENDIAN)
e gravar em um arquivo de saída esse
mesmo texto, codificado em UTF-8.
O arquivo de entrada UTF-16 terá um BOM inicial, mas o arquivo de saída UTF-8 não deverá conter um BOM inicial.
O protótipo da função é o seguinte:
int utf16_8(FILE *arq_entrada, FILE *arq_saida);
Os parâmetros da função são dois arquivos abertos em modo binário:
o arquivo de entrada (arq_entrada
) e o arquivo de
saída (arq_saida
).
A função deve inspecionar os primeiros dois bytes do conteúdo do arquivo de entrada (UTF-16) para verificar se eles contêm um BOM válido!
Assim como na função anterior, o valor de retorno é 0, em caso de sucesso e -1, em caso de erro. Os procedimentos para os casos de erro são:
Por simplicidade, você pode considerar que os arquivos de
entrada sempre conterão um texto CORRETAMENTE CODIFICADO.
Apenas o caso de BOM inválido (ou ausente) no início do arquivo precisa
ser tratado.
Você deve criar um arquivo fonte chamado
conv_utf.c
contendo as duas funções descritas acima,
e funções auxiliares, se for o caso.
Esse arquivo não
deve conter uma função main
!
O arquivo conv_utf.c
deverá incluir o arquivo
de cabeçalho
conv_utf.h
,
fornecido aqui.
Para testar seu programa, crie um outro arquivo, por exemplo,
teste.c
,
contendo uma função main
.
Crie seu programa executável, por exemplo teste
, com a linha:
gcc -Wall -o teste conv_utf.c teste.c
Tanto o arquivo conv_utf.c
como
teste.c
devem conter a linha:
#include "conv_utf.h"
Implemente seu trabalho por partes, testando cada parte implementada antes de prosseguir.
Por exemplo, você pode implementar primeiro a leitura de um arquivo UTF-8, decodificando os caracteres para códigos UNICODE de 32 bits e exibindo esses valores na tela. Quando essa parte estiver funcionando, implemente a codificação UTF-16 sobre os valores, gerando um arquivo de saída.
Para verificar o conteúdo do arquivo gravado, você pode usar o utilitário
hexdump
. Por exemplo, o comando
hexdump -C <nome-do-arquivo>
exibe o conteúdo do arquivo especificado byte a byte, em hexadecimal
(16 bytes por linha). A segunda coluna de cada linha (entre '|') exibe
os caracteres ASCII correspondentes a esses bytes, se eles existirem.
Experimente inspecionar os arquivos dados como exemplo com o hexdump...
Para abrir um arquivo para gravação ou leitura em formato binário,
use a função
FILE *fopen(char *path, char *mode);
descrita em stdio.h
. Seus argumentos são:
path
: nome do arquivo a ser aberto
mode
: uma string que, no nosso caso, será "rb"
para abrir o arquivo para leitura em modo binário ou "wb"
para abrir o arquivo para escrita em modo binário.
Para fazer a leitura e gravação do arquivo, uma sugestão é
pesquisar as funções fwrite
/fread
e
fputc
/fgetc
.
Deverão ser entregues via Moodle dois arquivos:
conv_utf.c
, contendo as
funções utf8_16
e
utf16_8
(e funções auxiliares, se for o caso).
main
, e só
deve incluir o arquivo de cabeçalho conv_utf.h
e
arquivos de cabeçalho da biblioteca padrão de C.
/* Nome_do_Aluno1 Matricula Turma */ /* Nome_do_Aluno2 Matricula Turma */
relatorio.txt
,
contendo um pequeno
relatório.