menu
  Home  ==>  articles  ==>  bdd  ==>  interbase  ==>  interbase_blobs   

blobs_interbase - John COLIBRI.

  • résumé : techniques de lecture et d'écriture de champs Blob Interbase (texte, image, binaire).
  • mots clé:blob - base de données - serveur Sql - Interbase - tutoriel - flux - tStream
  • logiciel utilisé: Windows 98, Delphi 5.0
  • matériel utilisé: Pentium 500Mhz, 128 M de mémoire
  • champ d'application: Delphi 1 à 6 sur Windows, Kylix
  • niveau: débutant en Pascal, Delphi et Interbase
  • plan:


1 - Introduction

Dans l'utilitaire d'analyse de fréquentation de notre site présenté dans l'article les_logs_http_bruts, nous avions utilisé des fichiers ayant des fiches de taille variable.

Rappelons succintement que chaque page visitée laisse dans un log du serveur une ligne contenant des éléments fixes (date, heure etc) et des éléments variables (URL, page demandée, paramètres de la requête HTTP). A titre d'exemple, sur les 17.163 requêtes intervenues entre le 8 Décembre 2001 et le 5 Janvier 2002, la taille de la page demandée variait entre 8 et 107 caractères, avec une médiane à 14 caractères:



Au lieu d'utiliser une solution "ad hoc", nous aurions pu employer Interbase, le bénéfice étant les possibilités de tris, d'extractions etc. Pour gérer avec Interbase les chaînes de taille variable, nous pouvons utiliser:

  • soit des colonnes de taille fixe (CHAR), avec les inconvénients évidents des formats fixes pour stocker des données de taille variable (surdimensionnement ou troncature)
  • soit des chaînes de taille variables (VARCHAR), qui sembleraient dans ce cas idéales. Leur programmation ne présente aucune difficulté
  • soit des Blobs. Leur utilisation ici est un luxe inutile, mais est un prétexte pour l'examen de ces Blobs.

2 - Architecture

2.1 - Les Blobs

Les Blobs Interbase sont mis en oeuvre de la façon suivante:
  • une fiche peut contenir 0 ou plusieurs champs Blobs
  • pour une fiche, chaque champ Blob est en fait un identificateur du Blob. La fiche elle-même ne contient pas les données du Blob, mais 8 octets permettant de retrouver le Blob. L'identificateur de Blob occupe 8 octets:
    • 4 octets identifient la table Interbase (0 pour les Blobs temporaires).
    • 4 octets identifient chaque Blob d'une même table
  • les données des Blobs d'une Base sont stockés dans le ficher même de la base (ils ne sont pas placés, comme pour dBase, dans un fichier séparé), mais pas dans la même page que l'enregistrement.
    Les pages de Blob sont hiérarchisées:
    • les blobs de niveau 0 sont stockés dans une seule page. Pour une page de 4K, cela correspond à environ 4000 octets (chaque page ayant une en-tête)
    • les blobs de niveau 1 utilisent plus d'une page, la page initiale contenant des pointeurs vers les pages suivantes
    • il existe finalement des blobs de niveau 2, ayant 2 niveau de pointeurs
En résumé, l'ensemble a l'allure suivante:

2.2 - Les API

Nous ne présenterons pas en détail dans cet article comment manipuler les Blobs à l'aide des API Interbase, car il faudrait présenter toute la machinerie de ce type de programmation (poignée de la base, poignée de transaction, xsqlda etc.). Néanmoins il est intéressant d'avoir une idée de la mécanique sous-jacente.

Voyons tout d'abord comment ajouter un enregistrement comportant un Blob à une table. Pour une table contenant des données usuelles (entier, Double, etc), l'instruction SQL d'ajout d'un enregistrement est du type:

 INSERT INTO clients
     (c_id, c_string)
     VALUES (123, "Delphi")

que nous exécutons avec des instructions Delphi du type:

PROCEDURE add_record(p_idIntegerp_stringStringp;
  BEGIN    
    WITH Query1Sql DO
    BEGIN
      Clear;

      Add('INSERT INTO clients ');
      Add('  (c_id, c_string)');
      Add('  VALUES ('IntToStr(p_id)+ ', "'p_string'")');

      ExecSql;
    END;
  END;

Il est aussi possible d'utiliser une requête SQL paramétrée:

 INSERT INTO clients
     (c_id, c_string)
     VALUES (123, :c_string)

correspondant au code Delphi suivant:

PROCEDURE add_record(p_idIntegerp_stringStringp;
  BEGIN    
    WITH Query1Sql DO
    BEGIN
      Clear;

      Add('INSERT INTO clients ');
      Add('  (c_id, c_string)');
      Add('  VALUES ('IntToStr(p_id)+ ', :c_string)');

      ParamByName('c_string').AsString:= p_string;    

      ExecSql;
    END;
  END;

Lorsque la table contient des Blobs, Interbase exige que nous utilisions des requêtes paramétrées. La syntaxe SQL ne prévois en effet aucun subterfuge pour indiquer la valeur binaire d'une zone de taille quelconque. En revanche, lorsque nous utilisons des requêtes paramétrées, nous fournissons des pointeurs vers les données, et cela permet, entre autres, les traitements de Blobs.

Prenons alors le cas d'une table contenant deux colonnes:

c_id: une clé numérique unique
c_blob: un blob

L'instruction SQL d'ajout ressemblera alors à:

 INSERT INTO clients
     (c_id, c_string)
     VALUES (:c_id, :c_blob)

Supposons que nous démarrions avec la situations suivante:

Pour insérer un nouvel enregistrement à l'aide des API, nous devrons:
   initialiser les variables utilisée par la requête:
  • une String contenant notre requête
  • des variables correspondant à :c_id et :c_blob

   écrire les octets du Blob en premier:
nous récupérons une "poignée de blob" et un "identificateur de blob" qui est stocké dans la structure gérant :b_blob:

    isc_create_blob(..., blob_handle, :c_blob, ...)

   nous utilisons la poignée pour transférer les octets

    isc_put_segment(..., blob_handle, length, pt_data);

   écrire l'enregistrement en exécutant INSERT INTO:

    isc_dsql_execute_immediate(... sql_request_string, ..., :c_id et :b_blob...)

3 - Utilisation en Delphi

3.1 - Définition d'un champ Blob

Nous allons commencer par créer une table en utilisant CREATE TABLE pour cela:
   créez une nouvelle application
   placez une tDatabase sur la tForm et initialisez:
  • DriverName IBLocal
  • DatabaseName à ma_base
  • ouvrez l'éditeur de tDataBase en cliquant deux fois dessus et initialisez:
    • USER NAME=SYSDBA
    • PASSWORD=masterkey
    Supprimez aussi l'encoche "Invite de Connection"
  • basculez Connected à True
   placez un tQuery sur la tForm et modifiez:
  • DataBaseName à ma_base
   utilisez un tButton pour créer la table:
  • placez un tButton, nommez-le create_table_, et créez sa méthode OnClick
  • voici le code de cette méthode:

        procedure TForm1.create_table_Click(SenderTObject);
          begin
            with Query1Sql do
            begin
              Close;
              Clear;
              Add('CREATE TABLE clients');
              Add('   (                         ');
              Add('       c_id NUMERIC(5) NOT NULL PRIMARY KEY');
              Add('     , c_blob BLOB SUB_TYPE 0');
              Add('   )                         ');

              ExecSql;
            end// with Query1, Sql
          end// create_table_Click


En parallèle, pour pouvoir recommencer nos essais, nous prévoyons de supprimer la table:

    procedure TForm1.drop_table_Click(SenderTObject);
      begin
        with Query1Sql do
        begin
          Close;
          Clear;
          Add('DROP TABLE clients');

          ExecSql;
        end// with Query1, Sql
      end// drop_table_Click


3.2 - Transfert de texte par LoadFromFile, SaveToFile

Nous allons commencer par placer dans un champ Blob le contenu d'un fichier. Pour cela:
  • il FAUT utiliser des requêtes paramétrées
  • la valeur du paramètre est chargée par tBlobField.LoadFromFile
Par conséquent:
  • nous plaçons sur disque un fichier ASCII contenant quelques lignes. Le fichier STAGES.TXT contiendra, par exemple:

    Stage Initiation Pascal
    Stage Perfectionnement Pascal
    Stage Initiation Delphi
    Stage Bases de Données Delphi
    Stage Midas

  • voici la procédure qui ajoute un enregistrement comportant donc un entier et le texte de notre fichier:

        procedure insert_with_file(p_idIntegerp_file_nameString);
          begin
            with Form1Query1Sql do
            begin
              Clear;

              Add('INSERT INTO clients (c_id, c_blob)');
              Add('  VALUES ('IntToStr(p_id)+ ', :c_blob'')');

              ParamByName('c_blob').LoadFromFile(p_file_nameftBlob);

              ExecSql;
            end// with Query1, Sql
          end// insert_with_file

    et l'événement qui écrit le flux est simplement:

        procedure TForm1.insert_with_file_Click(SenderTObject);
          begin
            insert_with_file(103, k_file_pathk_load_file_name);
          end// insert_with_file_Click

  • et voici la relecture de cet enregistrement, avec, à titre d'exemple, sauvegarde du blob dans un fichier sous un autre nom:

        procedure select_with_file(p_idIntegerp_file_nameString);
          begin
            with Form1Query1Sql do
            begin
              Close;
              Clear;
              Add('SELECT * FROM clients');
              Add('  WHERE c_id='IntToStr(p_id));
              Open;

              (FieldByName('c_blob'as tBlobField).SaveToFile(p_file_name);
            end// with Form1, Query1, Sql
          end// select_with_file

    et l'événement correspondant:

        procedure TForm1.select_with_file_Click(SenderTObject);
          begin
            select_with_file(103,  k_file_pathk_save_file_name);
          end// select_with_file_Click

3.3 - Utilisation de tStream

Le transfert entre des fichiers classiques et les Blobs fonctionne parfaitement, mais manque de souplesse. Si le blob contient du texte ASCII, l'utilisation la plus courante de ce texte est son affichage à l'écran. De même les images ont en général la vocation d'être présentées à l'utilisateur plutôt que stockées uniquement sur disque.

C'est à ce niveau que les flux (Stream) s'avèrent fort utiles.

Nous avons présenté les flux (principe, les flux abstrait, mémoire, disque, traitements de base, manipulations de blobs des bases "DeskTop" à l'aide des flux) dans le livre <%%livre_Delphi_dBase>, chapitre 11, pages 742 à 792.

Nous ne reprendrons pas cette présentation exhaustive ici. Seuls les traitements nécessaires pour la manipulation des Blobs Interbase seront abordés.

L'avantage essentiel des flux est de fournir un dénominateur commun pour les transferts de données:

  • provenant de plusieurs origine (mémoire, fichier disque, presse papier, String)
  • ayant des formats différents (ASCII, tBitMap, tForm .DFM binaire Delphi)
Dans le cas le plus simple du format ASCII, nous avons schématiquement:

Ce mécanisme universel fonctionne à partir du moment où chaque CLASS participant au transfert est dotée de méthodes SaveToStream et LoadFromStream.

3.4 - Blobs et tStream

Voici donc l'exemple précédent utilisant à présent les flux:
  • le chargement d'un fichier dans un Blob se fait par:

        procedure insert_with_stream(p_idIntegerp_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Clear;

              Add('INSERT INTO clients (c_id, c_blob)');
              Add('  VALUES ('IntToStr(p_id)+ ', :c_blob'')');

              ParamByName('c_blob').LoadFromStream(p_c_streamftBlob);

              ExecSql;
            end// with Query1, Sql
          end// insert_with_stream

    et l'événement qui écrit le flux est simplement:

        procedure TForm1.insert_with_stream_Click(SenderTObject);
          var l_c_memory_streamtMemoryStream;
          begin
            l_c_memory_stream:= tMemoryStream.Create;
            Try
              l_c_memory_stream.LoadFromFile(k_file_pathk_load_file_name);
              l_c_memory_stream.Seek(0,0);
              insert_with_stream(104, l_c_memory_stream);
            finally
              l_c_memory_stream.Free;
            end;
          end// insert_with_stream_Click

  • et voici la relecture de cet enregistrement, avec, à titre d'exemple, sauvegarde du blob dans un fichier sous un autre nom:

        procedure select_with_stream(p_idIntegerp_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Close;
              Clear;
              Add('SELECT * FROM clients');
              Add('  WHERE c_id='IntToStr(p_id));
              Open;

              (FieldByName('c_blob'as tBlobField).SaveToStream(p_c_stream);
            end// with Query1, Sql
          end// select_with_stream

    cette procédure est appelée par:

        procedure TForm1.select_with_stream_Click(SenderTObject);
          var l_c_memory_streamtMemoryStream;
          begin
            l_c_memory_stream:= tMemoryStream.Create;
            select_with_stream(104, l_c_memory_stream);
            l_c_memory_stream.SaveToFile(k_file_pathk_save_file_name);
            l_c_memory_stream.Free;
          end// select_with_stream_Click

Nous avons séparé les procédures qui effectuent le transfert des événements, ce qui permet de réutiliser la procédure, comme pour cet exemple avec insertion / sélection multiple qui utilise des tStringStream. Nous allons ici charger dans une table les lignes d'un fichier ASCII "FORMATION_INTERBASE.TXT":
  • voici l'événement qui insère plusieurs enregistrements:

        procedure TForm1.insert_many_with_stream_Click(SenderTObject);
          var l_c_string_listtStringList;
              l_idInteger;
              l_lineString;
              l_c_string_streamtStringStream;
          begin
            l_c_string_list:= tStringList.Create;
            l_c_string_list.LoadFromFile(k_unit_path'u_blobs_ib.pas');

            for l_id:= 0 to 10 do
            begin
              l_line:= l_c_string_list[l_id];
              l_c_string_stream:= tStringStream.Create(l_line);
              insert_with_stream(1000+ l_idl_c_string_stream);
              l_c_string_stream.Free;
            end;

            l_c_string_list.Free;
          end// insert_many_with_stream_Click

  • Quant à la relecture:

        procedure TForm1.select_many_with_stream_Click(SenderTObject);
          var l_c_string_streamtStringStream;
              l_idInteger;
          begin
            l_c_string_stream:= tStringStream.Create('');

            for l_id:= 0 to 10 do
            begin
              l_c_string_stream.Size:= 0;
              select_with_stream(1000+ l_idl_c_string_stream);

              display(l_c_string_stream.DataString);
            end;

            l_c_string_stream.Free;
          end// select_many_with_stream_Click


Notez que la procédure insert_with_stream utilise un paramètre formel de type tStream, et que le paramètres actuel peut être n'importe quel descendant de tStream, que ce soit un tMemoryStream ou un tStringStream.

La forme a l'allure suivante:




3.5 - Données arbitraires

Les Blobs ont un format libre, et nous pouvons, comme dans les FILE ou les tStream, stocker des octets sans que le format nous soit imposé, sachant que la lecture devra respecter le format utilisé pour l'écriture (même taille, dans le même ordre).

Voici un exemple dans le quel nous plaçons dans le Blob un entier, un réel, une date et une chaîne:

  • écriture des données:

        procedure TForm1.insert_binary_with_stream_Click(SenderTObject);
          var l_c_memory_streamtMemoryStream;
              l_my_integerInteger;
              l_my_doubleDouble;
              l_my_datetDateTime;
              l_my_booleanBoolean;
              l_my_stringString;
              l_string_lengthInteger;
          begin
            l_c_memory_stream:= tMemoryStream.Create;

            l_my_integer:= $12340000;
            l_c_memory_stream.Write(l_my_integer, 4);

            l_my_double:= 3.14;
            l_c_memory_stream.Write(l_my_doubleSizeOf(l_my_double));

            l_my_date:= Now;
            l_c_memory_stream.Write(l_my_dateSizeOf(l_my_date));

            l_my_boolean:= True;
            l_c_memory_stream.Write(l_my_boolean, 1);

            l_my_string:= 'aha';
            l_string_length:= Length(l_my_string);
            l_c_memory_stream.Write(l_string_length, 4);
            if l_string_length> 0
              then l_c_memory_stream.Write(l_my_string[1], l_string_length);

            l_c_memory_stream.Seek(0,0);
            insert_with_stream(105, l_c_memory_stream);

            l_c_memory_stream.Free;
          end// insert_binary_with_stream_Click

  • lecture des données:

        procedure TForm1.select_binary_with_stream_Click(SenderTObject);
          var l_c_memory_streamtMemoryStream;

              l_my_integerInteger;
              l_my_doubleDouble;
              l_my_datetDateTime;
              l_my_booleanBoolean;
              l_my_stringString;
              l_string_lengthInteger;
          begin
            l_c_memory_stream:= tMemoryStream.Create;
            select_with_stream(105, l_c_memory_stream);

            l_c_memory_stream.Seek(0,0);

            if l_c_memory_stream.Read(l_my_integer, 4)= 4
              then display('Integer: 'IntToStr(l_my_integer));

            if l_c_memory_stream.Read(l_my_doubleSizeOf(l_my_double))= 8
              then display('Double: 'FloatToStr(l_my_double));

            if l_c_memory_stream.Read(l_my_dateSizeOf(l_my_date))= 8
              then display('Date 'DateTimeToStr(l_my_date));

            if l_c_memory_stream.Read(l_my_boolean, 1)= 1
              then
                if l_my_boolean
                  then display('True')
                  else display('False');

            if l_c_memory_stream.Read(l_string_length, 4)= 4
              then
                if l_string_length> 0
                  then begin
                      SetLength(l_my_stringl_string_length);
                      l_c_memory_stream.Read(l_my_string[1], l_string_length);
                      display('String 'l_my_string);
                    end;

            l_c_memory_stream.Free;
          end// select_binary_with_stream_Click


3.6 - Utilisation de contrôles Delphi

Pour le moment nous ne nous sommes guère préoccupés du type des Blobs. Nous nous sommes contentés de transférer des octets, sans que le contenu (le format) des données transférée soit intervenu.

Or Delphi a prévu de gérer de façon spéciales deux cas particuliers très fréquents: le texte ASCII et les images au format BMP.

Dans ces deux cas, Delphi permet de gérer automatiquement l'affichage dans des contrôles visuels liés au champ blob:

  • pour le texte ASCII, Delphi utilise les dbMemo
  • pour les images .BMP, Delphi utilise les dbImage
Pour le texte ASCII, cette gestion exige que:
  • le champ Blob soit déclaré de TYPE 1
  • tBlobField.LoadFromStream utilise le type ftMemo
  • le contrôle visuel soit un dbMemo. Pour pouvoir connecter ce composant, il faut que l'environnement Delphi connaisse le nom de la table.
Voici un exemple avec ces contrôles visuels:
  • la création de la table se fait par:

        procedure TForm1.create_table_Click(SenderTObject);
          begin
            with Query1Sql do
            begin
              Close;
              Clear;
              Add('CREATE TABLE 'k_table_name);
              Add('   (                         ');
              Add('       c_id NUMERIC(5) NOT NULL PRIMARY KEY');
              Add('     , c_ascii BLOB SUB_TYPE 1');
              Add('   )                         ');

              ExecSql;
            end// with Query1, Sql
          end// create_table_Click

  • l'insertion de nouveaux enregistrements se fait par:

        procedure insert_with_stream_ascii(p_idIntegerp_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Clear;

              Add('INSERT INTO 'k_table_name' (c_id, c_ascii)');
              Add('  VALUES ('IntToStr(p_id)+ ', :c_ascii'')');

              ParamByName('c_ascii').LoadFromStream(p_c_streamftMemo);

              ExecSql;
            end// with Query1, Sql
          end// insert_with_stream_ascii

    Notez bien le paramètre ftMemo

  • et pour afficher le texte dans un dbMemo, il faut:
       ajouter une requête dans la propriété tQuery.SQL:

    SELECT * FROM client_ascii

       poser le dbMemo sur la tForm
       donner à DataSource la valeur DataSource1
       donner à DataField la valeur c_ascci
    Le lancement de l'affichage se fait alors par:

        procedure TForm1.select_all_Click(SenderTObject);
          begin
            with Query1Sql do
            begin
              Close;
              Clear;
              Add('SELECT * FROM 'k_table_name);
              Open;
            end// with Query1, Sql
          end// select_all_Click

Voici l'allure de cette application:



Pour la petite histoire, mentionnons un bug qui m'a occupé une heure ou deux (plutôt deux que une d'ailleurs):

  • mes premiers essais avaient été effectués sur des blobs ascii
  • l'utilisation de format binaire a été ajouté à l'article à la fin. La cerise sur le gâteau.
  • cerise qui a coûté bonbon: j'avais utilisé pour écrire le flux le paramètre ftMemo. Comme j'ajoutais un champ numérique 1234 sur 4 octets, les 2 octets de poids fort sont 0 et LoadFromStream s'arrête au premier 0 rencontré en mode ftMemo (ces pChar n'auront-ils dont jamais fini de nous pourrir la vie ?)
    Et l'affichage de tStream.Position ou tStream.Size ne me fut pas de grand secours: le flux était bien de taille 28 (dans mon cas), mais la lecture ne trouvait que 2 octets (les deux premiers de l'entier). Il est d'ailleurs regrettable que:
    • l'écriture du flux dans le Blob ne retourne pas le nombre d'octets effectivement écrits (comme BlockWrite)
    • Delphi ne permette pas d'interroger la taille d'un Blob
Ce qui est intéressant c'est que le changement au niveau Interbase du type 1 (Memo en principe) au type 0 (binaire pur en principe) ne changeait rien. En fait Interbase se moque du type: cet attribut est uniquement placé dans la table pour les logiciels au dessus d'Interbase (Delphi en l'occurrence) qui peuvent ainsi effectuer quelques vérifications ici ou là.

3.7 - Blob et BMP

De façon similaire, Delphi sait afficher les Blobs contenant des données au format .BMP.

Pour faire fonctionner l'exemple:

  • nous avons utilisé FigEdit (notre éditeur vectoriel objet) pour générer quelques .BMP
  • ces .BMP ont été placés dans le répertoire data_for_blobs\images
  • pour automatiser l'insertion de plusieurs blobs, nous avons utilisé une tFileListBox avec un tFilterComboBox: la tListBox contient tous les .BMP, et nous pouvons avec Random changer le fichier qui est chargé
Puis:
  • la table est créée par:

        procedure TForm1.create_table_Click(SenderTObject);
          begin
            with Query1Sql do
            begin
              Close;
              Clear;
              Add('CREATE TABLE 'k_table_name);
              Add('   (                         ');
              Add('       c_id NUMERIC(5) NOT NULL PRIMARY KEY');
              Add('     , c_image BLOB SUB_TYPE 0');
              Add('   )                         ');

              ExecSql;
            end// with Query1, Sql
          end// create_table_Click

  • le chargement d'un BMP dans un tStream se fait par:

        procedure TForm1.insert_many_with_stream_Click(SenderTObject);
          var l_c_file_streamtFileStream;
              l_idInteger;
              l_list_box_indexInteger;
              l_file_nameString;
          begin
            Randomize;
            for l_id:= 101 to 120 do
            begin
              l_list_box_index:= Random(6);

              l_c_file_stream:= tFileStream.Create(k_image_pathl_file_namefmOpenRead);

              insert_with_stream_image(1000+ l_idl_c_file_stream);

              l_c_file_stream.Free;
            end// for l_id
          end// insert_many_with_stream_Click

  • et l'insertion du Blob se fait par:

        procedure insert_with_stream_image(p_idIntegerp_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Clear;

              Add('INSERT INTO 'k_table_name' (c_id, c_image)');
              Add('  VALUES ('IntToStr(p_id)+ ', :c_image'')');

              ParamByName('c_image').LoadFromStream(p_c_streamftBlob);

              ExecSql;
            end// with Query1, Sql
          end// insert_with_stream_image


La tForm a la présentation que voici:




3.8 - Et les GIFs, PNGs, JPEGs ?

L'affichage des .BMP est donc automatique. Las, c'est le seul format automatisé. Pour les autres formats d'image, il faut transférer les données nous-mêmes entre le flux qui a lu le Blob et une tBitMap.

Nous avons donc demandé à FigEdit de sauvegarder quelques images avec le format JPEG (facile :-) ) et PNG ou GIF (un peu moins facile :-( )

Nous allons présenter la mise en place pour les .JPEG, en montrant d'abord comment manipuler les .JPEG.

3.8.1 - .JPEG depuis un fichier

Pour un .JPEG il faut:
  • charger le JPEG dans un tJpegImage (qui nécessite USES jpeg)
  • dessiner tJpegImage dans une tBitMap en dimensionnant la tBitmap au préalable
Voici la procédure qui se charge de l'ensemble:

    procedure TForm1.jpeg_from_file_Click(SenderTObject);
      var l_c_jpeg_imagetJpegImage;
          l_c_bitmaptBitMap;
      begin
        l_c_jpeg_image:= tJpegImage.Create;
        l_c_jpeg_image.LoadFromFile(k_image_path'-pc1.jpeg');

        l_c_bitmap:= tBitmap.Create;
        with l_c_bitmap do
        begin
          Width:= l_c_jpeg_image.Width;
          Height:= l_c_jpeg_image.Height;
          Canvas.Draw(0, 0, l_c_jpeg_image);
        end;

        Image1.Picture.Bitmap.Assign(l_c_bitmap);

        l_c_bitmap.Free;
        l_c_jpeg_image.Free;
      end// jpeg_Click


3.8.2 - .BMP depuis un tStream

En fait notre JPEG va provenir d'un Blob, donc d'un tStream. Alors voici comment afficher une image depuis un tStream:

    procedure TForm1.bmp_from_memory_stream_Click(SenderTObject);
      var l_c_bitmaptBitMap;
          l_c_memory_streamtMemoryStream;
      begin
        l_c_memory_stream:= tMemoryStream.Create;
        l_c_memory_stream.LoadFromFile(k_image_path'-pc1.bmp');

        l_c_bitmap:= tBitmap.Create;
        l_c_bitmap.LoadFromStream(l_c_memory_stream);

        Image1.Picture.Bitmap.Assign(l_c_bitmap);

        l_c_bitmap.Free;
        l_c_memory_stream.Free;
      end// bmp_from_memory_stream_Click


3.8.3 - .JPEG depuis un tStream

En assemblant les deux techniques, nous obtenons:

    procedure TForm1.jpeg_from_memory_stream_Click(SenderTObject);
      var l_c_bitmaptBitMap;
          l_c_memory_streamtMemoryStream;
          l_c_jpeg_imagetJpegImage;
      begin
        l_c_memory_stream:= tMemoryStream.Create;
        l_c_memory_stream.LoadFromFile(k_image_path'-pc1.jpeg');

        l_c_jpeg_image:= tJpegImage.Create;
        l_c_jpeg_image.LoadFromStream(l_c_memory_stream);

        l_c_bitmap:= tBitmap.Create;
        with l_c_bitmap do
        begin
          Width:= l_c_jpeg_image.Width;
          Height:= l_c_jpeg_image.Height;
          Canvas.Draw(0, 0, l_c_jpeg_image);
        end;

        Image1.Picture.Bitmap.Assign(l_c_bitmap);

        l_c_bitmap.Free;
        l_c_jpeg_image.Free;
        l_c_memory_stream.Free;
      end// jpeg_from_memory_stream_Click


3.8.4 - Table contenant des .JPEG

Nous créons donc un table avec des .JPEG. Pour éviter de les afficher par des dbImage (ce qui n'est actuellement pas possible), nous avons ajouté à la Table un champ c_extension que nous pouvons tester avant affichage. Par conséquent:
  • voici la procédure de création de la table:

        procedure TForm1.create_table_Click(SenderTObject);
          begin
            with Query1Sql do
            begin
              Close;
              Clear;
              Add('CREATE TABLE 'k_table_name);
              Add('   (                         ');
              Add('       c_id NUMERIC(5) NOT NULL PRIMARY KEY');
              Add('     , c_extension CHAR(5)');
              Add('     , c_image BLOB SUB_TYPE 0');
              Add('   )                         ');

              ExecSql;
            end// with Query1, Sql
          end// create_table_Click

  • l'insertion se fait par:

        procedure TForm1.insert_jpeg_Click(SenderTObject);
          var l_c_file_streamtFileStream;
              l_itemInteger;
              l_file_nameString;
          begin
            if FilterComboBox1.Text'*.jpeg'
              then
                with FileListBox1Items do
                  for l_item:= 0 to Count- 1 do
                  begin
                    l_file_name:= Items[l_item];

                    l_c_file_stream:= tFileStream.Create(k_image_pathl_file_namefmOpenRead);

                    insert_with_stream_jpeg(2000+ l_item'.jpeg'l_c_file_stream);

                    l_c_file_stream.Free;
                  end // if, with, for l_id
              else display('select *.jpeg or *.png first');
          end// insert_jpeg_Click

    et voici la procédure générale:

        procedure insert_with_stream_jpeg(p_idIntegerp_extensionString;
            p_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Clear;

              Add('INSERT INTO 'k_table_name' (c_id, c_extension, c_image)');
              Add('  VALUES ('IntToStr(p_id)+ ', "'p_extension'"'
                  + ', :c_image'')');

              ParamByName('c_image').LoadFromStream(p_c_streamftBlob);

              ExecSql;
            end// with Query1, Sql
          end// insert_with_stream_jpeg

  • pour afficher UNE image, nous ouvrons la Table et affichons le premier .JPEG. Tout d'abord nous sommes passés par des fichiers disque:

        procedure TForm1.select_display_one_jpeg_Click(SenderTObject);
          var l_c_memory_streamtMemoryStream;
              l_extensionString;
          begin
            DataSource1.OnDataChange:= Nil;
            
            l_c_memory_stream:= tMemoryStream.Create;

            select_jpeg(2000, l_extensionl_c_memory_stream);

            // -- display first image
            l_c_memory_stream.SaveToFile(k_image_path'result\lulu.jpeg');
            Image1.Picture.LoadFromFile(k_image_path'result\lulu.jpeg');

            l_c_memory_stream.Free;
          end// select_display_one_jpeg_Click

    l'ouverture de la table se faisant par:

        procedure select_jpeg(p_idIntegervar pv_extensionString;
            p_c_streamtStream);
          begin
            with Form1Query1Sql do
            begin
              Close;
              Clear;
              Add('SELECT * FROM 'k_table_name);
              Add('  WHERE c_id='IntToStr(p_id));
              display_strings_only(Form1.Query1.Sql);
              Open;

              pv_extension:= FieldByName('c_extension').AsString;
              (FieldByName('c_image'as tBlobField).SaveToStream(p_c_stream);
            end// with Form1, Query1, Sql
          end// select_jpeg


Voici l'affichage d'un .JPEG sur click d'un bouton en transférant le .JPEG du tMemoryStream dans le tJpegImage, puis la tBitMap:

    procedure TForm1.jpeg_from_blob_Click(SenderTObject);
      var l_c_memory_streamtMemoryStream;
          l_c_jpeg_imagetJpegImage;
          l_c_bitmaptBitMap;
      begin
        DataSource1.OnDataChange:= Nil;
        select_all;

        if Query1.FieldByName('c_extension').AsString'.jpeg'
          then begin
              l_c_memory_stream:= tMemoryStream.Create;

              (Query1.FieldByName('c_image'as tBlobField).SaveToStream(l_c_memory_stream);

              l_c_jpeg_image:= tJpegImage.Create;
              l_c_memory_stream.Position:= 0;
              l_c_jpeg_image.LoadFromStream(l_c_memory_stream);

              l_c_bitmap:= tBitmap.Create;
              with l_c_bitmap do
              begin
                Width:= l_c_jpeg_image.Width;
                Height:= l_c_jpeg_image.Height;
                Canvas.Draw(0, 0, l_c_jpeg_image);
              end;

              Image2.Picture.Bitmap.Assign(l_c_bitmap);

              l_c_bitmap.Free;
              l_c_jpeg_image.Free;
              l_c_memory_stream.Free;
            end
          else display('not jpeg');
      end// jpeg_from_blob_Click

la sélection sans filtrage de ligne se faisant par:

    procedure select_all;
      begin
        with Form1Query1Sql do
        begin
          Close;
          Clear;
          Add('SELECT * FROM 'k_table_name);
          Open;
        end// with Form1, Query1, Sql
      end// select_all

Mentionnons que nous avions oublié de rembobinner tMemoryStream (Position:= 0), ce qui explique les étapes prudentes que nous avons utilisé ensuite (et présenté en premier ici). Dans ce jeu de bonneteau avec les tStreams, plus on va doucement, plus on avance vite.

Et il faut dire que les message d'erreur de l'unité JPEG n'ont rien à envier aux "SYNTAX ERROR" chères au BASIC ou aux cris inarticulés poussés par l'interprète PostScript de la Laser Apple.

Finalement nous connectons l'affichage à DataSource.OnDataChange:

    procedure TForm1.DataSource1DataChange(SenderTObjectFieldTField);
      var l_c_memory_streamtMemoryStream;
          l_c_jpeg_imagetJpegImage;
          l_c_bitmaptBitMap;
      begin
        if Query1.FieldByName('c_extension').AsString'.jpeg'
          then begin
              l_c_memory_stream:= tMemoryStream.Create;

              (Query1.FieldByName('c_image'as tBlobField).SaveToStream(l_c_memory_stream);
              l_c_memory_stream.Position:= 0;

              l_c_jpeg_image:= tJpegImage.Create;
              l_c_jpeg_image.LoadFromStream(l_c_memory_stream);

              l_c_bitmap:= tBitmap.Create;
              with l_c_bitmap do
              begin
                Width:= l_c_jpeg_image.Width;
                Height:= l_c_jpeg_image.Height;
                Canvas.Draw(0, 0, l_c_jpeg_image);
              end;

              Image2.Picture.Bitmap.Assign(l_c_bitmap);

              l_c_bitmap.Free;
              l_c_jpeg_image.Free;
              l_c_memory_stream.Free;
            end
          else display('not jpeg');
      end// DataSource1DataChange

Voici la tForm ayant permis ces essais:



Vous noterez au passage les irisations des JPEGs autour des traits: vieux problème inhérent aux compression par Fourier de transitions brutales. Voilà qui explique pourquoi nous avons opté pour PNG, qui offre l'avantage d'une compression sans aucune perte, et permet donc une représentation parfaite de nos dessins au trait (parmi lesquels nos dessins par FigEdit, y compris les diagrammes de syntaxe).


4 - Utilisation Générale

4.1 - Interbase

Au niveau Interbase:
  • A ce qui paraît les données des blobs peuvent être gérées de 3 façons:
    • des flux, qui sont stockés exactement comme le programmeur les fournit
    • les segments, ou les transferts se font par "segments" (un peu comme les FILE OF Pascal où les transferts se font par paquets d'octets correspondant à une fiche). L'intérêt des segments est de pouvoir gérer soi-même les transferts (au lieu de laisser le Serveur récupérer tout le Blob en une fois). Mais pour cela il faut utiliser les API Interbase (isc_xxx)
    • les ARRAY qui sont stockés comme des flux, mais la définition du tableau se trouve au début du flux
    Vu du côté Delphi, ce sont les Blobs qui font le transfert, et nous n'avons pas le choix de la méthode
  • lorsque nous créons la table, nous pouvons aussi spécifier la taille du SEGMENT. L'influence de ce paramètre est semble-t-il peu visible.
  • le manuel Interbase nous présente une riche collection de types de blobs:
    • le type 0 pour les blobs binaires et les images
    • le type 1 pour les données au format ASCII
    • puis viennent une poignée de types correspondant, entre autres, aux code de leur interprète (BLR). Sans intérêt au niveau Delphi
    • finalement l'utilisateur peut "définir ses propres types" qui ont un identificateur négatif. Par exemple -3463. Soit. En fait les blobs de type 0 permettent aussi le stockage binaire, et comme le type n'est pas remonté par les lecture d'enregistrements, et qu'Interbase lui-même se soucie apparemment des types comme d'une guigne (vous pouvez stocker des images avec le type 3 ou 6, ou lire du texte depuis un type -17), cette classification est plutôt anecdotique.
      En fait, si nous souhaitons effectuer des traitements qui dépendent du format stocké, le plus simple est encore de créer une colonne qui contient cette information. C'est ce que nous avons fait pour les .JPEGs.
  • au niveau mémoire disque:
    • l'écriture d'un enregistrement comportant un Blob utilise donc 8 octets par Blob et par enregistrement, que le Blob contienne des données ou non
    • si nous effaçons des blob, l'emplacement utilisé par les données du blob est rendu au récupérateur de mémoire Interbase. La taille du fichier correspondant à la Base ne diminue donc pas. Pour réduire cette taille, il faut effectuer une sauvegarde / restauration (qui réinitialise la FreeList)

4.2 - Scripts

  • les différents langages de scripts fournis avec Interbase ne savent pas traiter les Blobs.
  • il est possible de créer des fonctions utilisateur (User Defined Functions) qui étendent ces scripts. Il est alors possible d'ajouter des UDFs qui savent gérer les Blobs. Le site Borland Community offre une librairie de ce type.
    Comme nous pouvons gérer les Blobs depuis Delphi, nous n'avons pas examiné ces possibilités

4.3 - Delphi

4.3.1 - Type des Blobs

En ce qui concerne le type des Blobs:
  • rappelons que le type d'un Blob n'est fourni que lors de la création de la Table (SUB_TYPE).
  • si nous transférons des données sans utiliser de composant sensible aux données (dbMemo et dbImage), le type du Blob est sans aucune importance
  • si nous utilisons un dbMemo:
    • le type doit être 1
    • la lecture du flux doit se faire avec l'attribut ftMemo
  • si nous utilisons une dbImage
    • le type doit être 0
    • la lecture du flux doit se faire avec l'attribut ftBlob

4.3.2 - Utilisation des API isc_xxx

Nous pouvons utiliser les API Interbase directement, mais la machinerie est plutôt lourde. Moins ahurissante que les dbi_xxx du BDE, mais tout de même... Compte tenu du langage utilisé pour écrire l'un ou l'autre, pas de quoi être surpris.

Ces isc_xxx permettent entre autre de récupérer des informations sur les Blobs (type, taille...).

Si la demande s'en fait sentir nous présenterons ce type de programmation.

4.3.3 - Le type de composant Delphi

Au début de Delphi, le programmeur de bases de données avait fondamentalement deux possibilités:
  • soit une tTable pour dBase
  • soit un tQuery pour toutes les autres bases de données dont le langage natif est SQL
Certes Delphi savait manipuler dBase à partir de tQuery ou générer les appels de tTable pour Oracle, mais cela ajoutait une étape dont le programmeur ne maîtrisait pas toute la finesse.

Puis sont venus les composants dédiés

  • soit par type de Serveur (Access, IBX, IBObjects pour Interbase, DOA pour Oracle, Titan ou autre Apollo pour dBase...)
  • soit pour le type d'architecture (Midas pour les applications multi-niveaux, ADO et MTS)
Le programmeur a donc de nombreuses possibilités, et naturellement autant de risque de ne pas faire au départ "le bon choix".

Notez que je ne suis pas contre cette amélioration des possiblités, mais il faut bien convenir que cette multiplicité nuit à la simplicité.

En ce qui concerne Interbase, nous avons donc employé des tQuery simples.

L'autre choix est l'emploi de composants de la suite Interbase Express (IBX). Bien que Jeff OVERCASH ait fait des miracles pour améliorer leur fiabilité, leur généralisation et se dépense sans compter dans les "news", je n'ai jamais utilisé ces composants. Libre à vous de les utiliser, sachant toutefois qu'ils présentent certaines limitations (le tIbTable qui singe les tTable est semble-t-il redoutable, et redouté).

En ce qui concerne MIDAS, j'utiliser sous LINUX les tSqlQuery avec grand bonheur sous Linux. Mais comme MIDAS sous WINDOWS est toujours encore empégué dans des problèmes de license, j'ai préféré utiliser les tQuery simples pour cet article.


5 - Télécharger le source

Vous pouvez télécharger:

Comme d'habitude:
  • nous vous remercions de nous signaler toute erreur, inexactitude ou problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections qui en résulteront pourront aider les prochains lecteurs
  • tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même les bienvenus à jcolibri@jcolibri.com.
  • plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en cliquant "envoyer" :
    Nom :
    E-mail :
    Commentaires * :
     

  • et si vous avez apprécié cet article, faites connaître notre site, ajoutez un lien dans vos listes de liens ou citez-nous dans vos blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.

6 - Bibliographie

  • Interbase and Blobs - A Technical Overview - Paul BLEACH - IB Phoenix - Nov 2000
    Très bon article général (pas spécifique Delphi). A lire en premier
  • Interbase API Guide - Chapter 7 - Working with Blob Data - p 117 to 148
    Les API pour écrire, lire et modifier des Blobs
  • TI 25364 - July 2000 - mod Oct 2000 - Borland community
    How to insert an InterBase Blob in Delphi using LoadFromFile
    Instructions pas à pas Delphi
  • TI 25238 - July 2000 - mod Oct 2000 - Borland community
    How to retrieve an InterBase Blob in Delphi using SaveToFile
    Instructions pas à pas Delphi
  • InterBase and Blob Fields
    Mer Systems 1998
    Instructions SQL pour manipuler les blobs
  • Bitmaps and InterBase Blob Fields
    Mer Systems 1998
    Instructions SQL et Delphi pour manipuler les blobs contenant des images
  • To Blob or Not To Blob: that is the question - Whether to store strings in Blob, or CHAR or VARCHAR ?
    Ivan PRENOSIL - 2001
    Comparaisons des différents formats texte d'Interbase
  • Delphi dBase - John COLIBRI - Mnemodyne 1996 - 1400 pages
    Les types de champs et les composants dbMemo et dbImage - Les tStream et les champs blobs. Chapitre 11, pages 742 à 792.

7 - Voir aussi

Vous pouvez aussi consulter sur ce site
  • interbase_tutorial: l'initiation à la programmation Sql avec Delphi et Interbase. Un tutorial complet, depuis l'installation en mode local ou Client / Serveur, la création d'une base, la création des tables, l'ajout, lecture, modification de données, le traitement de plusieurs tables
  • Interbase dbExpresss: le mode dbExpress (Delphi 6, dbExpress). Le mode qui permet le mieux de comprendre l'architecture Ado.Net qui en est directement issue
  • Interbase Ibx.Net: le portage sous .Net de la mécanique Ibx (Delphi 8, Ibx.Net). Le moyen le plus simple d'utiliser Interbase et Delphi 8. Contient aussi un comparatif de toutes ces architectures avec les schémas correspondants
  • Interbase dbExpress.Net: le portage sous .Net de la mécanique dbExpress (Delphi 8, dbExpress.Net). L'utilisation des techniques VCL pour Interbase ET pour les autres serveurs (Oracle, Sql Server, MyBase etc)
  • Delphi 8 Ado.Net: sous Windows Forms, en l'absence de couche spécifique à Interbase nous pouvons utiliser une mécanique similaire à Odbc et proche d'Ado sous Win32. Cet article présente ce fonctionnement
  • Interbase Borland Data Provider: la voie royale sous Windows Forms, et pour toutes les base, même pour tous les langages


  • formation_client_serveur_interbase: une formation spécialement dédiée à la programmation Client Serveur avec Delphi et Interbase. Le traitement complet qui en plus du tutorial présente les techniques plus avancées: gestion des erreurs, l'installation et l'analyse du schéma depuis Delphi, les contraintes (clés primaires, intégrité référentielle), les techniques de validation des saisies, les triggers, les procédures cataloguées (tStoredProc), les générateurs
  • vous pouvez aussi vous reporter au livre Delphi dBase qui présente, par le détail, la gestion des composants visuels et les techniques de traitement des données (formatage, vérfications, calculs...)

8 - L'auteur

John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le conseil (composants, architecture, test) et la formation. Son site contient des articles avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client.
Créé: mai-02. Maj: aou-15  148 articles, 471 sources .ZIP, 2.021 figures
Contact : John COLIBRI - Tel: 01.42.83.69.36 / 06.87.88.23.91 - email:jcolibri@jcolibri.com
Copyright © J.Colibri   http://www.jcolibri.com - 2001 - 2015
Retour:  Home  Articles  Formations  Développement Delphi  Livres  Pascalissime  Liens  Download
l'Institut Pascal

John COLIBRI

+ Home
  + articles_avec_sources
    + bases_de_donnees
      + programmation_oracle
      + interbase
        – interbase_blobs
        – interbase_tutorial
        – interbase_dbexpress
        – interbase_ibx_net
        – ib_dbexpress_net
        – delphi_8_ado_net
        – borland_data_provider
        – sql_script_extraction
        – interbase_udf
        – sql_script_executer
        – ib_blob_extraction
        – insert_blob_script
        – ib_stored_procedures
      + sql_server
      + firebird
      + mysql
      + xml
      – paradox_via_ado
      – mastapp
      – delphi_business_objects
      – clientdataset_xml
      – data_extractor
      – rave_report_tutorial
      – visual_livebindings
      – migration_bde
    + web_internet_sockets
    + prog_objet_composants
    + office_com_automation
    + colibri_utilities
    + uml_design_patterns
    + graphique
    + delphi
    + outils
    + firemonkey
    + vcl_rtl
    + colibri_helpers
    + colibri_skelettons
  + formations
  + developpement_delphi
  + présentations
  + pascalissime
  + livres
  + entre_nous
  – télécharger

contacts
plan_du_site
– chercher :

RSS feed  
Blog

Formation Bases de Données Delphi Gestion de bases de données : connexion, accès aux tables, édition d'états - 3 jours
Formation Ecriture de Composants Delphi Creation de composants : étapes, packages, composants, distribution - 3 jours