| /* |
| * Copyright 2015 Google Inc. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "SkBmpCodec.h" |
| #include "SkBmpMaskCodec.h" |
| #include "SkBmpRLECodec.h" |
| #include "SkBmpStandardCodec.h" |
| #include "SkCodecPriv.h" |
| #include "SkColorPriv.h" |
| #include "SkStream.h" |
| |
| /* |
| * Defines the version and type of the second bitmap header |
| */ |
| enum BmpHeaderType { |
| kInfoV1_BmpHeaderType, |
| kInfoV2_BmpHeaderType, |
| kInfoV3_BmpHeaderType, |
| kInfoV4_BmpHeaderType, |
| kInfoV5_BmpHeaderType, |
| kOS2V1_BmpHeaderType, |
| kOS2VX_BmpHeaderType, |
| kUnknown_BmpHeaderType |
| }; |
| |
| /* |
| * Possible bitmap compression types |
| */ |
| enum BmpCompressionMethod { |
| kNone_BmpCompressionMethod = 0, |
| k8BitRLE_BmpCompressionMethod = 1, |
| k4BitRLE_BmpCompressionMethod = 2, |
| kBitMasks_BmpCompressionMethod = 3, |
| kJpeg_BmpCompressionMethod = 4, |
| kPng_BmpCompressionMethod = 5, |
| kAlphaBitMasks_BmpCompressionMethod = 6, |
| kCMYK_BmpCompressionMethod = 11, |
| kCMYK8BitRLE_BmpCompressionMethod = 12, |
| kCMYK4BitRLE_BmpCompressionMethod = 13 |
| }; |
| |
| /* |
| * Used to define the input format of the bmp |
| */ |
| enum BmpInputFormat { |
| kStandard_BmpInputFormat, |
| kRLE_BmpInputFormat, |
| kBitMask_BmpInputFormat, |
| kUnknown_BmpInputFormat |
| }; |
| |
| /* |
| * Checks the start of the stream to see if the image is a bitmap |
| */ |
| bool SkBmpCodec::IsBmp(const void* buffer, size_t bytesRead) { |
| // TODO: Support "IC", "PT", "CI", "CP", "BA" |
| const char bmpSig[] = { 'B', 'M' }; |
| return bytesRead >= sizeof(bmpSig) && !memcmp(buffer, bmpSig, sizeof(bmpSig)); |
| } |
| |
| /* |
| * Assumes IsBmp was called and returned true |
| * Creates a bmp decoder |
| * Reads enough of the stream to determine the image format |
| */ |
| SkCodec* SkBmpCodec::NewFromStream(SkStream* stream) { |
| return SkBmpCodec::NewFromStream(stream, false); |
| } |
| |
| /* |
| * Creates a bmp decoder for a bmp embedded in ico |
| * Reads enough of the stream to determine the image format |
| */ |
| SkCodec* SkBmpCodec::NewFromIco(SkStream* stream) { |
| return SkBmpCodec::NewFromStream(stream, true); |
| } |
| |
| // Header size constants |
| static const uint32_t kBmpHeaderBytes = 14; |
| static const uint32_t kBmpHeaderBytesPlusFour = kBmpHeaderBytes + 4; |
| static const uint32_t kBmpOS2V1Bytes = 12; |
| static const uint32_t kBmpOS2V2Bytes = 64; |
| static const uint32_t kBmpInfoBaseBytes = 16; |
| static const uint32_t kBmpInfoV1Bytes = 40; |
| static const uint32_t kBmpInfoV2Bytes = 52; |
| static const uint32_t kBmpInfoV3Bytes = 56; |
| static const uint32_t kBmpInfoV4Bytes = 108; |
| static const uint32_t kBmpInfoV5Bytes = 124; |
| static const uint32_t kBmpMaskBytes = 12; |
| |
| static BmpHeaderType get_header_type(size_t infoBytes) { |
| if (infoBytes >= kBmpInfoBaseBytes) { |
| // Check the version of the header |
| switch (infoBytes) { |
| case kBmpInfoV1Bytes: |
| return kInfoV1_BmpHeaderType; |
| case kBmpInfoV2Bytes: |
| return kInfoV2_BmpHeaderType; |
| case kBmpInfoV3Bytes: |
| return kInfoV3_BmpHeaderType; |
| case kBmpInfoV4Bytes: |
| return kInfoV4_BmpHeaderType; |
| case kBmpInfoV5Bytes: |
| return kInfoV5_BmpHeaderType; |
| case 16: |
| case 20: |
| case 24: |
| case 28: |
| case 32: |
| case 36: |
| case 42: |
| case 46: |
| case 48: |
| case 60: |
| case kBmpOS2V2Bytes: |
| return kOS2VX_BmpHeaderType; |
| default: |
| SkCodecPrintf("Error: unknown bmp header format.\n"); |
| return kUnknown_BmpHeaderType; |
| } |
| } if (infoBytes >= kBmpOS2V1Bytes) { |
| // The OS2V1 is treated separately because it has a unique format |
| return kOS2V1_BmpHeaderType; |
| } else { |
| // There are no valid bmp headers |
| SkCodecPrintf("Error: second bitmap header size is invalid.\n"); |
| return kUnknown_BmpHeaderType; |
| } |
| } |
| |
| /* |
| * Read enough of the stream to initialize the SkBmpCodec. Returns a bool |
| * representing success or failure. If it returned true, and codecOut was |
| * not nullptr, it will be set to a new SkBmpCodec. |
| * Does *not* take ownership of the passed in SkStream. |
| */ |
| bool SkBmpCodec::ReadHeader(SkStream* stream, bool inIco, SkCodec** codecOut) { |
| // The total bytes in the bmp file |
| // We only need to use this value for RLE decoding, so we will only |
| // check that it is valid in the RLE case. |
| uint32_t totalBytes; |
| // The offset from the start of the file where the pixel data begins |
| uint32_t offset; |
| // The size of the second (info) header in bytes |
| uint32_t infoBytes; |
| |
| // Bmps embedded in Icos skip the first Bmp header |
| if (!inIco) { |
| // Read the first header and the size of the second header |
| SkAutoTDeleteArray<uint8_t> hBuffer(new uint8_t[kBmpHeaderBytesPlusFour]); |
| if (stream->read(hBuffer.get(), kBmpHeaderBytesPlusFour) != |
| kBmpHeaderBytesPlusFour) { |
| SkCodecPrintf("Error: unable to read first bitmap header.\n"); |
| return false; |
| } |
| |
| totalBytes = get_int(hBuffer.get(), 2); |
| offset = get_int(hBuffer.get(), 10); |
| if (offset < kBmpHeaderBytes + kBmpOS2V1Bytes) { |
| SkCodecPrintf("Error: invalid starting location for pixel data\n"); |
| return false; |
| } |
| |
| // The size of the second (info) header in bytes |
| // The size is the first field of the second header, so we have already |
| // read the first four infoBytes. |
| infoBytes = get_int(hBuffer.get(), 14); |
| if (infoBytes < kBmpOS2V1Bytes) { |
| SkCodecPrintf("Error: invalid second header size.\n"); |
| return false; |
| } |
| } else { |
| // This value is only used by RLE compression. Bmp in Ico files do not |
| // use RLE. If the compression field is incorrectly signaled as RLE, |
| // we will catch this and signal an error below. |
| totalBytes = 0; |
| |
| // Bmps in Ico cannot specify an offset. We will always assume that |
| // pixel data begins immediately after the color table. This value |
| // will be corrected below. |
| offset = 0; |
| |
| // Read the size of the second header |
| SkAutoTDeleteArray<uint8_t> hBuffer(new uint8_t[4]); |
| if (stream->read(hBuffer.get(), 4) != 4) { |
| SkCodecPrintf("Error: unable to read size of second bitmap header.\n"); |
| return false; |
| } |
| infoBytes = get_int(hBuffer.get(), 0); |
| if (infoBytes < kBmpOS2V1Bytes) { |
| SkCodecPrintf("Error: invalid second header size.\n"); |
| return false; |
| } |
| } |
| |
| // Determine image information depending on second header format |
| const BmpHeaderType headerType = get_header_type(infoBytes); |
| if (kUnknown_BmpHeaderType == headerType) { |
| return false; |
| } |
| |
| // We already read the first four bytes of the info header to get the size |
| const uint32_t infoBytesRemaining = infoBytes - 4; |
| |
| // Read the second header |
| SkAutoTDeleteArray<uint8_t> iBuffer(new uint8_t[infoBytesRemaining]); |
| if (stream->read(iBuffer.get(), infoBytesRemaining) != infoBytesRemaining) { |
| SkCodecPrintf("Error: unable to read second bitmap header.\n"); |
| return false; |
| } |
| |
| // The number of bits used per pixel in the pixel data |
| uint16_t bitsPerPixel; |
| |
| // The compression method for the pixel data |
| uint32_t compression = kNone_BmpCompressionMethod; |
| |
| // Number of colors in the color table, defaults to 0 or max (see below) |
| uint32_t numColors = 0; |
| |
| // Bytes per color in the color table, early versions use 3, most use 4 |
| uint32_t bytesPerColor; |
| |
| // The image width and height |
| int width, height; |
| |
| switch (headerType) { |
| case kInfoV1_BmpHeaderType: |
| case kInfoV2_BmpHeaderType: |
| case kInfoV3_BmpHeaderType: |
| case kInfoV4_BmpHeaderType: |
| case kInfoV5_BmpHeaderType: |
| case kOS2VX_BmpHeaderType: |
| // We check the size of the header before entering the if statement. |
| // We should not reach this point unless the size is large enough for |
| // these required fields. |
| SkASSERT(infoBytesRemaining >= 12); |
| width = get_int(iBuffer.get(), 0); |
| height = get_int(iBuffer.get(), 4); |
| bitsPerPixel = get_short(iBuffer.get(), 10); |
| |
| // Some versions do not have these fields, so we check before |
| // overwriting the default value. |
| if (infoBytesRemaining >= 16) { |
| compression = get_int(iBuffer.get(), 12); |
| if (infoBytesRemaining >= 32) { |
| numColors = get_int(iBuffer.get(), 28); |
| } |
| } |
| |
| // All of the headers that reach this point, store color table entries |
| // using 4 bytes per pixel. |
| bytesPerColor = 4; |
| break; |
| case kOS2V1_BmpHeaderType: |
| // The OS2V1 is treated separately because it has a unique format |
| width = (int) get_short(iBuffer.get(), 0); |
| height = (int) get_short(iBuffer.get(), 2); |
| bitsPerPixel = get_short(iBuffer.get(), 6); |
| bytesPerColor = 3; |
| break; |
| case kUnknown_BmpHeaderType: |
| // We'll exit above in this case. |
| SkASSERT(false); |
| return false; |
| } |
| |
| // Check for valid dimensions from header |
| SkCodec::SkScanlineOrder rowOrder = SkCodec::kBottomUp_SkScanlineOrder; |
| if (height < 0) { |
| height = -height; |
| rowOrder = SkCodec::kTopDown_SkScanlineOrder; |
| } |
| // The height field for bmp in ico is double the actual height because they |
| // contain an XOR mask followed by an AND mask |
| if (inIco) { |
| height /= 2; |
| } |
| |
| // Arbitrary maximum. Matches Chromium. |
| constexpr int kMaxDim = 1 << 16; |
| if (width <= 0 || height <= 0 || width >= kMaxDim || height >= kMaxDim) { |
| SkCodecPrintf("Error: invalid bitmap dimensions.\n"); |
| return false; |
| } |
| |
| // Create mask struct |
| SkMasks::InputMasks inputMasks; |
| memset(&inputMasks, 0, sizeof(SkMasks::InputMasks)); |
| |
| // Determine the input compression format and set bit masks if necessary |
| uint32_t maskBytes = 0; |
| BmpInputFormat inputFormat = kUnknown_BmpInputFormat; |
| switch (compression) { |
| case kNone_BmpCompressionMethod: |
| inputFormat = kStandard_BmpInputFormat; |
| break; |
| case k8BitRLE_BmpCompressionMethod: |
| if (bitsPerPixel != 8) { |
| SkCodecPrintf("Warning: correcting invalid bitmap format.\n"); |
| bitsPerPixel = 8; |
| } |
| inputFormat = kRLE_BmpInputFormat; |
| break; |
| case k4BitRLE_BmpCompressionMethod: |
| if (bitsPerPixel != 4) { |
| SkCodecPrintf("Warning: correcting invalid bitmap format.\n"); |
| bitsPerPixel = 4; |
| } |
| inputFormat = kRLE_BmpInputFormat; |
| break; |
| case kAlphaBitMasks_BmpCompressionMethod: |
| case kBitMasks_BmpCompressionMethod: |
| // Load the masks |
| inputFormat = kBitMask_BmpInputFormat; |
| switch (headerType) { |
| case kInfoV1_BmpHeaderType: { |
| // The V1 header stores the bit masks after the header |
| SkAutoTDeleteArray<uint8_t> mBuffer(new uint8_t[kBmpMaskBytes]); |
| if (stream->read(mBuffer.get(), kBmpMaskBytes) != |
| kBmpMaskBytes) { |
| SkCodecPrintf("Error: unable to read bit inputMasks.\n"); |
| return false; |
| } |
| maskBytes = kBmpMaskBytes; |
| inputMasks.red = get_int(mBuffer.get(), 0); |
| inputMasks.green = get_int(mBuffer.get(), 4); |
| inputMasks.blue = get_int(mBuffer.get(), 8); |
| break; |
| } |
| case kInfoV2_BmpHeaderType: |
| case kInfoV3_BmpHeaderType: |
| case kInfoV4_BmpHeaderType: |
| case kInfoV5_BmpHeaderType: |
| // Header types are matched based on size. If the header |
| // is V2+, we are guaranteed to be able to read at least |
| // this size. |
| SkASSERT(infoBytesRemaining >= 48); |
| inputMasks.red = get_int(iBuffer.get(), 36); |
| inputMasks.green = get_int(iBuffer.get(), 40); |
| inputMasks.blue = get_int(iBuffer.get(), 44); |
| break; |
| case kOS2VX_BmpHeaderType: |
| // TODO: Decide if we intend to support this. |
| // It is unsupported in the previous version and |
| // in chromium. I have not come across a test case |
| // that uses this format. |
| SkCodecPrintf("Error: huffman format unsupported.\n"); |
| return false; |
| default: |
| SkCodecPrintf("Error: invalid bmp bit masks header.\n"); |
| return false; |
| } |
| break; |
| case kJpeg_BmpCompressionMethod: |
| if (24 == bitsPerPixel) { |
| inputFormat = kRLE_BmpInputFormat; |
| break; |
| } |
| // Fall through |
| case kPng_BmpCompressionMethod: |
| // TODO: Decide if we intend to support this. |
| // It is unsupported in the previous version and |
| // in chromium. I think it is used mostly for printers. |
| SkCodecPrintf("Error: compression format not supported.\n"); |
| return false; |
| case kCMYK_BmpCompressionMethod: |
| case kCMYK8BitRLE_BmpCompressionMethod: |
| case kCMYK4BitRLE_BmpCompressionMethod: |
| // TODO: Same as above. |
| SkCodecPrintf("Error: CMYK not supported for bitmap decoding.\n"); |
| return false; |
| default: |
| SkCodecPrintf("Error: invalid format for bitmap decoding.\n"); |
| return false; |
| } |
| |
| // Most versions of bmps should be rendered as opaque. Either they do |
| // not have an alpha channel, or they expect the alpha channel to be |
| // ignored. V3+ bmp files introduce an alpha mask and allow the creator |
| // of the image to use the alpha channels. However, many of these images |
| // leave the alpha channel blank and expect to be rendered as opaque. This |
| // is the case for almost all V3 images, so we render these as opaque. For |
| // V4+ images in kMask mode, we will use the alpha mask. |
| // |
| // skbug.com/4116: We should perhaps also apply the alpha mask in kStandard |
| // mode. We just haven't seen any images that expect this |
| // behavior. |
| // |
| // Additionally, V3 bmp-in-ico may use the alpha mask. |
| SkAlphaType alphaType = kOpaque_SkAlphaType; |
| if ((kInfoV3_BmpHeaderType == headerType && inIco) || |
| kInfoV4_BmpHeaderType == headerType || |
| kInfoV5_BmpHeaderType == headerType) { |
| // Header types are matched based on size. If the header is |
| // V3+, we are guaranteed to be able to read at least this size. |
| SkASSERT(infoBytesRemaining > 52); |
| inputMasks.alpha = get_int(iBuffer.get(), 48); |
| if (inputMasks.alpha != 0) { |
| alphaType = kUnpremul_SkAlphaType; |
| } |
| } |
| iBuffer.free(); |
| |
| // Additionally, 32 bit bmp-in-icos use the alpha channel. |
| // FIXME (msarett): Don't all bmp-in-icos use the alpha channel? |
| // And, RLE inputs may skip pixels, leaving them as transparent. This |
| // is uncommon, but we cannot be certain that an RLE bmp will be opaque. |
| if ((inIco && 32 == bitsPerPixel) || (kRLE_BmpInputFormat == inputFormat)) { |
| alphaType = kUnpremul_SkAlphaType; |
| } |
| |
| // Check for valid bits per pixel. |
| // At the same time, use this information to choose a suggested color type |
| // and to set default masks. |
| SkColorType colorType = kN32_SkColorType; |
| switch (bitsPerPixel) { |
| // In addition to more standard pixel compression formats, bmp supports |
| // the use of bit masks to determine pixel components. The standard |
| // format for representing 16-bit colors is 555 (XRRRRRGGGGGBBBBB), |
| // which does not map well to any Skia color formats. For this reason, |
| // we will always enable mask mode with 16 bits per pixel. |
| case 16: |
| if (kBitMask_BmpInputFormat != inputFormat) { |
| inputMasks.red = 0x7C00; |
| inputMasks.green = 0x03E0; |
| inputMasks.blue = 0x001F; |
| inputFormat = kBitMask_BmpInputFormat; |
| } |
| break; |
| // We want to decode to kIndex_8 for input formats that are already |
| // designed in index format. |
| case 1: |
| case 2: |
| case 4: |
| case 8: |
| // However, we cannot in RLE format since we may need to leave some |
| // pixels as transparent. Similarly, we also cannot for ICO images |
| // since we may need to apply a transparent mask. |
| if (kRLE_BmpInputFormat != inputFormat && !inIco) { |
| colorType = kIndex_8_SkColorType; |
| } |
| |
| // Mask bmps must have 16, 24, or 32 bits per pixel. |
| if (kBitMask_BmpInputFormat == inputFormat) { |
| SkCodecPrintf("Error: invalid input value of bits per pixel for mask bmp.\n"); |
| return false; |
| } |
| case 24: |
| case 32: |
| break; |
| default: |
| SkCodecPrintf("Error: invalid input value for bits per pixel.\n"); |
| return false; |
| } |
| |
| // Check that input bit masks are valid and create the masks object |
| SkAutoTDelete<SkMasks> |
| masks(SkMasks::CreateMasks(inputMasks, bitsPerPixel)); |
| if (nullptr == masks) { |
| SkCodecPrintf("Error: invalid input masks.\n"); |
| return false; |
| } |
| |
| // Check for a valid number of total bytes when in RLE mode |
| if (totalBytes <= offset && kRLE_BmpInputFormat == inputFormat) { |
| SkCodecPrintf("Error: RLE requires valid input size.\n"); |
| return false; |
| } |
| |
| // Calculate the number of bytes read so far |
| const uint32_t bytesRead = kBmpHeaderBytes + infoBytes + maskBytes; |
| if (!inIco && offset < bytesRead) { |
| // TODO (msarett): Do we really want to fail if the offset in the header is invalid? |
| // Seems like we can just assume that the offset is zero and try to decode? |
| // Maybe we don't want to try to decode corrupt images? |
| SkCodecPrintf("Error: pixel data offset less than header size.\n"); |
| return false; |
| } |
| |
| // Skip to the start of the pixel array. |
| // We can do this here because there is no color table to read |
| // in bit mask mode. |
| if (!inIco && kBitMask_BmpInputFormat == inputFormat) { |
| if (stream->skip(offset - bytesRead) != offset - bytesRead) { |
| SkCodecPrintf("Error: unable to skip to image data.\n"); |
| return false; |
| } |
| } |
| |
| if (codecOut) { |
| // BMPs-in-ICOs contain an alpha mask after the image which means we |
| // cannot guarantee that an image is opaque, even if the bmp thinks |
| // it is. |
| bool isOpaque = kOpaque_SkAlphaType == alphaType; |
| if (inIco) { |
| alphaType = kUnpremul_SkAlphaType; |
| } |
| |
| // Set the image info |
| const SkImageInfo& imageInfo = SkImageInfo::Make(width, height, |
| colorType, alphaType); |
| |
| // Return the codec |
| switch (inputFormat) { |
| case kStandard_BmpInputFormat: |
| // We require streams to have a memory base for Bmp-in-Ico decodes. |
| SkASSERT(!inIco || nullptr != stream->getMemoryBase()); |
| *codecOut = new SkBmpStandardCodec(imageInfo, stream, bitsPerPixel, numColors, |
| bytesPerColor, offset - bytesRead, rowOrder, isOpaque, inIco); |
| return true; |
| case kBitMask_BmpInputFormat: |
| // Bmp-in-Ico must be standard mode |
| if (inIco) { |
| SkCodecPrintf("Error: Icos may not use bit mask format.\n"); |
| return false; |
| } |
| |
| *codecOut = new SkBmpMaskCodec(imageInfo, stream, bitsPerPixel, masks.detach(), |
| rowOrder); |
| return true; |
| case kRLE_BmpInputFormat: |
| // Bmp-in-Ico must be standard mode |
| // When inIco is true, this line cannot be reached, since we |
| // require that RLE Bmps have a valid number of totalBytes, and |
| // Icos skip the header that contains totalBytes. |
| SkASSERT(!inIco); |
| *codecOut = new SkBmpRLECodec(imageInfo, stream, bitsPerPixel, numColors, |
| bytesPerColor, offset - bytesRead, rowOrder); |
| return true; |
| default: |
| SkASSERT(false); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| /* |
| * Creates a bmp decoder |
| * Reads enough of the stream to determine the image format |
| */ |
| SkCodec* SkBmpCodec::NewFromStream(SkStream* stream, bool inIco) { |
| SkAutoTDelete<SkStream> streamDeleter(stream); |
| SkCodec* codec = nullptr; |
| if (ReadHeader(stream, inIco, &codec)) { |
| // codec has taken ownership of stream, so we do not need to |
| // delete it. |
| SkASSERT(codec); |
| streamDeleter.detach(); |
| return codec; |
| } |
| return nullptr; |
| } |
| |
| SkBmpCodec::SkBmpCodec(const SkImageInfo& info, SkStream* stream, |
| uint16_t bitsPerPixel, SkCodec::SkScanlineOrder rowOrder) |
| : INHERITED(info, stream) |
| , fBitsPerPixel(bitsPerPixel) |
| , fRowOrder(rowOrder) |
| , fSrcRowBytes(SkAlign4(compute_row_bytes(info.width(), fBitsPerPixel))) |
| {} |
| |
| bool SkBmpCodec::onRewind() { |
| return SkBmpCodec::ReadHeader(this->stream(), this->inIco(), nullptr); |
| } |
| |
| int32_t SkBmpCodec::getDstRow(int32_t y, int32_t height) const { |
| if (SkCodec::kTopDown_SkScanlineOrder == fRowOrder) { |
| return y; |
| } |
| SkASSERT(SkCodec::kBottomUp_SkScanlineOrder == fRowOrder); |
| return height - y - 1; |
| } |
| |
| SkCodec::Result SkBmpCodec::onStartScanlineDecode(const SkImageInfo& dstInfo, |
| const SkCodec::Options& options, SkPMColor inputColorPtr[], int* inputColorCount) { |
| if (!conversion_possible(dstInfo, this->getInfo())) { |
| SkCodecPrintf("Error: cannot convert input type to output type.\n"); |
| return kInvalidConversion; |
| } |
| |
| return prepareToDecode(dstInfo, options, inputColorPtr, inputColorCount); |
| } |
| |
| int SkBmpCodec::onGetScanlines(void* dst, int count, size_t rowBytes) { |
| // Create a new image info representing the portion of the image to decode |
| SkImageInfo rowInfo = this->dstInfo().makeWH(this->dstInfo().width(), count); |
| |
| // Decode the requested rows |
| return this->decodeRows(rowInfo, dst, rowBytes, this->options()); |
| } |
| |
| bool SkBmpCodec::skipRows(int count) { |
| const size_t bytesToSkip = count * fSrcRowBytes; |
| return this->stream()->skip(bytesToSkip) == bytesToSkip; |
| } |
| |
| bool SkBmpCodec::onSkipScanlines(int count) { |
| return this->skipRows(count); |
| } |