Часть 3. Реализация некоторых задач.
На главную самоучителя
20.06.2005
Графика.
Определение размеров изображения в графических файлах
JPG (JFIF). Формат JPG (JFIF)-файла.
Скачать исходник примера "infoJFIF"

Предупреждение: Эта статья является результатом сбора разрозненных данных о структуре популярного графического формата JPG и практического анализа структуры JFIF и не основана на стандартах CCITT. Автор не несет ответственности, если приемы, описанные в этой статье не позволят получить корректную информацию о структуре графических файлов JFIF. Однако во всех известных автору случаях, описанный здесь метод позволяет получить совершенно достоверные данные из корректно созданного файла.

В отличие от форматов BMP и GIF, структура графических файлов JPG (JFIF) мягко говоря, значительно сложнее. При работе с такими файлами предполагается его полное считывание, анализ и лишь затем загрузка изображения. Именно так поступают декодировщики JPG. Но мне, наоборт, хочется быстренько узнать размеры графического изображения файла, считав десяток-другой байт (условно). Именно этому вопросу данная статья и посвящается.

Растровый файл JPEG (Joint Photographic Experts Group ).

Во-первых, определимся, что файлы, имеющие расширение JPG, JPEG, JPE являются файлами JFIF (JPEG File Interchange Format) , а JPEG обозначает метод сжатия изображения.
Алгоритм сжатия изображения JPEG очень сложен. Если файлы BMP или GIF сохраняют изображение без потери качества, то алгоритм JPEG при каждом новом сохранении теряет часть малозначительной информации, чтобы достичь максимального сжатия изображения. На глаз такой потери незаметно, однако с каждым новым сохранением файла с использованием кодировщиков (например Photoshop или ACDSee) качество файла ухудшается.
К сожалению, структура файла JFIF такова, что в нем мы не найдем нужных данных по заранее определенным адресам. Однако, давайте посмотрим, что же мы можем сделать, исходя из практического применения некоторой известной информации.
Данные в файле JFIF хранится в сегментах. Начало того, или иного сегмента обозначается маркером. Иными словами, данные записываются в виде потоков, которые идентифицируются маркерами. Приблизительная структура JFIF-файла представлена в таблице 1 (в качестве примера использован файл "photo.jpg" с графическим размером изображения 1197x1165, сжатый с помощью Photoshop до размеров файла 232 kb и включенный в исходник примера):


Таблица 1. Начало файла JFIF.

Информация
SOI
APP0
Длина
Идентификатор JFIF
Номер версии
Ед. Изм.
Плотность пикселей
Возможные данные превью
Номер байта (десятичное)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
...
Пример значения (шестнадцат.)
FF
D8
FF
E0
00
10
4A
46
49
46
00
01
02
00
00
64
00
64
00
00

Первые два байта любого потока JPEG имеют значение маркера SOI (Start Of Image) - FF D8. Именно этими двумя байтами начинается любой файл JFIF. Также однозначно, что любой поток, как и сам файл заканчивается двумя байтами FF D9 - EOI (End Of Image). Таких потоков в файле может быть несколько.

За маркером SOI сразу же следует маркер сегмента приложения APP0 (APPlication 0, т.е. Приложение 0), представляющий собой два байта FF E0.

За ним следуют два байта длины (сначала старший байт, затем младший), которая представляет собой полную длину поля APP0, включая длину самого поля "длина" (2 байта), но без длины маркера APP0. В нашем случае это байты 00 10 или 16 в десятеричной системе (если значение меньше 16, то этот сегмент не JFIF).
После мы видим пять байт, которые представляют буквы JFIF0, заканчивающиеся нулем, которые идентифицируют файл, как JPG - 4A 46 49 46 00.

Следующие два байта имеют значение версии (всегда должно быть 01) и подверсии (от 0 до 2). В нашем случае это 01 02.

Далее следуют следующие значения:
- Единицы измерения плотности (1 байт) (0 - безразмерный (коэффициент), 1 - точки на дюйм, 2 - точки на сантиметр)
- Плотность пикселей по-горизонтали (2 байта)
- Плотность пикселей по-вертикали (2 байта)

Некоторые графические редакторы (например Photoshop) иногда вставляют в тело файла уменьшенное графическое изображение картинки для быстрого получения пользователем информации о содержимом файла (присутствуют только в версиях 1.02 и возможно выше). Но реально большинство кодировщиков ее не используют. Однако следующие байты могут хранить именно информацию об этих превьюшках:
- количество пикселов превью по-горизонтали (2 байта)
- количество пикселов превью по-вертикали (2 байта)
- значения RGB пикселов для превьюшки (3*количество пикселов превью по-горизонтали*количество пикселов превью по-вертикали байт).

Однако в нашем случае файл превью не содержит, поэтому последние 2 байта поля APP0 содержат нули. Однако очень часто уменьшенных графических изображений может быть несколько. Тогда далее бы шли байты для второго уменьшенного графического изображения, а именно, еще бы одно поле APP0, которое имело бы вид:
- маркер APP0 (2 байта)
- длина (2 байта)
- идентификатор JFIF0 (5 байт)
- код расширения (1 байт) - (10 - кодировка методом JPEG, 11 - 1 байт на пиксель, 12 - 3 байта на пиксель)
Затем идет сжатое изображение превьюшки.
Размер поля APP0 не может превышать 64 kb (теоретически).

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

Таблица 2. Используемые маркеры.

Маркер
Шестнадцатеричное значение
Определяемый сегмент
SOF0 (Start Of Frame)
FF C0
начало кадра
SOI (Start Of Image)
FF D8
начало изображения
EOI (End Of Image)
FF D9
конец изображения
SOS (Start Of Scan)
FF DA
начало сканирования
DQT (Definition Quant Table)
FF DB
определение таблицы квантирования
DRI (Definition Restart Interval)
FF DD
определение интервала перезапуска
APP0 (Application 0)
FF E0
сегмент приложения
COM (Comment)
FF FE
комментарии

Информация о размере графического изображения должна храниться в сегменте, идентифицируемом маркером SOF0 - начало кадра. Структура сегмента показана в таблице 3.

Таблица 3. Сегмент SOF0.

Информация
SOF0
Длина
Точность
Высота изображения
Ширина изображения
Кол-во компон.
Данные для каждого компонента
Иден. Дискр. Таб. Иден. Дискр. Таб. Иден. Дискр Таб.
Номер байта (десятичное)
...+1
...+2
...+3
...+4
...+5
...+6
...+7
...+8
...+9
..+10
...+11
...+12
...+13
...+14
...+15
...+16
...+17
...+18
...+19
Пример значения (шестнадцат.)
FF
C0
00
11
08
06
81
04
AD
03
01
22
00
02
11
01
03
11
01

Сегмент начинается с маркера FF C0, прописанного в первых двух байтах.
За ним в двух байтах находится длина сегмента, которая включает длину сегмента за исключением маркера FF C0.
Потом в одном байте лежит точность изображения (то, что вы выставляете при сохранении файла в графическом редакторе, обычно 8).
Далее, начиная с четвертого от маркера байта лежит то, что нам нужно - размер изображения: два байта высота и два байта ширина.

Исходя из описанного выше напрашивается простейший алгоритм определения размера графического изображения файла JFIF: проверить наличие в байтах с 7 по 11 наличие идентификатора JFIF0, чтобы убедится, что это JPG, а затем читать все байты подряд, пока не найдется маркер начала кадра SOF0 (FF CO), и отсчитав от него четыре байта, получить высоту и ширину картинки.
"О, какая фигня", скажешь ты, " все так просто!". Нет, мой пытливый друг. Все далеко не так ... просто. Не польются слезы разочарования из наших, покрасневших от монитора глаз. Не зря едят свой хлеб с маслом и черной икрой наши благодетели из C-Cube Microsystems, сотворившие этот популярнейший формат. Ибо, если мы реализуем этот алгоритм, то может и прочтем один файл из ста, где по недосмотру разработчиков графических редакторов все данные оказались именно на тех местах, где им и положено быть. Потому что по своей кривизне и безбашенности JPG превышает все мыслимые пределы.


Хотя то, что ты прочитал выше - в общем-то по большому счету соответствует истине, использовать эту информацию можно, внеся необходимые поправки на вольности обращения с форматом.
Единственное, что можно считать довольно-таки определенным, так это то, что первые два байта файла JPG содержат маркер начала изображения FF D8, а последние два - маркер конца изображения FF D9. Что касается остального, то:

- совершенно необязательно, что как в начале файла, так и в любом другом месте используется маркер сегмента приложения именно APP0. На самом деле он может иметь значения от APP0 до APP14 (кроме APP15, который должен игнорироваться), т.е. два байта маркера могут содержать значения FF E0, FF E1, FF E2, FF E3, FF E4, ... , FF EE.

- совершенно необязательно, что в любом сегменте APP? идентификатор файла должен содержать символы JFIF0. В первом сегменте APP? это может быть JFXX0 или Exif0, а возможно и еще какие-нибудь. В последующих сегментах APP? идентификатор может содержать практические любые символы, какие нравились разработчикам графического редактора, создавшего этот файл.

- совершенно необязательно, что первый найденные маркер начала кадра SOF0 (FF C0) будет содержать именно нужные нам данные о высоте и ширине графического изображения. Так как обычно кадров в файле несколько, то в большинстве случаев ты найдешь данные о размере уменьшенного изображения (превью) или вообще непотребные числа.

- совершенно необязательно, что в файле вообще будет присутствовать маркер SOF0 (FF C0). Вместо него могут быть использованы маркеры SOF1 (FF C1), SOF2 (FF C2) хотя считается, что он и другие далее не поддерживаются, SOF3 (FF C3), SOF5 (FF C5), SOF6 (FF C6), SOF7 (FF C7), SOF11 (FF CB), SOF13 (FF CD), SOF14 (FF CE) или SOF15 (FF CF).

Что же нам поможет, кто же нас спасет в этой безнадежной ситуации? Спасение утопающих - дело рук самих утопающих. В каждом сегменте APP? сразу вслед за маркером имеются два байта, которые содержат длину этого сегмента. Вот оно! Исходя из предположения, что кадр главного изображения, т.е. нужный нам маркер SOF? следует где-то за последним маркером APP?, то нам надо по длинам APP? пройти все сегменты приложений, а затем искать SOF?. Можно предложить следующий алгоритм решения нашей задачи:

1. Идентификация файла JPG. Исходя из неоднозначности идентификации, возможно целесообразно проверять файл по пяти признакам: наличия маркера начала кадра в начале файла, наличие маркера конца кадра в конце файла, наличие первого маркера APP?, наличия первых двух символов JF и последнего нуля в идентификаторе в первом маркере APP?, наличия трех последних символов IF0 в идентификаторе файла там же. При получении минимум трех любых совпадений можно с большой долей уверенности утверждать, что это JPG. Однако ты можешь проводить идентификацию более просто (или более сложно, хотя сложнее не знаю как).
2. Определение позиции начала поиска SOF? Исходя из длины каждого сигмента APP? можно найти последний и определить номер байта, с которого надо начинать сканирование в поисках нужного нам (последнего в файле) маркера начала кадра SOF?, т.е. сегмента содержащего данные о размерах изображения.
3. Считывание данных. Считывание и перевод шестнадцатеричных данных размеров изображения в десятичное.

Не смотря на все эти страсти-мордасти, реализовать этот алгоритм довольно-таки просто. Создадим новый exe-проект. Сразу пристроим к нему стандартный модуль, в котором находится код функции ConvertDec(), переводящей шестнадцатеричные строки в десятичные числа. Подробно о ней написано в статье "Шестнадцатеричное представление числа. Перевод из шестнадцатеричной системы счисления в десятичную."

Теперь закроем модуль и забудем про него. Напишем пример простого просмоторщика файлов JPG, который будет загружать картинки в Image1, изменяя размер последнего пропорционально размеру изображения, считанному из файла .jpg.
Установим свойство нашей формы ScaleMode=3 Pixels. Разместим на форме Image1 со свойством Stretch=True. Так же нам понадобятся элементы Drive1, Dir1 и File1 для файла JPG. Размер картинки будем выводить в Label1. Когда все это положим на форму, объявим переменные:


Option Explicit
Dim
PathSearch As String 'для определения файла JPG
Dim DiskName As String
Dim
b1 As Byte 'для считывания байта
Dim b2 As Byte 'для считывания байта
Dim FileName As String 'имя JPG-файла
Dim IdentFile As String 'вспомог переменная для считывания нескольких байтов
Dim NomerBaita As Long 'Байт, с которого начинаем сканирование
Dim FileW As Long 'ширина изображения
Dim FileH As Long 'высота изображения
Dim x As Long 'переменная для цикла
Dim Kvadrat As Long 'размер Image1 (квадрат)

Затем пишем процедуры Form_Load, Dir1_Change и Drive1_Change. О работе этих процедур подробно написано в части 1 главе 6 самоучителя.

Private Sub Form_Load()
PathSearch = App.Path
Drive1.Drive = PathSearch
Dir1.Path = PathSearch
File1.FileName = PathSearch
Kvadrat = 300
End Sub

Private Sub Dir1_Change()
File1.Path = Dir1.Path
End Sub

Private Sub Drive1_Change()
DiskName = Drive1.Drive
Dir1.Path = DiskName & "\"
File1.Path = Dir1.Path
End Sub

А вот теперь в процедуре File1_Click (чтобы файл открывался при щелчке по файлу) начнем ваять собственно наш код для чтения JPG-файла:

Private Sub File1_Click()
Dim FlagAPP As Boolean 'флаг присутствия сначала маркера APP*, а потом SOF
Dim FlagSOF As Boolean 'флаг начала кадра
Dim StartByte As Long ' начальный байт работы
Dim DlinaAPP0 As Long 'длина APP*
Dim JFIF As Integer 'количества совпадающих признаков JFIF

If Right(File1.Path, 1) = "\" Then
FileName = File1.Path & File1.FileName
Else
FileName = File1.Path & "\" & File1.FileName
End If

'открываем файл
Open FileName For Binary As #1
'проверяем, нормальный ли это вообще файл
If LOF(1) < 2 Then
Label1 = "Файл испорчен"
Image1.Picture = LoadPicture()
Close
Exit Sub 'если длина файла меньше 2 байт выходим из процедуры
End If
'проверяем наличие EOI в конце файла
Get #1, LOF(1) - 1, b1
Get #1, LOF(1), b2
If b1 = &HFF And b2 = &HD9 Then JFIF = JFIF + 1 'если имеется, увеличиваем количество признаков на 1
'проверяем наличие SOI в начале файла
Get #1, 1, b1
Get #1, , b2
If b1 = &HFF And b2 = &HD8 Then JFIF = JFIF + 1
'проверяем наличие кого-нибудь APP?
Get #1, , b1
Get #1, , b2
For x = 0 To 14
If Hex(b1) & Hex(b2) = "FFE" & Hex(x) Then JFIF = JFIF + 1
Next x
'считываем 4 байта идентификатора в IdentFile и последний ноль в b1
IdentFile = String(4, " ")
Get #1, 7, IdentFile
Get #1, , b1
'проверяем идентификатор на последние три символа IF0
If UCase(Right(IdentFile, 2)) & Right(Str(b1), 1) = "IF0" Then JFIF = JFIF + 1
'проверяем идентификатор на первые два символа JF и ноль в конце
If UCase(Left(IdentFile, 2)) & Right(Str(b1), 1) = "JF0" Then JFIF = JFIF + 1
'Проверяем, есть ли хотя бы три признака JFIF
If JFIF < 3 Then
Close
Label1 = "Не JPG-файл"
Image1.Picture = LoadPicture()
Exit Sub
End If


'Устанавливаем начальный байт нахождения первого APP?
StartByte = 3
'забабахиваем цикл поиска всех APP?
Do While Not EOF(1) '*********************************
'считываем байты
Seek #1, StartByte
Get #1, , b1
Get #1, , b2
'ищем маркер APP*
For x = 0 To 14
If Hex(b1) & Hex(b2) = "FFE" & Hex(x) Then
FlagAPP = True
Exit For 'выходим из цикла For...Next при первом совпадении
End If
Next
x
If FlagAPP = False Then 'если не найден, выходим из цикла Do...Loop
Exit Do
End If


'Узнаем длину APP*
Get #1, StartByte + 2, b1
Get #1, StartByte + 3, b2
'используем функцию
ConvertDec из модуля, чтобы получить данные о длине сегмента APP?
DlinaAPP0 = ConvertDec(String((Len(Hex(b1)) - 2) * -1, 0) & Hex(b1) & String((Len(Hex(b2)) - 2) * -1, 0) & Hex(b2))
FlagAPP = False
StartByte = DlinaAPP0 + Seek(1) - 2
Loop '*********************************************
'начинаем поиск SOF с байта номер StartByte
Seek #1, StartByte
'организаем цикл, который ищет SOF (FF C?) после последнего APP?
Do While Not FlagAPP
Get #1, , b1
If b1 = &HFF Then
Get #1, , b1
DoEvents 'на всякий случай даем возможность обрабатывать прерывания
'для перебора C3...CF используем их десятичное представление
If b1 >= 192 And b1 <= 207 Then
NomerBaita = Seek(1) + 3
FlagAPP = True
End If
End If
Loop

'Если нужный cегмент SOF найден, переходим к процедуре Reading:
If NomerBaita > 0 Then Reading Else Close #1
Close #1
End Sub

Процедура Reading считывает байты высоты и ширины, так же, как это делалось в статьях про файлы BMP и GIF, единственное отличие только в том, что порядок считывания байтов прямой, как они идут, так и читаем. После с помощью функции ConvertDec, которая должна быть у нас в модуле получаем размеры картинки в десятичной системе, меняем размер Image1 и загружаем картинку:

Private Sub Reading()
Dim RazmH As String
Dim
RazmW As String
Get #1, NomerBaita, b1
Get #1, , b2
RazmH = String((Len(Hex(b1)) - 2) * -1, 0) & Hex(b1) & String((Len(Hex(b2)) - 2) * -1, 0) & Hex(b2)
Get #1, , b1
Get #1, , b2
RazmW = String((Len(Hex(b1)) - 2) * -1, 0) & Hex(b1) & String((Len(Hex(b2)) - 2) * -1, 0) & Hex(b2)
FileW = ConvertDec(RazmW)
FileH = ConvertDec(RazmH)
Label1 = "Файл JPG " & FileW & " x " & FileH & " пикселов"

Image1.Picture = LoadPicture()
If FileW >= FileH Then
Image1.Width = Kvadrat
Image1.Height = FileH / (FileW / Kvadrat)
ElseIf FileW < FileH Then
Image1.Width = FileW / (FileH / Kvadrat)
Image1.Height = Kvadrat
End If
Image1.Picture = LoadPicture(FileName)
End Sub

Конечно, данный код можно оптимизировать (уменьшить количество переменных, все проверки сделать в одном цикле Do While и т.д. а главное,ха-ха-ха, убрать ремарки, а после чего оформить его в виде модуля, чтобы использовать в своих программах. Но я решил код оставить как есть.
Я проверял программку на разных JPG-файлах, в том числе созданных профессиональной камерой Canon EOS 1D, цифровой мыльницей, пленочным сканером, на файлах измененных программами Photoshop и ACDSee, а также на файлах, скаченных с Интернет и все работало. Но если ты найдешь файл, который эта программа читает с ошибками, не сочти за труд, пришли. А если ты увидел здесь ошибки, или есть другие идеи, то жду письма или сообщения на форуме.
Слава JPG!


Copyright © 2005 4us

Сайт создан в системе uCoz