Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_5733.html
 

Изображения в SQL Server 2008 Express, с использованием FILESTREAM и WIC

Опубликовано 19.11.2012 г. 23:41

При реализации небольшой программы описания некоторого, скажем, каталога товаров, потребовалось хранить изображения этих самых товаров. Собственно, эта статья и описывает один из множества вариантов, как можно организовать такое хранение.

Так называемый каталог товаров реализован с помощью MS SQL Server 2008 Express. Редакция бесплатная и имеет некоторые ограничения. Одним из таких ограничений является максимальный размер базы данных равный 1 гигабайту. При нашем желании хранить изображения товаров в БД вышеуказанный объем может быть достигнут достаточно быстро. В связи с этим для хранения изображений я решил использовать не обычные BLOB-поля, а поля FILESTREAM, которые появились в 2008й версии сервера (хотя, возможно, огранчение распространяется и на FS-поля). Суть этого модификатора для BLOB полей в том, что фактические данные сохраняются не в таблице БД, а в файловой системе. При настройке FS вы указываете соответствующую папку для хранения. Проектируемая таблица должна иметь два поля: rowguid для хранения идентификатора, и BLOB поля данных. Код для создания подобной таблицы (без учета ограничений) будет примерно таким:

CREATE TABLE [dbo].[ProductImages](
	[id] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
	[ProductID] [int] NOT NULL,
	[ImgNum] [int] NOT NULL,
	[ImageSchema] [varchar](255) NOT NULL,
	[Data] [varbinary](max) FILESTREAM  NULL)

В данном случае ProductID - внешний ключ к таблице каталога, а поля ImgNum & ImageSchema относятся к изображению. К каждому продукту может быть добавлено произвольное число изображений. Каждое новое изображение нумеруется новым ImgNum. Для каждого изображение следует хранить несколько вариантов, в данном случае - оригинал (schema = original) и уменьшенный вариант (preview). Таким образом поле id является первичным ключом, а тройка ProductId, ImgNum, Schema является уникальной.
К FILESTREAM возвращаться больше не будем. Общую информацию (когда использовать и как настривать) можно найти по этой ссылке. Работа с полями, имеющими модификатор FILESTREAM ничем не отличается от работы с обычными blob-полями.
Теперь, задача следующая. При открытии формы редактирования товара, необходимо загрузить preview картинки, уже имеющиеся в БД, а также реализовать загрузку новых изображений. При этом необходимо реализовать генерацию разных по размеру изображений (т.е. уменьшить оригинал до размера preview схемы). Для заливки данных на сервер, помимо ID продукта, нам потребуется название схемы, и поток с бинарными данными изображения. Для этих целей мы опишем тип следующий данных, на основе словаря TObjectDictionary, где ключом является название схемы ImageSchema:

TUploadImageData = TObjectDictionary<string,TStream>;

Также нам потребуется ввести перечисление операций с файлами:

TImageAction = (iaNoAction, iaAdd, iaRemove);

iaNoAction означает, что с уже имеющимся на сервере изображением не происходило никаких действий. При сохранении информации такие файлы пропускаются. iaAdd означет, что изображение было выбрано пользователем для загрузки. iaRemove означет, что изображение на сервере было помечено к удалению. Для описания информации о каждом изображении мы можем использовать класс TImageData: 

    TImageData = class(TObject)
      strict private
        FUploadData : TUploadImageData;
        FNum : integer;
        FAction : TImageAction;
        FPreview : TWicImage;
      public
        constructor Create(aNum : integer = 0; data : TStream = nil);
        destructor Destroy(); override;
        procedure Load(FileName : string);
        procedure Remove();

        property UploadData : TUploadImageData read FUploadData;
        property Preview : TWicImage read FPreview;
        property ImgNum : integer read FNum;
        property Action : TImageAction read FAction;
    end;
    TProductImages = TObjectList<TImageData>;

Конструктор имеет два параметра: номер изображения, и поток данных. Оба эти параметра используются для создания экземпляров TImageData при загрузке из БД (т.е. которые уже находятся в БД при старте формы редактирования). Используя второй параметр - поток данных полученный из БД, сразу же создается preview изображение. Для этих целей используется класс TWicImage:

constructor TImageData.Create(anum: integer = 0; data: TStream = nil);
begin
    inherited Create();
    FNum := aNum;

    if Assigned(data) then begin
        FPreview := TWicImage.Create();
        FPreview.LoadFromStream(data);
    end;
end;

Для загрузки preview изображений испольузется слеудющий метод InitImages, который является вложенным в обработчике события OnCreate формы редактирования:

    procedure InitImages();
    var num : integer;
        s : TStream;
        idata : TImageData;
    begin
        with query do begin
            sql.Text := 'SELECT ImgNum, data'#13+
                        'FROM ProductImages'#13+
                        'WHERE productId = :pid and ImageSchema = :schema';
            parameters.ParamByName('pid').Value := FId;
            parameters.ParamByName('schema').Value := 'preview';
            open();
            while not eof do begin
                num := fieldByName('ImgNum').AsInteger;
                s := CreateBlobStream(FieldByName('data'), bmRead);

                idata := TImageData.Create(num, s);
                AddImage(iData);

                s.Free();
                next();
            end;
            close();
        end;
    end;

 Здесь метод AddImage - метод формы, предназначеный для создания экземпляров TImage, и добавления их на форму:

procedure TPultEditForm.AddImage(iData: TImageData);
var img : TImage;
begin
    FImages.Add(iData);

    img := TImage.Create(ImagesFlowPanel);
    img.Parent := ImagesFlowPanel;
    img.AlignWithMargins := true;
    img.Tag := NativeInt(iData);
    img.PopupMenu := ImagePopupMenu;
    img.Center := true;

    img.Picture.Assign(iData.Preview);
end;

Как видно в качестве Parent компонента используется TFlowPanel.  Я не  так давно стал использовать в работе TFlowPanel & TGridPanel, но это очень функциональные и эффективные компоненты для размещения элементов. В общем говоря, TFlowPanel автоматически располагает добавляемые элементы. По умолчанию, слева-направо и сверху вниз. В поле tag сохраняем ссылку на исходный экземпляр TImageData, не забывая привести к NativeInt, для совместимости с Win64.

На этом этапе наши исходные изображения из БД загружены и показаны на форме. Следующий шаг - добавление новых изображений. Сложности большой нет, разве что генерация нескольких изображений с разными размерами из оригинала. Масштабирование выполняем с помощью WIC интерфейса  IWICBitmapScaler.
В событии загрузки нового изображения создаем экземпляр TImageData с параметрами по умолчанию: номер изображения равен 0,  поток данных отсуттвует:
 

procedure TPultEditForm.NewImageClick(Sender: TObject);
var fName : string;
    iData : TImageData;
begin
    if not FileOpenDialog.Execute() then exit;
    fName := FileOpenDialog.FileName;

    iData := TImageData.Create();
    iData.Load(fName);
    AddImage(iData);
end;

Вся основная работа переложена в метод TImageData.Load(). Здесь мы устанавливаем FAction в iaAdd, указывая на то, что изображение следует загрузить на сервер. Создаем файловый поток TFileStream и используем его для загрузки оригинала изображения с диска. Для хранения preview изображения создаем поток в памяти. Оба потока добавляем в FUploadData с указанием соответствующих схем. Далее для генерации уменьшенного изображения создаем экземпляр TWicImage, и загружаем в него оригинал. Используя WIC-фабрику, получаем ссылку на объект IWicBitmapScaler. Вычисляем новую высоту изображения, исходя из того что уменьшение будет пропорциональным, и ширину мы фиксируем в 100 пикселей. Далее, инициализируем scaler с заданными размерами, и методом сглаживания. Теперь остается получить новый экземпляр IWICBitmap с новыми размерами, и заменить изображение в TWicImage. После этого полученное изображение сохраняем в поток:

procedure TImageData.Load(FileName : string);
var fs : TFileStream;
    ms : TMemoryStream;
    scaler : IWICBitmapScaler;
    wicBitmap : IWICBitmap;
    newHeight : integer;
begin
    FAction := iaAdd;

    FUploadData := TUploadImageData.Create();

    fs := TFileStream.Create(fileName, fmOpenRead);
    ms := TMemoryStream.Create();

    FUploadData.AddOrSetValue('original', fs);
    FUploadData.AddOrSetValue('preview',  ms);

    FPreview := TWicImage.Create();
    FPreview.LoadFromStream(fs);

    FPreview.ImagingFactory.CreateBitmapScaler(scaler);
    newHeight := round(100* FPreview.height /FPreview.Width);
    scaler.Initialize(FPreview.Handle, 100, newHeight, WICBitmapInterpolationModeNearestNeighbor);
    FPreview.ImagingFactory.CreateBitmapFromSource(scaler, WICBitmapNoCache, wicBitmap);

    if assigned(wicBitmap) then
        FPreview.Handle  := wicBitmap;

    FPreview.SaveToStream(ms);


    FUploadData['original'].Position := 0;
    FUploadData['preview'].Position  := 0;
end;

При сохранении результатов выполняем необходимые действия для каждого из изображений. Если FAction установлена в iaRemove, то выполянем запрос на удаление всех изображений в БД для указанного ProductID и ImgNum (т.е. всех схем с одним изображением). При добавлении (iaAdd) загружаем данные из потоков для каждой схемы и отправляем их на сервер:

procedure TPultEditForm.SaveImages();
var imgNum : integer;
    img : TImageData;
    schema : string;
    dp : TParameter;
begin
    query.SQL.text := 'SELECT (COALESCE(max(ImgNum),0) + 1) AS newNum '#13+
                      'FROM ProductImages WHERE ProductId = :pid';
    query.Parameters.ParamByName('pid').Value := FId;
    query.Open();
    imgNum := query.FieldByName('newNum').AsInteger;
    query.Close();

    for img in FImages do begin
        case img.Action of
            iaAdd: begin
                for schema in img.UploadData.Keys do begin
                    query.SQL.Text := 'INSERT INTO dbo.ProductImages'#13+
                                      '       (ProductId, ImgNum, ImageSchema, data)'#13+
                                      'VALUES (:pid, :imgNum, :schema, :data)';

                    with Query.parameters do begin
                        ParamByName('pid').Value := FId;
                        ParamByName('imgNum').Value := imgNum;
                        ParamByName('schema').Value := schema;
                        dp := ParamByName('data');

                        dp.LoadFromStream(img.UploadData[schema], ftBlob);
                    end;
                    Query.ExecSQL();
                end;
                inc(imgNum);
            end;
            iaRemove : begin
                query.SQL.Text := 'DELETE FROM ProductImages '#13+
                                  'WHERE ProductId = :pid and ImgNum = :num';
                query.Parameters.ParamByName('pid').Value := FId;
                query.Parameters.ParamByName('num').Value := img.ImgNum;

                query.ExecSQL();
            end;
        end;
    end;
end;

В результате получилось нечто следующее:


 

Метки:  bitmap  |  WIC  |  SQL  |  ADO 

Комментарии

Александр
20.11.2012 в 07:25
Ограничение всегда было 4 гига на базу, в редакции 2008 R2 Express увеличили до 10 гигабайт... Ограничение в 1 гигабайт это на использование памяти.
teran
20.11.2012 в 11:04
Да, вы правы. 1 Гбайт по пямяти и 10 на размер БД (сравнение функционала редакций - http://msdn.microsoft.com/en-us/library/cc645993%28v=SQL.110%29.aspx). Изучал эти ограничения год назад, видимо попуталось в памяти.
Александр
19.08.2015 в 14:32
Ограничение на размер экспресса можно обойти. Костыль конечно, но можно..
direk
11.02.2014 в 01:15
teran, здравствуйте.
Если вы еще здесь бываете, разъясните один момент, пожалуйста.
На последним скриншоте (там, где окно вашей программы), видно в окне, где рисунки, колесико прокрутки. Если Вы использовали действительно TFlowPanel, как вам удалось этого добиться, так как вроде бы TFlowPanel scrollbar не поддерживает.
Для меня это очень актуальный вопрос. Заранее спасибо за ответ.
ter
15.02.2014 в 15:41
в данном случае FlowPanel находится внутри ScrollBar, и прокрутка именно от него. А FlowPanel имеет Align=alTop и свойства AutoSize и AutoWrap выставленs в true. Таким образом он сам по себе изменяет размеры вертикально, а скроллбар рисует прокрутку.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно