Criando a foto oficial da Python Brasil 2020

A Python Brasil é o maior encontro da comunidade Python na América Latina. Em 2020, por motivos de COVID-19, pela primeira vez em seus 16 anos, a Python Brasil aconteceu online.

Uma das várias tradições do evento, é a foto oficial. Durante a Python Brasil, geralmente no último dia de palestras, todos os participantes, incluindo palestrantes, keynotes e organização, se juntam para uma foto que marca aquela edição. Esse ano não havíamos planejado nada, já que o evento seria online. Até que, faltando pouco mais de uma hora para o encerramento, um dos membros da organização, André Pastore, relembrou da foto oficial no canal da organização.

Nessa hora, comentei o desafio da foto oficial com os amigos que estavam no canal de voz do #boteco. Após alguns minutos de conversa tivemos a primeira ideia: baixar todas fotos de perfil e enviar para a Camila montar, de alguma forma, a foto oficial (A Camila foi responsável pelo trabalho belíssimo nas artes do evento).

Se você quiser reproduzir os trechos de código que vou mostrar abaixo, comece criando um ambiente virtual e instalando as duas dependências necessárias:

$ python -m venv venv
$ source venv/bin/activate
$ pip install discord.py pillow

Começamos pesquisando se existia algum método na API do Discord que retornasse o que precisávamos. Como eu já estava trabalhando no robô que ajudou a organizar o servidor, as dependências e credenciais necessárias já estavam prontas.

Conseguimos resolver a primeira parte do desafio rapidamente. O Gabriel logo encontrou o atributo avatar_url que retorna o caminho da foto de perfil do usuário. Inclusive, a própria biblioteca contém o método save para salvar a imagem em um arquivo.

Seguimos fazendo o primeiro teste para 20 usuários. Checamos o tamanho dos arquivos que foram baixados e, limitamos a função para que fizesse download das imagens de perfil apenas dos usuários que alteraram a foto, deixando de fora a imagem padrão do Discord.

async def salvar_imagem(participante: discord.Member):
    if participante.default_avatar_url == participante.avatar_url:
        # Se default_avatar_url é igual ao avatar_url, quer dizer que
        # a pessoa não alterou a foto de perfil
        return

    caminho = f"fotos/{participante.id}.webp"
    with open(caminho, "wb") as arquivo:
        await participante.avatar_url.save(arquivo)

    return caminho


async def main():
    token = "Token do bot no Discord"
    guild = "ID do servidor da Python Brasil"

    # Aqui criamos uma instância do client do Discord. Nós não precisávamos
    # usar o `discord.Intents.all()`, mas essa parte de autorização é mais
    # complexa, então fica para um próximo texto.
    cliente = discord.Client(intents=discord.Intents.all())
    await cliente.login(token)

    # Nessa parte, buscamos o servidor da Python Brasil e listamos todos os
    # membros que estavam presentes.
    guild = await cliente.fetch_guild(guild)
    membros = await guild.fetch_members().flatten()

    # Pronto, agora é só chamar a função salvar_imagem para cada um dos membros
    tarefas = [salvar_imagem(membro) for membro in membros]
    await asyncio.gather(*tarefas)
    await client.close()

No fim, baixamos 1330 imagens de perfil dos participantes da Python Brasil 2020. A experiência migrando o rastreiobot para asyncio ajudou bastante, já que os métodos da biblioteca do discord.py, que fazem requisições para a API, também usam asyncio.

Em posse das 1330 fotos, ficou claro que pedir para a Camila montar, manualmente, a foto oficial era algo inviável. Ainda mais porque o encerramento já estava próximo. Mais alguns minutos de conversa sobre as possibilidades, decidimos tentar juntar todas as fotos em uma só, usando o Pillow, a principal biblioteca de processamento de imagem em Python.

A essa altura, várias pessoas já estavam acompanhando o canal de voz, o que foi crucial, já que mais gente pôde ajudar. Por exemplo, Mazza e Gabriel encontraram a proporção correta da imagem final, Pastore achou o esqueleto do código usando Pillow, eu juntei tudo em um script.py e o João Bueno (JS) afinou os últimos detalhes. O resultado foi a função abaixo:

def gerar_imagem():
    # As imagens vieram do Discord com 1024 pixels de largura e altura.
    # Logo, o primeiro passo é redimensioná-las para 100 pixels de largura e altura.
    imagens = [
        Image.open(foto).resize((100, 100), Image.ANTIALIAS)
        for foto in Path("fotos").iterdir()
    ]

    # Aqui criamos a estrutura da imagem final, com 4800 pixels de largura e
    # 2800 pixels de altura.
    imagem_final = Image.new('RGB', (4800, 2800))

    for x in range(0,48):
        for y in range(0,28):
            # A parte `% len(imagens)` faz com que o laço reuse imagens,
            # caso a proporção não seja perfeita.
            indice = (48 * y + x) % len(imagens)
            
            # Agora preenchemos a imagem final com cada uma das imagens de perfil
            # dos participamentes.
            imagem_final.paste(imagens[indice], (x * 100, y * 100))

    imagem_final.save("python-brasil-2020.jpg")

Durante a conversa, vimos alguns pontos que poderíamos melhorar o script, como por exemplo redimencionar a imagem dentro do laço, diminuindo o consumo de memória. Se você tiver alguma sugestões, comente abaixo.

A foto oficial da Python Brasil 2020, resultado de um processo colaborativo, sinônimo da comunidade, pode ser visto na melhor qualidade aqui.

Revisões: Leticia Portella e André Pastore.

o/