Before those levels where when implementing the plugin. Now these levels are for reporting issues from users. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4039>
		
			
				
	
	
		
			897 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			897 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* GStreamer
 | |
|  *
 | |
|  * jpegparse: a parser for JPEG streams
 | |
|  *
 | |
|  * Copyright (C) <2009> Arnout Vandecappelle (Essensium/Mind) <arnout@mind.be>
 | |
|  *               <2022> Víctor Manuel Jáquez Leal <vjaquez@igalia.com>
 | |
|  *
 | |
|  * This library is free software; you can redistribute it and/or
 | |
|  * modify it under the terms of the GNU Lesser General Public
 | |
|  * License as published by the Free Software Foundation; either
 | |
|  * version 2.1 of the License, or (at your option) any later version.
 | |
|  *
 | |
|  * This library is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | |
|  * Lesser General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU Lesser General Public
 | |
|  * License along with this library; if not, write to the
 | |
|  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 | |
|  * Boston, MA 02110-1301, USA.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * SECTION:element-jpegparse
 | |
|  * @title: jpegparse
 | |
|  * @short_description: JPEG parser
 | |
|  *
 | |
|  * Parses a JPEG stream into JPEG images.  It looks for EOI boundaries to
 | |
|  * split a continuous stream into single-frame buffers. Also reads the
 | |
|  * image header searching for image properties such as width and height
 | |
|  * among others. Jpegparse can also extract metadata (e.g. xmp).
 | |
|  *
 | |
|  * ## Example launch line
 | |
|  * |[
 | |
|  * gst-launch-1.0 -v souphttpsrc location=... ! jpegparse ! matroskamux ! filesink location=...
 | |
|  * ]|
 | |
|  * The above pipeline fetches a motion JPEG stream from an IP camera over
 | |
|  * HTTP and stores it in a matroska file.
 | |
|  *
 | |
|  */
 | |
| /* FIXME: output plain JFIF APP marker only. This provides best code reuse.
 | |
|  * JPEG decoders would not need to handle this part anymore. Also when remuxing
 | |
|  * (... ! jpegparse ! ... ! jifmux ! ...) metadata consolidation would be
 | |
|  * easier.
 | |
|  */
 | |
| 
 | |
| /* TODO:
 | |
|  *  + APP2 -- ICC color profile
 | |
|  *  + APP3 -- meta (same as exif)
 | |
|  *  + APP12 -- Photoshop Save for Web: Ducky / Picture info
 | |
|  *  + APP13 -- Adobe IRB
 | |
|  *  + check for interlaced mjpeg
 | |
|  */
 | |
| 
 | |
| #ifdef HAVE_CONFIG_H
 | |
| #include <config.h>
 | |
| #endif
 | |
| 
 | |
| #include <string.h>
 | |
| #include <gst/base/gstbytereader.h>
 | |
| #include <gst/codecparsers/gstjpegparser.h>
 | |
| #include <gst/tag/tag.h>
 | |
| #include <gst/video/video.h>
 | |
| 
 | |
| #include "gstjpegparse.h"
 | |
| 
 | |
| enum ParserState
 | |
| {
 | |
|   GST_JPEG_PARSER_STATE_GOT_SOI = 1 << 0,
 | |
|   GST_JPEG_PARSER_STATE_GOT_SOF = 1 << 1,
 | |
|   GST_JPEG_PARSER_STATE_GOT_SOS = 1 << 2,
 | |
|   GST_JPEG_PARSER_STATE_GOT_JFIF = 1 << 3,
 | |
|   GST_JPEG_PARSER_STATE_GOT_ADOBE = 1 << 4,
 | |
| 
 | |
|   GST_JPEG_PARSER_STATE_VALID_PICTURE = (GST_JPEG_PARSER_STATE_GOT_SOI |
 | |
|       GST_JPEG_PARSER_STATE_GOT_SOF | GST_JPEG_PARSER_STATE_GOT_SOS),
 | |
| };
 | |
| 
 | |
| static GstStaticPadTemplate gst_jpeg_parse_src_pad_template =
 | |
| GST_STATIC_PAD_TEMPLATE ("src",
 | |
|     GST_PAD_SRC,
 | |
|     GST_PAD_ALWAYS,
 | |
|     GST_STATIC_CAPS ("image/jpeg, "
 | |
|         "framerate = (fraction) [ 0/1, MAX ], " "parsed = (boolean) true")
 | |
|     );
 | |
| 
 | |
| static GstStaticPadTemplate gst_jpeg_parse_sink_pad_template =
 | |
| GST_STATIC_PAD_TEMPLATE ("sink",
 | |
|     GST_PAD_SINK,
 | |
|     GST_PAD_ALWAYS,
 | |
|     GST_STATIC_CAPS ("image/jpeg")
 | |
|     );
 | |
| 
 | |
| GST_DEBUG_CATEGORY_STATIC (jpeg_parse_debug);
 | |
| #define GST_CAT_DEFAULT jpeg_parse_debug
 | |
| 
 | |
| static GstFlowReturn
 | |
| gst_jpeg_parse_handle_frame (GstBaseParse * bparse, GstBaseParseFrame * frame,
 | |
|     gint * skipsize);
 | |
| static gboolean gst_jpeg_parse_set_sink_caps (GstBaseParse * parse,
 | |
|     GstCaps * caps);
 | |
| static gboolean gst_jpeg_parse_sink_event (GstBaseParse * parse,
 | |
|     GstEvent * event);
 | |
| static gboolean gst_jpeg_parse_start (GstBaseParse * parse);
 | |
| static gboolean gst_jpeg_parse_stop (GstBaseParse * parse);
 | |
| 
 | |
| #define gst_jpeg_parse_parent_class parent_class
 | |
| G_DEFINE_TYPE (GstJpegParse, gst_jpeg_parse, GST_TYPE_BASE_PARSE);
 | |
| GST_ELEMENT_REGISTER_DEFINE (jpegparse, "jpegparse", GST_RANK_NONE,
 | |
|     GST_TYPE_JPEG_PARSE);
 | |
| 
 | |
| enum GstJPEGColorspace
 | |
| {
 | |
|   GST_JPEG_COLORSPACE_NONE,
 | |
|   GST_JPEG_COLORSPACE_RGB,
 | |
|   GST_JPEG_COLORSPACE_YUV,
 | |
|   GST_JPEG_COLORSPACE_GRAY,
 | |
|   GST_JPEG_COLORSPACE_CMYK,
 | |
|   GST_JPEG_COLORSPACE_YCCK,
 | |
| };
 | |
| 
 | |
| static const gchar *gst_jpeg_colorspace_strings[] = {
 | |
|   [GST_JPEG_COLORSPACE_NONE] = NULL,
 | |
|   [GST_JPEG_COLORSPACE_RGB] = "sRGB",
 | |
|   [GST_JPEG_COLORSPACE_YUV] = "sYUV",
 | |
|   [GST_JPEG_COLORSPACE_GRAY] = "GRAY",
 | |
|   [GST_JPEG_COLORSPACE_CMYK] = "CMYK",
 | |
|   [GST_JPEG_COLORSPACE_YCCK] = "YCCK",
 | |
| };
 | |
| 
 | |
| enum GstJPEGSampling
 | |
| {
 | |
|   GST_JPEG_SAMPLING_NONE,
 | |
|   GST_JPEG_SAMPLING_RGB,
 | |
|   GST_JPEG_SAMPLING_BGR,
 | |
|   GST_JPEG_SAMPLING_YBR444,
 | |
|   GST_JPEG_SAMPLING_YBR422,
 | |
|   GST_JPEG_SAMPLING_YBR420,
 | |
|   GST_JPEG_SAMPLING_YBR440,
 | |
|   GST_JPEG_SAMPLING_YBR410,
 | |
|   GST_JPEG_SAMPLING_YBR411,
 | |
|   GST_JPEG_SAMPLING_GRAYSCALE,
 | |
| };
 | |
| 
 | |
| static const gchar *gst_jpeg_sampling_strings[] = {
 | |
|   [GST_JPEG_SAMPLING_NONE] = NULL,
 | |
|   [GST_JPEG_SAMPLING_RGB] = "RGB",
 | |
|   [GST_JPEG_SAMPLING_BGR] = "BGR",
 | |
|   [GST_JPEG_SAMPLING_YBR444] = "YCbCr-4:4:4",
 | |
|   [GST_JPEG_SAMPLING_YBR422] = "YCbCr-4:2:2",
 | |
|   [GST_JPEG_SAMPLING_YBR420] = "YCbCr-4:2:0",
 | |
|   [GST_JPEG_SAMPLING_YBR440] = "YCbCr-4:4:0",
 | |
|   [GST_JPEG_SAMPLING_YBR410] = "YCbCr-4:1:0",
 | |
|   [GST_JPEG_SAMPLING_YBR411] = "YCbCr-4:1:1",
 | |
|   [GST_JPEG_SAMPLING_GRAYSCALE] = "GRAYSCALE",
 | |
| };
 | |
| 
 | |
| static void
 | |
| gst_jpeg_parse_class_init (GstJpegParseClass * klass)
 | |
| {
 | |
|   GstBaseParseClass *gstbaseparse_class;
 | |
|   GstElementClass *gstelement_class;
 | |
| 
 | |
|   gstbaseparse_class = (GstBaseParseClass *) klass;
 | |
|   gstelement_class = (GstElementClass *) klass;
 | |
| 
 | |
|   gstbaseparse_class->start = gst_jpeg_parse_start;
 | |
|   gstbaseparse_class->stop = gst_jpeg_parse_stop;
 | |
|   gstbaseparse_class->set_sink_caps = gst_jpeg_parse_set_sink_caps;
 | |
|   gstbaseparse_class->sink_event = gst_jpeg_parse_sink_event;
 | |
|   gstbaseparse_class->handle_frame = gst_jpeg_parse_handle_frame;
 | |
| 
 | |
|   gst_element_class_add_static_pad_template (gstelement_class,
 | |
|       &gst_jpeg_parse_src_pad_template);
 | |
|   gst_element_class_add_static_pad_template (gstelement_class,
 | |
|       &gst_jpeg_parse_sink_pad_template);
 | |
| 
 | |
|   gst_element_class_set_static_metadata (gstelement_class,
 | |
|       "JPEG stream parser",
 | |
|       "Codec/Parser/Image",
 | |
|       "Parse JPEG images into single-frame buffers",
 | |
|       "Víctor Jáquez <vjaquez@igalia.com>");
 | |
| 
 | |
|   GST_DEBUG_CATEGORY_INIT (jpeg_parse_debug, "jpegparse", 0, "JPEG parser");
 | |
| }
 | |
| 
 | |
| static void
 | |
| gst_jpeg_parse_init (GstJpegParse * parse)
 | |
| {
 | |
|   parse->sof = -1;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_set_sink_caps (GstBaseParse * bparse, GstCaps * caps)
 | |
| {
 | |
|   GstJpegParse *parse = GST_JPEG_PARSE_CAST (bparse);
 | |
|   GstStructure *s = gst_caps_get_structure (caps, 0);
 | |
| 
 | |
|   GST_DEBUG_OBJECT (parse, "get sink caps %" GST_PTR_FORMAT, caps);
 | |
| 
 | |
|   gst_structure_get_fraction (s, "framerate",
 | |
|       &parse->framerate_numerator, &parse->framerate_denominator);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static inline gboolean
 | |
| valid_state (guint state, guint ref_state)
 | |
| {
 | |
|   return (state & ref_state) == ref_state;
 | |
| }
 | |
| 
 | |
| /* https://zpl.fi/chroma-subsampling-and-jpeg-sampling-factors/ */
 | |
| /* *INDENT-OFF* */
 | |
| static const struct
 | |
| {
 | |
|   gint h[3];
 | |
|   gint v[3];
 | |
|   enum GstJPEGSampling sampling;
 | |
| } subsampling_map[] = {
 | |
|   {{1, 1, 1}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR444},
 | |
|   {{2, 2, 2}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR444},
 | |
|   {{3, 3, 3}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR444},
 | |
|   {{1, 1, 1}, {2, 2, 2}, GST_JPEG_SAMPLING_YBR444},
 | |
|   {{1, 1, 1}, {3, 3, 3}, GST_JPEG_SAMPLING_YBR444},
 | |
|   {{1, 1, 1}, {2, 1, 1}, GST_JPEG_SAMPLING_YBR440},
 | |
|   {{2, 2, 2}, {2, 1, 1}, GST_JPEG_SAMPLING_YBR440},
 | |
|   {{1, 1, 1}, {4, 2, 2}, GST_JPEG_SAMPLING_YBR440},
 | |
|   {{2, 1, 1}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR422},
 | |
|   {{2, 1, 1}, {2, 2, 2}, GST_JPEG_SAMPLING_YBR422},
 | |
|   {{4, 2, 2}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR422},
 | |
|   {{2, 1, 1}, {2, 1, 1}, GST_JPEG_SAMPLING_YBR420},
 | |
|   {{4, 1, 1}, {1, 1, 1}, GST_JPEG_SAMPLING_YBR411},
 | |
|   {{4, 1, 1}, {2, 1, 1}, GST_JPEG_SAMPLING_YBR410},
 | |
| };
 | |
| /* *INDENT-ON* */
 | |
| 
 | |
| static guint16
 | |
| yuv_sampling (GstJpegFrameHdr * frame_hdr)
 | |
| {
 | |
|   int i, h0, h1, h2, v0, v1, v2;
 | |
| 
 | |
|   g_return_val_if_fail (frame_hdr->num_components == 3, GST_JPEG_SAMPLING_NONE);
 | |
| 
 | |
|   h0 = frame_hdr->components[0].horizontal_factor;
 | |
|   h1 = frame_hdr->components[1].horizontal_factor;
 | |
|   h2 = frame_hdr->components[2].horizontal_factor;
 | |
|   v0 = frame_hdr->components[0].vertical_factor;
 | |
|   v1 = frame_hdr->components[1].vertical_factor;
 | |
|   v2 = frame_hdr->components[2].vertical_factor;
 | |
| 
 | |
|   for (i = 0; i < G_N_ELEMENTS (subsampling_map); i++) {
 | |
|     if (subsampling_map[i].h[0] == h0
 | |
|         && subsampling_map[i].h[1] == h1 && subsampling_map[i].h[2] == h2
 | |
|         && subsampling_map[i].v[0] == v0
 | |
|         && subsampling_map[i].v[1] == v1 && subsampling_map[i].v[2] == v2)
 | |
|       return subsampling_map[i].sampling;
 | |
|   }
 | |
| 
 | |
|   return GST_JPEG_SAMPLING_NONE;
 | |
| }
 | |
| 
 | |
| static const gchar *
 | |
| colorspace_to_string (enum GstJPEGColorspace colorspace)
 | |
| {
 | |
|   return gst_jpeg_colorspace_strings[colorspace];
 | |
| }
 | |
| 
 | |
| /* https://entropymine.wordpress.com/2018/10/22/how-is-a-jpeg-images-color-type-determined/ */
 | |
| /* T-REC-T.872-201206  6.1 Colour encodings and associated values to define white and black */
 | |
| static gboolean
 | |
| gst_jpeg_parse_sof (GstJpegParse * parse, GstJpegSegment * seg)
 | |
| {
 | |
|   GstJpegFrameHdr hdr = { 0, };
 | |
| 
 | |
|   if (!gst_jpeg_segment_parse_frame_header (seg, &hdr)) {
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   parse->width = hdr.width;
 | |
|   parse->height = hdr.height;
 | |
| 
 | |
|   parse->colorspace = GST_JPEG_COLORSPACE_NONE;
 | |
|   parse->sampling = GST_JPEG_SAMPLING_NONE;
 | |
| 
 | |
|   switch (hdr.num_components) {
 | |
|     case 1:
 | |
|       parse->colorspace = GST_JPEG_COLORSPACE_GRAY;
 | |
|       parse->sampling = GST_JPEG_SAMPLING_GRAYSCALE;
 | |
|       break;
 | |
|     case 3:
 | |
|       if (valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_JFIF)) {
 | |
|         parse->colorspace = GST_JPEG_COLORSPACE_YUV;
 | |
|         parse->sampling = yuv_sampling (&hdr);
 | |
|       } else {
 | |
|         if (valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_ADOBE)) {
 | |
|           if (parse->adobe_transform == 0) {
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_RGB;
 | |
|             parse->sampling = GST_JPEG_SAMPLING_RGB;
 | |
|           } else if (parse->adobe_transform == 1) {
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_YUV;;
 | |
|             parse->sampling = yuv_sampling (&hdr);
 | |
|           } else {
 | |
|             GST_DEBUG_OBJECT (parse, "Unknown Adobe color transform code");
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_YUV;;
 | |
|             parse->sampling = yuv_sampling (&hdr);
 | |
|           }
 | |
|         } else {
 | |
|           int cid0, cid1, cid2;
 | |
| 
 | |
|           cid0 = hdr.components[0].identifier;
 | |
|           cid1 = hdr.components[1].identifier;
 | |
|           cid2 = hdr.components[2].identifier;
 | |
| 
 | |
|           if (cid0 == 1 && cid1 == 2 && cid2 == 3) {
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_YUV;
 | |
|             parse->sampling = yuv_sampling (&hdr);
 | |
|           } else if (cid0 == 'R' && cid1 == 'G' && cid2 == 'B') {
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_RGB;
 | |
|             parse->sampling = GST_JPEG_SAMPLING_RGB;
 | |
|           } else {
 | |
|             GST_DEBUG_OBJECT (parse, "Unrecognized component IDs");
 | |
|             parse->colorspace = GST_JPEG_COLORSPACE_YUV;
 | |
|             parse->sampling = yuv_sampling (&hdr);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       break;
 | |
|     case 4:
 | |
|       if (valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_ADOBE)) {
 | |
|         if (parse->adobe_transform == 0) {
 | |
|           parse->colorspace = GST_JPEG_COLORSPACE_CMYK;
 | |
|         } else if (parse->adobe_transform == 2) {
 | |
|           parse->colorspace = GST_JPEG_COLORSPACE_YCCK;
 | |
|         } else {
 | |
|           GST_DEBUG_OBJECT (parse, "Unknown Adobe color transform code");
 | |
|           parse->colorspace = GST_JPEG_COLORSPACE_YCCK;
 | |
|         }
 | |
|       } else {
 | |
|         parse->colorspace = GST_JPEG_COLORSPACE_CMYK;
 | |
|       }
 | |
|       break;
 | |
|     default:
 | |
|       GST_WARNING_OBJECT (parse, "Unknown color space");
 | |
|       break;
 | |
|   }
 | |
| 
 | |
|   GST_INFO_OBJECT (parse, "SOF [%dx%d] %d comp - %s", parse->width,
 | |
|       parse->height, hdr.num_components,
 | |
|       GST_STR_NULL (colorspace_to_string (parse->colorspace)));
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static inline GstTagList *
 | |
| get_tag_list (GstJpegParse * parse)
 | |
| {
 | |
|   if (!parse->tags)
 | |
|     parse->tags = gst_tag_list_new_empty ();
 | |
|   return parse->tags;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_app0 (GstJpegParse * parse, GstJpegSegment * seg)
 | |
| {
 | |
|   GstByteReader reader;
 | |
|   const gchar *id_str;
 | |
|   guint16 xd, yd;
 | |
|   guint8 unit, xt, yt;
 | |
| 
 | |
|   if (seg->size < 6)            /* less than 6 means no id string */
 | |
|     return FALSE;
 | |
| 
 | |
|   gst_byte_reader_init (&reader, seg->data + seg->offset, seg->size);
 | |
|   gst_byte_reader_skip_unchecked (&reader, 2);
 | |
| 
 | |
|   if (!gst_byte_reader_get_string_utf8 (&reader, &id_str))
 | |
|     return FALSE;
 | |
| 
 | |
|   if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_JFIF)
 | |
|       && g_strcmp0 (id_str, "JFIF") == 0) {
 | |
| 
 | |
|     parse->state |= GST_JPEG_PARSER_STATE_GOT_JFIF;
 | |
| 
 | |
|     /* version */
 | |
|     gst_byte_reader_skip_unchecked (&reader, 2);
 | |
| 
 | |
|     /* units */
 | |
|     if (!gst_byte_reader_get_uint8 (&reader, &unit))
 | |
|       return FALSE;
 | |
| 
 | |
|     /* x density */
 | |
|     if (!gst_byte_reader_get_uint16_be (&reader, &xd))
 | |
|       return FALSE;
 | |
|     /* y density */
 | |
|     if (!gst_byte_reader_get_uint16_be (&reader, &yd))
 | |
|       return FALSE;
 | |
| 
 | |
|     /* x thumbnail */
 | |
|     if (!gst_byte_reader_get_uint8 (&reader, &xt))
 | |
|       return FALSE;
 | |
|     /* y thumbnail */
 | |
|     if (!gst_byte_reader_get_uint8 (&reader, &yt))
 | |
|       return FALSE;
 | |
| 
 | |
|     if (unit == 0) {
 | |
|       /* no units, X and Y specify the pixel aspect ratio */
 | |
|       parse->x_density = xd;
 | |
|       parse->y_density = yd;
 | |
|     } else if (unit == 1 || unit == 2) {
 | |
|       /* tag pixel per inches */
 | |
|       double hppi = xd, vppi = yd;
 | |
| 
 | |
|       /* cm to in */
 | |
|       if (unit == 2) {
 | |
|         hppi *= 2.54;
 | |
|         vppi *= 2.54;
 | |
|       }
 | |
| 
 | |
|       gst_tag_register_musicbrainz_tags ();
 | |
|       gst_tag_list_add (get_tag_list (parse), GST_TAG_MERGE_REPLACE,
 | |
|           GST_TAG_IMAGE_HORIZONTAL_PPI, hppi, GST_TAG_IMAGE_VERTICAL_PPI, vppi,
 | |
|           NULL);
 | |
|     }
 | |
| 
 | |
|     if (xt > 0 && yt > 0)
 | |
|       GST_FIXME_OBJECT (parse, "embedded thumbnail ignored");
 | |
| 
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   /* JFIF  Extension  */
 | |
|   if (g_strcmp0 (id_str, "JFXX") == 0) {
 | |
|     if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_JFIF))
 | |
|       return FALSE;
 | |
| 
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   /* https://exiftool.org/TagNames/JPEG.html#AVI1 */
 | |
|   if (g_strcmp0 (id_str, "AVI1") == 0) {
 | |
|     /* polarity */
 | |
|     if (!gst_byte_reader_get_uint8 (&reader, &unit))
 | |
|       return FALSE;
 | |
| 
 | |
|     /* TODO: update caps for interlaced MJPEG */
 | |
|     GST_DEBUG_OBJECT (parse, "MJPEG interleaved field: %d", unit);
 | |
| 
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   GST_DEBUG_OBJECT (parse, "Unhandled app0: %s", id_str);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /* *INDENT-OFF* */
 | |
| static const struct
 | |
| {
 | |
|   const gchar *suffix;
 | |
|   GstTagList *(*tag_func) (GstBuffer * buff);
 | |
| } TagMap[] = {
 | |
|   {"Exif", gst_tag_list_from_exif_buffer_with_tiff_header},
 | |
|   {"http://ns.adobe.com/xap/1.0/", gst_tag_list_from_xmp_buffer},
 | |
| };
 | |
| /* *INDENT-ON* */
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_app1 (GstJpegParse * parse, GstJpegSegment * seg)
 | |
| {
 | |
|   GstByteReader reader;
 | |
|   GstBuffer *buf;
 | |
|   guint16 size = 0;
 | |
|   const gchar *id_str;
 | |
|   const guint8 *data;
 | |
|   gint i;
 | |
| 
 | |
|   if (seg->size < 6)            /* less than 6 means no id string */
 | |
|     return FALSE;
 | |
| 
 | |
|   gst_byte_reader_init (&reader, seg->data + seg->offset, seg->size);
 | |
|   gst_byte_reader_skip_unchecked (&reader, 2);
 | |
| 
 | |
|   if (!gst_byte_reader_get_string_utf8 (&reader, &id_str))
 | |
|     return FALSE;
 | |
| 
 | |
|   for (i = 0; i < G_N_ELEMENTS (TagMap); i++) {
 | |
|     if (!g_str_has_suffix (id_str, TagMap[i].suffix))
 | |
|       continue;
 | |
| 
 | |
|     /* skip NUL only for Exif */
 | |
|     if (i == 0) {
 | |
|       if (!gst_byte_reader_skip (&reader, 1))
 | |
|         return FALSE;
 | |
|     }
 | |
| 
 | |
|     size = gst_byte_reader_get_remaining (&reader);
 | |
| 
 | |
|     if (!gst_byte_reader_get_data (&reader, size, &data))
 | |
|       return FALSE;
 | |
| 
 | |
|     buf = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY,
 | |
|         (gpointer) data, size, 0, size, NULL, NULL);
 | |
| 
 | |
|     if (buf) {
 | |
|       GstTagList *tags;
 | |
| 
 | |
|       tags = TagMap[i].tag_func (buf);
 | |
|       gst_buffer_unref (buf);
 | |
| 
 | |
|       if (tags) {
 | |
|         GST_LOG_OBJECT (parse, "parsed marker %x: '%s' %" GST_PTR_FORMAT,
 | |
|             GST_JPEG_MARKER_APP1, id_str, tags);
 | |
|         gst_tag_list_insert (get_tag_list (parse), tags, GST_TAG_MERGE_REPLACE);
 | |
|         gst_tag_list_unref (tags);
 | |
|       } else {
 | |
|         GST_INFO_OBJECT (parse, "failed to parse %s: %s", id_str, data);
 | |
|         return FALSE;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   GST_DEBUG_OBJECT (parse, "Unhandled app1: %s", id_str);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_app14 (GstJpegParse * parse, GstJpegSegment * seg)
 | |
| {
 | |
|   GstByteReader reader;
 | |
|   const gchar *id_str;
 | |
|   guint8 transform;
 | |
| 
 | |
|   if (seg->size < 6)            /* less than 6 means no id string */
 | |
|     return FALSE;
 | |
| 
 | |
|   gst_byte_reader_init (&reader, seg->data + seg->offset, seg->size);
 | |
|   gst_byte_reader_skip_unchecked (&reader, 2);
 | |
| 
 | |
|   if (!gst_byte_reader_get_string_utf8 (&reader, &id_str))
 | |
|     return FALSE;
 | |
| 
 | |
|   if (!g_str_has_prefix (id_str, "Adobe")) {
 | |
|     GST_DEBUG_OBJECT (parse, "Unhandled app14: %s", id_str);
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   /* skip version and flags */
 | |
|   if (!gst_byte_reader_skip (&reader, 6))
 | |
|     return FALSE;
 | |
| 
 | |
|   parse->state |= GST_JPEG_PARSER_STATE_GOT_ADOBE;
 | |
| 
 | |
|   /* transform bit might not exist  */
 | |
|   if (!gst_byte_reader_get_uint8 (&reader, &transform))
 | |
|     return TRUE;
 | |
| 
 | |
|   parse->adobe_transform = transform;
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static inline gchar *
 | |
| get_utf8_from_data (const guint8 * data, guint16 size)
 | |
| {
 | |
|   const gchar *env_vars[] = { "GST_JPEG_TAG_ENCODING",
 | |
|     "GST_TAG_ENCODING", NULL
 | |
|   };
 | |
|   const char *str = (gchar *) data;
 | |
| 
 | |
|   return gst_tag_freeform_string_to_utf8 (str, size, env_vars);
 | |
| }
 | |
| 
 | |
| /* read comment and post as tag */
 | |
| static inline gboolean
 | |
| gst_jpeg_parse_com (GstJpegParse * parse, GstJpegSegment * seg)
 | |
| {
 | |
|   GstByteReader reader;
 | |
|   const guint8 *data = NULL;
 | |
|   guint16 size;
 | |
|   gchar *comment;
 | |
| 
 | |
|   gst_byte_reader_init (&reader, seg->data + seg->offset, seg->size);
 | |
|   gst_byte_reader_skip_unchecked (&reader, 2);
 | |
| 
 | |
|   size = gst_byte_reader_get_remaining (&reader);
 | |
| 
 | |
|   if (!gst_byte_reader_get_data (&reader, size, &data))
 | |
|     return FALSE;
 | |
| 
 | |
|   comment = get_utf8_from_data (data, size);
 | |
|   if (!comment)
 | |
|     return FALSE;
 | |
| 
 | |
|   GST_INFO_OBJECT (parse, "comment found: %s", comment);
 | |
|   gst_tag_list_add (get_tag_list (parse), GST_TAG_MERGE_REPLACE,
 | |
|       GST_TAG_COMMENT, comment, NULL);
 | |
|   g_free (comment);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static void
 | |
| gst_jpeg_parse_reset (GstJpegParse * parse)
 | |
| {
 | |
|   parse->width = 0;
 | |
|   parse->height = 0;
 | |
|   parse->last_offset = 0;
 | |
|   parse->state = 0;
 | |
|   parse->sof = -1;
 | |
|   parse->adobe_transform = 0;
 | |
|   parse->x_density = 0;
 | |
|   parse->y_density = 0;
 | |
| 
 | |
|   if (parse->tags) {
 | |
|     gst_tag_list_unref (parse->tags);
 | |
|     parse->tags = NULL;
 | |
|   }
 | |
| }
 | |
| 
 | |
| static const gchar *
 | |
| sampling_to_string (enum GstJPEGSampling sampling)
 | |
| {
 | |
|   return gst_jpeg_sampling_strings[sampling];
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_set_new_caps (GstJpegParse * parse)
 | |
| {
 | |
|   GstCaps *caps;
 | |
|   gboolean res;
 | |
| 
 | |
|   caps = gst_caps_new_simple ("image/jpeg", "parsed", G_TYPE_BOOLEAN, TRUE,
 | |
|       NULL);
 | |
| 
 | |
|   if (parse->width > 0)
 | |
|     gst_caps_set_simple (caps, "width", G_TYPE_INT, parse->width, NULL);
 | |
|   if (parse->width > 0)
 | |
|     gst_caps_set_simple (caps, "height", G_TYPE_INT, parse->height, NULL);
 | |
|   if (parse->sof >= 0)
 | |
|     gst_caps_set_simple (caps, "sof-marker", G_TYPE_INT, parse->sof, NULL);
 | |
|   if (parse->colorspace != GST_JPEG_COLORSPACE_NONE) {
 | |
|     gst_caps_set_simple (caps, "colorspace", G_TYPE_STRING,
 | |
|         colorspace_to_string (parse->colorspace), NULL);
 | |
|   }
 | |
|   if (parse->sampling != GST_JPEG_SAMPLING_NONE) {
 | |
|     gst_caps_set_simple (caps, "sampling", G_TYPE_STRING,
 | |
|         sampling_to_string (parse->sampling), NULL);
 | |
|   }
 | |
| 
 | |
|   gst_caps_set_simple (caps, "framerate", GST_TYPE_FRACTION,
 | |
|       parse->framerate_numerator, parse->framerate_denominator, NULL);
 | |
| 
 | |
|   if (parse->x_density > 0 && parse->y_density > 0) {
 | |
|     gst_caps_set_simple (caps, "pixel-aspect-ratio", GST_TYPE_FRACTION,
 | |
|         parse->x_density, parse->y_density, NULL);
 | |
|   }
 | |
| 
 | |
|   if (parse->prev_caps && gst_caps_is_equal_fixed (caps, parse->prev_caps)) {
 | |
|     gst_caps_unref (caps);
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   GST_DEBUG_OBJECT (parse,
 | |
|       "setting downstream caps on %s:%s to %" GST_PTR_FORMAT,
 | |
|       GST_DEBUG_PAD_NAME (GST_BASE_PARSE_SRC_PAD (parse)), caps);
 | |
|   res = gst_pad_set_caps (GST_BASE_PARSE_SRC_PAD (parse), caps);
 | |
| 
 | |
|   gst_caps_replace (&parse->prev_caps, caps);
 | |
|   gst_caps_unref (caps);
 | |
| 
 | |
|   return res;
 | |
| 
 | |
| }
 | |
| 
 | |
| static GstFlowReturn
 | |
| gst_jpeg_parse_finish_frame (GstJpegParse * parse, GstBaseParseFrame * frame,
 | |
|     gint size)
 | |
| {
 | |
|   GstBaseParse *bparse = GST_BASE_PARSE (parse);
 | |
|   GstFlowReturn ret;
 | |
| 
 | |
|   if (parse->tags)
 | |
|     gst_base_parse_merge_tags (bparse, parse->tags, GST_TAG_MERGE_REPLACE);
 | |
| 
 | |
|   if (!gst_jpeg_parse_set_new_caps (parse))
 | |
|     return GST_FLOW_ERROR;
 | |
| 
 | |
|   if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_VALID_PICTURE)) {
 | |
|     /* this validation breaks unit tests */
 | |
|     /* frame->flags |= GST_BASE_PARSE_FRAME_FLAG_DROP; */
 | |
|     GST_WARNING_OBJECT (parse, "Potentially invalid picture");
 | |
|   }
 | |
| 
 | |
|   ret = gst_base_parse_finish_frame (bparse, frame, size);
 | |
| 
 | |
|   gst_jpeg_parse_reset (parse);
 | |
| 
 | |
|   return ret;
 | |
| }
 | |
| 
 | |
| static GstFlowReturn
 | |
| gst_jpeg_parse_handle_frame (GstBaseParse * bparse, GstBaseParseFrame * frame,
 | |
|     gint * skipsize)
 | |
| {
 | |
|   GstJpegParse *parse = GST_JPEG_PARSE_CAST (bparse);
 | |
|   GstMapInfo mapinfo;
 | |
|   GstJpegMarker marker;
 | |
|   GstJpegSegment seg;
 | |
|   guint offset;
 | |
| 
 | |
|   if (!gst_buffer_map (frame->buffer, &mapinfo, GST_MAP_READ))
 | |
|     return GST_FLOW_ERROR;
 | |
| 
 | |
|   offset = parse->last_offset;
 | |
|   if (offset > 0)
 | |
|     offset -= 1;                /* it migth be in the middle marker */
 | |
| 
 | |
|   while (offset < mapinfo.size) {
 | |
|     if (!gst_jpeg_parse (&seg, mapinfo.data, mapinfo.size, offset)) {
 | |
|       if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_SOI)) {
 | |
|         /* Skip any garbage until SOI */
 | |
|         *skipsize = mapinfo.size;
 | |
|         GST_INFO_OBJECT (parse, "skipping %d bytes", *skipsize);
 | |
|       } else {
 | |
|         /* Accept anything after SOI */
 | |
|         parse->last_offset = mapinfo.size;
 | |
|       }
 | |
|       goto beach;
 | |
|     }
 | |
| 
 | |
|     offset = seg.offset;
 | |
|     marker = seg.marker;
 | |
| 
 | |
|     if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_SOI)
 | |
|         && marker != GST_JPEG_MARKER_SOI)
 | |
|       continue;
 | |
| 
 | |
|     /* check if the whole segment is available */
 | |
|     if (offset + seg.size > mapinfo.size) {
 | |
|       GST_DEBUG_OBJECT (parse, "incomplete segment: %x [offset %d]", marker,
 | |
|           offset);
 | |
|       parse->last_offset = offset - 2;
 | |
|       goto beach;
 | |
|     }
 | |
| 
 | |
|     offset += seg.size;
 | |
| 
 | |
|     GST_LOG_OBJECT (parse, "marker found: %x [offset %d / size %"
 | |
|         G_GSSIZE_FORMAT "]", marker, seg.offset, seg.size);
 | |
| 
 | |
|     switch (marker) {
 | |
|       case GST_JPEG_MARKER_SOI:
 | |
|         /* This means that new SOI comes without an previous EOI. */
 | |
|         if (offset > 2) {
 | |
|           /* If already some data segment parsed, push it as a frame. */
 | |
|           if (valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_SOS)) {
 | |
|             gst_buffer_unmap (frame->buffer, &mapinfo);
 | |
| 
 | |
|             frame->out_buffer = gst_buffer_copy_region (frame->buffer,
 | |
|                 GST_BUFFER_COPY_ALL, 0, seg.offset - 2);
 | |
|             GST_MINI_OBJECT_FLAGS (frame->out_buffer) |=
 | |
|                 GST_BUFFER_FLAG_CORRUPTED;
 | |
| 
 | |
|             GST_WARNING_OBJECT (parse, "Push a frame without EOI, size %d",
 | |
|                 seg.offset - 2);
 | |
|             return gst_jpeg_parse_finish_frame (parse, frame, seg.offset - 2);
 | |
|           }
 | |
| 
 | |
|           gst_jpeg_parse_reset (parse);
 | |
|           parse->state |= GST_JPEG_PARSER_STATE_GOT_SOI;
 | |
|           /* unset tags */
 | |
|           gst_base_parse_merge_tags (bparse, NULL, GST_TAG_MERGE_UNDEFINED);
 | |
| 
 | |
|           *skipsize = offset - 2;
 | |
|           GST_DEBUG_OBJECT (parse, "skipping %d bytes before SOI", *skipsize);
 | |
|           parse->last_offset = 2;
 | |
|           goto beach;
 | |
|         }
 | |
| 
 | |
|         /* unset tags */
 | |
|         gst_base_parse_merge_tags (bparse, NULL, GST_TAG_MERGE_UNDEFINED);
 | |
|         parse->state |= GST_JPEG_PARSER_STATE_GOT_SOI;
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_EOI:
 | |
|         gst_buffer_unmap (frame->buffer, &mapinfo);
 | |
|         return gst_jpeg_parse_finish_frame (parse, frame, seg.offset);
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_SOS:
 | |
|         if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_SOF))
 | |
|           GST_WARNING_OBJECT (parse, "SOS marker without SOF one");
 | |
|         parse->state |= GST_JPEG_PARSER_STATE_GOT_SOS;
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_COM:
 | |
|         if (!gst_jpeg_parse_com (parse, &seg)) {
 | |
|           GST_ELEMENT_WARNING (parse, STREAM, FORMAT,
 | |
|               ("Failed to parse com segment"), ("Invalid data"));
 | |
|         }
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_APP0:
 | |
|         if (!gst_jpeg_parse_app0 (parse, &seg)) {
 | |
|           GST_ELEMENT_WARNING (parse, STREAM, FORMAT,
 | |
|               ("Failed to parse app0 segment"), ("Invalid data"));
 | |
|         }
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_APP1:
 | |
|         if (!gst_jpeg_parse_app1 (parse, &seg)) {
 | |
|           GST_ELEMENT_WARNING (parse, STREAM, FORMAT,
 | |
|               ("Failed to parse app1 segment"), ("Invalid data"));
 | |
|         }
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_APP14:
 | |
|         if (!gst_jpeg_parse_app14 (parse, &seg)) {
 | |
|           GST_ELEMENT_WARNING (parse, STREAM, FORMAT,
 | |
|               ("Failed to parse app14 segment"), ("Invalid data"));
 | |
|         }
 | |
|         break;
 | |
|       case GST_JPEG_MARKER_DHT:
 | |
|       case GST_JPEG_MARKER_DAC:
 | |
|         /* to avoid break the below SOF interval */
 | |
|         break;
 | |
|       default:
 | |
|         /* SOFn segments */
 | |
|         if (marker >= GST_JPEG_MARKER_SOF_MIN &&
 | |
|             marker <= GST_JPEG_MARKER_SOF_MAX) {
 | |
|           if (!valid_state (parse->state, GST_JPEG_PARSER_STATE_GOT_SOF)
 | |
|               && gst_jpeg_parse_sof (parse, &seg)) {
 | |
|             parse->state |= GST_JPEG_PARSER_STATE_GOT_SOF;
 | |
|             parse->sof = marker - 0xc0;
 | |
|           } else {
 | |
|             GST_ELEMENT_ERROR (parse, STREAM, FORMAT,
 | |
|                 ("Duplicated or bad SOF marker"), (NULL));
 | |
|             gst_buffer_unmap (frame->buffer, &mapinfo);
 | |
|             gst_jpeg_parse_reset (parse);
 | |
|             return GST_FLOW_ERROR;
 | |
|           }
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   parse->last_offset = offset;
 | |
| 
 | |
| beach:
 | |
|   gst_buffer_unmap (frame->buffer, &mapinfo);
 | |
|   return GST_FLOW_OK;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_sink_event (GstBaseParse * bparse, GstEvent * event)
 | |
| {
 | |
|   GstJpegParse *parse = GST_JPEG_PARSE_CAST (bparse);
 | |
| 
 | |
|   GST_DEBUG_OBJECT (parse, "event : %s", GST_EVENT_TYPE_NAME (event));
 | |
| 
 | |
|   switch (GST_EVENT_TYPE (event)) {
 | |
|     case GST_EVENT_FLUSH_STOP:
 | |
|       gst_jpeg_parse_reset (parse);
 | |
|       break;
 | |
|     default:
 | |
|       break;
 | |
|   }
 | |
| 
 | |
|   return GST_BASE_PARSE_CLASS (parent_class)->sink_event (bparse, event);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_start (GstBaseParse * bparse)
 | |
| {
 | |
|   GstJpegParse *parse = GST_JPEG_PARSE_CAST (bparse);
 | |
| 
 | |
|   parse->framerate_numerator = 0;
 | |
|   parse->framerate_denominator = 1;
 | |
| 
 | |
|   gst_jpeg_parse_reset (parse);
 | |
| 
 | |
|   gst_base_parse_set_min_frame_size (bparse, 2);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| gst_jpeg_parse_stop (GstBaseParse * bparse)
 | |
| {
 | |
|   GstJpegParse *parse = GST_JPEG_PARSE_CAST (bparse);
 | |
| 
 | |
|   if (parse->tags) {
 | |
|     gst_tag_list_unref (parse->tags);
 | |
|     parse->tags = NULL;
 | |
|   }
 | |
|   gst_clear_caps (&parse->prev_caps);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 |