
Konwersja obrazków PNG do WebP w Pythonie – praktyczny przykład z regex
Spis treści
Z artykułu dowiesz się, jak za pomocą wyrażeń regularnych i biblioteki Pillow zamienić obrazki PNG w pliku Markdown na WebP.
Konwersja obrazków PNG do WebP w pliku Markdown
W przykładzie mamy plik Markdown z treścią zadań szkoleniowych, w którym obrazki są osadzone bezpośrednio w tekście w formacie PNG zakodowanym w Base64. Chcemy zmniejszyć rozmiar pliku, zamieniając obrazki na nowocześniejszy format WebP.
Do wyszukania i podmiany obrazków użyjemy funkcji re.search() oraz re.sub() z modułu re w Pythonie, a do konwersji obrazków biblioteki Pillow.
Plan działania:
- przeszukać kolejne linie dokumentu
- odnaleźć linie z osadzonymi obrazkami, czyli zawierające znacznik
<imgz danymi PNG - dla każdego obrazka:
- odczytać dane obrazka zakodowane w Base64
- przekonwertować je do formatu WebP
- ponownie zakodować wynik w Base64 i zapisać go w tym samym miejscu dokumentu.
Do konwersji będą potrzebne wyrażenia regularne. Ich szczegółowy opis znajdziesz w artykule Wyrażenia regularne w Pythonie.
Krok 1 - wczytaj przykładowy dokument do programu
Przykładowy dokument:
### PB1.213Wykonaj poniższe polecenia używając tabeli **raporty_sprzedazowe**:- dodaj nową stronę- wstaw wykres liniowy, na którym zaprezentujesz sprzedaż produktów per rok <img style="width: 10px;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAFUlEQVR4nGNU2ujDgBsw4ZFjGLnSAGaDATMBem2QAAAAAElFTkSuQmCC"/>- wstaw filtr, aby można było filtrować wizualizację po imieniu sprzedawcy- zmodyfikuj wykres zgodnie z wzorem na poniższym obrazkuZapisz przykładowy dokument w pliku kurs_PB1_zadania.md w tym samym folderze, w którym zapisany jest skrypt Pythona.
Następnie wczytaj plik i rozbij go na linie:
fn = "kurs_PB1_zadania.md"md = open(fn, "r", encoding="utf-8").read().splitlines()W przykładzie:
- metoda
read()wczytuje cały plik jako jeden napis splitlines()dzieli plik na osobne linie- parametr
encoding="utf-8"zapewnia poprawne odczytanie polskich znaków. - zmienna md zawiera listę linii.
Jeśli pojawi się błąd FileNotFoundError upewnij się, że plik "kurs_PB1_zadania.md" jest w tym samym katalogu, w którym znajduje się aplikacja Python (plik *.py)
Krok 2 - znajdź linie z obrazkiem re.search()
Szukamy linii, które zawierają na początku <img i na końcu />. Dla każdej linii używamy wzorca <img.*/> i drukujemy znaleziony fragment.
Uwaga! W kroku pierwszym plik wczytany został do zmiennej md. IDLE przy każdym uruchomieniu skryptu (F5) restartuje powłokę. Restart kasuje wszystkie wcześniej utworzone zmienne.
Dlatego wczytywanie pliku musi zostać w tym samym skrypcie, w którym dalej pracujesz i używasz zmiennej md.
Ewentualnie możesz uruchamiać kolejne polecenia w powłoce Shell. Jeśli dopiero zaczynasz pracę z Pythonem, zajrzyj do artykułu Jak rozpocząć pracę z IDLE w Pythonie.
import re
for line in md: z = re.search(r'<img.*/>', line) if z: s = z.group() print(s)Wynik:
<img style="width: 10px;" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAFUlEQVR4nGNU2ujDgBsw4ZFjGLnSAGaDATMBem2QAAAAAElFTkSuQmCC"/>Jak działa wzorzec <img.*/>?
<img– dosłowny tekst.*– dowolne znaki w dowolnej liczbie (atrybuty, dane obrazka). Może też być pusty tekst./>– dosłowny tekst
Linie bez obrazka nie pasują do wzorca, więc re.search() zwraca dla nich None i nic nie jest drukowane. Tekst przed <img (spacje, myślnik listy) nie należy do dopasowania – re.search() szuka w dowolnym miejscu linii.
Krok 3 - wyciągnij dane Base64 grupą
Nie potrzebujemy całego znacznika, tylko samych danych obrazka. Użyjemy wzorca z grupą:
r'<img.*image/png;base64,(.*?)"/>'
Wzorzec czytamy od lewej:
<img– początek znacznika.*– dowolne atrybuty (np.style="width: 10px;" src="data:)image/png;base64,– dosłowny tekst poprzedzający dane PNG(.*?)– grupa, czyli fragment, który chcemy odczytać. Leniwe*?sprawia, że grupa kończy się na pierwszym"/>"/>– cudzysłów zamykający atrybutsrci koniec znacznika.
for line in md: z = re.search(r'<img.*image/png;base64,(.*?)"/>', line) if z: base64_png = z.group(1) print(base64_png)Metoda group(1) zwraca zawartość pierwszej grupy (grupy numerujemy od 1).
Do zmiennej base64_png trafia zakodowany w Base64 obrazek – bez atrybutów i znaczników.
Teraz dekodujemy obrazek z Base64, konwertujemy go z PNG do WebP biblioteką Pillow i ponownie kodujemy w Base64.
from PIL import ImageGrab # pip install pillowimport ioimport base64
img = ImageGrab.Image.open(io.BytesIO(base64.b64decode(base64_png)))imgByteArr = io.BytesIO()img.save(imgByteArr, format='WebP')imgByteArr = imgByteArr.getvalue()base64_webp = base64.b64encode(imgByteArr).decode('UTF-8')W przykładzie:
-
base64.b64decode(base64_png)– zamienia tekst Base64 z powrotem na dane binarne obrazka -
io.BytesIO(...)– opakowuje te dane w obiekt, który zachowuje się jak plik w pamięci. Dzięki temu nie musimy zapisywać obrazka na dysku. -
ImageGrab.Image.open()– otwiera obrazek z danych binarnych. ModułImageGrabudostępnia klasęImagez biblioteki Pillow, więc możemy z niej skorzystać bez osobnego importu. -
imgByteArr = io.BytesIO()– tworzy nowy, pusty bufor w pamięci, do którego zapiszemy przekonwertowany obrazek. -
img.save(imgByteArr, format='WebP')– zapisuje obrazek w formacie WebP do tego bufora. -
imgByteArr.getvalue()– pobiera z bufora gotowe bajty pliku WebP i przypisuje je do zmiennejimgByteArr. -
base64.b64encode(imgByteArr).decode('UTF-8')– koduje nowy obrazek w Base64 i zamienia wynik na zwykły napis.
Uwaga: Równoważnie można użyć importu
from PIL import Imagei wywołać bezpośrednioImage.open(). ModułImageGrabsłuży głównie do wykonywania zrzutów ekranu.
Do zainstalowania biblioteki Pillow, jeśli jeszcze jej nie masz, możesz użyć menedżera pakietów uv. W artykule Python uv znajdziesz instrukcje, w jaki sposób go używać.
Krok 4 - podmień obrazek re.sub()
Kodowanie Base64 zapisuje dane binarne obrazka w postaci tekstu, który można umieścić w pliku Markdown.
Dane nowego obrazka zastąpią dane starego. Zamienimy też informację o typie z image/png na image/webp.
Użyjemy funkcji re.sub():
img = ImageGrab.Image.open(io.BytesIO(base64.b64decode(base64_png)))imgByteArr = io.BytesIO()img.save(imgByteArr, format='WebP')imgByteArr = imgByteArr.getvalue()base64_webp = base64.b64encode(imgByteArr).decode('UTF-8')
line2 = re.sub( r'<img(.*?)image/png;base64,.*?"/>', rf'<img\1image/webp;base64,{base64_webp}"/>', line )print(line2)Wynik (dane Base64 skrócone):
<img style="width: 10px;" src="data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACwAQCdAS..."/>W przykładzie:
-
we wzorcu grupa
(.*?)dopasowuje wszystko pomiędzy<imgaimage/png– czyli atrybutystyle="width: 10px;" src="data:. Chcemy je zachować bez zmian -
w zamienniku
\1wstawia zawartość tej grupy z powrotem w to samo miejsce -
image/pngzamieniamy naimage/webp, a stare dane – na nowe -
przedrostek
rfłączy dwa rodzaje napisów:r(surowy –\1nie jest interpretowane przez Pythona) orazf(f-string –{base64_webp}wstawia wartość zmiennej).
Dane Base64 zawierają tylko litery, cyfry oraz znaki +, / i =, więc można je bezpiecznie wstawić do zamiennika. Gdyby wstawiany tekst mógł zawierać backslash, lepiej użyć funkcji jako zamiennika (zobacz sekcję o re.sub() w artykule Wyrażenia regularne w Pythonie).
Uwaga: Dla przejrzystości pokazujemy ten fragment osobno. W pełnym programie znajduje się on wewnątrz pętli
for line in md, w blokuif z:. Dzięki temu konwersja i zamiana są wykonywane dla każdej linii, w której znaleziono obrazek, a zmiennalinezawsze wskazuje właściwą linię.
Kompletny skrypt
Na koniec łączymy wszystkie kroki w jeden skrypt. Program przetwarza plik linia po linii, zamienia obrazki PNG na WebP i zapisuje wynik w miejscu oryginalnego pliku.
from PIL import ImageGrab # pip install pillowimport ioimport base64import re
fn = r"kurs_PB1_zadania.md"md = open(fn, "r", encoding="utf-8").read().splitlines()
wynik = []for line in md: z = re.search(r'<img.*image/png;base64,(.*?)"/>', line) if z: base64_png = z.group(1)
img = ImageGrab.Image.open(io.BytesIO(base64.b64decode(base64_png))) imgByteArr = io.BytesIO() img.save(imgByteArr, format='WebP') imgByteArr = imgByteArr.getvalue() base64_webp = base64.b64encode(imgByteArr).decode('UTF-8')
line2 = re.sub( r'<img(.*?)image/png;base64,.*?"/>', rf'<img\1image/webp;base64,{base64_webp}"/>', line ) line = line2 wynik.append(line)
with open(fn, "w", encoding="utf-8") as f: f.write("\n".join(wynik))Uwaga! Skrypt nadpisuje oryginalny plik Markdown, dlatego przed jego uruchomieniem warto zrobić kopię zapasową.
W przykładzie używamy pętli. Informacje o pętlach znajdziesz w artykule Pętle w Pythonie.
W kilkunastu linijkach kodu połączyliśmy trzy umiejętności: wyszukiwanie wzorców wyrażeniami regularnymi, przetwarzanie obrazów biblioteką Pillow oraz automatyczną modyfikację plików tekstowych. Ten sam schemat możesz zastosować do innych zadań, np. masowej zmiany linków, porządkowania dokumentacji czy przetwarzania raportów.
Szukasz okazji, aby w praktyce zmierzyć się z zaawansowanymi zagadnieniami Pythona? Sprawdź nasz kurs Python poziom zaawansowany.
