forked from soutade/libgourou
Add support for PDF (needs uPDFParser library)
This commit is contained in:
parent
8bc346d139
commit
3d9e343734
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -4,5 +4,5 @@ lib
|
|||
*.so
|
||||
*~
|
||||
utils/acsmdownloader
|
||||
utils/activate
|
||||
utils/adept_activate
|
||||
.adept
|
||||
|
|
25
Makefile
25
Makefile
|
@ -2,8 +2,21 @@
|
|||
AR ?= $(CROSS)ar
|
||||
CXX ?= $(CROSS)g++
|
||||
|
||||
CXXFLAGS=-Wall -fPIC -I./include -I./lib -I./lib/pugixml/src/
|
||||
LDFLAGS=
|
||||
UPDFPARSERLIB = ./lib/updfparser/libupdfparser.a
|
||||
|
||||
CXXFLAGS=-Wall -fPIC -I./include -I./lib -I./lib/pugixml/src/ -I./lib/updfparser/include
|
||||
LDFLAGS = $(UPDFPARSERLIB)
|
||||
|
||||
BUILD_STATIC ?= 0
|
||||
BUILD_SHARED ?= 1
|
||||
|
||||
TARGETS =
|
||||
ifneq (BUILD_STATIC, 0)
|
||||
TARGETS += libgourou.a
|
||||
endif
|
||||
ifneq (BUILD_SHARED, 0)
|
||||
TARGETS += libgourou.so
|
||||
endif
|
||||
|
||||
ifneq ($(DEBUG),)
|
||||
CXXFLAGS += -ggdb -O0
|
||||
|
@ -18,12 +31,12 @@ TARGETDIR := bin
|
|||
SRCEXT := cpp
|
||||
OBJEXT := o
|
||||
|
||||
SOURCES=src/libgourou.cpp src/user.cpp src/device.cpp src/fulfillment_item.cpp src/bytearray.cpp src/pugixml.cpp
|
||||
SOURCES = src/libgourou.cpp src/user.cpp src/device.cpp src/fulfillment_item.cpp src/bytearray.cpp src/pugixml.cpp
|
||||
OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT)))
|
||||
|
||||
.PHONY: utils
|
||||
|
||||
all: lib obj libgourou utils
|
||||
all: lib obj $(TARGETS) utils
|
||||
|
||||
lib:
|
||||
mkdir lib
|
||||
|
@ -38,9 +51,9 @@ $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT)
|
|||
libgourou: libgourou.a libgourou.so
|
||||
|
||||
libgourou.a: $(OBJECTS)
|
||||
$(AR) crs $@ obj/*.o
|
||||
$(AR) crs $@ obj/*.o $(LDFLAGS)
|
||||
|
||||
libgourou.so: libgourou.a
|
||||
libgourou.so: $(OBJECTS) $(UPDFPARSERLIB)
|
||||
$(CXX) obj/*.o $(LDFLAGS) -o $@ -shared
|
||||
|
||||
utils:
|
||||
|
|
|
@ -47,7 +47,7 @@ Compilation
|
|||
|
||||
Use _make_ command
|
||||
|
||||
make [CROSS=XXX] [DEBUG=1] [STATIC_UTILS=1]
|
||||
make [CROSS=XXX] [DEBUG=1] [STATIC_UTILS=1] [BUILD_STATIC=(0|1)] [BUILD_SHARED=(0|1)]
|
||||
|
||||
CROSS can define a cross compiler prefix (ie arm-linux-gnueabihf-)
|
||||
|
||||
|
@ -55,6 +55,9 @@ DEBUG can be set to compile in DEBUG mode
|
|||
|
||||
STATIC_UTILS to build utils with static library (libgourou.a) instead of default dynamic one (libgourou.so)
|
||||
|
||||
BUILD_STATIC build libgourou.a if 1, nothing if 0 (default value), can be combined with BUILD_SHARED
|
||||
|
||||
BUILD_SHARED build libgourou.so if 1 (default value), nothing if 0, can be combined with BUILD_STATIC
|
||||
|
||||
Utils
|
||||
-----
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#define _DRMPROCESSORCLIENT_H_
|
||||
|
||||
#include <string>
|
||||
#include <bytearray.h>
|
||||
|
||||
namespace gourou
|
||||
{
|
||||
|
@ -93,11 +94,14 @@ namespace gourou
|
|||
/**
|
||||
* @brief Send HTTP (GET or POST) request
|
||||
*
|
||||
* @param URL HTTP URL
|
||||
* @param POSTData POST data if needed, if not set, a GET request is done
|
||||
* @param contentType Optional content type of POST Data
|
||||
* @param URL HTTP URL
|
||||
* @param POSTData POST data if needed, if not set, a GET request is done
|
||||
* @param contentType Optional content type of POST Data
|
||||
* @param responseHeaders Optional Response headers of HTTP request
|
||||
*
|
||||
* @return data of HTTP response
|
||||
*/
|
||||
virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string("")) = 0;
|
||||
virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string(""), std::map<std::string, std::string>* responseHeaders=0) = 0;
|
||||
};
|
||||
|
||||
class RSAInterface
|
||||
|
@ -355,6 +359,27 @@ namespace gourou
|
|||
* @param handler ZIP file handler
|
||||
*/
|
||||
virtual void zipClose(void* handler) = 0;
|
||||
|
||||
/**
|
||||
* @brief Inflate algorithm
|
||||
*
|
||||
* @param data Data to inflate
|
||||
* @param result Zipped data
|
||||
* @param wbits Window bits value for libz
|
||||
*/
|
||||
virtual void inflate(std::string data, gourou::ByteArray& result,
|
||||
int wbits=-15) = 0;
|
||||
|
||||
/**
|
||||
* @brief Deflate algorithm
|
||||
*
|
||||
* @param data Data to deflate
|
||||
* @param result Unzipped data
|
||||
* @param wbits Window bits value for libz
|
||||
* @param compressionLevel Compression level for libz
|
||||
*/
|
||||
virtual void deflate(std::string data, gourou::ByteArray& result,
|
||||
int wbits=-15, int compressionLevel=8) = 0;
|
||||
};
|
||||
|
||||
class DRMProcessorClient: public DigestInterface, public RandomInterface, public HTTPInterface, \
|
||||
|
|
|
@ -53,10 +53,16 @@ namespace gourou
|
|||
*/
|
||||
std::string getDownloadURL();
|
||||
|
||||
/**
|
||||
* @brief Return resource value
|
||||
*/
|
||||
std::string getResource();
|
||||
|
||||
private:
|
||||
pugi::xml_node metadatas;
|
||||
pugi::xml_document rights;
|
||||
std::string downloadURL;
|
||||
std::string resource;
|
||||
|
||||
void buildRights(const pugi::xml_node& licenseToken, User* user);
|
||||
};
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
#define ACS_SERVER "http://adeactivate.adobe.com/adept"
|
||||
#endif
|
||||
|
||||
#define LIBGOUROU_VERSION "0.3.2"
|
||||
#define LIBGOUROU_VERSION "0.4"
|
||||
|
||||
namespace gourou
|
||||
{
|
||||
|
@ -53,6 +53,7 @@ namespace gourou
|
|||
|
||||
static const std::string VERSION;
|
||||
|
||||
enum ITEM_TYPE { EPUB=0, PDF };
|
||||
/**
|
||||
* @brief Main constructor. To be used once all is configured (user has signedIn, device is activated)
|
||||
*
|
||||
|
@ -80,8 +81,10 @@ namespace gourou
|
|||
*
|
||||
* @param item Item from fulfill() method
|
||||
* @param path Output file path
|
||||
*
|
||||
* @return Type of downloaded item
|
||||
*/
|
||||
void download(FulfillmentItem* item, std::string path);
|
||||
ITEM_TYPE download(FulfillmentItem* item, std::string path);
|
||||
|
||||
/**
|
||||
* @brief SignIn into ACS Server (required to activate device)
|
||||
|
@ -130,8 +133,11 @@ namespace gourou
|
|||
* @param URL HTTP URL
|
||||
* @param POSTData POST data if needed, if not set, a GET request is done
|
||||
* @param contentType Optional content type of POST Data
|
||||
* @param responseHeaders Optional Response headers of HTTP request
|
||||
*
|
||||
* @return data of HTTP response
|
||||
*/
|
||||
ByteArray sendRequest(const std::string& URL, const std::string& POSTData=std::string(), const char* contentType=0);
|
||||
ByteArray sendRequest(const std::string& URL, const std::string& POSTData=std::string(), const char* contentType=0, std::map<std::string, std::string>* responseHeaders=0);
|
||||
|
||||
/**
|
||||
* @brief Send HTTP POST request to URL with document as POSTData
|
||||
|
|
|
@ -233,6 +233,32 @@ namespace gourou
|
|||
return trim(res);
|
||||
}
|
||||
|
||||
static inline std::string extractTextElem(const pugi::xml_node& doc, const char* tagName, bool throwOnNull=true)
|
||||
{
|
||||
pugi::xpath_node xpath_node = doc.select_node(tagName);
|
||||
|
||||
if (!xpath_node)
|
||||
{
|
||||
if (throwOnNull)
|
||||
EXCEPTION(GOUROU_TAG_NOT_FOUND, "Tag " << tagName << " not found");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pugi::xml_node node = xpath_node.node().first_child();
|
||||
|
||||
if (!node)
|
||||
{
|
||||
if (throwOnNull)
|
||||
EXCEPTION(GOUROU_TAG_NOT_FOUND, "Text element for tag " << tagName << " not found");
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string res = node.value();
|
||||
return trim(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Append an element to root with a sub text element
|
||||
*
|
||||
|
@ -267,7 +293,7 @@ namespace gourou
|
|||
*/
|
||||
static inline void writeFile(std::string path, ByteArray& data)
|
||||
{
|
||||
writeFile(path, data.data(), data.length());
|
||||
writeFile(path, data.data(), data.length()-1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,3 +12,11 @@ fi
|
|||
if [ ! -d lib/base64 ] ; then
|
||||
git clone https://gist.github.com/f0fd86b6c73063283afe550bc5d77594.git lib/base64
|
||||
fi
|
||||
|
||||
# uPDFParser
|
||||
if [ ! -d lib/updfparser ] ; then
|
||||
git clone http://indefero.soutade.fr/p/updfparser lib/updfparser
|
||||
pushd lib/updfparser
|
||||
make STATIC=1 SHARED=0
|
||||
popd
|
||||
fi
|
||||
|
|
|
@ -22,6 +22,8 @@
|
|||
#include <time.h>
|
||||
#include <vector>
|
||||
|
||||
#include <uPDFParser.h>
|
||||
|
||||
#include <libgourou.h>
|
||||
#include <libgourou_common.h>
|
||||
#include <libgourou_log.h>
|
||||
|
@ -299,11 +301,11 @@ namespace gourou
|
|||
appendTextElem(root, "adept:expiration", buffer);
|
||||
}
|
||||
|
||||
ByteArray DRMProcessor::sendRequest(const std::string& URL, const std::string& POSTdata, const char* contentType)
|
||||
ByteArray DRMProcessor::sendRequest(const std::string& URL, const std::string& POSTdata, const char* contentType, std::map<std::string, std::string>* responseHeaders)
|
||||
{
|
||||
if (contentType == 0)
|
||||
contentType = "";
|
||||
std::string reply = client->sendHTTPRequest(URL, POSTdata, contentType);
|
||||
std::string reply = client->sendHTTPRequest(URL, POSTdata, contentType, responseHeaders);
|
||||
|
||||
pugi::xml_document replyDoc;
|
||||
replyDoc.load_buffer(reply.c_str(), reply.length());
|
||||
|
@ -580,12 +582,16 @@ namespace gourou
|
|||
return new FulfillmentItem(fulfillReply, user);
|
||||
}
|
||||
|
||||
void DRMProcessor::download(FulfillmentItem* item, std::string path)
|
||||
DRMProcessor::ITEM_TYPE DRMProcessor::download(FulfillmentItem* item, std::string path)
|
||||
{
|
||||
ITEM_TYPE res = EPUB;
|
||||
|
||||
if (!item)
|
||||
EXCEPTION(DW_NO_ITEM, "No item");
|
||||
|
||||
std::map<std::string, std::string> headers;
|
||||
|
||||
ByteArray replyData = sendRequest(item->getDownloadURL());
|
||||
ByteArray replyData = sendRequest(item->getDownloadURL(), "", 0, &headers);
|
||||
|
||||
writeFile(path, replyData);
|
||||
|
||||
|
@ -593,9 +599,53 @@ namespace gourou
|
|||
|
||||
std::string rightsStr = item->getRights();
|
||||
|
||||
void* handler = client->zipOpen(path);
|
||||
client->zipWriteFile(handler, "META-INF/rights.xml", rightsStr);
|
||||
client->zipClose(handler);
|
||||
if (headers.count("Content-Type") && headers["Content-Type"] == "application/pdf")
|
||||
res = PDF;
|
||||
|
||||
if (res == EPUB)
|
||||
{
|
||||
void* handler = client->zipOpen(path);
|
||||
client->zipWriteFile(handler, "META-INF/rights.xml", rightsStr);
|
||||
client->zipClose(handler);
|
||||
}
|
||||
else if (res == PDF)
|
||||
{
|
||||
uPDFParser::Parser parser;
|
||||
|
||||
try
|
||||
{
|
||||
GOUROU_LOG(DEBUG, "Parse PDF");
|
||||
parser.parse(path);
|
||||
}
|
||||
catch(std::invalid_argument& e)
|
||||
{
|
||||
GOUROU_LOG(ERROR, "Invalid PDF");
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<uPDFParser::Object*> objects = parser.objects();
|
||||
std::vector<uPDFParser::Object*>::reverse_iterator it;
|
||||
|
||||
for(it = objects.rbegin(); it != objects.rend(); it++)
|
||||
{
|
||||
// Update EBX_HANDLER with rights
|
||||
if ((*it)->hasKey("Filter") && (**it)["Filter"]->str() == "/EBX_HANDLER")
|
||||
{
|
||||
uPDFParser::Object* ebx = (*it)->clone();
|
||||
(*ebx)["ADEPT_ID"] = new uPDFParser::String(item->getResource());
|
||||
(*ebx)["EBX_BOOKID"] = new uPDFParser::String(item->getResource());
|
||||
ByteArray zipped;
|
||||
client->deflate(rightsStr, zipped);
|
||||
(*ebx)["ADEPT_LICENSE"] = new uPDFParser::String(zipped.toBase64());
|
||||
parser.addObject(ebx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parser.write(path, true);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void DRMProcessor::buildSignInRequest(pugi::xml_document& signInRequest,
|
||||
|
|
|
@ -5,7 +5,7 @@ CXXFLAGS=-Wall `pkg-config --cflags Qt5Core Qt5Network` -fPIC -I$(ROOT)/include
|
|||
ifneq ($(STATIC_UTILS),)
|
||||
LDFLAGS=`pkg-config --libs Qt5Core Qt5Network` -L$(ROOT) $(ROOT)/libgourou.a -lcrypto -lzip
|
||||
else
|
||||
LDFLAGS=`pkg-config --libs Qt5Core Qt5Network` -L$(ROOT) -lgourou -lcrypto -lzip
|
||||
LDFLAGS=`pkg-config --libs Qt5Core Qt5Network` -L$(ROOT) -lgourou -lcrypto -lzip -lz
|
||||
endif
|
||||
|
||||
ifneq ($(DEBUG),)
|
||||
|
|
|
@ -79,9 +79,7 @@ public:
|
|||
{
|
||||
filename = item->getMetadata("title");
|
||||
if (filename == "")
|
||||
filename = "output.epub";
|
||||
else
|
||||
filename += ".epub";
|
||||
filename = "output";
|
||||
}
|
||||
else
|
||||
filename = outputFile;
|
||||
|
@ -95,7 +93,19 @@ public:
|
|||
filename = std::string(outputDir) + "/" + filename;
|
||||
}
|
||||
|
||||
processor.download(item, filename);
|
||||
gourou::DRMProcessor::ITEM_TYPE type = processor.download(item, filename);
|
||||
|
||||
if (!outputFile)
|
||||
{
|
||||
std::string finalName = filename;
|
||||
if (type == gourou::DRMProcessor::ITEM_TYPE::PDF)
|
||||
finalName += ".pdf";
|
||||
else
|
||||
finalName += ".epub";
|
||||
QDir dir;
|
||||
dir.rename(filename.c_str(), finalName.c_str());
|
||||
filename = finalName;
|
||||
}
|
||||
std::cout << "Created " << filename << std::endl;
|
||||
} catch(std::exception& e)
|
||||
{
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include <QFile>
|
||||
|
||||
#include <zip.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#include <libgourou_common.h>
|
||||
#include <libgourou_log.h>
|
||||
|
@ -82,7 +83,7 @@ void DRMProcessorClientImpl::randBytes(unsigned char* bytesOut, unsigned int len
|
|||
}
|
||||
|
||||
/* HTTP interface */
|
||||
std::string DRMProcessorClientImpl::sendHTTPRequest(const std::string& URL, const std::string& POSTData, const std::string& contentType)
|
||||
std::string DRMProcessorClientImpl::sendHTTPRequest(const std::string& URL, const std::string& POSTData, const std::string& contentType, std::map<std::string, std::string>* responseHeaders)
|
||||
{
|
||||
QNetworkRequest request(QUrl(URL.c_str()));
|
||||
QNetworkAccessManager networkManager;
|
||||
|
@ -121,12 +122,12 @@ std::string DRMProcessorClientImpl::sendHTTPRequest(const std::string& URL, cons
|
|||
if (reply->error() != QNetworkReply::NoError)
|
||||
EXCEPTION(gourou::CLIENT_NETWORK_ERROR, "Error " << reply->errorString().toStdString());
|
||||
|
||||
if (gourou::logLevel >= gourou::DEBUG)
|
||||
{
|
||||
QList<QByteArray> headers = reply->rawHeaderList();
|
||||
for (int i = 0; i < headers.size(); ++i) {
|
||||
QList<QByteArray> headers = reply->rawHeaderList();
|
||||
for (int i = 0; i < headers.size(); ++i) {
|
||||
if (gourou::logLevel >= gourou::DEBUG)
|
||||
std::cout << headers[i].constData() << " : " << reply->rawHeader(headers[i]).constData() << std::endl;
|
||||
}
|
||||
if (responseHeaders)
|
||||
(*responseHeaders)[headers[i].constData()] = reply->rawHeader(headers[i]).constData();
|
||||
}
|
||||
|
||||
replyData = reply->readAll();
|
||||
|
@ -420,3 +421,78 @@ void DRMProcessorClientImpl::zipClose(void* handler)
|
|||
{
|
||||
zip_close((zip_t*)handler);
|
||||
}
|
||||
|
||||
void DRMProcessorClientImpl::inflate(std::string data, gourou::ByteArray& result,
|
||||
int wbits)
|
||||
{
|
||||
unsigned int dataSize = data.size()*2;
|
||||
unsigned char* buffer = new unsigned char[dataSize];
|
||||
|
||||
z_stream infstream;
|
||||
|
||||
infstream.zalloc = Z_NULL;
|
||||
infstream.zfree = Z_NULL;
|
||||
infstream.opaque = Z_NULL;
|
||||
|
||||
infstream.avail_in = (uInt)data.size();
|
||||
infstream.next_in = (Bytef *)data.c_str(); // input char array
|
||||
infstream.avail_out = (uInt)dataSize; // size of output
|
||||
infstream.next_out = (Bytef *)buffer; // output char array
|
||||
|
||||
int ret = inflateInit2(&infstream, wbits);
|
||||
|
||||
ret = ::inflate(&infstream, Z_SYNC_FLUSH);
|
||||
while (ret == Z_OK || ret == Z_STREAM_END)
|
||||
{
|
||||
result.append(buffer, dataSize-infstream.avail_out);
|
||||
if (ret == Z_STREAM_END) break;
|
||||
infstream.avail_out = (uInt)dataSize; // size of output
|
||||
infstream.next_out = (Bytef *)buffer; // output char array
|
||||
ret = ::inflate(&infstream, Z_SYNC_FLUSH);
|
||||
}
|
||||
|
||||
inflateEnd(&infstream);
|
||||
|
||||
delete[] buffer;
|
||||
|
||||
if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR)
|
||||
EXCEPTION(gourou::CLIENT_ZIP_ERROR, zError(ret));
|
||||
}
|
||||
|
||||
void DRMProcessorClientImpl::deflate(std::string data, gourou::ByteArray& result,
|
||||
int wbits, int compressionLevel)
|
||||
{
|
||||
unsigned int dataSize = data.size();
|
||||
unsigned char* buffer = new unsigned char[dataSize];
|
||||
|
||||
z_stream defstream;
|
||||
|
||||
defstream.zalloc = Z_NULL;
|
||||
defstream.zfree = Z_NULL;
|
||||
defstream.opaque = Z_NULL;
|
||||
|
||||
defstream.avail_in = (uInt)data.size();
|
||||
defstream.next_in = (Bytef *)data.c_str(); // input char array
|
||||
defstream.avail_out = (uInt)dataSize; // size of output
|
||||
defstream.next_out = (Bytef *)buffer; // output char array
|
||||
|
||||
int ret = deflateInit2(&defstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, wbits,
|
||||
compressionLevel, Z_DEFAULT_STRATEGY);
|
||||
|
||||
ret = ::deflate(&defstream, Z_SYNC_FLUSH);
|
||||
while (ret == Z_OK || ret == Z_STREAM_END)
|
||||
{
|
||||
result.append(buffer, dataSize-defstream.avail_out);
|
||||
if (ret == Z_STREAM_END) break;
|
||||
defstream.avail_out = (uInt)dataSize; // size of output
|
||||
defstream.next_out = (Bytef *)buffer; // output char array
|
||||
ret = ::deflate(&defstream, Z_SYNC_FLUSH);
|
||||
}
|
||||
|
||||
deflateEnd(&defstream);
|
||||
|
||||
delete[] buffer;
|
||||
|
||||
if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR)
|
||||
EXCEPTION(gourou::CLIENT_ZIP_ERROR, zError(ret));
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class DRMProcessorClientImpl : public gourou::DRMProcessorClient
|
|||
virtual void randBytes(unsigned char* bytesOut, unsigned int length);
|
||||
|
||||
/* HTTP interface */
|
||||
virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string(""));
|
||||
virtual std::string sendHTTPRequest(const std::string& URL, const std::string& POSTData=std::string(""), const std::string& contentType=std::string(""), std::map<std::string, std::string>* responseHeaders=0);
|
||||
|
||||
virtual void RSAPrivateEncrypt(const unsigned char* RSAKey, unsigned int RSAKeyLength,
|
||||
const RSA_KEY_TYPE keyType, const std::string& password,
|
||||
|
@ -108,6 +108,10 @@ class DRMProcessorClientImpl : public gourou::DRMProcessorClient
|
|||
|
||||
virtual void zipClose(void* handler);
|
||||
|
||||
virtual void inflate(std::string data, gourou::ByteArray& result, int wbits=-15);
|
||||
|
||||
virtual void deflate(std::string data, gourou::ByteArray& result,
|
||||
int wbits=-15, int compressionLevel=8);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue
Block a user