Работаю над организацией файлового хранилища и сценариев к нему для хранения электронного архива чертежей. Одна из возникших задач – рядом с файлом чертежа в формате CAD должна лежать “картинка”. Для роли картинки выбрал .tiff. Основная причина – необходима поддержка хранения нескольких страниц в одном файле, причём – страниц разного размера.

Как получен .tiff – не тема этой статьи (через сканер, через печать на виртуальный принтер – не важно). Суть в том, что при обработке tiff файла в Microsoft Office Document Imaging, или при “неудачном” сканировании мы имеем файл с 32 bits per pixel и размером под 1Мб даже для формата А4, что недопустимо (хранить то не проблема, а по почте подрядчикам хотя бы 100 чертежей как будем посылать?).

Вот и пришлось написать маленький сценарий на PoSh, который “сожмёт” .tiff файлы.

Пока решал эту задачу трижды пожалел, что не застал dotNetFramework, пока ещё программистом трудился. Уж слишком часто последнее время приходится изучать dotNetFramework при программировании на powerShell. И эта задача – яркий тому пример.

Приведу первое (и неправильное) решение, которое опробовал:

$systemDrawing = [Reflection.Assembly]::LoadWithPartialName("System.Drawing");

( New-Object `
    -TypeName System.Drawing.Bitmap `
    -ArgumentList 'source.tif' `
).Save( `
    'destination.tif' `
    , ( `
        [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() `
        | ? { $_.FormatID -eq [System.Drawing.Imaging.ImageFormat]::Tiff.guid } `
    ) `
    , ( `
        New-Object -TypeName System.Drawing.Imaging.EncoderParameters `
        | %{ `
            $_.Param = New-Object -TypeName System.Drawing.Imaging.EncoderParameter -ArgumentList `
                ([System.Drawing.Imaging.Encoder]::ColorDepth) `
                , 24 `
            ;
            $_;
        }
    ) `
);

Даже на этот код потратил прилично времени. Этот код загружает картинку из файла ‘source.tif’, и сохраняет её в ‘destination.tif’, изменяя при этом глубину цвета – 24 бита. С таким же успехом, как Вы понимаете, можно сохранить картинку и в другой формат, поддерживаемый dotNetFramework.

Таким путём пошёл в надежде, что смогу указать в качестве ColorDepth – 1, и задача будет решена. А не тут то было. Можно указать 24, можно 32, но до 1 бита не удастся опуститься. Необходимо изменить не глубину цвета, а сам формат картинки и её палитру. И для этого потребовались совершенно другие классы.

Итак, вторая редакция кода (ещё полдня убил):

$presentationCore = [Reflection.Assembly]::LoadWithPartialName("PresentationCore");

$image = new-object -typeName System.Windows.Media.Imaging.BitmapImage -argumentList `
    ( new-object -typeName System.URI -argumentList `
        'source.tif' `
    ) `
;

if ( $image.Format.BitsPerPixel -gt 1 ) {
    $encoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapEncoder;

    $newImage = new-object -typeName System.Windows.Media.Imaging.FormatConvertedBitmap -argumentList `
        $image `
        , ([System.Windows.Media.PixelFormats]::BlackWhite) `
        , ([System.Windows.Media.Imaging.BitmapPalettes]::BlackAndWhite) `
        , 1.0 `
    ;
    $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create($newImage) );

    $desfFile = new-object -typeName System.IO.FileStream -argumentList `
        "destination.tif" `
        , ([System.IO.FileMode]::Create) `
    ;
    $encoder.Compression = [System.Windows.Media.Imaging.TiffCompressOption]::Ccitt3;
    $encoder.Save( $destFile );
    $destFile.Close();
};

И этот код работает! Но некоторых .tiff файлах (как правило – сохранённых Microsoft Office Document Imaging) получал исключение – не удалось загрузить файл. Применил такой вот обходной маневр:

$systemDrawing = [Reflection.Assembly]::LoadWithPartialName("System.Drawing");
$presentationCore = [Reflection.Assembly]::LoadWithPartialName("PresentationCore");

$temp = New-Object -TypeName System.IO.MemoryStream;

(New-Object `
    -TypeName System.Drawing.Bitmap `
    -ArgumentList 'source.tif' `
).Save( `
    $temp `
    , ([System.Drawing.Imaging.ImageFormat]::Tiff)
);

$image = new-object -typeName System.Windows.Media.Imaging.BitmapImage;
$image.BeginInit();
$image.StreamSource = $temp;
$image.EndInit();

if ( $image.Format.BitsPerPixel -gt 1 ) {

    $encoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapEncoder;

    $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create(
        ( new-object -typeName System.Windows.Media.Imaging.FormatConvertedBitmap -argumentList `
            $image `
            , ([System.Windows.Media.PixelFormats]::BlackWhite) `
            , ([System.Windows.Media.Imaging.BitmapPalettes]::BlackAndWhite) `
            , 1.0 `
        )
    ) );

    $destFile = new-object -typeName System.IO.FileStream -argumentList `
        "destination.tif" `
        , ([System.IO.FileMode]::Create) `
    ;
    $encoder.Compression = [System.Windows.Media.Imaging.TiffCompressOption]::Ccitt4;
    $encoder.Save( $destFile );
    $destFile.Close();

};

На приведённый выше вариант натолкнули в форуме на microsoft.com. Однако, этот вариант не обработает многостраничные .tiff файлы, точнее – сохранит только первую страницу. А для моей задачи это недопустимо.

Поэтому пришлось включи мозги и фантазию, родил следующий вариант:

$systemDrawing = [Reflection.Assembly]::LoadWithPartialName("System.Drawing");
$presentationCore = [Reflection.Assembly]::LoadWithPartialName("PresentationCore");

$sourceFile = new-object -typeName System.IO.FileStream -argumentList `
    ('source.tif') `
    , ([System.IO.FileMode]::Open) `
;

$decoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapDecoder -argumentList `
    ( $sourceFile ) `
    , ( [System.Windows.Media.Imaging.BitmapCreateOptions]::None ) `
    , ( [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad ) `
;
$encoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapEncoder;

foreach ($frame in $decoder.Frames) {
    $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create(
        ( new-object -typeName System.Windows.Media.Imaging.FormatConvertedBitmap -argumentList `
            ($frame.Clone()) `
            , ([System.Windows.Media.PixelFormats]::BlackWhite) `
            , ([System.Windows.Media.Imaging.BitmapPalettes]::BlackAndWhite) `
            , 1.0 `
        )
    ) );
};

$sourceFile.Close();

$destFile = new-object -typeName System.IO.FileStream -argumentList `
    ('source.tif') `
    , ([System.IO.FileMode]::Create) `
;
$encoder.Compression = [System.Windows.Media.Imaging.TiffCompressOption]::Ccitt4;
$encoder.Save( $destFile );
$destFile.Close();

И только этот вариант я могу считать более менее правильным. Приведённый выше код корректно “обновляет” файл на месте (то есть изменяется исходный файл, а не создаётся новый). Для этого пришлось явно использовать файловые потоки и использовать кэширование при загрузке для декодера (без кэширования файл необходим до окончания процедуры записи). И этот код прекрасно обрабатывает многостраничные .tiff файлы.

Ну а теперь “причешем” код – оформим модули и функции. Первый – ITG.Imaging.MDI.psm1. Как уже понятно, этот модуль даёт нам возможность конвертации .mdi файлов в .tiff. Теперь – модуль с функцией обработки .tiff – ITG.Imaging.TIFF.psm1 (так же ссылка на svn архив):

$systemDrawing = [Reflection.Assembly]::LoadWithPartialName("System.Drawing");
$presentationCore = [Reflection.Assembly]::LoadWithPartialName("PresentationCore");

function Copy-Tiff2TiffBlackWhite {
    <#
        .Synopsis
            Преобразует TIFF файл в монохромный TIFF
        .Description
            Преобразует TIFF файл в монохромный TIFF
        .Parameter sourceFile
            Исходный файл (объект).
        .Parameter destination
            Полный путь (к папке), в который будет сохранён полученный .tiff файл.
            Если параметр не указан, будет создан файл с тем же именем
            в каталоге исходного файла
        .Parameter newName
            Имя создаваемого файла.
            Если параметр не указан, будет создан файл с тем же именем
        .Example
            Обработка файлов каталога с их перезаписью "на месте":
            dir 'c:\temp\*.tif' | Convert-Tiff2TiffBlackWhite
    #>
    
    param (
        [Parameter(
            Mandatory=$true,
            Position=0,
            ValueFromPipeline=$true,
            HelpMessage='Исходный файл (объект).'
        )]
        [System.IO.FileInfo] $sourceFile
        , [Parameter(
            Mandatory=$false,
            Position=1,
            ValueFromPipeline=$false,
            HelpMessage='Полный путь (к папке), в который будет сохранён полученный файл.'
        )]
        [System.IO.DirectoryInfo] $destination = ($sourceFile.Directory)
        , [Parameter(
            Mandatory=$false,
            Position=2,
            ValueFromPipeline=$false,
            HelpMessage='Имя создаваемого файла.'
        )]
          [string] $newName = ([System.IO.Path]::GetFileNameWithoutExtension($sourceFile.name) + '.tif')
        , [switch] $PassThru
    )
    
    process {
        $sourceStream = new-object -typeName System.IO.FileStream -argumentList `
            ( $sourceFile ) `
            , ( [System.IO.FileMode]::Open ) `
        ;
        [System.IO.FileInfo] $tempFile = [System.IO.Path]::GetTempFileName();
        [System.IO.FileInfo] $destFile = ( join-path -path $destination.Fullname -childPath $newName );

        $decoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapDecoder -argumentList `
            ( $sourceStream ) `
            , ( [System.Windows.Media.Imaging.BitmapCreateOptions]::None ) `
            , ( [System.Windows.Media.Imaging.BitmapCacheOption]::None ) `
        ;
        $encoder = new-object -typeName System.Windows.Media.Imaging.TiffBitmapEncoder;

        foreach ($frame in $decoder.Frames) {
            $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create(
                ( new-object -typeName System.Windows.Media.Imaging.FormatConvertedBitmap -argumentList `
                    ($frame.Clone()) `
                    , ([System.Windows.Media.PixelFormats]::BlackWhite) `
                    , ([System.Windows.Media.Imaging.BitmapPalettes]::BlackAndWhite) `
                    , 1.0 `
                )
            ) );
        };

        $tempStream = new-object -typeName System.IO.FileStream -argumentList `
            ( $tempFile ) `
            , ( [System.IO.FileMode]::Create ) `
        ;
        $encoder.Compression = [System.Windows.Media.Imaging.TiffCompressOption]::Ccitt4;
        $encoder.Save( $tempStream );
        $sourceStream.Close();
        $tempStream.Close();
        $destFile.Delete();
        $tempFile.MoveTo( $destFile.FullName );
        $tempFile.Delete();

        if ( $PassThru ) { $destFile };
    }
};  

Export-ModuleMember `
    Copy-Tiff2TiffBlackWhite `
;

P.S. Приведённая выше редакция – от 20.03.2012. Предыдущая редакция (смотри примеры выше) не использовала временных промежуточных файлов, но при этом “упала” на файле большого размера – больше 10Мб (формат А1 в 300 dpi всего). Не подразумевает декодер кэш такого размера PowerShell: массовая обработка картинок (“сжимаем” tiff на powershell, grayscale TIFF to black & white) Да и не зачем всё-таки так нерационально память использовать. Поэтому и решил оптимально использовать память и обеспечить возможность обрабатывать файлы с неограниченным по сути количеством страниц (преобразование осуществляется постранично) – через промежуточный файл.

Ну а теперь – пример использования наших модулей:

Import-Module 'C:\work\tools\Common\ITG.Imaging\trunk\ITG.Imaging.MDI' # -force
Import-Module 'C:\work\tools\Common\ITG.Imaging\trunk\ITG.Imaging.TIFF' # -force

get-childItem `
    -path 'X:\Путь к чертежам\Чертежи' `
    -filter '*.mdi' `
| Copy-Mdi2Tiff -passThru `
| Copy-Tiff2TiffBlackWhite `
;    

Согласитесь, так красивее и читабельнее :-).

P.S. для чертежей опробовал разные варианты сжатия, поддерживаемые dotNetFramework. Неоспоримым лидером (повторюсь – для чёрно-белых чертежей) оказался Ccitt4. На нём для себя и остановился.

Наткнулся на обсуждение проблемы неэффективности сжатия из-за dithering, возникающего при конвертации. Предложено решение — “ручная” предварительная обработка яркости точек. Я пока обхожусь без этого (всё-таки чертежи достаточно контрастны).

Отзывы » (3)

  1. Однако, и xps получилось создать приличного размера. Так что и XPS буду пробовать создавать программно сценарием. В таком варианте следует ещё подумать, в каком формате хранить файлы в архиве. Может быть имеет смысл хранить их в xps, преобразуя перед отправкой в tiff.
    - Windows SDK XPS Document samples for Windows 7
    - Introducing APIs for Creating XPS
    - XPS Document API Programming Guide

  2. Dmitry:

    Неужели 
    Ccitt4  лучше подходит для решения вашей задачи чем JBIG2?

    • Вывод для себя сделал следующим образом: взял каталог с 10ю .tiff файлами, и «сжал» их в разные папки разными методами. И по результатам сравнения суммарного объёма разных папок выбрал CCITT4. Но должен повторить — сравнивал только те методы сжатия, которые поддеживаются dotNet framework. К тому же — чертежи обрабатывал, а не произвольные картинки.

Опубликовать комментарий

XHTML: Вы можете использовать следующие HTML теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Tags Связь с комментариями статьи:
RSS комментарии
Обратная ссылка