Azure Functions 2 – HTTP Trigger

18. 5. 2022 Azure, Programming

HTTP Trigger je jeden z nejvíce používaných Azure Functions triggerů, jelikož je využíván k vytvoření serverless API endpointů. V tomto článku si ho ukážeme od založení, po publikaci a při tom si vytvoříme i aplikaci, co má reálné využití!

Co tedy budeme dělat, ptáte se? No, vytvoříme si jednoduchý endpoint na Azure Functions za pomocí HTTP Triggeru, který nám přemění obrázek na ASCII art. V jednoduchosti na tento endpoint zašle uživatel Base64 text, ze kterého se poté pošle zpět ASCII art. Pokud nevíte, co ASCII art je, tak zde je jeden příklad, který náš kód vytvořil:

ASCII art známého obrazu Mona Lisa.
ASCII art známého obrazu Mona Lisa | Zdroj: Tomáš Diblík

Základ projektu

Pokud by se vám něco v následujících krocích nepovedlo zprovoznit, tak se na celý projekt můžete kouknout na Githubu.

Jelikož už jsme si dopodrobna popsali proces založení Azure Functions, tak sem pouze přidám list věcí, co musíte před programováním založit. Pokud nevíte, jak něco z výše uvedených udělat, tak si prosím přečtěte první část této série, kde se všechny potřebné informace dozvíte.

Dále, abychom pořád neopakovali jeden a ten samý proces, tak tento projekt vyvineme v Pythonu. Co to pro vás znamená? No, jenom to, že si musíte na Azure vybrat, že chcete, aby se projekt psal v Pythonu a také po příkazu func init musíte vybrat Python. Nic jiného se však nemění. V tom je krása Azure Functions, jakmile je máte nastavené, tak to jde psát bez problémů ve většině populárních jazyků. Dále si poté přes func new přidáte nový HTTP Trigger. Tento trigger poté používáme na převedení obrázků na ASCII art.

Po splnění kroků výše, by struktura vašeho vývojového souboru měla vypadat následovně:

Struktura projektu pro psaní azure funkce
Struktura projektu | Zdroj: Azure Portal

Všimněte si, jak se struktura tohoto projektu liší od projektu v Node.js. Pokud vás zajímá, co každý soubor v projektu dělá, tak si můžete přečíst getting_started.md. Pro nás ale budou hlavní soubory requirements.txt__init__.py. V souboru requirements.txt jsou sepsány všechny balíčky, které budeme v programu používat. Samotný program poté píšeme do __init__.py, který by měl vypadat nějak takto:

import logging
import azure.functions as func

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )

Vytváření funkcionality

Jako první krok, musíme soubor trošičku pročistit, abychom odstranili „Hello World“ funkcionalitu.

import logging
import azure.functions as func

def main(req: func.HttpRequest) -> func.HttpResponse:
    # Logni start funkce
    logging.info('Python HTTP endpoint triggered.')

    # Pošli zpět odpověď serveru
    return func.HttpResponse(
        "This HTTP triggered function executed successfully.",
        status_code=200
    )

Tak, teď máme čistou aplikaci, která pouze vrací HTTP kód 200 a zprávu. Vzhledem k tomu, že naše aplikace získá velké množství dat na request, nastavíme funkci pouze na POST requesty. To uděláme tak, že půjdeme do functions.json a zde změníme pole methods na [„post“].

Teď už musíme jenom naprogramovat konvertování obrázků na ASCII art.

První věc, kterou potřebujeme, je získání obrázku. Proto si pod logování startu funkce přidáme vyžádání obrázku v těle requestu.

    # Získej parametr obrazek. 
    # Pokus se vzít z těla requestu.
    obrazek = {}
    try:
        req_body = req.get_json()
    except ValueError:
        pass
    else:
        obrazek = req_body.get('obrazek')
    
    # Pokud ani po kontrolach neni obrazek nadefinovan, pošli zpět 422
    if not obrazek:
        return func.HttpResponse(
            "Prosím, zadejte base64 'obrazek' argument",
            status_code=422
        )

Dále, vzhledem k tomu, že potřebujeme nějaké knihovny na zpracování obrázku, přidáme si je do projektu. K tomu nám poslouží soubor requirements.txt. V tomto souboru byste měli mít defaultně přidán package azure-functions. Na náš projekt potřebujeme následující knihovny: PIL, io, base64. Váš soubor by poté měl vypadat nějak takto:

# Do not include azure-functions-worker as it may conflict with the Azure Functions platform

azure-functions
PIL
io
base64

Jakmile máme balíčky přidané, tak je můžeme v kódu použít, proto si naimportujeme tyto knihovny v horní části souboru.

# Importy pro azure
import logging
import azure.functions as func

# Importy pro práci s obrázkem
from PIL import Image
from io import BytesIO
import base64

Po dlouhém nastavování a importování se už konečně můžeme pustit na úpravu samotného obrázku. Jako první musíme obrázek převést z base64 na Image. Dále upravíme rozlišení obrázku, jelikož nechceme, aby byl náš ASCII art obrovský. Jako poslední věc, co uděláme, je to, že si obrázek převedeme na černobílý. To děláme z toho důvodu, abychom poté mohli každému pixelu přiřadit správnou hodnotu. To vše můžeme vidět v následujícím kódu:

    # Získej obrázek z base64 formátu
    img = Image.open(BytesIO(base64.b64decode(obrazek)))

    # Změň šířku a výšku obrázku
    new_width = 180
    width, height = img.size
    aspect_ratio = height/width
    new_height = aspect_ratio * new_width * 0.55
    img = img.resize((new_width, int(new_height)))

    # Přehoď obrázek na černobílý
    img = img.convert('L')

Jakmile máme obrázek zpracovaný, tak už pouze stačí jít pixel po pixelu, vzít hodnotu, přiřadit jí k charakteru. Na to, abychom toto mohli provést, tak potřebujeme pár věcí. Zaprvé, pole charakterů, které poté budeme dosazovat a z kterého budeme obrázek tvořit. Poté musíme jít pixel po pixelu a přiřadit tyto charaktery. Jako poslední část je poskládání ASCII art dohromady. To by měl udělat tento kus kódu:

    chars = ["@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."]    

    # Získej pixely z obrázku
    pixels = img.getdata()

    # Nahraď každý pixel s chrakterem z pole
    new_pixels = [chars[pixel//25] for pixel in pixels]
    new_pixels = ''.join(new_pixels)

    # Vytvoř ASCII art
    new_pixels_count = len(new_pixels)
    ascii_image = [new_pixels[index:index + new_width] for index in range(0, new_pixels_count, new_width)]
    ascii_image = "\n".join(ascii_image)

    # Logni finální obrázek
    logging.info(ascii_image)

    # Pošli zpět ASCII art
    return func.HttpResponse(
        ascii_image,
        status_code=200
    )

Finální funkce by měla vypadat takto:

# Importy pro azure
import logging
import azure.functions as func

# Importy pro práci s obrázkem
from PIL import Image
from io import BytesIO
import base64


# Nastavení konstant
new_width = 180
chars = ["@", "#", "$", "%", "?", "*", "+", ";", ":", ",", "."]

def main(req: func.HttpRequest) -> func.HttpResponse:
    # Logni start funkce
    logging.info('Python HTTP endpoint triggered.')

    # Získej parametr obrazek. 
    # Pokus se vzít z těla requestu.
    obrazek = {}
    try:
        req_body = req.get_json()
    except ValueError:
        pass
    else:
        obrazek = req_body.get('obrazek')
    
    # Pokud ani po kontrolach neni obrazek nadefinovan, pošli zpět 422
    if not obrazek:
        return func.HttpResponse(
            "Prosím, zadejte base64 'obrazek' argument",
            status_code=422
        )

    # Získej obrázek z base64 formátu
    img = Image.open(BytesIO(base64.b64decode(obrazek)))

    # Změň šířku a výšku obrázku
    width, height = img.size
    aspect_ratio = height/width
    new_height = aspect_ratio * new_width * 0.55
    img = img.resize((new_width, int(new_height)))

    # Přehoď obrázek na černobílý
    img = img.convert('L')
    
    # Získej pixely z obrázku
    pixels = img.getdata()

    # Nahraď každý pixel s chrakterem z pole
    new_pixels = [chars[pixel//25] for pixel in pixels]
    new_pixels = ''.join(new_pixels)

    # Vytvoř ASCII art
    new_pixels_count = len(new_pixels)
    ascii_image = [new_pixels[index:index + new_width] for index in range(0, new_pixels_count, new_width)]
    ascii_image = "\n".join(ascii_image)

    # Logni finální obrázek
    logging.info(ascii_image)

    # Pošli zpět ASCII art
    return func.HttpResponse(
        ascii_image,
        status_code=200
    )

Testování programu

Jelikož naše funkce přijímá pouze POST request, tak musíme napsat kód na její vyzkoušení. Vzhledem k tomu, že se celý článek táhne v duchu Pythonu, tak si kód na otestování rovnou napíšeme v něm. Na to však potřebujete už mít nainstalovaný Python!

Abychom si to zrekapitulovali, máme endpoint, který přijímá POST requesty a vyžaduje v těle requestu json s proměnnou obrazek. Tato proměnná musí být base64 data. Jakmile toto odešleme, vrátí se nám pouze text, který už je hotový ASCII art. Tento ASCII art poté chceme uložit do txt souboru, aby se nám to hezky zobrazovalo.

# Musíte si doinstalovat přes: pip install <název>
import requests
import json

# Nadefinujeme si endpoint na vyzkoušení
url = "http://localhost:7071/api/HttpTrigger"

# Nadefinujeme si tělo requestu
payload = json.dumps({
  "obrazek": "<Vložte base64>"
})
# Zašleme hlavičku, že se jedná o json
headers = {
  'Content-Type': 'application/json'
}

# Získáme odpověď od serveru
response = requests.request("POST", url, headers=headers, data=payload)

with open("ASCII_art.txt", "w") as text_file:
    text_file.write(response.text)

Závěr

Tak a jsme u konce. Vím, že to nebylo úplně nejlehčí, ale zároveň si myslím, že se to dá zvládnout. Na závěr bych také chtěl dát odkaz na skvělý tutoriál, jak udělat z obrázku ASCII art, kde se můžete dozvědět více technických informací o tom, jak se pracuje v Pythonu s pixely. Dále bych se také rád odkázal na Azure Functions Http Trigger dokumentaci, která mi v nastavování tohoto projektu hodně pomohla.

Pokud se vám článek líbil, neváhejte a přečtěte si pokračování této série.