Grafika w bazie MySQL
Podczas tworzenia generatora galerii zdjęć spotkałem się z problemem przechowywania obrazków w bazie MySQL. W internecie o wykorzystaniu tej możliwości pisze się stosunkowo mało. W opisach które znalazłem zwykle było dużo niedomówień, albo okazywało się że przedstawiony gotowy skrypt nie działa tak doskonale jak zarzeka się autor. Zatem w tym artykule postaram się w prosty sposób omówić zapis plików do bazy, odczyt z niej i wyświetlanie pobranego z bazy obrazka za pomocą funkcji z biblioteki PHP_GD2.
W bazie MySQL istnieje specjalny typ pola, służący do przechowywania obiektów binarnych.
Pole typu BLOB, bo o nim tu mowa, ma aż 4 odmiany mogące przechowywać obiekty o różnych wielkościach:
- TINYBLOB (1 B),
- BLOB (64 kB),
- MEDIUMBLOB (16 MB),
- LONGBLOB (4 GB),
Dobrze jest wybrać odpowiedni dla naszych potrzeb rodzaj pola BLOB, gdyż beztroskie korzystanie z pola LONGBLOB niepotrzebnie obciąży naszą bazę.
Takwięc, stwórzmy sobie prościutką tabelę w MySQL zawierającą 5 pól: pole klucza typu INT, pole typu MEDIUMBLOB na zawartość obrazka, pole typu CHAR w celu przechowania typu MIME tego obrazka, pole typu CHAR do przechowywania jego nazwy i pole typu INT do przechowywania jego rozmiaru. Wszystko za pomocą PHP.
<?PHP
//Nawiązujemy połączenie z serwerem bazy MySQL
$conn=mysql_connect("localhost","user","hasło")
or die ('Błąd połączenia z bazą MySQL: '.mysql_error());
//Tworzymy nową bazę
$result1=mysql_create_db("naszabaza",$conn)
or die('Błąd podczas tworzenia nowej bazy: '.mysql_error());
//Wybieramy do pracy bazę którą przed chwilą stworzyliśmy
$result2=mysql_select_db("naszabaza",$conn)
or die ('Błąd podczas wyboru bazy: '. mysql_error());
//Tworzymy zapytanie tworzące tabelę "obrazy",
//która będzie składać się z pięciu pól:
//"ID_obrazy" typu INT, "obrazek" typu MediumBlob,
//"typ" typu Char, "nazwa" typu Char i "rozmiar" typu INT.
$sql1='Create table obrazy (
ID_obrazy Int UNSIGNED
NOT NULL AUTO_INCREMENT,
obrazek MediumBlob NOT NULL,
typ Char(60) NOT NULL,
nazwa Char(255) NOT NULL,
rozmiar INT UNSIGNED NOT NULL,
UNIQUE (ID_obrazy),
Primary Key (ID_obrazy))
TYPE = MyISAM ROW_FORMAT = Default;';
//Wykonujemy zapytanie $sql1
$result3=mysql_query($sql1,$conn)
or die ('Błąd wykonania zapytania 1: '. mysql_error());
?>
|
W ten oto sposób utworzyliśmy tabelkę z polem MEDIUMBLOB.
Przydałoby się zapisać w tej tabelce jakąś grafikę.
W tym celu stwórzmy w HTML'u prosty formularz do uploadu plików.
<!-- plik upload1.html -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-type" CONTENT="text/html; charset=UTF-8">
</HEAD>
<BODY>
<FORM ACTION="upload2.php" METHOD="POST" ENCTYPE="multipart/form-data">
<INPUT TYPE="HIDDEN" NAME="MAX_FILE_SIZE" VALUE="8388608">
<INPUT TYPE="FILE" NAME="plik" VALUE="" size="30">
<INPUT TYPE="SUBMIT" VALUE="Wczytaj">
</FORM>
</BODY>
</HTML>
|
W przypadku tego listingu jest kilka rzeczy na które należy zwrócić uwagę:
- Atrybut ACTION odnosi się do skryptu PHP, który wczyta plik do bazy,
- Atrybut METHOD jest ustawiony na "POST",
- Dodatkowy atrybut ENCTYPE jest ustawiony na "multipart/form-data",
co pozwoli przegladarce rozpoznać typ pliku i prawidłowo wysłać go
do serwera WWW,
- Ukryte pole MAX_FILE_SIZE określa maksymalny rozmiar przesyłanego
pliku określony w bajtach. Wartość ta nie może przekraczać wartości
ustawionej w pliku PHP.INI przy dyrektywie "upload_max_filesize".
Wartość dyrektywy "upload_max_filesize" z kolei nie może przewyższać
wartości dyrektywy "post_max_size".
- Pole formularza typu "FILE" służy do wskazania ścieżki do naszego pliku.
Pole to musi mieć unikatową nazwę. W przykładzie jego nazwa to "plik".
- Przycisk typu "SUBMIT" służy do inicjacji wysłania danych z formularza.
Teraz możemy przystąpić do tworzenia skryptu PHP wczytującego nasz plik, w tym przypadku wczytującego grafikę do bazy MySQL.
<?PHP
//plik upload2.php
if (isset($_FILES['plik']))
{
if ($_FILES['plik']['error']==UPLOAD_ERR_OK)
{
$filename=$_FILES['plik']['name']; //Nazwa wysłanego pliku
$filetype=$_FILES['plik']['type']; //Typ wysłanego pliku
$filesize=$_FILES['plik']['size']; //Rozmiar wysyłanego pliku
$filesrc=$_FILES['plik']['tmp_name']; //ścieżka do pliku
//tymczasowego na serwerze
if ($filetype=="image/png" || $filetype=="image/x-png" ||
$filetype=="image/gif" || $filetype=="image/jpeg" ||
$filetype=="image/pjpeg")
{
$plik=fopen($filesrc,"r"); //otwórz ten plik w trybie do odczytu
$mysqlplik = addslashes(fread($plik,$filesize));
//odczytaj go do końca, dodaj znaki "\" przed apostrofami
//i cudzysłowami. Całość zapisz w zmiennej $mysqlplik.
fclose($plik); //zamykamy plik
unlink($filesrc); //i kasujemy go, bo już nie jest potrzebny.
$mysqlfiletype = addslashes($filetype); //dodajemy znaki "\"
//przed apostrofami i cudzysłowami do wartości zmiennej
//$filetype, w której został zdefiniowany typ MIME naszego
//pliku. Wynik zapisujemy w zmiennej $mysqlfiletype.
$mysqlfilename = addslashes($filename); //dodajemy znaki "\"
//przed apostrofami i cudzysłowami do wartości zmiennej
//$filename, w której została zapisana nazwa naszego pliku.
//Wynik zapisujemy w zmiennej $mysqlfilename.
//Nawiązujemy połączenie z serwerem bazy MySQL
$conn=mysql_connect("localhost","user","hasło")
or die ('Błąd połączenia z bazą MySQL: '.mysql_error());
//Wybieramy bazę w której mamy tabelę z polem blob
$result2=mysql_select_db("naszabaza",$conn)
or die ('Błąd podczas wyboru bazy: '. mysql_error());
//Tworzymy zapytanie zapisujące do tabeli "obrazy" plik,
//w naszym przypadku obrazek, jego typ MIME, nazwę
//oraz rozmiar w bajtach.
$sql1="INSERT INTO obrazy (ID_obrazy,obrazek,typ,nazwa,rozmiar)
VALUES ('','$mysqlplik','$mysqlfiletype','$mysqlfilename','$filesize')";
//Wykonujemy zapytanie $sql1
$result1=mysql_query($sql1,$conn)
or die ('Błąd wykonania zapytania 1: '. mysql_error());
}
else {die("Nieobsługiwany format pliku !!!"); exit;}
}
else {die("Błąd podczas wysyłania pliku !!!"); exit;}
}
else {die("Nie wskazano pliku do wysłania !!!"); exit;}
?>
|
W tym listingu należy zwrócić uwagę na przedstawione poniżej szczegóły:
- Wynik funkcji "isset($_FILES['plik'])", która sprawdza czy podaliśmy ścieżkę do pliku który ma być później wczytany. Jeśli funkcja zwróci wartość "false", będzie to oznaczało że nie wskazaliśmy pliku do wczytania.
- Atrybut "$_FILES['plik']['error']", który powinnien być równy predefiniowanej w PHP stałej "UPLOAD_ERR_OK", z tego atrybutu dowiadujemy się czy upload przebiegł prawidłowo, czy został wczytany właściwy plik, czy jego rozmiar nie przekracza wartośći określonej przez element "MAX_FILE_SIZE" itp.
- W naszym przypadku sprawdzamy typ MIME przesyłanego pliku, żeby dowiedzieć się czy jest to akceptowany przez nas format obrazka. Ja ograniczyłem się do formatów JPG, PNG i GIF.
- Dopiero gdy wszystkie powyższe warunki zostaną spełnione, otwieramy w trybie do odczytu przesłany plik znajdujący się na serwerze jako plik tymczasowy o lokalizacji ustalonej przez serwer, a wiadomej nam z atrybutu "$_FILES['plik']['tmp_name']".
- Odczytu zawartości pliku dokonujemy funkcją "fread()", w której jako pierwszy parametr podajemy wskaźnik do struktury plikowej obrazka, a jako drugi parametr podajemy znany nam z atrybutu $_FILES['plik']['size'] rozmiar pliku.
- Odczytaną zawartość pliku musimy jeszcze na potrzeby jej użycia w zapytaniu MySQL specjalnie "oszlifować" funkcją "addslashes()", która wstawi nam znaki "\" przed znaki apostrofu ['], cudzysłowia ["] oraz slash[\]. Jest to konieczne, bo inaczej PHP niepoprawnie by interpretował te znaki w trakcie wykonywania zapytania zapisującego do bazy zawartość pliku. Po prostu uznałby że zawartość pliku kończy się na jednym z tych znaków.
- Po odczytaniu zawartości pliku, zamykamy go funkcją "fclose()" i kasujemy funkcją "unlink()". Jeśli sami go nie usuniemy, serwer powinien to zrobić za nas zaraz po zakończeniu działania skryptu "upload2.php".
- Dodajemy slashe do zmiennej $filetype i zapisujemy wynik w zmiennej $mysqlfiletype. I tu muszę zaznaczyć że jest to zabieg tak na wszelki wypadek, bo nie widziałem jeszcze typu MIME, który w nazwie zawierałby znaki ["],['],[\].
- Następnie inicjujemy połączenie z serwerem MySQL i wybieramy bazę na której chcemy pracować.
- Potem zostało już tylko wstawienie do tabeli "obrazy" nowego rekordu, który w drugiej komórce będzie zawierał nasz obrazek wczytany tym razem z utworzonej przez nas zmiennej $mysqlplik, zawierającej zawartość przesyłanego pliku ze wspomnianymi wcześniej znakami "\", w trzeciej komórce będzie zawierał definicję typu MIME wczytywanego pliku również ze wspomnianymi wcześniej znakami "\" jeśli będzie to konieczne, w kolejnej komórce pojawi się nazwa pliku "oszlifowana" funkcją "addslashes()" oraz w ostatniej komórce znajdzie się rozmiar przesyłanego pliku liczony w bajtach.
- Uwaga !!! Znaków "\" potrzebował jedynie sam PHP do wysłania prawidłowego zapytania do serwera MySQL. W bazie MySQL została zapisana oryginalna zawartość obrazka, bez dodanych znaków "\" przed cudzysłowiami, apostrofami i slashami.
W taki oto sposób zapisaliśmy plik graficzny do bazy MySQL.
Teraz pokażę w jaki sposób można ten obrazek wyświetlić.
Można to zrobić na 2 sposoby.
Sposób pierwszy tradycyjny.
<?PHP
//Nawiązujemy połączenie z serwerem bazy MySQL
$conn=mysql_connect("localhost","user","hasło")
or die ('Błąd połączenia z bazą MySQL: '.mysql_error());
//Wybieramy bazę w której mamy tabelę z polem blob
$result1=mysql_select_db("naszabaza",$conn)
or die ('Błąd podczas wyboru bazy: '. mysql_error());
//Tworzymy zapytanie wybierające z bazy MySQL zapisaną w niej
//zawartość wczytanego wcześniej pliku wraz z jego typem MIME,
//nazwą i rozmiarem.
$sql1="select * from obrazy where ID_obrazy='1'";
//Wykonujemy zapytanie $sql1
$result2=mysql_query($sql1,$conn)
or die ('Błąd wykonania zapytania 1: '.mysql_error());
//tworzymy tablicę asocjacyjną $row i wczytujemy do niej
//dane z wybranego rekordu tabeli. W naszym przypadku
//jest to rekord 1.
$row = mysql_fetch_assoc($result2);
//Z tablicy $row wydobywamy zawartość komórki "obrazek"
//i zapisujemy ją do zmiennej $grafika.
$grafika=$row['obrazek'];
//Z tablicy $row wydobywamy zawartość komórki "typ"
//i zapisujemy ją do zmiennej $typpliku.
$typpliku=$row['typ'];
//Wysyłamy do przeglądarki nagłówek HTTP, informujący
//przeglądarkę że wynik który za chwilę wyświetlimy to
//obrazek w formacie określonym przez zmienną $typpliku.
header("Content-type:$typpliku");
//Drukujemy zawartość zmiennej $grafika,
//czyli wyświetlamy zawartość naszego obrazka
print $grafika;
?>
|
W przypadku powyższego kodu, należy między innymi zwrócić uwagę na sam sposób wyświetlenia obrazka. Mianowicie, najpierw wysyłamy do przeglądarki nagłówek HTTP z definicją typu MIME naszego obrazka, a dopiero potem drukujemy zawartość obrazka. Dzięki wysłaniu nagłówka z definicją określonego typu MIME, przeglądarka prawidłowo zinterpretuje wyświetlane dane będące zawartością obrazka i wyświetli je jako obrazek. W przeciwnym wypadku, gdybyśmy takiego nagłówka nie ustawili, zawartość obrazka zostałaby wyświetlona jako niezrozumiały tekst.
Pokazany tutaj sposób wyświetlania można również zastosować do wszystkich innych typów plików, nie tylko obrazków. Można przykładowo podczas procedury uploadu pliku do bazy znieść ograniczenie co do typu MIME przesyłanego pliku i już w bazie można zapisywać dosłownie wszystko.
Należy jednak zwrócić uwagę na dość istotny szczegół. W przypadku gdy będziemy próbowali wyświetlić zawartość pliku o rozszeżeniu nieznanym przeglądarce jako element współtworzący stronę WWW (np. *.zip, *.exe, *.notype), przeglądarka rozpozna taki plik oczywiście po definicji typu MIME danego pliku w wysłanym nagłówku i wyświetli okienko w rodzaju "Zapisz jako".
W przypadku gdy będziemy próbowali wyświetlić plik w formacie nieznanym przeglądarce jako element tworzący stronę WWW, ale przeglądarka będzie posiadała określony plugin do jego wyświetlania, wtedy przeglądarka wyświetli zawartość takiego pliku w tym samym oknie ale w środowisku jakie oferuje nam dany plugin. Coś takiego ma miejsce gdy np. otwieramy dokumenty *.PDF. Zwykle uruchamia się wtedy plugin programu Adobe Acrobat Reader.
Można jednak zmusić przeglądarkę do wyświetlania okienka "Zapisz jako" w przypadku każdego formatu pliku, również *.html, *.txt, *.jpg, *.png, *.gif, *.pdf, *.doc, itd. Można to osiągnąć poprzez wysłanie do przeglądarki dodatkowych nagłówków HTTP. Wygląda to następująco:
<?PHP
$conn=mysql_connect("localhost","user","hasło")
or die ('Błąd połączenia z bazą MySQL: '.mysql_error());
//Wybieramy bazę w której mamy tabelę z polem blob
$result1=mysql_select_db("naszabaza",$conn)
or die ('Błąd podczas wyboru bazy: '. mysql_error());
$sql1="select * from obrazy where ID_obrazy='1'";
$result2=mysql_query($sql1,$conn)
or die ('Błąd wykonania zapytania 1: '.mysql_error());
//tworzymy tablicę asocjacyjną $row i wczytujemy do niej
//dane z wybranego rekordu tabeli. W naszym przypadku
//jest to rekord 1.
$row = mysql_fetch_assoc($result2);
//Z tablicy $row wydobywamy zawartość komórki "obrazek"
//i zapisujemy ją do zmiennej $grafika.
$grafika=$row['obrazek'];
//Z tablicy $row wydobywamy zawartość komórki "typ"
//i zapisujemy ją do zmiennej $typpliku.
$typpliku=$row['typ'];
//Z tablicy $row wydobywamy zawartość komórki "nazwa"
//i zapisujemy ją do zmiennej $nazwapliku.
$nazwapliku=$row['nazwa'];
//Z tablicy $row wydobywamy zawartość komórki "rozmiar"
//i zapisujemy ją do zmiennej $rozmiarpliku.
$rozmiarpliku=$row['rozmiar'];
//Wysyłamy do przeglądarki nagłówek HTTP, informujący
//że wynik który za chwilę będziemy chcieli pobrać to
//obrazek w formacie określonym przez zmienną $typpliku.
header("Content-type:$typpliku");
//Wysyłamy do przeglądarki nagłówek HTTP, podający
//przeglądarce rozmiar pobieranego pliku określony
//przez zmienną $rozmiarpliku.
header("Content-length: $rozmiarpliku");
//Wysyłamy do przeglądarki nagłówek HTTP, wymuszający
//na przeglądarce wyświetlenie okienka "Zapisz jako".
//Dodatkowo w nagłówku tym podajemy domyślną docelową
//nazwę pliku zdefiniowaną przez zmienneą $nazwapliku
header("Content-Disposition: attachment; filename=$nazwapliku");
//Drukujemy zawartość zmiennej $grafika,
//ale tym razem nie zostanie ona wyświetlona.
//Będziemy mogli ją zapisać lub otworzyć lokalnie.
print $grafika;
?>
|
Niestety przedstawione tutaj rozwiązanie ma pewną wadę. Nie wszystkie przeglądarki z nim sobie poradzą. W manualu PHP czytamy:
"W Microsoft Internet Explorer 4.01 jest błąd, który uniemożliwia wykorzystanie tego mechanizmu. Nie ma na to rozwiązania. Błąd, który zahacza o ten mechanizm, jest także w Microsoft Internet Explorer 5.5, jednak da się go ominąć aktualizując przeglądarkę poprzez Service Pack 2 lub późniejszy."
Z moich doświadczeń wynika że z tym mechanizmem nie radzą sobie również przeglądarki Opera 6, Mozilla 0.9.7 i Netscape 6 lub ich starsze wersje. Internet Explorer 6, Opera 7 i Mozilla 1.2 radzą sobie doskonale. Nie testowałem przeglądarki Mozilla w wersjach (0.9.7,1.1> oraz Netscape nowszej niż wersja 6.
Tyle jeśli chodzi o sposób tradycyjny, w którym wysyłaliśmy do przeglądarki nagłówki HTTP.
Sposób wykorzystujący możliwości biblioteki PHP_GD2.
<?PHP
$conn=mysql_connect("localhost","user","hasło")
or die ('Błąd połączenia z bazą MySQL: '.mysql_error());
//Wybieramy bazę w której mamy tabelę z polem blob
$result1=mysql_select_db("naszabaza",$conn)
or die ('Błąd podczas wyboru bazy: '. mysql_error());
$sql1="select * from obrazy where ID_obrazy='1'";
$result2=mysql_query($sql1,$conn)
or die ('Błąd wykonania zapytania 1: '.mysql_error());
//tworzymy tablicę asocjacyjną $row i wczytujemy do niej
//dane z wybranego rekordu tabeli. W naszym przypadku
//jest to rekord 1.
$row = mysql_fetch_assoc($result2);
//Z tablicy $row wydobywamy zawartość komórki "obrazek"
//i zapisujemy ją do zmiennej $grafika.
$grafika=$row['obrazek'];
//Na podstawie strumienia danych przechowywanych w polu BLOB,
//traktowanych w tej chwili jako ciąg znaków tworzymy obrazek
//w pamięci serwera i przypisujemy go do wskaźnika $obraz.
$obraz=imageCreateFromString($grafika);
//Generujemy w przeglądarce obrazek, w tym przypadku JPG.
imageJPEG($obraz);
//Uwalniamy z pamięci wskaźnik $obraz.
ImageDestroy($obraz);
?>
|
Jak widać nie jest to specjalnie skomplikowana metoda. Kryje ona w sobie wiele możliwości, ale niestety ma też pewne wady.
Możliwości jest dużo. Dzięki wykorzystaniu PHP_GD2 można właściwie rysować (!) za pomocą PHP, tworzyć tzw. znaki wodne na obrazkach, zmieniać swobodnie ich rozmiar, ustawiać indeks koloru przeźroczystego, konwertować paletę kolorów do 256, 16 i 2 i wiele, wiele więcej. Dodatkowo odpowiednie nagłówki HTTP są wysyłane automatycznie.
Wad jest zdecydowanie mniej, ale czasem potrafią być dokuczliwe.
Chodzi mi przede wszystkim o obsługę tylko plików graficznych w formatach *.JPG, *.GIF, *.PNG i *.WBMP (nie mylić z *.BMP). Istnieją jeszcze 2 inne formaty o rozszerzeniach *.XBM i *.XPM, ale nie warto sobie zawracać nimi głowy.
Generalnie biblioteka ta jest w dwóch wersjach: GD i GD2. Pociąga to za sobą kilka niezgodności. Biblioteka GD nie potrafi wyświetlić obrazka w formacie PNG, a GD2 nie potrafi wyświetlić obrazka w formacie GIF. Obie potrafią wczytywać do pamięci obrazki za pomocą funkcji "ImageCreateFromGIF()" i "ImageCreateFromPNG()", ale pierwsza nie obsługuje funkcji "ImagePNG()", a druga "ImageGIF()". Dodatkowo pierwsza wersja GD, mimo że w pełni obsługuje format GIF nie wczyta prawidłowo animowanego GIF'a. Wersja GD2 nie generuje obrazków GIF więc nie ma nad czym się rozwodzić. Reasumując PHP_GD i PHP_GD2 obsługują tylko obrazy statyczne.
Kolejną niedogodnością jest fakt że określone funkcje szerzej omówione w manualu PHP nie działają poprawnie ze starszymi wersjami PHP. Nie powinno nam to jednak przeszkadzać. Wystarczy pracować na PHP 4.3 i wszystkie funkcje będą działać.
Niektórzy twierdzą, że nie powinno się przechowywać grafiki w bazie, bo grafika znacznie obciąża serwer MySQL. Duża ilość obrazków i innych plików faktycznie potrafi znacznie obciążyć bazę. Czasami jednak trzeba się oprzeć na bazie danych.
Obiecałem omówić zapis do bazy, odczyt z bazy i wyświetlanie grafiki wykorzystując PHP_GD2. Słowa dotrzymałem. W tym artykule na tym koniec :-).
|