Avançar para o conteúdo
Imagem com logotipo, contendo link para a página inicial
  • United Stated of America flag, representing the option for the English language.
  • Bandeira do Brasil, simbolizando a opção pelo idioma Português do Brasil.

Aprenda Programação: Arquivos e Serialização (Marshalling)

Exemplos de criação de um arquivo texto em quatro linguagens de programação: Python, Lua, GDScript e JavaScript.

Créditos para a imagem: Imagem criada pelo autor usando o programa Spectacle.

Pré-Requisitos

Na introdução sobre ambientes de desenvolvimento, indiquei Python, Lua e JavaScript como boas escolhas de linguagens de programação para iniciantes. Posteriormente, comentei sobre GDScript como opção para pessoas que tenham interesse em programar jogos digitais ou simulações. Para as atividades de introdução a programação, você precisará de, no mínimo, um ambiente de desenvolvimento configurado em uma das linguagens anteriores.

Caso queira experimentar programação sem configurar um ambiente, você pode usar um dos editores online que criei:

Contudo, eles não possuem todos os recursos dos interpretadores para as linguagens. Assim, cedo ou tarde, você precisará configurar um ambiente de desenvolvimento. Caso precise configurar um, confira os recursos a seguir.

Assim, se você tem um Ambiente Integrado de Desenvolvimento (em inglês, Integrated Development Environment ou IDE) ou a combinação de editor de texto com interpretador, você está pronto para começar. Os exemplos assumem que você saiba executar código em sua linguagem escolhida, como apresentado nas páginas de configuração.

Caso queira usar outra linguagem, a introdução provê links para configuração de ambientes para as linguagens C, C++, Java, LISP, Prolog e SQL (com SQLite). Em muitas linguagens, basta seguir os modelos da seção de experimentação para adaptar sintaxe, comandos e funções dos trechos de código. C e C++ são exceções, por requerem o uso de ponteiros para acesso à memória.

Memória Secundária, Sistemas de Arquivos e Arquivos

Alguns dos primeiros tópicos deste material definiram sistemas de arquivos. O primeiro dos tópicos sobre sistemas de arquivos definiu arquivos, diretórios (pastas) e caminhos (caminhos podem ser relativos ou absolutos). Para o uso de arquivos em programação, é importante entender sobre os conceitos de caminhos, caminhos relativos e caminhos absolutos. Em programação, normalmente é preferível usar arquivos com caminhos relativos, pois eles facilitam usar arquivos com uma mesma estrutura de diretórios em diferentes máquinas. Assim, caso você não sabia o que são caminhos ou um diretório de trabalho (work directory ou work dir), a leitura do tópico mencionado é recomendada para entender este tópico.

O segundo tópico sobre sistemas de arquivos descreveu gerenciadores de arquivos, programas que permitem realizar operações comuns para criação, organização e manipulação de arquivos e diretórios. Em particular, um gerenciador de arquivos será necessário neste tópico, para que você possa acessar o conteúdo que criar.

Linguagens de programação também fornecem recursos e abstrações para manipular arquivos. Arquivos são abstrações da memória secundária (persistente) de uma máquina. Pode-se criar um novo arquivo (vazio), escrever dados neles e salvá-lo no disco. Um arquivo salvo pode ser carregado e lido. Ele também pode ser modificado, realizando-se novas operações de escrita. Com as operações anteriores, é possível salvar dados em uma sessão do programa para carregá-los em qualquer momento futuro no qual os dados armazenados sejam necessários.

Contudo, nem todo arquivo é manipulado da mesma forma. Linguagens de programação costumam fornecer dois tipos de arquivos:

  1. Arquivos texto (ou arquivos de texto ou text files);
  2. Arquivos binário (ou binary files).

Arquivos texto são arquivos que armazenam dados codificados como texto. Por exemplo, todos os arquivos de código-fonte criados para JavaScript, Lua, Python e GDScript são arquivos texto. Você pode criar, abrir e modificar arquivos texto usando um editor de texto. Arquivos texto são convenientes para uso e leitura por seres humanos, mas requerem mais bytes para armazenamento (por exemplo, o número inteiro 01234 é salvo como o texto "01234", que requer, por exemplo, 5 bytes para a escrita em ASCII ou UTF-8), tempo para processamento e permitem acesso seqüencial (isto é, a leitura é feita byte a byte).

Arquivos binários são arquivos que armazenam dados codificados pelos tipos os quais representam. Por exemplo, o número inteiro 01234 seria salvo como a seqüencia de bytes 00000100 11010010 (que pode ser escrita em 2 bytes). Para ler e editar um arquivo binário, usa-se um editor hexadecimal (hexadecimal editor ou hex editor) ao invés de um editor de texto. Arquivos binários tendem a ser menores (tamanho em bytes) que arquivos texto, mais rápidos para escrita e leitura, e permitem acesso aleatório (isto é, a leitura de uma posição específica do arquivo), porém não são acessíveis para seres humanos. A maioria dos arquivos de imagens, vídeos e áudio são exemplos típicos de arquivos binários. Embora seja possível abri-los em um editor de texto, o resultado não será o esperado. De fato, a manipulação e o uso eficiente de arquivos binários costumam requerer programas especializados. Por exemplo, um visualizador ou editor de imagens, ou um reprodutor (player) ou editor de áudio ou vídeo.

Qualquer que seja o caso, todo programa que retém informações entre usos utiliza arquivos. Mesmo implementações de sistemas gerenciadores de bancos de dados (SGDBs) utilizam arquivos internamente. Assim, convém aprender a usar arquivos.

Antes, contudo, um aviso é necessário.

AVISO IMPORTANTE

Antes de executar os exemplos deste tópico, é importante saber que operações com arquivos podem levar a perda de dados. Em particular, a criação de um novo arquivo pode apagar o conteúdo de um arquivo existente no diretório escolhido com o mesmo nome. Da mesma forma, modificar o conteúdo de um arquivo existente é uma operação persistente, ou seja, as alterações serão permanentes. Portanto, é preciso cuidado ao criar novos arquivos ou modificar arquivos existentes. Antes de abrir um arquivo, verifique se você não possui um arquivo com o mesmo nome e extensão no diretório utilizado (para evitar perdas de dados).

O autor desta página não se responsabiliza por dados perdidos (como será mencionado em breve, ele tentará, inclusive, minimizar os riscos). A leitora ou o leitor é responsável pelos arquivos armazenados em seu sistema. Arquivos importantes sempre devem ter cópias de segurança (back-ups) e/ou versionamento.

O aviso pode ser assustador, mas é importante. Como arquivos utilizam memória secundária, os resultados de operações são persistentes. Assim, é importante tomar os devidos cuidados para se evitar perda indesejada de dados.

Para minimizar chances de conflitos de nomes, todos os arquivos criados terão o prefixo franco-garcia-. Por exemplo, franco-garcia-meu_arquivo.txt ao invés de meu_arquivo.txt ou meu arquivo.txt. Todos os arquivos serão criados no diretório de trabalho, que provavelmente será o mesmo no qual o arquivo com o código-fonte for salvo e/ou o código for interpretado. Exceto caso você também chame Franco Garcia e/ou crie arquivos usando o mesmo prefixo e convenção, as chances de ter arquivos com o mesmo nome serão baixas. Entretanto, elas existem e requerem cuidados de sua parte. Caso você precise modificar os nomes, o caminho (nome e diretório) do arquivo a ser utilizado estará definido variável no início do código de cada programa (mas que pode estar após definições de subrotinas, constantes e registros), chamada caminho_arquivo (no caso do exemplo para cópia, também existirá uma variável chada caminho_copia). Preferencialmente, escolha um nome único, para não correr o risco de sobrescrever um arquivo existente.

Uma boa prática para atividades introdutórias é criar um novo diretório em seu computador para usar como diretório de trabalho para os programas. Todos os arquivos que você criar ou ler devem pertencer a este diretório. Outro benefício da criação de um diretório é que você terá certeza que o local escolhido permite operações de escrita. Sistemas operacionais podem restringir o acesso a arquivos e diretórios, assim como operações de leitura e escrita, para certas combinações de conta de usuário e diretório. Isso é chamado de permissões de acesso. Sempre que se trabalhar com arquivos, é necessário que a conta atual tenha permissão de leitura para o diretório e arquivo (caso se deseje ler um arquivo), e/ou permissão de escrita para o diretório e arquivo (caso se queira criar um novo arquivo, escrever nele ou modificar um arquivo existente). Erros de permissão são comuns, especialmente em máquinas compartilhadas. Assim, se seu código estiver correto, mas um programa não funcionar, verifique se você possui permissões suficientes para o caminho definido para o arquivo.

Arquivos Texto (Text Files)

Arquivos texto tendem a ser mais simples de operar que arquivos binários. Isso ocorre por alguns motivos:

  1. Pode-se inspecionar o conteúdo de um arquivo texto usando-se um editor de textos;
  2. Pode-se modificar o conteúdo de um arquivo texto usando um editor de texto;
  3. Existem diversas ferramentas de linha de comando para manipulação de arquivos texto. De fato, sistemas baseados em Unix (como Linux) utilizam primariamente arquivos texto para configuração e troca de dados em programas de linha de comando;
  4. Em programação, operar com arquivos textos é similar a usar subrotinas ou comandos como print() e input().

O último motivo pode parecer estranho, mas é aplicável a muitas linguagens de programação. Muitas linguagens abstraem operações em consoles (terminais) usando arquivos. Em linguagens assim, existem três arquivos com nomes e propósitos especiais:

  1. Saída padrão (standard output), mais conhecida como stdout;
  2. Entrada padrão (standard input), mais conhecida como stdin;
  3. Saída de erro padrão (standard error), mais conhecida como stderr.

O uso de print() ou console.log() redireciona a mensagem escrita para stdout, que, então, é escrito em um terminal. O uso de input() ou io.read() e similares usa stdin como um buffer (memória) de entrada, usado para armazenar temporariamente valores lidos do teclado. É por isso que algumas subrotinas ou comandos de leitura (como io.read() em Lua) fornecem valores incorretos após a leitura de certos tipos de dados -- restos de dados lidos podem ficar armazenados no arquivo stdin, sendo fornecidos para a próxima leitura. Embora, no caso de Lua, a razão seja o uso de uma subrotina C usada para a leitura pela linguagem.

A saída de erro padrão não foi abordada em tópicos anteriores, mas, a partir deste momento, você pode começar a usá-la em seus programas. Por exemplo:

console.error("Mensagem de erro")
// JavaScript também possui uma mensagem de aviso:
console.warn("Mensagem de aviso")
import sys

print("Mensagem de erro", file=sys.stderr)

# Python também possui uma mensagem de aviso:
import warnings
warnings.warn("Mensagem de aviso")
io.stderr:write("Mensagem de erro")
error("Mensagem de erro")
extends Node

func _ready():
    printerr("Mensagem de erro")
    # JavaScript também possui uma mensagem de aviso:
    push_warning("Mensagem de aviso")

Documentações (as versões para aviso podem usar outro arquivo de saída -- por exemplo, stdout ou algum outro):

Os exemplos anteriores ilustram o que fazer para escrever em outro arquivo: deve-se designá-lo de alguma forma para indicar em qual arquivo a saída deverá ocorrer. O mesmo princípio vale para arquivos criados pela programadora ou pelo programador. A diferença é que, neste caso, a pessoa deverá realizar, no mínimo, duas tarefas adicionais: abrir (ou criar um arquivo) antes do uso e fechar o arquivo aberto quando ele não for mais necessário.

Operações Básicas Usando Arquivos Texto

Existem três formas principais de usar um arquivo:

  1. Usar um arquivo para escrita de dados;
  2. Usar um arquivo para leitura de dados;
  3. Usar um arquivo para leitura e escrita de dados.

Nos exemplos a seguir, a primeira subseção criará um arquivo de texto com cinco linhas.

Olá, meu nome é Franco.
Olá, meu nome é Franco.
1 2 3
-1.23

As duas primeiras linhas são frases. A terceira linha contém três números inteiros (que serão armazenados como texto). A quarta linha contém um número real, que será armazenado como texto. A quinta linha é uma linha vazia. Infelizmente, ela não aparece no código formatado em HTML.

Em seguida, criar-se-á um programa para ler o conteúdo escrito.

Toda operação de arquivo segue um mesmo modelo (padrão):

  1. Abertura ou criação de um arquivo;
  2. Operações usando o arquivo;
  3. Fechamento do arquivo.

Um arquivo pode ser fechado quando não for mais necessário. Isso pode ser feito imediatamente após o término de todas as operações de leitura e/ou escrita, ou antes do final do programa. Contudo, não se deve usar um arquivo após fechá-lo; isso é um erro. Para usar um arquivo fechado, deve-se abri-lo novamente, repetindo-se o processo.

Além disso, é importante fechar o arquivo. Algumas implementações podem fazer isso automaticamente; pessoalmente, eu prefiro sempre fazê-lo. O fechamento do arquivo é importante para garantir que todos os dados sejam salvos. Por questões de desempenho, algumas implementações agrupam operações de escrita antes de salvá-las no arquivo. O fechamento força um despejo de memória (flush). Existem subrotinas para realizar um flush no momento desejado; elas devem ser usadas apenas para arquivos de saída (comumente chamados de fluxos de saída ou output streams).

Arquivos texto também são comumente usados para tarefas de logging, normalmente para fins de depuração. Pode-se armazenar mensagens ou valores de interesse em um arquivo para inspecioná-los em caso de problemas ou travamentos de um programa. Algumas linguagens de programação fornecem implementações para log na biblioteca padrão. Caso elas não existam, é comum existirem bibliotecas que forneçam a funcionalidade. Caso contrário, basta criar um arquivo de texto e mantê-lo aberto durante o uso do programa. Quando se quiser persistir uma mensagem, basta usá-lo. Antes do final do programa, fecha-se o arquivo.

Escrita de Arquivo Texto

Para escrever em um arquivo texto, deve-se converter os dados desejados para uma cadeia de caracteres. Algumas implementações fazem conversões de tipos automaticamente; caso uma não fizer, basta fazer a conversão antes. Para melhor desempenho, costuma ser melhor armazenar o todo conteúdo em uma cadeia de caracteres para se escrever de uma única vez. Uma variável utilizada assim é chamada de buffer.

Embora seja possível usar JavaScript fora de navegadores, o exemplo a seguir considera o uso da linguagem em navegadores. Por isso, ele será um pouco diferente das versões escritas para Python, Lua e GDScript, que são comumente usadas fora de um navegador.

let caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
let conteudo = "Olá, meu nome é Franco.\n"
conteudo += "Olá, meu nome é Franco.\n"
conteudo += "1 2 3\n"
conteudo += "-1.23\n"

let arquivo = new File([conteudo], caminho_arquivo, {type: "text/plain"})
console.log("Arquivo criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    arquivo = open(caminho_arquivo, "w")

    arquivo.writelines([
        "Olá, meu nome é Franco.\n",
        "Olá, meu nome é Franco.\n",
        "1 2 3\n",
        "-1.23"
    ])

    arquivo.close()

    print("Arquivo criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
-- <https://en.cppreference.com/w/c/program/EXIT_status>
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

arquivo:write("Olá, meu nome é Franco.\n")
arquivo:write("Olá, meu nome é Franco.\n")
arquivo:write("1 2 3\n")
arquivo:write("-1.23\n")

io.close(arquivo)

print("Arquivo criado com sucesso.")
extends Node

# <https://en.cppreference.com/w/c/program/EXIT_status>
const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string("Olá, meu nome é Franco.\n")
    arquivo.store_string("Olá, meu nome é Franco.\n")
    arquivo.store_string("1 2 3\n")
    arquivo.store_string("-1.23\n")

    arquivo.close()

    print("Arquivo criado com sucesso.")

A execução dos programas em Python, Lua e GDScript gerará um arquivo texto chamado franco-garcia-arquivo_texto_escrita.txt no diretório em que o programa for executado (ou seja, no diretório de trabalho). A execução do programa em JavaScript gerará um arquivo texto que pode ser salvo na máquina. Todos os arquivos terão o mesmo conteúdo; é possível usar um editor de texto para abrir o arquivo e ler o conteúdo. Também é possível modificar o arquivo resultante em um editor de texto. Contudo, os exemplos de leitura assumem a existência dos dados criados no programa; portanto, caso você modifique os valores, execute os programas novamente para gerar um arquivo equivalente ao original.

Os exemplos em Python, Lua e GDScript fornecem a forma clássica de trabalhar com arquivos.

A versão em Python abre um novo arquivo usando open() (documentação; é possível usar um parâmetro nomeado para a escolha de codificação; por exemplo, encoding="utf-8"). O parâmetro "w" (write ou escrita) indica o modo da abertura de arquivo; "w" significa abrir o arquivo em modo de escrita, criando um novo arquivo vazio (caso ele não exista) ou apagando todos os dados existentes (caso o arquivo exista). Em seguida, a manipulação do arquivo é feita usando a classe TextIOBase (documentação) e TextIOWrapper (documentação). O método writelines() (documentação) permite escrever um vetor de cadeias de caracteres no arquivo. Ao final do uso, utiliza-se close() (documentação) para fechar o arquivo.

A versão em Lua usa io.open() (documentação) para abrir o arquivo; o parâmetro "w" funciona da mesma forma que em Python. A escrita utiliza io.write() (documentação). Após o término do uso, io.close() (documentação) permite fechar o arquivo. Como io.open(), io.write() e io.close() usam, respectivamente, file.open(), file.write() e file.close(), convém consultar também a documentação desses métodos. Em caso de erro, os.exit() (documentação) permite terminar o programa prematuramente. EXIT_FAILURE é uma constante definida em C para indicar o término de um programa como erro (documentação); como ela não existe em Lua, definiu-se uma com o valor 1 (o valor pode variar entre sistemas operacionais e plataformas; 1 é um valor comum para arquiteturas para desktop). A função debug.traceback() (documentação) provê dados sobre a pilha de chamadas (call stack) no momento do erro.

A versão em GDScript utiliza a classe File (documentação) para as operações. O método open() (documentação) abre o arquivo; o parâmetro File.WRITE (documentação) indica o modo de escrita, funcionando da mesma forma que "w". O método store_string() (documentação) permite escrever uma cadeia de caracteres no arquivo. Ao final do uso, close() (documentação) permite fechar o arquivo. A constante EXIT_FAILURE é definida como em Lua, seguindo as mesmas considerações. Para terminar o programa, usa-se get_tree() (documentação), depois quit() (documentação).

Em JavaScript para back-end (por exemplo, usando Node.js), é possível usar arquivos como em Python, Lua e GDScript. Para front-end (ou seja, em navegadores), o uso é um pouco diferente, como ilustrado no exemplo. Ao invés de criar-se um arquivo no sistema, cria-se um arquivo temporariamente no navegador para download (ser baixado para a máquina). Isso requer que todo o texto a ser armazenado esteja pronto (no caso, ele está salvo em conteudo). Em seguida, usa-se File() (documentação) para criar o arquivo. O tipo escolhido "text/plain" é uma designação de media type ou Multipurpose Internet Mail Extensions types (MIME type; documentação). Com o arquivo criado, gera-se um link na página usando-se document.createElement() (documentação) que é preenchido com os dados do arquivo: destino em target ("_blank" significa nova aba ou janela), endereço para o arquivo em href (gerado com URL.createObjectURL(); documentação), e nome do arquivo para download em download. Por fim, cria-se um diálogo de confirmação usando-se confirm() (documentação). Caso a pessoa confirme o diálogo, clica-se programaticamente no link criado usando-se click() (documentação). Em seguida, remove-se o link para download com URL.revokeObjectURL() (documentação). O arquivo será transferido do navegador para o computador, como se fosse um download real. Entretanto, como o código é local, não é necessária conexão com a Internet para se obter o arquivo resultante.

Além do modo "w" para escrita, outro modo usual chama-se append ("a"), que funciona de forma similar à "w" caso o arquivo não exista. Contudo, caso um arquivo com o mesmo caminho exista, o modo append abre o arquivo existente no fim para a adição de novos dados (ao invés de apagar o conteúdo existente). Isso é bastante prático quando se deseja acrescentar novos conteúdos no fim de um arquivo, como em um log. A inclusão de novos dados é fácil no final um arquivo texto, mas mais complicada (e menos eficiente) em qualquer outra posição.

Também existe o modo "rw+" ou "r+w", que abre o arquivo em modo de leitura e escrita. O modo é útil para edição de arquivos existentes, pois preserva os valores armazenados (caso um arquivo já exista) ou cria um arquivo vazio (caso o arquivo não exista). Diferentemente de append, o arquivo pode ser lido e escrito em qualquer posição (ao invés de apenas escrito no fim). Contudo, a escrita antes do final do arquivo requer mover o conteúdo existente para frente. Como será comentado como uma técnica, costuma ser mais simples criar um novo arquivo com o conteúdo modificado e sobrescrever o original (ou mantê-lo como back-up).

Fechamento Automático ao Final de Escopo

Em Python e Lua (a partir da versão 5.4), existe uma forma alternativa de fechar um arquivo. A alternativa fecha automaticamente o arquivo após o final do escopo da variável. O exemplo a seguir utiliza a escrita de um arquivo texto; contudo, bastaria modificar as chamadas de open() em Python ou io.open() em Lua para usá-las com quaisquer outros tipos ou operações com arquivos.

import io
import sys

caminho_arquivo = "franco-garcia-arquivo_texto_escrita_escopo.txt"
with open(caminho_arquivo, "w") as arquivo:
    arquivo.writelines([
        "Olá, meu nome é Franco.\n",
        "Olá, meu nome é Franco.\n",
        "1 2 3\n",
        "-1.23"
    ])

    print("Arquivo criado com sucesso.")
    # Fim do escopo; arquivo fechado automaticamente.

print("Arquivo foi fechado.")
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_texto_escrita_escopo.txt"
do
    local arquivo <close> = io.open(caminho_arquivo, "w")
    if (arquivo == nil) then
        print(debug.traceback())

        error("Erro ao tentar criar arquivo de texto.")
        os.exit(EXIT_FAILURE)
    end

    arquivo:write("Olá, meu nome é Franco.\n")
    arquivo:write("Olá, meu nome é Franco.\n")
    arquivo:write("1 2 3\n")
    arquivo:write("-1.23\n")

    print("Arquivo criado com sucesso.")
    -- Fim do escopo; arquivo fechado automaticamente.
end

print("Arquivo foi fechado.")

Em Lua, pode-se forçar a criação de um novo escopo usando um par do e end (isso pode não ser necessário caso se defina o código dentro de uma subrotina, que possuirá sem próprio escopo local). Em seguida, usa-se o modificador <close> para indicar que se deseja fechar o arquivo ao final do escopo (documentação). Também é possível definir <close> personalizados para tipos definidos como registros em tabelas.

Em Python, usa-se a palavra reservada with para a abertura do arquivo (documentação). Deve-se notar que exceções ainda podem ocorrer (o tratamento foi omitido no código).

A forma alternativa fecha o arquivo assim que o escopo terminar. Isso possui duas vantagens. A primeira é que não se corre o risco de fechar o arquivo. A segunda é que o arquivo é fechado automaticamente em caso de problemas (como exceções). Assim, o fechamento automático pode ser mais seguro que a forma tradicional. Em C++, a generalização dessa técnica é chamada de resource acquisition is initialization (RAII; aquisição de recurso é inicialização).

Leitura de Arquivo Texto

Existem três formas principais de ler um arquivo texto:

  1. Ler o arquivo todo como uma única cadeia de caracteres;
  2. Separar o conteúdo do arquivo em um vetor de valores separados por um delimitador. Por exemplo, ler o arquivo linha por linha;
  3. Extrair dados do arquivo texto. Na terceira forma, tenta-se extrair os dados e convertê-los para variáveis de tipos mais apropriados.

Também existem outras formas, como ler o arquivo caractere por caractere. O final de um arquivo texto possui um valor especial chamado end of file (EOF, que significa fim do arquivo). Para comodidade de processamento, é comum terminar o arquivo com uma linha vazia antes de EOF (embora ela não seja necessária).

Leitura do Arquivo Texto Inteiro

A leitura do arquivo texto inteiro normalmente é simples, embora possa requerer mais memória primária que as outras formas de leitura. Armazenar todo o conteúdo de um arquivo em uma variável é útil como otimização, para evitar múltiplas leituras de dados da memória secundária (mais lenta que a primária). Após a leitura de todo o conteúdo, pode-se processar a variável lida como qualquer outra cadeia de caracteres.

A versão em JavaScript não poderá ser usada diretamente, pois ela requer uma página definida em código HTML para acompanhá-la (o código HTML é apresentado na seqüência desta seção). Ela também será mais complexa que as demais, por algumas restrições impostas por navegadores. Por outro lado, a implementação permitirá exibir o conteúdo lido no navegador.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

// <https://www.francogarcia.com/pt-br/blog/ambientes-de-desenvolvimento-javascript/>
function adicione_elemento(valor, nome_elemento = "p") {
    const pai = document.getElementById("conteudo")
    const novo_elemento = document.createElement(nome_elemento)
    novo_elemento.innerHTML = valor
    pai.appendChild(novo_elemento)
}

function leia_arquivo(arquivo_texto) {
    // console.log(arquivo_texto)
    if (!arquivo_texto) {
        return
    } else if (!(arquivo_texto instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        let conteudo = evento.target.result
        console.log(conteudo)

        // Troca todas as ocorrência de \n por uma tag de quebra de linha <br/>
        // para exibição do texto no navegador.
        // Opcionalmente, caso o sistema defina quebras de linha como \r\n,
        // a expressão regular troca ambas as ocorrências pela tag.
        adicione_elemento(conteudo.replace(new RegExp("\\r?\\n", "g"), "<br/>"))
    }
    leitor_arquivos.readAsText(arquivo_texto)

    // Impede a submissão do formulário, permitindo a visualização do
    // resultado de adicione_elemento() na mesma página.
    return false
}
import io
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    arquivo = open(caminho_arquivo, "r")
    conteudo = arquivo.read()
    arquivo.close()

    print("Arquivo lido com sucesso.")
    print(conteudo)
except IOError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
local arquivo = io.open(caminho_arquivo, "r")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar ler arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

local conteudo = arquivo:read("*all")
io.close(arquivo)

print("Arquivo lido com sucesso.")
print(conteudo)
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        printerr("Erro ao tentar ler arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    var conteudo = arquivo.get_as_text()
    arquivo.close()

    print("Arquivo lido com sucesso.")
    print(conteudo)

A estrutura de um código para ler um arquivo é similar a usada para escrever em um arquivo, pois ela também começa com a abertura do arquivo e termina com o fechamento dele. Uma tentativa de ler um arquivo inexistente resultaria em erro. O programa tentará ler o arquivo criado gerado pelo programa anterior para criação de arquivo texto. Caso ele tenha sido apagado por qualquer motivo, deve-se recriá-lo.

Em Python, a operação de leitura usa o parâmetro "r" (read ou leitura). Em seguida, o uso do método read() (documentação) sem nenhum parâmetro permite ler o arquivo todo; a provisão de um parâmetro permite ler partes do arquivo (o parâmetro é um número com a quantidade desejada de bytes para a leitura). Alternativamente, pode-se usar readall() (documentação) para a leitura do arquivo todo.

Em Lua, também usa-se o parâmetro "r" para se especificar o modo de leitura. O uso de file:read() (documentação) permite ler dados do arquivo da mesma forma que para io.read() (documentação) para a entrada padrão. Assim, pode-se ler um número com "*number", uma linha com "*line", o arquivo todo com "*all" ou uma quantidade de bytes especificando-se um número inteiro.

Em GDScript, o método get_as_text() (documentação) lê o arquivo todo como um texto codificado em UTF-8. Para a leitura de partes do arquivo, deve-se usar os outros métodos disponíveis.

A versão em JavaScript requererá uma página HTML com um formulário para envio do arquivo, pois, por motivos de segurança, o envio de um arquivo em um navegador requer que uma usuário ou um usuário inicie a interação com um clique (ou com qualquer interação explícita). A página é uma adaptação do exemplo fornecido para configuração de ambiente em JavaScript. O procedimento adicione_elemento() também foi definido no exemplo da configuração de ambiente. Neste tópico, ele é usado para exibir o conteúdo do arquivo no navegador.

<!DOCTYPE html>
<html lang="pt-BR">

  <head>
    <meta charset="utf-8">
    <title>Leitura de Arquivo Texto</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <header>
      <h1>Leitura de Arquivo</h1>
    </header>

    <main>
      <!-- Formulário para envio do arquivo texto. -->
      <form method="post"
            enctype="multipart/form-data"
            onsubmit="return leia_arquivo(arquivos.files[0])">
        <label for="arquivos">Escolha um arquivo texto:</label>
        <input id="arquivos"
               name="arquivos"
               type="file"
               accept="text/plain"/>
        <input type="submit"/>
      </form>

      <div id="conteudo">
      </div>

      <!-- O nome do arquivo JavaScript deve ser igual ao definido abaixo. -->
      <script src="./script.js"></script>
    </main>
  </body>

</html>

Na página HTML, define-se um formulário usando-se a tag <form> (documentação). O formulário contém uma tag de entrada <input> (documentação), configurada para a escolha de um arquivo (file picker). Para isso, usa-se type="file" (documentação). O formulário possui um botão para envio do arquivo escolhido, definido por <input type="submit"/>. O processamento feito pela função leia_arquivo(), como definido em onsubmit na definição de <form>. A função leia_arquivo() deverá estar implementada no arquivo script.js, armazenado no mesmo diretório do arquivo HTML.

No caso, o formulário faz processamento local, então nenhum dado é enviado para a Internet. No código JavaScript, instanceof (documentação) verifica se a variável é instância do tipo File. Caso o parâmetro represente um arquivo, usa-se FileReader (documentação) para a leitura do conteúdo. A leitura é feita na chamada do método readAsText() (documentação). Quando ela termina, a implementação executa o código definido em onload (documentação), que deve ser definido antes da chamada de readAsText(). Em onload, definiu-se uma função anônima (lambda) para escrever o conteúdo no console (terminal) e também na página exibida 2 2navegador, usando adicione_elemento(). A expressão regular troca quebras de linha da cadeia de caracteres por tags para quebra de linha no navegador, para que as linhas do texto apareçam corretamente.

No caso, onload define uma função callback, chamada pela implementação da classe FileReader para processar os dados lidos no momento adequado.

Leitura do Arquivo Texto Delimitado (Linha por Linha)

Quando a quantidade de memória primária (RAM) livre é suficiente para armazenar todo o conteúdo do arquivo, pode-se lê-lo de uma única vez. Embora isso seja comum em máquinas modernas (como computadores de mesa e dispositivos móveis recentes), existem máquinas com quantidades limitada de memória (como dispositivos embarcados). Em outros casos, pode ser desejável operar com partes menores do arquivo em memória primária.

Uma segunda abordagem comum para a leitura de arquivos textos é ler partes do arquivo até o próximo delimitador. Normalmente, o delimitador escolhido é a quebra de linhas. Ou seja, lê-se uma linha do arquivo de cada vez.

Implementações de arquivos texto comumente fornecem uma subrotina para a leitura de uma linha (JavaScript para navegadores é uma exceção). Com uma estrutura de repetição, pode-se ler cada linha do arquivo até seu final.

import io
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    arquivo = open(caminho_arquivo, "r")

    linha_texto = arquivo.readline()
    while (linha_texto):
        print(linha_texto, end="")
        linha_texto = arquivo.readline()

    arquivo.close()
    print("Arquivo lido com sucesso.")
except IOError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
local arquivo = io.open(caminho_arquivo, "r")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar ler arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

local linha_texto = arquivo:read("*line")
while (linha_texto) do
    print(linha_texto)
    linha_texto = arquivo:read("*line")
end

io.close(arquivo)

print("Arquivo lido com sucesso.")

-- Alternativa:
for linha_texto in io.lines(caminho_arquivo) do
    print(linha_texto)
end
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        printerr("Erro ao tentar ler arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    var linha_texto = arquivo.get_line()
    while (linha_texto):
        print(linha_texto)
        linha_texto = arquivo.get_line()

    print("Arquivo lido com sucesso.")

Python fornece o método readline() (documentação) para a leitura da próxima linha. Lua permite usar io.read() ou file:read() como o parâmetro "*line" como se fosse a entrada padrão; a linguagem também fornece io.lines() (documentação) para um iterador sobre as linhas. GDScript fornece get_line() (documentação) para a leitura da próxima linha.

Em todos os casos, pode-se usar o valor lido como condição para a estrutura de repetição enquanto. Enquanto o valor for uma cadeia de caracteres válida ou não nula, a repetição continuará.

Caso se deseje contar o número de linhas no arquivo, pode-se instanciar uma variável do tipo inteiro como contador e incrementar o valor a cada nova linha lida com sucesso.

Leitura de Arquivo Texto com Extração de Dados

Quando se conhece o conteúdo de um arquivo de texto ou ele possua um formato bem definido e regular, pode-se extrair dados armazenados para processamento mais granular. Para isso, pode-se converter os dados de cadeias de caracteres para tipos mais adequados, como números inteiros, números reais ou valores lógicos. Assim, é possível, por exemplo, efetuar operações aritméticas, relacionais e lógicas usando os valores extraídos.

Por exemplo, o arquivo criado nas seções anteriores contém duas linhas de texto, seguidas de uma linha três números inteiros, seguida de um linha um com um número real, seguida de uma linha vazia e do final do arquivo. Como o formato do arquivo é conhecido, pode-se ler o arquivo todo (ou cada linha) e extrair os valores.

Existem duas abordagens principais para a leitura de um arquivo para extração.

  1. Pode-se ler o arquivo todo de uma vez e separá-lo (split) em um vetor usando delimitadores. Em seguida, processa-se cada valor do vetor.
  2. Pode-se imaginar o arquivo de texto como se ele representasse todas as entradas fornecidas por uma usuária ou um usuário final durante o uso de um programa. Nesse caso, pode-se interpretar o arquivo como se ele fosse a origem de valores para comandos de leitura como input(), io.read() ou prompt().

Em algumas linguagens de programação (por exemplo, JavaScript em navegadores), o primeiro caso será imposto, pois o arquivo será lido por inteiro. Em outras linguagens, pode-se escolher a abordagem mais simples para se resolver o problema.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

// <https://www.francogarcia.com/pt-br/blog/ambientes-de-desenvolvimento-javascript/>
function adicione_elemento(valor, nome_elemento = "p") {
    const pai = document.getElementById("conteudo")
    const novo_elemento = document.createElement(nome_elemento)
    novo_elemento.innerHTML = valor
    pai.appendChild(novo_elemento)
}

function leia_arquivo(arquivo_texto) {
    if (!arquivo_texto) {
        return
    } else if (!(arquivo_texto instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        // Abordagem 1: leitura do arquivo todo e uso de split.
        console.log("Abordagem 1")
        let conteudo = evento.target.result

        let linhas = conteudo.split("\n")
        console.log(linhas)

        let primeira_frase = linhas[0]
        let segunda_frase = linhas[1]
        let numeros_inteiros = []
        let soma_numeros_inteiros = 0
        for (let texto_numero of linhas[2].split(" ")) {
            let numero = parseInt(texto_numero)
            numeros_inteiros.push(numero)
            soma_numeros_inteiros += numero
        }

        let numero_real = parseFloat(linhas[3])

        console.log(primeira_frase)
        console.log(segunda_frase)
        console.log(numeros_inteiros, soma_numeros_inteiros)
        console.log(numero_real, "Número positivo?", numero_real > 0)

        adicione_elemento(primeira_frase)
        adicione_elemento(segunda_frase)
        adicione_elemento("[" + numeros_inteiros + "] " + soma_numeros_inteiros)
        adicione_elemento(numero_real + " " +  "Número positivo?" + " " + (numero_real > 0))
    }
    leitor_arquivos.readAsText(arquivo_texto)

    // Impede a submissão do formulário, permitindo a visualização do
    // resultado de adicione_elemento() na mesma página.
    return false
}
import io
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    arquivo = open(caminho_arquivo, "r")

    # Abordagem 1: leitura do arquivo todo e uso de split.
    print("Abordagem 1")
    conteudo = arquivo.read()

    linhas = conteudo.split("\n")
    print(linhas)

    primeira_frase = linhas[0]
    segunda_frase = linhas[1]
    numeros_inteiros = []
    soma_numeros_inteiros = 0
    for texto_numero in linhas[2].split():
        numero = int(texto_numero)
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    numero_real = float(linhas[3])

    print(primeira_frase)
    print(segunda_frase)
    print(numeros_inteiros, soma_numeros_inteiros)
    print(numero_real, "Número positivo?", numero_real > 0)

    # Abordagem 2: leitura do arquivo como se fosse a entrada de um usuário.
    print("\nAbordagem 2")
    arquivo.seek(0) # ou arquivo.seek(0, 0) ou arquivo.seek(0, io.SEEK_SET)

    primeira_frase = arquivo.readline().rstrip()
    segunda_frase = arquivo.readline().rstrip()
    numeros_inteiros = []
    soma_numeros_inteiros = 0
    for texto_numero in arquivo.readline().split():
        numero = int(texto_numero)
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    numero_real = float(arquivo.readline())

    print(primeira_frase)
    print(segunda_frase)
    print(numeros_inteiros, soma_numeros_inteiros)
    print(numero_real, "Número positivo?", numero_real > 0)

    arquivo.close()

except IOError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
function escreva_indentacao(nivel)
    for indentacao = 1, nivel do
        io.write("  ")
    end
end

function escreva_tabela(tabela, nivel)
    nivel = nivel or 1
    if (type(tabela) == "table") then
        io.write("{\n")
        for chave, valor in pairs(tabela) do
            escreva_indentacao(nivel)
            io.write(tostring(chave) .. ": ")
            escreva_tabela(valor, nivel + 1)
            io.write("\n")
        end
        escreva_indentacao(nivel - 1)
        io.write("},")
    else
        local aspas = ""
        if (type(tabela) == "string") then
            aspas = "\""
        end
        io.write(aspas .. tostring(tabela) .. aspas .. ",")
    end
end

function split(cadeia_caracteres, delimitador)
    delimitador = delimitador or " "
    local resultado = {}
    local tamanho = #cadeia_caracteres
    local inicio = 1
    while (inicio <= tamanho) do
        local fim, proximo = string.find(cadeia_caracteres, delimitador, inicio, true)
        if (fim ~= nil) then
            table.insert(resultado, string.sub(cadeia_caracteres, inicio, fim - 1))
            inicio = proximo + 1
        else
            table.insert(resultado, string.sub(cadeia_caracteres, inicio))
            inicio = tamanho + 1
        end
    end

    if (string.sub(cadeia_caracteres, -#delimitador) == delimitador) then
        table.insert(resultado, "")
    end

    return resultado
end

-- O programa começa aqui.
local EXIT_FAILURE = 1
local caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
local arquivo = io.open(caminho_arquivo, "r")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar ler arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

-- Abordagem 1: leitura do arquivo todo e uso de split.
print("Abordagem 1")
local conteudo = arquivo:read("*all")

local linhas = split(conteudo, "\n")
escreva_tabela(linhas)
print()

local primeira_frase = linhas[1]
local segunda_frase = linhas[2]
local numeros_inteiros = {}
local soma_numeros_inteiros = 0
for _, texto_numero in ipairs(split(linhas[3], " ")) do
    local numero = tonumber(texto_numero)
    table.insert(numeros_inteiros, numero)
    soma_numeros_inteiros = soma_numeros_inteiros + numero
end

local numero_real = tonumber(linhas[4])

print(primeira_frase)
print(segunda_frase)
escreva_tabela(numeros_inteiros)
print(" " .. soma_numeros_inteiros)
print(numero_real, "Número positivo?", numero_real > 0)

-- Abordagem 2: leitura do arquivo como se fosse a entrada de um usuário.
print("\nAbordagem 2")
arquivo:seek("set")

primeira_frase = arquivo:read("*line")
segunda_frase = arquivo:read("*line")
numeros_inteiros = {}
soma_numeros_inteiros = 0
for indice_numero = 1, 3 do
    local numero = arquivo:read("*number")
    table.insert(numeros_inteiros, numero)
    soma_numeros_inteiros = soma_numeros_inteiros + numero
end

numero_real = tonumber(arquivo:read("*number"))

print(primeira_frase)
print(segunda_frase)
escreva_tabela(numeros_inteiros)
print(" " .. soma_numeros_inteiros)
print(numero_real, "Número positivo?", numero_real > 0)

io.close(arquivo)
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_texto_escrita.txt"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        printerr("Erro ao tentar ler arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    # Abordagem 1: leitura do arquivo todo e uso de split.
    print("Abordagem 1")
    var conteudo = arquivo.get_as_text()

    var linhas = conteudo.split("\n")
    print(linhas)

    var primeira_frase = linhas[0]
    var segunda_frase = linhas[1]
    var numeros_inteiros = []
    var soma_numeros_inteiros = 0
    for texto_numero in linhas[2].split(" "):
        var numero = int(texto_numero)
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    var numero_real = float(linhas[3])

    print(primeira_frase)
    print(segunda_frase)
    printt(numeros_inteiros, soma_numeros_inteiros)
    printt(numero_real, "Número positivo?", numero_real > 0)

    # Abordagem 2: leitura do arquivo como se fosse a entrada de um usuário.
    print("\nAbordagem 2")
    arquivo.seek(0)

    primeira_frase = arquivo.get_line()
    segunda_frase = arquivo.get_line()
    numeros_inteiros = []
    soma_numeros_inteiros = 0
    for texto_numero in arquivo.get_line().split(" "):
        var numero = int(texto_numero)
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    numero_real = float(arquivo.get_line())

    print(primeira_frase)
    print(segunda_frase)
    printt(numeros_inteiros, soma_numeros_inteiros)
    printt(numero_real, "Número positivo?", numero_real > 0)

    arquivo.close()

O exemplo em JavaScript requer a mesma página HTML usada anteriormente para a submissão de um arquivo texto para processamento. Para o uso em navegadores, apenas a primeira abordagem é possível, já que o arquivo será lido por inteiro. Na primeira abordagem, divide-se o conteúdo inteiro em um vetor de linhas. Em seguida, processa-se cada linha de acordo com o(s) tipo(s) de dado(s) armazenado(s), para a extração de valores. Para exemplificar operações com os valores convertidos, os números inteiros foram somados, e o número real foi comparado.

Em Python, Lua e GDScript, ambas as abordagens são possíveis. O exemplo em Lua é um pouco mais longo porque usa subrotinas definidas anteriormente para escrita e processamento de valores. A primeira abordagem funciona da mesma que em JavaScript, então ela não será comentada novamente. Para a segunda abordagem, em cada uma das linguagens:

  1. Utiliza-se seek() para alterar a posição no arquivo. Subrotinas como seek() permitem alterar posições de leitura e/ou escrita no arquivo, para acessos ou modificações de valores em posições arbitrárias. Em arquivos texto, é comum usar a operação para retornar ao início do arquivo (posição set) ou para avançar para o final (posição end). Também é possível avançar ou voltar de acordo com a posição atual (posição cur ou current), embora tais deslocamentos nem sempre sejam possíveis com arquivos de texto.

    Os valores usados para cada linguagem de programação para operações de skip tendem a variar. Assim, é importante consultar a documentação. Para Python: documentação; para Lua: documentação; para GDScript: documentação.

    No caso do exemplo, retornar-se ao início do arquivo (seek set), com deslocamento zero. Na prática, isso permite evitar fechar o arquivo e abri-lo novamente para lê-lo desde o início novamente.

  2. Em seguida, processa-se cada linha de acordo com o seu valor. As linhas são lidas individualmente. Em Python, como readline() mantém a quebra de linha ao final, usa-se rstrip() (documentação) para removê-la.

    Em Lua, é possível ler os valores da mesma forma que a entrada padrão (stdin).

A extração de dados de arquivos texto permite manipulações mais sofisticadas do conteúdo. Contudo, isso nem sempre é possível ou simples extrair dados corretamente de qualquer arquivo texto. Por exemplo, caso o conteúdo do arquivo usado no exemplo mudasse, seria necessário reescrever o código para a extração de dados. Assim, uma solução melhor precisa ser genérica o suficiente para acomodar cenários de modificações. Para isso, estruturação é necessária, para se definir um padrão para os dados armazenados.

Arquivos de Texto Estruturados

Uma forma de facilitar significativamente a extração de dados consiste na adoção de formatos de arquivos de texto estruturado. As próximas subseções apresentam alguns formatos e linguagens de marcação (markup languages) populares para armazenamento e troca de dados. A sigla de cada formato é comumente usada como extensão para arquivos de texto armazenando dados no formato.

Embora alguns formatos possam ser facilmente processados com manipulação de cadeias de caracteres, o ideal é usar (ou criar) uma biblioteca de qualidade para uso profissional de qualquer um dos formatos.

Além disso, existem ferramentas para conversão e apoio de uso de arquivos de texto estruturados. Dois exemplos interessantes são Data-Selector (Dasel) e uma lista de ferramentas disponível neste repositório.

Comma Separated Values (CSV)

Um formato popular usado em programas matemáticos e de planilhas de cálculos chama-se Comma Separated Values (CSV). O nome do formato é a sua própria especificação: valores são separados por vírgulas.

Olá,meu,nome,é,Franco,Tudo bem?
1,2,3,4,5,6
1.1,2.2,3.3,4.4,5.5,6.6

Caso um valor precise ser armazenado com uma vírgula, ele pode ser especificado entre aspas duplas. Contudo, a convenção pode variar de acordo com a implementação.

Para a especificação oficial do formato, pode-se consultar a Request for Comment (RFC) 4180.

Tab Separated Values (TSV)

Uma variação do formato CSV consiste no uso tabulações (tabs) ao invés de vírgulas para separar valores. Em cadeias de caracteres de linguagens de programação, tabulações são comumente representadas por \t. O formato resultante chama-se Tab Separated Values (TSV), ou seja, valores separados por tabulações.

Olá	meu	nome	é	Franco	Tudo bem?
1	2	3	4	5	6
1.1	2.2	3.3	4.4	5.5	6.6

Quando se usa TSV, é importante configurar o editor de texto para inserção de tabulações. Editores de texto para programação podem ser configurados para trocar uma tabulação por uma certa quantidade de espaços. Para o formato TSV, é importante que as tabulações sejam, de fato, caracteres de tabulações reais.

A especificação do formato é simples; ela pode ser encontrada nesta página da Internet Assigned Numbers Authority (IANA).

Extensible Markup Language (XML)

Extensible Markup Language (XML) é um dos formatos pioneiros para troca de dados. A especificação do formato está disponível na página oficial do formato.

O formato XML é parecido com HTML, embora uma programadora ou um programador possa escolher os nomes para as tags. O único requisito é a correspondência entre a tag que inicia um valor com a que termina. Uma tag pode ser definida como <NomeTag atributo="valor">Valor armazenado</NomeTag>. Caso ela não possua valor entre o início e o fim, pode-se escrevê-la como <NomeTag atributo="valor"></NomeTag>, ou, simplesmente, <NomeTag atributo="valor"/>.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Este é um comentário. -->
<Valores>
  <Textos>
    <Texto>Olá</Texto>
    <Texto>meu</Texto>
    <Texto>nome</Texto>
    <Texto>é</Texto>
    <Texto>Franco</Texto>
    <Texto>Tudo bem?</Texto>
  </Textos>
  <NumerosInteiros>
    <NumeroInteiro>1</NumeroInteiro>
    <NumeroInteiro>2</NumeroInteiro>
    <NumeroInteiro>3</NumeroInteiro>
    <NumeroInteiro>4</NumeroInteiro>
    <NumeroInteiro>5</NumeroInteiro>
    <NumeroInteiro>6</NumeroInteiro>
  <NumerosInteiros>
  <NumerosReais>
    <NumeroReal valor="1.1"/>
    <NumeroReal valor="2.2"/>
    <NumeroReal valor="3.3"/>
    <NumeroReal valor="4.4"/>
    <NumeroReal valor="5.5"/>
    <NumeroReal valor="6.6"/>
  <NumerosReais>
</Valores>

Valores podem ser armazenados entre tags ou como atributos de uma tag. Por exemplo, poder-se-ia alterar Texto para <Texto texto="Franco"/>. Da mesma forma, poder-se-ia escrever <NumeroReal>1.1</NumeroReal>.

O formato XML é versátil, embora seja verboso (isto é, longo para escrever). Para maior facilidade de escrita e desempenho, formatos mais recentes são mais concisos e fáceis de processar que XML. Por outro lado, XML fornece recursos adicionais como esquemas e namespaces que são raros em outros formatos. Programadoras e programadores podem definir esquemas como forma de validação de arquivos seguindo o esquema proposto. Eles são úteis para garantir a validade de um arquivo com dados para um determinado domínio ou problema.

JavaScript Object Notation (JSON)

Atualmente, JavaScript Object Notation (JSON) é um dos formatos mais populares para arquivos de texto estruturados. O formato pode ser usado facilmente com a linguagem JavaScript. Como o nome sugere, o formato é similar a JavaScript Objects.

{
  "Textos": ["Olá", "meu", "nome", "é", "Franco", "Tudo bem?"],
  "Numeros Inteiros": [1, 2, 3, 4, 5, 6],
  "Numeros Reais": [1.1, 2.2, 3.3, 4.4, 5.5, 6.6]
}

Em outras palavras, você já sabe usá-los. JSON pode ter vetores, dicionários e tipos de dados primitivos como valores. Valores inteiros ou reais são escritos sem aspas (simples ou duplas). Cadeias de caracteres ou valores lógicos requerem o uso de aspas. Algo que se deve observar é que JSON não admite o uso de comentários.

A especificação do formato está disponível na página oficial. Linguagens de programação modernas costumam oferecer implementações para uso de JSON na biblioteca padrão. Caso contrário, é bastante provável que exista uma biblioteca para uso do formato na linguagem de programação escolhida.

Além disso, existem ferramentas para edição de arquivos JSON como DEPOT. A interface de tais ferramentas são mais próximas de programas de planilhas de cálculo.

Para operação de aquivos usando linha de comando JSON, o programa jq é bastante conveniente para buscas e extração de valores.

YAML Ain't Markup Language (YAML)

JSON é um bom formato para programação, porém bastante próximo da estrutura de linguagens de programação. Existem formatos que tentam ser mais acessíveis a usuários finais. Um deles chama-se YAML Ain't Markup Language (YAML), cuja especificação está disponível na página oficial. O conteúdo da página oficial, inclusive, é um exemplo de arquivo no formato.

%YAML 1.2
---
Textos:
  - Olá
  - meu
  - nome
  - é
  - Franco
  - Tudo bem?
Numeros Inteiros:
  - 1
  - 2
  - 3
  - 4
  - 5
  - 6
Numeros Reais:
  - 1.1
  - 2.2
  - 3.3
  - 4.4
  - 5.5
  - 6.6

O formato YAML assemelha-se a uma lista estruturada feita em um editor de textos.

Tom's Obvious Minimal Language (TOML)

Tom's Obvious Minimal Language (TOML) é outro formato para arquivos texto estruturados. O formato assemelha-se a arquivos de configuração usados no Windows. A especificação está disponível na página oficial do projeto.

Textos = ["Olá", "meu", "nome", "é", "Franco", "Tudo bem?"]
NumerosInteiros = [1, 2, 3, 4, 5, 6]
NumerosReais = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6]

Em particular, Godot Engine usa um formato parecido com TOML para armazenamento das cenas em formato de texto criadas pelo editor.

Arquivos Binários (Binary Files)

Além de arquivos texto, é possível armazenar dados em arquivos como despejos (dumps) diretos de memória. Em outras palavras, ao invés de codificar o conteúdo como texto, salva-se em memória secundária os bytes armazenados em memória primária. Arquivos assim são chamados de arquivos binários (mesmo que, tecnicamente, arquivos textos também sejam arquivos binários codificados).

Ao contrário de arquivos texto, arquivos binários não possuem o objetivo de serem legíveis por seres humanos. Não há marcações de onde um dado começa ou termina; dados são, simplesmente, seqüências de bits armazenados na memória. Assim, para determinar o conteúdo de um arquivo binário com exatidão, deve-se conhecer a ordem e os tipos de dados armazenados em seqüência. Caso essas informações não sejam conhecidas, a identificação de valores em um arquivo binário requer esforços de engenharia reversa para determinação do que está armazenado.

Pode-se pensar em um arquivo binário como um grande vetor de dados. Entretanto, ao invés de posições pré-definidas, cada dado ocupa um certo número de bits ou bytes. A leitura de um certo número de bytes a partir de uma posição inicial até uma posição final permite extrair o valor de um dado. Da mesma forma, a escrita do valor do tipo lido nas mesmas posições permite armazenar um valor. Assim, arquivos binários permitem leitura e escrita com acesso aleatório; sabendo-se o "endereço" do valor no arquivo, é possível acessá-lo e/ou modificá-lo. Ou seja, o deslocamento (offset) a partir de uma posição conhecida (como início, fim ou a posição atual) permite escrever ou extrair dados em posições arbitrárias do arquivo. Para fazer o deslocamento, basta usar uma operação de seek. Isso facilita a extração de dados de arquivos binários.

De fato, a combinação de arquivos binários com registros (no sentido de Plain Old Data ou POD) pode tornar trivial o armazenamento e recuperação de dados em algumas linguagens de programação. Contudo, isso tende a ser mais fácil de fazer em linguagens de programação nível mais baixo (como C e C++) que em linguagens de programação de nível mais alto (como JavaScript, Python e Lua). A razão é que, em linguagens de baixo nível, os tipos de dados tendem a possuir tamanhos fixos em bytes, o que facilita a obtenção do tamanho de um registro POD.

Editor Hexadecimal (Hexadecimal Editor ou Hex Editor)

Arquivos texto podem ser lidos e modificados usando editores de texto. Arquivos binários podem ser lidos e modificados usando editores hexadecimais.

Ao contrário de editores de texto que (mesmo que simples) costumam ser fornecidos por padrão em quaisquer sistemas operacionais, normalmente é necessário instalar um editor hexadecimal. Alguns ambientes gráficos incluem editores hexadecimais entre o programas instalados por padrão. Por exemplo, o KDE fornece o editor Okteta. Okteta pode ser usado e instalado em sistemas operacionais Windows e Linux.

Uma alternativa de código aberto que pode ser usada em Windows, Linux e macOS chama-se ImHex. Existem também ferramentas que podem ser usadas online, como HexEd.it (gratuito, mas não é de código aberto).

Por fim, alguns editores de texto incluem modos de editor hexadecimal. Por exemplo, GNU Emacs fornece hexl-mode para visualização e edição de arquivos binários.

Neste tópico, o editor ImHex será usado para inspeções de arquivos binários. Ele será escolhido por permitir definir valores baseados em deslocamentos ("endereços de memória") e realçá-los com diferentes cores. A sintaxe utilizada para definição dos dados está disponível na documentação do programa.

Operações Básicas Usando Arquivos Binários

Operações usando arquivos binários são semelhantes às feitas para arquivos texto, embora o arquivo resultante seja mais difícil de visualizar que um arquivo texto. Por isso a sugestão de uso de um editor hexadecimal.

Assim como arquivos textos, todo o uso de um arquivo binário inicia-se pela abertura (ou criação) de um arquivo, e termina com o fechamento dele. A principal diferença são as operações de leitura e escrita. Ao invés de caracteres ou linhas (qualquer tipo de texto codificado), as operações trabalham com blocos de memória cujo tamanho é definido em bytes.

Escrita de Arquivo Binário

As implementações em Python, Lua e GDScript são mais simples que a implementação em JavaScript. A versão em JavaScript requer a montagem dos bytes para se salvar no arquivo. Python, Lua e GDScript fornecem subrotinas para conversão automática, simplificando o processo. A versão em GDScript é a mais simples de ler, pois a linguagem fornece métodos específicos para cada tipo de dado. Assim, pode-se se interessante ler primeiro o código em GDScript, depois o código em Python ou Lua, e, por último, o código em JavaScript. Como as quatro implementações são equivalentes, isso pode facilitar o entendimento dos programas.

A implementação em Lua requer versão 5.3 ou mais recente para uso de string.pack(). Para as versões 5.1 e 5.2 da linguagem, pode-se usar uma extensão criada pelo autor de Lua para se obter struct.pack() e struct.unpack(), que funcionam como string.pack() e string.unpack(). É possível alterar a versão do interpretador Lua usada pelo ZeroBrane Studio em Projeto (Project), depois Interpretador de Lua (Lua Interpreter), depois Lua 5.3.

Os arquivos nos exemplos possuem a extensão .bin, escolhida por ser comum para arquivos binários. Contudo, pode-se escolher qualquer extensão. Uma extensão não modifica o conteúdo de um arquivo, ou seja, ela não define o que um arquivo é. A extensão serve como dica (heurística) para que o sistema operacional escolha um programa apropriado para abrir o arquivo. Para visualizar o conteúdo do arquivo gerado em um editor de textos, pode-se escolher a extensão .txt (algo interessante de fazer). Para se criar uma extensão própria, pode-se adotar a extensão .dados ou .franco.

let caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
let conteudo = []

let linha_texto = "Olá, meu nome é Franco.\n"
let text_encoder = new TextEncoder()
let texto_codificado = text_encoder.encode(linha_texto)
let tamanho_texto_codificado = texto_codificado.length

for (let repeticoes = 0; repeticoes < 2; ++repeticoes) {
    let tamanho = new Int32Array(1)
    tamanho[0] = tamanho_texto_codificado
    let bytes_tamanho = new Uint8Array(tamanho.buffer)
    for (let byte_tamanho of bytes_tamanho) {
        conteudo.push(byte_tamanho)
    }

    for (let byte_texto of texto_codificado) {
        conteudo.push(byte_texto)
    }
}

let numeros_inteiros = new Int32Array(3)
numeros_inteiros[0] = 1
numeros_inteiros[1] = 2
numeros_inteiros[2] = 3
let bytes_numeros_inteiros = new Uint8Array(numeros_inteiros.buffer)
for (let byte_numeros of bytes_numeros_inteiros) {
    conteudo.push(byte_numeros)
}

let numero_real = new Float32Array(1)
numero_real[0] = -1.23
let bytes_numero_real = new Uint8Array(numero_real.buffer)
for (let byte_numero of bytes_numero_real) {
    conteudo.push(byte_numero)
}

let bytes = new Uint8Array(conteudo)
let dados = new Blob([bytes], {type: "application/octet-stream"})
let arquivo = new File([dados], caminho_arquivo, {type: dados.type})
console.log("Arquivo criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import struct
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
    arquivo = open(caminho_arquivo, "wb")

    linha_texto = "Olá, meu nome é Franco.\n"
    texto_codificado = bytearray(linha_texto.encode("utf-8"))
    tamanho_texto_codificado = len(texto_codificado)
    arquivo.write(struct.pack("i", tamanho_texto_codificado))
    arquivo.write(texto_codificado)

    arquivo.write(struct.pack("i", tamanho_texto_codificado))
    arquivo.write(texto_codificado)

    arquivo.write(struct.pack("i", 1))
    arquivo.write(struct.pack("i", 2))
    arquivo.write(struct.pack("i", 3))

    arquivo.write(struct.pack("f", -1.23))

    arquivo.close()

    print("Arquivo criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
local arquivo = io.open(caminho_arquivo, "wb")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo binário.")
    os.exit(EXIT_FAILURE)
end

local linha_texto = "Olá, meu nome é Franco.\n"
arquivo:write(string.pack("s", linha_texto))
arquivo:write(string.pack("s", linha_texto))

arquivo:write(string.pack("i", 1))
arquivo:write(string.pack("i", 2))
arquivo:write(string.pack("i", 3))

arquivo:write(string.pack("f", -1.23))

io.close(arquivo)

print("Arquivo criado com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_pascal_string("Olá, meu nome é Franco.\n")
    arquivo.store_pascal_string("Olá, meu nome é Franco.\n")

    arquivo.store_32(1)
    arquivo.store_32(2)
    arquivo.store_32(3)

    arquivo.store_float(-1.23)

    arquivo.close()

    print("Arquivo criado com sucesso.")

Em cada um dos programas, escreveu-se o tamanho da cadeia de caracteres antes da frase. Isso permitirá ler o número correto de bytes para extrair os dados nos programas de leitura. Em linguagens de programação compiladas, como C e C++, pode-se sofisticar esta técnica e incluir o tamanho como primeira posição de um vetor. A técnica chama-se Pascal string, pois foi popularizada em implementações da linguagem de programação Pascal.

Para a implementação do exemplo, a abertura e fechamento de arquivos ocorre de forma similar a arquivos texto. Em Python e Lua, usa-se "b" na abertura ("wb") para a criação de um arquivo binário em modo de escrita. Em JavaScript, muda-se o tipo MIME para "application/octet-stream" (todavia, o formato será devido pelo processo de criação) Em GDScript, cria-se o arquivo normalmente.

A escrita varia de linguagem para linguagem. A mais simples dentre as adotadas para exemplos é GDScript, que fornece métodos específicos para cada tipo de dado. O método store_pascal_string() (documentação) permite escrever uma cadeia de caracteres precedida pelo valor inteiro com seu tamanho; store_32() (documentação) permite escrever números inteiros com 4 bytes (32 bits); store_float() (documentação) permite escrever números reais em ponto flutuante com 4 bytes (32 bits).

Python e Lua funcionam de forma similar, com uma subrotina para gerar seqüências de bytes (vetores de bytes) para cada tipo. O princípio é criar tipos compatíveis com struct (registros) em C.

Python provê o módulo struct (documentação); o método pack() (documentação) permite criar as seqüências binárias. Cada parâmetro corresponde a um tipo de dado; "i" é para inteiros de 4 bytes; "f" é para ponto flutuante de 4 bytes. Para cadeias de caracteres, é preciso codificá-las (por exemplo, em UTF-8) com encode() (documentação) e gerar um bytearray (documentação).

Em Lua, a versão 5.3 introduziu string.pack() (documentação) para o mesmo propósito. O parâmetro "s" escreve o tamanho e a cadeia de caracteres; "i" escreve um número inteiro de tamanho padrão; "f" escreve um número em ponto flutuante de tamanho padrão. Na máquina do autor, o tamanho padrão foi 4 bytes para ambos os casos.

JavaScript é a linguagem que demanda maior esforço, porque não há subrotina para conversão direta de tipos. Em JavaScript, deve-se criar um vetor com tamanho fixo de valores desejados, depois converter o vetor para um vetor de bytes (Uint8), depois inserir cada valor em um vetor para se escrever no arquivo. Assumindo-se valores de 4 bytes (32 bits):

  • Para números inteiros com sinal, primeiro cria-se um vetor Int32Array (documentação), armazena-se os valores desejados, depois cria-se um vetor Uint8Array (documentação).
  • Para números reais, primeiro cria-se um vetor Float32Array (documentação), armazena-se os valores desejados, depois cria-se um vetor Uint8Array;
  • Para cadeias de caracteres, primeiro cria-se um codificador de texto TextEncoder (documentação), armazena-se os valores desejados, depois cria-se um vetor Uint8Array. Como nos outros casos, também é interessante armazenar o tamanho da cadeia de caracteres antes dos bytes do texto.

Para facilitar o uso em JavaScript, poderia ser interessante criar funções para a conversão. Isso será feito posteriormente em alguns exemplos, como para a criação de um arquivo de som (funções prefixadas com pack_) e para montagem de valores lidos de arquivos (funções prefixadas com unpack_). As funções não foram criadas para este primeiro exemplo para demonstrar que é possível criar um vetor maior e convertê-lo de uma única vez.

Além disso, JavaScript utiliza um Blob (documentação) para armazenar os bytes antes de salvá-los. O termo blob é usado em programação para se referir a dados binários, sem uma interpretação de tipo. No caso, trata-se de todos os bytes que se deseja salvar.

Usar tipos com uma quantidade fixa de bytes pode economizar memória para salvar arquivos. Por exemplo, os valores 0, 1, 0000001, -123, 123456, 2147483647 e -2147483648 ocupam 4 bytes de memória como números inteiros de 32 bits. Em texto, qualquer valor com 5 ou mais dígitos (incluindo sinal, pontos, vírgulas, ou zeros à esquerda) requereria, no mínimo, o mesmo número de bytes para cada caractere utilizado. Por exemplo, -2147483648 possui 11 caracteres, o que requereria 11 bytes para codificação em ASCII ou UTF-8. O mesmo vale para números em ponto flutuante.

Uma segunda vantagem é a maior facilidade para extração de valores. Como se conhece todos os tamanhos, pode-se determinar onde cada um deles começa e termina. Para carregar o arquivo criado, basta seguir a mesma ordem da criação, mas lendo dados ao invés de escrevê-los. Salvar o tamanho de cadeias de caracteres permite ler a quantidade exata de caracteres armazenados para cada texto salvo.

Ordem de Bytes e Endianness

Diferentemente de arquivos texto, os arquivos binários gerados para cada exemplo anterior podem ser diferentes. Isso ocorre porque eles dependem, por exemplo, do número de bytes escolhido (ou adotado pela linguagem) para representar cada tipo de dados. Além disso, arquivos binários (e a memória de um computador propriamente dita) podem adotar uma entre duas ordens para bytes: little-endian (LE) e big-endian (BE). Na ordem big-endian, o byte mais significativo é armazenado no menor endereço de memória. Na ordem little-endian, o byte mais significativo é armazenado no maior endereço de memória. Isso significa que, na ordem little-endian, os bytes são armazenados invertidos em memória. O ordem little-endian é comum em arquiteturas para processadores de arquiteturas x86 e AMD64 (x64). A ordem big-endian é comum em operações de rede (network order).

A rigor, a ordem de bytes também pode afetar arquivos texto. Por exemplo, arquivos codificados em Unicode podem ter um valor inicial chamado Byte Order Mark (BOM), que pode ser usado para se determinar se o arquivo segue a ordem little-endian ou big-endian. Os valores para cada codificação Unicode (como UTF-8) podem ser encontrados neste artigo da Wikipedia. Dependendo do formato adotado, o BOM pode ser opcional, obrigatório ou proibido. Para codificação UTF-8, ele é opcional (por exemplo, os exemplos deste tópico não usam BOM). Para qualquer caso, caso existente, os valores do BOM não devem ser exibidos pelo editor de texto (ou programa processando o arquivo); eles apenas são úteis para a decodificação dos demais bytes do arquivo.

Para ilustrar a diferença de ordem de bytes, pode-se considerar a tabela a seguir, extraída das perguntas gerais relacionadas a UTF ou Forma de Codificação. O cabeçalho foi traduzido.

BytesCodificação
00 00 FE FFUTF-32, big-endian
FF FE 00 00UTF-32, little-endian
FE FFUTF-16, big-endian
FF FEUTF-16, little-endian
EF BB BFUTF-8

Um byte possui 8 bits. Dois dígitos hexadecimais podem representar 256 valores, ou seja, de 00 (0 como decimal) até FF (255 como decimal). Em outras palavras, dois dígitos hexadecimais correspondem a um byte. Como curiosidade, um único dígito hexadecimal corresponde a 4 bits (meio byte), chamado de um nibble (ou nybble ou nyble). Como informação útil, é fácil de converter um valor hexadecimal para binário. Para isso, basta escrever o valor de cada dígito hexadecimal em formato binário. Por exemplo, F corresponde a 1111; E corresponde a 1110. Assim, FE corresponde a 11111110. Da mesma forma, pode-se converter cada seqüência de nibbles em um valor hexadecimal. Por exemplo, 1011 em binário corresponde ao valor hexadecimal B.

Com as informações anteriores, pode-se analisar a tabela. Em particular, as entradas para UTF-32 e UTF-16 permitem observar as diferenças de ordem com maior facilidade. As linhas para UTF-32 representam um mesmo valor inteiro de 4 bytes: 00 00 FE FF (valor hexadecimal; como valor decimal: 65279). Na versão big-endian, o valor é armazenado na ordem em que é escrito. Na versão little-endian, os bytes aparecem em ordem invertida. A interpretação como número inteiro sem sinal resultaria no valor 4294836224. No caso de BOM, a leitura do valor permite identificar a ordem de bytes no arquivo (por exemplo, comparando-se o valor lido com cada um dos possíveis resultados). O mesmo vale para as entradas em UTF-16.

No caso de UTF-8, usa-se três bytes como três números de 1 byte cada um (como decimais: 239, 187, 191). Como UTF-8 codifica valores em seqüências de 1 byte, a ordem de bits não afeta o resultado. Afinal, não é possível inverter a ordem de bytes caso exista apenas um byte por valor. Por exemplo, FE (254 como decimal) é um único byte. FE invertido continua FE, pois não se inverte bits individuais, mas bytes inteiros. Assim, a utilidade de BOM em arquivos UTF-8 é sinalizar que se trata de um arquivo de texto codificado em UTF-8 (ao invés de outra codificação, como ISO-8859-1, que codifica o alfabeto latino). Em outras palavras, ele serve como uma garantia adicional para reconhecimento do arquivo.

Quando se programa, o interpretador ou compilador encarrega-se de armazenar valores em memória na ordem de bytes correta. Contudo, quando se cria arquivos binários, é preciso saber a ordem adotada caso se deseje compartilhar o arquivo criado em máquinas com diferentes arquiteturas. Caso contrário, a interpretação de valores lidos pode ser feita incorretamente, resultando em erros potencialmente inesperados.

Inspecionando-se o Arquivo Binário Criado

Embora seja possível abrir um arquivo binário (que não seja um arquivo de texto) em um editor de texto (na realidade, qualquer arquivo pode ser aberto em um editor de texto), o resultado será peculiar.

Por exemplo, pode-se abrir o arquivo franco-garcia-arquivo_binario_escrita.bin em um editor de texto. Alguns caracteres estarão corretos, outros serão estranhos. Isso ocorre porque o editor de texto tenta interpretar todos os dados como caracteres codificados, algo que não reflete realmente os dados dos arquivos. Por exemplo, a imagem a seguir ilustra o arquivo binário criado na implementação em Python, aberto no editor de texto GNU Emacs. A parte esquerda mostra o arquivo interpretado como texto. A parte direita da imagem ilustra o arquivo como exibido pelo modo hexadecimal hexl-mode. No modo hexadecimal, os números em linhas e colunas são deslocamentos para acesso à memória. Os valores exibidos são números hexadecimais representado os bytes armazenados, em ordem little-endian.

Arquivo binário `franco-garcia-arquivo_binario_escrita.bin` aberto como arquivo texto (na parte esquerda da imagem) e em inspeção em modo hexadecimal (na parte direita da imagem) no editor de texto GNU Emacs. A imagem da esquerda apresenta vários valores que não são caracteres, escritos como valores de escape. A imagem da direita apresenta valores corretos para caracteres ASCII (ou seja, caracteres não acentuados).

Na parte esquerda da imagem, apenas valores codificados usando valores válidos em ASCII são apresentados corretamente. Caracteres acentuados são trocados por valores de escape. Os números inteiros e o número real armazenados também. Na parte direita, com a visualização em hexadecimal, todos os valores para os bytes estão corretos. Novamente, valores em texto ASCII são apresentados corretamente na parte interpretada assim (extremo direito da imagem). Os demais valores requerem conhecer o tipo de dado armazenado. Por exemplo, na última linha, a seqüência 0200 0000 em little-endian corresponde a seqüência hexadecimal 0000 0002 em big-endian, ou seja, ao número inteiro 2.

Logo, para interpretar corretamente os dados armazenados, é preciso saber onde cada dado começa e termina, assim como o tipo de dado armazenado. Para isso, pode-se usar um editor hexadecimal. A próxima imagem inspeciona o conteúdo gerado pelo programa em Python usando o editor hexadecimal ImHex. O editor hexadecimal apresenta valores binários armazenados no arquivo como números hexadecimais (por isso o nome). Caso se prefira visualizá-los de outra forma (por exemplo, como os valores binários), pode-se configurar o editor para alterar a forma de exibição de valores.

Inspeção do arquivo binário `franco-garcia-arquivo_binario_escrita.bin` gerado em Python usando o editor hexadecimal ImHex.

Na imagem, cada um dos dados está realçado para facilitar a identificação. A correspondência de padrões é feita usando as opções Pattern Editor e Pattern Data no editor ImHex. Quando os valores estão corretos, os dados são interpretados corretamente. Os tipos de dados estão transcritos no bloco abaixo. O tipo s32 é um inteiro com sinal de 32 bits (4 bytes); char é um caractere de 1 byte; float é um número real de 32 bits (4 bytes).

s32 tamanho_frase1 @ 0x00;
char frase1[26] @ 0x04;
s32 tamanho_frase2 @ 0x1E;
char frase2[26] @ 0x22;
s32 inteiro1 @ 0x3C;
s32 inteiro2 @ 0x40;
s32 inteiro3 @ 0x44;
float real @0x48;

O valor após o símbolo de arroba (@) é o offset do dado considerando o início do arquivo. Isso significa, por exemplo, que tamanho_frase2 inicia-se no byte com endereço 1E16 em base hexadecimal, ou seja, 3010 em base decimal. Caso se comece a leitura no offset 1E e leia-se 4 bytes, o valor extraído será o valor armazenado para o tamanho da segunda frase.

A definição de tipos permite apresentar os valores esperados.

NomeTipoValor
tamanho_frase1s3226
frase1StringOlá, meu nome é Franco.
tamanho_frase2s3226
frase1StringOlá, meu nome é Franco.
inteiro1s321
inteiro2s322
inteiro3s323
realfloat-1.23

O tamanho usando em cada vetor é o tamanho da cadeia de caracteres considerando os acentos (pois o arquivo está codificado em UTF-8).

Para as implementações em JavaScript e GDScript, a definição de tipos feita para Python deve funcionar corretamente (embora possa ser preciso alterar a ordem de bytes em JavaScript). Para a implementação em Lua, pode ser necessário ajustar os formatos e endereços, pois string.pack("s") utiliza um inteiro sem sinal de 8 bytes em máquinas 64-bit para armazenar o tamanho da cadeia de caracteres. Isso corresponde ao tipo u64 em ImHex.

u64 tamanho_frase1 @ 0x00;
char frase1[26] @ 0x08;
u64 tamanho_frase2 @ 0x22;
char frase2[26] @ 0x2A;
s32 inteiro1 @ 0x44;
s32 inteiro2 @ 0x48;
s32 inteiro3 @ 0x4C;
float real @0x50;

Em máquinas 32-bit, a definição para as demais linguagens pode funcionar em Lua, já que o valor usado para o tamanho da cadeia de caracteres seria um inteiro de 4 bytes, como nos outros casos.

Leitura de Arquivo Binário

Existem duas formas principais de ler um arquivo binário:

  1. Ler o arquivo todo como um bloco de memória, possivelmente abstraído como vetor de bytes;
  2. Extrair dados do arquivo binário.

Também é possível ler o arquivo byte por byte, embora isso nem sempre seja útil. Um dos exemplos deste tópico demonstra a leitura byte a byte para copiar arquivos.

Leitura do Arquivo Binário Inteiro

Em algumas linguagens, é possível ler um arquivo binário inteiro de uma vez e extrair todos os dados como campos de um registro. Por exemplo, em C e C++, basta uma chamada de função para ler e carregar todos os dados salvos de um arquivo para um registro POD de uma única vez (assim como também é possível salvar todos os dados de um registro em uma única operação). Isso ocorre porque é possível manipular a memória como um bloco de bytes. Caso os dados sejam armazenados em endereços contíguos, é possível salvar e restaurar a memória como se fosse um único bloco (porque ela é, de certa forma). Desde que o layout de dados seja o mesmo, é possível salvar e restaurar múltiplas variáveis cujos endereços comecem em um mesmo endereço base.

Em JavaScript, Python, Lua e GDScript, isso não é tão imediato, pois as linguagens abstraem o uso da memória. Embora em JavaScript o arquivo todo seja lido de uma vez, o resultado é um vetor de bytes, não um bloco de memória que pode ser interpretado de acordo com a vontade da programadora ou do programador.

Leitura de Arquivo Binário com Extração de Dados

Embora não se possa carregar todos os dados de uma única vez, ainda é simples extrair cada um dos dados salvos. As implementações em Python, Lua e GDScript são mais simples que a em JavaScript. A implementação em JavaScript requer converter bytes armazenados em variáveis de tipos intermediários antes de carregá-las em memória. Ela também requer uma página HTML para o envio do arquivo em navegadores; o código é apresentado na seqüência.

A implementação em Lua assume uso da versão 5.3 ou mais recente, como comentado para a criação de arquivos binários.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

// <https://www.francogarcia.com/pt-br/blog/ambientes-de-desenvolvimento-javascript/>
function adicione_elemento(valor, nome_elemento = "p") {
    const pai = document.getElementById("conteudo")
    const novo_elemento = document.createElement(nome_elemento)
    novo_elemento.innerHTML = valor
    pai.appendChild(novo_elemento)
}

function unpack_int32(bytes) {
    var array_buffer = new ArrayBuffer(4)
    var resultado = new DataView(array_buffer)
    for (let indice in bytes) {
        resultado.setUint8(indice, bytes[indice])
    }

    // true para little-endian, false para big-endian.
    return resultado.getInt32(0, true)
}

function unpack_float32(bytes) {
    var array_buffer = new ArrayBuffer(4)
    var resultado = new DataView(array_buffer)
    for (let indice in bytes) {
        resultado.setUint8(indice, bytes[indice])
    }

    // true para little-endian, false para big-endian.
    return resultado.getFloat32(0, true)
}

function leia_arquivo(arquivo_binario) {
    if (!arquivo_binario) {
        return
    } else if (!(arquivo_binario instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        let bytes = new Uint8Array(evento.target.result)
        let indice = 0

        let text_decoder = new TextDecoder()

        let tamanho_primeira_frase = unpack_int32(new Uint8Array(bytes.slice(indice, 4)))
        indice += 4
        let primeira_frase = text_decoder.decode(bytes.slice(indice, indice + tamanho_primeira_frase))
        indice += tamanho_primeira_frase

        let tamanho_segunda_frase = unpack_int32(new Uint8Array(bytes.slice(indice, indice + 4)))
        indice += 4
        let segunda_frase = text_decoder.decode(bytes.slice(indice, indice + tamanho_segunda_frase))
        indice += tamanho_segunda_frase

        let numeros_inteiros = []
        let soma_numeros_inteiros = 0
        for (let i = 0; i < 3; ++i) {
            let numero = unpack_int32(new Uint8Array(bytes.slice(indice, indice + 4)))
            indice += 4
            numeros_inteiros.push(numero)
            soma_numeros_inteiros += numero
        }

        console.log(indice, bytes)
        let numero_real = unpack_float32(new Uint8Array(bytes.slice(indice, indice + 4)))
        indice += 4

        console.log(primeira_frase)
        console.log(segunda_frase)
        console.log(numeros_inteiros, soma_numeros_inteiros)
        console.log(numero_real, "Número positivo?", numero_real > 0)

        adicione_elemento(primeira_frase)
        adicione_elemento(segunda_frase)
        adicione_elemento("[" + numeros_inteiros + "] " + soma_numeros_inteiros)
        adicione_elemento(numero_real + " " +  "Número positivo?" + " " + (numero_real > 0))
    }
    leitor_arquivos.readAsArrayBuffer(arquivo_binario)

    // Impede a submissão do formulário, permitindo a visualização do
    // resultado de adicione_elemento() na mesma página.
    return false
}
import io
import struct
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
    arquivo = open(caminho_arquivo, "rb")

    # A vírgula é importante, porque string.unpack() retorna uma tupla.
    tamanho_primeira_frase, = struct.unpack("i", arquivo.read(struct.calcsize("i")))
    texto_codificado = arquivo.read(tamanho_primeira_frase)
    primeira_frase = texto_codificado.decode("utf-8")

    tamanho_segunda_frase, = struct.unpack("i", arquivo.read(struct.calcsize("i")))
    texto_codificado = arquivo.read(tamanho_segunda_frase)
    segunda_frase = texto_codificado.decode("utf-8")

    numeros_inteiros = []
    soma_numeros_inteiros = 0
    for i in range(3):
        numero, = struct.unpack("i", arquivo.read(struct.calcsize("i")))
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    numero_real, = struct.unpack("f", arquivo.read(struct.calcsize("f")))

    arquivo.close()

    # Para remover a quebra de linha:
    # primeira_frase.rstrip()
    print(primeira_frase)
    print(segunda_frase)
    print(numeros_inteiros, soma_numeros_inteiros)
    print(numero_real, "Número positivo?", numero_real > 0)
except IOError as excecao:
    print("Erro ao tentar ler arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar ler arquivo binário.", file=sys.stderr)
    print(excecao)
function escreva_indentacao(nivel)
    for indentacao = 1, nivel do
        io.write("  ")
    end
end

function escreva_tabela(tabela, nivel)
    nivel = nivel or 1
    if (type(tabela) == "table") then
        io.write("{\n")
        for chave, valor in pairs(tabela) do
            escreva_indentacao(nivel)
            io.write(tostring(chave) .. ": ")
            escreva_tabela(valor, nivel + 1)
            io.write("\n")
        end
        escreva_indentacao(nivel - 1)
        io.write("},")
    else
        local aspas = ""
        if (type(tabela) == "string") then
            aspas = "\""
        end
        io.write(aspas .. tostring(tabela) .. aspas .. ",")
    end
end

-- O programa começa aqui.
local EXIT_FAILURE = 1
local caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
local arquivo = io.open(caminho_arquivo, "rb")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar ler arquivo binário.")
    os.exit(EXIT_FAILURE)
end

local tamanho_primeira_frase = string.unpack("T", arquivo:read(string.packsize("T")))
print(tamanho_primeira_frase)
local primeira_frase = arquivo:read(tamanho_primeira_frase)

local tamanho_segunda_frase = string.unpack("T", arquivo:read(string.packsize("T")))
print(tamanho_primeira_frase)
local segunda_frase = arquivo:read(tamanho_segunda_frase)

local numeros_inteiros = {}
local soma_numeros_inteiros = 0
for indice_numero = 1, 3 do
    local numero = string.unpack("i", arquivo:read(string.packsize("i")))
    table.insert(numeros_inteiros, numero)
    soma_numeros_inteiros = soma_numeros_inteiros + numero
end

local numero_real = string.unpack("f", arquivo:read(string.packsize("f")))

io.close(arquivo)

-- Para remover a quebra de linha:
-- string.sub(primeira_frase, 1, tamanho_primeira_frase - 1)
print(primeira_frase)
print(segunda_frase)
escreva_tabela(numeros_inteiros)
print(" " .. soma_numeros_inteiros)
print(numero_real, "Número positivo?", numero_real > 0)
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_binario_escrita.bin"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        printerr("Erro ao tentar ler arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    var primeira_frase = arquivo.get_pascal_string()
    var segunda_frase = arquivo.get_pascal_string()

    var numeros_inteiros = []
    var soma_numeros_inteiros = 0
    for i in range(3):
        var numero = arquivo.get_32()
        numeros_inteiros.append(numero)
        soma_numeros_inteiros += numero

    var numero_real = arquivo.get_float()

    arquivo.close()

    # Para remover a quebra de linha:
    # primeira_frase.rstrip("\n")
    print(primeira_frase)
    print(segunda_frase)
    printt(numeros_inteiros, soma_numeros_inteiros)
    printt(numero_real, "Número positivo?", numero_real > 0)

Para o código JavaScript, é necessário criar uma página HTML para o envio do arquivo.

<!DOCTYPE html>
<html lang="pt-BR">

  <head>
    <meta charset="utf-8">
    <title>Leitura de Arquivo Binário</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <header>
      <h1>Leitura de Arquivo</h1>
    </header>

    <main>
      <!-- Formulário para envio do arquivo texto. -->
      <form method="post"
            enctype="multipart/form-data"
            onsubmit="return leia_arquivo(arquivos.files[0])">
        <label for="arquivos">Escolha um arquivo binário:</label>
        <input id="arquivos"
               name="arquivos"
               type="file"
               accept="application/octet-stream"/>
        <input type="submit"/>
      </form>

      <div id="conteudo">
      </div>

      <!-- O nome do arquivo JavaScript deve ser igual ao definido abaixo. -->
      <script src="./script.js"></script>
    </main>
  </body>

</html>

Pode-se, agora, discutir o código-fonte. Os programas são simples; contudo, as operações necessárias para trabalhar com bytes brutos nas linguagens JavaScript, Python e Lua tornam a solução mais complexa do que ela realmente é. Comparar cada implementação com a versão em GDScript pode ser uma boa idéia, pois os códigos fazem exatamente as mesmas operações (normalmente nas mesmas ordens).

Um detalhe: a execução dos programas imprimirá uma quebra de linha ao final da escrita de cada frase. Isso não é um erro; a quebra de linha foi armazenada no arquivo e recuperada durante a leitura. Como o programa original salvou cadeias de caracteres com uma quebra de linha no final, elas foram recuperadas na última posição dos valores lidos. É possível removê-las após a leitura ou, caso elas não sejam desejadas, salvar as frases sem a quebra de linha final em cadeias de caracteres.

Assim como para a criação do arquivo binário, é comum que subrotinas para abertura de arquivos utilizem uma flag ou valor para a abertura de um arquivo binário (ao invés de um arquivo texto). O "rb" usando em algumas implementações serve para esse propósito.

Em Python, pode-se ler bytes de um arquivo usando-se read() (documentação). O parâmetro passado é o número de bytes desejado; struct.calcsize() (documentação) fornece tamanhos (em bytes) para os tipos de dados primitivos usados. O método unpack() (documentação) converte os dados lidos nos tipos de dados escolhidos no primeiro parâmetro. Para texto, usa-se decode() (documentação) para converter a seqüência de bytes em uma cadeia de caracteres.

Em Lua, io.read() ou file:read() (documentação) permitem ler bytes, caso se passe um número como parâmetro. Assim como em Python, usa-se o tamanho do dado do tipo esperado, usando-se string.packsize() (documentação); para a conversão de bytes em dados do tipo, usa-se string.unpack() (documentação).

Em GDScript, a manipulação de arquivos binários é simples, porque existem métodos para abstrair as conversões. A leitura de dados é feita com o método get_ correspondente ao método set_ usado para armazenar o dado no arquivo binário. Assim, por exemplo, o método get_pascal_string() (documentação) lê uma seqüência de um inteiro com tamanho e os bytes para carregar uma cadeia de caracteres; get_32() (documentação) carrega um número inteiro (sem sinal) de 4 bytes (32 bits; a recuperação de sinais será apresentada em uma função chamada uint32_para_int32() disponível em um exemplo); get_float() (documentação) carrega um número em ponto flutuante de 4 bytes.

A versão em JavaScript requer mais trabalho que as demais. A leitura do arquivo usa FileReader, como em arquivos texto. Contudo, o método escolhido para leitura é readAsArrayBuffer() (documentação), que fornece um vetor de bytes. O vetor é salvo em um Uint8Array (documentação), que é um vetor de números inteiros de um byte sem sinal (ou seja, um vetor de bytes). O restante da solução consiste em interpretar seqüências de bytes armazenadas no vetor como outros tipos de dados. A função criada unpack_int32() cria um inteiro de 4 bytes (32 bits; por isso o valor 4 passado para ArrayBuffer) usando um vetor com quatro bytes, criado por slice(). Os valores são armazenados em um ArrayBuffer (documentação), que é manipulado usando DataView (documentação) para a montagem do número inteiro, armazenado-se cada um dos quatro bytes com setUint8() (documentação), e lendo o valor resultante como um inteiro de 32 bits por meio de getInt32() (documentação). A função unpack_float32() opera de forma similar, mas interpreta o valor criado como um número real em ponto flutuante de 32 bits usando getFloat32() (documentação). Para decodificar os bytes codificados de uma cadeia de caracteres, usa-se o método decode() (documentação) de TextDecoder (documentação). Deve-se notar que indice é incrementado com o número de bytes processados após cada operação, para permitir a leitura do próximo valor esperado em seu índice inicial no vetor.

O exemplo em JavaScript reforça a idéia de que linguagens de programação são ferramentas e que o ideal é escolher a melhor ferramenta para cada problema. A análise do exemplo permite sugerir que, embora possível, JavaScript não é uma linguagem muito conveniente para manipulação de bytes. Outras linguagens podem ser opções melhores para operações assim. Por exemplo, Python, Lua e GDScript fornecem algumas facilidades para manipulação de dados, por meio de abstrações como subrotinas. Caso realmente se deseje trabalhar com JavaScript para manipulação de bytes, é conveniente definir subrotinas similares. Por outro lado, elas ainda tendem exigir mais esforços para manipulação de bytes que linguagens de nível mais baixo como C e C++, nas quais bastaria informar o compilador de como se deve interpretar a seqüencia de bytes.

Técnicas Usando Arquivos

Existem técnicas e recursos avançados para uso de arquivos, como memory-mapped files, paginação, e operações de entrada e saída não bloqueantes (assíncronas; asynchronous input/output (IO)). Existem recursos convenientes para segurança, como travas para arquivos (file locking). Também existem técnicas práticas para armazenamento e recuperação de dados, especialmente em linguagens de nível mais baixo.

Como este é um tópico introdutório, as próximas subseções destacam algumas técnicas mais simples, que podem ser usadas em todos os programas. Algumas delas são mais próximas de dicas ou conselhos que de técnicas propriamente ditas, embora ainda sejam úteis.

Verificar Se Arquivo Existe Antes de Criar um Novo

A abertura de um arquivo em modo de escrita é uma operação potencialmente destrutiva, porque trunca (apaga) dados existentes caso já exista um arquivo com o caminho escolhido. Existem duas formas tradicionais para se evitar o problema:

  1. A primeira é usar uma biblioteca de sistemas de arquivo que forneça uma função para verificar se um arquivo com o mesmo nome existe no caminho fornecido. O suporte para essa opção, contudo, depende da biblioteca padrão ou do uso de bibliotecas externas como dependências. Por exemplo, Python fornece vários módulos para esse fim. GDScript fornece a classe Directory (documentação);

  2. A segunda funciona em qualquer linguagem de programação com suporte a arquivos. Antes de criar um novo arquivo, pode-se tentar abri-lo. Caso a operação falhe, não existe arquivo no caminho especificado. Ou seja, é possível criar um novo arquivo sem riscos de perda de dados. Caso contrário, já existe um arquivo. Nesse caso, pode-se solicitar uma confirmação para ação, preferencialmente com um aviso sobre perda de dados. Outra opção é abrir o arquivo em modo de leitura e escrita, para se preservar os dados existes e inserir novos, ou em modo append, para adição de novo conteúdo no final do arquivo.

    Também existe uma técnica derivada da segunda forma, caso se deseje usar sempre a mesma forma de manipular arquivos. Caso o arquivo não exista, cria-se um. Após a criação, o novo arquivo é fechado e reaberto em modo de leitura e escrita. Caso o arquivo já exista, ele também é aberto para leitura a escrita. Essa abordagem será necessária para GDScript, já que não existe um modo append.

O próximo exemplo utiliza a segunda forma para:

  • Se o arquivo não existir, criá-lo com a mensagem: Olá, meu nome é Franco!;
  • Se o arquivo já existir, abri-lo em modo append, adicionando-se uma nova exclamação ao final do arquivo.

O modo de abertura para escrita append não destrói o arquivo original; contudo, neste exemplo, ele é usado para fins ilustrativos, já que não foi usado em nenhum momento prévio.

Como o exemplo não se aplica para JavaScript (para navegadores), ele será apresentado apenas para as demais linguagens.

import io
import sys

caminho_arquivo = "franco-garcia-reuso_arquivo.txt"

try:
    arquivo = open(caminho_arquivo, "r")
    arquivo.close()

    arquivo = open(caminho_arquivo, "a")
    arquivo.write("!")
    arquivo.close()

    print("Arquivo atualizado com sucesso.")
except IOError as excecao:
    try:
        arquivo = open(caminho_arquivo, "w")
        arquivo.write("Olá, meu nome é Franco!")
        arquivo.close()

        print("Arquivo criado com sucesso.")
    except IOError as excecao:
        print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
        print(excecao)
    except OSError as excecao:
        print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
        print(excecao)
except OSError as excecao:
    print("Erro ao tentar atualizar arquivo de texto.", file=sys.stderr)
    print(excecao)
local caminho_arquivo = "franco-garcia-reuso_arquivo.txt"
local arquivo = io.open(caminho_arquivo, "r")
if (arquivo == nil) then
    arquivo = io.open(caminho_arquivo, "w")
    arquivo:write("Olá, meu nome é Franco!")
    io.close(arquivo)

    print("Arquivo criado com sucesso.")
else
    io.close(arquivo)
    arquivo = io.open(caminho_arquivo, "a")
    arquivo:write("!")
    io.close(arquivo)

    print("Arquivo atualizado com sucesso.")
end
extends Node

func _ready():
    var caminho_arquivo = "franco-garcia-reuso_arquivo.txt"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        arquivo.open(caminho_arquivo, File.WRITE)
        arquivo.store_string("Olá, meu nome é Franco!")
        arquivo.close()

        print("Arquivo criado com sucesso.")
    else:
        arquivo.close()

        arquivo.open(caminho_arquivo, File.READ_WRITE)
        arquivo.seek_end()
        arquivo.store_string("!")
        arquivo.close()

        print("Arquivo atualizado com sucesso.")

As versões em Python e Lua utilizam o modo "a" para modo append em arquivo texto. Em Python, pode-se usar write() ao invés de writelines() para escrever uma única linha. Também é possível usar writelines(["Mensagem"]), caso se prefira.

Como GDScript não fornece-se o modo, abre-se o arquivo para leitura e atualização, usando-se File.READ_WRITE (não se deve usar File.WRITE_READ para esse propósito, pois ele remove os dados do arquivo original da mesma forma que File.WRITE). Em seguida, usa-se seek_end() (documentação) para avançar até o final do arquivo.

No primeiro uso do programa (ou ao executar o programa após apagar o arquivo), o programa criará e escreverá Olá, meu nome é Franco! no arquivo. Em execuções seguintes (assumindo que o arquivo exista), adicionar-se-á uma exclamação. Por exemplo, após executar o programa mais duas vezes, o arquivo armazenará Olá, meu nome é Franco!!!.

Uma aplicação prática para o código criado é logging.

Edição de Arquivo Existente: Inserção de Novo Conteúdo

Existem duas formas principais de modificar um arquivo existente: a forma simples e a forma mais complicada. A forma simples consiste em ler o arquivo todo em memória primária, modificar o conteúdo em memória primária, e reescrever o arquivo original usando a cópia modificada.

A forma mais complicada é fazer uma alteração in-place. Para preservar o conteúdo existente no arquivo, deve-se:

  1. Avançar até o ponto da nova inserção;
  2. Salvar o conteúdo deste ponto até o final do arquivo (em uma variável ou outro arquivo);
  3. Adicionar o novo conteúdo;
  4. Restaurar o conteúdo salvo após o novo conteúdo.

Isso é necessário porque a escrita de novos dados em qualquer posição que não seja o fim do arquivo pode resultar em sobrescrita de valores. Para entender o que ocorre, pode-se abrir um editor de texto, e escrever uma frase qualquer. Em seguida, pode-se pressionar a tecla Insert (Ins) do teclado e escrever em qualquer posição antes do final do texto. Ao invés de adicionar novos caracteres, o texto será modificado com o que se digitar. Para restaurar o comportamento original, deve-se apertar Insert novamente.

Para a programação de arquivos, existem bibliotecas que facilitam a operação. Por exemplo, em Python, pode-se usar o módulo fileinput (documentação) para modificações in-place de arquivos texto. Como o próximo exemplo aplica-se apenas para Python, a implementação adota fechamento automático.

import fileinput

caminho_arquivo = "franco-garcia-modificacao_in_place.txt"
try:
    with open(caminho_arquivo, "w", encoding="utf-8") as arquivo:
        arquivo.writelines([
            "Olá, meu nome é Franco!\n",
            "Tudo bem?\n",
            "Tchau!\n"
    ])

    with fileinput.FileInput(caminho_arquivo, inplace = True) as arquivo:
        for linha in arquivo:
            # print() é modificado para alterar o arquivo original.
            # linha contém a linha original.
            print("[Franco] ", linha, sep="", end="")

    print("Arquivo criado e modificado com sucesso.")
except IOError as excecao:
    print("Erro ao manipular arquivo.")
    print(excecao)
except OSError as excecao:
    print("Erro ao manipular arquivo.")
    print(excecao)

Por outro lado, desde que o número de bytes da alteração substitua o mesmo número de bytes presentes originalmente, a alteração pode ser mais simples. Assim, para simplificar a modificação, pode-se definir formatos de arquivos com estruturas de tamanho fixo pré-definido. Com arquivos binários, isso é mais simples: basta definir tamanhos esperados para cada parte do arquivo (preferencialmente usando um registro); a técnica será comentada em uma seção, usando um vetor como exemplo. Com arquivos textos, a técnica também é possível, embora mais restrita. Para se definir conteúdo com tamanho fixo, pode-se adicionar espaços ou outros caracteres considerados neutros (ou vazios) para preencher lacunas.

Por exemplo, para armazenar uma linha com nomes, pode-se definir um tamanho máximo de 15 caracteres. Caso um nome ocupasse menos caracteres, o texto é preenchido com o caractere escolhido como neutro para completar 15 caracteres (deve-se considerar que caracteres acentos podem ocupar múltiplos bytes). or ............... seriam duas possibilidades para um nome vazio. A primeira tem 15 espaços; a segunda tem 15 pontos finais. Franco ou Franco......... são dois exemplos de nome que cabem nos espaços. Uma modificação para Franco Garcia ou Franco Garcia.. seria possível no espaço disponível. A modificação também remover conteúdo. Uma modificação para Garcia ou Garcia......... também seria válida e caberia no espaço existente.

O mesmo vale para números. No caso, existiria um limite máximo para uma combinação da quantidade de dígitos, vírgula decimal, sinal, etc. Por exemplo, 12345, 1.234, -1.23, 0 ou 0 são números codificados em texto com exatamente 5 caracteres.

Em suma, quando possível, é mais fácil usar a forma simples e reescrever todo o conteúdo do arquivo. Embora ela possa parecer rudimentar, ela é usada com freqüência em muitos programas. A próxima seção fornece um exemplo de como aproveitá-la para gerar cópias de segurança.

Arquivo Binário: Acesso Aleatório e Modificação

Em arquivos binários, é possível manipular valores armazenados como se fosse um vetor ou um registro. Para isso, pode usar operações de seek para acesso aleatório.

Como JavaScript para navegadores lê o arquivo todo de uma vez, omite-se o exemplo para a linguagem. Afinal, para o uso do exemplo, é necessário usar uma subrotina seek() para alterar a posição do arquivo. Para um exemplo mais compacto, usa-se asserções (assert()) ao invés de tratamento de erros. Em um programa real, o tratamento adequado para erros é essencial. Para facilitar a leitura, dividiu-se a implementação nos procedimentos crie_arquivo(), modifique_arquivo() e imprima_arquivo(), chamados nessa ordem. Por curiosidade, o procedimento imprima_arquivo() foge ao padrão adotado de escreva_arquivo(), para evitar confusão com escreva_valor().

import io
import struct
import sys

TAMANHO_INTEIRO = struct.calcsize("i")

# indice 0 corresponde ao primeiro valor.
def acesse_valor(arquivo, indice):
    # O incremento por TAMANHO_INTEIRO permite pular o valor
    # armazenado com o total de números, para leitura do início
    # dos valores do vetor.
    offset_cabecalho = TAMANHO_INTEIRO
    offset_numeros = offset_cabecalho + indice * TAMANHO_INTEIRO
    arquivo.seek(offset_numeros)

def leia_valor(arquivo, indice):
    acesse_valor(arquivo, indice)
    resultado, = struct.unpack("i", arquivo.read(TAMANHO_INTEIRO))

    return resultado

def escreva_valor(arquivo, indice, valor):
    acesse_valor(arquivo, indice)
    arquivo.write(struct.pack("i", valor))

def crie_arquivo(caminho_arquivo):
    print("Criação de " + caminho_arquivo)

    arquivo = open(caminho_arquivo, "wb")
    assert(arquivo)

    total_numeros = 20
    arquivo.write(struct.pack("i", total_numeros))

    for numero in range(total_numeros):
        arquivo.write(struct.pack("i", numero + 1))

    arquivo.close()

def modifique_arquivo(caminho_arquivo):
    print("Modificação de " + caminho_arquivo)

    arquivo = open(caminho_arquivo, "rb+")
    assert(arquivo)

    total_numeros, = struct.unpack("i", arquivo.read(TAMANHO_INTEIRO))

    # Acessa o 11o valor inteiro armazenado.
    indice = 10
    valor = leia_valor(arquivo, indice)
    print(valor, valor == 11)
    # Modifica o 11o valor inteiro armazenado.
    escreva_valor(arquivo, indice, 12345)
    valor = leia_valor(arquivo, indice)
    print(valor, valor == 12345)

    escreva_valor(arquivo, 0, -1111)
    escreva_valor(arquivo, 19, 191919)

    arquivo.close()

def imprima_arquivo(caminho_arquivo):
    print("Valores em " + caminho_arquivo)

    arquivo = open(caminho_arquivo, "rb")
    assert(arquivo)

    total_numeros, = struct.unpack("i", arquivo.read(TAMANHO_INTEIRO))
    for indice_numero in range(total_numeros):
        numero, = struct.unpack("i", arquivo.read(TAMANHO_INTEIRO))
        print(numero)

    arquivo.close()

def main():
    caminho_arquivo = "franco-garcia-numeros.bin"
    crie_arquivo(caminho_arquivo)
    print()
    modifique_arquivo(caminho_arquivo)
    print()
    imprima_arquivo(caminho_arquivo)

if (__name__ == "__main__"):
    main()
local TAMANHO_INTEIRO = string.packsize("i")

-- indice 0 corresponde ao primeiro valor.
function acesse_valor(arquivo, indice)
    -- O incremento por TAMANHO_INTEIRO permite pular o valor
    -- armazenado com o total de números, para leitura do início
    -- dos valores do vetor.
    local offset_cabecalho = TAMANHO_INTEIRO
    local offset_numeros = offset_cabecalho + indice * TAMANHO_INTEIRO
    arquivo:seek("set", offset_numeros)
end

function leia_valor(arquivo, indice)
    acesse_valor(arquivo, indice)
    local resultado = string.unpack("i", arquivo:read(TAMANHO_INTEIRO))

    return resultado
end

function escreva_valor(arquivo, indice, valor)
    acesse_valor(arquivo, indice)
    arquivo:write(string.pack("i", valor))
end

function crie_arquivo(caminho_arquivo)
    print("Criação de " .. caminho_arquivo)

    local arquivo = io.open(caminho_arquivo, "wb")
    assert(arquivo)

    local total_numeros = 20
    arquivo:write(string.pack("i", total_numeros))

    for numero = 1, total_numeros do
        arquivo:write(string.pack("i", numero))
    end

    arquivo:close()
end

function modifique_arquivo(caminho_arquivo)
    print("Modificação de " .. caminho_arquivo)

    local arquivo = io.open(caminho_arquivo, "r+b")
    assert(arquivo)

    local total_numeros = string.unpack("i", arquivo:read(TAMANHO_INTEIRO))

    -- Acessa o 11o valor inteiro armazenado.
    local indice = 10
    local valor = leia_valor(arquivo, indice)
    print(valor, valor == 11)
    -- Modifica o 11o valor inteiro armazenado.
    escreva_valor(arquivo, indice, 12345)
    valor = leia_valor(arquivo, indice)
    print(valor, valor == 12345)

    escreva_valor(arquivo, 0, -1111)
    escreva_valor(arquivo, 19, 191919)

    arquivo:close()
end

function imprima_arquivo(caminho_arquivo)
    print("Valores em " .. caminho_arquivo)

    local arquivo = io.open(caminho_arquivo, "rb")
    assert(arquivo)

    local total_numeros = string.unpack("i", arquivo:read(TAMANHO_INTEIRO))
    for indice_numero = 1, total_numeros do
        local numero = string.unpack("i", arquivo:read(TAMANHO_INTEIRO))
        print(numero)
    end

    arquivo:close()
end

function main()
    local caminho_arquivo = "franco-garcia-numeros.bin"
    crie_arquivo(caminho_arquivo)
    print()
    modifique_arquivo(caminho_arquivo)
    print()
    imprima_arquivo(caminho_arquivo)
end

main()
extends Node

# 4 bytes = 32 bits
const TAMANHO_INTEIRO = 4

const INT31_MAX = 1 << 31
const INT32_MAX = 1 << 32

func uint32_para_int32(valor):
    var resultado = (valor + INT31_MAX) % INT32_MAX - INT31_MAX

    return resultado

# indice 0 corresponde ao primeiro valor.
func acesse_valor(arquivo, indice):
    # O incremento por TAMANHO_INTEIRO permite pular o valor
    # armazenado com o total de números, para leitura do início
    # dos valores do vetor.
    var offset_cabecalho = TAMANHO_INTEIRO
    var offset_numeros = offset_cabecalho + indice * TAMANHO_INTEIRO
    arquivo.seek(offset_numeros)

func leia_valor(arquivo, indice):
    acesse_valor(arquivo, indice)
    var resultado = arquivo.get_32()

    return resultado

func escreva_valor(arquivo, indice, valor):
    acesse_valor(arquivo, indice)
    arquivo.store_32(valor)

func crie_arquivo(caminho_arquivo):
    print("Criação de " + caminho_arquivo)

    var arquivo = File.new()
    arquivo.open(caminho_arquivo, File.WRITE)
    assert(arquivo.is_open())

    var total_numeros = 20
    arquivo.store_32(total_numeros)

    for numero in range(total_numeros):
        arquivo.store_32(numero + 1)

    arquivo.close()

func modifique_arquivo(caminho_arquivo):
    print("Modificação de " + caminho_arquivo)

    var arquivo = File.new()
    arquivo.open(caminho_arquivo, File.READ_WRITE)
    assert(arquivo.is_open())

    var total_numeros = arquivo.get_32()

    # Acessa o 11o valor inteiro armazenado.
    var indice = 10
    var valor = leia_valor(arquivo, indice)
    printt(valor, valor == 11)
    # Modifica o 11o valor inteiro armazenado.
    escreva_valor(arquivo, indice, 12345)
    valor = leia_valor(arquivo, indice)
    printt(valor, valor == 12345)

    escreva_valor(arquivo, 0, -1111)
    escreva_valor(arquivo, 19, 191919)

    arquivo.close()

func imprima_arquivo(caminho_arquivo):
    print("Valores em " + caminho_arquivo)

    var arquivo = File.new()
    arquivo.open(caminho_arquivo, File.READ)
    assert(arquivo.is_open())

    var total_numeros = arquivo.get_32()
    for indice_numero in range(total_numeros):
        var numero = uint32_para_int32(arquivo.get_32())
        print(numero)

    arquivo.close()

func _ready():
    var caminho_arquivo = "franco-garcia-numeros.bin"
    crie_arquivo(caminho_arquivo)
    print()
    modifique_arquivo(caminho_arquivo)
    print()
    imprima_arquivo(caminho_arquivo)

O arquivo criado armazena o número de valores inteiros armazenados no arquivo (como um inteiro de 4 bytes ou 32 bits), seguido desta quantidade de números inteiros de 4 bytes. O tamanho definido por total_numeros permite conhecer quantos números existem no arquivo. Alternativamente, caso se omitisse o tamanho e se salvasse apenas números no arquivo, poder-se-ia determinar a quantidade dos números dividindo-se o tamanho total do arquivo pelo tamanho de cada valor armazenado (4 bytes ou 32 bits).

De qualquer forma, a adição do tamanho (como comprimento) é interessante em termos conceituais. Por conveniência, o valor 4 bytes foi armazenado em uma constante chamada TAMANHO_INTEIRO.

A função mais importante na implementação chama-se acesse_valor(). Ela utiliza seek() para avançar na posição do arquivo. A implementação pula 4 bytes (que corresponde à região de memória na qual o valor do tamanho está armazenada armazenada no arquivo), e, em seguida, calcula um deslocamento multiplicando o índice pelo tamanho de cada valor (4 bytes). A operação é similar cálculo de um deslocamento de memória em um vetor, usando um recurso de baixo nível chamado ponteiro (pointer). Com a função, pode-se acessar os valores armazenados como se fossem valores de um vetor, mas armazenados em memória secundária (no arquivo) ao invés de em memória primária, e com as subrotinas leia_valor() e escreva_valor() ao invés do uso do operador colchetes. Como se trata de deslocamentos em memória a partir de um endereço base, a "indexação" inicia-se em zero.

O restante da implementação consiste na criação, modificação e leitura do arquivo criado. Em modifique_arquivo(), é importante ressaltar que o arquivo foi aberto em modo de leitura e escrita binária. Em Lua, usa-se "w+b" para designer o modo, Em Python, pode usar-se "wb+", "w+b" ou qualquer outra combinação dos três símbolos. Em GDScript, usa-se File.READ_WRITE; novamente, é importante usar File.READ_WRITE ao invés de File.WRITE_READ, pois não se deseja apagar o conteúdo de um arquivo existe, mas mantê-lo.

Para leitura e escrita de arquivo texto, o modo é semelhante, mas se omite o b em Lua e Python. Ou seja, usa-se "wb".

Para o arquivo binário, a edição pode ser feita in-place desde que o tamanho em bytes de cada valor modificado continue o mesmo. Isso é o que permite o acesso aleatório na posição específica tanto para leitura, quanto para escrita. Desde que o tipo e o tamanho seja o mesmo, qualquer valor válido para o tipo pode ser modificado sem comprometer a estrutura do arquivo.

Para comparação, arquivos texto utilizam acesso seqüêncial. Ou seja, é necessário percorrer o arquivo até encontrar um valor desejado, pois cada dado pode possui um número diferente de bytes armazenados. Em particular, qualquer modificação que altere o tamanho da string alterada afetará a seqüência de bytes no arquivo, inibindo o uso de acesso aleatório. Por outro lado, a leitura atenta da frase anterior revela que, caso sempre se salve valores com o mesmo tamanho para a cadeia de caracteres representando o valor (como mencionado em seção prévia), é possível simular acesso aleatório no arquivo texto.

Voltando ao exemplo, a implementação em GDScript requer atenção ao usar números negativos em arquivos binários. Como os valores são salvos e recuperados sem sinal, deve-se reinterpretar o número após a leitura caso o valor possa ser negativo. A função uint32_para_int32() possui este propósito, implementada como uma adaptação da nota fornecida para store_16() (documentação). Caso o valor como um número sem sinal esteja no intervalo considerado como um número negativo em complemento de dois, a seqüência de operações restaura o sinal negativo para o número. Caso o número seja positivo, o valor é preservado. O operador << usado é uma operação bit a bit (bitwise) de deslocamento de bits. Em particular, para números inteiros positivos (ou seja, números reais), o resultado de um deslocamento para a esquerda é equivalente a multiplicar um número por dois; contudo, a operação é mais rápida que a potenciação. Assim, o valor resultante corresponde a para INT31_MAX e para INT32_MAX.

Cópia Temporária Ao Salvar (ou Para Versões Anteriores do Arquivo)

Uma das formas mais simples de editar um arquivo existente é substituí-lo por um novo com o conteúdo atualizado. Uma prática comum é criar o arquivo novo da forma de um arquivo temporário, mantendo o original como uma cópia de segurança (back-up). Para isso:

  1. Cria-se um novo arquivo com um nome diferente do original;
  2. Se o arquivo for criado com sucesso, renomeia-se o arquivo original como uma cópia de segurança. Alternativamente, pode-se apagar o arquivo original (caso não se queira uma cópia de segurança);
  3. Renomeia-se o nome do novo arquivo com o nome original.

Operações de renomear e excluir arquivos são comuns em Interface de Programação de Aplicações (Application Programming Interfaces; APIs) para manipulação de arquivos.

Como esse tipo de operação não é comum em navegadores (nem seguro; permitir que um website apague arquivos do computador não é uma boa idéia), novamente omitir-se-á um exemplo para JavaScript.

import io
import os
import sys

caminho_arquivo = "franco-garcia-arquivo_back_up.txt"
arquivo = open(caminho_arquivo, "w")
assert(arquivo)
arquivo.write("Olá, meu nome é Franco!\n")
arquivo.close()

arquivo = open(caminho_arquivo, "r")
assert(arquivo)
conteudo = arquivo.read()
arquivo.close()

conteudo += "Tchau!\n"

arquivo = open(caminho_arquivo + ".TMP", "w")
assert(arquivo)
arquivo.write(conteudo)
arquivo.close()

os.rename(caminho_arquivo, caminho_arquivo + ".BAK")
# Alternativamente, para apagar o arquivo original:
# os.remove(caminho_arquivo)

os.rename(caminho_arquivo + ".TMP", caminho_arquivo)

print("Arquivo criado e modificado com sucesso.")
local caminho_arquivo = "franco-garcia-arquivo_back_up.txt"
local arquivo = io.open(caminho_arquivo, "w")
assert(arquivo)
arquivo:write("Olá, meu nome é Franco!\n")
io.close(arquivo)

arquivo = io.open(caminho_arquivo, "r")
assert(arquivo)
local conteudo = arquivo:read("*all")
io.close(arquivo)

conteudo = conteudo .. "Tchau!\n"

arquivo = io.open(caminho_arquivo .. ".TMP", "w")
assert(arquivo)
arquivo:write(conteudo)
io.close(arquivo)

os.rename(caminho_arquivo, caminho_arquivo .. ".BAK")
-- Alternativamente, para apagar o arquivo original:
-- os.remove(caminho_arquivo)

os.rename(caminho_arquivo .. ".TMP", caminho_arquivo)

print("Arquivo criado e modificado com sucesso.")
extends Node

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_back_up.txt"
    var arquivo = File.new()
    arquivo.open(caminho_arquivo, File.WRITE)
    assert(arquivo.is_open())
    arquivo.store_string("Olá, meu nome é Franco!\n")
    arquivo.close()

    arquivo.open(caminho_arquivo, File.READ)
    assert(arquivo.is_open())
    var conteudo = arquivo.get_as_text()
    arquivo.close()

    conteudo += "Tchau!\n"

    arquivo.open(caminho_arquivo + ".TMP", File.WRITE)
    assert(arquivo.is_open())
    arquivo.store_string(conteudo)
    arquivo.close()

    var directory = Directory.new()
    directory.rename(caminho_arquivo, caminho_arquivo + ".BAK")
    # Alternativamente, para apagar o arquivo original:
    # directory.remove(caminho_arquivo)

    directory.rename(caminho_arquivo + ".TMP", caminho_arquivo)

    print("Arquivo criado e modificado com sucesso.")

Para simplificar o exemplo, a implementação utiliza asserções (assert()) ao invés de tratamento de erro. Em um programa real, o tratamento adequado para erros é essencial.

A implementação é simples. Cria-se um arquivo com algum conteúdo (que será modificado); fecha-o. Abre-se o arquivo criado; lê-se o conteúdo; fecha-o. Modifica-se o conteúdo do arquivo. Cria-se um novo arquivo para o conteúdo modificado; para diferenciá-lo do original, anexou-se a extensão .TMP, comumente usada para arquivos temporários. A extensão poderia usar letras minúsculas; o uso de maiúsculas é para realçar a extensão.

Com o novo arquivo criado, pode-se renomear ou apagar o arquivo original. No exemplo, a implementação padrão renomeia o arquivo original adicionado o sufixo .BAK, comumente usando para back-up. A linha comentada permite apagar o arquivo original. Caso ela seja usada, não se deve renomear o arquivo (caso contrário, deve-se atualizar o valor do parâmetro). É importante notar que se deve apagar o arquivo original, não o novo arquivo. Apagar o arquivo original apenas após a geração do novo arquivo permite restaurar o arquivo antigo caso a criação do novo arquivo falhe. Além disso, arquivos apagados por APIs de programação normalmente são deletados permanentemente, ou seja, eles não são enviados para a lixeira do sistema operacional. Portanto, deve-se tomar cuidado ao apagar arquivos em programas.

Em Python, o módulo os abstrai operações do sistema operacional. A subrotina os.rename() (documentação) permite renomear arquivos; os.remove() (documentação) permite apagar arquivos. Em Lua, os.rename() (documentação) permite renomear arquivos; os.remove() (documentação) permite apagar um arquivo. Em GDScript, a classe Directory fornece o método rename() (documentação) para renomear arquivos, e remove() (documentação) para apagar arquivos.

Novamente, deve usar a subrotina para apagar arquivos com cuidado. Caso contrário, perdas de dados podem ocorrer. Além disso, embora seja possível (em potencial) apagar qualquer arquivo que se tenha permissão de leitura ou escrita, não é um comportamento ético apagar arquivos que não sejam criados pelo próprio programa (exceto caso o programa criado tenha o propósito de apagar arquivos; ainda assim, é educado solicitar autorização antes de quaisquer remoções ou renomeações).

Além disso, algumas APIs podem fornecer subrotinas específicas para a criação de arquivos temporários. Por exemplo, Python provê o módulo tempfile (documentação) para criação de arquivos e diretórios temporários. Lua fornece io.tmpfile() (documentação) para criar um arquivo que é apagado automaticamente ao final do programa. A linguagem também fornece os.tmpname() (documentação) para gerar nomes para arquivos temporários.

Tamanho do Arquivo

A operação de seek permite alterar a posição no arquivo. Tanto em arquivos texto quanto em binários, é possível avançar para o final do arquivo ou retroceder para o início. A operação complementar de seek normalmente chama-se tell, que fornece a posição atual do cursor no arquivo. Caso se obtenha a posição do final e a do início, a subtração do valor do fim pelo do início pode fornecer o tamanho do arquivo em bytes.

import io

caminho_arquivo = "franco-garcia-tamanho_arquivo.txt"
arquivo = open(caminho_arquivo, "w")
assert(arquivo)
arquivo.write("Olá, meu nome é Franco!\n")
arquivo.close()

arquivo = open(caminho_arquivo, "r")
assert(arquivo)

posicao_inicio = arquivo.tell()
arquivo.seek(0, io.SEEK_END) # ou 2
posicao_fim = arquivo.tell()

arquivo.close()

tamanho = posicao_fim - posicao_inicio
print("Tamanho do arquivo: ", tamanho, " bytes.")
local caminho_arquivo = "franco-garcia-tamanho_arquivo.txt"
local arquivo = io.open(caminho_arquivo, "w")
assert(arquivo)
arquivo:write("Olá, meu nome é Franco!\n")
io.close(arquivo)

arquivo = io.open(caminho_arquivo, "r")
assert(arquivo)

posicao_inicio = arquivo:seek()
arquivo:seek("end")
posicao_fim = arquivo:seek()

io.close(arquivo)

local tamanho = posicao_fim - posicao_inicio
print("Tamanho do arquivo: " .. tamanho .. " bytes.")
extends Node

func _ready():
    var caminho_arquivo = "franco-garcia-tamanho_arquivo.txt"
    var arquivo = File.new()
    arquivo.open(caminho_arquivo, File.WRITE)
    assert(arquivo.is_open())
    arquivo.store_string("Olá, meu nome é Franco!\n")
    arquivo.close()

    arquivo.open(caminho_arquivo, File.READ)
    assert(arquivo.is_open())

    var posicao_inicio = arquivo.get_position()
    arquivo.seek_end()
    var posicao_fim = arquivo.get_position()

    arquivo.close()

    var tamanho = posicao_fim - posicao_inicio
    printt("Tamanho do arquivo: ", tamanho, " bytes.")

Em Python, tell() (documentação) fornece o valor para a posição atual do arquivo. Em Lua, isso é feito usando-se seek() sem parâmetros (documentação). Em GDScript, usa-se get_position() (documentação).

Contudo, algumas implementações de subrotinas para arquivos podem fornecer um tamanho incorreto. Assim, caso possível, pode ser preferível usar uma API para o sistema do arquivo para solicitar o tamanho real do arquivo.

Serialização (Serialization ou Marshalling) e Desserialização (Deserialization ou ou Unmarshalling)

Quando se trabalha com arquivos ou transmissão de dados via rede, termos como serialização ou marshalling são usados para se referir ao armazenamento de dados em um formato para posterior recuperação. Os dados restaurados devem ser idênticos aos dados originais, assim como os tipos usados para armazená-los. Uma aplicação comum é serializar dados armazenados em objetos de classes ou variáveis de registros.

Uma abordagem popular de serialização usando arquivos texto consiste no uso de JSON. Para ilustrar a prática, a próxima seção fornece um exemplo.

Exemplos

Em problemas envolvendo arquivos, as operações de entrada e saída usando arquivos tende a ser a parte mais simples da solução. Carrega-se dados de um arquivo para a memória primária; salva-se dados da memória primária para o arquivo. O restante da solução compreende processamento de cadeias de caracteres, conversões de tipos, inicialização de dados em registros...

Como os exemplos fornecidos ao longo deste tópico ilustram como usar as subrotinas importantes para manipulação de arquivos, os exemplos desta seção serão mais práticos e diferentes, embora potencialmente mais complexos.

Com os conceitos explorados anteriormente e arquivos, é possível começar a explorar tópicos e problemas mais interessantes. Por exemplo, a possibilidade de gerar arquivos para uso com outros programas permite a criação de conteúdo multimídia. Assim, pode-se investigar formatos formatos simples para imagens e sons. Os formatos simples podem ser reproduzidos por programas específicos (como reprodutores de mídia (media player)) ou convertidos para formatos mais comuns. Isso pode ser feito programaticamente ou usando ferramentas para conversão.

Logo, os exemplos demonstram possíveis aplicações do conhecimento em programação obtido até este ponto. De certa forma, eles são o fechamento de uma jornada básica de aprendizado de programação, marcando a transição de iniciante para alguém com conhecimento adequado de programação. Adequado no sentido de que se possui a base e os fundamentos para programação de muitos sistemas complexos, mas ainda sem a experiência e a prática necessária para se atingir competência e proficiência. Em outras palavras, tem-se potencial; é hora, pois, de materializar o potencial em sistemas.

Para propósitos didáticos e facilitar a leitura e o entendimento, as implementações não possuem otimizações. A falta de otimizações poderá ser notada em alguns exemplos, principalmente caso se aumente o valor de alguns parâmetros. Por exemplo, algumas implementações utilizam arquivos texto e cadeia de caracteres, quando o ideal seria usar arquivos binários e tipos numéricos. Ainda assim, mesmo optando-se pela simplicidade, os exemplos podem ser complexos para iniciantes (ou mesmo para profissionais; não é raro encontrar profissionais que nunca criaram uma imagem ou som programaticamente, especialmente sem o uso de bibliotecas). Por outro lado, ao final dos exemplos, você entenderá melhor como um computador funciona.

Além disso, algumas seções listam e descrevem programas para uso de alguns dos arquivos criados. A simplicidade para implementação de um formato pode resultar em maior complexidade para uso. Arquivos multimídia sem cabeçalhos requerem configuração manual para uso. Em geral, pode-se instalar apenas o recomendado, usar uma alternativa online, ou mesmo apenas simplesmente acompanhar o material.

Alternativamente, o autor disponibilizou aplicações online para visualização/reprodução dos formatos mais incomuns na página de Ferramentas. Caso você opte por usá-las, você pode ignorar as subseções descrevendo ferramentas especializadas, embora a leitura ainda possa ser útil.

Serialização e Desserialização com Arquivos JSON

O programa para receitas definido em Registros pode servir como uma boa aplicação para ilustrar o uso se serialização usando JSON. Para simplificar os blocos de código apresentados como exemplo, apenas a definição de cada registro, e o código para serialização e desserialização são apresentados. O restante do código original pode ser restaurado para um programa completo. Inclusive, poder-se-ia adicionar novas opções para o menu para salvar e carregar arquivos.

A versão em Lua requer o uso de uma biblioteca externa para manipulação de JSON. Por simplicidade, escolheu-se a biblioteca json.lua, que define subrotinas para manipulação de JSON em um único arquivo. Detalhes para uso são fornecidos antes da documentação das subrotinas empregadas.

Exemplo Simples: Registro Ingrediente

Para começar com um exemplo simples, pode-se considerar JSON como o formato do conteúdo, sem usar arquivos. Para isso, o registro Ingrediente pode ser uma boa escolha para entender como JSON funciona.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

let original = new Ingrediente("Água", 3.0, "Xícaras")
// Serialização.
let texto_json = JSON.stringify(original)
console.log(texto_json)

// Desserialização.
let objeto_json = JSON.parse(texto_json)
console.log(objeto_json)
let recuperado = new Ingrediente
recuperado.nome = objeto_json["nome"]
recuperado.quantidade = objeto_json.quantidade
recuperado.unidade_medida = objeto_json.unidade_medida
console.log(recuperado)

// Se Ingrediente fosse um JavaScript Object usado como registro
// (por exemplo, sem subrotinas em variáveis):
original = {
    "nome": "Água",
    "quantidade": 3.0,
    "unidade_medida": "Xícaras"
}
texto_json = JSON.stringify(original)
console.log(texto_json)

recuperado = JSON.parse(texto_json)
console.log(recuperado)
import json

class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

original = Ingrediente("Água", 3.0, "Xícaras")
# Serialização.
texto_json = json.dumps({
    "nome": original.nome,
    "quantidade": original.quantidade,
    "unidade_medida": original.unidade_medida
})
print(texto_json)

# Desserialização.
objeto_json = json.loads(texto_json)
print(objeto_json)
recuperado = Ingrediente()
recuperado.nome = objeto_json["nome"]
recuperado.quantidade = objeto_json["quantidade"]
recuperado.unidade_medida = objeto_json["unidade_medida"]
print(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)
-- <https://github.com/rxi/json.lua>
local json = require "json"

function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

local original = crie_ingrediente("Água", 3.0, "Xícaras")
-- Serialização.
local texto_json = json.encode(original)
print(texto_json)

-- Desserialização.
local objeto_json = json.decode(texto_json)
print(objeto_json.nome, objeto_json.quantidade, objeto_json.unidade_medida)
local recuperado = crie_ingrediente()
recuperado.nome = objeto_json["nome"]
recuperado.quantidade = objeto_json["quantidade"]
recuperado.unidade_medida = objeto_json["unidade_medida"]
print(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)
extends Node

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

func _ready():
    var original = Ingrediente.new("Água", 3.0, "Xícaras")
    # Serialização.
    var texto_json = to_json({
        "nome": original.nome,
        "quantidade": original.quantidade,
        "unidade_medida": original.unidade_medida
    })
    print(texto_json)

    # Desserialização.
    var objeto_json = parse_json(texto_json)
    print(objeto_json)
    var recuperado = Ingrediente.new()
    recuperado.nome = objeto_json["nome"]
    recuperado.quantidade = objeto_json["quantidade"]
    recuperado.unidade_medida = objeto_json["unidade_medida"]
    printt(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)

Pode-se criar conteúdo em JSON sem arquivos (embora possivelmente não seja muito útil). JSON define o formato do conteúdo a ser armazenado em um arquivo texto; ele não é um tipo diferente de arquivo. O conteúdo JSON é armazenado em um arquivo texto. Assim, para se criar um arquivo JSON com o conteúdo gerado, bastaria salvar texto_json em um arquivo de texto (por exemplo, meu_ingrediente.json). O arquivo resultante possuiria o seguinte conteúdo:

{"nome":"Água","quantidade":3.0,"unidade_medida":"Xícaras"}

O valor 3.0 pode ser convertido para 3. Além disso, a ordem dos pares chave e valor pode ser diferente; assim como em um dicionário ou tabela hash, ela é irrelevante. Algumas implementações ordenam as chaves em ordem alfabética, outras usam a ordem de inserção dos valores, outras empregam ordem aleatória. Para se manter a ordem de valores em um arquivo JSON de forma portável, é necessário usar um vetor.

Assim como linguagens de programação como JavaScript e Lua, pode-se omitir quebras de linhas e espaçamentos em um arquivo JSON. Isso reduz o tamanho do arquivo para compartilhamento e torna o processamento mais rápido, mas dificulta a leitura por seres humanos. Para facilitar a leitura por pessoas, pode-se formatá-lo com indentação adequada. Existem ferramentas para isso, inclusive online (por exemplo, pode-se procurar por "json pretty print", "json beautifier" ou "json formatter"). Alguns editores de texto para programação contém funcionalidades para formatação.

{
  "nome": "Água",
  "quantidade": 3.0,
  "unidade_medida": "Xícaras"
}

Como o nome sugere, o conteúdo de um arquivo JSON é um objeto JavaScript (JavaScript Object) válido. Assim, pode-se copiar e colar qualquer um dos blocos anteriores no console de um navegador para visualizar o conteúdo do arquivo de forma estruturada. Também é possível abrir arquivos JSON no navegador, para visualização estruturada.

Trabalhar com JSON é bastante similar a trabalhar com dicionários e vetores em linguagens de programação. De fato, várias APIs para JSON abstraem o uso do formato por meio de dicionários ou de vetores de dicionários.

No caso deste primeiro exemplo, trabalhar com o conteúdo JSON criado é como usar um dicionário. Cada dicionário corresponde ao tipo definido para o registro Ingrediente codificado como dicionário.

A criação do conteúdo para JSON é simples em JavaScript. As subrotinas, inclusive, já foram usadas para cópias de vetores e dicionários. Para converter dados em JavaScript para uma cadeia de caracteres JSON, usa-se JSON.stringify() (documentação). Para converter uma cadeia de caracteres JSON em um objeto JSON, usa-se JSON.parse() (documentação).

Nas demais linguagens, o processo é similar. Python e GDScript fornecem subrotinas para processar JSON na biblioteca padrão. Lua requer o uso de uma biblioteca externa, usada como dependência. Também seria possível criar uma implementação própria para processar JSON em Lua.

Com o básico sobre o formato, agora pode-se considerar o uso de JSON com arquivos para se armazenar e recuperar dados. Para isso, pode-se definir um ou vários programas. Por exemplo, um programa pode criar um arquivo, outro pode ler o arquivo criado. Alternativamente, um mesmo programa poderia criar e ler um arquivo. Os exemplos utilizam dois programas, para separar as tarefas.

Salvando um Arquivo JSON

Os próximos blocos de código ilustram como salvar o texto_json gerado em um arquivo texto.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

let caminho_arquivo = "franco-garcia-ingrediente.json"

let original = new Ingrediente("Água", 3.0, "Xícaras")
// Serialização.
let texto_json = JSON.stringify(original)

let arquivo = new File([texto_json], caminho_arquivo, {type: "application/json"})
console.log("Arquivo criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import json
import sys

class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

caminho_arquivo = "franco-garcia-ingrediente.json"

original = Ingrediente("Água", 3.0, "Xícaras")
# Serialização.
texto_json = json.dumps({
    "nome": original.nome,
    "quantidade": original.quantidade,
    "unidade_medida": original.unidade_medida
})

try:
    arquivo = open(caminho_arquivo, "w")
    arquivo.write(texto_json)
    arquivo.close()

    print("Arquivo criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
-- <https://github.com/rxi/json.lua>
local json = require "json"

local EXIT_FAILURE = 1

function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

local caminho_arquivo = "franco-garcia-ingrediente.json"

local original = crie_ingrediente("Água", 3.0, "Xícaras")
-- Serialização.
local texto_json = json.encode(original)

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(texto_json)
io.close(arquivo)

print("Arquivo criado com sucesso.")
extends Node

const EXIT_FAILURE = 1

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

func _ready():
    var caminho_arquivo = "franco-garcia-ingrediente.json"

    var original = Ingrediente.new("Água", 3.0, "Xícaras")
    # Serialização.
    var texto_json = to_json({
        "nome": original.nome,
        "quantidade": original.quantidade,
        "unidade_medida": original.unidade_medida
    })

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_line(texto_json)
    arquivo.close()

    print("Arquivo criado com sucesso.")

Pode-se observar que o código de serialização é idêntico ao exemplo original. A única diferença é que os dados são salvos em um arquivo. O arquivo franco-garcia-ingrediente.json pode ser aberto em um editor de texto ou navegador.

Carregando um Arquivo JSON

O próximo passo consiste em recuperar os dados do arquivo para realizar a desserialização.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

function leia_arquivo(arquivo_json) {
    if (!arquivo_json) {
        return
    }

    if (!(arquivo_json instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        let texto_json = evento.target.result

        // Desserialização.
        let objeto_json = JSON.parse(texto_json)
        console.log(objeto_json)
        let recuperado = new Ingrediente
        recuperado.nome = objeto_json["nome"]
        recuperado.quantidade = objeto_json.quantidade
        recuperado.unidade_medida = objeto_json.unidade_medida
        console.log(recuperado)
    }
    leitor_arquivos.readAsText(arquivo_json)

    return false
}
import io
import json
import sys

class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

caminho_arquivo = "franco-garcia-ingrediente.json"

try:
    arquivo = open(caminho_arquivo, "r")
    texto_json = arquivo.read()
    arquivo.close()

    # Desserialização.
    objeto_json = json.loads(texto_json)
    print(objeto_json)
    recuperado = Ingrediente()
    recuperado.nome = objeto_json["nome"]
    recuperado.quantidade = objeto_json["quantidade"]
    recuperado.unidade_medida = objeto_json["unidade_medida"]
    print(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)
except IOError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
    print(excecao)
-- <https://github.com/rxi/json.lua>
local json = require "json"

local EXIT_FAILURE = 1

function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

local caminho_arquivo = "franco-garcia-ingrediente.json"

local arquivo = io.open(caminho_arquivo, "r")
if (arquivo == nil) then
    print(debug.traceback())

    print("Erro ao tentar ler arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

local texto_json = arquivo:read("*all")
io.close(arquivo)

-- Desserialização.
local objeto_json = json.decode(texto_json)
print(objeto_json.nome, objeto_json.quantidade, objeto_json.unidade_medida)
local recuperado = crie_ingrediente()
recuperado.nome = objeto_json["nome"]
recuperado.quantidade = objeto_json["quantidade"]
recuperado.unidade_medida = objeto_json["unidade_medida"]
print(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)
extends Node

const EXIT_FAILURE = 1

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

func _ready():
    var caminho_arquivo = "franco-garcia-ingrediente.json"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.READ) != OK):
        printerr("Erro ao tentar ler arquivo de texto.")

    var texto_json = arquivo.get_as_text()
    arquivo.close()

    # Desserialização.
    var objeto_json = parse_json(texto_json)
    print(objeto_json)
    var recuperado = Ingrediente.new()
    recuperado.nome = objeto_json["nome"]
    recuperado.quantidade = objeto_json["quantidade"]
    recuperado.unidade_medida = objeto_json["unidade_medida"]
    printt(recuperado.nome, recuperado.quantidade, recuperado.unidade_medida)

A versão para JavaScript requer uma página HTML com um formulário para o envio do arquivo JSON com o ingrediente.

<!DOCTYPE html>
<html lang="pt-BR">

  <head>
    <meta charset="utf-8">
    <title>Leitura de Arquivo JSON</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <header>
      <h1>Leitura de Arquivo JSON com Ingrediente</h1>
    </header>

    <main>
      <!-- Formulário para envio do arquivo texto. -->
      <form method="post"
            enctype="multipart/form-data"
            onsubmit="return leia_arquivo(arquivos.files[0])">
        <label for="arquivos">Escolha um arquivo com um ingrediente:</label>
        <input id="arquivos"
               name="arquivos"
               type="file"
               accept="application/json"/>
        <input type="submit"/>
      </form>

      <div id="conteudo">
      </div>

      <!-- O nome do arquivo JavaScript deve ser igual ao definido abaixo. -->
      <script src="./script.js"></script>
    </main>
  </body>

</html>

Assim como no caso da serialização, o carregamento implementa a parte da desserialização do exemplo original.

Como se pode perceber, vários programas podem compartilhar arquivos. Isso permite, dentre outros, troca dados entre diferentes programas. Pode-se criar o arquivo em um programa, modificá-lo em outro, compartilhá-lo em um terceiro, e visualizá-lo em um quarto programa (potencialmente em outra máquina).

Exemplo Mais Completo: Registro Receita

Após um exemplo básico, é útil considerar um exemplo mais complexo, contendo vetores e registros. Para organizar o código em blocos menores, criou-se subrotinas auxiliares.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

class Ingrediente {
    constructor(nome = "", quantidade = 0.0, unidade_medida = "") {
        this.nome = nome
        this.quantidade = quantidade
        this.unidade_medida = unidade_medida
    }
}

class Receita {
    constructor(nome = "", modo_preparo = "", ingredientes = []) {
        this.nome = nome
        this.modo_preparo = modo_preparo
        this.ingredientes = ingredientes
    }
}

function ingrediente_para_dicionario(ingrediente) {
    let resultado = JSON.stringify(ingrediente)

    return resultado
}

function dicionario_para_ingrediente(dicionario_ingrediente) {
    let resultado = new Ingrediente()
    resultado.nome = dicionario_ingrediente["nome"]
    resultado.quantidade = dicionario_ingrediente["quantidade"]
    resultado.unidade_medida = dicionario_ingrediente["unidade_medida"]

    return resultado
}

function receita_para_dicionario(receita) {
    let resultado = JSON.stringify(receita)

    return resultado
}

function dicionario_para_receita(dicionario_receita) {
    let resultado = new Receita()
    resultado.nome = dicionario_receita["nome"]
    resultado.modo_preparo = dicionario_receita["modo_preparo"]

    for (dicionario_ingrediente of dicionario_receita["ingredientes"]) {
        let ingrediente = dicionario_para_ingrediente(dicionario_ingrediente)
        resultado.ingredientes.push(ingrediente)
    }

    return resultado
}

function receitas_para_vetor_dicionarios(receitas) {
    let resultado = JSON.stringify(receitas)

    return resultado
}

function vetor_dicionarios_para_receitas(dicionario_receitas) {
    let resultado = []
    for (dicionario_receita of dicionario_receitas) {
        let receita = dicionario_para_receita(dicionario_receita)
        resultado.push(receita)
    }

    return resultado
}

function salve_receitas(receitas, caminho_arquivo) {
    var conteudo_arquivo = receitas_para_vetor_dicionarios(receitas)

    let arquivo = new File([conteudo_arquivo], caminho_arquivo, {type: "application/json"})
    console.log("Arquivo criado com sucesso.")

    let link_download = document.createElement("a")
    link_download.target = "_blank"
    link_download.href = URL.createObjectURL(arquivo)
    link_download.download = arquivo.name
    if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
        link_download.click()
        // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
        // quanto para cancelamento.
        URL.revokeObjectURL(link_download.href)
    }
}

function carregue_receitas(arquivo_texto = null) {
    // NOTA Caso o carregamento falhe, a implementação usa valores padrão.
    // Isso é feito para criar um arquivo inicial com receitas.
    let resultado = [
        new Receita("Pão",
                    "...",
                    [
                        new Ingrediente("Água", 3.0, "Xícaras"),
                        new Ingrediente("Farinha", 4.0, "Xícaras"),
                        new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                        new Ingrediente("Fermento", 2.0, "Colheres de Chá")
                    ]),
        new Receita("Pão Doce",
                    "...",
                    [
                        new Ingrediente("Água", 3.0, "Xícaras"),
                        new Ingrediente("Farinha", 4.0, "Xícaras"),
                        new Ingrediente("Açúcar", 2.0, "Xícaras"),
                        new Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                        new Ingrediente("Fermento", 2.0, "Colheres de Chá")
                    ]),
    ]

    return resultado
}

function main() {
    let caminho_arquivo = "franco-garcia-receitas.json"

    let receitas = carregue_receitas(/*caminho_arquivo*/)
    salve_receitas(receitas, caminho_arquivo)
}

function leia_arquivo(arquivo_json) {
    if (!arquivo_json) {
        return
    }

    if (!(arquivo_json instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        let conteudo = evento.target.result
        let dicionario_receitas = JSON.parse(conteudo)
        let resultado = vetor_dicionarios_para_receitas(dicionario_receitas)

        console.log(resultado)
    }
    leitor_arquivos.readAsText(arquivo_json)

    // Impede a submissão do formulário, permitindo a visualização do
    // resultado de adicione_elemento() na mesma página.
    return false
}

main()
import io
import json
import sys

class Ingrediente:
    def __init__(self, nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    def __init__(self, nome = "", modo_preparo = "", ingredientes = None):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes if (ingredientes != None) else []

def ingrediente_para_dicionario(ingrediente):
    resultado = {
        "nome": ingrediente.nome,
        "quantidade": ingrediente.quantidade,
        "unidade_medida": ingrediente.unidade_medida
    }

    return resultado

def dicionario_para_ingrediente(dicionario_ingrediente):
    resultado = Ingrediente()
    resultado.nome = dicionario_ingrediente["nome"]
    resultado.quantidade = dicionario_ingrediente["quantidade"]
    resultado.unidade_medida = dicionario_ingrediente["unidade_medida"]

    return resultado

def receita_para_dicionario(receita):
    resultado = {
        "nome": receita.nome,
        "modo_preparo": receita.modo_preparo,
        "ingredientes": []
    }

    for ingrediente in receita.ingredientes:
        resultado["ingredientes"].append(ingrediente_para_dicionario(ingrediente))

    return resultado

def dicionario_para_receita(dicionario_receita):
    resultado = Receita()
    resultado.nome = dicionario_receita["nome"]
    resultado.modo_preparo = dicionario_receita["modo_preparo"]

    for dicionario_ingrediente in dicionario_receita["ingredientes"]:
        ingrediente = dicionario_para_ingrediente(dicionario_ingrediente)
        resultado.ingredientes.append(ingrediente)

    return resultado

def receitas_para_vetor_dicionarios(receitas):
    resultado = []
    for receita in receitas:
        resultado.append(receita_para_dicionario(receita))

    return resultado

def vetor_dicionarios_para_receitas(dicionario_receitas):
    resultado = []
    for dicionario_receita in dicionario_receitas:
        receita = dicionario_para_receita(dicionario_receita)
        resultado.append(receita)

    return resultado

def salve_receitas(receitas, caminho_arquivo):
    conteudo_arquivo = receitas_para_vetor_dicionarios(receitas)

    try:
        arquivo = open(caminho_arquivo, "w")
        arquivo.write(json.dumps(conteudo_arquivo))
        arquivo.close()

        print("Arquivo criado com sucesso.")
    except IOError as excecao:
        print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
        print(excecao)
    except OSError as excecao:
        print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
        print(excecao)

def carregue_receitas(caminho_arquivo = None):
    if (caminho_arquivo != None):
        try:
            arquivo = open(caminho_arquivo, "r")
            conteudo = arquivo.read()
            arquivo.close()

            dicionario_receitas = json.loads(conteudo)
            resultado = vetor_dicionarios_para_receitas(dicionario_receitas)

            return resultado
        except IOError as excecao:
            print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
            print(excecao)
        except OSError as excecao:
            print("Erro ao tentar ler arquivo de texto.", file=sys.stderr)
            print(excecao)

    # NOTA Caso o carregamento falhe, a implementação usa valores padrão.
    # Isso é feito para criar um arquivo inicial com receitas.
    resultado = [
        Receita("Pão",
                "...",
                [
                    Ingrediente("Água", 3.0, "Xícaras"),
                    Ingrediente("Farinha", 4.0, "Xícaras"),
                    Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                    Ingrediente("Fermento", 2.0, "Colheres de Chá")
                ]),
        Receita("Pão Doce",
                "...",
                [
                    Ingrediente("Água", 3.0, "Xícaras"),
                    Ingrediente("Farinha", 4.0, "Xícaras"),
                    Ingrediente("Açúcar", 2.0, "Xícaras"),
                    Ingrediente("Sal", 2.0, "Colheres de Sopa"),
                    Ingrediente("Fermento", 2.0, "Colheres de Chá")
                ]),
    ]

    return resultado

def main():
    caminho_arquivo = "franco-garcia-receitas.json"

    receitas = carregue_receitas(caminho_arquivo)
    salve_receitas(receitas, caminho_arquivo)

if (__name__ == "__main__"):
    main()
-- <https://github.com/rxi/json.lua>
local json = require "json"

local EXIT_FAILURE = 1

function crie_ingrediente(nome, quantidade, unidade_medida)
    local resultado = {
        nome = nome or "",
        quantidade = quantidade or 0.0,
        unidade_medida = unidade_medida or ""
    }

    return resultado
end

function crie_receita(nome, modo_preparo, ingredientes)
    local resultado = {
        nome = nome or "",
        modo_preparo = quantidade or "",
        ingredientes = ingredientes or {}
    }

    return resultado
end

function ingrediente_para_dicionario(ingrediente)
    local resultado = {
        nome = ingrediente.nome,
        quantidade = ingrediente.quantidade,
        unidade_medida = ingrediente.unidade_medida
    }

    return resultado
end

function dicionario_para_ingrediente(dicionario_ingrediente)
    local resultado = crie_ingrediente()
    resultado.nome = dicionario_ingrediente["nome"]
    resultado.quantidade = dicionario_ingrediente["quantidade"]
    resultado.unidade_medida = dicionario_ingrediente["unidade_medida"]

    return resultado
end

function receita_para_dicionario(receita)
    local resultado = {
        nome = receita.nome,
        modo_preparo = receita.modo_preparo,
        ingredientes = {}
    }

    for _, ingrediente in ipairs(receita.ingredientes) do
        table.insert(resultado["ingredientes"], ingrediente_para_dicionario(ingrediente))
    end

    return resultado
end

function dicionario_para_receita(dicionario_receita)
    local resultado = crie_receita()
    resultado.nome = dicionario_receita["nome"]
    resultado.modo_preparo = dicionario_receita["modo_preparo"]

    for _, dicionario_ingrediente in ipairs(dicionario_receita["ingredientes"]) do
        local ingrediente = dicionario_para_ingrediente(dicionario_ingrediente)
        table.insert(resultado.ingredientes, ingrediente)
    end

    return resultado
end

function receitas_para_vetor_dicionarios(receitas)
    local resultado = {}
    for _, receita in ipairs(receitas) do
        table.insert(resultado, receita_para_dicionario(receita))
    end

    return resultado
end

function vetor_dicionarios_para_receitas(dicionario_receitas)
    local resultado = {}
    for _, dicionario_receita in pairs(dicionario_receitas) do
        local receita = dicionario_para_receita(dicionario_receita)
        table.insert(resultado, receita)
    end

    return resultado
end

function salve_receitas(receitas, caminho_arquivo)
    local conteudo_arquivo = receitas_para_vetor_dicionarios(receitas)

    local arquivo = io.open(caminho_arquivo, "w")
    if (arquivo == nil) then
        print(debug.traceback())

        error("Erro ao tentar criar arquivo de texto.")
        os.exit(EXIT_FAILURE)
    end

    arquivo:write(json.encode(conteudo_arquivo))
    io.close(arquivo)

    print("Arquivo criado com sucesso.")
end

function carregue_receitas(caminho_arquivo)
    caminho_arquivo = caminho_arquivo or nil
    if (caminho_arquivo ~= nil) then
        local arquivo = io.open(caminho_arquivo, "r")
        if (arquivo == nil) then
            print(debug.traceback())

            print("Erro ao tentar ler arquivo de texto.")
            -- os.exit(EXIT_FAILURE)
        else
            local conteudo = arquivo:read("*all")
            io.close(arquivo)

            local dicionario_receitas = json.decode(conteudo)
            local resultado = vetor_dicionarios_para_receitas(dicionario_receitas)

            return resultado
        end
    end

    -- NOTA Caso o carregamento falhe, a implementação usa valores padrão.
    -- Isso é feito para criar um arquivo inicial com receitas.
    local resultado = {
        crie_receita("Pão",
                     "...",
                     {
                         crie_ingrediente("Água", 3.0, "Xícaras"),
                         crie_ingrediente("Farinha", 4.0, "Xícaras"),
                         crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
                         crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
                     }),
        crie_receita("Pão Doce",
                     "...",
                     {
                         crie_ingrediente("Água", 3.0, "Xícaras"),
                         crie_ingrediente("Farinha", 4.0, "Xícaras"),
                         crie_ingrediente("Açúcar", 2.0, "Xícaras"),
                         crie_ingrediente("Sal", 2.0, "Colheres de Sopa"),
                         crie_ingrediente("Fermento", 2.0, "Colheres de Chá")
                     }),
    }

    return resultado
end

function main()
    local caminho_arquivo = "franco-garcia-receitas.json"

    local receitas = carregue_receitas(caminho_arquivo)
    salve_receitas(receitas, caminho_arquivo)
end

main()
extends Node

const EXIT_FAILURE = 1

class Ingrediente:
    var nome
    var quantidade
    var unidade_medida

    func _init(nome = "", quantidade = 0.0, unidade_medida = ""):
        self.nome = nome
        self.quantidade = quantidade
        self.unidade_medida = unidade_medida

class Receita:
    var nome
    var modo_preparo
    var ingredientes

    func _init(nome = "", modo_preparo = "", ingredientes = []):
        self.nome = nome
        self.modo_preparo = modo_preparo
        self.ingredientes = ingredientes

func ingrediente_para_dicionario(ingrediente):
    var resultado = {
        "nome": ingrediente.nome,
        "quantidade": ingrediente.quantidade,
        "unidade_medida": ingrediente.unidade_medida
    }

    return resultado

func dicionario_para_ingrediente(dicionario_ingrediente):
    var resultado = Ingrediente.new()
    resultado.nome = dicionario_ingrediente["nome"]
    resultado.quantidade = dicionario_ingrediente["quantidade"]
    resultado.unidade_medida = dicionario_ingrediente["unidade_medida"]

    return resultado

func receita_para_dicionario(receita):
    var resultado = {
        "nome": receita.nome,
        "modo_preparo": receita.modo_preparo,
        "ingredientes": []
    }

    for ingrediente in receita.ingredientes:
        resultado["ingredientes"].append(ingrediente_para_dicionario(ingrediente))

    return resultado

func dicionario_para_receita(dicionario_receita):
    var resultado = Receita.new()
    resultado.nome = dicionario_receita["nome"]
    resultado.modo_preparo = dicionario_receita["modo_preparo"]

    for dicionario_ingrediente in dicionario_receita["ingredientes"]:
        var ingrediente = dicionario_para_ingrediente(dicionario_ingrediente)
        resultado.ingredientes.append(ingrediente)

    return resultado

func receitas_para_vetor_dicionarios(receitas):
    var resultado = []
    for receita in receitas:
        resultado.append(receita_para_dicionario(receita))

    return resultado

func vetor_dicionarios_para_receitas(dicionario_receitas):
    var resultado = []
    for dicionario_receita in dicionario_receitas:
        var receita = dicionario_para_receita(dicionario_receita)
        resultado.append(receita)

    return resultado

func salve_receitas(receitas, caminho_arquivo):
    var conteudo_arquivo = receitas_para_vetor_dicionarios(receitas)

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_line(to_json(conteudo_arquivo))
    arquivo.close()

    print("Arquivo criado com sucesso.")

func carregue_receitas(caminho_arquivo = null):
    if (caminho_arquivo != null):
        var arquivo = File.new()
        if (arquivo.open(caminho_arquivo, File.READ) != OK):
            printerr("Erro ao tentar ler arquivo de texto.")
        else:
            var conteudo = arquivo.get_as_text()
            arquivo.close()

            var dicionario_receitas = parse_json(conteudo)
            var resultado = vetor_dicionarios_para_receitas(dicionario_receitas)

            return resultado

    # NOTA Caso o carregamento falhe, a implementação usa valores padrão.
    # Isso é feito para criar um arquivo inicial com receitas.
    var resultado = [
        Receita.new("Pão",
                    "...",
                    [
                        Ingrediente.new("Água", 3.0, "Xícaras"),
                        Ingrediente.new("Farinha", 4.0, "Xícaras"),
                        Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
                        Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
                    ]),
        Receita.new("Pão Doce",
                    "...",
                    [
                        Ingrediente.new("Água", 3.0, "Xícaras"),
                        Ingrediente.new("Farinha", 4.0, "Xícaras"),
                        Ingrediente.new("Açúcar", 2.0, "Xícaras"),
                        Ingrediente.new("Sal", 2.0, "Colheres de Sopa"),
                        Ingrediente.new("Fermento", 2.0, "Colheres de Chá")
                    ]),
    ]

    return resultado

func _ready():
    var caminho_arquivo = "franco-garcia-receitas.json"

    var receitas = carregue_receitas(caminho_arquivo)
    salve_receitas(receitas, caminho_arquivo)

As versões em Python, GDScript e Lua são mais simples de entender, já que é possível usar arquivos sem um formulário do navegador. A versão JavaScript é um pouco diferente das demais, devido à necessidade de uma página HTML para envio do arquivo. O próximo exemplo fornece uma página HTML simples com formulário para submissão de arquivo. Para facilitar o uso do programa (embora com péssima usabilidade), um arquivo JSON será criado com receitas e oferecido para download. O arquivo JSON baixado deve ser submetido na página HTML. Após envio dos arquivos, o conteúdo é apresentado no console. Para uma apresentação melhor, poder-se-ia usar o restante do código definido originalmente para converter os dados para cadeia de caracteres, e apresentar a cadeia de caracteres resultante no navegador.

<!DOCTYPE html>
<html lang="pt-BR">

  <head>
    <meta charset="utf-8">
    <title>Criação e Leitura de Arquivo JSON</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <header>
      <h1>Leitura de Arquivo JSON com Receitas</h1>
    </header>

    <main>
      <!-- Formulário para envio do arquivo texto. -->
      <form method="post"
            enctype="multipart/form-data"
            onsubmit="return leia_arquivo(arquivos.files[0])">
        <label for="arquivos">Escolha um arquivo com receitas:</label>
        <input id="arquivos"
               name="arquivos"
               type="file"
               accept="application/json"/>
        <input type="submit"/>
      </form>

      <div id="conteudo">
      </div>

      <!-- O nome do arquivo JavaScript deve ser igual ao definido abaixo. -->
      <script src="./script.js"></script>
    </main>
  </body>

</html>

Todos os programas gerarão o arquivo franco-garcia-receitas.json com duas receitas pré-definidas. O arquivo gerado provavelmente terá uma única linha, sem espaços entre valores. Para facilitar a leitura, apresenta-se, também, um arquivo formatado com indentação.

[{"nome":"Pão","modo_preparo":"...","ingredientes":[{"nome":"Água","quantidade":3,"unidade_medida":"Xícaras"},{"nome":"Farinha","quantidade":4,"unidade_medida":"Xícaras"},{"nome":"Sal","quantidade":2,"unidade_medida":"Colheres de Sopa"},{"nome":"Fermento","quantidade":2,"unidade_medida":"Colheres de Chá"}]},{"nome":"Pão Doce","modo_preparo":"...","ingredientes":[{"nome":"Água","quantidade":3,"unidade_medida":"Xícaras"},{"nome":"Farinha","quantidade":4,"unidade_medida":"Xícaras"},{"nome":"Açúcar","quantidade":2,"unidade_medida":"Xícaras"},{"nome":"Sal","quantidade":2,"unidade_medida":"Colheres de Sopa"},{"nome":"Fermento","quantidade":2,"unidade_medida":"Colheres de Chá"}]}]
[
  {
    "nome": "Pão",
    "modo_preparo": "...",
    "ingredientes": [
      {
        "nome": "Água",
        "quantidade": 3,
        "unidade_medida": "Xícaras"
      },
      {
        "nome": "Farinha",
        "quantidade": 4,
        "unidade_medida": "Xícaras"
      },
      {
        "nome": "Sal",
        "quantidade": 2,
        "unidade_medida": "Colheres de Sopa"
      },
      {
        "nome": "Fermento",
        "quantidade": 2,
        "unidade_medida": "Colheres de Chá"
      }
    ]
  },
  {
    "nome": "Pão Doce",
    "modo_preparo": "...",
    "ingredientes": [
      {
        "nome": "Água",
        "quantidade": 3,
        "unidade_medida": "Xícaras"
      },
      {
        "nome": "Farinha",
        "quantidade": 4,
        "unidade_medida": "Xícaras"
      },
      {
        "nome": "Açúcar",
        "quantidade": 2,
        "unidade_medida": "Xícaras"
      },
      {
        "nome": "Sal",
        "quantidade": 2,
        "unidade_medida": "Colheres de Sopa"
      },
      {
        "nome": "Fermento",
        "quantidade": 2,
        "unidade_medida": "Colheres de Chá"
      }
    ]
  }
]

Neste segundo exemplo, operar o conteúdo de arquivo um JSON seria como manipular um vetor de dicionários. Cada dicionário corresponde ao tipo definido para o registro Receita codificado como dicionário. Da mesma forma, para se criar o arquivo JSON, cria-se um dicionário com o conteúdo desejado (ou um vetor contendo dicionários com o conteúdo).

Em Python, Lua e GDScript, cria-se um vetor de dicionários para a conversão de dados para JSON (ou seja, a serialização). A rigor, poder-se-ia omitir a criação da tabela na versão em Lua, pois os dados já estavam em uma tabela. Os dados de cada registro são convertidos para um dicionário para preparação de dados para serialização. Em seguida, todos os dados são agrupados em um vetor. A serialização converte os dados do vetor para uma cadeia de caracteres JSON que, na seqüência, é salva em um arquivo texto.

A desserialização é o processo contrário. Lê-se o conteúdo do arquivo JSON como uma cadeia de caracteres, que, em seguida, é convertida em um vetor de dicionários para abstrair JSON. Para restaurar os registros originais, usa-se cada dicionário para inicializar valores de novas variáveis para os respectivos tipos. A iteração em cada um dos valores cria o vetor de receitas.

Algo interessante sobre os programas é que, como todos eles usam JSON para armazenar e carregar receitas, pode-se compartilhar os arquivos de receitas gerados entre implementações em diferentes linguagens de programação. Desde que a codificação seja a mesma (por exemplo, UTF-8), os programas funcionarão corretamente. Com os devidos cuidados, também é possível incluir ou remover receitas (e/ou ingredientes) do arquivo usando um editor de textos.

Ou seja, além de uso de JSON, este exemplo serve como modelo de uso de arquivos para trocas de dados entre diferentes programas. Desde que todos eles sigam um mesmo formato (no caso, o formato especificado pelo vetor de receitas em JSON), pode-se usar um mesmo arquivo em diferentes programas, independentemente da linguagem de programação usada para a escrita do programa. Por exemplo, poder-se-ia propor a extensão .receitas para uso com os quatro programas anteriores. Todos eles podem gerar e carregar dados usando o formato estabelecido, que é representado usando JSON.

Aliás, abrir arquivos em editores de texto, editores hexadecimais e em compressores de arquivo (como para o formato .zip) pode fornecer indícios do que o arquivo armazena. Caso o indício sugira o formato seja de um arquivo conhecido (ou simples), é possível editar o arquivo externamente, sem o programa original. Seria até mesmo possível criar um novo programa para manipulá-lo (ao invés de usar o programa original).

Copiar Arquivo

Gerenciadores de arquivos permitem copiar e colar arquivos para gerar uma cópia idêntica ao arquivo original. Isso também pode ser feito usando arquivos em programação. De fato, a implementação é bastante simples: basta ler cada um dos bytes de um arquivo e escrevê-los em um segundo arquivo.

Para este exemplo, crie um arquivo chamado franco-garcia-arquivo_original.txt com qualquer conteúdo (preferencialmente não vazio). Por exemplo, o arquivo pode armazenar o clássico Olá, meu nome é Franco!. O arquivo pode ser de qualquer tipo; o exemplo utilizará um arquivo texto porque será mais fácil ler os resultados da cópia em um editor de texto. O exemplo também serve para demonstrar que se pode abrir um arquivo texto em modo binário (porque um arquivo texto é um arquivo binário). Entretanto, você pode escolher um arquivo de qualquer tipo e com qualquer conteúdo, como uma imagem, um vídeo, um arquivo executável de um programa, ou o próprio arquivo de código-fonte usado para escrever o programa (embora alguns editores de texto ou IDEs possam bloquear o uso do arquivo em edição). Evidentemente, escolha um arquivo que possua cópia de segurança.

Tanto o caminho para o arquivo original (caminho_arquivo) quanto o da cópia a ser criada (caminho_copia) podem ser modificados. Portanto, escolha um nome para a cópia que não sobrescreva um arquivo existente.

As versões em Python, Lua e GDScript fazem a cópia byte a byte. A versão em JavaScript copia o arquivo todo de uma vez, já que todos os bytes foram lidos em readAsArrayBuffer(). Na linguagem e usando um navegador, como o arquivo deve ser gerado de uma única vez, não é possível salvar os dados byte por byte. Para fins educativos, poder-se-ia criar um segundo vetor para fazer uma cópia índice a índice do vetor com bytes. Contudo, ele seria uma cópia do primeiro. Ou seja, desperdiçar-se-ia ciclos do processador e memória sem nenhum benefício adicional.

Atenção. Neste exemplo, além de caminho_arquivo, também será gerado um arquivo no caminho definido em caminho_copia, com valor franco-garcia-copia_criada.txt. Tome os devidos cuidados para não sobrescrever um arquivo existente.

// Este código deve ser salvo em um arquivo chamado "script.js".
// Ele será processado por uma página HTML com código para envio do arquivo
// texto via formulário.

function copie_arquivo(original) {
    if (!original) {
        return
    } else if (!(original instanceof File)) {
        return
    }

    let leitor_arquivos = new FileReader()
    leitor_arquivos.onload = function(evento) {
        let caminho_copia = "franco-garcia-copia_criada.txt"

        // bytes já representa todo o conteúdo do arquivo original, então
        // basta escrever o valor.
        let bytes = new Uint8Array(evento.target.result)
        let dados = new Blob([bytes], {type: "application/octet-stream"})
        let copia = new File([dados], caminho_copia, {type: dados.type})
        console.log("Arquivo copiado com sucesso.")

        let link_download = document.createElement("a")
        link_download.target = "_blank"
        link_download.href = URL.createObjectURL(copia)
        link_download.download = copia.name
        if (confirm("Fazer download do arquivo '" + copia.name + "'?")) {
            link_download.click()
            // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
            // quanto para cancelamento.
            URL.revokeObjectURL(link_download.href)
        }
    }
    leitor_arquivos.readAsArrayBuffer(original)

    // Impede a submissão do formulário, permitindo a visualização do
    // resultado de adicione_elemento() na mesma página.
    return false
}
import io
import struct
import sys

try:
    caminho_arquivo = "franco-garcia-arquivo_original.txt"
    caminho_copia = "franco-garcia-copia_criada.txt"

    original = open(caminho_arquivo, "rb")
    copia = open(caminho_copia, "wb")

    byte_lido = original.read(1)
    while (byte_lido):
        copia.write(byte_lido)
        byte_lido = original.read(1)

    original.close()
    copia.close()

    print("Arquivo copiado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-arquivo_original.txt"
local caminho_copia = "franco-garcia-copia_criada.txt"

local original = io.open(caminho_arquivo, "rb")
local copia = io.open(caminho_copia, "wb")
if ((original == nil) or (copia == nil)) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo binário.")
    os.exit(EXIT_FAILURE)
end

local byte_lido = original:read(1)
while (byte_lido) do
    copia:write(byte_lido)
    byte_lido = original:read(1)
end

io.close(original)
io.close(copia)

print("Arquivo copiado com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-arquivo_original.txt"
    var caminho_copia = "franco-garcia-copia_criada.txt"

    var original = File.new()
    var copia = File.new()
    if ((original.open(caminho_arquivo, File.READ) != OK) or
        (copia.open(caminho_copia, File.WRITE) != OK)):
        printerr("Erro ao tentar criar arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    var byte_lido = original.get_8()
    while (byte_lido):
        copia.store_8(byte_lido)
        byte_lido = original.get_8()

    original.close()
    copia.close()

    print("Arquivo copiado com sucesso.")

A versão JavaScript requer uma página HTML para o envio do arquivo original. Caso você reaproveite uma das páginas HTML anteriores, deve-se observar que, em onsubmit, trocou-se leia_arquivo() por copie_arquivo(), que é o nome da função definida no arquivo com código JavaScript.

<!DOCTYPE html>
<html lang="pt-BR">

  <head>
    <meta charset="utf-8">
    <title>Cópia de Arquivo</title>
    <meta name="author" content="Franco Eusébio Garcia">
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>

  <body>
    <header>
      <h1>Cópia de Arquivo</h1>
    </header>

    <main>
      <!-- Formulário para envio do arquivo texto. -->
      <form method="post"
            enctype="multipart/form-data"
            onsubmit="return copie_arquivo(arquivos.files[0])">
        <label for="arquivos">Escolha um arquivo:</label>
        <input id="arquivos"
               name="arquivos"
               type="file"
               accept="application/octet-stream"/>
        <input type="submit"/>
      </form>

      <div id="conteudo">
      </div>

      <!-- O nome do arquivo JavaScript deve ser igual ao definido abaixo. -->
      <script src="./script.js"></script>
    </main>
  </body>

</html>

Assim como feito em JavaScript, em linguagens de programação que permitem operar com vetores de bytes ou blocos de memória, pode-se, simplesmente, ler todo o conteúdo do arquivo de origem e salvá-lo no arquivo de destino. Evidentemente, isso pode depender do tamanho do arquivo de origem; a quantidade de memória primária disponível deve ser suficiente para armazenar todos os bytes do arquivo original. Para arquivos de tamanho superior à quantidade disponível de memória primária, a cópia usando blocos menores de memória (chamados pedaços ou chunks) é inevitável.

Arquivos de Imagens e Imagens Raster (Matriciais)

A implementação do Jogo da Vida (Conway's Game Of Life) definiu a primeira animação (em terminal ou console, mas é uma animação) deste material. Com arquivos, pode-se criar a primeira imagem. Ainda não para se exibir em um programa, mas como um arquivo.

Se você estiver usando um leitor de tela, esta subseção será estranha de ouvir. Vários valores são partes de matrizes, então a narração incluirá seqüencias numéricas potencialmente extensas.

Visualização e Manipulação de Imagens

Se você optara por usar as Ferramentas online, não é necessário instalar nenhum programa desta subseção.

Como as imagens criadas serão pequenas, ampliar a imagem resultante (com zoom) ou aplicar uma operação de escala pode ajudar a visualizá-la. Uma subseção apresentará uma forma simples de ampliar a imagem, embora o resultado seja adequado apenas para pixel art.

Em alguns sistemas operacionais (como Windows), o visualizador de imagens padrão não lê arquivos com os formatos usados nesta seção. Para abrir as imagens criadas, pode-se usar um programa como GIMP (que também é um editor de imagens), IrfanView, ou XnView. Todos esses programas podem ser instalados via Ninite, Chocolatey e Scoop. GIMP possui código aberto, mas IrfanView e XnView não possuem (embora sejam grátis para uso pessoal). Para um visualizador de imagens de código aberto com aparência mais moderna, uma alternativa chama-se ImageGlass (para instalação manual, ou usando Chocolatey ou Scoop). Para outra opção de código aberto, pode-se optar por Gwenview, do KDE. Gwenview aparecerá em ilustrações com as imagens criadas nas próximas seções.

Caso você não se queira instalar um programa, pode-se usar Magick Online Studio. Escolha um arquivo em Browse... e aperte o botão View para carregar a imagem no serviço. Evidentemente, use os cuidados adequados ao enviar qualquer imagem (ou qualquer arquivo) para um serviço na Internet; existem dados que nunca devem sair de sua máquina. Outra opção é usar JqMagick, também fornecido por ImageMagick. Para enviar a imagem, escolha o ícone de uma folha com uma seta verde dentro de um círculo.

O software ImageMagick é um programa de linha de comando de código aberto para criação, edição e conversão de imagens. A versão online Magick Online Studio fornece uma interface Web para alguns dos comandos disponíveis. Por exemplo, para aumentar o tamanho de uma imagem, pode-se escolher Transform, depois Resize, escolher um novo tamanho (por exemplo, alterar de 23x11 para 230x110 para aplicar uma escala de dez vezes), e apertar o botão resize.

Caso ImageMagick esteja instalado em sua máquina, o mesmo pode ser feito em linha de comando, inclusive com conversão de formatos. ImageMagick fornece o comando convert para conversão e edição de imagens.

convert entrada.pbm -scale 1000% resultado.png

O comando anterior amplia a imagem entrada.pbm em dez vezes (1000%), converte o resultado para o formato PNG e salva o resultado no arquivo resultado.png.

ImageMagick também pode exibir imagens com o comando display.

display resultado.png

Ambas as operações podem ser úteis se combinadas em um interpretador de comandos. Quanto um programa termina, o valor retornado ao fim da execução pode indicar se o programa terminou com sucesso. O valor 0 costuma indicar sucesso; o valor 1, escrito como EXIT_FAILURE em alguns exemplos, simboliza erro em vários sistemas. Os valores 0 e 1 podem variar entre plataformas e sistemas operacionais.

De qualquer forma, em um interpretador de linhas de comando como Bash ou ZSH, pode-se usar o operador && para executar um próximo comando caso o anterior tenha sucesso. Assim, pode-se combinar a execução de um programa com os comandos convert e display para exibir a imagem ampliada após usar o código de um programa.

python script.py &&
convert franco-garcia-nome_franco.pbm -scale 1000% franco-garcia-nome_franco_ampliado.pbm &&
display franco-garcia-nome_franco_ampliado.pbm

O comando anterior executa o código em Python de um arquivo chamado script.py. Caso a execução tenha sucesso, a próxima linha é executada com o comando convert, para ampliar a imagem. Caso a ampliação tenha sucesso, a imagem ampliada é exibida usando display. Em outras palavras, automatizou-se a ampliação e a exibição do resultado do programa em Python em um único comando que combina três.

Como se pode sugerir pelo exemplo, a linha de comando pode servir como um poderoso complemento ao uso de linguagens de programação. Ao invés de Python, poder-se-ia usar Lua, GDScript ou qualquer outra linguagem de programação (com suporte a uso via linha de comando) com programas para computadores para processar resultados como seqüências de operações. Embora seja mais complicado de fazer isso usando JavaScript para navegadores (por exemplo, usando um navegador em modo headless), pode-se fazê-lo usando interpretadores como SpiderMonkey ou Node.js.

Em muitas linguagens de programação, também é possível definir comandos para se executar outros programas e obter os resultados no código de um programa, como uma chamada externa. Embora isso possa ser prático, o uso de um programa externo pode constituir uma vulnerabilidade, com potenciais riscos de segurança. Uma forma mais segura é usar um arquivo para compartilhar informações entre programas, ou alternativas mais avançadas como um socket ou pipe para comunicação ou passagem de resultados para frente.

Portable Bitmap Format (PBM)

O pacote gráfico Netpbm (ou Pbmplus; página oficial) define três formatos simples para imagens:

  • Portable Bitmap Format (PBM);
  • Portable Graymap Format (PGM);
  • Portable Pixmap Format (PPM).

Os formatos não são eficientes quanto ao tamanho e ao desempenho (para as versões em texto); contudo, eles são simples para entender e implementar, servindo como uma boa introdução a arquivos de imagens. A página em Inglês da Wikipedia descreve os formatos. A página em Português apresenta um exemplo de imagem de um disquete no formato PBM.

A especificação do format PBM está disponível na documentação do formato. Pode-se definir imagens no formato usando um arquivo binário ou um arquivo texto. A definição em arquivo texto é uma facilidade do formato; imagens costumam ser definidas como arquivos binários, para reduzir o tamanho do arquivo e melhorar o desempenho para carregar os dados.

Embora ineficiente, a versão em texto é útil para fins de aprendizado -- afinal, pode-se criar e editar imagens usando-se um editor de texto. Por exemplo, o próximo trecho contém uma imagem em texto no formato PBM que desenha um quadrado com bordas pretas e centro branco. Caso se salve o conteúdo em um editor de texto com a extensão .pbm (por exemplo, quadrado.pbm), poder-se-á abrir o arquivo resultante em visualizador de imagens compatível com o formato. Também é possível usar o Visualizador de Imagens Portable Bitmap Format (PBM) do autor. Uma imagem (bem pequena) será gerada representando o conteúdo.

P1
# O contorno de um quadrado.
5 5
1 1 1 1 1
1 0 0 0 1
1 0 0 0 1
1 0 0 0 1
1 1 1 1 1

As linhas estão numeradas para facilitar a explicação. Os números na lateral não fazem parte do formato; para conveniência, eles não podem ser selecionados ou copiados no navegador.

No formato, as três primeiras linhas formam o cabeçalho (header) do arquivo, com metadados para auxiliar na interpretação. P1 é o código para o formato PBM. A linguagem seguinte (# O contorno de um quadrado.) é um comentário; ela é opcional. A terceira linha é obrigatória: o primeiro número (no exemplo, 5) é o número de colunas da imagem. O segundo número (no exemplo, 5) é o número de linhas. A imagem, portanto, é representada como uma matriz.

As demais linhas armazenam valores para os pixels (picture elements ou elementos de imagem) que definem a imagem. O valor 0 significa que o pixel deve ter cor branca; o valor 1 significa cor preta para o pixel. Com 5 linhas contendo 5 colunas cada uma, a imagem possui 25 pixels brancos ou pretos. Como se apresentará para o formato PPM, imagens coloridas possuem múltiplos bytes por pixel, para definir o valor para cada cor.

Com combinações adequadas de valores para pixels, pode-se desenhar qualquer imagem. Por exemplo, imagens nos formatos Joint Photographic Experts Group (JPEG, com extensões .jpeg ou .jpg) e Portable Network Graphics (PNG, com extensão .png) também mapeiam pixels como valores numéricos uma matriz. Esse tipo de imagem chama-se mapa de bits (bitmap, raster ou matricial). Quanto maior o número de pixels, maior será a resolução da imagem, assim como seu tamanho, potencial qualidade, e detalhamento. Imagens de alta qualidade normalmente possuem milhões de pixels. Por exemplo, câmeras fotográficas definem a resolução em megapixels (MP), ou seja, em milhões pixels.

Como o autor não é um artista, este tópico explorará outro uso para imagens. Em particular, pode-se criar representações para letras do alfabeto, números e símbolos para desenhar texto em imagem. Quando um programa exibe conteúdo na tela de um computador, esse é um dos passos necessários para converter dados de cadeias de caracteres para imagens para exibição. Ou seja, quando texto é apresentado em um monitor, ele é processado por um código parecido (mas mais sofisticado) que o que será implementado a construção de uma imagem. Para fontes de computadores, o processo de conversão chama-se font rasterization (ou rasterização de fonte), e versões de alta qualidade utilizam imagens vetoriais. Contudo, ao invés de escrever a imagem criada em um arquivo, a memória que armazena a imagem é enviada para desenho na tela.

Nesta seção, os exemplos usam arquivos por simplicidade. Por exemplo, o trecho a seguir contém uma imagem em texto no formato PBM para a desenhar a cadeia de caracteres Franco Garcia. Ela será usada ao longo dos demais exemplos.

P1
# Franco Garcia
23 11
1 1 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 1 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 0 1 1 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
1 0 1 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 1 1 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
0 1 1 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1

A imagem possui 23 colunas (primeiro valor da terceira linha) e 11 linhas (segundo valor da terceira linha).

JavaScript, Python, Lua e GDScript permitem definir cadeias de caracteres de múltiplas linhas usando sintaxes especiais. A implementação utiliza o recurso para salvar os valores do texto da imagem em uma cadeia de caracteres. Em linguagens que não forneça o recurso, dever-se-ia adicionar \n para cada quebra de linha.

let caminho_arquivo = "franco-garcia-nome_franco.pbm"
let conteudo = `P1
# Franco Garcia
23 11
1 1 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 1 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 0 1 1 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
1 0 1 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 1 1 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
0 1 1 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1
`

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-bitmap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import sys

try:
    caminho_arquivo = "franco-garcia-nome_franco.pbm"
    arquivo = open(caminho_arquivo, "w")

    arquivo.write("""P1
# Franco Garcia
23 11
1 1 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 1 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 0 1 1 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
1 0 1 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 1 1 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
0 1 1 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1
""")
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo de texto.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-nome_franco.pbm"
local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo de texto.")
    os.exit(EXIT_FAILURE)
end

arquivo:write([[
P1
# Franco Garcia
23 11
1 1 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 1 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 0 1 1 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
1 0 1 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 1 1 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
0 1 1 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1
]])

io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-nome_franco.pbm"
    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo de texto.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string("""P1
# Franco Garcia
23 11
1 1 1 0 1 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 1 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1
1 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 0 1 1 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 1 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 1 0 0 1 0
1 0 0 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
1 0 1 0 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 1 1 1
1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 1 0 0 1 0 1
0 1 1 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1
""")

    arquivo.close()

    print("Imagem criada com sucesso.")

A imagem resultante será pequena (23x11 pixels, ou seja, 253 pixels no total). Na ilustração a seguir, compara-se a imagem resultante em cima e uma ampliação de 1600% embaixo, ambas visualizadas no programa Gwenview. As imagens estão em tamanho real (algo que potencialmente pode exceder a largura máxima para visualização em um dispositivo móvel).

Imagem PBM resultante exibida no Gwenview, com o texto 'Franco Garcia' escrito em preto sobre fundo branco. A imagem do topo é o resultado da execução do programa, com dimensões 23x11 pixels. A imagem de baixo é uma ampliação de 1600%.

Caso você use o Visualizador de Imagens Portable Bitmap Format (PBM) do autor, o programa mostrará tanto a imagem original quanto uma ampliação dela.

Um exemplo de como ampliá-la na implementação será fornecido em momento hábil. Antes, contudo, convém apresentar padrões para outras letras. Afinal, com habilidades de desenho em arquivos de imagens, você provavelmente não desejará escrever o nome do autor. Exceto caso você também chame Franco Garcia.

Padrões para o Alfabeto e Caracteres Selecionados

Para mais representações de letras, pode-se consultar fontes em pixel art para inspiração. Por exemplo, a fonte DatCub, criada por GGBot foi usada como modelo a escrita da frase anterior (o posicionamento dos valores 1 segue a fonte). Os exemplos a seguir contém imagens em formato PBM para todos os caracteres da língua Inglesa, dígitos e alguns símbolos de pontuação. Para acesso a todas as abas, provavelmente será necessário visualizar esta página em um navegador para desktop (computador de mesa), porque eu não implementei rolagem no menu para blocos de código.

Letras maiúsculas:

P1
# A
3 5
0 1 0
1 0 1
1 1 1
1 0 1
1 0 1
P1
# B
3 5
1 1 0
1 0 1
1 1 0
1 0 1
1 1 0
P1
# C
3 5
0 1 1
1 0 0
1 0 0
1 0 0
0 1 1
P1
# D
3 5
1 1 0
1 0 1
1 0 1
1 0 1
1 1 0
P1
# E
3 5
1 1 1
1 0 0
1 1 1
1 0 0
1 1 1
P1
# F
3 5
1 1 1
1 0 0
1 1 1
1 0 0
1 0 0
P1
# G
3 5
0 1 1
1 0 0
1 0 1
1 0 1
0 1 1
P1
# H
3 5
1 0 1
1 0 1
1 1 1
1 0 1
1 0 1
P1
# I
3 5
1 1 1
0 1 0
0 1 0
0 1 0
1 1 1
P1
# J
3 5
0 1 1
0 0 1
0 0 1
1 0 1
0 1 0
P1
# K
3 5
1 0 1
1 0 1
1 1 0
1 0 1
1 0 1
P1
# L
3 5
1 0 0
1 0 0
1 0 0
1 0 0
1 1 1
P1
# M
3 5
1 0 1
1 1 1
1 0 1
1 0 1
1 0 1
P1
# N
3 5
1 1 0
1 0 1
1 0 1
1 0 1
1 0 1
P1
# O
3 5
0 1 0
1 0 1
1 0 1
1 0 1
0 1 0
P1
# P
3 5
1 1 0
1 0 1
1 1 0
1 0 0
1 0 0
P1
# Q
3 5
0 1 0
1 0 1
1 0 1
1 1 1
0 1 1
P1
# R
3 5
1 1 0
1 0 1
1 1 0
1 0 1
1 0 1
P1
# S
3 5
0 1 1
1 0 0
1 1 1
0 0 1
1 1 0
P1
# T
3 5
1 1 1
0 1 0
0 1 0
0 1 0
0 1 0
P1
# U
3 5
1 0 1
1 0 1
1 0 1
1 0 1
1 1 1
P1
# V
3 5
1 0 1
1 0 1
1 0 1
0 1 0
0 1 0
P1
# W
3 5
1 0 1
1 0 1
1 0 1
1 1 1
1 0 1
P1
# X
3 5
1 0 1
1 0 1
0 1 0
1 0 1
1 0 1
P1
# Y
3 5
1 0 1
1 0 1
0 1 0
0 1 0
0 1 0
P1
# Z
3 5
1 1 1
0 0 1
0 1 0
1 0 0
1 1 1

Números e símbolos:

P1
# 0
3 5
1 1 1
1 0 1
1 0 1
1 0 1
1 1 1
P1
# 1
3 5
0 1 0
1 1 0
0 1 0
0 1 0
1 1 1
P1
# 2
3 5
0 1 1
1 0 1
0 1 0
1 0 0
1 1 1
P1
# 3
3 5
1 1 0
0 0 1
1 1 1
0 0 1
1 1 0
P1
# 4
3 5
1 0 1
1 0 1
1 1 1
0 0 1
0 0 1
P1
# 5
3 5
1 1 1
1 0 0
1 1 1
0 0 1
1 1 0
P1
# 6
3 5
0 1 1
1 0 0
1 1 1
1 0 1
1 1 1
P1
# 7
3 5
1 1 1
0 0 1
0 1 0
0 1 0
0 1 0
P1
# 8
3 5
1 1 1
1 0 1
0 1 0
1 0 1
1 1 1
P1
# 9
3 5
1 1 1
1 0 1
0 1 1
0 0 1
1 1 0
P1
# .
3 5
0 0 0
0 0 0
0 0 0
0 0 0
1 0 0
P1
# ;
3 5
0 0 0
0 0 0
0 0 0
0 1 0
0 1 0
P1
# ,
3 5
0 0 0
0 0 0
0 0 0
0 1 0
0 1 0
P1
# :
3 5
0 0 0
0 1 0
0 0 0
0 1 0
0 0 0
P1
# '
3 5
0 0 1
0 0 1
0 0 0
0 0 0
0 0 0
P1
# "
3 5
0 1 1
0 1 1
0 0 0
0 0 0
0 0 0
P1
# (
3 5
0 1 0
1 0 0
1 0 0
1 0 0
0 1 0
P1
# !
3 5
0 1 0
0 1 0
0 1 0
0 0 0
0 1 0
P1
# ?
3 5
1 1 1
0 0 1
0 1 0
0 0 0
0 1 0
P1
# )
3 5
0 1 0
0 0 1
0 0 1
0 0 1
0 1 0
P1
# +
3 5
0 0 0
0 1 0
1 1 1
0 1 0
0 0 0
P1
# -
3 5
0 0 0
0 0 0
1 1 1
0 0 0
0 0 0
P1
# *
3 5
0 1 0
1 0 1
0 1 0
0 0 0
0 0 0
P1
# /
3 5
0 0 1
0 0 1
0 1 0
1 0 0
1 0 0
P1
# =
3 5
0 0 0
1 1 1
0 0 0
1 1 1
0 0 0

Com os códigos anteriores, pode-se desenhar texto alfanumérico como imagens. Com um pouco de criatividade e programação, pode-se mapear cada representação individual com o respectivo caractere para converter cadeias de caracteres em imagens para representá-las.

Portable Graymap Format (PGM)

O próximo passo é introduzir tons para a imagem. O formato PGM permite trabalhar com valores em escala de cinza; ele está especificado na documentação. Para a definição dos metadados, usa-se P2 e uma linha adicional, para a definição do número de possíveis valores para a escala de cinza. O valor pode ter um ou dois bytes. Pode-se escolher, portanto, qualquer valor entre 1 até 65535 (conta-se o zero). Versões antigas do formato limitavam a escala em um byte (ou seja, o valor máximo era 255).

No formato PGM, os valores são invertidos para as cores (quando comparados a PBM). 0 é preto; o valor máximo definido é branco. Valores intermediários definem a escala de cinza. Quanto mais próximos de 0, mais escuros. Quanto mais próximos do valor máximo, mais claros.

O exemplo a seguir define 255 como valor máximo para a escala de cinza. Ou seja, pode-se usar 256 tons. Para manter os valores alinhados, usou-se 000 para preto e 255 para branco.

P2
# Franco Garcia
23 11
255
000 000 000 255 000 000 255 255 255 000 255 255 000 000 255 255 255 000 000 255 255 000 255
000 255 255 255 000 255 000 255 000 255 000 255 000 255 000 255 000 255 255 255 000 255 000
000 000 000 255 000 000 255 255 000 000 000 255 000 255 000 255 000 255 255 255 000 255 000
000 255 255 255 000 255 000 255 000 255 000 255 000 255 000 255 000 255 255 255 000 255 000
000 255 255 255 000 255 000 255 000 255 000 255 000 255 000 255 255 000 000 255 255 000 255
255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255 255
255 000 000 255 255 000 255 255 000 000 255 255 255 000 000 255 000 000 000 255 255 000 255
000 255 255 255 000 255 000 255 000 255 000 255 000 255 255 255 255 000 255 255 000 255 000
000 255 000 255 000 000 000 255 000 000 255 255 000 255 255 255 255 000 255 255 000 000 000
000 255 000 255 000 255 000 255 000 255 000 255 000 255 255 255 255 000 255 255 000 255 000
255 000 000 255 000 255 000 255 000 255 000 255 255 000 000 255 000 000 000 255 000 255 000

Evidentemente, usar apenas branco e preto quando se pode usar outros 254 tons é um desperdício. Para começar a criar uma imagem programaticamente, pode-se pensar uma imagem como uma matriz de valores inteiros. Dessa forma, poder-se-á sortear ou definir alguma lógica em código para a escolha de um tom.

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

// Branco
const B = 255
// Preto
const P = 0

let caminho_arquivo = "franco-garcia-nome_franco.pgm"

let dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
let linhas = dados.length
let colunas = dados[0].length

for (let linha = 0; linha < linhas; ++linha) {
    for (let coluna = 0; coluna < colunas; ++coluna) {
        if (dados[linha][coluna] === P) {
            dados[linha][coluna] = inteiro_aleatorio(P, (linha * colunas + coluna) % B)
        }
    }
}

let conteudo = "P2\n# Franco Garcia\n"
conteudo += colunas + " " + linhas + "\n"
conteudo += B + "\n"
for (let linha = 0; linha < linhas; ++linha) {
    for (let coluna = 0; coluna < colunas; ++coluna) {
        conteudo += dados[linha][coluna] + " "
    }

    conteudo += "\n"
}

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-graymap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import random
import sys

from typing import Final

# Branco
B: Final = 255
# Preto
P: Final = 0

caminho_arquivo = "franco-garcia-nome_franco.pgm"

random.seed()

dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
linhas = len(dados)
colunas = len(dados[0])

for linha in range(linhas):
    for coluna in range(colunas):
        if (dados[linha][coluna] == P):
            dados[linha][coluna] = random.randint(P, (linha * colunas + coluna) % B)

conteudo = "P2\n# Franco Garcia\n"
conteudo += str(colunas) + " " + str(linhas) + "\n"
conteudo += str(B) + "\n"
for linha in range(linhas):
    for coluna in range(colunas):
        conteudo += str(dados[linha][coluna]) + " "

    conteudo += "\n"

try:
    arquivo = open(caminho_arquivo, "w")

    arquivo.write(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

-- Branco
local B = 255
-- Preto
local P = 0

local caminho_arquivo = "franco-garcia-nome_franco.pgm"

math.randomseed(os.time())

local dados = {
    {P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B},
    {B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B},
    {B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P},
    {P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P}
}
local linhas = #dados
local colunas = #dados[1]

for linha = 1, linhas do
    for coluna = 1, colunas do
        if (dados[linha][coluna] == P) then
            dados[linha][coluna] = math.random(P, (linha * colunas + coluna) % B)
        end
    end
end

local conteudo = "P2\n# Franco Garcia\n"
conteudo = conteudo .. colunas .. " " .. linhas .. "\n"
conteudo = conteudo .. B .. "\n"
for linha = 1, linhas do
    for coluna = 1, colunas do
        conteudo = conteudo .. dados[linha][coluna] .. " "

    conteudo = conteudo .. "\n"
    end
end

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    printerr("Erro ao tentar criar imagem.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(conteudo)
io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

# Branco
const B = 255
# Preto
const P = 0

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func _ready():
    var caminho_arquivo = "franco-garcia-nome_franco.pgm"

    randomize()

    var dados = [
        [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
        [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
        [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
        [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
    ]
    var linhas = len(dados)
    var colunas = len(dados[0])

    for linha in range(linhas):
        for coluna in range(colunas):
            if (dados[linha][coluna] == P):
                dados[linha][coluna] = inteiro_aleatorio(P, (linha * colunas + coluna) % B)

    var conteudo = "P2\n# Franco Garcia\n"
    conteudo += str(colunas) + " " + str(linhas) + "\n"
    conteudo += str(B) + "\n"
    for linha in range(linhas):
        for coluna in range(colunas):
            conteudo += str(dados[linha][coluna]) + " "

        conteudo += "\n"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar imagem.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")

Nas implementações, dados é uma matriz com valores inteiros usada como molde para a criação da imagem em escala de cinza. Todos os valores começam com a cor branca (B) ou com a cor preta (P).

Para manter branco como cor de fundo, o programa não altera valores B. Para variar tons de cinza, os valores P são trocados por um tom aleatório entre preto e branco. Como o valor máximo varia em função da linha e coluna, linhas e colunas com valores maiores tendem a ser mais claras que as anteriores -- até certo ponto. Como se usa o operador de resto de divisão, os tons voltam a tornar-se mais escuros quando o resultado exceder B (255), repetindo-se o padrão. Isso acontece, por exemplo, na implementação para Lua. A última linha da imagem terá tons semelhantes aos da primeira. Para um resultado similar aos de linguagens indexadas em zero, pode decrementar o valor de linha e coluna de uma unidade na geração do número aleatório.

dados[linha][coluna] = math.random(P, ((linha - 1) * colunas + coluna - 1) % B)

O restante da implementação cria o conteúdo do arquivo, com o cabeçalho e os valores convertidos para cadeias de caracteres. Campos com valores fixos são escritos diretamente. Os números de colunas, linhas e tons são escritos conforme os valores das variáveis. O mesmo ocorre para cada pixel da imagem: utiliza-se o valor armazenado no vetor dados convertido para cadeia de caracteres.

A imagem resultante deverá ser um pouco diferente a cada execução do programa. Na ilustração a seguir, compara-se uma possível imagem resultante em cima e uma ampliação de 1600% embaixo, ambas visualizadas no programa Gwenview.

Imagem PGM resultante exibida no Gwenview, com o texto 'Franco Garcia' escrito em tons de cinza sobre fundo branco. A imagem do topo é o resultado da execução do programa, com dimensões 23x11 pixels. A imagem de baixo é uma ampliação de 1600%.

Para visualizar a imagem resultante, você pode usar o Visualizador de Imagens Portable Graymap Format (PGM) criado pelo autor.

Uma próxima execução do programa provavelmente resultaria em um resultado um pouco diferente, excetuando-se a chance improvável do sorteio de todos os mesmos valores. Isso ocorreria caso não se definisse uma semente a cada execução do programa.

Ampliando a Imagem PGM

Uma forma simples de ampliar a imagem é repetir cada pixel como um quadrado. Por exemplo, para triplicar o tamanho da imagem, desenha o mesmo pixel 9 vezes: três linhas com três colunas do pixel. Repetindo-se o processo para todos os pixels, a imagem toda terá o triplo do tamanho original.

Para um exemplo, pode-se considerar o contorno de um quadrado.

1 1 1
1 0 1
1 1 1

Para triplicar a imagem, cada valor seria desenhado como uma submatriz 3x3.

1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 0 0 0 1 1 1
1 1 1 0 0 0 1 1 1
1 1 1 0 0 0 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1

Usando a implementação em Python para o exemplo, pode-se repetir o desenho de cada linha e de coluna algumas vezes.

escala = 10conteudo = "P2\n# Franco Garcia\n"
conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"conteudo += str(B) + "\n"
for linha in range(linhas):
    for escala_altura in range(escala):        for coluna in range(colunas):
            pixel = str(dados[linha][coluna])            for escala_largura in range(escala):                conteudo += pixel + " "
        conteudo += "\n"

Pode-se efetuar a alteração em todas as implementações para criar uma imagem maior. É importante notar que o tempo de execução de cada programa também aumentará. Afinal, para um valor de escala 10, serão feitas 100 vezes mais operações que para a geração da imagem original, resultado da inclusão de duas estruturas de repetição aninhadas no bloco.

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

// Branco
const B = 255
// Preto
const P = 0

let caminho_arquivo = "franco-garcia-nome_franco.pgm"

let dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
let linhas = dados.length
let colunas = dados[0].length

for (let linha = 0; linha < linhas; ++linha) {
    for (let coluna = 0; coluna < colunas; ++coluna) {
        if (dados[linha][coluna] === P) {
            dados[linha][coluna] = inteiro_aleatorio(P, (linha * colunas + coluna) % B)
        }
    }
}

let escala = 10
let conteudo = "P2\n# Franco Garcia\n"
conteudo += (escala * colunas) + " " + (escala * linhas) + "\n"
conteudo += B + "\n"
for (let linha = 0; linha < linhas; ++linha) {
    for (let escala_altura = 0; escala_altura < escala; ++escala_altura) {
        for (let coluna = 0; coluna < colunas; ++coluna) {
            let pixel = dados[linha][coluna]
            for (let escala_largura = 0; escala_largura < escala; ++escala_largura) {
                conteudo += dados[linha][coluna] + " "
            }
        }

        conteudo += "\n"
    }
}

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-graymap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import random
import sys

from typing import Final

# Branco
B: Final = 255
# Preto
P: Final = 0

caminho_arquivo = "franco-garcia-nome_franco.pgm"

random.seed()

dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
linhas = len(dados)
colunas = len(dados[0])

for linha in range(linhas):
    for coluna in range(colunas):
        if (dados[linha][coluna] == P):
            dados[linha][coluna] = random.randint(P, (linha * colunas + coluna) % B)

escala = 10
conteudo = "P2\n# Franco Garcia\n"
conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
conteudo += str(B) + "\n"
for linha in range(linhas):
    for escala_altura in range(escala):
        for coluna in range(colunas):
            pixel = str(dados[linha][coluna])
            for escala_largura in range(escala):
                conteudo += pixel + " "

        conteudo += "\n"

try:
    arquivo = open(caminho_arquivo, "w")

    arquivo.write(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

-- Branco
local B = 255
-- Preto
local P = 0

local caminho_arquivo = "franco-garcia-nome_franco.pgm"

math.randomseed(os.time())

local dados = {
    {P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B},
    {B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B},
    {B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P},
    {P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P}
}
local linhas = #dados
local colunas = #dados[1]

for linha = 1, linhas do
    for coluna = 1, colunas do
        if (dados[linha][coluna] == P) then
            dados[linha][coluna] = math.random(P, (linha * colunas + coluna) % B)
        end
    end
end

local escala = 10
local conteudo = "P2\n# Franco Garcia\n"
conteudo = conteudo .. (escala * colunas) .. " " .. (escala * linhas) .. "\n"
conteudo = conteudo .. B .. "\n"
for linha = 1, linhas do
    for escala_altura = 1, escala do
        for coluna = 1, colunas do
            local pixel = dados[linha][coluna]
            for escala_altura = 1, escala do
                conteudo = conteudo .. pixel .. " "
            end

        conteudo = conteudo .. "\n"
        end
    end
end

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    printerr("Erro ao tentar criar imagem.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(conteudo)
io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

# Branco
const B = 255
# Preto
const P = 0

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func _ready():
    var caminho_arquivo = "franco-garcia-nome_franco.pgm"

    randomize()

    var dados = [
        [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
        [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
        [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
        [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
    ]
    var linhas = len(dados)
    var colunas = len(dados[0])

    for linha in range(linhas):
        for coluna in range(colunas):
            if (dados[linha][coluna] == P):
                dados[linha][coluna] = inteiro_aleatorio(P, (linha * colunas + coluna) % B)

    var escala = 10
    var conteudo = "P2\n# Franco Garcia\n"
    conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
    conteudo += str(B) + "\n"
    for linha in range(linhas):
        for escala_altura in range(escala):
            for coluna in range(colunas):
                var pixel = str(dados[linha][coluna])
                for escala_largura in range(escala):
                    conteudo += pixel + " "

            conteudo += "\n"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar imagem.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")

A técnica usada para ampliação é bastante simples. O resultado será satisfatório para pixel art, mas inadequado para imagens complexas como fotografias. Para melhores resultados para imagens complexas, pode-se explorar algoritmos de processamento de imagem (assim como filtros).

Além disso, para imagens maiores, o uso da representação textual é inadequado. A representação binária melhoria o desempenho, especialmente por eliminar operações mais caras usando cadeias de caracteres.

De qualquer forma, o exemplo fornece indícios de como um editor de imagens (do tipo bitmap, raster ou matriciais) funciona. Operações como escala, rotação, translação, aplicação de filtros, e alteração de cores executam algoritmos para manipular pixels e editar a imagem. Para saber mais, convém conhecer affine transformations (transformações afins). Operações com imagens vetoriais (como no formato Scalable Vector Graphics ou, mais popularmente, SVG) são um pouco diferentes, pois operam matematicamente com vetores (ao invés de pixels). A conversão de uma imagem vetorial para uma imagem raster é chamada de rasterization (rasterização, em Português).

Portable Pixmap Format (PPM)

O passo final é introduzir cores na imagem. O formato PPM define cores usando o modelo de cores Red, Blue, Green (RGB). A especificação do formato está disponível na documentação. O modelo utiliza vermelho, azul e verde como cores primárias para a composição de outras cores.

Cada pixel da imagem é definido por três valores inteiros, considerados componentes do modelo RGB. O primeiro corresponde ao tom de vermelho para o pixel. O segundo corresponde ao tom de verde para o pixel. O terceiro corresponde ao tom de azul para o pixel.

O valor 0 para todos os componentes gera a cor preta. O valor máximo para todos os componentes gera a cor branca. Valores intermediários para cada componente gera uma cor como combinação das cores primárias.

O formato do arquivo PPM é similar ao do PGM. Ele possui código P3 para a versão em arquivo texto. O número de tons é aplicado para cada cor primária. Além disso, o formato armazena o triplo de valores: um valor por cor primária por pixel.

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

// Branco
const B = 255
// Preto
const P = 0

class Pixel {
    constructor(vermelho, verde, azul) {
        this.vermelho = vermelho
        this.verde = verde
        this.azul = azul
    }
}

let caminho_arquivo = "franco-garcia-nome_franco.ppm"

let dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
let linhas = dados.length
let colunas = dados[0].length

var pixels = []
for (let linha = 0; linha < linhas; ++linha) {
    pixels.push([])
    for (let coluna = 0; coluna < colunas; ++coluna) {
        if (dados[linha][coluna] === P) {
            let pixel = new Pixel(inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                  inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                  inteiro_aleatorio(P, (linha * colunas + coluna) % B))
            pixels[linha].push(pixel)
        } else {
            pixels[linha].push(new Pixel(B, B, B))
        }
    }
}

let conteudo = "P3\n# Franco Garcia\n"
conteudo += colunas + " " + linhas + "\n"
conteudo += B + "\n"
for (let linha = 0; linha < linhas; ++linha) {
    for (let coluna = 0; coluna < colunas; ++coluna) {
        let pixel = pixels[linha][coluna]
        conteudo += pixel.vermelho + " " + pixel.verde + " " + pixel.azul + " "
    }

    conteudo += "\n"
}

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-pixmap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import random
import sys

from typing import Final

# Branco
B: Final = 255
# Preto
P: Final = 0

class Pixel:
    def __init__(self, vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

caminho_arquivo = "franco-garcia-nome_franco.ppm"

random.seed()

dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
linhas = len(dados)
colunas = len(dados[0])

pixels = []
for linha in range(linhas):
    pixels.append([])
    for coluna in range(colunas):
        if (dados[linha][coluna] == P):
            pixel = Pixel(random.randint(P, (linha * colunas + coluna) % B),
                          random.randint(P, (linha * colunas + coluna) % B),
                          random.randint(P, (linha * colunas + coluna) % B))
            pixels[linha].append(pixel)
        else:
            pixels[linha].append(Pixel(B, B, B))

conteudo = "P3\n# Franco Garcia\n"
conteudo += str(colunas) + " " + str(linhas) + "\n"
conteudo += str(B) + "\n"
for linha in range(linhas):
    for coluna in range(colunas):
        pixel = pixels[linha][coluna]
        conteudo += str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "

    conteudo += "\n"

try:
    arquivo = open(caminho_arquivo, "w")

    arquivo.write(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

function crie_pixel(vermelho, verde, azul)
    local resultado = {
        vermelho = vermelho,
        verde = verde,
        azul = azul
    }

    return resultado
end

-- Branco
local B = 255
-- Preto
local P = 0

local caminho_arquivo = "franco-garcia-nome_franco.ppm"

math.randomseed(os.time())

local dados = {
    {P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B},
    {B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B},
    {B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P},
    {P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P}
}
local linhas = #dados
local colunas = #dados[1]

local pixels = {}
for linha = 1, linhas do
    table.insert(pixels, {})
    for coluna = 1, colunas do
        if (dados[linha][coluna] == P) then
            local pixel = crie_pixel(math.random(P, (linha * colunas + coluna) % B),
                                     math.random(P, (linha * colunas + coluna) % B),
                                     math.random(P, (linha * colunas + coluna) % B))
            table.insert(pixels[linha], pixel)
        else
            table.insert(pixels[linha], crie_pixel(B, B, B))
        end
    end
end

local conteudo = "P3\n# Franco Garcia\n"
conteudo = conteudo .. colunas .. " " .. linhas .. "\n"
conteudo = conteudo .. B .. "\n"
for linha = 1, linhas do
    for coluna = 1, colunas do
        local pixel = pixels[linha][coluna]
        conteudo = conteudo .. pixel.vermelho .. " " .. pixel.verde .. " " .. pixel.azul .. " "

    conteudo = conteudo .. "\n"
    end
end

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    printerr("Erro ao tentar criar imagem.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(conteudo)
io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

# Branco
const B = 255
# Preto
const P = 0

class Pixel:
    var vermelho
    var verde
    var azul

    func _init(vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func _ready():
    var caminho_arquivo = "franco-garcia-nome_franco.ppm"

    randomize()

    var dados = [
        [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
        [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
        [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
        [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
    ]
    var linhas = len(dados)
    var colunas = len(dados[0])

    var pixels = []
    for linha in range(linhas):
        pixels.append([])
        for coluna in range(colunas):
            if (dados[linha][coluna] == P):
                var pixel = Pixel.new(inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                      inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                      inteiro_aleatorio(P, (linha * colunas + coluna) % B))
                pixels[linha].append(pixel)
            else:
                pixels[linha].append(Pixel.new(B, B, B))

    var conteudo = "P3\n# Franco Garcia\n"
    conteudo += str(colunas) + " " + str(linhas) + "\n"
    conteudo += str(B) + "\n"
    for linha in range(linhas):
        for coluna in range(colunas):
            var pixel = pixels[linha][coluna]
            conteudo += str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "

        conteudo += "\n"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar imagem.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")

Para manter o padrão e apenas adicionar cores, usa-se dados como modelo para o desenho e pixels para armazenar uma matriz de registros do novo tipo Pixel, que armazena um número inteiro para cada cor primária. A nova implementação é similar a feita para PGM. Contudo, desta vez sorteia-se três valores para cada pixel: um para cada cor primária. Da mesma forma, a cadeia de caracteres conteudo armazena os três valores convertidos para texto e delimitados por um espaço.

Como na imagem PGM, no caso de Lua, a última linha deve ficar mais escura, com tons mais próximos de preto. Para um resultado similar aos de linguagens indexadas em 0, pode-se decrementar o valor linha e coluna para o sorteio da cor.

local pixel = crie_pixel(math.random(P, ((linha - 1) * colunas + coluna - 1) % B),
                         math.random(P, ((linha - 1) * colunas + coluna - 1) % B),
                         math.random(P, ((linha - 1) * colunas + coluna - 1) % B))

De qualquer forma, uma imagem com mais linhas apresentaria o mesmo comportamento para todas as implementações. Devido ao uso do módulo, a oscilação de cores seria, de certa forma, cíclica: de tons mais próximos de preto, para tons mais coloridos, para tons mais próximos de preto...

Na ilustração a seguir, compara-se uma possível imagem resultante em cima e uma ampliação de 1600% embaixo, ambas visualizadas no programa Gwenview.

Imagem PPM resultante exibida no Gwenview, com o texto 'Franco Garcia' escrita com cores aleatórias sobre fundo branco. A imagem do topo é o resultado da execução do programa, com dimensões 23x11 pixels. A imagem de baixo é uma ampliação de 1600%.

Para visualizar a imagem resultante, você pode usar o Visualizador de Imagens Portable Pixmap Format (PPM) criado pelo autor.

Em outro uso do programa, as cores possivelmente seriam diferentes.

Ampliando a Imagem PPM

A mesma técnica usada para imagens PGM pode ser usada para imagens PPM.

function inteiro_aleatorio(minimo_inclusive, maximo_inclusive) {
    let minimo = Math.ceil(minimo_inclusive)
    let maximo = Math.floor(maximo_inclusive)

    return Math.floor(minimo + Math.random() * (maximo + 1 - minimo))
}

// Branco
const B = 255
// Preto
const P = 0

class Pixel {
    constructor(vermelho, verde, azul) {
        this.vermelho = vermelho
        this.verde = verde
        this.azul = azul
    }
}

let caminho_arquivo = "franco-garcia-nome_franco.ppm"

let dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
let linhas = dados.length
let colunas = dados[0].length

var pixels = []
for (let linha = 0; linha < linhas; ++linha) {
    pixels.push([])
    for (let coluna = 0; coluna < colunas; ++coluna) {
        if (dados[linha][coluna] === P) {
            let pixel = new Pixel(inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                  inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                  inteiro_aleatorio(P, (linha * colunas + coluna) % B))
            pixels[linha].push(pixel)
        } else {
            pixels[linha].push(new Pixel(B, B, B))
        }
    }
}

let escala = 10
let conteudo = "P3\n# Franco Garcia\n"
conteudo += (escala * colunas) + " " + (escala * linhas) + "\n"
conteudo += B + "\n"
for (let linha = 0; linha < linhas; ++linha) {
    for (let escala_altura = 0; escala_altura < escala; ++escala_altura) {
        for (let coluna = 0; coluna < colunas; ++coluna) {
            let pixel = pixels[linha][coluna]
            let pixel_texto = pixel.vermelho + " " + pixel.verde + " " + pixel.azul + " "
            for (let escala_largura = 0; escala_largura < escala; ++escala_largura) {
                conteudo += pixel_texto
            }
        }

        conteudo += "\n"
    }
}

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-pixmap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import random
import sys

from typing import Final

# Branco
B: Final = 255
# Preto
P: Final = 0

class Pixel:
    def __init__(self, vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

caminho_arquivo = "franco-garcia-nome_franco.ppm"

random.seed()

dados = [
    [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
    [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
    [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
    [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
    [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
    [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
]
linhas = len(dados)
colunas = len(dados[0])

pixels = []
for linha in range(linhas):
    pixels.append([])
    for coluna in range(colunas):
        if (dados[linha][coluna] == P):
            pixel = Pixel(random.randint(P, (linha * colunas + coluna) % B),
                          random.randint(P, (linha * colunas + coluna) % B),
                          random.randint(P, (linha * colunas + coluna) % B))
            pixels[linha].append(pixel)
        else:
            pixels[linha].append(Pixel(B, B, B))

escala = 10
conteudo = "P3\n# Franco Garcia\n"
conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
conteudo += str(B) + "\n"
for linha in range(linhas):
    for escala_altura in range(escala):
        for coluna in range(colunas):
            pixel = pixels[linha][coluna]
            pixel_texto = str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "
            for escala_largura in range(escala):
                conteudo += pixel_texto

        conteudo += "\n"

try:
    arquivo = open(caminho_arquivo, "w")

    arquivo.write(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

function crie_pixel(vermelho, verde, azul)
    local resultado = {
        vermelho = vermelho,
        verde = verde,
        azul = azul
    }

    return resultado
end

-- Branco
local B = 255
-- Preto
local P = 0

local caminho_arquivo = "franco-garcia-nome_franco.ppm"

math.randomseed(os.time())

local dados = {
    {P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B},
    {B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B},
    {B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B},
    {P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P},
    {P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P},
    {B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P}
}
local linhas = #dados
local colunas = #dados[1]

local pixels = {}
for linha = 1, linhas do
    table.insert(pixels, {})
    for coluna = 1, colunas do
        if (dados[linha][coluna] == P) then
            local pixel = crie_pixel(math.random(P, (linha * colunas + coluna) % B),
                                     math.random(P, (linha * colunas + coluna) % B),
                                     math.random(P, (linha * colunas + coluna) % B))
            table.insert(pixels[linha], pixel)
        else
            table.insert(pixels[linha], crie_pixel(B, B, B))
        end
    end
end

local escala = 10
local conteudo = "P3\n# Franco Garcia\n"
conteudo = conteudo .. (escala * colunas) .. " " .. (escala * linhas) .. "\n"
conteudo = conteudo .. B .. "\n"
for linha = 1, linhas do
    for escala_altura = 1, escala do
        for coluna = 1, colunas do
            local pixel = pixels[linha][coluna]
            local pixel_texto = pixel.vermelho .. " " .. pixel.verde .. " " .. pixel.azul .. " "
            for escala_altura = 1, escala do
                conteudo = conteudo .. pixel_texto
            end

            conteudo = conteudo .. "\n"
        end
    end
end

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    printerr("Erro ao tentar criar imagem.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(conteudo)
io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

# Branco
const B = 255
# Preto
const P = 0

class Pixel:
    var vermelho
    var verde
    var azul

    func _init(vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

func inteiro_aleatorio(minimo_inclusive, maximo_inclusive):
    var minimo = ceil(minimo_inclusive)
    var maximo = floor(maximo_inclusive)

    # randi(): [0.0, 1.0[
    return randi() % int(maximo + 1 - minimo) + minimo

func _ready():
    var caminho_arquivo = "franco-garcia-nome_franco.ppm"

    randomize()

    var dados = [
        [P, P, P, B, P, P, B, B, B, P, B, B, P, P, B, B, B, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, P, P, B, P, P, B, B, P, P, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, P, B, P],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, P, B, B, P, P, B, B, P, B],
        [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B],
        [B, P, P, B, B, P, B, B, P, P, B, B, B, P, P, B, P, P, P, B, B, P, B],
        [P, B, B, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [P, B, P, B, P, P, P, B, P, P, B, B, P, B, B, B, B, P, B, B, P, P, P],
        [P, B, P, B, P, B, P, B, P, B, P, B, P, B, B, B, B, P, B, B, P, B, P],
        [B, P, P, B, P, B, P, B, P, B, P, B, B, P, P, B, P, P, P, B, P, B, P]
    ]
    var linhas = len(dados)
    var colunas = len(dados[0])

    var pixels = []
    for linha in range(linhas):
        pixels.append([])
        for coluna in range(colunas):
            if (dados[linha][coluna] == P):
                var pixel = Pixel.new(inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                      inteiro_aleatorio(P, (linha * colunas + coluna) % B),
                                      inteiro_aleatorio(P, (linha * colunas + coluna) % B))
                pixels[linha].append(pixel)
            else:
                pixels[linha].append(Pixel.new(B, B, B))

    var escala = 10
    var conteudo = "P3\n# Franco Garcia\n"
    conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
    conteudo += str(B) + "\n"
    for linha in range(linhas):
        for escala_altura in range(escala):
            for coluna in range(colunas):
                var pixel = pixels[linha][coluna]
                var pixel_texto = str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "
                for escala_largura in range(escala):
                    conteudo += pixel_texto

            conteudo += "\n"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar imagem.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")

A imagem ampliada potencializa as cores aleatórias usadas na imagem. Dependendo da sorte, o efeito pode ficar interessante; contudo, ele é ruído.

O tempo de execução do programa também pode aumentar significativamente com a ampliação. Novamente, operações com cadeias de caracteres são mais caras que com números. Em potencial, a forma como as implementações realizam concatenações é ineficiente e lenta (embora simples). Embora seja possível modificar a implementação para salvar os dados gradualmente em arquivo ao invés de armazenar todo o conteúdo na cadeia de caracteres conteudo, o ideal seria usar um arquivo binário. Para melhorá-lo, poder-se-ia optar pela representação binária para os dados, trocando-se operações usando cadeias de caracteres por operações usando bytes números inteiros, preferencialmente em um vetor com tamanho pré-alocado para evitar redimensionamentos.

Possíveis Melhorias e Variações

Existem algumas possibilidades para melhorar o programa:

  • Salvar o resultado como arquivo binário;
  • Usar um algoritmo melhor para ampliação da imagem. Uma alternativa mais simples é usar recursos de linguagens de programação para executar um comando externo dentro do programa. Por exemplo, poder-se-ia adicionar um comando que acionasse ImageMagick para ampliar a imagem;
  • Escrever texto arbitrário na imagem, ao invés de um padrão pré-definido. Para isso, poder-se-ia armazenar cada possível caractere em um dicionário e montar a imagem sob demanda. Um exemplo é fornecido como ferramenta online em Conversor de Texto para Imagem PPM.

Também é possível tentar a implementação de um novo formato, como Bitmap Image File, (.bmp). O formato .bmp é exclusivamente binário e um pouco mais complexo que PBM, PGM e PPM.

Caso se queira trabalhar com transparência, pode-se explorar o modelo de cores Red, Blue, Green, Alpha (RGBA). Um formato similar a PPM com suporte a transparência chama-se Portable Arbitrary Map (PAM) (documentação).

Para atividades mais criativas, pode-se criar imagens mais complexas e/ou desenhos. A implementação definida pode servir como base para criar novas imagens. A base do programa para a criação da imagem é a mesma; o que muda é a forma de preencher os pixels. Em particular, também pode ser interessante criar imagens programaticamente, como ilustrado na próxima subseção.

Gradientes de Cores

Uma possível aplicação imediata é usar estruturas de repetição para iterar entre tons de uma mesma cor, para se criar um gradiente. No exemplo a seguir, define-se cores para pixels como combinações de cores primárias sucessivas.

// Branco
const B = 255
// Preto
const P = 0
const TONS = 256
const INCREMENTO = 20

class Pixel {
    constructor(vermelho, verde, azul) {
        this.vermelho = vermelho
        this.verde = verde
        this.azul = azul
    }
}

let caminho_arquivo = "franco-garcia-cores.ppm"

var pixels = []
var linha = -1
for (let vermelho = 0; vermelho < TONS; vermelho += INCREMENTO) {
    pixels.push([])
    linha += 1
    for (let verde = 0; verde < TONS; verde += INCREMENTO) {
        for (let azul = 0; azul < TONS; azul += INCREMENTO) {
            pixels[linha].push(new Pixel(vermelho, verde, azul))
        }
    }
}

let linhas = pixels.length
let colunas = pixels[0].length

let escala = 5
let conteudo = "P3\n# Gradiente de cores\n"
conteudo += (escala * colunas) + " " + (escala * linhas) + "\n"
conteudo += B + "\n"
for (let linha = 0; linha < linhas; ++linha) {
    for (let escala_altura = 0; escala_altura < escala; ++escala_altura) {
        for (let coluna = 0; coluna < colunas; ++coluna) {
            let pixel = pixels[linha][coluna]
            let pixel_texto = pixel.vermelho + " " + pixel.verde + " " + pixel.azul + " "
            for (let escala_largura = 0; escala_largura < escala; ++escala_largura) {
                conteudo += pixel_texto
            }
        }

        conteudo += "\n"
    }
}

let arquivo = new File([conteudo], caminho_arquivo, {type: "image/x-portable-pixmap"})
console.log("Imagem criada com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import sys

from typing import Final

# Branco
B: Final = 255
# Preto
P: Final = 0
TONS: Final = 256
INCREMENTO: Final = 20

class Pixel:
    def __init__(self, vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

caminho_arquivo = "franco-garcia-cores.ppm"

pixels = []
linha = -1
for vermelho in range(0, TONS, INCREMENTO):
    pixels.append([])
    linha += 1
    for verde in range(0, TONS, INCREMENTO):
        for azul in range(0, TONS, INCREMENTO):
            pixels[linha].append(Pixel(vermelho, verde, azul))

linhas = len(pixels)
colunas = len(pixels[0])

escala = 5
conteudo = "P3\n# Gradiente de cores\n"
conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
conteudo += str(B) + "\n"
for linha in range(linhas):
    for escala_altura in range(escala):
        for coluna in range(colunas):
            pixel = pixels[linha][coluna]
            pixel_texto = str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "
            for escala_largura in range(escala):
                conteudo += pixel_texto

        conteudo += "\n"

try:
    arquivo = open(caminho_arquivo, "w")

    arquivo.write(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar imagem.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

function crie_pixel(vermelho, verde, azul)
    local resultado = {
        vermelho = vermelho,
        verde = verde,
        azul = azul
    }

    return resultado
end

-- Branco
local B = 255
-- Preto
local P = 0
local TONS = 256
local INCREMENTO = 20

local caminho_arquivo = "franco-garcia-cores.ppm"

local pixels = {}
local linha = 0
for vermelho = 0, TONS -1, INCREMENTO do
    table.insert(pixels, {})
    linha = linha + 1
    for verde = 0, TONS -1, INCREMENTO do
        for azul = 0, TONS -1, INCREMENTO do
            table.insert(pixels[linha], crie_pixel(vermelho, verde, azul))
        end
    end
end

local linhas = #pixels
local colunas = #pixels[1]

local escala = 5
local conteudo = "P3\n# Gradiente de cores\n"
conteudo = conteudo .. (escala * colunas) .. " " .. (escala * linhas) .. "\n"
conteudo = conteudo .. B .. "\n"
for linha = 1, linhas do
    for escala_altura = 1, escala do
        for coluna = 1, colunas do
            local pixel = pixels[linha][coluna]
            local pixel_texto = pixel.vermelho .. " " .. pixel.verde .. " " .. pixel.azul .. " "
            for escala_altura = 1, escala do
                conteudo = conteudo .. pixel_texto
            end

            conteudo = conteudo .. "\n"
        end
    end
end

local arquivo = io.open(caminho_arquivo, "w")
if (arquivo == nil) then
    print(debug.traceback())

    printerr("Erro ao tentar criar imagem.")
    os.exit(EXIT_FAILURE)
end

arquivo:write(conteudo)
io.close(arquivo)

print("Imagem criada com sucesso.")
extends Node

const EXIT_FAILURE = 1

# Branco
const B = 255
# Preto
const P = 0
const TONS = 256
const INCREMENTO = 20

class Pixel:
    var vermelho
    var verde
    var azul

    func _init(vermelho, verde, azul):
        self.vermelho = vermelho
        self.verde = verde
        self.azul = azul

func _ready():
    var caminho_arquivo = "franco-garcia-cores.ppm"

    var pixels = []
    var linha_atual = -1
    for vermelho in range(0, TONS, INCREMENTO):
        pixels.append([])
        linha_atual += 1
        for verde in range(0, TONS, INCREMENTO):
            for azul in range(0, TONS, INCREMENTO):
                pixels[linha_atual].append(Pixel.new(vermelho, verde, azul))

    var linhas = len(pixels)
    var colunas = len(pixels[0])

    var escala = 5
    var conteudo = "P3\n# Gradiente de cores\n"
    conteudo += str(escala * colunas) + " " + str(escala * linhas) + "\n"
    conteudo += str(B) + "\n"
    for linha in range(linhas):
        for escala_altura in range(escala):
            for coluna in range(colunas):
                var pixel = pixels[linha][coluna]
                var pixel_texto = str(pixel.vermelho) + " " + str(pixel.verde) + " " + str(pixel.azul) + " "
                for escala_largura in range(escala):
                    conteudo += pixel_texto

            conteudo += "\n"

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar imagem.")
        get_tree().quit(EXIT_FAILURE)

    arquivo.store_string(conteudo)
    arquivo.close()

    print("Imagem criada com sucesso.")

Na implementação, TONS armazena o número máximo de cores por tom. Como se usa um byte por cor, o valor máximo é 256. INCREMENTO define o incremento para o próximo tom. Como se usa um arquivo texto, o uso de todas as cores com INCREMENTO igual 1 levaria um tempo maior para a execução do programa. Idem o ajuste de escala para 5.

Imagem PPM resultante com gradiente de cores. O gradiente começa com cores mais escuras e próximas de vermelho, e termina com cores claras mais próximas de verde, amarelo, ciano e branco.

Caso queira visualizar todos os possíveis gradientes, basta alterar o valor de INCREMENTO para 1 e aguardar (possivelmente alguns segundos ou minutos). Nesse caso, alterar a escala para 1 pode ser uma boa idéia; com esse valor, o arquivo resultante terá cerca de 171 MB (o GIMP pode visualizá-lo, mas outros visualizadores podem ter problemas).

Para comparação, pode-se converter o arquivo resultante para PNG. Por exemplo, usando-se ImageMagick, o arquivo convertido para PNG teria certa de 50 kB.

convert franco-garcia-cores.ppm franco-garcia-cores.png

Ou seja, trabalhar com arquivos texto para imagens é conveniente para aprendizado, mas não para uso cotidiano ou profissional. Para imagens complexas, o ideal é trabalhar com formatos binários mais eficientes, potencialmente que empreguem algoritmos de compressão para reduzir o tamanho do arquivo. O formato PNG é uma boa opção para ilustrações (e é lossless, ou seja, a compressão não resulta em perda de informação). O formato JPEG é uma boa opção para fotografias (embora seja lossy, isto é, a compressão resulta em perda de informação).

Outros Formatos e Compressão

PBM, PGM e PPM são formatos simples. Entretanto, eles ocupam espaço significativo para armazenamento, sobretudo no formato de arquivo texto. Para verificar a afirmação, pode-se gerar imagens ampliadas (por exemplo, com escala com valor 10 ou 20) em formato PPM e obter o tamanho das imagens. É possível que a imagem tenha dezenas de megabytes.

A implementação como arquivo binário reduziria um pouco o tamanho (contudo, ele ainda seria grande). Para economizar espaço, pode-se converter a imagem para formatos como PNG, ou formatos mais modernos como WebP e AV1 Image File Format (AVIF). Contudo, esses são formatos mais complexos para implementar, devido aos algoritmos para compressão. É mais simples usar uma biblioteca para a criação de arquivos no formato que implementar uma.

Arquivos de Áudio (Som)

Para complementar animações e imagens, pode-se explorar som. Com seqüências de imagens (quadros ou frames) e sons, pode-se formar vídeos. Ou seja, conteúdo multimídia básico.

Para trabalhar com de áudio, alguns conceitos são importantes:

  • Amostra ou amostragem de sinal (sampling): uma amostra seria o equivalente de um quadro para uma imagem. Ela é um valor que representa o sinal de áudio em um determinado tempo;
  • Taxa de amostragem (sampling rate): quantidade de amostras por unidade de tempo, é uma freqüência comumente medida em Hertz (Hz). Algumas taxas de amostragem tradicionais são 44.1 kHz (usada para CDs) e 48 kHZ (usada para DVDs);
  • Endianness: como em arquivos binários, para o ordem de bits big-endian (BE) ou little-endian (LE);
  • Quantidade ou profundidade de bits para cada amostra (bit depth): número de bits usado para representar dados em cada amostra;
  • Fonte (source): origem do som;
  • Número de canais (channels): um canal é a posição de cada fonte de áudio em um sinal;
  • Amplitude: medida de magnitude de oscilação de uma onda no tempo, considerada entre o ponto nulo e o ponto máximo positivo (ou negativo).

O MDN fornece uma boa introdução para áudio digital; contudo, o material não possui tradução para Português. Nesta subseção, explora-se a criação de arquivos de áudio. Arquivos de áudio são armazenados como arquivos binários.

Onda Senoidal: O "Olá, Mundo!" do Áudio

Converter texto para áudio é algo mais complexo que representar texto em uma imagem. A técnica chama-se text-to-speech (TTS; texto para fala) e é um recurso importante para acessibilidade. Embora seja possível usar APIs de TTS em programas, a complexidade de criar um algoritmo de síntese de voz não é adequada para uma subseção deste tópico.

Assim, os exemplos usarão sons mais simples. Criar um som com valores aleatórios provavelmente resultaria em ruído (noise). Por exemplo, nas imagens coloridas criadas, as cores resultam de ruído. Não há padrão nem critério artístico; elas são aleatórias. Visualmente, elas podem gerar um efeito interessante, embora caótico. Em um som, contudo, ruído tende a ser desagradável.

Uma onda senoidal (onda seno, senóide, ou onda sinusoidal) talvez possa ser considerada como o Olá, mundo! da programação em áudio. O som codificado a partir de uma onda senoidal é simples, mas limpo. Pode-se calcular uma onda senoidal em função do tempo, pode-se usar uma equação.

Na equação, é a função seno. Em Português, ela tende a ser escrita como .

Para o restante dos símbolos:

  • é o tempo (em segundos);
  • é a constante matemática pi;
  • é amplitude máxima para a onda. Para som, ela costuma ser media em decibéis (dB);
  • é a freqüência angular, que pode ser calculada como ;
  • é freqüência (em Hertz) da onda;
  • (letra grega phi) é fase, que indica o deslocamento inicial da onda relativa ao ciclo de oscilação. Um valor negativo significa que a onda está "atrasada" em relação ao ciclo normal (valor 0). Um valor positivo significa que a onda está "adiantada" em relação ao ciclo normal. A fase normalmente é medida como um ângulo.

Para um som digital, os valores para , e tendem a ser constantes. Embora eles possam variar, assumir-se-á que eles sejam valores fixos após a definição. Isso significa que apenas o tempo variará na construção do som como onda senoidal. Assim, pode-se definir um intervalo de tempo calculado em uma estrutura de repetição para gerar o som para um intervalo (por exemplo, com alguns segundos de duração).

O processo é chamado de discretização. O código a seguir faz a discretização de uma onda senoidal para um vetor de amostras de som.

// Valor máximo para um inteiro com sinal de um byte.
// Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
let amplitude = 127
// Em Hz (44.1 KHz: qualidade de CD)
let taxa_amostragem = 44100
let canais = 1
// Duração desejada para o áudio, em segundos
let duracao = 1

// Tamanho do vetor que armazenará as amostras para a onda.
let numero_amostras = duracao * taxa_amostragem * canais

// Freqüência escolhida para o som, em Hertz.
let frequencia = 440

// Parte constante da equação.
let multiplicador = 2.0 * Math.PI * frequencia / taxa_amostragem

// Geração de valores para o som.
let amostras = []
for (let t = 0; t < numero_amostras; ++t) {
    let amostra = Math.floor(amplitude * Math.sin(multiplicador * t))
    amostras.push(amostra)
}
import math

# Valor máximo para um inteiro com sinal de um byte.
# Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
amplitude = 127
# Em Hz (44.1 KHz: qualidade de CD)
taxa_amostragem = 44100
canais = 1
# Duração desejada para o áudio, em segundos
duracao = 1

# Tamanho do vetor que armazenará as amostras para a onda.
numero_amostras = duracao * taxa_amostragem * canais

# Freqüência escolhida para o som, em Hertz.
frequencia = 440

# Parte constante da equação.
multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

# Geração de valores para o som.
amostras = []
for t in range(numero_amostras):
    amostra = math.floor(amplitude * math.sin(multiplicador * t))
    amostras.append(amostra)
-- Valor máximo para um inteiro com sinal de um byte.
-- Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
local amplitude = 127
-- Em Hz (44.1 KHz: qualidade de CD)
local taxa_amostragem = 44100
local canais = 1
-- Duração desejada para o áudio, em segundos
local duracao = 1

-- Tamanho do vetor que armazenará as amostras para a onda.
local numero_amostras = duracao * taxa_amostragem * canais

-- Freqüência escolhida para o som, em Hertz.
local frequencia = 440

-- Parte constante da equação.
local multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

-- Geração de valores para o som.
local amostras = {}
for t = 0, numero_amostras - 1 do
    local amostra = math.floor(amplitude * math.sin(multiplicador * t))
    table.insert(amostras, amostra)
end
extends Node

func _ready():
    # Valor máximo para um inteiro com sinal de um byte.
    # Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
    var amplitude = 127
    # Em Hz (44.1 KHz: qualidade de CD)
    var taxa_amostragem = 44100
    var canais = 1
    # Duração desejada para o áudio, em segundos
    var duracao = 1

    # Tamanho do vetor que armazenará as amostras para a onda.
    var numero_amostras = duracao * taxa_amostragem * canais

    # Freqüência escolhida para o som, em Hertz.
    var frequencia = 440

    # Parte constante da equação.
    var multiplicador = 2.0 * PI * frequencia / taxa_amostragem

    # Geração de valores para o som.
    var amostras = []
    for t in range(numero_amostras):
        var amostra = floor(amplitude * sin(multiplicador * t))
        amostras.append(amostra)

Na implementação, pode-se personalizar os valores para frequencia, taxa_amostragem e duracao.

A implementação da equação é simples (ela está na linha que utiliza multiplicador). O restante do código prepara o vetor de amostras com a discretização de um segundo de amostras de valores para a onda senoidal. Para alterar a duração do som, pode-se modificar o valor de duracao, definida em segundos. Caso se use um valor real ao invés de um valor inteiro, pode ser necessário arredondar o resultado para cima ou para baixo.

Cada amostra é um valor inteiro com sinal ocupando um byte. Para transformar números reais em números inteiros, os resultados são arredondados para baixo. No código definido, como o som possui apenas um canal (mono) e possui duração de um segundo, o resultado de numero_amostras será 44100 amostras. Ou seja, o vetor definido armazenará 44100 valores inteiros. Para sons de maior qualidade, pode-se usar um número maior de bits ou bytes para representação da amostra. O valor pode ser um número inteiro ou real. Para simplicidade, usa-se um único byte como número inteiro para facilitar a criação do vetor de amostras.

O valor escolhido para frequencia determinará o som. Valores mais altos resultarão em sons mais agudos. Valores mais baixos resultarão em sons mais graves. É importante que o valor esteja entre a freqüência de sons audíveis por seres humanos (faixa de 20 Hz e 20 kHZ para a maioria das pessoas). O valor 440 Hz é o padrão usado pelo programa SoX, que será mencionado na próxima seção.

O IDE Thonny possui um recurso interessante para visualizar as amostras (ou qualquer seqüência de números). Na saída do console (Shell), é possível habilitar um plotter para exibir valores impressos na saída padrão como um gráfico. Para isso, clique em Visualizar (View), depois em Plotador (Plotter). Outra opção é clicar com o botão direito no console e escolher a mesma opção. Caso se imprima o valor de cada amostra no laço, é possível visualizar a aproximação gerada para a onda senoidal discretizada.

# ...
amostras = []
for t in range(numero_amostras):
    amostra = math.floor(amplitude * math.sin(multiplicador * t))
    print(amostra)    amostras.append(amostra)
O plotador fornecido por Thonny para a saída de programa permite visualizar saídas numéricas como um gráfico. No exemplo, a escrita das amostras resulta na visualização da onda senoidal discretizada.

Para os próximos programas, é uma boa idéia remover (ou comentar) a linha para impressão porque a escrita pode levar um tempo significativo (especialmente em JavaScript e Python).

Com as amostras prontas, o próximo passo é criar um arquivo binário em formato de áudio. Por outro lado, alguns programas serão necessários para ouvir os sons. Assim, convém instalá-los antes.

Edição e Manipulação de Áudio

Se você optara por usar as Ferramentas online, não é necessário instalar nenhum programa desta subseção.

Para processar som bruto (raw) e reproduzi-lo (ou convertê-lo para outro formato), será necessário usar um programa para edição de áudio (sound editor). Uma boa opção de código aberto chama-se Audacity. Audacity será o programa usado para a conversão do arquivo criado em formato .raw para .wav. Os formatos .raw e .wav serão abordados na próxima seção.

Para linha de comando, também é possível usar Sound eXchange (SoX). De certa forma, SoX é como um ImageMagick para áudio. No exemplo a seguir, -r é a taxa de amostragem, -c é o número de canais, -e o tipo de inteiro (com ou sem sinal), -b o número de bits. No exemplo, o arquivo entrada.raw é convertido para o arquivo saida.wav usando-se sox.

sox -r 44100 -c 1 -e signed -b 8 entrada.raw saida.wav

SoX também é capaz de gerar sons. O exemplo a seguir gera um arquivo equivalente ao que será gerado no exemplo para o formato Raw. O valor 440 Hz é a freqüência padrão usada por SoX; o parâmetro synth 1 especifica o tempo (um segundo de duração).

sox -n -r 44100 -c 1 -e signed -b 8 saida.raw synth 1 sine 440

Alternativamente, pode-se usar FFmpeg para conversões de áudio (e de vídeo) usando a linha de comando. O parâmetro -ar é a taxa de amostragem, -ac é o número de canais, -f define o formato, -i o arquivo de entrada. O último parâmetro é o arquivo de saída.

ffmpeg -ar 44100 -ac 1 -f s8 -i entrada.raw saida.wav

Para arquivos com cabeçalhos, FFmpeg é capaz de extrair os dados da mídia de entrada para simplificar o comando.

Para a reprodução de sons, é necessário um reprodutor de mídia. Você provavelmente possui um instalado. Caso precise de uma recomendação, VLC é um reprodutor de mídia de código aberto capaz de processar diversos formatos.

Tanto Audacity quando VLC podem ser instalados via Ninite, Chocolatey e Scoop.

SoX também permite a reprodução de sons em linha de comando, usando o comando play. Isso também é possível com FFmpeg (via ffplay) e VLC (via cvlc ou vlc). Para play e ffplay, os parâmetros são os mesmos usados para ffmpeg. Para cvlc ou vlc (interface gráfica), --demux especifica o decodificador, --rawaud-fourcc especifica o tipo (os dois espaços ao final de s8 são importantes, porque o código requer quatro caracteres), --rawaud-channels é o número de canais, e --rawaud-samplerate é a taxa de amostragem.

play -r 44100 -c 1 -e signed -b 8 entrada.raw
ffplay -autoexit -ar 44100 -ac 1 -f s8 entrada.raw
cvlc --demux=rawaud --rawaud-fourcc="s8  " --rawaud-channels 1 --rawaud-samplerate 44100 entrada.raw

O uso de um dos comandos anteriores evitaria a necessidade de conversão de formato para arquivos Raw. Em particular, também pode-se combinar comandos para criar o arquivo de som e reproduzi-lo na seqüência.

lua script.lua && play -r 44100 -c 1 -e signed -b 8 franco-garcia-som.raw

O exemplo anterior ilustra o cenário com Lua; alterando-se a primeira parte do comando lua script.lua, pode-se modificar o comando para outras linguagens de programação.

Pulse-Code Modulation (PCM) e o Formato Raw

Um dos modos mais simples de armazenar sons é na forma de valores brutos (raw values) para pulse-code modulation (PCM). Um arquivo criado assim tem um formato dito raw (raw audio file). A vantagem do formato raw é a simplicidade para criação, pois o arquivo consiste apenas de valores, sem cabeçalho. As desvantagens são dificuldade e inconveniência para o uso. Um reprodutor de mídia não possuirá dados para inferir como reproduzir as amostras armazenadas no arquivo, requerendo configuração manual para tocar o som.

Para um primeira atividade de programação com áudio, Raw é conveniente. O próximo programa cria um arquivo de som chamado franco-garcia-som.raw em formato raw.

function pack_int8(valor) {
    let resultado = new Uint8Array(1)
    let data_view = new DataView(resultado.buffer)
    data_view.setInt8(0, valor)

    return Array.from(resultado)
}

let caminho_arquivo = "franco-garcia-som.raw"

// Valor máximo para um inteiro com sinal de um byte.
// Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
let amplitude = 127
// Em Hz (44.1 KHz: qualidade de CD)
let taxa_amostragem = 44100
let canais = 1
// Duração desejada para o áudio, em segundos
let duracao = 1

// Tamanho do vetor que armazenará as amostras para a onda.
let numero_amostras = duracao * taxa_amostragem * canais

// Freqüência escolhida para o som, em Hertz.
let frequencia = 440

// Parte constante da equação.
let multiplicador = 2.0 * Math.PI * frequencia / taxa_amostragem

// Geração de valores para o som.
let amostras = []
for (let t = 0; t < numero_amostras; ++t) {
    let amostra = Math.floor(amplitude * Math.sin(multiplicador * t))
    amostras.push(amostra)
}

// let conteudo = []
// for (let t = 0; t < numero_amostras; ++t) {
//     let amostra = pack_int8(amostras[t])[0]
//     conteudo.push(amostra)
// }

let bytes = new Uint8Array(amostras)
// let bytes = new Uint8Array(conteudo)
let dados = new Blob([bytes], {type: "application/octet-stream"})
let arquivo = new File([dados], caminho_arquivo, {type: dados.type})
console.log("Som criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import math
import struct
import sys

caminho_arquivo = "franco-garcia-som.raw"

# Valor máximo para um inteiro com sinal de um byte.
# Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
amplitude = 127
# Em Hz (44.1 KHz: qualidade de CD)
taxa_amostragem = 44100
canais = 1
# Duração desejada para o áudio, em segundos
duracao = 1

# Tamanho do vetor que armazenará as amostras para a onda.
numero_amostras = duracao * taxa_amostragem * canais

# Freqüência escolhida para o som, em Hertz.
frequencia = 440

# Parte constante da equação.
multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

# Geração de valores para o som.
amostras = []
for t in range(numero_amostras):
    amostra = math.floor(amplitude * math.sin(multiplicador * t))
    amostras.append(amostra)

try:
    arquivo = open(caminho_arquivo, "wb")
    for t in range(numero_amostras):
        # Inteiro com sinal de 1 byte, em ordem LE.
        arquivo.write(struct.pack("<b", amostras[t]))

    arquivo.close()

    print("Som criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-som.raw"

-- Valor máximo para um inteiro com sinal de um byte.
-- Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
local amplitude = 127
-- Em Hz (44.1 KHz: qualidade de CD)
local taxa_amostragem = 44100
local canais = 1
-- Duração desejada para o áudio, em segundos
local duracao = 1

-- Tamanho do vetor que armazenará as amostras para a onda.
local numero_amostras = duracao * taxa_amostragem * canais

-- Freqüência escolhida para o som, em Hertz.
local frequencia = 440

-- Parte constante da equação.
local multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

-- Geração de valores para o som.
local amostras = {}
for t = 0, numero_amostras - 1 do
    local amostra = math.floor(amplitude * math.sin(multiplicador * t))
    table.insert(amostras, amostra)
end

local arquivo = io.open(caminho_arquivo, "wb")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo binário.")
    os.exit(EXIT_FAILURE)
end

for t = 1, numero_amostras do
    -- Inteiro com sinal de 1 byte, em ordem LE.
    arquivo:write(string.pack("<b", amostras[t]))
end

io.close(arquivo)

print("Som criado com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-som.raw"

    # Valor máximo para um inteiro com sinal de um byte.
    # Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
    var amplitude = 127
    # Em Hz (44.1 KHz: qualidade de CD)
    var taxa_amostragem = 44100
    var canais = 1
    # Duração desejada para o áudio, em segundos
    var duracao = 1

    # Tamanho do vetor que armazenará as amostras para a onda.
    var numero_amostras = duracao * taxa_amostragem * canais

    # Freqüência escolhida para o som, em Hertz.
    var frequencia = 440

    # Parte constante da equação.
    var multiplicador = 2.0 * PI * frequencia / taxa_amostragem

    # Geração de valores para o som.
    var amostras = []
    for t in range(numero_amostras):
        var amostra = floor(amplitude * sin(multiplicador * t))
        amostras.append(amostra)

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    # false para little-endian, true para big-endian.
    arquivo.endian_swap = false
    for t in range(numero_amostras):
        arquivo.store_8(amostras[t])

    arquivo.close()

    print("Som criado com sucesso.")

O arquivo é simples de criar: basta salvar os bytes armazenados no vetor de amostras amostras no arquivo binário.

Em JavaScript, a função pack_int8() é desnecessária (cada valor terá um byte, e não é necessário adaptar os valores para little-endian, pois existe apenas um byte); assim, o uso está comentado. De qualquer forma, ela existe como exemplo de uso de setInt8() (documentação) e de getInt8() (documentação). Como Lua, Python e GDScript permitem especificar a ordem de bytes, a implementação usa para garantir que seja a mesma ordem (litte-endian) independentemente de máquina. Novamente, como se trata de um único byte por amostra, o uso não é necessário; ele tem caráter ilustrativo. Contudo, caso se codificasse cada amostra com dois ou mais bytes, o uso tornar-se-ia importante.

O arquivo de som criado não pode ser reproduzido automaticamente por um reprodutor de mídia. Para ouvir o som resultante, você pode usar o Reprodutor de Áudio RAW criado pelo autor. Tipicamente, todavia, deve-se usar um programa como Audacity para importar os dados e exportá-los para outro formato. Para isso, após abrir o Audacity:

  1. Acesse Arquivo (File), depois Importar (Import), depois Áudio sem formatação (RAW) (Raw Data);
  2. Escolha o arquivo criado;
  3. Para a configuração, assumindo os valores definidos na implementação:
    • Codificação (Encoding): Signed 8-bit PCM;
    • Ordenação de bytes (Byte order): Extremidade pequena (Endianness) (Little-endian);
    • Canais (Channels): 1 canal (Mono) (1 Channel (Mono));
    • Compensação no início (Start offset): 0 bytes;
    • Quantidade a importar (Amount to import): 100%;
    • Taxa de amostragem (Sample rate): 44100 Hz.
  4. Confirme a importação.

O Audacity pode reproduzir o áudio carregado. Ele também pode convertê-lo para outro formato. Por exemplo:

  1. Acesse Arquivo (File), depois Exportar (Export), depois o formato de sua preferência:
    • .wav ou Waveform Audio File Format (WAVE ou WAV) é um formato simples para áudio, embora sem compressão. O tamanho do arquivo resultante pode ser grande;
    • .ogg ou Ogg Vorbis é um formato aberto e de qualidade para áudio com compressão. O formato é livre de royalties, inclusive para uso comercial;
    • .mp3 ou MP3 é um formato popular para áudio com compressão. Até alguns anos, era necessário pagar por uma licença para uso comercial do formato MP3. Contudo, a patente expirou recentemente;
    • Para outras opções, escolha Exportar Áudio Como... (Export Selected Audio...).
  2. Ajuste o volume de suas caixas de som (ou fones de ouvido) para um valor baixo e abra o arquivo de áudio convertido em seu reprodutor de mídia favorito. Além de VLC, outras opções de código aberto interessantes são MPV (para áudio e vídeo), Amarok e Elisa.

O som resultante de uma onda senoidal provavelmente não faria sucesso em uma canção, mas é útil para verificar se a implementação está correta. Embora ele possa ser classificado como um barulho aleatório para novos ouvintes destreinados, ele na realidade é limpo. Caso ele apresente distorções, é um indicativo que algo errado aconteceu. Para sons codificados com mais de 1 byte, um possível problema é a ordem de bytes. Por exemplo, talvez a máquina use ordem big-endian ao invés de little-endian. Nesse caso, seria necessário alterar a configuração para a importação.

Para se ouvir o que realmente é ruído para um som, pode-se gerar amostras com valores aleatórios. A reprodução provavelmente não será agradável para se ouvir.

Waveform Audio File Format (WAVE ou WAV)

O formato Raw é conveniente para salvar amostras, mas inconveniente para usar. Como não há um cabeçalho, deve-se registrar informações sobre codificação, ordem de bits, canais e taxa de amostragem separadamente. Também é necessário fornecer todos os dados anteriores para converter ou reproduzir o conteúdo de áudio armazenado.

Após aprender discretizar uma onda e salvar os valores, é interessante usar um formato com cabeçalho para maior conveniência de uso após a criação. O formato WAVE é, grosso modo, o formato RAW com um cabeçalho.

Um entrave para a escrita de um arquivo no formato WAVE é a falta de uma especificação oficial. A documentação da Microsoft fornece a descrição para o formato Resource Interchange File Format (RIFF), mas não para o WAVE como um todo. A implementação do cabeçalho segue a especificação criada pela BlackBerry. A especificação foi movida desde o acesso, então o link provê uma cópia arquivada a qual eu tinha criado.

Para criar o cabeçalho, basta seguir a especificação. A escrita de valores em formato binário é mais fácil em linguagens de nível mais baixo (como C e C++) que nas linguagens consideradas. É importante atentar ao tamanho em bytes de cada campo, para a escolha do tipo de dados adequado para armazenamento no arquivo binário. O tamanho em bytes do tipo inteiro escolhido deve corresponder ao esperado.

A implementação abaixo demonstra a criação do cabeçalho em Python.

bits_por_amostra = 8
bits_por_byte = 8
# Chunk size in bytes.
tamanho = numero_amostras * canais * bits_por_amostra // bits_por_byte
# Taxa de bits
bit_rate = bits_por_amostra * taxa_amostragem * canais
byte_rate = bit_rate // bits_por_byte
# Block align
alinhamento = canais * bits_por_amostra

arquivo = open(caminho_arquivo, "wb")

# RIFF
arquivo.write(struct.pack("<b", ord("R")))
arquivo.write(struct.pack("<b", ord("I")))
arquivo.write(struct.pack("<b", ord("F")))
arquivo.write(struct.pack("<b", ord("F")))
# I: unsigned int
arquivo.write(struct.pack("<I", 36 + tamanho))
arquivo.write(struct.pack("<b", ord("W")))
arquivo.write(struct.pack("<b", ord("A")))
arquivo.write(struct.pack("<b", ord("V")))
arquivo.write(struct.pack("<b", ord("E")))

# Format subchunk
arquivo.write(struct.pack("<b", ord("f")))
arquivo.write(struct.pack("<b", ord("m")))
arquivo.write(struct.pack("<b", ord("t")))
arquivo.write(struct.pack("<b", ord(" ")))
# 16: PCM
arquivo.write(struct.pack("<I", 16))
# H: unsigned short
# 1: PCM
arquivo.write(struct.pack("<H", 1))
arquivo.write(struct.pack("<H", canais))
arquivo.write(struct.pack("<I", taxa_amostragem))
arquivo.write(struct.pack("<I", byte_rate))
arquivo.write(struct.pack("<H", alinhamento))
arquivo.write(struct.pack("<H", bits_por_amostra))

# Data subchunk
arquivo.write(struct.pack("<b", ord("d")))
arquivo.write(struct.pack("<b", ord("a")))
arquivo.write(struct.pack("<b", ord("t")))
arquivo.write(struct.pack("<b", ord("a")))
arquivo.write(struct.pack("<I", tamanho))
# Amostras...
for t in range(numero_amostras):
    # Inteiro com sinal de 1 byte, em ordem LE.
    arquivo.write(struct.pack("<b", amostras[t]))

arquivo.close()

Para testar o cabeçalho, o FFmpeg é uma boa opção. O comando a seguir exporta dados para o arquivo metadados.txt; mais interessante que o arquivo, contudo, é a saída do programa no terminal, que pode indicar erros no cabeçalho.

ffmpeg -i franco-garcia-som.wav -f ffmetadata metadados.txt

O programa a seguir utiliza a definição de cabeçalho anterior para criar um arquivo franco-garcia-som.wav em formato wav.

function pack_int8(valor) {
    let resultado = new Uint8Array(1)
    let data_view = new DataView(resultado.buffer)
    data_view.setInt8(0, valor)

    return Array.from(resultado)
}

function pack_uint8(valor) {
    let resultado = new Uint8Array(1)
    let data_view = new DataView(resultado.buffer)
    data_view.setUint8(0, valor)

    return Array.from(resultado)
}

function pack_uint16(valor) {
    let resultado = new Uint16Array(1)
    let data_view = new DataView(resultado.buffer)
    // true para little-endian, false para big-endian.
    data_view.setUint16(0, valor, true)

    return Array.from(new Uint8Array(resultado.buffer))
}

function pack_uint32(valor) {
    let resultado = new Uint32Array(1)
    let data_view = new DataView(resultado.buffer)
    // true para little-endian, false para big-endian.
    data_view.setUint32(0, valor, true)

    return Array.from(new Uint8Array(resultado.buffer))
}

function adicione_vetor_bytes(destino, vetor_bytes) {
    for (let valor of vetor_bytes) {
        destino.push(valor)
    }
}

let caminho_arquivo = "franco-garcia-som.wav"

// Valor máximo para um inteiro com sinal de um byte.
// Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
let amplitude = 127
// Em Hz (44.1 KHz: qualidade de CD)
let taxa_amostragem = 44100
let canais = 1
// Duração desejada para o áudio, em segundos
let duracao = 1

// Tamanho do vetor que armazenará as amostras para a onda.
let numero_amostras = duracao * taxa_amostragem * canais

// Freqüência escolhida para o som, em Hertz.
let frequencia = 440

// Parte constante da equação.
let multiplicador = 2.0 * Math.PI * frequencia / taxa_amostragem

// Geração de valores para o som.
let amostras = []
for (let t = 0; t < numero_amostras; ++t) {
    let amostra = Math.floor(amplitude * Math.sin(multiplicador * t))
    amostras.push(amostra)
}

let bits_por_amostra = 8
let bits_por_byte = 8
// Chunk size in bytes.
let tamanho = Math.floor(numero_amostras * canais * bits_por_amostra / bits_por_byte)
// Taxa de bits
let bit_rate = bits_por_amostra * taxa_amostragem * canais
let byte_rate = Math.floor(bit_rate / bits_por_byte)
// Block align
let alinhamento = canais * bits_por_amostra

let conteudo = []
// RIFF
adicione_vetor_bytes(conteudo, pack_uint8("R".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("I".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("F".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("F".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint32(36 + tamanho))
adicione_vetor_bytes(conteudo, pack_uint8("W".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("A".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("V".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("E".charCodeAt(0)))

// Format subchunk
adicione_vetor_bytes(conteudo, pack_uint8("f".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("m".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("t".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8(" ".charCodeAt(0)))
// 16: PCM
adicione_vetor_bytes(conteudo, pack_uint32(16))
// 1: PCM
adicione_vetor_bytes(conteudo, pack_uint16(1))
adicione_vetor_bytes(conteudo, pack_uint16(canais))
adicione_vetor_bytes(conteudo, pack_uint32(taxa_amostragem))
adicione_vetor_bytes(conteudo, pack_uint32(byte_rate))
adicione_vetor_bytes(conteudo, pack_uint16(alinhamento))
adicione_vetor_bytes(conteudo, pack_uint16(bits_por_amostra))

// Data subchunk
adicione_vetor_bytes(conteudo, pack_uint8("d".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("a".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("t".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("a".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint32(tamanho))
for (let t = 0; t < numero_amostras; ++t) {
    // let amostra = amostras[t]
    // conteudo.push(amostra)
    adicione_vetor_bytes(conteudo, pack_int8(amostras[t]))
}

let bytes = new Uint8Array(conteudo)
let dados = new Blob([bytes], {type: "application/octet-stream"})
let arquivo = new File([dados], caminho_arquivo, {type: dados.type})
console.log("Som criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import math
import struct
import sys

caminho_arquivo = "franco-garcia-som.wav"

# Valor máximo para um inteiro com sinal de um byte.
# Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
amplitude = 127
# Em Hz (44.1 KHz: qualidade de CD)
taxa_amostragem = 44100
canais = 1
# Duração desejada para o áudio, em segundos
duracao = 1

# Tamanho do vetor que armazenará as amostras para a onda.
numero_amostras = duracao * taxa_amostragem * canais

# Freqüência escolhida para o som, em Hertz.
frequencia = 440

# Parte constante da equação.
multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

# Geração de valores para o som.
amostras = []
for t in range(numero_amostras):
    amostra = math.floor(amplitude * math.sin(multiplicador * t))
    amostras.append(amostra)

bits_por_amostra = 8
bits_por_byte = 8
# Chunk size in bytes.
tamanho = numero_amostras * canais * bits_por_amostra // bits_por_byte
# Taxa de bits
bit_rate = bits_por_amostra * taxa_amostragem * canais
byte_rate = bit_rate // bits_por_byte
# Block align
alinhamento = canais * bits_por_amostra

try:
    arquivo = open(caminho_arquivo, "wb")

    # RIFF
    arquivo.write(struct.pack("<b", ord("R")))
    arquivo.write(struct.pack("<b", ord("I")))
    arquivo.write(struct.pack("<b", ord("F")))
    arquivo.write(struct.pack("<b", ord("F")))
    # I: unsigned int
    arquivo.write(struct.pack("<I", 36 + tamanho))
    arquivo.write(struct.pack("<b", ord("W")))
    arquivo.write(struct.pack("<b", ord("A")))
    arquivo.write(struct.pack("<b", ord("V")))
    arquivo.write(struct.pack("<b", ord("E")))

    # Format subchunk
    arquivo.write(struct.pack("<b", ord("f")))
    arquivo.write(struct.pack("<b", ord("m")))
    arquivo.write(struct.pack("<b", ord("t")))
    arquivo.write(struct.pack("<b", ord(" ")))
    # 16: PCM
    arquivo.write(struct.pack("<I", 16))
    # H: unsigned short
    # 1: PCM
    arquivo.write(struct.pack("<H", 1))
    arquivo.write(struct.pack("<H", canais))
    arquivo.write(struct.pack("<I", taxa_amostragem))
    arquivo.write(struct.pack("<I", byte_rate))
    arquivo.write(struct.pack("<H", alinhamento))
    arquivo.write(struct.pack("<H", bits_por_amostra))

    # Data subchunk
    arquivo.write(struct.pack("<b", ord("d")))
    arquivo.write(struct.pack("<b", ord("a")))
    arquivo.write(struct.pack("<b", ord("t")))
    arquivo.write(struct.pack("<b", ord("a")))
    arquivo.write(struct.pack("<I", tamanho))
    for t in range(numero_amostras):
        # Inteiro com sinal de 1 byte, em ordem LE.
        arquivo.write(struct.pack("<b", amostras[t]))

    arquivo.close()

    print("Som criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-som.wav"

-- Valor máximo para um inteiro com sinal de um byte.
-- Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
local amplitude = 127
-- Em Hz (44.1 KHz: qualidade de CD)
local taxa_amostragem = 44100
local canais = 1
-- Duração desejada para o áudio, em segundos
local duracao = 1

-- Tamanho do vetor que armazenará as amostras para a onda.
local numero_amostras = duracao * taxa_amostragem * canais

-- Freqüência escolhida para o som, em Hertz.
local frequencia = 440

-- Parte constante da equação.
local multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

-- Geração de valores para o som.
local amostras = {}
for t = 0, numero_amostras - 1 do
    local amostra = math.floor(amplitude * math.sin(multiplicador * t))
    table.insert(amostras, amostra)
end

local bits_por_amostra = 8
local bits_por_byte = 8
-- Chunk size in bytes.
local tamanho = numero_amostras * canais * bits_por_amostra // bits_por_byte
-- Taxa de bits
local bit_rate = bits_por_amostra * taxa_amostragem * canais
local byte_rate = bit_rate // bits_por_byte
-- Block align
local alinhamento = canais * bits_por_amostra

local arquivo = io.open(caminho_arquivo, "wb")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo binário.")
    os.exit(EXIT_FAILURE)
end

-- RIFF
arquivo:write(string.pack("<b", string.byte("R")))
arquivo:write(string.pack("<b", string.byte("I")))
arquivo:write(string.pack("<b", string.byte("F")))
arquivo:write(string.pack("<b", string.byte("F")))
-- I: unsigned int
arquivo:write(string.pack("<I", 36 + tamanho))
arquivo:write(string.pack("<b", string.byte("W")))
arquivo:write(string.pack("<b", string.byte("A")))
arquivo:write(string.pack("<b", string.byte("V")))
arquivo:write(string.pack("<b", string.byte("E")))

-- Format subchunk
arquivo:write(string.pack("<b", string.byte("f")))
arquivo:write(string.pack("<b", string.byte("m")))
arquivo:write(string.pack("<b", string.byte("t")))
arquivo:write(string.pack("<b", string.byte(" ")))
-- 16: PCM
arquivo:write(string.pack("<I", 16))
-- H: unsigned short
-- 1: PCM
arquivo:write(string.pack("<H", 1))
arquivo:write(string.pack("<H", canais))
arquivo:write(string.pack("<I", taxa_amostragem))
arquivo:write(string.pack("<I", byte_rate))
arquivo:write(string.pack("<H", alinhamento))
arquivo:write(string.pack("<H", bits_por_amostra))

-- Data subchunk
arquivo:write(string.pack("<b", string.byte("d")))
arquivo:write(string.pack("<b", string.byte("a")))
arquivo:write(string.pack("<b", string.byte("t")))
arquivo:write(string.pack("<b", string.byte("a")))
arquivo:write(string.pack("<I", tamanho))
for t = 1, numero_amostras do
    -- Inteiro com sinal de 1 byte, em ordem LE.
    arquivo:write(string.pack("<b", amostras[t]))
end

io.close(arquivo)

print("Som criado com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-som.wav"

    # Valor máximo para um inteiro com sinal de um byte.
    # Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
    var amplitude = 127
    # Em Hz (44.1 KHz: qualidade de CD)
    var taxa_amostragem = 44100
    var canais = 1
    # Duração desejada para o áudio, em segundos
    var duracao = 1

    # Tamanho do vetor que armazenará as amostras para a onda.
    var numero_amostras = duracao * taxa_amostragem * canais

    # Freqüência escolhida para o som, em Hertz.
    var frequencia = 440

    # Parte constante da equação.
    var multiplicador = 2.0 * PI * frequencia / taxa_amostragem

    # Geração de valores para o som.
    var amostras = []
    for t in range(numero_amostras):
        var amostra = floor(amplitude * sin(multiplicador * t))
        amostras.append(amostra)

    var bits_por_amostra = 8
    var bits_por_byte = 8
    # Chunk size in bytes.
    var tamanho = numero_amostras * canais * bits_por_amostra / bits_por_byte
    # Taxa de bits
    var bit_rate = bits_por_amostra * taxa_amostragem * canais
    var byte_rate = bit_rate / bits_por_byte
    # Block align
    var alinhamento = canais * bits_por_amostra

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    # false para little-endian, true para big-endian.
    arquivo.endian_swap = false

    # RIFF
    arquivo.store_8("R".to_ascii()[0])
    arquivo.store_8("I".to_ascii()[0])
    arquivo.store_8("F".to_ascii()[0])
    arquivo.store_8("F".to_ascii()[0])
    arquivo.store_32(36 + tamanho)
    arquivo.store_8("W".to_ascii()[0])
    arquivo.store_8("A".to_ascii()[0])
    arquivo.store_8("V".to_ascii()[0])
    arquivo.store_8("E".to_ascii()[0])

    # Format subchunk
    arquivo.store_8("f".to_ascii()[0])
    arquivo.store_8("m".to_ascii()[0])
    arquivo.store_8("t".to_ascii()[0])
    arquivo.store_8(" ".to_ascii()[0])
    # 16: PCM
    arquivo.store_32(16)
    # 1: PCM
    arquivo.store_16(1)
    arquivo.store_16(canais)
    arquivo.store_32(taxa_amostragem)
    arquivo.store_32(byte_rate)
    arquivo.store_16(alinhamento)
    arquivo.store_16(bits_por_amostra)

    # Data subchunk
    arquivo.store_8("d".to_ascii()[0])
    arquivo.store_8("a".to_ascii()[0])
    arquivo.store_8("t".to_ascii()[0])
    arquivo.store_8("a".to_ascii()[0])
    arquivo.store_16(tamanho)

    for t in range(numero_amostras):
        arquivo.store_8(amostras[t])

    arquivo.close()

    print("Som criado com sucesso.")

Além do cabeçalho, as implementações usam um novo recurso: uma subrotina para converter um caractere para o valor inteiro correspondente para codificação em ASCII. Em JavaScript, isso pode ser feito usando charCodeAt() (documentação). Embora o método forneça valores para codificação UTF-16, o resultado será equivalente para caracteres pertencentes ao idioma Inglês. Em Python, pode-se usar ord() (documentação) para a obtenção do valor. Em Lua, string.byte() (documentação) fornece o valor. Em GDScript, to_ascii() (documentação) fornece um vetor com o resultado para todos os caracteres de uma cadeia de caracteres. Assim, poder-se-ia criar um laço para converter toda a palavra de uma vez. Para deixar o código similar às outras implementações, fez-se uma chamada para cada caractere e acessou-se o primeiro valor.

Como os dados da amostra em formato WAVE devem estar em ordem little-endian, a ordem é imposta em todas as linguagens. Em JavaScript, as funções criadas para geração de vetores de bytes são similares às fornecidas por Python e Lua. As funções pack_uint16() e pack_uint32() foram criadas para esse propósito. As funções pack_int8() e pack_uint8() são, tecnicamente, desnecessárias, caso todos os valores armazenados tenham valor máximo 255 (sem sinal) ou 127 (com sinal). Elas usam Int8Array (documentação) para inteiros com sinal de 1 byte (8 bits), Uint16Array (documentação) para inteiros sem sinal de 2 bytes (16 bits) e Uint32Array (documentação) para inteiros sem sinal de 4 bytes (32 bits). O método usado para adicionar um valor inteiro é similar aos apresentados antes. Para inteiros com 2 ou mais bytes, usa-se true no método set() para forçar o uso de ordem little-endian.

Como WAVE é um formato de áudio comum, praticamente todo reprodutor de mídia deve ser capaz de tocar o som criado sem a necessidade de configurações adicionais. Assim, é possível usar um player para reproduzir o som criado (como o fornecido por padrão pelo sistema operacional ou o seu favorito).

Um Problema Sutil

O arquivo resultante é válido, mas existe um porém. Ouvindo o som com atenção, pode-se constatar uma diferença: o som estará diferente do original, porque arquivos WAVE assumem que valores com 8 bits (ou menos) sejam sempre sem sinal. Contudo, os valores da amostra original possuíam sinal, o que resulta em erro na conversão de formato.

Para uma possível solução, pode-se modificar os valores sem sinal para cada amostra. Em complemento de dois para um byte:

  • Inteiros positivos com sinal tem valores entre 0 a 127;
  • Inteiros negativos com sinal tem valores entre -128 a -1.

Quando interpretados como inteiros sem sinal:

  • Inteiros positivos com sinal continuam valores entre 0 a 127 sem sinal;
  • Inteiros negativos com sinal são interpretados como 128 (-128) a 255 (-1) sem sinal.

Uma forma de interpretar corretamente os valores como números crescentes em uma escala de 0 a 255 consiste em colocar os valores positivos após os negativos. Em outras palavras:

  • Se um valor for zero ou positivo, deve-se somar 128 ao resultado;
  • Se o valor for negativo, deve-se subtrair 128 do resultado.

Assim, os valores estariam representados como:

  • -128 a -1 nos valores 0 a 127;
  • 0 a 127 nos valores 128 a 255.

Para uma solução mais genérica, pode calcular o valor 128 em função de bits_por_amostra. Para isso, pode-se fazer algo como valor_maximo_com_sinal = pow(2, bits_por_amostra - 1). Em seguida, no momento de salvar as amostras, adotando Python para o exemplo:

amostras_com_sinal = True
# ...
valor_maximo_com_sinal = 2 ** (bits_por_amostra - 1)
for t in range(numero_amostras):
    amostra, = struct.unpack("<B", struct.pack("<b", amostras[t]))
    if (amostras_com_sinal):
        if (amostra >= valor_maximo_com_sinal):
            amostra -= valor_maximo_com_sinal
        else:
            amostra += valor_maximo_com_sinal

    arquivo.write(struct.pack("<B", amostra))

Como os valores de amostras estão entre -128 e 127 na ipmlementação original, também é possível evitar as conversões. Para isso, basta incrementar o valor da amostra a ser salva no arquivo por valor_maximo_com_sinal e salvar o valor como um inteiro sem sinal de um byte.

amostras_com_sinal = True
# ...
valor_maximo_com_sinal = 2 ** (bits_por_amostra - 1)
for t in range(numero_amostras):
    amostra = amostras[t]
    if (amostras_com_sinal):
        amostra += valor_maximo_com_sinal

    arquivo.write(struct.pack("<B", amostra))

Com a correção, a conversão de RAW para WAVE estará correta para números inteiros de 1 byte com sinal. A inclusão da flag amostras_com_sinal permite alternar entre salvar valores com ou sem sinal no arquivo WAVE.

function pack_int8(valor) {
    let resultado = new Uint8Array(1)
    let data_view = new DataView(resultado.buffer)
    data_view.setInt8(0, valor)

    return Array.from(resultado)
}

function pack_uint8(valor) {
    let resultado = new Uint8Array(1)
    let data_view = new DataView(resultado.buffer)
    data_view.setUint8(0, valor)

    return Array.from(resultado)
}

function pack_uint16(valor) {
    let resultado = new Uint16Array(1)
    let data_view = new DataView(resultado.buffer)
    // true para little-endian, false para big-endian.
    data_view.setUint16(0, valor, true)

    return Array.from(new Uint8Array(resultado.buffer))
}

function pack_uint32(valor) {
    let resultado = new Uint32Array(1)
    let data_view = new DataView(resultado.buffer)
    // true para little-endian, false para big-endian.
    data_view.setUint32(0, valor, true)

    return Array.from(new Uint8Array(resultado.buffer))
}

function adicione_vetor_bytes(destino, vetor_bytes) {
    for (let valor of vetor_bytes) {
        destino.push(valor)
    }
}

let caminho_arquivo = "franco-garcia-som.wav"

// Valor máximo para um inteiro com sinal de um byte.
// Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
let amplitude = 127
// Em Hz (44.1 KHz: qualidade de CD)
let taxa_amostragem = 44100
let canais = 1
// Duração desejada para o áudio, em segundos
let duracao = 1
let amostras_com_sinal = true

// Tamanho do vetor que armazenará as amostras para a onda.
let numero_amostras = duracao * taxa_amostragem * canais

// Freqüência escolhida para o som, em Hertz.
let frequencia = 440

// Parte constante da equação.
let multiplicador = 2.0 * Math.PI * frequencia / taxa_amostragem

// Geração de valores para o som.
let amostras = []
for (let t = 0; t < numero_amostras; ++t) {
    let amostra = Math.floor(amplitude * Math.sin(multiplicador * t))
    amostras.push(amostra)
}

let bits_por_amostra = 8
let bits_por_byte = 8
// Chunk size in bytes.
let tamanho = Math.floor(numero_amostras * canais * bits_por_amostra / bits_por_byte)
// Taxa de bits
let bit_rate = bits_por_amostra * taxa_amostragem * canais
let byte_rate = Math.floor(bit_rate / bits_por_byte)
// Block align
let alinhamento = canais * bits_por_amostra

let conteudo = []
// RIFF
adicione_vetor_bytes(conteudo, pack_uint8("R".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("I".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("F".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("F".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint32(36 + tamanho))
adicione_vetor_bytes(conteudo, pack_uint8("W".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("A".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("V".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("E".charCodeAt(0)))

// Format subchunk
adicione_vetor_bytes(conteudo, pack_uint8("f".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("m".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("t".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8(" ".charCodeAt(0)))
// 16: PCM
adicione_vetor_bytes(conteudo, pack_uint32(16))
// 1: PCM
adicione_vetor_bytes(conteudo, pack_uint16(1))
adicione_vetor_bytes(conteudo, pack_uint16(canais))
adicione_vetor_bytes(conteudo, pack_uint32(taxa_amostragem))
adicione_vetor_bytes(conteudo, pack_uint32(byte_rate))
adicione_vetor_bytes(conteudo, pack_uint16(alinhamento))
adicione_vetor_bytes(conteudo, pack_uint16(bits_por_amostra))

// Data subchunk
adicione_vetor_bytes(conteudo, pack_uint8("d".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("a".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("t".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint8("a".charCodeAt(0)))
adicione_vetor_bytes(conteudo, pack_uint32(tamanho))
let valor_maximo_com_sinal = Math.pow(2, bits_por_amostra - 1)
for (let t = 0; t < numero_amostras; ++t) {
    let amostra = amostras[t]
    if (amostras_com_sinal) {
        amostra += valor_maximo_com_sinal
    }

    adicione_vetor_bytes(conteudo, pack_uint8(amostra))
}

let bytes = new Uint8Array(conteudo)
let dados = new Blob([bytes], {type: "application/octet-stream"})
let arquivo = new File([dados], caminho_arquivo, {type: dados.type})
console.log("Som criado com sucesso.")

let link_download = document.createElement("a")
link_download.target = "_blank"
link_download.href = URL.createObjectURL(arquivo)
link_download.download = arquivo.name
if (confirm("Fazer download do arquivo '" + arquivo.name + "'?")) {
    link_download.click()
    // Neste caso, revokeObjectURL() poderia ser usado tanto para confirmação,
    // quanto para cancelamento.
    URL.revokeObjectURL(link_download.href)
}
import io
import math
import struct
import sys

caminho_arquivo = "franco-garcia-som.wav"

# Valor máximo para um inteiro com sinal de um byte.
# Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
amplitude = 127
# Em Hz (44.1 KHz: qualidade de CD)
taxa_amostragem = 44100
canais = 1
# Duração desejada para o áudio, em segundos
duracao = 1
amostras_com_sinal = True

# Tamanho do vetor que armazenará as amostras para a onda.
numero_amostras = duracao * taxa_amostragem * canais

# Freqüência escolhida para o som, em Hertz.
frequencia = 440

# Parte constante da equação.
multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

# Geração de valores para o som.
amostras = []
for t in range(numero_amostras):
    amostra = math.floor(amplitude * math.sin(multiplicador * t))
    amostras.append(amostra)

bits_por_amostra = 8
bits_por_byte = 8
# Chunk size in bytes.
tamanho = numero_amostras * canais * bits_por_amostra // bits_por_byte
# Taxa de bits
bit_rate = bits_por_amostra * taxa_amostragem * canais
byte_rate = bit_rate // bits_por_byte
# Block align
alinhamento = canais * bits_por_amostra

try:
    arquivo = open(caminho_arquivo, "wb")

    # RIFF
    arquivo.write(struct.pack("<b", ord("R")))
    arquivo.write(struct.pack("<b", ord("I")))
    arquivo.write(struct.pack("<b", ord("F")))
    arquivo.write(struct.pack("<b", ord("F")))
    # I: unsigned int
    arquivo.write(struct.pack("<I", 36 + tamanho))
    arquivo.write(struct.pack("<b", ord("W")))
    arquivo.write(struct.pack("<b", ord("A")))
    arquivo.write(struct.pack("<b", ord("V")))
    arquivo.write(struct.pack("<b", ord("E")))

    # Format subchunk
    arquivo.write(struct.pack("<b", ord("f")))
    arquivo.write(struct.pack("<b", ord("m")))
    arquivo.write(struct.pack("<b", ord("t")))
    arquivo.write(struct.pack("<b", ord(" ")))
    # 16: PCM
    arquivo.write(struct.pack("<I", 16))
    # H: unsigned short
    # 1: PCM
    arquivo.write(struct.pack("<H", 1))
    arquivo.write(struct.pack("<H", canais))
    arquivo.write(struct.pack("<I", taxa_amostragem))
    arquivo.write(struct.pack("<I", byte_rate))
    arquivo.write(struct.pack("<H", alinhamento))
    arquivo.write(struct.pack("<H", bits_por_amostra))

    # Data subchunk
    arquivo.write(struct.pack("<b", ord("d")))
    arquivo.write(struct.pack("<b", ord("a")))
    arquivo.write(struct.pack("<b", ord("t")))
    arquivo.write(struct.pack("<b", ord("a")))
    arquivo.write(struct.pack("<I", tamanho))
    valor_maximo_com_sinal = 2 ** (bits_por_amostra - 1)
    for t in range(numero_amostras):
        amostra = amostras[t]
        if (amostras_com_sinal):
            amostra += valor_maximo_com_sinal

        arquivo.write(struct.pack("<B", amostra))

    arquivo.close()

    print("Som criado com sucesso.")
except IOError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
except OSError as excecao:
    print("Erro ao tentar criar arquivo binário.", file=sys.stderr)
    print(excecao)
local EXIT_FAILURE = 1

local caminho_arquivo = "franco-garcia-som.wav"

-- Valor máximo para um inteiro com sinal de um byte.
-- Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
local amplitude = 127
-- Em Hz (44.1 KHz: qualidade de CD)
local taxa_amostragem = 44100
local canais = 1
-- Duração desejada para o áudio, em segundos
local duracao = 1
local amostras_com_sinal = true

-- Tamanho do vetor que armazenará as amostras para a onda.
local numero_amostras = duracao * taxa_amostragem * canais

-- Freqüência escolhida para o som, em Hertz.
local frequencia = 440

-- Parte constante da equação.
local multiplicador = 2.0 * math.pi * frequencia / taxa_amostragem

-- Geração de valores para o som.
local amostras = {}
for t = 0, numero_amostras - 1 do
    local amostra = math.floor(amplitude * math.sin(multiplicador * t))
    table.insert(amostras, amostra)
end

local bits_por_amostra = 8
local bits_por_byte = 8
-- Chunk size in bytes.
local tamanho = numero_amostras * canais * bits_por_amostra // bits_por_byte
-- Taxa de bits
local bit_rate = bits_por_amostra * taxa_amostragem * canais
local byte_rate = bit_rate // bits_por_byte
-- Block align
local alinhamento = canais * bits_por_amostra

local arquivo = io.open(caminho_arquivo, "wb")
if (arquivo == nil) then
    print(debug.traceback())

    error("Erro ao tentar criar arquivo binário.")
    os.exit(EXIT_FAILURE)
end

-- RIFF
arquivo:write(string.pack("<b", string.byte("R")))
arquivo:write(string.pack("<b", string.byte("I")))
arquivo:write(string.pack("<b", string.byte("F")))
arquivo:write(string.pack("<b", string.byte("F")))
-- I: unsigned int
arquivo:write(string.pack("<I", 36 + tamanho))
arquivo:write(string.pack("<b", string.byte("W")))
arquivo:write(string.pack("<b", string.byte("A")))
arquivo:write(string.pack("<b", string.byte("V")))
arquivo:write(string.pack("<b", string.byte("E")))

-- Format subchunk
arquivo:write(string.pack("<b", string.byte("f")))
arquivo:write(string.pack("<b", string.byte("m")))
arquivo:write(string.pack("<b", string.byte("t")))
arquivo:write(string.pack("<b", string.byte(" ")))
-- 16: PCM
arquivo:write(string.pack("<I", 16))
-- H: unsigned short
-- 1: PCM
arquivo:write(string.pack("<H", 1))
arquivo:write(string.pack("<H", canais))
arquivo:write(string.pack("<I", taxa_amostragem))
arquivo:write(string.pack("<I", byte_rate))
arquivo:write(string.pack("<H", alinhamento))
arquivo:write(string.pack("<H", bits_por_amostra))

-- Data subchunk
arquivo:write(string.pack("<b", string.byte("d")))
arquivo:write(string.pack("<b", string.byte("a")))
arquivo:write(string.pack("<b", string.byte("t")))
arquivo:write(string.pack("<b", string.byte("a")))
arquivo:write(string.pack("<I", tamanho))
local valor_maximo_com_sinal = 2 ^ (bits_por_amostra - 1)
for t = 1, numero_amostras do
    local amostra = amostras[t]
    if (amostras_com_sinal) then
        amostra = amostra + valor_maximo_com_sinal
    end

    arquivo:write(string.pack("<B", amostra))
end

io.close(arquivo)

print("Som criado com sucesso.")
extends Node

const EXIT_FAILURE = 1

func _ready():
    var caminho_arquivo = "franco-garcia-som.wav"

    # Valor máximo para um inteiro com sinal de um byte.
    # Assumindo 1 byte (8 bits), 2 ^ (8 - 1) - 1 == 127.
    var amplitude = 127
    # Em Hz (44.1 KHz: qualidade de CD)
    var taxa_amostragem = 44100
    var canais = 1
    # Duração desejada para o áudio, em segundos
    var duracao = 1
    var amostras_com_sinal = true

    # Tamanho do vetor que armazenará as amostras para a onda.
    var numero_amostras = duracao * taxa_amostragem * canais

    # Freqüência escolhida para o som, em Hertz.
    var frequencia = 440

    # Parte constante da equação.
    var multiplicador = 2.0 * PI * frequencia / taxa_amostragem

    # Geração de valores para o som.
    var amostras = []
    for t in range(numero_amostras):
        var amostra = floor(amplitude * sin(multiplicador * t))
        amostras.append(amostra)

    var bits_por_amostra = 8
    var bits_por_byte = 8
    # Chunk size in bytes.
    var tamanho = numero_amostras * canais * bits_por_amostra / bits_por_byte
    # Taxa de bits
    var bit_rate = bits_por_amostra * taxa_amostragem * canais
    var byte_rate = bit_rate / bits_por_byte
    # Block align
    var alinhamento = canais * bits_por_amostra

    var arquivo = File.new()
    if (arquivo.open(caminho_arquivo, File.WRITE) != OK):
        printerr("Erro ao tentar criar arquivo binário.")
        get_tree().quit(EXIT_FAILURE)

    # false para little-endian, true para big-endian.
    arquivo.endian_swap = false

    # RIFF
    arquivo.store_8("R".to_ascii()[0])
    arquivo.store_8("I".to_ascii()[0])
    arquivo.store_8("F".to_ascii()[0])
    arquivo.store_8("F".to_ascii()[0])
    arquivo.store_32(36 + tamanho)
    arquivo.store_8("W".to_ascii()[0])
    arquivo.store_8("A".to_ascii()[0])
    arquivo.store_8("V".to_ascii()[0])
    arquivo.store_8("E".to_ascii()[0])

    # Format subchunk
    arquivo.store_8("f".to_ascii()[0])
    arquivo.store_8("m".to_ascii()[0])
    arquivo.store_8("t".to_ascii()[0])
    arquivo.store_8(" ".to_ascii()[0])
    # 16: PCM
    arquivo.store_32(16)
    # 1: PCM
    arquivo.store_16(1)
    arquivo.store_16(canais)
    arquivo.store_32(taxa_amostragem)
    arquivo.store_32(byte_rate)
    arquivo.store_16(alinhamento)
    arquivo.store_16(bits_por_amostra)

    # Data subchunk
    arquivo.store_8("d".to_ascii()[0])
    arquivo.store_8("a".to_ascii()[0])
    arquivo.store_8("t".to_ascii()[0])
    arquivo.store_8("a".to_ascii()[0])
    arquivo.store_16(tamanho)

    var valor_maximo_com_sinal = pow(2, bits_por_amostra - 1)
    for t in range(numero_amostras):
        var amostra = amostras[t]
        if (amostras_com_sinal):
            amostra += valor_maximo_com_sinal

        arquivo.store_8(amostra)

    arquivo.close()

    print("Som criado com sucesso.")

De qualquer modo, o reprodutor pôde tocar o arquivo de sons sem configurações adicionais. Assim, o exemplo demonstra a utilidade de cabeçalhos em arquivos. Programas podem ler os metadados de um arquivo para determinarem como os dados devem ser processados. Embora a escrita do código para gerar o arquivo possa ser mais trabalhosa, o uso é mais simples. Além disso, após escrito uma vez, basta reusar o código quando se quiser criar um novo arquivo no formato. Alternativamente, pode-se procurar por uma biblioteca que gere o cabeçalho (ou o arquivo todo) no formato desejado. Por sinal, é hora de aprender a usar bibliotecas externas e criar suas próprias.

Novos Itens para Seu Inventário

Ferramentas:

  • Editor hexadecimal;
  • Editor de imagem;
  • Editor de som.

Habilidades:

  • Criação de arquivos;
  • Manipulação de arquivos;
  • Escrita em arquivo;
  • Leitura de arquivo;
  • Extração de dados em arquivos;
  • Serialização e desserialização de dados;
  • Trocas de dados entre programas;
  • Criação de arquivos de imagens;
  • Criação de arquivos de áudio;
  • Discretização de sinais.

Conceitos:

  • Arquivos;
  • Arquivos texto;
  • Arquivos binários;
  • Sistemas de arquivos;
  • Permissões;
  • Caminhos (caminho absoluto e caminho relativo);
  • Acesso seqüêncial;
  • Acesso aleatório;
  • Saída padrão (stdout);
  • Entrada padrão (stdin);
  • Saída de erro padrão (stderr);
  • Abertura e fechamento de arquivo;
  • Formato de arquivo;
  • Arquivos de texto estruturados (CSV, TSV, XML, JSON, YAML, TOML);
  • Editores hexadecimais (hex editor);
  • Cabeçalhos;
  • Metadados;
  • Pixel;
  • Quadro (frame);
  • Sinais;
  • Amostra;
  • Taxa de amostragem;
  • Canais;
  • Fonte;
  • Amplitude;
  • Ruído;
  • Discretização;
  • Ordem de bits ou endianness (litle-endian e big-endian);
  • Serialização (marshalling) e desserialização (unmarshalling).

Recursos de programação:

  • Entrada e saída usando arquivos;
  • Logs;
  • Pascal strings;
  • Serialização e desserialização de dados;
  • Criação de arquivos com conteúdo multimídia;
  • Uso de bibliotecas externas em Lua.

Pratique

Exceto caso especificado, você pode optar por usar arquivos texto ou binários conforme sua preferência.

  1. Escreva um programa que armazene valores de um vetor em um arquivo texto em formato CSV. Abra o arquivo resultante em um editor de texto.

  2. Escreva um programa que armazene valores de um vetor em um arquivo texto em formato TSV. Abra o arquivo resultante em um editor de texto.

  3. Verifique se uma palavra ou frase está armazenada em um arquivo texto. Por exemplo, se o arquivo texto contém:

    Olá, meu nome é Franco!
    Tudo bem?
    Tchau!

    Caso a palavra seja Franco, o programa deve informar que a palavra consta no arquivo.

  4. Escreva um programa que inverta a ordem de todas as palavras de salvar em um arquivo texto. O programa deve ler os dados e atualizar o arquivo (ou criar um novo). Por exemplo, Olá, meu nome é Franco!: Franco! é nome meu Olá,.

  5. Salve e recupere dados de programas criados em exercícios de tópicos anteriores usando vetores de registros.

  6. Escreva um programa que leia números de um arquivo e salve o arquivo com os números ordenados.

  7. Escreva um programa que solicite a entrada de uma palavra e conte quantas vezes ela aparece em um arquivo texto.

  8. Escreva um programa que leia todas as palavras de um arquivo texto. O programa deve contar quantas vezes cada uma delas aparece no arquivo, depois escreva a palavra e o número de ocorrência dela, assim como o número total de palavras únicas armazenadas no arquivo.

  9. Serialize e desserialize valores para o registro Pessoa, que contém primeiro nome, sobrenome, idade (em anos), peso (ou massa, em quilogramas) e altura (em metros). Armazene dados de três pessoas no arquivo. Recupere os dados em um vetor de registros. Os valores armazenados no vetor devem ser variáveis do tipo Pessoa definido pelo registro. Para praticar, faça a serialização e desserialização em arquivo texto simples (um dado por linha), depois arquivo CSV ou TSV (dados de cada pessoa separados delimitados a cada linha), JSON e binário.

    [
        {
            "nome": "Franco",
            "sobrenome": "Garcia",
            "idade": 1234,
            "altura": 1.234,
            "peso": 12.34
        },
        {
            "nome": "SeuNome",
            "sobrenome": "SeuSobrenome",
            "idade": 0,
            "altura": 0.0,
            "peso": 0.0
        }
    ]

    Os nomes para os campos dos arquivos JSON não precisam ser os mesmos definidos para o registro, mas usar os mesmos nomes pode tornar a serialização mais fácil (principalmente em JavaScript). Para nomes de campos diferentes dos definidos no registro, pode-se criar um dicionário cujas chaves são os nomes desejados para os campos do arquivo JSON.

  10. Salve e recupere dados do programa criado para controle de estoque usando registros.

  11. Crie suas próprias imagens em PPM. Você pode procurar por pixel art como referência. Outra possibilidade é criar seu nome.

Próximos Passos

Parabéns! Agora você possui todos os principais recursos para programação fornecidos por linguagens de programação. É hora, pois, de subir de nível. Ao invés de iniciante, agora você possui um conhecimento adequado sobre programação. Você conhece os conceitos e fundamentos para programação, embora prática seja necessária para se tornar competente (ou proficiente).

Originalmente, o tópico sobre arquivos era o último planejado para este material introdutório. Entretanto, existem alguns tópicos adicionais que podem ser úteis. Por exemplo, criação e uso de bibliotecas; utilização de entrada com linha de comando; e operações bit-a-bit (bitwise operations). Assim, os próximos tópicos serão como anexos ou apêndices em livros ou dissertações.

Com os fundamentos adquiridos, você pode explorar novos paradigmas de programação para continuar sua jornada e expandir seu conhecimento. Programação Orientada a Objetos, Programação Funcional, Programação Lógica... Seu conhecimento atual é adequado para começar a explorar qualquer um deles. Em particular, é provável que alguns desses paradigmas sejam abordados nesta página. Por exemplo, o autor pretende começar uma série sobre Programação Orientada a Objetos em algum momento.

Além de paradigmas, existem conceitos, habilidades e recursos intermediários e avançados para aprendizado. Alguns exemplos incluem estruturas de dados, programação paralela, computação gráfica, sistemas distribuídos e redes, arquiteturas de software, padrões de projetos, e bancos de dados. Também existem ferramentas. Em particular, antes de começar a programar sistemas complexos, convém conhecer sistemas de gerenciamento para controle de versões de código-fonte (source-control management ou SCM), como git.

Ao longo desta introdução, JavaScript, Python, Lua e GDScript foram adotadas como linguagens principais para os exemplos. Contudo, o conhecimento adquirido é aplicável em qualquer linguagem de programação. Mais que uma linguagem de programação, você desenvolveu habilidades de pensamento computacional. Muitas técnicas apresentadas independem de linguagem de programação e de paradigma; elas podem ser usadas para a resolução de problemas usando computadores e qualquer linguagem. O paradigma e a sintaxe podem ser diferentes, mas o pensamento algorítmico independe de linguagem. Linguagens são ferramentas; o pensamento computacional é mais importante para a resolução de problemas que a linguagem adotada.

Para palavras finais para este tópico, arquivos são como lembranças para programas. Ao invés de começar a execução sempre com um mesmo estado (como uma página vazia ou em branco), pode-se recuperar o progresso obtido em usos anteriores de um programa. Arquivos permitem armazenar dados para uso em múltiplas sessões de um programa. Eles também permitem criar conteúdo digital para outros programas.

Dentre os possíveis conteúdos, pode-se incluir imagens e sons. O tópico apresentou a criação de arquivos com conteúdo gráfico e aural. O entendimento, mesmo que básico, de como imagens e sons digitais funcionam ajudam a compreender melhor como um computador representa, armazena e processa conteúdo multimídia.

Os exemplos fornecidos não são triviais; existem programadores profissionais que nunca criaram imagens e sons programaticamente usando linguagens de programação. Com os exemplos, também explicou-se como renderizar texto de forma simples. Com algumas melhorias, os programas podem servir como um ótimo exemplo para portfólio. Entretanto, seria melhor usar arquivos binários para reduzir o uso de operações com cadeias de caracteres e melhorar o desempenho do programa.

Como parte de seu caminho para tornar-se competente e proficiente, é hora de começar a pensar sobre desempenho. Especialmente em como escolhas para operações, tipos de dados e estruturas de dados podem afetar o desempenho de seus programas.

  1. Introdução;
  2. Ponto de entrada e estrutura de programa;
  3. Saída (para console ou terminal);
  4. Tipos de dados;
  5. Variáveis e constantes;
  6. Entrada (para console ou terminal);
  7. Aritmética e Matemática básica;
  8. Operações relacionais e comparações;
  9. Operações lógicas e Álgebra Booleana;
  10. Estruturas de condição (ou condicionais ou de seleção);
  11. Subrotinas: funções e procedimentos;
  12. Estruturas de repetição (ou laços ou loops);
  13. Vetores (arrays), cadeias de caracteres (strings), coleções (collections) e estruturas de dados;
  14. Registros (structs ou records);
  15. Arquivos e serialização (serialization ou marshalling);
  16. Bibliotecas;
  17. Entrada em linha de comando;
  18. Operações bit-a-bit (bitwise operations);
  19. Testes e depuração.
  • Informática
  • Programação
  • Iniciante
  • Pensamento Computacional
  • Aprenda a Programar
  • Python
  • Lua
  • Javascript
  • Godot
  • Gdscript