| Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате |
| Форум программистов > Delphi: Звук, графика и видео > GDI+ отредактировать многостраничный TIFF |
| Автор: AnTeml 1.8.2013, 16:18 | ||||||||
| Здравствуйте! Возникла задача, имеется многостраничный TIFF-файл, нужно на каждой страничке подрисовать "печать" - небольшое изображение. TIFF файлы - отсканированные чертежи и текстовые документы в ч/б, многие многостраничные. Средний размер страницы в пикселах 4000*2516, dpi колеблется от 300 до 600 (причём в одном файле могут находиться страницы, отсканированные в разное время, с разным dpi), файлов очень много, задача запускается периодически оператором Перед тем, как "рисовать" печать нужно определить место для неё, это будет делаться с распознаванием, т.е. для каждой странички нужно будет получить структуру типа TBitmap После недолгих мытарств, была выбрана библиотека GDI+ : как никак microsoft, да и отобразить странички сходу получилось довольно просто. Однако, для решения задачи редактирования и сохранения файла наткнулся сначала на проблемы "как делать?", а потом на самые настоящие загадки. Первая загадка встретилась, как ни странно, на винграде: статья "Использование декодеров и енкодеров GDI+ для загрузки и сохранения изображений", http://forum.vingrad.ru/faq/topic-157721.html В ней описывается достаточно громоздкий способ загрузки в TBitmap uses GDIPAPI, GDIPOBJ, GDIPUTIL; var Bitmap : TBitmap;
Однако, тот же самый результат, т.е. загрузить GpImage в Tbitmap, можно получить буквально парой строк:
Две последних строчки примера "штатно" выгружают GPImage в TBitmap Для чего такие сложности с использованием дополнительных потоков и классов-адаптеров? Быть может, я что-то недопонял? Или это просто альтернативный способ? Сразу оговорюсь, что с графикой windows опыта работы практически нет. Возможно, большинство моих вопросов из-за этого :( Дальше - больше. Теперь с сохранением в файл. Как я понял логику, объект GPImage нужно "листать", делая активной нужную страницу (Frame), и сохранять. Если первая страница - вызывается метод Save, передавая через структуру параметров EncoderValueMultiFrame Если последнующие - SaveAdd, с EncoderValueFrameDimensionPage, по окончании завершить, с EncoderValueFlush В одной из лучшей информации, статьи на эту тему пересохраняют многостраничный PDF в многостраничны TIFF http://www.gnostice.com/nl_article.asp?id=194&t=Convert_PDF_Documents_to_Multipage_TIFF_Images_In_Delphi Однако, оба примера, приведённые в статье, делают это мудрёным способом: Они читают PDF по одной страничке, при этом каждую: 1. сохраняют в объект TempTiff типа TGPimage, этот объект сохраняется в файл на диске (!) сам объект удаляется. 2. Следующим шагом создаётся новый объект AdditionalFrameTiff, имеющий тип тоже GpImage, и он загружается из этого временного файла (!!) Этот объект добавляется как новая страница в MasterTIFF (тоже объект TGPImage). зачем это это делается? Почему не использовать сразу первый объект TempTiff? Просто голова кругом идёт от непонятных сложностей, почему так делают, что я тут не понимаю. Теперь о сложностях, которые не решил: Во-первых, структура TEncoderParameters, которая передаётся при сохранении файла в заголовочном файле она определена так:
Таким образом, более одного параметра передать невозможно. И плевать на значение свойства Count Мне нужно передать два параметра: помимо признака "мультистраничности" - и тип енкодера. Гуглил несколько часов - ничего подходящего не нашёл. На сях видимо параметры определены по-другому, во встреченных примерах на делфи всюду передают только один параметр. Забил "костыль", в заголовочном файле написал: Parameter : array[0..1] of TEncoderParameter; Всё заработало. Но это кривое решение не радует. И остаётся главная проблема. Каким же образом мне "дорисовать" мою печать на конкретной страничке файла? С большими надеждами загрузил TIFF файл не в сам TGPImage, а в его наследника - TGPBitmap заявляются большие возможности, как доступа к пикселам, так и к отрисовке на нём Но после того, как я сделал активной нужную страницу, и попытался что-то сделать с объектом, например, повернуть его - все остальные страницы, кроме текущей, из документа исчезли!
как же это всё барахло работает? Неужели действительно ничего не остаётся, как выгружать каждую страницу в TBitmap, редактировать, затем сохранять в промежуточный файл, потом загружать из файла и добавлять в новый объект? Может, не зря на gnotice.com извращаются именно так, сохраняя pdf в tiff? нет ли хороших статей на тему работы с GDI+ ? Нельзя ли решить мою задачу, изучив какой-нибудь минимум информации, или без дебрей не обойтись? |
| Автор: AnTeml 1.8.2013, 18:15 | ||||||||
В данной ситуации? На входе обеих функций - GPImage, на выходе - только что созданный TBitmap, не связанный ни с каким устройством, в обоих случаях, т.е. функция должна отработать одинаково? Ведь в поток распаковывают тем же декодером, что и отрисовывают..? Ну в принципе, это уже не особо важно. Я в обоих вариантах наткнулся на проблемы. Вариант с промежуточными потоками удивил сразу. функцию оставил как в примере на винграде, даже комментарии, только переименовал её
Почему Image.GetHeight перестало работать? Ведь в функции получения битмапа создаётся класс потока, класс адаптера, сохраняется в битмап и потом все промежуточные объекты удаляются.. Ладно, пробую вариант с отрисовкой:
По всей видимости, эта функция, отрисовывая текущую страницу на Graphics, т.е. в TBitmap, добавляет в объект Image промежуточные распакованные данные, т.к. после удаления Graphics и bmp память не освобождается, и освобождается она только после удаления Image Как принудительно "освободить" Image от этого мусора я не знаю. Похоже, придётся удалять объект Image. Похоже, раскрывается загадка с использованием промежуточного файла. Вы правы! по всей видимости, авторы статьи не нашли иного способа ни изменить тип текущего объекта, ни возможности "правильно" инициализировать объект Image с нужным им форматом, поэтому тупо выгрузили в файл и загрузили из файла объект-страницу заново. И так для каждой страницы... Кто задаёт такую "моду" на такое программирование? Откуда этот геморрой, или мне, далёкому от технических подробностей работы операционки, это лишь кажется геморроем, а на самом деле такие выкрутасы в порядке вещей?
Правильно ли я понимаю, что необходимость в таких нечитабельных костылях возникает из за несоответствия типов данных С++ и Delphi ? В С++ будет прозрачнее, или ещё хуже? |
| Автор: AnTeml 1.8.2013, 19:02 |
| Прошу прощения за эмоции, казалось бы простенькая задача но похоже за пару дней с GDI+ не разобраться, особенно без навыков программирования в windows конкретизирую свой основной вопрос, больше к тем, кто с GDI+ имел дело (если есть такие) каким образом можно загрузить страничку (пусть одну) TIFF из файла (размером ~10000*7000), нарисовать в ней пару линий (в идеале - получить доступ к битовой карте, т.е. прочитать значение пиксела, например, с координатами [10,10] и нарисовать (!) свой пиксел, например, с координатами [20,20] цветом 0), и сохранить её в такой же файл (ну или объект Image), закодировав в таком же формате с теми же параметрами, что и в исходном? Без километрового кода тут не обойтись? |
| Автор: Alexeis 1.8.2013, 19:12 | ||||||||||
Да, это частное решение работает аналогично, возможно даже быстрее.
Потому что внутри CreateBitmapFromGpimage делается ненужная операция удаления объекта Image: TGPImage который ему передали
У меня объект рождался и уничтожался внутри функции. Если объект создается во вне, то он должен также уничтожаться во вне.
Возможно не предполагался такой режим использования. Насколько я помню с библиотекой GDI+ для делфи поставляется вагон примеров. Возможно, авторы библиотеки предполагали иной способ решения задачи. К сожалению с многостраничными картинками я не работал. Не знаю замысла авторов системы. Но перекачка через промежуточные буфера в памяти путем помещения в поток сравнительно быстрая, поскольку она не задействует железо (жесткий диск). Единственные затраты на создание правильных заголовков.
Да, предполагается запись в цикле через переменную. В С++ будет почти также, за исключением того, что компилятор не будет проверять границы диапазонов. В С++ нет способа задать структуру с динамическим массивом так чтобы массив был продолжением структуры. Это даже скорее не С++, а наследие С . В языке С модно было запихивать массив в конец структуры, для того чтобы потом выделять под нее памяти столько сколько нужно всем элементам + полям. |
| Автор: AnTeml 2.8.2013, 15:29 | ||||||||||||||
Перед отрисовкой на битмапе, с использованием Graphics.DrawImage, мне необходимо этот битмап создать. И я его создаю, с настройками "с потолка":
У меня есть много страничек, с разрешением ~10.000*7.000, по умолчанию в памяти создаётся битмап огроменного размера, а то и программа вываливается с сообщением о нехватке памяти. однако в файлах битмапы как минимум монохромны, и что-то там ещё есть, т.о. при запуске вашего решения автоматически памяти под битмап минимум, файл в диспетчере задач занимает десятки мегабайт. Никаких проблем, и думать, какой битмап создавать - не нужно. прошу прощения за "корявость" использования терминов, как я уже писал, я в графике да и в winapi ни в зуб ногой :(
Но похоже, частично победил и методом научного тыка. Но естественно, возникло множество загадок. Прежде всего, очень загадочно работают многие функции в плане, непонятно когда, куда и почему утекает память. К примеру, после упомянутого
Но самое главное - после этих операций ImgTMP перестаёт давать "переключать" свои страницы..... А если я загружаю не в IMAGE, а в наследника - TGPBitmap, делаю текущей какую-то страницу, а затем провожу штатную операцию GPBitmap-а, например, поворачиваю её: (Image as TGPBitmap).RotateFlip(Rotate90FlipNone) то вуаля! страничка повёрнута, но Image.GetFrameCount(FrameDimensionPage) возвращает уже число страниц: 1 (до запуска поворота возвращало 4, что правильно). В общем, в объекте остаётся одна-единственная страничка, над которой было преобразование. Предположу, об этом тоже где-то в документации предупреждается..... В общем, пришлось плюнуть на всё это и использовать классический TBitmap, вкупе с вашими преобразованиями его, посредством потоков. В общем, результат: Процедура, пересохраняющая TIFF файл, добавляющая на каждую страницу нового по случайной линии: Логика следующая: Каждая страница сохраняется в битмап (та самая модифицированная ваша процедура, теперь она называется CreateBitmapFromGpimage), на битмап выводится произвольная линия, из битмапа она сохраняется в файл (процедура по мотивам вашей второй, теперь она называется SaveBmpToTiffFile), из временного файла она загружается в новый ImgChangedFrame := TGPImage.Create(TempFileName), затем этот объект добавляется как новая страничка к сохраняемому:
Логика с использованием временных файлов взята у парней с сайта gnostice, из их статьи: http://www.gnostice.com/nl_article.asp?id=194&t=Convert_PDF_Documents_to_Multipage_TIFF_Images_In_Delphi В процессе разработки стало ясно, почему они использую много временных файлов, по числу страниц в документе: с какого-то перепугу файл то ли блокируется, то ли что (я не проверял), и после создания первого Temp.Tif (объект ImgChangedFrame, который в него сохранял - удаляется!) - второй раз сохранить в этот файл не получается. Наверное он всё-таки заблокировался, я не стал искать причину, сделал по аналогии - N файлов, по числу страниц. Только в этой процедуре не написано их удаление, потому что я таки модифицировал процедуру, используя потоки. Для потоков, как оказалось, нет необходимости создавать целый "пул" временных потоков, достаточно одной переменной, итак, конечный код, привожу целиком:
В принципе, код рабочий. Но либо опять в нём присутствует какая-то глупая ошибка, либо это снова чудеса GDI+, но в нём имеется утечка памяти. Файл создаётся, однако, несмотря на то, что вроде всё удалено, память не освобождается. Причину пока не нашёл :(( если не успею найти, нужно будет прописать это как фичу программы в документации |