qpdf-compress 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/binding.gyp +23 -9
- package/package.json +3 -2
- package/scripts/download-harfbuzz.mjs +166 -0
- package/scripts/download-mozjpeg.mjs +1 -0
- package/scripts/download-qpdf.mjs +8 -1
- package/scripts/install.mjs +1 -0
- package/src/content.cc +291 -0
- package/src/content.h +6 -0
- package/src/encoding_tables.h +42 -0
- package/src/font_subset.cc +128 -425
- package/src/font_subset.h +17 -7
- package/src/fonts.cc +1209 -0
- package/src/fonts.h +5 -0
- package/src/hash_utils.h +15 -0
- package/src/images.cc +156 -132
- package/src/images.h +20 -2
- package/src/jpeg.cc +6 -0
- package/src/jpeg.h +1 -0
- package/src/optimize.h +5 -13
- package/src/qpdf_addon.cc +137 -55
- package/src/strip.cc +391 -0
- package/src/strip.h +9 -0
- package/src/structure.cc +346 -0
- package/src/structure.h +7 -0
- package/src/optimize.cc +0 -1048
package/src/fonts.h
ADDED
package/src/hash_utils.h
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// FNV-1a hash utility — shared between image deduplication and stream
|
|
2
|
+
// deduplication
|
|
3
|
+
#pragma once
|
|
4
|
+
|
|
5
|
+
#include <cstddef>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
|
|
8
|
+
inline uint64_t fnv1aHash(const uint8_t *data, size_t size) {
|
|
9
|
+
uint64_t hash = 14695981039346656037ULL;
|
|
10
|
+
for (size_t i = 0; i < size; ++i) {
|
|
11
|
+
hash ^= static_cast<uint64_t>(data[i]);
|
|
12
|
+
hash *= 1099511628211ULL;
|
|
13
|
+
}
|
|
14
|
+
return hash;
|
|
15
|
+
}
|
package/src/images.cc
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
#include "images.h"
|
|
2
|
+
#include "hash_utils.h"
|
|
2
3
|
#include "jpeg.h"
|
|
3
4
|
|
|
4
5
|
#include <algorithm>
|
|
5
6
|
#include <cmath>
|
|
6
7
|
#include <cstdint>
|
|
8
|
+
#include <cstdlib>
|
|
7
9
|
#include <cstring>
|
|
10
|
+
|
|
11
|
+
#ifdef __APPLE__
|
|
12
|
+
#include <xlocale.h>
|
|
13
|
+
#elif defined(_WIN32)
|
|
14
|
+
#include <locale.h>
|
|
15
|
+
#else
|
|
16
|
+
#include <locale.h>
|
|
17
|
+
#endif
|
|
8
18
|
#include <limits>
|
|
9
19
|
#include <map>
|
|
10
20
|
#include <set>
|
|
@@ -12,6 +22,17 @@
|
|
|
12
22
|
|
|
13
23
|
#include <qpdf/Buffer.hh>
|
|
14
24
|
|
|
25
|
+
// locale-independent double parser for PDF content stream operands
|
|
26
|
+
static double parsePdfDouble(const std::string &s) {
|
|
27
|
+
#ifdef _WIN32
|
|
28
|
+
static _locale_t c_locale = _create_locale(LC_NUMERIC, "C");
|
|
29
|
+
return _strtod_l(s.c_str(), nullptr, c_locale);
|
|
30
|
+
#else
|
|
31
|
+
static locale_t c_locale = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0);
|
|
32
|
+
return strtod_l(s.c_str(), nullptr, c_locale);
|
|
33
|
+
#endif
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
// ---------------------------------------------------------------------------
|
|
16
37
|
// Color space helpers
|
|
17
38
|
// ---------------------------------------------------------------------------
|
|
@@ -223,12 +244,12 @@ getImageRenderedSizes(QPDFPageObjectHelper &page) {
|
|
|
223
244
|
// cm operator: a b c d e f cm
|
|
224
245
|
try {
|
|
225
246
|
Matrix m;
|
|
226
|
-
m.a =
|
|
227
|
-
m.b =
|
|
228
|
-
m.c =
|
|
229
|
-
m.d =
|
|
230
|
-
m.e =
|
|
231
|
-
m.f =
|
|
247
|
+
m.a = parsePdfDouble(operandStack[operandStack.size() - 6]);
|
|
248
|
+
m.b = parsePdfDouble(operandStack[operandStack.size() - 5]);
|
|
249
|
+
m.c = parsePdfDouble(operandStack[operandStack.size() - 4]);
|
|
250
|
+
m.d = parsePdfDouble(operandStack[operandStack.size() - 3]);
|
|
251
|
+
m.e = parsePdfDouble(operandStack[operandStack.size() - 2]);
|
|
252
|
+
m.f = parsePdfDouble(operandStack[operandStack.size() - 1]);
|
|
232
253
|
ctm = multiply(m, ctm);
|
|
233
254
|
} catch (...) {
|
|
234
255
|
}
|
|
@@ -403,10 +424,7 @@ void optimizeImages(QPDF &qpdf, const CompressOptions &opts) {
|
|
|
403
424
|
return;
|
|
404
425
|
|
|
405
426
|
// replace stream data with JPEG
|
|
406
|
-
|
|
407
|
-
jpegData.size());
|
|
408
|
-
xobj.replaceStreamData(jpegStr, QPDFObjectHandle::newName("/DCTDecode"),
|
|
409
|
-
QPDFObjectHandle::newNull());
|
|
427
|
+
replaceWithJpeg(xobj, jpegData);
|
|
410
428
|
|
|
411
429
|
// update color space to DeviceRGB if converted from CMYK/ICCBased
|
|
412
430
|
if (isCMYK || !dict.getKey("/ColorSpace").isName() ||
|
|
@@ -453,13 +471,7 @@ void deduplicateImages(QPDF &qpdf) {
|
|
|
453
471
|
auto rawData = xobj.getRawStreamData();
|
|
454
472
|
size_t size = rawData->getSize();
|
|
455
473
|
|
|
456
|
-
|
|
457
|
-
uint64_t hash = 14695981039346656037ULL;
|
|
458
|
-
auto *p = rawData->getBuffer();
|
|
459
|
-
for (size_t i = 0; i < size; ++i) {
|
|
460
|
-
hash ^= static_cast<uint64_t>(p[i]);
|
|
461
|
-
hash *= 1099511628211ULL;
|
|
462
|
-
}
|
|
474
|
+
uint64_t hash = fnv1aHash(rawData->getBuffer(), size);
|
|
463
475
|
|
|
464
476
|
hashGroups[hash].push_back({og, size, xobj});
|
|
465
477
|
} catch (...) {
|
|
@@ -536,10 +548,7 @@ void optimizeExistingJpegs(QPDF &qpdf) {
|
|
|
536
548
|
if (optimized.size() >= rawData->getSize())
|
|
537
549
|
return;
|
|
538
550
|
|
|
539
|
-
|
|
540
|
-
optimized.size());
|
|
541
|
-
xobj.replaceStreamData(jpegStr, QPDFObjectHandle::newName("/DCTDecode"),
|
|
542
|
-
QPDFObjectHandle::newNull());
|
|
551
|
+
replaceWithJpeg(xobj, optimized);
|
|
543
552
|
} catch (...) {
|
|
544
553
|
}
|
|
545
554
|
});
|
|
@@ -681,10 +690,7 @@ void downscaleImages(QPDF &qpdf, int maxDpi, int quality) {
|
|
|
681
690
|
if (jpegData.size() >= rawData->getSize())
|
|
682
691
|
return;
|
|
683
692
|
|
|
684
|
-
|
|
685
|
-
jpegData.size());
|
|
686
|
-
xobj.replaceStreamData(jpegStr, QPDFObjectHandle::newName("/DCTDecode"),
|
|
687
|
-
QPDFObjectHandle::newNull());
|
|
693
|
+
replaceWithJpeg(xobj, jpegData);
|
|
688
694
|
|
|
689
695
|
dict.replaceKey("/Width", QPDFObjectHandle::newInteger(newW));
|
|
690
696
|
dict.replaceKey("/Height", QPDFObjectHandle::newInteger(newH));
|
|
@@ -702,11 +708,13 @@ void downscaleImages(QPDF &qpdf, int maxDpi, int quality) {
|
|
|
702
708
|
}
|
|
703
709
|
|
|
704
710
|
// ---------------------------------------------------------------------------
|
|
705
|
-
//
|
|
706
|
-
//
|
|
711
|
+
// Color space optimization — single pass over all images:
|
|
712
|
+
// 1. Replace ICCBased color spaces with Device equivalents
|
|
713
|
+
// 2. Convert RGB images that are actually grayscale to DeviceGray
|
|
714
|
+
// 3. Convert 8-bit grayscale images that are black & white to 1-bit
|
|
707
715
|
// ---------------------------------------------------------------------------
|
|
708
716
|
|
|
709
|
-
void
|
|
717
|
+
void optimizeColorSpaces(QPDF &qpdf) {
|
|
710
718
|
std::set<QPDFObjGen> processed;
|
|
711
719
|
|
|
712
720
|
forEachImage(qpdf, [&](const std::string & /*key*/, QPDFObjectHandle xobj,
|
|
@@ -719,20 +727,37 @@ void convertGrayscaleImages(QPDF &qpdf) {
|
|
|
719
727
|
|
|
720
728
|
auto dict = xobj.getDict();
|
|
721
729
|
|
|
722
|
-
//
|
|
723
|
-
if (!dict.getKey("/BitsPerComponent").isInteger() ||
|
|
724
|
-
dict.getKey("/BitsPerComponent").getIntValue() != 8)
|
|
725
|
-
return;
|
|
726
|
-
|
|
730
|
+
// --- phase 1: strip ICC profiles ---
|
|
727
731
|
auto cs = dict.getKey("/ColorSpace");
|
|
728
|
-
if (
|
|
732
|
+
if (cs.isArray() && cs.getArrayNItems() >= 2) {
|
|
733
|
+
auto csName = cs.getArrayItem(0);
|
|
734
|
+
if (csName.isName() && csName.getName() == "/ICCBased") {
|
|
735
|
+
auto profile = cs.getArrayItem(1);
|
|
736
|
+
if (profile.isStream()) {
|
|
737
|
+
auto n = profile.getDict().getKey("/N");
|
|
738
|
+
if (n.isInteger()) {
|
|
739
|
+
int components = static_cast<int>(n.getIntValue());
|
|
740
|
+
if (components == 3)
|
|
741
|
+
dict.replaceKey("/ColorSpace",
|
|
742
|
+
QPDFObjectHandle::newName("/DeviceRGB"));
|
|
743
|
+
else if (components == 1)
|
|
744
|
+
dict.replaceKey("/ColorSpace",
|
|
745
|
+
QPDFObjectHandle::newName("/DeviceGray"));
|
|
746
|
+
else if (components == 4)
|
|
747
|
+
dict.replaceKey("/ColorSpace",
|
|
748
|
+
QPDFObjectHandle::newName("/DeviceCMYK"));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// re-read after potential ICC replacement
|
|
755
|
+
cs = dict.getKey("/ColorSpace");
|
|
756
|
+
if (!cs.isName())
|
|
729
757
|
return;
|
|
730
758
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// re-encodes, but in lossless mode nothing would, causing size inflation.
|
|
734
|
-
auto filter = dict.getKey("/Filter");
|
|
735
|
-
if (filter.isName() && filter.getName() == "/DCTDecode")
|
|
759
|
+
if (!dict.getKey("/BitsPerComponent").isInteger() ||
|
|
760
|
+
dict.getKey("/BitsPerComponent").getIntValue() != 8)
|
|
736
761
|
return;
|
|
737
762
|
|
|
738
763
|
int width = 0, height = 0;
|
|
@@ -743,114 +768,86 @@ void convertGrayscaleImages(QPDF &qpdf) {
|
|
|
743
768
|
if (width <= 0 || height <= 0)
|
|
744
769
|
return;
|
|
745
770
|
|
|
746
|
-
// decode pixels
|
|
747
|
-
std::shared_ptr<Buffer> streamData;
|
|
748
|
-
try {
|
|
749
|
-
streamData = xobj.getStreamData(qpdf_dl_all);
|
|
750
|
-
} catch (...) {
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
771
|
auto w = static_cast<size_t>(width);
|
|
755
772
|
auto h = static_cast<size_t>(height);
|
|
756
|
-
size_t expectedSize = w * h * 3;
|
|
757
|
-
if (streamData->getSize() != expectedSize)
|
|
758
|
-
return;
|
|
759
|
-
|
|
760
|
-
const auto *pixels = streamData->getBuffer();
|
|
761
|
-
size_t pixelCount = w * h;
|
|
762
773
|
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
auto
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
774
|
+
// --- phase 2: RGB → grayscale detection ---
|
|
775
|
+
if (cs.getName() == "/DeviceRGB") {
|
|
776
|
+
// skip JPEG-compressed images — converting to raw gray + Flate would be
|
|
777
|
+
// larger than the original JPEG
|
|
778
|
+
auto filter = dict.getKey("/Filter");
|
|
779
|
+
if (filter.isName() && filter.getName() == "/DCTDecode")
|
|
780
|
+
return;
|
|
781
|
+
if (filter.isArray()) {
|
|
782
|
+
for (int i = 0; i < filter.getArrayNItems(); ++i) {
|
|
783
|
+
auto f = filter.getArrayItem(i);
|
|
784
|
+
if (f.isName() && f.getName() == "/DCTDecode")
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
772
787
|
}
|
|
773
|
-
}
|
|
774
788
|
|
|
775
|
-
|
|
776
|
-
|
|
789
|
+
std::shared_ptr<Buffer> streamData;
|
|
790
|
+
try {
|
|
791
|
+
streamData = xobj.getStreamData(qpdf_dl_all);
|
|
792
|
+
} catch (...) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
777
795
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
for (size_t i = 0; i < pixelCount; ++i)
|
|
781
|
-
grayPixels[i] = pixels[i * 3];
|
|
796
|
+
if (streamData->getSize() != w * h * 3)
|
|
797
|
+
return;
|
|
782
798
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
grayPixels.size());
|
|
786
|
-
xobj.replaceStreamData(grayStr, QPDFObjectHandle::newNull(),
|
|
787
|
-
QPDFObjectHandle::newNull());
|
|
788
|
-
dict.replaceKey("/ColorSpace", QPDFObjectHandle::newName("/DeviceGray"));
|
|
799
|
+
const auto *pixels = streamData->getBuffer();
|
|
800
|
+
size_t pixelCount = w * h;
|
|
789
801
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
802
|
+
bool isGray = true;
|
|
803
|
+
for (size_t i = 0; i < pixelCount; ++i) {
|
|
804
|
+
if (pixels[i * 3] != pixels[i * 3 + 1] ||
|
|
805
|
+
pixels[i * 3 + 1] != pixels[i * 3 + 2]) {
|
|
806
|
+
isGray = false;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
797
810
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
// black & white (all pixels near 0 or 255) to 1-bit, saving ~8x raw data
|
|
801
|
-
// ---------------------------------------------------------------------------
|
|
811
|
+
if (!isGray)
|
|
812
|
+
return;
|
|
802
813
|
|
|
803
|
-
|
|
804
|
-
|
|
814
|
+
std::vector<uint8_t> grayPixels(pixelCount);
|
|
815
|
+
for (size_t i = 0; i < pixelCount; ++i)
|
|
816
|
+
grayPixels[i] = pixels[i * 3];
|
|
805
817
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
QPDFPageObjectHelper & /*page*/) {
|
|
809
|
-
auto og = xobj.getObjGen();
|
|
810
|
-
if (processed.count(og))
|
|
811
|
-
return;
|
|
812
|
-
processed.insert(og);
|
|
818
|
+
replaceWithRaw(xobj, grayPixels);
|
|
819
|
+
dict.replaceKey("/ColorSpace", QPDFObjectHandle::newName("/DeviceGray"));
|
|
813
820
|
|
|
814
|
-
|
|
821
|
+
if (dict.hasKey("/DecodeParms"))
|
|
822
|
+
dict.removeKey("/DecodeParms");
|
|
823
|
+
if (dict.hasKey("/Predictor"))
|
|
824
|
+
dict.removeKey("/Predictor");
|
|
815
825
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return;
|
|
826
|
+
// fall through to bitonal check on the now-gray image
|
|
827
|
+
cs = dict.getKey("/ColorSpace");
|
|
828
|
+
}
|
|
820
829
|
|
|
821
|
-
|
|
822
|
-
if (
|
|
830
|
+
// --- phase 3: grayscale → bitonal conversion ---
|
|
831
|
+
if (cs.getName() != "/DeviceGray")
|
|
823
832
|
return;
|
|
824
833
|
|
|
825
|
-
// skip images with masks
|
|
834
|
+
// skip images with masks
|
|
826
835
|
if (dict.hasKey("/SMask") || dict.hasKey("/Mask"))
|
|
827
836
|
return;
|
|
828
837
|
|
|
829
|
-
|
|
830
|
-
if (dict.getKey("/Width").isInteger())
|
|
831
|
-
width = static_cast<int>(dict.getKey("/Width").getIntValue());
|
|
832
|
-
if (dict.getKey("/Height").isInteger())
|
|
833
|
-
height = static_cast<int>(dict.getKey("/Height").getIntValue());
|
|
834
|
-
if (width <= 0 || height <= 0)
|
|
835
|
-
return;
|
|
836
|
-
|
|
837
|
-
// decode pixels
|
|
838
|
-
std::shared_ptr<Buffer> streamData;
|
|
838
|
+
std::shared_ptr<Buffer> grayData;
|
|
839
839
|
try {
|
|
840
|
-
|
|
840
|
+
grayData = xobj.getStreamData(qpdf_dl_all);
|
|
841
841
|
} catch (...) {
|
|
842
842
|
return;
|
|
843
843
|
}
|
|
844
844
|
|
|
845
|
-
|
|
846
|
-
auto h = static_cast<size_t>(height);
|
|
847
|
-
if (streamData->getSize() != w * h)
|
|
845
|
+
if (grayData->getSize() != w * h)
|
|
848
846
|
return;
|
|
849
847
|
|
|
850
|
-
const auto *pixels =
|
|
848
|
+
const auto *pixels = grayData->getBuffer();
|
|
851
849
|
size_t pixelCount = w * h;
|
|
852
850
|
|
|
853
|
-
// check if all pixels are near black (<=32) or near white (>=224)
|
|
854
851
|
bool isBitonal = true;
|
|
855
852
|
for (size_t i = 0; i < pixelCount; ++i) {
|
|
856
853
|
if (pixels[i] > 32 && pixels[i] < 224) {
|
|
@@ -862,29 +859,22 @@ void convertBitonalImages(QPDF &qpdf) {
|
|
|
862
859
|
if (!isBitonal)
|
|
863
860
|
return;
|
|
864
861
|
|
|
865
|
-
// pack into 1-bit: 0 = black, 1 = white
|
|
866
|
-
// each row padded to byte boundary
|
|
867
862
|
size_t rowBytes = (w + 7) / 8;
|
|
868
863
|
std::vector<uint8_t> bitonalData(rowBytes * h, 0);
|
|
869
864
|
|
|
870
865
|
for (size_t y = 0; y < h; ++y) {
|
|
871
866
|
for (size_t x = 0; x < w; ++x) {
|
|
872
|
-
|
|
873
|
-
if (white)
|
|
867
|
+
if (pixels[y * w + x] >= 224)
|
|
874
868
|
bitonalData[y * rowBytes + x / 8] |=
|
|
875
869
|
static_cast<uint8_t>(0x80 >> (x % 8));
|
|
876
870
|
}
|
|
877
871
|
}
|
|
878
872
|
|
|
879
|
-
// only replace if 1-bit data is smaller than original raw stream
|
|
880
873
|
auto rawData = xobj.getRawStreamData();
|
|
881
874
|
if (bitonalData.size() >= rawData->getSize())
|
|
882
875
|
return;
|
|
883
876
|
|
|
884
|
-
|
|
885
|
-
bitonalData.size());
|
|
886
|
-
xobj.replaceStreamData(bitStr, QPDFObjectHandle::newNull(),
|
|
887
|
-
QPDFObjectHandle::newNull());
|
|
877
|
+
replaceWithRaw(xobj, bitonalData);
|
|
888
878
|
dict.replaceKey("/BitsPerComponent", QPDFObjectHandle::newInteger(1));
|
|
889
879
|
|
|
890
880
|
if (dict.hasKey("/DecodeParms"))
|
|
@@ -892,6 +882,43 @@ void convertBitonalImages(QPDF &qpdf) {
|
|
|
892
882
|
if (dict.hasKey("/Predictor"))
|
|
893
883
|
dict.removeKey("/Predictor");
|
|
894
884
|
});
|
|
885
|
+
|
|
886
|
+
// strip ICC profiles from page-level color space resources
|
|
887
|
+
for (auto &page : QPDFPageDocumentHelper(qpdf).getAllPages()) {
|
|
888
|
+
auto resources = page.getObjectHandle().getKey("/Resources");
|
|
889
|
+
if (!resources.isDictionary())
|
|
890
|
+
continue;
|
|
891
|
+
|
|
892
|
+
auto colorSpaces = resources.getKey("/ColorSpace");
|
|
893
|
+
if (!colorSpaces.isDictionary())
|
|
894
|
+
continue;
|
|
895
|
+
|
|
896
|
+
for (auto &key : colorSpaces.getKeys()) {
|
|
897
|
+
auto cs = colorSpaces.getKey(key);
|
|
898
|
+
if (!cs.isArray() || cs.getArrayNItems() < 2)
|
|
899
|
+
continue;
|
|
900
|
+
|
|
901
|
+
auto csName = cs.getArrayItem(0);
|
|
902
|
+
if (!csName.isName() || csName.getName() != "/ICCBased")
|
|
903
|
+
continue;
|
|
904
|
+
|
|
905
|
+
auto profile = cs.getArrayItem(1);
|
|
906
|
+
if (!profile.isStream())
|
|
907
|
+
continue;
|
|
908
|
+
|
|
909
|
+
auto n = profile.getDict().getKey("/N");
|
|
910
|
+
if (!n.isInteger())
|
|
911
|
+
continue;
|
|
912
|
+
|
|
913
|
+
int components = static_cast<int>(n.getIntValue());
|
|
914
|
+
if (components == 3)
|
|
915
|
+
colorSpaces.replaceKey(key, QPDFObjectHandle::newName("/DeviceRGB"));
|
|
916
|
+
else if (components == 1)
|
|
917
|
+
colorSpaces.replaceKey(key, QPDFObjectHandle::newName("/DeviceGray"));
|
|
918
|
+
else if (components == 4)
|
|
919
|
+
colorSpaces.replaceKey(key, QPDFObjectHandle::newName("/DeviceCMYK"));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
895
922
|
}
|
|
896
923
|
|
|
897
924
|
// ---------------------------------------------------------------------------
|
|
@@ -933,10 +960,7 @@ void optimizeSoftMasks(QPDF &qpdf) {
|
|
|
933
960
|
if (optimized.size() >= rawData->getSize())
|
|
934
961
|
return;
|
|
935
962
|
|
|
936
|
-
|
|
937
|
-
optimized.size());
|
|
938
|
-
smask.replaceStreamData(jpegStr, QPDFObjectHandle::newName("/DCTDecode"),
|
|
939
|
-
QPDFObjectHandle::newNull());
|
|
963
|
+
replaceWithJpeg(smask, optimized);
|
|
940
964
|
} catch (...) {
|
|
941
965
|
}
|
|
942
966
|
});
|
package/src/images.h
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
3
7
|
#include <qpdf/QPDF.hh>
|
|
4
8
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
5
9
|
#include <qpdf/QPDFPageDocumentHelper.hh>
|
|
@@ -28,6 +32,21 @@ template <typename Fn> void forEachImage(QPDF &qpdf, Fn &&fn) {
|
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// replace an image XObject's stream data with JPEG-encoded bytes
|
|
36
|
+
inline void replaceWithJpeg(QPDFObjectHandle xobj, std::vector<uint8_t> &data) {
|
|
37
|
+
std::string str(reinterpret_cast<char *>(data.data()), data.size());
|
|
38
|
+
xobj.replaceStreamData(str, QPDFObjectHandle::newName("/DCTDecode"),
|
|
39
|
+
QPDFObjectHandle::newNull());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// replace an image XObject's stream data with raw bytes (Flate-compressed by
|
|
43
|
+
// QPDFWriter)
|
|
44
|
+
inline void replaceWithRaw(QPDFObjectHandle xobj, std::vector<uint8_t> &data) {
|
|
45
|
+
std::string str(reinterpret_cast<char *>(data.data()), data.size());
|
|
46
|
+
xobj.replaceStreamData(str, QPDFObjectHandle::newNull(),
|
|
47
|
+
QPDFObjectHandle::newNull());
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
struct CompressOptions {
|
|
32
51
|
int skipThreshold = 0; // skip existing JPEGs at or below this quality
|
|
33
52
|
int targetQuality = 0; // encode/re-encode at this quality
|
|
@@ -37,6 +56,5 @@ void optimizeImages(QPDF &qpdf, const CompressOptions &opts);
|
|
|
37
56
|
void downscaleImages(QPDF &qpdf, int maxDpi, int quality);
|
|
38
57
|
void deduplicateImages(QPDF &qpdf);
|
|
39
58
|
void optimizeExistingJpegs(QPDF &qpdf);
|
|
40
|
-
void
|
|
41
|
-
void convertBitonalImages(QPDF &qpdf);
|
|
59
|
+
void optimizeColorSpaces(QPDF &qpdf);
|
|
42
60
|
void optimizeSoftMasks(QPDF &qpdf);
|
package/src/jpeg.cc
CHANGED
|
@@ -8,6 +8,9 @@ void jpegErrorExit(j_common_ptr cinfo) {
|
|
|
8
8
|
std::longjmp(myerr->jmpbuf, 1);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// suppress non-fatal warnings (e.g. "Corrupt JPEG data: N extraneous bytes")
|
|
12
|
+
void jpegSilenceOutput(j_common_ptr) {}
|
|
13
|
+
|
|
11
14
|
// ---------------------------------------------------------------------------
|
|
12
15
|
// Lossless JPEG optimization
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
@@ -22,6 +25,7 @@ static bool losslessJpegOptimizeImpl(const unsigned char *data, size_t size,
|
|
|
22
25
|
|
|
23
26
|
srcinfo.err = jpeg_std_error(&jerr.pub);
|
|
24
27
|
jerr.pub.error_exit = jpegErrorExit;
|
|
28
|
+
jerr.pub.output_message = jpegSilenceOutput;
|
|
25
29
|
dstinfo.err = &jerr.pub;
|
|
26
30
|
|
|
27
31
|
if (setjmp(jerr.jmpbuf)) {
|
|
@@ -95,6 +99,7 @@ static bool encodeJpegImpl(const unsigned char *pixels, int width, int height,
|
|
|
95
99
|
|
|
96
100
|
cinfo.err = jpeg_std_error(&jerr.pub);
|
|
97
101
|
jerr.pub.error_exit = jpegErrorExit;
|
|
102
|
+
jerr.pub.output_message = jpegSilenceOutput;
|
|
98
103
|
|
|
99
104
|
if (setjmp(jerr.jmpbuf)) {
|
|
100
105
|
jpeg_destroy_compress(&cinfo);
|
|
@@ -176,6 +181,7 @@ static int estimateJpegQualityImpl(const unsigned char *data, size_t size) {
|
|
|
176
181
|
|
|
177
182
|
cinfo.err = jpeg_std_error(&jerr.pub);
|
|
178
183
|
jerr.pub.error_exit = jpegErrorExit;
|
|
184
|
+
jerr.pub.output_message = jpegSilenceOutput;
|
|
179
185
|
|
|
180
186
|
if (setjmp(jerr.jmpbuf)) {
|
|
181
187
|
jpeg_destroy_decompress(&cinfo);
|
package/src/jpeg.h
CHANGED
package/src/optimize.h
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
+
// umbrella include for all optimization passes
|
|
1
2
|
#pragma once
|
|
2
3
|
|
|
3
|
-
#include
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
void coalesceContentStreams(QPDF &qpdf);
|
|
8
|
-
void deduplicateStreams(QPDF &qpdf);
|
|
9
|
-
void subsetFonts(QPDF &qpdf);
|
|
10
|
-
void stripIccProfiles(QPDF &qpdf);
|
|
11
|
-
void stripEmbeddedFiles(QPDF &qpdf);
|
|
12
|
-
void stripJavaScript(QPDF &qpdf);
|
|
13
|
-
void flattenForms(QPDF &qpdf);
|
|
14
|
-
void flattenPageTree(QPDF &qpdf);
|
|
15
|
-
void minifyContentStreams(QPDF &qpdf);
|
|
4
|
+
#include "content.h"
|
|
5
|
+
#include "fonts.h"
|
|
6
|
+
#include "strip.h"
|
|
7
|
+
#include "structure.h"
|