From 434aa248ad5710c7f65283fc3beb7e8adb8b1ad7 Mon Sep 17 00:00:00 2001
From: Eirik Aavitsland <eirik.aavitsland@qt.io>
Date: Tue, 26 Feb 2019 16:23:08 +0100
Subject: [PATCH] Heic handler: fix orientation and other image properties
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The mac heic handler lacked support for any meta-data i/o. Most
notably, the image orientation proprty was ignored, so images read in
could be wrongly oriented.

Fixes: QTBUG-73415
Change-Id: I779f91dc28c7441b124aab4557e1abcd3e69fde9
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
---
 .../imageformats/macheif/qmacheifhandler.cpp  |  30 +--
 .../imageformats/macheif/qmacheifhandler.h    |   7 +-
 .../imageformats/shared/qiiofhelpers.cpp      | 194 +++++++++++++++---
 .../imageformats/shared/qiiofhelpers_p.h      |  24 +++
 tests/auto/heif/tst_qheif.cpp                 |  80 +++++++-
 tests/shared/images/heif.qrc                  |   1 +
 tests/shared/images/heif/newlogoCCW.heic      | Bin 0 -> 4847 bytes
 7 files changed, 287 insertions(+), 49 deletions(-)
 create mode 100644 tests/shared/images/heif/newlogoCCW.heic

diff --git a/src/plugins/imageformats/macheif/qmacheifhandler.cpp b/src/plugins/imageformats/macheif/qmacheifhandler.cpp
index 7c63e52a..385d5e2a 100644
--- a/src/plugins/imageformats/macheif/qmacheifhandler.cpp
+++ b/src/plugins/imageformats/macheif/qmacheifhandler.cpp
@@ -43,22 +43,8 @@
 
 QT_BEGIN_NAMESPACE
 
-class QMacHeifHandlerPrivate
-{
-    Q_DECLARE_PUBLIC(QMacHeifHandler)
-    Q_DISABLE_COPY(QMacHeifHandlerPrivate)
-public:
-    QMacHeifHandlerPrivate(QMacHeifHandler *q_ptr)
-        : writeQuality(-1), q_ptr(q_ptr)
-    {}
-
-    int writeQuality;
-    QMacHeifHandler *q_ptr;
-};
-
-
 QMacHeifHandler::QMacHeifHandler()
-    : d_ptr(new QMacHeifHandlerPrivate(this))
+    : d(new QIIOFHelper(this))
 {
 }
 
@@ -90,28 +76,30 @@ bool QMacHeifHandler::canRead() const
 
 bool QMacHeifHandler::read(QImage *image)
 {
-    return QIIOFHelpers::readImage(this, image);
+    return d->readImage(image);
 }
 
 bool QMacHeifHandler::write(const QImage &image)
 {
-    return QIIOFHelpers::writeImage(this, image, QStringLiteral("public.heic"));
+    return d->writeImage(image, QStringLiteral("public.heic"));
 }
 
 QVariant QMacHeifHandler::option(ImageOption option) const
 {
-    return QVariant();
+    return d->imageProperty(option);
 }
 
 void QMacHeifHandler::setOption(ImageOption option, const QVariant &value)
 {
-    Q_UNUSED(option)
-    Q_UNUSED(value)
+    d->setOption(option, value);
 }
 
 bool QMacHeifHandler::supportsOption(ImageOption option) const
 {
-    return false;
+    return option == Quality
+        || option == Size
+        || option == ImageTransformation
+        || option == TransformedByDefault;
 }
 
 QT_END_NAMESPACE
diff --git a/src/plugins/imageformats/macheif/qmacheifhandler.h b/src/plugins/imageformats/macheif/qmacheifhandler.h
index 6e94a596..cf63de8b 100644
--- a/src/plugins/imageformats/macheif/qmacheifhandler.h
+++ b/src/plugins/imageformats/macheif/qmacheifhandler.h
@@ -49,13 +49,13 @@ class QImage;
 class QByteArray;
 class QIODevice;
 class QVariant;
-class QMacHeifHandlerPrivate;
+class QIIOFHelper;
 
 class QMacHeifHandler : public QImageIOHandler
 {
 public:
     QMacHeifHandler();
-    ~QMacHeifHandler();
+    ~QMacHeifHandler() override;
 
     bool canRead() const override;
     bool read(QImage *image) override;
@@ -67,8 +67,7 @@ public:
     static bool canRead(QIODevice *iod);
 
 private:
-    Q_DECLARE_PRIVATE(QMacHeifHandler)
-    QScopedPointer<QMacHeifHandlerPrivate> d_ptr;
+    QScopedPointer<QIIOFHelper> d;
 };
 
 QT_END_NAMESPACE
diff --git a/src/plugins/imageformats/shared/qiiofhelpers.cpp b/src/plugins/imageformats/shared/qiiofhelpers.cpp
index 2b16787d..f8172fe8 100644
--- a/src/plugins/imageformats/shared/qiiofhelpers.cpp
+++ b/src/plugins/imageformats/shared/qiiofhelpers.cpp
@@ -38,15 +38,12 @@
 ****************************************************************************/
 
 #include <QGuiApplication>
-#include <qpa/qplatformnativeinterface.h>
 #include <QBuffer>
 #include <QImageIOHandler>
 #include <QImage>
-#include <private/qcore_mac_p.h>
 
 #include "qiiofhelpers_p.h"
 
-#include <ImageIO/ImageIO.h>
 
 QT_BEGIN_NAMESPACE
 
@@ -57,14 +54,14 @@ static size_t cbGetBytes(void *info, void *buffer, size_t count)
     QIODevice *dev = static_cast<QIODevice *>(info);
     if (!dev || !buffer)
         return 0;
-    qint64 res = dev->read(static_cast<char *>(buffer), count);
-    return qMax(qint64(0), res);
+    qint64 res = dev->read(static_cast<char *>(buffer), qint64(count));
+    return size_t(qMax(qint64(0), res));
 }
 
 static off_t cbSkipForward(void *info, off_t count)
 {
     QIODevice *dev = static_cast<QIODevice *>(info);
-    if (!dev || !count)
+    if (!dev || count <= 0)
         return 0;
     qint64 res = 0;
     if (!dev->isSequential()) {
@@ -72,7 +69,7 @@ static off_t cbSkipForward(void *info, off_t count)
         dev->seek(prevPos + count);
         res = dev->pos() - prevPos;
     } else {
-        char *buf = new char[count];
+        char *buf = new char[quint64(count)];
         res = dev->read(buf, count);
         delete[] buf;
     }
@@ -89,8 +86,8 @@ static size_t cbPutBytes(void *info, const void *buffer, size_t count)
     QIODevice *dev = static_cast<QIODevice *>(info);
     if (!dev || !buffer)
         return 0;
-    qint64 res = dev->write(static_cast<const char *>(buffer), count);
-    return qMax(qint64(0), res);
+    qint64 res = dev->write(static_cast<const char *>(buffer), qint64(count));
+    return size_t(qMax(qint64(0), res));
 }
 
 
@@ -117,23 +114,50 @@ QImageIOPlugin::Capabilities QIIOFHelpers::systemCapabilities(const QString &uti
 }
 
 bool QIIOFHelpers::readImage(QImageIOHandler *q_ptr, QImage *out)
+{
+    QIIOFHelper h(q_ptr);
+    return h.readImage(out);
+}
+
+bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti)
+{
+    QIIOFHelper h(q_ptr);
+    return h.writeImage(in, uti);
+}
+
+QIIOFHelper::QIIOFHelper(QImageIOHandler *q)
+    : q_ptr(q)
+{
+}
+
+bool QIIOFHelper::initRead()
 {
     static const CGDataProviderSequentialCallbacks cgCallbacks = { 0, &cbGetBytes, &cbSkipForward, &cbRewind, nullptr };
 
-    if (!q_ptr || !q_ptr->device() || !out)
+    if (cgImageSource)
+        return true;
+    if (!q_ptr || !q_ptr->device())
         return false;
 
-    QCFType<CGDataProviderRef> cgDataProvider;
     if (QBuffer *b = qobject_cast<QBuffer *>(q_ptr->device())) {
         // do direct access to avoid data copy
         const void *rawData = b->data().constData() + b->pos();
-        cgDataProvider = CGDataProviderCreateWithData(nullptr, rawData, b->data().size() - b->pos(), nullptr);
+        cgDataProvider = CGDataProviderCreateWithData(nullptr, rawData, size_t(b->data().size() - b->pos()), nullptr);
     } else {
         cgDataProvider = CGDataProviderCreateSequential(q_ptr->device(), &cgCallbacks);
     }
 
-    QCFType<CGImageSourceRef> cgImageSource = CGImageSourceCreateWithDataProvider(cgDataProvider, nullptr);
-    if (!cgImageSource)
+    cgImageSource = CGImageSourceCreateWithDataProvider(cgDataProvider, nullptr);
+
+    if (cgImageSource)
+        cfImageDict = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nullptr);
+
+    return (cgImageSource);
+}
+
+bool QIIOFHelper::readImage(QImage *out)
+{
+    if (!out || !initRead())
         return false;
 
     QCFType<CGImageRef> cgImage = CGImageSourceCreateImageAtIndex(cgImageSource, 0, nullptr);
@@ -141,11 +165,117 @@ bool QIIOFHelpers::readImage(QImageIOHandler *q_ptr, QImage *out)
         return false;
 
     *out = qt_mac_toQImage(cgImage);
-    return !out->isNull();
+    if (out->isNull())
+        return false;
+
+    int dpi = 0;
+    if (getIntProperty(kCGImagePropertyDPIWidth, &dpi))
+        out->setDotsPerMeterX(qRound(dpi / 0.0254f));
+    if (getIntProperty(kCGImagePropertyDPIHeight, &dpi))
+        out->setDotsPerMeterY(qRound(dpi / 0.0254f));
+
+    return true;
 }
 
+bool QIIOFHelper::getIntProperty(CFStringRef property, int *value)
+{
+    if (!cfImageDict)
+        return false;
 
-bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti)
+    CFNumberRef cfNumber = static_cast<CFNumberRef>(CFDictionaryGetValue(cfImageDict, property));
+    if (cfNumber) {
+        int intVal;
+        if (CFNumberGetValue(cfNumber, kCFNumberIntType, &intVal)) {
+            if (value)
+                *value = intVal;
+            return true;
+        }
+    }
+    return false;
+}
+
+static QImageIOHandler::Transformations exif2Qt(int exifOrientation)
+{
+    switch (exifOrientation) {
+    case 1: // normal
+        return QImageIOHandler::TransformationNone;
+    case 2: // mirror horizontal
+        return QImageIOHandler::TransformationMirror;
+    case 3: // rotate 180
+        return QImageIOHandler::TransformationRotate180;
+    case 4: // mirror vertical
+        return QImageIOHandler::TransformationFlip;
+    case 5: // mirror horizontal and rotate 270 CW
+        return QImageIOHandler::TransformationFlipAndRotate90;
+    case 6: // rotate 90 CW
+        return QImageIOHandler::TransformationRotate90;
+    case 7: // mirror horizontal and rotate 90 CW
+        return QImageIOHandler::TransformationMirrorAndRotate90;
+    case 8: // rotate 270 CW
+        return QImageIOHandler::TransformationRotate270;
+    }
+    return QImageIOHandler::TransformationNone;
+}
+
+static int qt2Exif(QImageIOHandler::Transformations transformation)
+{
+    switch (transformation) {
+    case QImageIOHandler::TransformationNone:
+        return 1;
+    case QImageIOHandler::TransformationMirror:
+        return 2;
+    case QImageIOHandler::TransformationRotate180:
+        return 3;
+    case QImageIOHandler::TransformationFlip:
+        return 4;
+    case QImageIOHandler::TransformationFlipAndRotate90:
+        return 5;
+    case QImageIOHandler::TransformationRotate90:
+        return 6;
+    case QImageIOHandler::TransformationMirrorAndRotate90:
+        return 7;
+    case QImageIOHandler::TransformationRotate270:
+        return 8;
+    }
+    qWarning("Invalid Qt image transformation");
+    return 1;
+}
+
+QVariant QIIOFHelper::imageProperty(QImageIOHandler::ImageOption option)
+{
+    if (!initRead())
+        return QVariant();
+
+    switch (option) {
+    case QImageIOHandler::Size: {
+        QSize sz;
+        if (getIntProperty(kCGImagePropertyPixelWidth, &sz.rwidth())
+                && getIntProperty(kCGImagePropertyPixelHeight, &sz.rheight())) {
+            return sz;
+        }
+        break;
+    }
+    case QImageIOHandler::ImageTransformation: {
+        int orient;
+        if (getIntProperty(kCGImagePropertyOrientation, &orient))
+            return int(exif2Qt(orient));
+        break;
+    }
+    default:
+        break;
+    }
+
+    return QVariant();
+}
+
+void QIIOFHelper::setOption(QImageIOHandler::ImageOption option, const QVariant &value)
+{
+    if (writeOptions.size() < option + 1)
+        writeOptions.resize(option + 1);
+    writeOptions[option] = value;
+}
+
+bool QIIOFHelper::writeImage(const QImage &in, const QString &uti)
 {
     static const CGDataConsumerCallbacks cgCallbacks = { &cbPutBytes, nullptr };
 
@@ -159,17 +289,35 @@ bool QIIOFHelpers::writeImage(QImageIOHandler *q_ptr, const QImage &in, const QS
     if (!cgImageDest || !cgImage)
         return false;
 
-    QCFType<CFNumberRef> cfVal;
-    QCFType<CFDictionaryRef> cfProps;
+    QCFType<CFNumberRef> cfQuality = nullptr;
+    QCFType<CFNumberRef> cfOrientation = nullptr;
+    const void *dictKeys[2];
+    const void *dictVals[2];
+    int dictSize = 0;
+
     if (q_ptr->supportsOption(QImageIOHandler::Quality)) {
         bool ok = false;
-        int writeQuality = q_ptr->option(QImageIOHandler::Quality).toInt(&ok);
+        int writeQuality = writeOptions.value(QImageIOHandler::Quality).toInt(&ok);
         // If quality is unset, default to 75%
-        float quality = (ok && writeQuality >= 0 ? (qMin(writeQuality, 100)) : 75) / 100.0;
-        cfVal = CFNumberCreate(nullptr, kCFNumberFloatType, &quality);
-        cfProps = CFDictionaryCreate(nullptr, (const void **)&kCGImageDestinationLossyCompressionQuality, (const void **)&cfVal, 1,
-                                     &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+        float quality = (ok && writeQuality >= 0 ? (qMin(writeQuality, 100)) : 75) / 100.0f;
+        cfQuality = CFNumberCreate(nullptr, kCFNumberFloatType, &quality);
+        dictKeys[dictSize] = static_cast<const void *>(kCGImageDestinationLossyCompressionQuality);
+        dictVals[dictSize] = static_cast<const void *>(cfQuality);
+        dictSize++;
+    }
+    if (q_ptr->supportsOption(QImageIOHandler::ImageTransformation)) {
+        int orient = qt2Exif(static_cast<QImageIOHandler::Transformation>(writeOptions.value(QImageIOHandler::ImageTransformation).toInt()));
+        cfOrientation = CFNumberCreate(nullptr, kCFNumberIntType, &orient);
+        dictKeys[dictSize] = static_cast<const void *>(kCGImagePropertyOrientation);
+        dictVals[dictSize] = static_cast<const void *>(cfOrientation);
+        dictSize++;
     }
+
+    QCFType<CFDictionaryRef> cfProps = nullptr;
+    if (dictSize)
+        cfProps = CFDictionaryCreate(nullptr, dictKeys, dictVals, dictSize,
+                                     &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
     CGImageDestinationAddImage(cgImageDest, cgImage, cfProps);
     return CGImageDestinationFinalize(cgImageDest);
 }
diff --git a/src/plugins/imageformats/shared/qiiofhelpers_p.h b/src/plugins/imageformats/shared/qiiofhelpers_p.h
index da517318..1b1771b9 100644
--- a/src/plugins/imageformats/shared/qiiofhelpers_p.h
+++ b/src/plugins/imageformats/shared/qiiofhelpers_p.h
@@ -52,6 +52,9 @@
 //
 
 #include <QImageIOPlugin>
+#include <private/qcore_mac_p.h>
+#include <ImageIO/ImageIO.h>
+#include <QVector>
 
 QT_BEGIN_NAMESPACE
 
@@ -67,6 +70,27 @@ public:
     static bool writeImage(QImageIOHandler *q_ptr, const QImage &in, const QString &uti);
 };
 
+class QIIOFHelper
+{
+public:
+    QIIOFHelper(QImageIOHandler *q);
+
+    bool readImage(QImage *out);
+    bool writeImage(const QImage &in, const QString &uti);
+    QVariant imageProperty(QImageIOHandler::ImageOption option);
+    void setOption(QImageIOHandler::ImageOption option, const QVariant &value);
+
+protected:
+    bool initRead();
+    bool getIntProperty(CFStringRef property, int *value);
+
+    QImageIOHandler *q_ptr = nullptr;
+    QVector<QVariant> writeOptions;
+    QCFType<CGDataProviderRef> cgDataProvider = nullptr;
+    QCFType<CGImageSourceRef> cgImageSource = nullptr;
+    QCFType<CFDictionaryRef> cfImageDict = nullptr;
+};
+
 QT_END_NAMESPACE
 
 #endif
diff --git a/tests/auto/heif/tst_qheif.cpp b/tests/auto/heif/tst_qheif.cpp
index faf22fa1..26ddf731 100644
--- a/tests/auto/heif/tst_qheif.cpp
+++ b/tests/auto/heif/tst_qheif.cpp
@@ -37,6 +37,9 @@ private slots:
     void initTestCase();
     void readImage_data();
     void readImage();
+    void readProperties_data();
+    void readProperties();
+    void writeImage();
 };
 
 void tst_qheif::initTestCase()
@@ -49,8 +52,10 @@ void tst_qheif::readImage_data()
 {
     QTest::addColumn<QString>("fileName");
     QTest::addColumn<QSize>("size");
+    QTest::addColumn<int>("transform");
 
-    QTest::newRow("col") << QString("col320x480.heic") << QSize(320, 480);
+    QTest::newRow("col") << QString("col320x480.heic") << QSize(320, 480) << int(QImageIOHandler::TransformationNone);
+    QTest::newRow("rot") << QString("newlogoCCW.heic") << QSize(110, 78) << int(QImageIOHandler::TransformationRotate90);
 }
 
 void tst_qheif::readImage()
@@ -66,5 +71,78 @@ void tst_qheif::readImage()
     QCOMPARE(image.size(), size);
 }
 
+void tst_qheif::readProperties_data()
+{
+    readImage_data();
+}
+
+void tst_qheif::readProperties()
+{
+    QFETCH(QString, fileName);
+    QFETCH(QSize, size);
+    QFETCH(int, transform);
+
+    QSize rawSize = (transform & QImageIOHandler::TransformationRotate90) ? size.transposed() : size;
+
+    QString path = QStringLiteral(":/heif/") + fileName;
+    QImageReader reader(path);
+    QCOMPARE(reader.size(), rawSize);
+    QCOMPARE(int(reader.transformation()), transform);
+
+    QImage image = reader.read();
+    QCOMPARE(image.size(), size);
+
+    QCOMPARE(reader.size(), rawSize);
+    QCOMPARE(int(reader.transformation()), transform);
+}
+
+void tst_qheif::writeImage()
+{
+    QImage img(20, 10, QImage::Format_ARGB32_Premultiplied);
+    img.fill(Qt::green);
+
+    QBuffer buf1, buf2;
+    QImage rimg1;
+
+    {
+        buf1.open(QIODevice::WriteOnly);
+        QImageWriter writer(&buf1, "heic");
+        QVERIFY(writer.write(img));
+        buf1.close();
+        QVERIFY(buf1.size() > 0);
+
+        buf1.open(QIODevice::ReadOnly);
+        QImageReader reader(&buf1);
+        QVERIFY(reader.read(&rimg1));
+        buf1.close();
+        QVERIFY(rimg1.size() == img.size());
+    }
+
+    {
+        buf2.open(QIODevice::WriteOnly);
+        QImageWriter writer(&buf2, "heic");
+        writer.setQuality(20);
+        QVERIFY(writer.write(img));
+        buf2.close();
+        QVERIFY(buf2.size() > 0);
+        QVERIFY(buf2.size() < buf1.size());
+    }
+
+    {
+        buf2.open(QIODevice::WriteOnly);
+        QImageWriter writer(&buf2, "heic");
+        writer.setTransformation(QImageIOHandler::TransformationRotate270);
+        QVERIFY(writer.write(img));
+        buf2.close();
+
+        QImage rimg2;
+        buf2.open(QIODevice::ReadOnly);
+        QImageReader reader(&buf2);
+        QVERIFY(reader.read(&rimg2));
+        buf2.close();
+        QVERIFY(rimg2.size() == img.size().transposed());
+    }
+}
+
 QTEST_MAIN(tst_qheif)
 #include "tst_qheif.moc"
diff --git a/tests/shared/images/heif.qrc b/tests/shared/images/heif.qrc
index 2a41c36b..8232b6a5 100644
--- a/tests/shared/images/heif.qrc
+++ b/tests/shared/images/heif.qrc
@@ -1,5 +1,6 @@
 <RCC>
     <qresource prefix="/">
         <file>heif/col320x480.heic</file>
+        <file>heif/newlogoCCW.heic</file>
     </qresource>
 </RCC>
diff --git a/tests/shared/images/heif/newlogoCCW.heic b/tests/shared/images/heif/newlogoCCW.heic
new file mode 100644
index 0000000000000000000000000000000000000000..1604947e8b7998c14c42954f560608674da43be8
GIT binary patch
literal 4847
zcmbW42{@G99>C8#J7#P%ge+s<*BC>|zLThIsW3Ab%oxMiqEb?pFOp<wqY^2tmV6bh
zh{zgkNJy!)D3!SHaPL?5KF@u=``rIC?>WEoU*5C6&-@PnKq@3QkrT#X(f}+hW{BYu
z6JE0zu|a$tEJ|VYNbV8>z+uv2|HXgRDs(101a8UFxeR{I7YKgvJZ>cUpDfH_#<E~u
zvQU{z5da`yjRf3a@PQu{7DqGW<G+*;-^D(Gx%grk=C2-vY4jKxY=rnx1U!?&<-lFO
zWpZdyaJ`-u70Kmrqu?;SlN*yAg%E{j$d2WDIognY{rt(;4mef-U;x}(5G{t|zRqDi
zzh7s2TXGES#OGh~rGIfophwqfEt&jF|38r!jl+dk4O$C(ub?yFQ3EIq=E(S14qrbF
zGbuQN&j=Ksx$ppCmf~~h66-AbEU^`z=`1!K_Ti72Lub+Xd<W*eaXbdhNH>_%<Cu(i
zn0sMXiR7`EFweqFVljeZ06^jS`d9`n3}yqE@!a)ZwlJ>(0EZ9#je~#VSVjUoP5^A9
zIEh?lXjm*+gQiI~G&7@;ofz?vjM!M+wL!FqATFJ38^z)Tu@eDUnlnENi1XK$3@<Xp
z(2PRSHPTz$>_yYx82{<>cNKqcmwY~XF76p5+5SuSEBco%stf??47@hEzjVPx0MzY<
zKaj`2bSlRI5J>}|w)407Nb=81SZplETwgyvK3<Q>py}}!^e_2ahrc}kJN(w49>2f8
z){bn)2npgv#*+Dq3ZM8W9+w=$38FE`y8n5I|L?%xhV|Pxbi5cL3@(EWcjXPAGA26|
z-flLX8Ow}flbP)Qw!{Bt*l%Ot<I=f?fp~HrkhbUn(M}RTzHJ96q7Xm^9EB~=U;E}J
z;0^d+o{z%B(z%Bjwl9|dRv>cVCL)FzO6IF=z1EXyJZ>DH;ctRppn)I|0VE&|<bg8K
z0J^{kn1YqS3fKb|-~rYHKd>3lKp0>FE{F%oAQhy8ERYKhf?{wCoCKAi8e9T(;2O9I
z?twP&5cGj3U>J;nH((lk0Y4xJ!a@Xy2uVW<kSe4N89`KNHDnLDL0*s_6bOYvY={RX
zL%X0XC=V)zN})>VB6JnH3AI99&>%DdO+p``?+7FUk02rB5NZg01QlV8a7C;~Y(j)0
zq7g}mG{incA)*X%7Ez11iD*OgA%+oe5Hm=C6hcZO6_L6~D$*9|f%Hd)Ai2nG$V_Ab
zvJ6>`Y(O?6dyvD(Ddaa421P`XQM#y=C@0hg6b%)P+J?$T6{9LpwWvF&9@GeG8a0n5
zpk>k8XmhkP+7}&)jz_1X3()208uVRsA9@V^1%tszV$?CF7$=M$CLFUBlZ`ooIge?=
zbYVs@GgvHE8mo=9z<OYVu(8;5Y!UV>wh`Nn9mCGzgm4NtBb)=yAIHY+#1-Jq;BMf$
zaTB;70-^${0_Fl90(5~yfgFL80u2J40%HO{1jPi^1uX>E3x*4(3Kj}h3*HrcD)>oA
zNJvS@OvqD+DYQeVQ0RhCi_nPBH@rAr3vY`L!1M6?@Tc)j_(A+90)e1Lup;;qVhQ^Q
zm4w@b=Y&~dqOhK@voKwFyKsqct#G&SI}rg9RS|2E%_2!61tOP4Iz^^L1w_?EZAF7b
zw~3aBUKM>LIwM9DGZga>V~J&poe^sldnJw)R};4vr;DeFpAf$-K1xIqm5H`Q8gUo#
zB=IhBjD#VnlblK6q%6`oQU_^TLR`XF!doIiqD11l#IPh%QccoXGEy=}@{(k~<o9KA
z%dD3%mSrrfUe>+rtJHESD=CIlrqp?<KB@20WNCZp2<iROb<)pdP%>IFo-*+=M`iBH
zOf4rZU%8yNJahS_<&R|%vRblUvRh?O$hOPQ$jQq&%5mh1<ZjDN$xF&x$%o75%ioZn
zR3Iv>R$waRD>N#+CQFd5$&utj@*VQDqO78mB2TeYu|x5PlDg6crJYLWm7XaJDpQq1
zl=GEuDNm~?sJN>nt5m5xRuxdCs)nf^Qf*fKs-~{yqqaw_PVJStl)AHel6sZ;Qw@TK
zrAD+ynMSWBMw6-;p?O5}p%y~RL@P|IM5|L9p-s_dY9G<=(n0H(>9BRmbROyAb***d
zbgOhn^d$A%^mgji>Alle(f8NiuivacZ(w2&X>iiuiJ`cmi{Vbg2E&g=T1GUZ5~JP~
zgcS}ewy&sP@xfTzm|=X>c))~c;%>6X<d(@#iYbLlsisVts+b0v9yWbMB~jN>v#Bj+
zXfqqL?Pk}^zL`_ZW6dv_zh9}lGGgVKm9JK*uVSn^xoXrx$s)+2)M9uwd3E6G<Ew`)
z6)l4-%Pe15DO=I4%B?1>HLb&~&sx8;F|gs<)YyErHMdQ%ZL&k!Iof5~b=ZsBZ?G@6
zf9{~<5b99nFzsmKxYe=A3GL+SwBPBGvz#-{xzc&s#ndIm<&G=fb)9Rm>kBt6H?CWQ
zJHp+?{eb(^HL7daYwA3JhqK24k7sMu*G8}X(-Y&l*0aQO+{@4_*{gXSaov`6XV!gL
zZ?isU{gVwE8+aRTd5d@lcvpIV^|ABG^BMNl_f7F_^ONyo`qlem{k{Fm{bx4XZ#=kh
zEWjinJ)nP+`lf_Ut(#>wM{d5pMPy6Rmdk<YK%c-ffpb9~L1jTR!Op>lgQsb>v_rHv
zbSrv3eUf3p$YV@|ScK$-yb84lJrFt>W*JrxHpR4M7BfGDJB1$${~F;DaXMll(kJpf
z3&#p%U1f{2BiMJM<f9Uzx;WaLbk1<JdGx{PX|5}`JO+x{6jL8df|IFso(69>ZzOJY
zTuIz)ym$QN1hE8mLVKcCVn*Ubl0(v|t*EWEt+$dDlT(vNQmj)-w?W&2x82&VyghCE
z*bc`X6{&)$5vdP$8tly5`FWSmu7)(Zv>j=qyB&8|r3<HX(;w|wx#!qk)L!P^j*Jx<
zMH%y%^vwHN23ZAJbJ?`)`+pezQTWHgzL0$#Ih35ExtLsbZvTGE{S|p)c}aPr2iy<T
z=9BX?^FJTla<H|)xZv0!!9%=5!-Z~z^+hU0xkYouVa0tVHYMi|%O1`+{PhU^NcT~z
zqt(acj%6MDew=xHpwzLnwoI+8-~{GG+==m%-Y1(+QBPHt%amu8|2)k;JzU{cai`L>
zvhs}VnVc#_6|ZXYY{1!v=j_haSL;@no|inIbpc%9U3h&l@M8ZZ*Gso9Q!iIvQN41w
zhFFtX3)LppPS=Ii4cGhCcU^V4daGe&L(QLhf1bXkc&+5R#PysTf;ZA`EHoxH&NOkF
zrf!Db9KE&W)|1=5w|nn+-s!mOdbj1C!@b+h*3FGA7A@CW&0DYDr{1q?qqNnwo3z(F
zFnLhZLFuUNH0`W^X!fw7YgN~cZp-eQJ$60!dYyaQ`quPy^>64Ocogtxcz{0eYA|x}
z!{fNeKc4J(ihi2;O!Qg7bJ^!*Lz+Y9hbhC?N9;!0U#xraWRy1g=4H&wxv{iy!g#@i
z!bIgOqgU4^9VQ>X-uQa#4d=~|sr0vEZx6rIcz5N!)%*5o-|4Xr+z$&Mvp-3HI{n%B
z^R1b+Gta-UzWn%_IV(L|@s0Ye`Mb~ei68NE*tx==nm-%nUFV-Juof2J|9=*IH?f7F
zKo~E;4*>GhVD9HEoKlM*kHZ<oGAjrVEiCXIkbDm$|0k~_Pzu|~Ht=8jzZ;Hgq&9En
zzTB_mno&a*ErXqKc2HArs{v63em993!(qTRU)I168-7GN%mg@zhhK}s;oxc+lfw$)
zw_$;=L8#Q7NPeQC!;FktywTag`3!7W%yxt+a29iwf0^QIgvH^S-wni~2QB9Fu=Oq6
z#w=a6+}(i|+`<6}66enYj={j?qM&NjpmR;wKtw^V&*@K*3DR}4JvDS}CMDfs=5=_8
z;#9}uuFaQI8uzqa2)*VegN_F-y92r@BIW|yP)9#!AABjKhv-M?Bz*jS*J7mV#n3`<
zu!v1>`WUscj53jKQ)_p`^5@EWlYZP#VMWTNr>)NOv+@&oURuw|$C@dm$|hvHwP8|9
zW`fPpy*>egiu0aPn-aEi5)Ce`i9#30a{`MtB1}bdmm97+6RcQ$Y0JIDwgH>{Z+go7
zM`H>nb1yJnv{$Wd=GxgwYTZzgJ*R&551|T`JZrb?Fy_`|=uJ93ZY|4QWOU7b)QT2+
z`d0s~dXx-SK<uE{6_YvOnmB9m-h$CPFWu-ocAGMJ;vmo2K9_>6Gz|U;rL>x*)?G|J
zATT6kmv?cu*IQpk)nEuVLT=JJH2xT?066Md9VcmP73Gi%B;08F#$HWl*Jzn_>iv;C
zMYx|`u^RC?AiX;J?0e-F>5qfn*=kRfymc(T$w%3=c7cVmDBkh5&ZwR+N12?3D;>?x
zlsk{y+&+->_@<_=;NYwitI`>_@G@a|U)pp*V~=mkc26hsF1hL#Bgy$z);fvL9af2^
z4!z$WUyc7+6T8Vwv`sz!`M$2hhW6276KpX<5PSLK;1jD?wWmwNvy#IK-z7&i#-zx6
z$n@W`{psk%P1<{2mVV2Smq<AHVmdm1-aqSVl8KRD$_Io)=B2lzLqqItqn8&lF0%z<
z2m?QAOm7qHU7$Vrn{$(ACQ>)rRFqf>Leie<vliXPcbxV8)_gfwnJw*oan+bvX!NAi
zj8RAo@4Zhy^z=zsQ~JyE`xGo^vi|&}PxA=ebtIahC`PT>f6d9k$Y9;-w^S@LCRKM0
z;pp|-`wa3c?)l#sXO-jfno13iI)(;?=|%((J@>@e$fbrh7)0RC8kf~Ty4QRUudDnq
zsA4}o`(Y|~1lMfGvraA-akUiG8ahi-R4UH*6}1x`eR|}8$0^i?#wHQ({(Bmk&jiGF
zT_4wxa^?+u+|=N=z5MgJnW<;nh=_6K>xnc#!N~_6L4L>Xm|=ypmA+CVH8$V6$QkfI
z6gQkHudmRyX7iqLjf)k=pVVYp9Cmnp>#b=_ZR#N0*)<t{{N86-o$1g`rX$98ifx=v
zc3xB(SB(34^?dN%6dCWk=O#^Ebsq(GmTr#MIB8P&pdt0dfN!p;O8Z>%BvEPS1)PGF
zU+W|mAmWuqn0^OdZ@iwIOFuTZpyI5(;@ds)cRRvvnI~VQ-n%N?uM-^dO$G0XE0E1E
zFHBLT+EiS=n|*mc-#w1Me{mUW@F&|OGSF3YrMt@V;rseco^5-2KerUWHa~ED^w4Nu
z$Ck7r{m325&35-vX*%<H1mvkJ_irCDj$GAqb`MWxgQ4^6#>aB+IxHyn-`4kqiIARU
z-2FlEj#HwoO4t}Q^D)<Vue`%vzj~kN<%H)}a&zqqQK3m^61pzbw+t1w&FK#2+YhQ5
zcv|%BY?64W^z+l?;bEl60x3GpOqFTUTJ~fr>C@*yOdciWN>d9~zhM59J6^`SzmF8{
zMS74*I%J+Hd@ti|j8ZZ!zui3ifyC=^qC@}D!%dAv0|gq57NO;+ahJNB4Q~13r(7of
E0dyOg?*IS*

literal 0
HcmV?d00001

-- 
GitLab