bump product version to 6.4.0.3
[LibreOffice.git] / vcl / source / filter / png / pngwrite.cxx
blob8190c1efd38aa88c924acb88a96f0dc35106d132
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <vcl/pngwrite.hxx>
21 #include <vcl/bitmapex.hxx>
23 #include <com/sun/star/beans/PropertyValue.hpp>
24 #include <com/sun/star/uno/Sequence.hxx>
26 #include <limits>
27 #include <rtl/crc.h>
28 #include <tools/zcodec.hxx>
29 #include <tools/stream.hxx>
30 #include <vcl/bitmapaccess.hxx>
31 #include <vcl/alpha.hxx>
32 #include <osl/endian.h>
33 #include <memory>
34 #include <vcl/BitmapTools.hxx>
36 #define PNG_DEF_COMPRESSION 6
38 #define PNGCHUNK_IHDR 0x49484452
39 #define PNGCHUNK_PLTE 0x504c5445
40 #define PNGCHUNK_IDAT 0x49444154
41 #define PNGCHUNK_IEND 0x49454e44
42 #define PNGCHUNK_pHYs 0x70485973
43 #define PNGCHUNK_tRNS 0x74524e53
45 namespace vcl
48 class PNGWriterImpl
50 public:
52 PNGWriterImpl(const BitmapEx& BmpEx,
53 const css::uno::Sequence<css::beans::PropertyValue>* pFilterData);
55 bool Write(SvStream& rOutStream);
57 std::vector<vcl::PNGWriter::ChunkData>& GetChunks()
59 return maChunkSeq;
62 private:
64 std::vector<vcl::PNGWriter::ChunkData> maChunkSeq;
66 sal_Int32 mnCompLevel;
67 sal_Int32 mnInterlaced;
68 sal_uInt32 mnMaxChunkSize;
69 bool mbStatus;
71 Bitmap::ScopedReadAccess mpAccess;
72 BitmapReadAccess* mpMaskAccess;
73 ZCodec mpZCodec;
75 std::unique_ptr<sal_uInt8[]> mpDeflateInBuf; // as big as the size of a scanline + alphachannel + 1
76 std::unique_ptr<sal_uInt8[]> mpPreviousScan; // as big as mpDeflateInBuf
77 std::unique_ptr<sal_uInt8[]> mpCurrentScan;
78 sal_uLong mnDeflateInSize;
80 sal_uLong mnWidth;
81 sal_uLong mnHeight;
82 sal_uInt8 mnBitsPerPixel;
83 sal_uInt8 mnFilterType; // 0 or 4;
84 sal_uLong mnBBP; // bytes per pixel ( needed for filtering )
85 bool mbTrueAlpha;
87 void ImplWritepHYs(const BitmapEx& rBitmapEx);
88 void ImplWriteIDAT();
89 sal_uLong ImplGetFilter(sal_uLong nY, sal_uLong nXStart = 0, sal_uLong nXAdd = 1);
90 void ImplClearFirstScanline();
91 void ImplWriteTransparent();
92 bool ImplWriteHeader();
93 void ImplWritePalette();
94 void ImplOpenChunk(sal_uLong nChunkType);
95 void ImplWriteChunk(sal_uInt8 nNumb);
96 void ImplWriteChunk(sal_uInt32 nNumb);
97 void ImplWriteChunk(unsigned char const * pSource, sal_uInt32 nDatSize);
100 PNGWriterImpl::PNGWriterImpl( const BitmapEx& rBitmapEx,
101 const css::uno::Sequence<css::beans::PropertyValue>* pFilterData )
102 : mnCompLevel(PNG_DEF_COMPRESSION)
103 , mnInterlaced(0)
104 , mnMaxChunkSize(0)
105 , mbStatus(true)
106 , mpMaskAccess(nullptr)
107 , mnDeflateInSize(0)
108 , mnWidth(0)
109 , mnHeight(0)
110 , mnBitsPerPixel(0)
111 , mnFilterType(0)
112 , mnBBP(0)
113 , mbTrueAlpha(false)
115 if (!rBitmapEx.IsEmpty())
117 BitmapEx aBitmapEx;
119 if (rBitmapEx.GetBitmap().GetBitCount() == 32)
121 if (!vcl::bitmap::convertBitmap32To24Plus8(rBitmapEx, aBitmapEx))
122 return;
124 else
126 aBitmapEx = rBitmapEx;
129 Bitmap aBmp(aBitmapEx.GetBitmap());
131 mnMaxChunkSize = std::numeric_limits<sal_uInt32>::max();
133 if (pFilterData)
135 for (const auto& rPropVal : *pFilterData)
137 if (rPropVal.Name == "Compression")
138 rPropVal.Value >>= mnCompLevel;
139 else if (rPropVal.Name == "Interlaced")
140 rPropVal.Value >>= mnInterlaced;
141 else if (rPropVal.Name == "MaxChunkSize")
143 sal_Int32 nVal = 0;
144 if (rPropVal.Value >>= nVal)
145 mnMaxChunkSize = static_cast<sal_uInt32>(nVal);
149 mnBitsPerPixel = static_cast<sal_uInt8>(aBmp.GetBitCount());
151 if (aBitmapEx.IsTransparent())
153 if (mnBitsPerPixel <= 8 && aBitmapEx.IsAlpha())
155 aBmp.Convert( BmpConversion::N24Bit );
156 mnBitsPerPixel = 24;
159 if (mnBitsPerPixel <= 8) // transparent palette
161 aBmp.Convert(BmpConversion::N8BitTrans);
162 aBmp.Replace(aBitmapEx.GetMask(), BMP_COL_TRANS);
163 mnBitsPerPixel = 8;
164 mpAccess = Bitmap::ScopedReadAccess(aBmp);
165 if (mpAccess)
167 if (ImplWriteHeader())
169 ImplWritepHYs(aBitmapEx);
170 ImplWritePalette();
171 ImplWriteTransparent();
172 ImplWriteIDAT();
174 mpAccess.reset();
176 else
178 mbStatus = false;
181 else
183 mpAccess = Bitmap::ScopedReadAccess(aBmp); // true RGB with alphachannel
184 if (mpAccess)
186 mbTrueAlpha = aBitmapEx.IsAlpha();
187 if (mbTrueAlpha)
189 AlphaMask aMask(aBitmapEx.GetAlpha());
190 mpMaskAccess = aMask.AcquireReadAccess();
191 if (mpMaskAccess)
193 if (ImplWriteHeader())
195 ImplWritepHYs(aBitmapEx);
196 ImplWriteIDAT();
198 aMask.ReleaseAccess(mpMaskAccess);
199 mpMaskAccess = nullptr;
201 else
203 mbStatus = false;
206 else
208 Bitmap aMask(aBitmapEx.GetMask());
209 mpMaskAccess = aMask.AcquireReadAccess();
210 if (mpMaskAccess)
212 if (ImplWriteHeader())
214 ImplWritepHYs(aBitmapEx);
215 ImplWriteIDAT();
217 Bitmap::ReleaseAccess(mpMaskAccess);
218 mpMaskAccess = nullptr;
220 else
222 mbStatus = false;
225 mpAccess.reset();
227 else
229 mbStatus = false;
233 else
235 mpAccess = Bitmap::ScopedReadAccess(aBmp); // palette + RGB without alphachannel
236 if (mpAccess)
238 if (ImplWriteHeader())
240 ImplWritepHYs(aBitmapEx);
241 if (mpAccess->HasPalette())
242 ImplWritePalette();
244 ImplWriteIDAT();
246 mpAccess.reset();
248 else
250 mbStatus = false;
254 if (mbStatus)
256 ImplOpenChunk(PNGCHUNK_IEND); // create an IEND chunk
261 bool PNGWriterImpl::Write(SvStream& rOStm)
263 /* png signature is always an array of 8 bytes */
264 SvStreamEndian nOldMode = rOStm.GetEndian();
265 rOStm.SetEndian(SvStreamEndian::BIG);
266 rOStm.WriteUInt32(0x89504e47);
267 rOStm.WriteUInt32(0x0d0a1a0a);
269 for (auto const& chunk : maChunkSeq)
271 sal_uInt32 nType = chunk.nType;
272 #if defined(__LITTLEENDIAN) || defined(OSL_LITENDIAN)
273 nType = OSL_SWAPDWORD(nType);
274 #endif
275 sal_uInt32 nCRC = rtl_crc32(0, &nType, 4);
276 sal_uInt32 nDataSize = chunk.aData.size();
277 if (nDataSize)
278 nCRC = rtl_crc32(nCRC, chunk.aData.data(), nDataSize);
279 rOStm.WriteUInt32(nDataSize);
280 rOStm.WriteUInt32(chunk.nType);
281 if (nDataSize)
282 rOStm.WriteBytes(chunk.aData.data(), nDataSize);
283 rOStm.WriteUInt32(nCRC);
285 rOStm.SetEndian(nOldMode);
286 return mbStatus;
290 bool PNGWriterImpl::ImplWriteHeader()
292 ImplOpenChunk(PNGCHUNK_IHDR);
293 mnWidth = mpAccess->Width();
294 ImplWriteChunk(sal_uInt32(mnWidth));
295 mnHeight = mpAccess->Height();
296 ImplWriteChunk(sal_uInt32(mnHeight));
298 if (mnWidth && mnHeight && mnBitsPerPixel && mbStatus)
300 sal_uInt8 nBitDepth = mnBitsPerPixel;
301 if (mnBitsPerPixel <= 8)
302 mnFilterType = 0;
303 else
304 mnFilterType = 4;
306 sal_uInt8 nColorType = 2; // colortype:
308 // bit 0 -> palette is used
309 if (mpAccess->HasPalette()) // bit 1 -> color is used
310 nColorType |= 1; // bit 2 -> alpha channel is used
311 else
312 nBitDepth /= 3;
314 if (mpMaskAccess)
315 nColorType |= 4;
317 ImplWriteChunk(nBitDepth);
318 ImplWriteChunk(nColorType); // colortype
319 ImplWriteChunk(static_cast<sal_uInt8>(0)); // compression type
320 ImplWriteChunk(static_cast<sal_uInt8>(0)); // filter type - is not supported in this version
321 ImplWriteChunk(static_cast<sal_uInt8>(mnInterlaced)); // interlace type
323 else
325 mbStatus = false;
327 return mbStatus;
330 void PNGWriterImpl::ImplWritePalette()
332 const sal_uLong nCount = mpAccess->GetPaletteEntryCount();
333 std::unique_ptr<sal_uInt8[]> pTempBuf(new sal_uInt8[nCount * 3]);
334 sal_uInt8* pTmp = pTempBuf.get();
336 ImplOpenChunk(PNGCHUNK_PLTE);
338 for ( sal_uLong i = 0; i < nCount; i++ )
340 const BitmapColor& rColor = mpAccess->GetPaletteColor(i);
341 *pTmp++ = rColor.GetRed();
342 *pTmp++ = rColor.GetGreen();
343 *pTmp++ = rColor.GetBlue();
345 ImplWriteChunk(pTempBuf.get(), nCount * 3);
348 void PNGWriterImpl::ImplWriteTransparent()
350 const sal_uLong nTransIndex = mpAccess->GetBestPaletteIndex(BMP_COL_TRANS);
352 ImplOpenChunk(PNGCHUNK_tRNS);
354 for (sal_uLong n = 0; n <= nTransIndex; n++)
356 ImplWriteChunk((nTransIndex == n) ? static_cast<sal_uInt8>(0x0) : static_cast<sal_uInt8>(0xff));
360 void PNGWriterImpl::ImplWritepHYs(const BitmapEx& rBmpEx)
362 if (rBmpEx.GetPrefMapMode().GetMapUnit() == MapUnit::Map100thMM)
364 Size aPrefSize(rBmpEx.GetPrefSize());
366 if (aPrefSize.Width() && aPrefSize.Height() && mnWidth && mnHeight)
368 ImplOpenChunk(PNGCHUNK_pHYs);
369 sal_uInt32 nPrefSizeX = static_cast<sal_uInt32>(100000.0 / (static_cast<double>(aPrefSize.Width()) / mnWidth) + 0.5);
370 sal_uInt32 nPrefSizeY = static_cast<sal_uInt32>(100000.0 / (static_cast<double>(aPrefSize.Height()) / mnHeight) + 0.5);
371 ImplWriteChunk(nPrefSizeX);
372 ImplWriteChunk(nPrefSizeY);
373 ImplWriteChunk(sal_uInt8(1)); // nMapUnit
378 void PNGWriterImpl::ImplWriteIDAT()
380 mnDeflateInSize = mnBitsPerPixel;
382 if (mpMaskAccess)
383 mnDeflateInSize += 8;
385 mnBBP = (mnDeflateInSize + 7) >> 3;
387 mnDeflateInSize = mnBBP * mnWidth + 1;
389 mpDeflateInBuf.reset(new sal_uInt8[mnDeflateInSize]);
391 if (mnFilterType) // using filter type 4 we need memory for the scanline 3 times
393 mpPreviousScan.reset(new sal_uInt8[mnDeflateInSize]);
394 mpCurrentScan.reset(new sal_uInt8[mnDeflateInSize]);
395 ImplClearFirstScanline();
397 mpZCodec.BeginCompression(mnCompLevel);
398 SvMemoryStream aOStm;
399 if (mnInterlaced == 0)
401 for (sal_uLong nY = 0; nY < mnHeight; nY++)
403 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY));
406 else
408 // interlace mode
409 sal_uLong nY;
410 for (nY = 0; nY < mnHeight; nY += 8) // pass 1
412 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 8));
414 ImplClearFirstScanline();
416 for (nY = 0; nY < mnHeight; nY += 8) // pass 2
418 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 4, 8));
420 ImplClearFirstScanline();
422 if (mnHeight >= 5) // pass 3
424 for (nY = 4; nY < mnHeight; nY += 8)
426 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 4));
428 ImplClearFirstScanline();
431 for (nY = 0; nY < mnHeight; nY += 4) // pass 4
433 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 2, 4));
435 ImplClearFirstScanline();
437 if (mnHeight >= 3) // pass 5
439 for (nY = 2; nY < mnHeight; nY += 4)
441 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 0, 2));
443 ImplClearFirstScanline();
446 for (nY = 0; nY < mnHeight; nY += 2) // pass 6
448 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter(nY, 1, 2));
450 ImplClearFirstScanline();
452 if (mnHeight >= 2) // pass 7
454 for (nY = 1; nY < mnHeight; nY += 2)
456 mpZCodec.Write(aOStm, mpDeflateInBuf.get(), ImplGetFilter (nY));
460 mpZCodec.EndCompression();
462 if (mnFilterType) // using filter type 4 we need memory for the scanline 3 times
464 mpCurrentScan.reset();
465 mpPreviousScan.reset();
467 mpDeflateInBuf.reset();
469 sal_uInt32 nIDATSize = aOStm.Tell();
470 sal_uInt32 nBytes, nBytesToWrite = nIDATSize;
471 while(nBytesToWrite)
473 nBytes = nBytesToWrite <= mnMaxChunkSize ? nBytesToWrite : mnMaxChunkSize;
474 ImplOpenChunk(PNGCHUNK_IDAT);
475 ImplWriteChunk(const_cast<unsigned char *>(static_cast<unsigned char const *>(aOStm.GetData())) + (nIDATSize - nBytesToWrite), nBytes);
476 nBytesToWrite -= nBytes;
480 // ImplGetFilter writes the complete Scanline (nY) - in interlace mode the parameter nXStart and nXAdd
481 // appends to the currently used pass
482 // the complete size of scanline will be returned - in interlace mode zero is possible!
484 sal_uLong PNGWriterImpl::ImplGetFilter (sal_uLong nY, sal_uLong nXStart, sal_uLong nXAdd)
486 sal_uInt8* pDest;
488 if (mnFilterType)
489 pDest = mpCurrentScan.get();
490 else
491 pDest = mpDeflateInBuf.get();
493 if (nXStart < mnWidth)
495 *pDest++ = mnFilterType; // in this version the filter type is either 0 or 4
497 if (mpAccess->HasPalette()) // alphachannel is not allowed by pictures including palette entries
499 switch (mnBitsPerPixel)
501 case 1:
503 Scanline pScanline = mpAccess->GetScanline( nY );
504 sal_uLong nX, nXIndex;
505 for (nX = nXStart, nXIndex = 0; nX < mnWidth; nX += nXAdd, nXIndex++)
507 sal_uLong nShift = (nXIndex & 7) ^ 7;
508 if (nShift == 7)
509 *pDest = mpAccess->GetIndexFromData(pScanline, nX) << nShift;
510 else if (nShift == 0)
511 *pDest++ |= mpAccess->GetIndexFromData(pScanline, nX) << nShift;
512 else
513 *pDest |= mpAccess->GetIndexFromData(pScanline, nX) << nShift;
515 if ( (nXIndex & 7) != 0 )
516 pDest++; // byte is not completely used, so the bufferpointer is to correct
518 break;
520 case 4:
522 Scanline pScanline = mpAccess->GetScanline( nY );
523 sal_uLong nX, nXIndex;
524 for (nX = nXStart, nXIndex = 0; nX < mnWidth; nX += nXAdd, nXIndex++)
526 if(nXIndex & 1)
527 *pDest++ |= mpAccess->GetIndexFromData(pScanline, nX);
528 else
529 *pDest = mpAccess->GetIndexFromData(pScanline, nX) << 4;
531 if (nXIndex & 1)
532 pDest++;
534 break;
536 case 8:
538 Scanline pScanline = mpAccess->GetScanline( nY );
539 for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
541 *pDest++ = mpAccess->GetIndexFromData( pScanline, nX );
544 break;
546 default :
547 mbStatus = false;
548 break;
551 else
553 if (mpMaskAccess) // mpMaskAccess != NULL -> alphachannel is to create
555 if (mbTrueAlpha)
557 Scanline pScanline = mpAccess->GetScanline( nY );
558 Scanline pScanlineMask = mpMaskAccess->GetScanline( nY );
559 for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
561 const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
562 *pDest++ = rColor.GetRed();
563 *pDest++ = rColor.GetGreen();
564 *pDest++ = rColor.GetBlue();
565 *pDest++ = 255 - mpMaskAccess->GetIndexFromData(pScanlineMask, nX);
568 else
570 const BitmapColor aTrans(mpMaskAccess->GetBestMatchingColor(COL_WHITE));
571 Scanline pScanline = mpAccess->GetScanline( nY );
572 Scanline pScanlineMask = mpMaskAccess->GetScanline( nY );
574 for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
576 const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
577 *pDest++ = rColor.GetRed();
578 *pDest++ = rColor.GetGreen();
579 *pDest++ = rColor.GetBlue();
581 if(mpMaskAccess->GetPixelFromData(pScanlineMask, nX) == aTrans)
582 *pDest++ = 0;
583 else
584 *pDest++ = 0xff;
588 else
590 Scanline pScanline = mpAccess->GetScanline( nY );
591 for (sal_uLong nX = nXStart; nX < mnWidth; nX += nXAdd)
593 const BitmapColor& rColor = mpAccess->GetPixelFromData(pScanline, nX);
594 *pDest++ = rColor.GetRed();
595 *pDest++ = rColor.GetGreen();
596 *pDest++ = rColor.GetBlue();
601 // filter type4 ( PAETH ) will be used only for 24bit graphics
602 if (mnFilterType)
604 mnDeflateInSize = pDest - mpCurrentScan.get();
605 pDest = mpDeflateInBuf.get();
606 *pDest++ = 4; // filter type
608 sal_uInt8* p1 = mpCurrentScan.get() + 1; // Current Pixel
609 sal_uInt8* p2 = p1 - mnBBP; // left pixel
610 sal_uInt8* p3 = mpPreviousScan.get(); // upper pixel
611 sal_uInt8* p4 = p3 - mnBBP; // upperleft Pixel;
613 while (pDest < mpDeflateInBuf.get() + mnDeflateInSize)
615 sal_uLong nb = *p3++;
616 sal_uLong na, nc;
617 if (p2 >= mpCurrentScan.get() + 1)
619 na = *p2;
620 nc = *p4;
622 else
624 na = nc = 0;
627 long np = na + nb - nc;
628 long npa = np - na;
629 long npb = np - nb;
630 long npc = np - nc;
632 if (npa < 0)
633 npa =-npa;
634 if (npb < 0)
635 npb =-npb;
636 if (npc < 0)
637 npc =-npc;
639 if (npa <= npb && npa <= npc)
640 *pDest++ = *p1++ - static_cast<sal_uInt8>(na);
641 else if ( npb <= npc )
642 *pDest++ = *p1++ - static_cast<sal_uInt8>(nb);
643 else
644 *pDest++ = *p1++ - static_cast<sal_uInt8>(nc);
646 p4++;
647 p2++;
649 for (long i = 0; i < static_cast<long>(mnDeflateInSize - 1); i++)
651 mpPreviousScan[i] = mpCurrentScan[i + 1];
654 else
656 mnDeflateInSize = pDest - mpDeflateInBuf.get();
658 return mnDeflateInSize;
661 void PNGWriterImpl::ImplClearFirstScanline()
663 if (mnFilterType)
664 memset(mpPreviousScan.get(), 0, mnDeflateInSize);
667 void PNGWriterImpl::ImplOpenChunk (sal_uLong nChunkType)
669 maChunkSeq.emplace_back();
670 maChunkSeq.back().nType = nChunkType;
673 void PNGWriterImpl::ImplWriteChunk (sal_uInt8 nSource)
675 maChunkSeq.back().aData.push_back(nSource);
678 void PNGWriterImpl::ImplWriteChunk (sal_uInt32 nSource)
680 vcl::PNGWriter::ChunkData& rChunkData = maChunkSeq.back();
681 rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 24));
682 rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 16));
683 rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource >> 8));
684 rChunkData.aData.push_back(static_cast<sal_uInt8>(nSource));
687 void PNGWriterImpl::ImplWriteChunk (unsigned char const * pSource, sal_uInt32 nDatSize)
689 if (nDatSize)
691 vcl::PNGWriter::ChunkData& rChunkData = maChunkSeq.back();
692 sal_uInt32 nSize = rChunkData.aData.size();
693 rChunkData.aData.resize(nSize + nDatSize);
694 memcpy(&rChunkData.aData[nSize], pSource, nDatSize);
698 PNGWriter::PNGWriter(const BitmapEx& rBmpEx,
699 const css::uno::Sequence<css::beans::PropertyValue>* pFilterData)
700 : mpImpl(new vcl::PNGWriterImpl(rBmpEx, pFilterData))
704 PNGWriter::~PNGWriter()
708 bool PNGWriter::Write(SvStream& rStream)
710 return mpImpl->Write(rStream);
713 std::vector<vcl::PNGWriter::ChunkData>& PNGWriter::GetChunks()
715 return mpImpl->GetChunks();
718 } // namespace vcl
720 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */