| OLD | NEW |
| 1 /* | 1 /* |
| 2 * Copyright 2015 Google Inc. | 2 * Copyright 2015 Google Inc. |
| 3 * | 3 * |
| 4 * Use of this source code is governed by a BSD-style license that can be | 4 * Use of this source code is governed by a BSD-style license that can be |
| 5 * found in the LICENSE file. | 5 * found in the LICENSE file. |
| 6 */ | 6 */ |
| 7 | 7 |
| 8 #include "SkCodec_libgif.h" | 8 #include "SkCodec_libgif.h" |
| 9 #include "SkCodecPriv.h" | 9 #include "SkCodecPriv.h" |
| 10 #include "SkColorPriv.h" | 10 #include "SkColorPriv.h" |
| 11 #include "SkColorTable.h" | 11 #include "SkColorTable.h" |
| 12 #include "SkGifInterlaceIter.h" | 12 #include "SkGifInterlaceIter.h" |
| 13 #include "SkScaledCodec.h" |
| 13 #include "SkStream.h" | 14 #include "SkStream.h" |
| 14 #include "SkSwizzler.h" | 15 #include "SkSwizzler.h" |
| 15 #include "SkUtils.h" | 16 #include "SkUtils.h" |
| 16 | 17 |
| 17 /* | 18 /* |
| 18 * Checks the start of the stream to see if the image is a gif | 19 * Checks the start of the stream to see if the image is a gif |
| 19 */ | 20 */ |
| 20 bool SkGifCodec::IsGif(SkStream* stream) { | 21 bool SkGifCodec::IsGif(SkStream* stream) { |
| 21 char buf[GIF_STAMP_LEN]; | 22 char buf[GIF_STAMP_LEN]; |
| 22 if (stream->read(buf, GIF_STAMP_LEN) == GIF_STAMP_LEN) { | 23 if (stream->read(buf, GIF_STAMP_LEN) == GIF_STAMP_LEN) { |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 55 return (int32_t) stream->read(out, size); | 56 return (int32_t) stream->read(out, size); |
| 56 } | 57 } |
| 57 | 58 |
| 58 /* | 59 /* |
| 59 * Open the gif file | 60 * Open the gif file |
| 60 */ | 61 */ |
| 61 static GifFileType* open_gif(SkStream* stream) { | 62 static GifFileType* open_gif(SkStream* stream) { |
| 62 return DGifOpen(stream, read_bytes_callback, NULL); | 63 return DGifOpen(stream, read_bytes_callback, NULL); |
| 63 } | 64 } |
| 64 | 65 |
| 65 /* | |
| 66 * This function cleans up the gif object after the decode completes | |
| 67 * It is used in a SkAutoTCallIProc template | |
| 68 */ | |
| 69 void SkGifCodec::CloseGif(GifFileType* gif) { | |
| 70 #if GIFLIB_MAJOR < 5 || (GIFLIB_MAJOR == 5 && GIFLIB_MINOR == 0) | |
| 71 DGifCloseFile(gif); | |
| 72 #else | |
| 73 DGifCloseFile(gif, NULL); | |
| 74 #endif | |
| 75 } | |
| 76 | |
| 77 /* | |
| 78 * This function free extension data that has been saved to assist the image | |
| 79 * decoder | |
| 80 */ | |
| 81 void SkGifCodec::FreeExtension(SavedImage* image) { | |
| 82 if (NULL != image->ExtensionBlocks) { | |
| 83 GifFreeExtensions(&image->ExtensionBlockCount, &image->ExtensionBlocks); | |
| 84 } | |
| 85 } | |
| 86 | |
| 87 /* | 66 /* |
| 88 * Check if a there is an index of the color table for a transparent pixel | 67 * Check if a there is an index of the color table for a transparent pixel |
| 89 */ | 68 */ |
| 90 static uint32_t find_trans_index(const SavedImage& image) { | 69 static uint32_t find_trans_index(const SavedImage& image) { |
| 91 // If there is a transparent index specified, it will be contained in an | 70 // If there is a transparent index specified, it will be contained in an |
| 92 // extension block. We will loop through extension blocks in reverse order | 71 // extension block. We will loop through extension blocks in reverse order |
| 93 // to check the most recent extension blocks first. | 72 // to check the most recent extension blocks first. |
| 94 for (int32_t i = image.ExtensionBlockCount - 1; i >= 0; i--) { | 73 for (int32_t i = image.ExtensionBlockCount - 1; i >= 0; i--) { |
| 95 // Get an extension block | 74 // Get an extension block |
| 96 const ExtensionBlock& extBlock = image.ExtensionBlocks[i]; | 75 const ExtensionBlock& extBlock = image.ExtensionBlocks[i]; |
| 97 | 76 |
| 98 // Specifically, we need to check for a graphics control extension, | 77 // Specifically, we need to check for a graphics control extension, |
| 99 // which may contain transparency information. Also, note that a valid | 78 // which may contain transparency information. Also, note that a valid |
| 100 // graphics control extension is always four bytes. The fourth byte | 79 // graphics control extension is always four bytes. The fourth byte |
| 101 // is the transparent index (if it exists), so we need at least four | 80 // is the transparent index (if it exists), so we need at least four |
| 102 // bytes. | 81 // bytes. |
| 103 if (GRAPHICS_EXT_FUNC_CODE == extBlock.Function && | 82 if (GRAPHICS_EXT_FUNC_CODE == extBlock.Function && extBlock.ByteCount >=
4) { |
| 104 extBlock.ByteCount >= 4) { | |
| 105 | |
| 106 // Check the transparent color flag which indicates whether a | 83 // Check the transparent color flag which indicates whether a |
| 107 // transparent index exists. It is the least significant bit of | 84 // transparent index exists. It is the least significant bit of |
| 108 // the first byte of the extension block. | 85 // the first byte of the extension block. |
| 109 if (1 == (extBlock.Bytes[0] & 1)) { | 86 if (1 == (extBlock.Bytes[0] & 1)) { |
| 110 | |
| 111 // Use uint32_t to prevent sign extending | 87 // Use uint32_t to prevent sign extending |
| 112 return extBlock.Bytes[3]; | 88 return extBlock.Bytes[3]; |
| 113 } | 89 } |
| 114 | 90 |
| 115 // There should only be one graphics control extension for the image
frame | 91 // There should only be one graphics control extension for the image
frame |
| 116 break; | 92 break; |
| 117 } | 93 } |
| 118 } | 94 } |
| 119 | 95 |
| 120 // Use maximum unsigned int (surely an invalid index) to indicate that a val
id | 96 // Use maximum unsigned int (surely an invalid index) to indicate that a val
id |
| 121 // index was not found. | 97 // index was not found. |
| 122 return SK_MaxU32; | 98 return SK_MaxU32; |
| 123 } | 99 } |
| 124 | 100 |
| 125 /* | 101 /* |
| 102 * Gets the output row corresponding to the encoded row for interlaced gifs |
| 103 */ |
| 104 static uint32_t get_output_row_interlaced(uint32_t encodedRow, uint32_t height)
{ |
| 105 SkASSERT(encodedRow < height); |
| 106 // First pass |
| 107 if (encodedRow * 8 < height) { |
| 108 return encodedRow * 8; |
| 109 // Second pass |
| 110 } else if (encodedRow * 4 < height) { |
| 111 return 4 + 8 * (encodedRow - ((height + 7) / 8)); |
| 112 // Third pass |
| 113 } else if (encodedRow * 2 < height) { |
| 114 return 2 + 4 * (encodedRow - ((height + 3) / 4)); |
| 115 // Fourth pass |
| 116 } else { |
| 117 return 1 + 2 * (encodedRow - ((height + 1) / 2)); |
| 118 } |
| 119 } |
| 120 |
| 121 /* |
| 122 * This function cleans up the gif object after the decode completes |
| 123 * It is used in a SkAutoTCallIProc template |
| 124 */ |
| 125 void SkGifCodec::CloseGif(GifFileType* gif) { |
| 126 #if GIFLIB_MAJOR < 5 || (GIFLIB_MAJOR == 5 && GIFLIB_MINOR == 0) |
| 127 DGifCloseFile(gif); |
| 128 #else |
| 129 DGifCloseFile(gif, NULL); |
| 130 #endif |
| 131 } |
| 132 |
| 133 /* |
| 134 * This function free extension data that has been saved to assist the image |
| 135 * decoder |
| 136 */ |
| 137 void SkGifCodec::FreeExtension(SavedImage* image) { |
| 138 if (NULL != image->ExtensionBlocks) { |
| 139 GifFreeExtensions(&image->ExtensionBlockCount, &image->ExtensionBlocks); |
| 140 } |
| 141 } |
| 142 |
| 143 /* |
| 126 * Read enough of the stream to initialize the SkGifCodec. | 144 * Read enough of the stream to initialize the SkGifCodec. |
| 127 * Returns a bool representing success or failure. | 145 * Returns a bool representing success or failure. |
| 128 * | 146 * |
| 129 * @param codecOut | 147 * @param codecOut |
| 130 * If it returned true, and codecOut was not NULL, | 148 * If it returned true, and codecOut was not NULL, |
| 131 * codecOut will be set to a new SkGifCodec. | 149 * codecOut will be set to a new SkGifCodec. |
| 132 * | 150 * |
| 133 * @param gifOut | 151 * @param gifOut |
| 134 * If it returned true, and codecOut was NULL, | 152 * If it returned true, and codecOut was NULL, |
| 135 * gifOut must be non-NULL and gifOut will be set to a new | 153 * gifOut must be non-NULL and gifOut will be set to a new |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 193 if (ReadHeader(stream, &codec, NULL)) { | 211 if (ReadHeader(stream, &codec, NULL)) { |
| 194 return codec; | 212 return codec; |
| 195 } | 213 } |
| 196 return NULL; | 214 return NULL; |
| 197 } | 215 } |
| 198 | 216 |
| 199 SkGifCodec::SkGifCodec(const SkImageInfo& srcInfo, SkStream* stream, | 217 SkGifCodec::SkGifCodec(const SkImageInfo& srcInfo, SkStream* stream, |
| 200 GifFileType* gif) | 218 GifFileType* gif) |
| 201 : INHERITED(srcInfo, stream) | 219 : INHERITED(srcInfo, stream) |
| 202 , fGif(gif) | 220 , fGif(gif) |
| 221 , fSrcBuffer(SkNEW_ARRAY(uint8_t, this->getInfo().width())) |
| 222 , fFillIndex(SK_MaxU32) |
| 223 , fFrameDims(SkIRect::MakeEmpty()) |
| 224 , fFrameIsSubset(false) |
| 225 , fColorTable(NULL) |
| 226 , fSwizzler(NULL) |
| 203 {} | 227 {} |
| 204 | 228 |
| 205 bool SkGifCodec::onRewind() { | 229 bool SkGifCodec::onRewind() { |
| 206 GifFileType* gifOut = NULL; | 230 GifFileType* gifOut = NULL; |
| 207 if (!ReadHeader(this->stream(), NULL, &gifOut)) { | 231 if (!ReadHeader(this->stream(), NULL, &gifOut)) { |
| 208 return false; | 232 return false; |
| 209 } | 233 } |
| 210 | 234 |
| 211 SkASSERT(NULL != gifOut); | 235 SkASSERT(NULL != gifOut); |
| 212 fGif.reset(gifOut); | 236 fGif.reset(gifOut); |
| 213 return true; | 237 return true; |
| 214 } | 238 } |
| 215 | 239 |
| 216 /* | 240 SkCodec::Result SkGifCodec::readUpToFirstImage(uint32_t* transIndex) { |
| 217 * Initiates the gif decode | |
| 218 */ | |
| 219 SkCodec::Result SkGifCodec::onGetPixels(const SkImageInfo& dstInfo, | |
| 220 void* dst, size_t dstRowBytes, | |
| 221 const Options& opts, | |
| 222 SkPMColor* inputColorPtr, | |
| 223 int* inputColorCount) { | |
| 224 // Rewind if necessary | |
| 225 if (!this->rewindIfNeeded()) { | |
| 226 return kCouldNotRewind; | |
| 227 } | |
| 228 | |
| 229 // Check for valid input parameters | |
| 230 if (opts.fSubset) { | |
| 231 // Subsets are not supported. | |
| 232 return kUnimplemented; | |
| 233 } | |
| 234 if (dstInfo.dimensions() != this->getInfo().dimensions()) { | |
| 235 return gif_error("Scaling not supported.\n", kInvalidScale); | |
| 236 } | |
| 237 if (!conversion_possible(dstInfo, this->getInfo())) { | |
| 238 return gif_error("Cannot convert input type to output type.\n", | |
| 239 kInvalidConversion); | |
| 240 } | |
| 241 | |
| 242 // Use this as a container to hold information about any gif extension | 241 // Use this as a container to hold information about any gif extension |
| 243 // blocks. This generally stores transparency and animation instructions. | 242 // blocks. This generally stores transparency and animation instructions. |
| 244 SavedImage saveExt; | 243 SavedImage saveExt; |
| 245 SkAutoTCallVProc<SavedImage, FreeExtension> autoFreeExt(&saveExt); | 244 SkAutoTCallVProc<SavedImage, FreeExtension> autoFreeExt(&saveExt); |
| 246 saveExt.ExtensionBlocks = NULL; | 245 saveExt.ExtensionBlocks = NULL; |
| 247 saveExt.ExtensionBlockCount = 0; | 246 saveExt.ExtensionBlockCount = 0; |
| 248 GifByteType* extData; | 247 GifByteType* extData; |
| 249 int32_t extFunction; | 248 int32_t extFunction; |
| 250 | 249 |
| 251 // We will loop over components of gif images until we find an image. Once | 250 // We will loop over components of gif images until we find an image. Once |
| 252 // we find an image, we will decode and return it. While many gif files | 251 // we find an image, we will decode and return it. While many gif files |
| 253 // contain more than one image, we will simply decode the first image. | 252 // contain more than one image, we will simply decode the first image. |
| 254 const int32_t width = dstInfo.width(); | |
| 255 const int32_t height = dstInfo.height(); | |
| 256 GifRecordType recordType; | 253 GifRecordType recordType; |
| 257 do { | 254 do { |
| 258 // Get the current record type | 255 // Get the current record type |
| 259 if (GIF_ERROR == DGifGetRecordType(fGif, &recordType)) { | 256 if (GIF_ERROR == DGifGetRecordType(fGif, &recordType)) { |
| 260 return gif_error("DGifGetRecordType failed.\n", kInvalidInput); | 257 return gif_error("DGifGetRecordType failed.\n", kInvalidInput); |
| 261 } | 258 } |
| 262 | |
| 263 switch (recordType) { | 259 switch (recordType) { |
| 264 case IMAGE_DESC_RECORD_TYPE: { | 260 case IMAGE_DESC_RECORD_TYPE: { |
| 265 // Read the image descriptor | 261 *transIndex = find_trans_index(saveExt); |
| 266 if (GIF_ERROR == DGifGetImageDesc(fGif)) { | |
| 267 return gif_error("DGifGetImageDesc failed.\n", | |
| 268 kInvalidInput); | |
| 269 } | |
| 270 | |
| 271 // If reading the image descriptor is successful, the image | |
| 272 // count will be incremented | |
| 273 SkASSERT(fGif->ImageCount >= 1); | |
| 274 SavedImage* image = &fGif->SavedImages[fGif->ImageCount - 1]; | |
| 275 | |
| 276 // Process the descriptor | |
| 277 const GifImageDesc& desc = image->ImageDesc; | |
| 278 int32_t imageLeft = desc.Left; | |
| 279 int32_t imageTop = desc.Top; | |
| 280 int32_t innerWidth = desc.Width; | |
| 281 int32_t innerHeight = desc.Height; | |
| 282 // Fail on non-positive dimensions | |
| 283 if (innerWidth <= 0 || innerHeight <= 0) { | |
| 284 return gif_error("Invalid dimensions for inner image.\n", | |
| 285 kInvalidInput); | |
| 286 } | |
| 287 // Treat the following cases as warnings and try to fix | |
| 288 if (innerWidth > width) { | |
| 289 gif_warning("Inner image too wide, shrinking.\n"); | |
| 290 innerWidth = width; | |
| 291 imageLeft = 0; | |
| 292 } else if (imageLeft + innerWidth > width) { | |
| 293 gif_warning("Shifting inner image to left to fit.\n"); | |
| 294 imageLeft = width - innerWidth; | |
| 295 } else if (imageLeft < 0) { | |
| 296 gif_warning("Shifting image to right to fit\n"); | |
| 297 imageLeft = 0; | |
| 298 } | |
| 299 if (innerHeight > height) { | |
| 300 gif_warning("Inner image too tall, shrinking.\n"); | |
| 301 innerHeight = height; | |
| 302 imageTop = 0; | |
| 303 } else if (imageTop + innerHeight > height) { | |
| 304 gif_warning("Shifting inner image up to fit.\n"); | |
| 305 imageTop = height - innerHeight; | |
| 306 } else if (imageTop < 0) { | |
| 307 gif_warning("Shifting image down to fit\n"); | |
| 308 imageTop = 0; | |
| 309 } | |
| 310 | |
| 311 // Create a color table to store colors the giflib colorMap | |
| 312 SkPMColor alternateColorPtr[256]; | |
| 313 SkPMColor* colorTable; | |
| 314 SkColorType dstColorType = dstInfo.colorType(); | |
| 315 if (kIndex_8_SkColorType == dstColorType) { | |
| 316 SkASSERT(NULL != inputColorPtr); | |
| 317 SkASSERT(NULL != inputColorCount); | |
| 318 colorTable = inputColorPtr; | |
| 319 } else { | |
| 320 colorTable = alternateColorPtr; | |
| 321 } | |
| 322 | |
| 323 // Set up the color table | |
| 324 uint32_t colorCount = 0; | |
| 325 // Allocate maximum storage to deal with invalid indices safely | |
| 326 const uint32_t maxColors = 256; | |
| 327 ColorMapObject* colorMap = fGif->Image.ColorMap; | |
| 328 // If there is no local color table, use the global color table | |
| 329 if (NULL == colorMap) { | |
| 330 colorMap = fGif->SColorMap; | |
| 331 } | |
| 332 if (NULL != colorMap) { | |
| 333 colorCount = colorMap->ColorCount; | |
| 334 SkASSERT(colorCount == | |
| 335 (unsigned) (1 << (colorMap->BitsPerPixel))); | |
| 336 SkASSERT(colorCount <= 256); | |
| 337 for (uint32_t i = 0; i < colorCount; i++) { | |
| 338 colorTable[i] = SkPackARGB32(0xFF, | |
| 339 colorMap->Colors[i].Red, | |
| 340 colorMap->Colors[i].Green, | |
| 341 colorMap->Colors[i].Blue); | |
| 342 } | |
| 343 } | |
| 344 | |
| 345 // This is used to fill unspecified pixels in the image data. | |
| 346 uint32_t fillIndex = fGif->SBackGroundColor; | |
| 347 ZeroInitialized zeroInit = opts.fZeroInitialized; | |
| 348 | |
| 349 // Gifs have the option to specify the color at a single | |
| 350 // index of the color table as transparent. | |
| 351 { | |
| 352 // Get the transparent index. If the return value of this | |
| 353 // function is greater than the colorCount, we know that | |
| 354 // there is no valid transparent color in the color table. | |
| 355 // This occurs if there is no graphics control extension or | |
| 356 // if the index specified by the graphics control extension | |
| 357 // is out of range. | |
| 358 uint32_t transIndex = find_trans_index(saveExt); | |
| 359 | |
| 360 if (transIndex < colorCount) { | |
| 361 colorTable[transIndex] = SK_ColorTRANSPARENT; | |
| 362 // If there is a transparent index, we also use this as | |
| 363 // the fill index. | |
| 364 fillIndex = transIndex; | |
| 365 } else if (fillIndex >= colorCount) { | |
| 366 // If the fill index is invalid, we default to 0. This | |
| 367 // behavior is unspecified but matches SkImageDecoder. | |
| 368 fillIndex = 0; | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 // Fill in the color table for indices greater than color count. | |
| 373 // This allows for predictable, safe behavior. | |
| 374 for (uint32_t i = colorCount; i < maxColors; i++) { | |
| 375 colorTable[i] = colorTable[fillIndex]; | |
| 376 } | |
| 377 | |
| 378 // Check if image is only a subset of the image frame | |
| 379 SkAutoTDelete<SkSwizzler> swizzler(NULL); | |
| 380 if (innerWidth < width || innerHeight < height) { | |
| 381 | |
| 382 // Modify the destination info | |
| 383 const SkImageInfo subsetDstInfo = | |
| 384 dstInfo.makeWH(innerWidth, innerHeight); | |
| 385 | |
| 386 // Fill the destination with the fill color | |
| 387 // FIXME: This may not be the behavior that we want for | |
| 388 // animated gifs where we draw on top of the | |
| 389 // previous frame. | |
| 390 SkSwizzler::Fill(dst, dstInfo, dstRowBytes, height, | |
| 391 fillIndex, colorTable, zeroInit); | |
| 392 | |
| 393 // Modify the dst pointer | |
| 394 const int32_t dstBytesPerPixel = | |
| 395 SkColorTypeBytesPerPixel(dstColorType); | |
| 396 dst = SkTAddOffset<void*>(dst, | |
| 397 dstRowBytes * imageTop + | |
| 398 dstBytesPerPixel * imageLeft); | |
| 399 | |
| 400 // Create the subset swizzler | |
| 401 swizzler.reset(SkSwizzler::CreateSwizzler( | |
| 402 SkSwizzler::kIndex, colorTable, subsetDstInfo, | |
| 403 zeroInit, this->getInfo())); | |
| 404 } else { | |
| 405 // Create the fully dimensional swizzler | |
| 406 swizzler.reset(SkSwizzler::CreateSwizzler( | |
| 407 SkSwizzler::kIndex, colorTable, dstInfo, | |
| 408 zeroInit, this->getInfo())); | |
| 409 } | |
| 410 | |
| 411 // Stores output from dgiflib and input to the swizzler | |
| 412 SkAutoTDeleteArray<uint8_t> | |
| 413 buffer(SkNEW_ARRAY(uint8_t, innerWidth)); | |
| 414 | |
| 415 // Check the interlace flag and iterate over rows of the input | |
| 416 if (fGif->Image.Interlace) { | |
| 417 // In interlace mode, the rows of input are rearranged in | |
| 418 // the output image. We use an iterator to take care of | |
| 419 // the rearranging. | |
| 420 SkGifInterlaceIter iter(innerHeight); | |
| 421 for (int32_t y = 0; y < innerHeight; y++) { | |
| 422 if (GIF_ERROR == DGifGetLine(fGif, buffer.get(), | |
| 423 innerWidth)) { | |
| 424 // Recover from error by filling remainder of image | |
| 425 memset(buffer.get(), fillIndex, innerWidth); | |
| 426 for (; y < innerHeight; y++) { | |
| 427 void* dstRow = SkTAddOffset<void>(dst, | |
| 428 dstRowBytes * iter.nextY()); | |
| 429 swizzler->swizzle(dstRow, buffer.get()); | |
| 430 } | |
| 431 return gif_error(SkStringPrintf( | |
| 432 "Could not decode line %d of %d.\n", | |
| 433 y, height - 1).c_str(), kIncompleteInput); | |
| 434 } | |
| 435 void* dstRow = SkTAddOffset<void>( | |
| 436 dst, dstRowBytes * iter.nextY()); | |
| 437 swizzler->swizzle(dstRow, buffer.get()); | |
| 438 } | |
| 439 } else { | |
| 440 // Standard mode | |
| 441 void* dstRow = dst; | |
| 442 for (int32_t y = 0; y < innerHeight; y++) { | |
| 443 if (GIF_ERROR == DGifGetLine(fGif, buffer.get(), | |
| 444 innerWidth)) { | |
| 445 SkSwizzler::Fill(dstRow, dstInfo, dstRowBytes, | |
| 446 innerHeight - y, fillIndex, colorTable, | |
| 447 zeroInit); | |
| 448 return gif_error(SkStringPrintf( | |
| 449 "Could not decode line %d of %d.\n", | |
| 450 y, height - 1).c_str(), kIncompleteInput); | |
| 451 } | |
| 452 swizzler->swizzle(dstRow, buffer.get()); | |
| 453 dstRow = SkTAddOffset<void>(dstRow, dstRowBytes); | |
| 454 } | |
| 455 } | |
| 456 | |
| 457 // FIXME: Gif files may have multiple images stored in a single | 262 // FIXME: Gif files may have multiple images stored in a single |
| 458 // file. This is most commonly used to enable | 263 // file. This is most commonly used to enable |
| 459 // animations. Since we are leaving animated gifs as a | 264 // animations. Since we are leaving animated gifs as a |
| 460 // TODO, we will return kSuccess after decoding the | 265 // TODO, we will return kSuccess after decoding the |
| 461 // first image in the file. This is the same behavior | 266 // first image in the file. This is the same behavior |
| 462 // as SkImageDecoder_libgif. | 267 // as SkImageDecoder_libgif. |
| 463 // | 268 // |
| 464 // Most times this works pretty well, but sometimes it | 269 // Most times this works pretty well, but sometimes it |
| 465 // doesn't. For example, I have an animated test image | 270 // doesn't. For example, I have an animated test image |
| 466 // where the first image in the file is 1x1, but the | 271 // where the first image in the file is 1x1, but the |
| 467 // subsequent images are meaningful. This currently | 272 // subsequent images are meaningful. This currently |
| 468 // displays the 1x1 image, which is not ideal. Right | 273 // displays the 1x1 image, which is not ideal. Right |
| 469 // now I am leaving this as an issue that will be | 274 // now I am leaving this as an issue that will be |
| 470 // addressed when we implement animated gifs. | 275 // addressed when we implement animated gifs. |
| 471 // | 276 // |
| 472 // It is also possible (not explicitly disallowed in the | 277 // It is also possible (not explicitly disallowed in the |
| 473 // specification) that gif files provide multiple | 278 // specification) that gif files provide multiple |
| 474 // images in a single file that are all meant to be | 279 // images in a single file that are all meant to be |
| 475 // displayed in the same frame together. I will | 280 // displayed in the same frame together. I will |
| 476 // currently leave this unimplemented until I find a | 281 // currently leave this unimplemented until I find a |
| 477 // test case that expects this behavior. | 282 // test case that expects this behavior. |
| 478 return kSuccess; | 283 return kSuccess; |
| 479 } | 284 } |
| 480 | |
| 481 // Extensions are used to specify special properties of the image | 285 // Extensions are used to specify special properties of the image |
| 482 // such as transparency or animation. | 286 // such as transparency or animation. |
| 483 case EXTENSION_RECORD_TYPE: | 287 case EXTENSION_RECORD_TYPE: |
| 484 // Read extension data | 288 // Read extension data |
| 485 if (GIF_ERROR == | 289 if (GIF_ERROR == |
| 486 DGifGetExtension(fGif, &extFunction, &extData)) { | 290 DGifGetExtension(fGif, &extFunction, &extData)) { |
| 487 return gif_error("Could not get extension.\n", | 291 return gif_error("Could not get extension.\n", |
| 488 kIncompleteInput); | 292 kIncompleteInput); |
| 489 } | 293 } |
| 490 | 294 |
| 491 // Create an extension block with our data | 295 // Create an extension block with our data |
| 492 while (NULL != extData) { | 296 while (NULL != extData) { |
| 493 // Add a single block | 297 // Add a single block |
| 494 if (GIF_ERROR == | 298 if (GIF_ERROR == |
| 495 GifAddExtensionBlock(&saveExt.ExtensionBlockCount, | 299 GifAddExtensionBlock(&saveExt.ExtensionBlockCount, |
| 496 &saveExt.ExtensionBlocks, extFunction, extData[0], | 300 &saveExt.ExtensionBlocks, extFunction, extData[0], |
| 497 &extData[1])) { | 301 &extData[1])) { |
| 498 return gif_error("Could not add extension block.\n", | 302 return gif_error("Could not add extension block.\n", |
| 499 kIncompleteInput); | 303 kIncompleteInput); |
| 500 } | 304 } |
| 501 // Move to the next block | 305 // Move to the next block |
| 502 if (GIF_ERROR == DGifGetExtensionNext(fGif, &extData)) { | 306 if (GIF_ERROR == DGifGetExtensionNext(fGif, &extData)) { |
| 503 return gif_error("Could not get next extension.\n", | 307 return gif_error("Could not get next extension.\n", kInc
ompleteInput); |
| 504 kIncompleteInput); | |
| 505 } | 308 } |
| 506 } | 309 } |
| 507 break; | 310 break; |
| 508 | 311 |
| 509 // Signals the end of the gif file | 312 // Signals the end of the gif file |
| 510 case TERMINATE_RECORD_TYPE: | 313 case TERMINATE_RECORD_TYPE: |
| 511 break; | 314 break; |
| 512 | 315 |
| 513 default: | 316 default: |
| 514 // giflib returns an error code if the record type is not known. | 317 // giflib returns an error code if the record type is not known. |
| 515 // We should catch this error immediately. | 318 // This should not be reached. |
| 516 SkASSERT(false); | 319 SkASSERT(false); |
| 517 break; | 320 break; |
| 518 } | 321 } |
| 519 } while (TERMINATE_RECORD_TYPE != recordType); | 322 } while (TERMINATE_RECORD_TYPE != recordType); |
| 520 | 323 |
| 521 return gif_error("Could not find any images to decode in gif file.\n", | 324 return gif_error("Could not find any images to decode in gif file.\n", kInva
lidInput); |
| 522 kInvalidInput); | 325 } |
| 523 } | 326 |
| 327 /* |
| 328 * A gif may contain many image frames, all of different sizes. |
| 329 * This function checks if the frame dimensions are valid and corrects them if |
| 330 * necessary. |
| 331 */ |
| 332 bool SkGifCodec::setFrameDimensions(const GifImageDesc& desc) { |
| 333 // Fail on non-positive dimensions |
| 334 int32_t frameLeft = desc.Left; |
| 335 int32_t frameTop = desc.Top; |
| 336 int32_t frameWidth = desc.Width; |
| 337 int32_t frameHeight = desc.Height; |
| 338 int32_t height = this->getInfo().height(); |
| 339 int32_t width = this->getInfo().width(); |
| 340 if (frameWidth <= 0 || frameHeight <= 0) { |
| 341 return false; |
| 342 } |
| 343 |
| 344 // Treat the following cases as warnings and try to fix |
| 345 if (frameWidth > width) { |
| 346 gif_warning("Image frame too wide, shrinking.\n"); |
| 347 frameWidth = width; |
| 348 frameLeft = 0; |
| 349 } else if (frameLeft + frameWidth > width) { |
| 350 gif_warning("Shifting image frame to left to fit.\n"); |
| 351 frameLeft = width - frameWidth; |
| 352 } else if (frameLeft < 0) { |
| 353 gif_warning("Shifting image frame to right to fit\n"); |
| 354 frameLeft = 0; |
| 355 } |
| 356 if (frameHeight > height) { |
| 357 gif_warning("Image frame too tall, shrinking.\n"); |
| 358 frameHeight = height; |
| 359 frameTop = 0; |
| 360 } else if (frameTop + frameHeight > height) { |
| 361 gif_warning("Shifting image frame up to fit.\n"); |
| 362 frameTop = height - frameHeight; |
| 363 } else if (frameTop < 0) { |
| 364 gif_warning("Shifting image frame down to fit\n"); |
| 365 frameTop = 0; |
| 366 } |
| 367 fFrameDims.setXYWH(frameLeft, frameTop, frameWidth, frameHeight); |
| 368 |
| 369 // Indicate if the frame dimensions do not match the header dimensions |
| 370 if (this->getInfo().width() != fFrameDims.width() || |
| 371 this->getInfo().height() != fFrameDims.height()) { |
| 372 fFrameIsSubset = true; |
| 373 } |
| 374 |
| 375 return true; |
| 376 } |
| 377 |
| 378 void SkGifCodec::initializeColorTable(int* inputColorCount, uint32_t transIndex)
{ |
| 379 // Set up our own color table |
| 380 const uint32_t maxColors = 256; |
| 381 SkPMColor colorPtr[256]; |
| 382 if (NULL != inputColorCount) { |
| 383 // We set the number of colors to maxColors in order to ensure |
| 384 // safe memory accesses. Otherwise, an invalid pixel could |
| 385 // access memory outside of our color table array. |
| 386 *inputColorCount = maxColors; |
| 387 } |
| 388 |
| 389 // Get local color table |
| 390 ColorMapObject* colorMap = fGif->Image.ColorMap; |
| 391 // If there is no local color table, use the global color table |
| 392 if (NULL == colorMap) { |
| 393 colorMap = fGif->SColorMap; |
| 394 } |
| 395 |
| 396 uint32_t colorCount = 0; |
| 397 if (NULL != colorMap) { |
| 398 colorCount = colorMap->ColorCount; |
| 399 SkASSERT(colorCount == (unsigned) (1 << (colorMap->BitsPerPixel))); |
| 400 SkASSERT(colorCount <= 256); |
| 401 for (uint32_t i = 0; i < colorCount; i++) { |
| 402 colorPtr[i] = SkPackARGB32(0xFF, colorMap->Colors[i].Red, |
| 403 colorMap->Colors[i].Green, colorMap->Colors[i].Blue); |
| 404 } |
| 405 } |
| 406 |
| 407 // This is used to fill unspecified pixels in the image data. |
| 408 uint32_t fillIndex = fGif->SBackGroundColor; |
| 409 |
| 410 // Gifs have the option to specify the color at a single index of the color |
| 411 // table as transparent. If the transparent index is greater than the |
| 412 // colorCount, we know that there is no valid transparent color in the |
| 413 // color table. This occurs if there is no graphics control extension or |
| 414 // if the index specified by the graphics control extension is out of range. |
| 415 if (transIndex < colorCount) { |
| 416 colorPtr[transIndex] = SK_ColorTRANSPARENT; |
| 417 // If there is a transparent index, we also use this as the fill index. |
| 418 fillIndex = transIndex; |
| 419 } else if (fillIndex >= colorCount) { |
| 420 // If the fill index is invalid, we default to 0. This behavior is |
| 421 // unspecified but matches SkImageDecoder. |
| 422 fillIndex = 0; |
| 423 } |
| 424 |
| 425 // Fill in the color table for indices greater than color count. |
| 426 // This allows for predictable, safe behavior. |
| 427 for (uint32_t i = colorCount; i < maxColors; i++) { |
| 428 colorPtr[i] = colorPtr[fillIndex]; |
| 429 } |
| 430 |
| 431 fColorTable.reset(SkNEW_ARGS(SkColorTable, (colorPtr, maxColors))); |
| 432 fFillIndex = fillIndex; |
| 433 } |
| 434 |
| 435 SkCodec::Result SkGifCodec::initializeSwizzler(const SkImageInfo& dstInfo, |
| 436 ZeroInitialized zeroInit) { |
| 437 const SkPMColor* colorPtr = get_color_ptr(fColorTable.get()); |
| 438 fSwizzler.reset(SkSwizzler::CreateSwizzler(SkSwizzler::kIndex, |
| 439 colorPtr, dstInfo, zeroInit, this->getInfo())); |
| 440 if (NULL != fSwizzler.get()) { |
| 441 return kSuccess; |
| 442 } |
| 443 return kUnimplemented; |
| 444 } |
| 445 |
| 446 SkCodec::Result SkGifCodec::readRow() { |
| 447 if (GIF_ERROR == DGifGetLine(fGif, fSrcBuffer.get(), fFrameDims.width())) { |
| 448 return kIncompleteInput; |
| 449 } |
| 450 return kSuccess; |
| 451 } |
| 452 |
| 453 /* |
| 454 * Initiates the gif decode |
| 455 */ |
| 456 SkCodec::Result SkGifCodec::onGetPixels(const SkImageInfo& dstInfo, |
| 457 void* dst, size_t dstRowBytes, |
| 458 const Options& opts, |
| 459 SkPMColor* inputColorPtr, |
| 460 int* inputColorCount) { |
| 461 // Rewind if necessary |
| 462 if (!this->rewindIfNeeded()) { |
| 463 return kCouldNotRewind; |
| 464 } |
| 465 |
| 466 // Check for valid input parameters |
| 467 if (opts.fSubset) { |
| 468 // Subsets are not supported. |
| 469 return kUnimplemented; |
| 470 } |
| 471 if (dstInfo.dimensions() != this->getInfo().dimensions()) { |
| 472 return gif_error("Scaling not supported.\n", kInvalidScale); |
| 473 } |
| 474 if (!conversion_possible(dstInfo, this->getInfo())) { |
| 475 return gif_error("Cannot convert input type to output type.\n", |
| 476 kInvalidConversion); |
| 477 } |
| 478 |
| 479 // Read through gif extensions to get to the image data. Set the |
| 480 // transparent index based on the extension data. |
| 481 uint32_t transIndex; |
| 482 SkCodec::Result result = this->readUpToFirstImage(&transIndex); |
| 483 if (kSuccess != result){ |
| 484 return result; |
| 485 } |
| 486 |
| 487 // Read the image descriptor |
| 488 if (GIF_ERROR == DGifGetImageDesc(fGif)) { |
| 489 return gif_error("DGifGetImageDesc failed.\n", kInvalidInput); |
| 490 } |
| 491 |
| 492 // If reading the image descriptor is successful, the image count will be |
| 493 // incremented |
| 494 SkASSERT(fGif->ImageCount >= 1); |
| 495 SavedImage* image = &fGif->SavedImages[fGif->ImageCount - 1]; |
| 496 const GifImageDesc& desc = image->ImageDesc; |
| 497 |
| 498 // Check that the frame dimensions are valid and set them |
| 499 if(!this->setFrameDimensions(desc)) { |
| 500 return gif_error("Invalid dimensions for image frame.\n", kInvalidInput)
; |
| 501 } |
| 502 |
| 503 // Initialize color table and copy to the client if necessary |
| 504 this->initializeColorTable(inputColorCount, transIndex); |
| 505 copy_color_table(dstInfo, this->fColorTable, inputColorPtr, inputColorCount)
; |
| 506 |
| 507 // Initialize the swizzler |
| 508 if (fFrameIsSubset) { |
| 509 const SkImageInfo subsetDstInfo = dstInfo.makeWH(fFrameDims.width(), fFr
ameDims.height()); |
| 510 if (kSuccess != this->initializeSwizzler(subsetDstInfo, opts.fZeroInitia
lized)) { |
| 511 return gif_error("Could not initialize swizzler.\n", kUnimplemented)
; |
| 512 } |
| 513 |
| 514 // Fill the background |
| 515 const SkPMColor* colorPtr = get_color_ptr(fColorTable.get()); |
| 516 SkSwizzler::Fill(dst, dstInfo, dstRowBytes, this->getInfo().height(), |
| 517 fFillIndex, colorPtr, opts.fZeroInitialized); |
| 518 |
| 519 // Modify the dst pointer |
| 520 const int32_t dstBytesPerPixel = SkColorTypeBytesPerPixel(dstInfo.colorT
ype()); |
| 521 dst = SkTAddOffset<void*>(dst, dstRowBytes * fFrameDims.top() + |
| 522 dstBytesPerPixel * fFrameDims.left()); |
| 523 } else { |
| 524 if (kSuccess != this->initializeSwizzler(dstInfo, opts.fZeroInitialized)
) { |
| 525 return gif_error("Could not initialize swizzler.\n", kUnimplemented)
; |
| 526 } |
| 527 } |
| 528 |
| 529 // Check the interlace flag and iterate over rows of the input |
| 530 uint32_t width = fFrameDims.width(); |
| 531 uint32_t height = fFrameDims.height(); |
| 532 if (fGif->Image.Interlace) { |
| 533 // In interlace mode, the rows of input are rearranged in |
| 534 // the output image. We use an iterator to take care of |
| 535 // the rearranging. |
| 536 for (int32_t y = 0; y < height; y++) { |
| 537 if (kSuccess != this->readRow()) { |
| 538 // Recover from error by filling remainder of image |
| 539 memset(fSrcBuffer.get(), fFillIndex, width); |
| 540 for (; y < height; y++) { |
| 541 void* dstRow = SkTAddOffset<void>(dst, |
| 542 dstRowBytes * get_output_row_interlaced(y, height)); |
| 543 fSwizzler->swizzle(dstRow, fSrcBuffer.get()); |
| 544 } |
| 545 return gif_error("Could not decode line.\n", kIncompleteInput); |
| 546 } |
| 547 void* dstRow = SkTAddOffset<void>(dst, |
| 548 dstRowBytes * get_output_row_interlaced(y, height)); |
| 549 fSwizzler->swizzle(dstRow, fSrcBuffer.get()); |
| 550 } |
| 551 } else { |
| 552 // Standard mode |
| 553 void* dstRow = dst; |
| 554 for (int32_t y = 0; y < height; y++) { |
| 555 if (kSuccess != this->readRow()) { |
| 556 const SkPMColor* colorPtr = get_color_ptr(fColorTable.get()); |
| 557 SkSwizzler::Fill(dstRow, dstInfo, dstRowBytes, |
| 558 height - y, fFillIndex, colorPtr, opts.fZeroInitialized)
; |
| 559 return gif_error("Could not decode line\n", kIncompleteInput); |
| 560 } |
| 561 fSwizzler->swizzle(dstRow, fSrcBuffer.get()); |
| 562 dstRow = SkTAddOffset<void>(dstRow, dstRowBytes); |
| 563 } |
| 564 } |
| 565 return kSuccess; |
| 566 } |
| 567 |
| 568 class SkGifScanlineDecoder : public SkScanlineDecoder { |
| 569 public: |
| 570 SkGifScanlineDecoder(const SkImageInfo& srcInfo, SkGifCodec* codec) |
| 571 : INHERITED(srcInfo) |
| 572 , fCodec(codec) |
| 573 {} |
| 574 |
| 575 SkEncodedFormat onGetEncodedFormat() const override { |
| 576 return kGIF_SkEncodedFormat; |
| 577 } |
| 578 |
| 579 SkCodec::Result onStart(const SkImageInfo& dstInfo, const SkCodec::Options&
opts, |
| 580 SkPMColor inputColorPtr[], int* inputColorCount) ove
rride { |
| 581 |
| 582 // Rewind if necessary |
| 583 if (!fCodec->rewindIfNeeded()) { |
| 584 return SkCodec::kCouldNotRewind; |
| 585 } |
| 586 // Check for valid input parameters |
| 587 if (opts.fSubset) { |
| 588 // Subsets are not supported. |
| 589 return SkCodec::kUnimplemented; |
| 590 } |
| 591 // Check to see if scaling was requested. |
| 592 if (dstInfo.dimensions() != this->getInfo().dimensions()) { |
| 593 if (!SkScaledCodec::DimensionsSupportedForSampling(this->getInfo(),
dstInfo)) { |
| 594 return gif_error("Scaling not supported.\n", SkCodec::kInvalidSc
ale); |
| 595 } |
| 596 } |
| 597 if (!conversion_possible(dstInfo, this->getInfo())) { |
| 598 return gif_error("Cannot convert input type to output type.\n", |
| 599 SkCodec::kInvalidConversion); |
| 600 } |
| 601 |
| 602 // Read through gif extensions to get to the image data |
| 603 uint32_t transIndex; |
| 604 SkCodec::Result result = fCodec->readUpToFirstImage(&transIndex); |
| 605 if (SkCodec::kSuccess != result){ |
| 606 return result; |
| 607 } |
| 608 |
| 609 // Read the image descriptor |
| 610 if (GIF_ERROR == DGifGetImageDesc(fCodec->fGif)) { |
| 611 return gif_error("DGifGetImageDesc failed.\n", SkCodec::kInvalidInpu
t); |
| 612 } |
| 613 |
| 614 // If reading the image descriptor is successful, the image count will b
e |
| 615 // incremented |
| 616 SkASSERT(fCodec->fGif->ImageCount >= 1); |
| 617 SavedImage* image = &fCodec->fGif->SavedImages[fCodec->fGif->ImageCount
- 1]; |
| 618 const GifImageDesc& desc = image->ImageDesc; |
| 619 |
| 620 // Check that the frame dimensions are valid and set them |
| 621 if(!fCodec->setFrameDimensions(desc)) { |
| 622 return gif_error("Invalid dimensions for image frame.\n", SkCodec::k
InvalidInput); |
| 623 } |
| 624 |
| 625 // Initialize color table and copy to the client if necessary |
| 626 fCodec->initializeColorTable(inputColorCount, transIndex); |
| 627 copy_color_table(dstInfo, fCodec->fColorTable, inputColorPtr, inputColor
Count); |
| 628 |
| 629 // Initialize the swizzler |
| 630 if (fCodec->fFrameIsSubset) { |
| 631 int sampleX; |
| 632 SkScaledCodec::ComputeSampleSize(dstInfo, fCodec->getInfo(), &sample
X, NULL); |
| 633 const SkImageInfo subsetDstInfo = dstInfo.makeWH( |
| 634 SkScaledCodec::GetScaledDimension(fCodec->fFrameDims.width()
, sampleX), |
| 635 fCodec->fFrameDims.height()); |
| 636 if (SkCodec::kSuccess != fCodec->initializeSwizzler(subsetDstInfo, |
| 637 opts.fZeroInitialized)) { |
| 638 return gif_error("Could not initialize swizzler.\n", SkCodec::kU
nimplemented); |
| 639 } |
| 640 } else { |
| 641 if (SkCodec::kSuccess != fCodec->initializeSwizzler(dstInfo, opts.fZ
eroInitialized)) { |
| 642 return gif_error("Could not initialize swizzler.\n", SkCodec::kU
nimplemented); |
| 643 } |
| 644 } |
| 645 |
| 646 return SkCodec::kSuccess; |
| 647 } |
| 648 |
| 649 SkCodec::Result onGetScanlines(void* dst, int count, size_t rowBytes) overri
de { |
| 650 if (fCodec->fFrameIsSubset) { |
| 651 // Fill the requested rows |
| 652 const SkPMColor* colorPtr = get_color_ptr(fCodec->fColorTable.get())
; |
| 653 SkSwizzler::Fill(dst, this->dstInfo(), rowBytes, count, fCodec->fFil
lIndex, |
| 654 colorPtr, this->options().fZeroInitialized); |
| 655 |
| 656 // Do nothing for rows before the image frame |
| 657 int rowsBeforeFrame = fCodec->fFrameDims.top() - INHERITED::getY(); |
| 658 if (rowsBeforeFrame > 0) { |
| 659 count = SkTMin(0, count - rowsBeforeFrame); |
| 660 dst = SkTAddOffset<void>(dst, rowBytes * rowsBeforeFrame); |
| 661 } |
| 662 |
| 663 // Do nothing for rows after the image frame |
| 664 int rowsAfterFrame = INHERITED::getY() + count - fCodec->fFrameDims.
bottom(); |
| 665 if (rowsAfterFrame > 0) { |
| 666 count = SkTMin(0, count - rowsAfterFrame); |
| 667 } |
| 668 |
| 669 // Adjust dst pointer for left offset |
| 670 dst = SkTAddOffset<void>(dst, SkColorTypeBytesPerPixel( |
| 671 this->dstInfo().colorType()) * fCodec->fFrameDims.left()); |
| 672 } |
| 673 |
| 674 for (int i = 0; i < count; i++) { |
| 675 if (SkCodec::kSuccess != fCodec->readRow()) { |
| 676 const SkPMColor* colorPtr = get_color_ptr(fCodec->fColorTable.ge
t()); |
| 677 SkSwizzler::Fill(dst, this->dstInfo(), rowBytes, |
| 678 fCodec->fFrameDims.height() - i, fCodec->fFillIndex, col
orPtr, |
| 679 this->options().fZeroInitialized); |
| 680 return gif_error("Could not decode line\n", SkCodec::kIncomplete
Input); |
| 681 } |
| 682 fCodec->fSwizzler->swizzle(dst, fCodec->fSrcBuffer.get()); |
| 683 dst = SkTAddOffset<void>(dst, rowBytes); |
| 684 } |
| 685 return SkCodec::kSuccess; |
| 686 } |
| 687 |
| 688 SkScanlineOrder onGetScanlineOrder() const override { |
| 689 if (fCodec->fGif->Image.Interlace) { |
| 690 return kOutOfOrder_SkScanlineOrder; |
| 691 } else { |
| 692 return kTopDown_SkScanlineOrder; |
| 693 } |
| 694 } |
| 695 |
| 696 int onGetY() const override { |
| 697 if (fCodec->fGif->Image.Interlace) { |
| 698 return get_output_row_interlaced(INHERITED::onGetY(), this->dstInfo
().height()); |
| 699 } else { |
| 700 return INHERITED::onGetY(); |
| 701 } |
| 702 } |
| 703 |
| 704 private: |
| 705 SkAutoTDelete<SkGifCodec> fCodec; |
| 706 |
| 707 typedef SkScanlineDecoder INHERITED; |
| 708 }; |
| 709 |
| 710 SkScanlineDecoder* SkGifCodec::NewSDFromStream(SkStream* stream) { |
| 711 SkAutoTDelete<SkGifCodec> codec (static_cast<SkGifCodec*>(SkGifCodec::NewFro
mStream(stream))); |
| 712 if (!codec) { |
| 713 return NULL; |
| 714 } |
| 715 |
| 716 const SkImageInfo& srcInfo = codec->getInfo(); |
| 717 |
| 718 return SkNEW_ARGS(SkGifScanlineDecoder, (srcInfo, codec.detach())); |
| 719 } |
| OLD | NEW |