docxmlater 10.1.3 → 10.1.5
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/README.md +759 -754
- package/dist/constants/legacyCompatFlags.js +1 -1
- package/dist/constants/legacyCompatFlags.js.map +1 -1
- package/dist/constants/limits.js.map +1 -1
- package/dist/core/Document.d.ts +50 -50
- package/dist/core/Document.d.ts.map +1 -1
- package/dist/core/Document.js +483 -471
- package/dist/core/Document.js.map +1 -1
- package/dist/core/DocumentContent.d.ts +9 -9
- package/dist/core/DocumentContent.d.ts.map +1 -1
- package/dist/core/DocumentContent.js +1 -1
- package/dist/core/DocumentContent.js.map +1 -1
- package/dist/core/DocumentGenerator.d.ts +11 -11
- package/dist/core/DocumentGenerator.d.ts.map +1 -1
- package/dist/core/DocumentGenerator.js +251 -251
- package/dist/core/DocumentGenerator.js.map +1 -1
- package/dist/core/DocumentIdManager.js.map +1 -1
- package/dist/core/DocumentParser.d.ts +15 -15
- package/dist/core/DocumentParser.d.ts.map +1 -1
- package/dist/core/DocumentParser.js +2123 -2155
- package/dist/core/DocumentParser.js.map +1 -1
- package/dist/core/DocumentValidator.d.ts.map +1 -1
- package/dist/core/DocumentValidator.js +2 -5
- package/dist/core/DocumentValidator.js.map +1 -1
- package/dist/core/Relationship.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +3 -3
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/elements/AlternateContent.js.map +1 -1
- package/dist/elements/Bookmark.d.ts.map +1 -1
- package/dist/elements/Bookmark.js +3 -1
- package/dist/elements/Bookmark.js.map +1 -1
- package/dist/elements/BookmarkManager.d.ts.map +1 -1
- package/dist/elements/BookmarkManager.js.map +1 -1
- package/dist/elements/Comment.d.ts.map +1 -1
- package/dist/elements/Comment.js +9 -6
- package/dist/elements/Comment.js.map +1 -1
- package/dist/elements/CommentManager.d.ts.map +1 -1
- package/dist/elements/CommentManager.js +18 -17
- package/dist/elements/CommentManager.js.map +1 -1
- package/dist/elements/CommonTypes.d.ts +21 -21
- package/dist/elements/CommonTypes.d.ts.map +1 -1
- package/dist/elements/CommonTypes.js +56 -56
- package/dist/elements/CommonTypes.js.map +1 -1
- package/dist/elements/CustomXml.js.map +1 -1
- package/dist/elements/Endnote.d.ts.map +1 -1
- package/dist/elements/Endnote.js +6 -6
- package/dist/elements/Endnote.js.map +1 -1
- package/dist/elements/EndnoteManager.d.ts.map +1 -1
- package/dist/elements/EndnoteManager.js +6 -7
- package/dist/elements/EndnoteManager.js.map +1 -1
- package/dist/elements/Field.d.ts.map +1 -1
- package/dist/elements/Field.js +82 -25
- package/dist/elements/Field.js.map +1 -1
- package/dist/elements/FieldHelpers.d.ts.map +1 -1
- package/dist/elements/FieldHelpers.js.map +1 -1
- package/dist/elements/FontManager.d.ts.map +1 -1
- package/dist/elements/FontManager.js +1 -1
- package/dist/elements/FontManager.js.map +1 -1
- package/dist/elements/Footer.js +2 -2
- package/dist/elements/Footer.js.map +1 -1
- package/dist/elements/Footnote.d.ts.map +1 -1
- package/dist/elements/Footnote.js +6 -6
- package/dist/elements/Footnote.js.map +1 -1
- package/dist/elements/FootnoteManager.d.ts.map +1 -1
- package/dist/elements/FootnoteManager.js +6 -7
- package/dist/elements/FootnoteManager.js.map +1 -1
- package/dist/elements/Header.js +2 -2
- package/dist/elements/Header.js.map +1 -1
- package/dist/elements/HeaderFooterManager.js.map +1 -1
- package/dist/elements/Hyperlink.d.ts +5 -3
- package/dist/elements/Hyperlink.d.ts.map +1 -1
- package/dist/elements/Hyperlink.js +134 -76
- package/dist/elements/Hyperlink.js.map +1 -1
- package/dist/elements/Image.d.ts.map +1 -1
- package/dist/elements/Image.js +238 -106
- package/dist/elements/Image.js.map +1 -1
- package/dist/elements/ImageManager.d.ts.map +1 -1
- package/dist/elements/ImageManager.js +1 -1
- package/dist/elements/ImageManager.js.map +1 -1
- package/dist/elements/ImageRun.js +1 -1
- package/dist/elements/ImageRun.js.map +1 -1
- package/dist/elements/MathElement.js.map +1 -1
- package/dist/elements/Paragraph.d.ts +24 -24
- package/dist/elements/Paragraph.d.ts.map +1 -1
- package/dist/elements/Paragraph.js +181 -188
- package/dist/elements/Paragraph.js.map +1 -1
- package/dist/elements/PreservedElement.js.map +1 -1
- package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
- package/dist/elements/PropertyChangeTypes.js +6 -6
- package/dist/elements/PropertyChangeTypes.js.map +1 -1
- package/dist/elements/RangeMarker.d.ts.map +1 -1
- package/dist/elements/RangeMarker.js.map +1 -1
- package/dist/elements/Revision.d.ts.map +1 -1
- package/dist/elements/Revision.js +4 -5
- package/dist/elements/Revision.js.map +1 -1
- package/dist/elements/RevisionContent.js.map +1 -1
- package/dist/elements/RevisionManager.d.ts.map +1 -1
- package/dist/elements/RevisionManager.js +40 -48
- package/dist/elements/RevisionManager.js.map +1 -1
- package/dist/elements/Run.d.ts +16 -16
- package/dist/elements/Run.d.ts.map +1 -1
- package/dist/elements/Run.js +256 -238
- package/dist/elements/Run.js.map +1 -1
- package/dist/elements/Section.d.ts.map +1 -1
- package/dist/elements/Section.js +36 -11
- package/dist/elements/Section.js.map +1 -1
- package/dist/elements/Shape.d.ts.map +1 -1
- package/dist/elements/Shape.js.map +1 -1
- package/dist/elements/StructuredDocumentTag.d.ts +6 -6
- package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
- package/dist/elements/StructuredDocumentTag.js +99 -104
- package/dist/elements/StructuredDocumentTag.js.map +1 -1
- package/dist/elements/Table.d.ts +11 -11
- package/dist/elements/Table.d.ts.map +1 -1
- package/dist/elements/Table.js +102 -107
- package/dist/elements/Table.js.map +1 -1
- package/dist/elements/TableCell.d.ts +10 -10
- package/dist/elements/TableCell.d.ts.map +1 -1
- package/dist/elements/TableCell.js +105 -106
- package/dist/elements/TableCell.js.map +1 -1
- package/dist/elements/TableGridChange.d.ts.map +1 -1
- package/dist/elements/TableGridChange.js.map +1 -1
- package/dist/elements/TableOfContents.d.ts.map +1 -1
- package/dist/elements/TableOfContents.js +4 -4
- package/dist/elements/TableOfContents.js.map +1 -1
- package/dist/elements/TableOfContentsElement.js.map +1 -1
- package/dist/elements/TableRow.d.ts.map +1 -1
- package/dist/elements/TableRow.js +13 -6
- package/dist/elements/TableRow.js.map +1 -1
- package/dist/elements/TextBox.d.ts.map +1 -1
- package/dist/elements/TextBox.js +3 -5
- package/dist/elements/TextBox.js.map +1 -1
- package/dist/formatting/AbstractNumbering.d.ts +4 -4
- package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
- package/dist/formatting/AbstractNumbering.js +54 -49
- package/dist/formatting/AbstractNumbering.js.map +1 -1
- package/dist/formatting/NumberingInstance.d.ts.map +1 -1
- package/dist/formatting/NumberingInstance.js +1 -3
- package/dist/formatting/NumberingInstance.js.map +1 -1
- package/dist/formatting/NumberingLevel.d.ts +5 -5
- package/dist/formatting/NumberingLevel.d.ts.map +1 -1
- package/dist/formatting/NumberingLevel.js +119 -125
- package/dist/formatting/NumberingLevel.js.map +1 -1
- package/dist/formatting/NumberingManager.d.ts.map +1 -1
- package/dist/formatting/NumberingManager.js +9 -9
- package/dist/formatting/NumberingManager.js.map +1 -1
- package/dist/formatting/Style.d.ts +11 -11
- package/dist/formatting/Style.d.ts.map +1 -1
- package/dist/formatting/Style.js +219 -247
- package/dist/formatting/Style.js.map +1 -1
- package/dist/formatting/StylesManager.d.ts +2 -2
- package/dist/formatting/StylesManager.d.ts.map +1 -1
- package/dist/formatting/StylesManager.js +96 -102
- package/dist/formatting/StylesManager.js.map +1 -1
- package/dist/helpers/CleanupHelper.d.ts +1 -1
- package/dist/helpers/CleanupHelper.d.ts.map +1 -1
- package/dist/helpers/CleanupHelper.js +6 -6
- package/dist/helpers/CleanupHelper.js.map +1 -1
- package/dist/images/ImageOptimizer.js +7 -7
- package/dist/images/ImageOptimizer.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/DrawingManager.js.map +1 -1
- package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
- package/dist/tracking/DocumentTrackingContext.js +23 -7
- package/dist/tracking/DocumentTrackingContext.js.map +1 -1
- package/dist/tracking/TrackingContext.d.ts.map +1 -1
- package/dist/tracking/TrackingContext.js.map +1 -1
- package/dist/types/compatibility-types.js.map +1 -1
- package/dist/types/formatting.js.map +1 -1
- package/dist/types/list-types.d.ts +6 -6
- package/dist/types/list-types.js.map +1 -1
- package/dist/types/settings-types.js.map +1 -1
- package/dist/types/styleConfig.d.ts +2 -2
- package/dist/types/styleConfig.js.map +1 -1
- package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
- package/dist/utils/ChangelogGenerator.js +97 -101
- package/dist/utils/ChangelogGenerator.js.map +1 -1
- package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
- package/dist/utils/CompatibilityUpgrader.js +1 -1
- package/dist/utils/CompatibilityUpgrader.js.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
- package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
- package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
- package/dist/utils/MoveOperationHelper.js +1 -1
- package/dist/utils/MoveOperationHelper.js.map +1 -1
- package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
- package/dist/utils/RevisionAwareProcessor.js +2 -4
- package/dist/utils/RevisionAwareProcessor.js.map +1 -1
- package/dist/utils/RevisionWalker.d.ts.map +1 -1
- package/dist/utils/RevisionWalker.js +4 -12
- package/dist/utils/RevisionWalker.js.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
- package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
- package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
- package/dist/utils/ShadingResolver.d.ts.map +1 -1
- package/dist/utils/ShadingResolver.js +1 -1
- package/dist/utils/ShadingResolver.js.map +1 -1
- package/dist/utils/acceptRevisions.d.ts.map +1 -1
- package/dist/utils/acceptRevisions.js +23 -12
- package/dist/utils/acceptRevisions.js.map +1 -1
- package/dist/utils/cnfStyleDecoder.d.ts +1 -1
- package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
- package/dist/utils/cnfStyleDecoder.js +40 -40
- package/dist/utils/cnfStyleDecoder.js.map +1 -1
- package/dist/utils/corruptionDetection.d.ts.map +1 -1
- package/dist/utils/corruptionDetection.js.map +1 -1
- package/dist/utils/dateFormatting.js.map +1 -1
- package/dist/utils/deepClone.js +1 -1
- package/dist/utils/deepClone.js.map +1 -1
- package/dist/utils/diagnostics.d.ts.map +1 -1
- package/dist/utils/diagnostics.js +1 -1
- package/dist/utils/diagnostics.js.map +1 -1
- package/dist/utils/errorHandling.js.map +1 -1
- package/dist/utils/formatting.d.ts.map +1 -1
- package/dist/utils/formatting.js +10 -2
- package/dist/utils/formatting.js.map +1 -1
- package/dist/utils/list-detection.d.ts +2 -2
- package/dist/utils/list-detection.d.ts.map +1 -1
- package/dist/utils/list-detection.js +21 -23
- package/dist/utils/list-detection.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +12 -7
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/parsingHelpers.js.map +1 -1
- package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
- package/dist/utils/stripTrackedChanges.js +3 -3
- package/dist/utils/stripTrackedChanges.js.map +1 -1
- package/dist/utils/textDiff.d.ts +1 -1
- package/dist/utils/textDiff.js +8 -8
- package/dist/utils/textDiff.js.map +1 -1
- package/dist/utils/units.js.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +24 -7
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/xmlSanitization.d.ts.map +1 -1
- package/dist/utils/xmlSanitization.js +3 -3
- package/dist/utils/xmlSanitization.js.map +1 -1
- package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
- package/dist/validation/RevisionAutoFixer.js +5 -5
- package/dist/validation/RevisionAutoFixer.js.map +1 -1
- package/dist/validation/RevisionValidator.d.ts.map +1 -1
- package/dist/validation/RevisionValidator.js +7 -9
- package/dist/validation/RevisionValidator.js.map +1 -1
- package/dist/validation/ValidationRules.js +3 -3
- package/dist/validation/ValidationRules.js.map +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/xml/XMLBuilder.d.ts +1 -1
- package/dist/xml/XMLBuilder.d.ts.map +1 -1
- package/dist/xml/XMLBuilder.js +98 -100
- package/dist/xml/XMLBuilder.js.map +1 -1
- package/dist/xml/XMLParser.d.ts.map +1 -1
- package/dist/xml/XMLParser.js +61 -66
- package/dist/xml/XMLParser.js.map +1 -1
- package/dist/zip/ZipHandler.d.ts.map +1 -1
- package/dist/zip/ZipHandler.js.map +1 -1
- package/dist/zip/ZipReader.d.ts.map +1 -1
- package/dist/zip/ZipReader.js +1 -3
- package/dist/zip/ZipReader.js.map +1 -1
- package/dist/zip/ZipWriter.d.ts +1 -1
- package/dist/zip/ZipWriter.d.ts.map +1 -1
- package/dist/zip/ZipWriter.js +28 -36
- package/dist/zip/ZipWriter.js.map +1 -1
- package/dist/zip/types.js +1 -1
- package/dist/zip/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/helper-methods.test.ts +512 -512
- package/src/constants/legacyCompatFlags.ts +138 -138
- package/src/constants/limits.ts +50 -50
- package/src/core/Document.ts +985 -1145
- package/src/core/DocumentContent.ts +461 -467
- package/src/core/DocumentGenerator.ts +1133 -1104
- package/src/core/DocumentIdManager.ts +158 -158
- package/src/core/DocumentParser.ts +2347 -2716
- package/src/core/DocumentValidator.ts +363 -372
- package/src/core/Relationship.ts +367 -367
- package/src/core/RelationshipManager.ts +429 -428
- package/src/elements/AlternateContent.ts +42 -42
- package/src/elements/Bookmark.ts +212 -210
- package/src/elements/BookmarkManager.ts +247 -250
- package/src/elements/Comment.ts +356 -359
- package/src/elements/CommentManager.ts +499 -502
- package/src/elements/CommonTypes.ts +524 -549
- package/src/elements/CustomXml.ts +36 -36
- package/src/elements/Endnote.ts +221 -217
- package/src/elements/EndnoteManager.ts +246 -249
- package/src/elements/Field.ts +1292 -1233
- package/src/elements/FieldHelpers.ts +329 -333
- package/src/elements/FontManager.ts +336 -339
- package/src/elements/Footer.ts +269 -269
- package/src/elements/Footnote.ts +221 -217
- package/src/elements/FootnoteManager.ts +246 -249
- package/src/elements/Header.ts +269 -269
- package/src/elements/HeaderFooterManager.ts +219 -219
- package/src/elements/Hyperlink.ts +1288 -1193
- package/src/elements/Image.ts +1982 -1756
- package/src/elements/ImageManager.ts +437 -432
- package/src/elements/ImageRun.ts +59 -59
- package/src/elements/MathElement.ts +65 -65
- package/src/elements/Paragraph.ts +4347 -4287
- package/src/elements/PreservedElement.ts +53 -53
- package/src/elements/PropertyChangeTypes.ts +458 -442
- package/src/elements/RangeMarker.ts +382 -400
- package/src/elements/Revision.ts +1198 -1217
- package/src/elements/RevisionContent.ts +73 -73
- package/src/elements/RevisionManager.ts +1070 -1070
- package/src/elements/Run.ts +3103 -3073
- package/src/elements/Section.ts +1521 -1421
- package/src/elements/Shape.ts +884 -873
- package/src/elements/StructuredDocumentTag.ts +1176 -1207
- package/src/elements/Table.ts +2468 -2524
- package/src/elements/TableCell.ts +1617 -1621
- package/src/elements/TableGridChange.ts +149 -151
- package/src/elements/TableOfContents.ts +701 -691
- package/src/elements/TableOfContentsElement.ts +89 -89
- package/src/elements/TableRow.ts +960 -929
- package/src/elements/TextBox.ts +766 -768
- package/src/formatting/AbstractNumbering.ts +580 -579
- package/src/formatting/NumberingInstance.ts +295 -299
- package/src/formatting/NumberingLevel.ts +981 -1040
- package/src/formatting/NumberingManager.ts +833 -827
- package/src/formatting/Style.ts +1785 -1879
- package/src/formatting/StylesManager.ts +1090 -1130
- package/src/helpers/CleanupHelper.ts +524 -524
- package/src/images/ImageOptimizer.ts +274 -274
- package/src/index.ts +559 -554
- package/src/managers/DrawingManager.ts +319 -319
- package/src/tracking/DocumentTrackingContext.ts +687 -674
- package/src/tracking/TrackingContext.ts +175 -173
- package/src/types/compatibility-types.ts +49 -49
- package/src/types/formatting.ts +210 -210
- package/src/types/list-types.ts +14 -14
- package/src/types/settings-types.ts +59 -59
- package/src/types/styleConfig.ts +189 -189
- package/src/utils/ChangelogGenerator.ts +1583 -1581
- package/src/utils/CompatibilityUpgrader.ts +235 -237
- package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
- package/src/utils/MoveOperationHelper.ts +233 -238
- package/src/utils/RevisionAwareProcessor.ts +518 -526
- package/src/utils/RevisionWalker.ts +427 -457
- package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
- package/src/utils/ShadingResolver.ts +105 -107
- package/src/utils/acceptRevisions.ts +723 -714
- package/src/utils/cnfStyleDecoder.ts +212 -217
- package/src/utils/corruptionDetection.ts +346 -345
- package/src/utils/dateFormatting.ts +20 -20
- package/src/utils/deepClone.ts +77 -78
- package/src/utils/diagnostics.ts +125 -129
- package/src/utils/errorHandling.ts +80 -80
- package/src/utils/formatting.ts +220 -213
- package/src/utils/list-detection.ts +32 -42
- package/src/utils/logger.ts +412 -404
- package/src/utils/parsingHelpers.ts +190 -190
- package/src/utils/stripTrackedChanges.ts +356 -353
- package/src/utils/textDiff.ts +100 -100
- package/src/utils/units.ts +421 -421
- package/src/utils/validation.ts +553 -542
- package/src/utils/xmlSanitization.ts +179 -182
- package/src/validation/RevisionAutoFixer.ts +541 -542
- package/src/validation/RevisionValidator.ts +470 -460
- package/src/validation/ValidationRules.ts +338 -338
- package/src/validation/index.ts +30 -30
- package/src/xml/XMLBuilder.ts +857 -871
- package/src/xml/XMLParser.ts +877 -919
- package/src/zip/ZipHandler.ts +629 -637
- package/src/zip/ZipReader.ts +295 -299
- package/src/zip/ZipWriter.ts +374 -390
- package/src/zip/types.ts +116 -116
|
@@ -1,274 +1,274 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ImageOptimizer - Lossless image optimization for DOCX documents
|
|
3
|
-
*
|
|
4
|
-
* Two techniques using only Node.js built-in zlib (zero dependencies):
|
|
5
|
-
* 1. PNG re-compression — Re-compress IDAT chunks at zlib level 9 + strip metadata
|
|
6
|
-
* 2. BMP → PNG conversion — Lossless format change, typically 10-50x smaller
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as zlib from 'zlib';
|
|
10
|
-
|
|
11
|
-
// =============================================================================
|
|
12
|
-
// Types
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
export interface ImageOptimizationResult {
|
|
16
|
-
optimizedCount: number;
|
|
17
|
-
totalSavedBytes: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// CRC-32 (IEEE polynomial)
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
const CRC_TABLE: Uint32Array = (() => {
|
|
25
|
-
const table = new Uint32Array(256);
|
|
26
|
-
for (let n = 0; n < 256; n++) {
|
|
27
|
-
let c = n;
|
|
28
|
-
for (let k = 0; k < 8; k++) {
|
|
29
|
-
c =
|
|
30
|
-
}
|
|
31
|
-
table[n] = c;
|
|
32
|
-
}
|
|
33
|
-
return table;
|
|
34
|
-
})();
|
|
35
|
-
|
|
36
|
-
function crc32(buf: Buffer): number {
|
|
37
|
-
let crc =
|
|
38
|
-
for (let i = 0; i < buf.length; i++) {
|
|
39
|
-
crc = CRC_TABLE[(crc ^ buf[i]!) &
|
|
40
|
-
}
|
|
41
|
-
return (crc ^
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// =============================================================================
|
|
45
|
-
// PNG Chunk Builder
|
|
46
|
-
// =============================================================================
|
|
47
|
-
|
|
48
|
-
const PNG_SIGNATURE = Buffer.from([0x89, 0x50,
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Builds a PNG chunk: [4-byte length][4-byte type][data][4-byte CRC]
|
|
52
|
-
* CRC covers type + data bytes
|
|
53
|
-
*/
|
|
54
|
-
function buildPngChunk(type: string, data: Buffer): Buffer {
|
|
55
|
-
const length = Buffer.alloc(4);
|
|
56
|
-
length.writeUInt32BE(data.length, 0);
|
|
57
|
-
|
|
58
|
-
const typeBuffer = Buffer.from(type, 'ascii');
|
|
59
|
-
|
|
60
|
-
const crcInput = Buffer.concat([typeBuffer, data]);
|
|
61
|
-
const crcValue = crc32(crcInput);
|
|
62
|
-
const crcBuffer = Buffer.alloc(4);
|
|
63
|
-
crcBuffer.writeUInt32BE(crcValue, 0);
|
|
64
|
-
|
|
65
|
-
return Buffer.concat([length, typeBuffer, data, crcBuffer]);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// =============================================================================
|
|
69
|
-
// PNG Re-compression
|
|
70
|
-
// =============================================================================
|
|
71
|
-
|
|
72
|
-
/** Chunks that must be preserved for correct PNG rendering */
|
|
73
|
-
const ESSENTIAL_CHUNK_TYPES = new Set(['IHDR', 'PLTE', 'tRNS', 'IDAT', 'IEND']);
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Re-compresses a PNG buffer at zlib level 9 and strips non-essential metadata chunks.
|
|
77
|
-
* Returns the optimized buffer, or null if the PNG is invalid/cannot be optimized.
|
|
78
|
-
* The caller should compare sizes to decide whether to use the result.
|
|
79
|
-
*/
|
|
80
|
-
export function optimizePng(buffer: Buffer): Buffer | null {
|
|
81
|
-
// Verify PNG signature
|
|
82
|
-
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Parse chunks
|
|
87
|
-
let ihdrData: Buffer | null = null;
|
|
88
|
-
let plteData: Buffer | null = null;
|
|
89
|
-
let trnsData: Buffer | null = null;
|
|
90
|
-
const idatBuffers: Buffer[] = [];
|
|
91
|
-
|
|
92
|
-
let offset = 8; // Skip signature
|
|
93
|
-
while (offset + 12 <= buffer.length) {
|
|
94
|
-
const chunkLength = buffer.readUInt32BE(offset);
|
|
95
|
-
const chunkType = buffer.subarray(offset + 4, offset + 8).toString('ascii');
|
|
96
|
-
|
|
97
|
-
// Bounds check: length + type(4) + data + crc(4) = 12 + chunkLength
|
|
98
|
-
if (offset + 12 + chunkLength > buffer.length) break;
|
|
99
|
-
|
|
100
|
-
const chunkData = buffer.subarray(offset + 8, offset + 8 + chunkLength);
|
|
101
|
-
|
|
102
|
-
if (chunkType === 'IHDR') {
|
|
103
|
-
ihdrData = Buffer.from(chunkData);
|
|
104
|
-
} else if (chunkType === 'PLTE') {
|
|
105
|
-
plteData = Buffer.from(chunkData);
|
|
106
|
-
} else if (chunkType === 'tRNS') {
|
|
107
|
-
trnsData = Buffer.from(chunkData);
|
|
108
|
-
} else if (chunkType === 'IDAT') {
|
|
109
|
-
idatBuffers.push(chunkData);
|
|
110
|
-
}
|
|
111
|
-
// Non-essential chunks (tEXt, iTXt, zTXt, iCCP, sRGB, gAMA, cHRM, tIME, pHYs, etc.)
|
|
112
|
-
// are intentionally discarded
|
|
113
|
-
|
|
114
|
-
offset += 12 + chunkLength;
|
|
115
|
-
|
|
116
|
-
// Stop after IEND
|
|
117
|
-
if (chunkType === 'IEND') break;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!ihdrData || idatBuffers.length === 0) return null;
|
|
121
|
-
|
|
122
|
-
// Decompress all IDAT data
|
|
123
|
-
const combinedIdat = Buffer.concat(idatBuffers);
|
|
124
|
-
let decompressed: Buffer;
|
|
125
|
-
try {
|
|
126
|
-
decompressed = zlib.inflateSync(combinedIdat);
|
|
127
|
-
} catch {
|
|
128
|
-
return null; // Corrupted IDAT data
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Re-compress at maximum level
|
|
132
|
-
const recompressed = zlib.deflateSync(decompressed, { level: 9 });
|
|
133
|
-
|
|
134
|
-
// Rebuild PNG: signature + IHDR + [PLTE] + [tRNS] + IDAT + IEND
|
|
135
|
-
const resultParts: Buffer[] = [Buffer.from(PNG_SIGNATURE)];
|
|
136
|
-
|
|
137
|
-
resultParts.push(buildPngChunk('IHDR', ihdrData));
|
|
138
|
-
if (plteData) resultParts.push(buildPngChunk('PLTE', plteData));
|
|
139
|
-
if (trnsData) resultParts.push(buildPngChunk('tRNS', trnsData));
|
|
140
|
-
resultParts.push(buildPngChunk('IDAT', recompressed));
|
|
141
|
-
resultParts.push(buildPngChunk('IEND', Buffer.alloc(0)));
|
|
142
|
-
|
|
143
|
-
return Buffer.concat(resultParts);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// =============================================================================
|
|
147
|
-
// BMP → PNG Conversion
|
|
148
|
-
// =============================================================================
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Converts a BMP buffer to PNG format. Lossless conversion.
|
|
152
|
-
* Supports 24-bit (RGB) and 32-bit (RGBA) uncompressed BMPs.
|
|
153
|
-
* Returns null for unsupported variants (indexed, 16-bit, RLE-compressed).
|
|
154
|
-
*/
|
|
155
|
-
export function convertBmpToPng(buffer: Buffer): Buffer | null {
|
|
156
|
-
// Minimum BMP size: 14 (file header) + 40 (BITMAPINFOHEADER) = 54 bytes
|
|
157
|
-
if (buffer.length < 54 || buffer[0] !== 0x42 || buffer[1] !==
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Parse BMP file header (14 bytes)
|
|
162
|
-
const pixelDataOffset = buffer.readUInt32LE(10);
|
|
163
|
-
|
|
164
|
-
// Parse DIB header (BITMAPINFOHEADER)
|
|
165
|
-
const dibHeaderSize = buffer.readUInt32LE(14);
|
|
166
|
-
if (dibHeaderSize < 40) return null; // Only support BITMAPINFOHEADER and larger
|
|
167
|
-
|
|
168
|
-
const width = buffer.readInt32LE(18);
|
|
169
|
-
const height = buffer.readInt32LE(22);
|
|
170
|
-
const bitsPerPixel = buffer.readUInt16LE(28);
|
|
171
|
-
const compression = buffer.readUInt32LE(30);
|
|
172
|
-
|
|
173
|
-
// Only support uncompressed (0) and BI_BITFIELDS (3) for 32-bit
|
|
174
|
-
if (compression !== 0 && !(compression === 3 && bitsPerPixel === 32)) return null;
|
|
175
|
-
|
|
176
|
-
// Only support 24-bit and 32-bit
|
|
177
|
-
if (bitsPerPixel !== 24 && bitsPerPixel !== 32) return null;
|
|
178
|
-
|
|
179
|
-
if (width <= 0) return null;
|
|
180
|
-
const absHeight = Math.abs(height);
|
|
181
|
-
if (absHeight === 0) return null;
|
|
182
|
-
const isTopDown = height < 0;
|
|
183
|
-
|
|
184
|
-
const bytesPerPixel = bitsPerPixel / 8;
|
|
185
|
-
|
|
186
|
-
// BMP rows are padded to 4-byte boundaries
|
|
187
|
-
const rowSize = Math.ceil((width * bytesPerPixel) / 4) * 4;
|
|
188
|
-
|
|
189
|
-
// Validate buffer has enough pixel data
|
|
190
|
-
if (pixelDataOffset + absHeight * rowSize > buffer.length) return null;
|
|
191
|
-
|
|
192
|
-
// PNG color type: 2 = RGB (24-bit), 6 = RGBA (32-bit)
|
|
193
|
-
const colorType = bitsPerPixel === 32 ? 6 : 2;
|
|
194
|
-
const pngBytesPerPixel = bitsPerPixel === 32 ? 4 : 3;
|
|
195
|
-
|
|
196
|
-
// Build IHDR data (13 bytes)
|
|
197
|
-
const ihdrData = Buffer.alloc(13);
|
|
198
|
-
ihdrData.writeUInt32BE(width, 0);
|
|
199
|
-
ihdrData.writeUInt32BE(absHeight, 4);
|
|
200
|
-
ihdrData[8] = 8;
|
|
201
|
-
ihdrData[9] = colorType;
|
|
202
|
-
ihdrData[10] = 0;
|
|
203
|
-
ihdrData[11] = 0;
|
|
204
|
-
ihdrData[12] = 0;
|
|
205
|
-
|
|
206
|
-
// Build raw image data: filter byte (0 = None) + pixel data per row
|
|
207
|
-
const rawDataSize = absHeight * (1 + width * pngBytesPerPixel);
|
|
208
|
-
const rawData = Buffer.alloc(rawDataSize);
|
|
209
|
-
|
|
210
|
-
for (let y = 0; y < absHeight; y++) {
|
|
211
|
-
// BMP stores rows bottom-up by default; top-down if height is negative
|
|
212
|
-
const bmpRow = isTopDown ? y :
|
|
213
|
-
const bmpRowOffset = pixelDataOffset + bmpRow * rowSize;
|
|
214
|
-
|
|
215
|
-
const pngRowOffset = y * (1 + width * pngBytesPerPixel);
|
|
216
|
-
rawData[pngRowOffset] = 0; // Filter type: None
|
|
217
|
-
|
|
218
|
-
for (let x = 0; x < width; x++) {
|
|
219
|
-
const bmpPixelOffset = bmpRowOffset + x * bytesPerPixel;
|
|
220
|
-
const pngPixelOffset = pngRowOffset + 1 + x * pngBytesPerPixel;
|
|
221
|
-
|
|
222
|
-
// Convert BGR(A) → RGB(A)
|
|
223
|
-
rawData[pngPixelOffset] = buffer[bmpPixelOffset + 2]!;
|
|
224
|
-
rawData[pngPixelOffset + 1] = buffer[bmpPixelOffset + 1]!; // G
|
|
225
|
-
rawData[pngPixelOffset + 2] = buffer[bmpPixelOffset]!;
|
|
226
|
-
|
|
227
|
-
if (bitsPerPixel === 32) {
|
|
228
|
-
rawData[pngPixelOffset + 3] = buffer[bmpPixelOffset + 3]!; // A
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Compress pixel data
|
|
234
|
-
const compressedData = zlib.deflateSync(rawData, { level: 9 });
|
|
235
|
-
|
|
236
|
-
// Assemble PNG file
|
|
237
|
-
return Buffer.concat([
|
|
238
|
-
Buffer.from(PNG_SIGNATURE),
|
|
239
|
-
buildPngChunk('IHDR', ihdrData),
|
|
240
|
-
buildPngChunk('IDAT', compressedData),
|
|
241
|
-
buildPngChunk('IEND', Buffer.alloc(0)),
|
|
242
|
-
]);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// =============================================================================
|
|
246
|
-
// Router
|
|
247
|
-
// =============================================================================
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Optimizes an image buffer based on its format.
|
|
251
|
-
* - PNG: re-compresses at zlib level 9 and strips metadata. Only returns result if smaller.
|
|
252
|
-
* - BMP: converts to PNG (always smaller than uncompressed BMP).
|
|
253
|
-
* - Other formats: returns null (unsupported / cannot be losslessly optimized further).
|
|
254
|
-
*/
|
|
255
|
-
export function optimizeImage(
|
|
256
|
-
buffer: Buffer,
|
|
257
|
-
extension: string
|
|
258
|
-
): { data: Buffer; newExtension: string } | null {
|
|
259
|
-
const ext = extension.toLowerCase();
|
|
260
|
-
|
|
261
|
-
if (ext === 'png') {
|
|
262
|
-
const optimized = optimizePng(buffer);
|
|
263
|
-
if (!optimized || optimized.length >= buffer.length) return null;
|
|
264
|
-
return { data: optimized, newExtension: 'png' };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (ext === 'bmp') {
|
|
268
|
-
const converted = convertBmpToPng(buffer);
|
|
269
|
-
if (!converted) return null;
|
|
270
|
-
return { data: converted, newExtension: 'png' };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ImageOptimizer - Lossless image optimization for DOCX documents
|
|
3
|
+
*
|
|
4
|
+
* Two techniques using only Node.js built-in zlib (zero dependencies):
|
|
5
|
+
* 1. PNG re-compression — Re-compress IDAT chunks at zlib level 9 + strip metadata
|
|
6
|
+
* 2. BMP → PNG conversion — Lossless format change, typically 10-50x smaller
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as zlib from 'zlib';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export interface ImageOptimizationResult {
|
|
16
|
+
optimizedCount: number;
|
|
17
|
+
totalSavedBytes: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// CRC-32 (IEEE polynomial)
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
const CRC_TABLE: Uint32Array = (() => {
|
|
25
|
+
const table = new Uint32Array(256);
|
|
26
|
+
for (let n = 0; n < 256; n++) {
|
|
27
|
+
let c = n;
|
|
28
|
+
for (let k = 0; k < 8; k++) {
|
|
29
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
30
|
+
}
|
|
31
|
+
table[n] = c;
|
|
32
|
+
}
|
|
33
|
+
return table;
|
|
34
|
+
})();
|
|
35
|
+
|
|
36
|
+
function crc32(buf: Buffer): number {
|
|
37
|
+
let crc = 0xffffffff;
|
|
38
|
+
for (let i = 0; i < buf.length; i++) {
|
|
39
|
+
crc = CRC_TABLE[(crc ^ buf[i]!) & 0xff]! ^ (crc >>> 8);
|
|
40
|
+
}
|
|
41
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// PNG Chunk Builder
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Builds a PNG chunk: [4-byte length][4-byte type][data][4-byte CRC]
|
|
52
|
+
* CRC covers type + data bytes
|
|
53
|
+
*/
|
|
54
|
+
function buildPngChunk(type: string, data: Buffer): Buffer {
|
|
55
|
+
const length = Buffer.alloc(4);
|
|
56
|
+
length.writeUInt32BE(data.length, 0);
|
|
57
|
+
|
|
58
|
+
const typeBuffer = Buffer.from(type, 'ascii');
|
|
59
|
+
|
|
60
|
+
const crcInput = Buffer.concat([typeBuffer, data]);
|
|
61
|
+
const crcValue = crc32(crcInput);
|
|
62
|
+
const crcBuffer = Buffer.alloc(4);
|
|
63
|
+
crcBuffer.writeUInt32BE(crcValue, 0);
|
|
64
|
+
|
|
65
|
+
return Buffer.concat([length, typeBuffer, data, crcBuffer]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// PNG Re-compression
|
|
70
|
+
// =============================================================================
|
|
71
|
+
|
|
72
|
+
/** Chunks that must be preserved for correct PNG rendering */
|
|
73
|
+
const ESSENTIAL_CHUNK_TYPES = new Set(['IHDR', 'PLTE', 'tRNS', 'IDAT', 'IEND']);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Re-compresses a PNG buffer at zlib level 9 and strips non-essential metadata chunks.
|
|
77
|
+
* Returns the optimized buffer, or null if the PNG is invalid/cannot be optimized.
|
|
78
|
+
* The caller should compare sizes to decide whether to use the result.
|
|
79
|
+
*/
|
|
80
|
+
export function optimizePng(buffer: Buffer): Buffer | null {
|
|
81
|
+
// Verify PNG signature
|
|
82
|
+
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse chunks
|
|
87
|
+
let ihdrData: Buffer | null = null;
|
|
88
|
+
let plteData: Buffer | null = null;
|
|
89
|
+
let trnsData: Buffer | null = null;
|
|
90
|
+
const idatBuffers: Buffer[] = [];
|
|
91
|
+
|
|
92
|
+
let offset = 8; // Skip signature
|
|
93
|
+
while (offset + 12 <= buffer.length) {
|
|
94
|
+
const chunkLength = buffer.readUInt32BE(offset);
|
|
95
|
+
const chunkType = buffer.subarray(offset + 4, offset + 8).toString('ascii');
|
|
96
|
+
|
|
97
|
+
// Bounds check: length + type(4) + data + crc(4) = 12 + chunkLength
|
|
98
|
+
if (offset + 12 + chunkLength > buffer.length) break;
|
|
99
|
+
|
|
100
|
+
const chunkData = buffer.subarray(offset + 8, offset + 8 + chunkLength);
|
|
101
|
+
|
|
102
|
+
if (chunkType === 'IHDR') {
|
|
103
|
+
ihdrData = Buffer.from(chunkData);
|
|
104
|
+
} else if (chunkType === 'PLTE') {
|
|
105
|
+
plteData = Buffer.from(chunkData);
|
|
106
|
+
} else if (chunkType === 'tRNS') {
|
|
107
|
+
trnsData = Buffer.from(chunkData);
|
|
108
|
+
} else if (chunkType === 'IDAT') {
|
|
109
|
+
idatBuffers.push(chunkData);
|
|
110
|
+
}
|
|
111
|
+
// Non-essential chunks (tEXt, iTXt, zTXt, iCCP, sRGB, gAMA, cHRM, tIME, pHYs, etc.)
|
|
112
|
+
// are intentionally discarded
|
|
113
|
+
|
|
114
|
+
offset += 12 + chunkLength;
|
|
115
|
+
|
|
116
|
+
// Stop after IEND
|
|
117
|
+
if (chunkType === 'IEND') break;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!ihdrData || idatBuffers.length === 0) return null;
|
|
121
|
+
|
|
122
|
+
// Decompress all IDAT data
|
|
123
|
+
const combinedIdat = Buffer.concat(idatBuffers);
|
|
124
|
+
let decompressed: Buffer;
|
|
125
|
+
try {
|
|
126
|
+
decompressed = zlib.inflateSync(combinedIdat);
|
|
127
|
+
} catch {
|
|
128
|
+
return null; // Corrupted IDAT data
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Re-compress at maximum level
|
|
132
|
+
const recompressed = zlib.deflateSync(decompressed, { level: 9 });
|
|
133
|
+
|
|
134
|
+
// Rebuild PNG: signature + IHDR + [PLTE] + [tRNS] + IDAT + IEND
|
|
135
|
+
const resultParts: Buffer[] = [Buffer.from(PNG_SIGNATURE)];
|
|
136
|
+
|
|
137
|
+
resultParts.push(buildPngChunk('IHDR', ihdrData));
|
|
138
|
+
if (plteData) resultParts.push(buildPngChunk('PLTE', plteData));
|
|
139
|
+
if (trnsData) resultParts.push(buildPngChunk('tRNS', trnsData));
|
|
140
|
+
resultParts.push(buildPngChunk('IDAT', recompressed));
|
|
141
|
+
resultParts.push(buildPngChunk('IEND', Buffer.alloc(0)));
|
|
142
|
+
|
|
143
|
+
return Buffer.concat(resultParts);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// BMP → PNG Conversion
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Converts a BMP buffer to PNG format. Lossless conversion.
|
|
152
|
+
* Supports 24-bit (RGB) and 32-bit (RGBA) uncompressed BMPs.
|
|
153
|
+
* Returns null for unsupported variants (indexed, 16-bit, RLE-compressed).
|
|
154
|
+
*/
|
|
155
|
+
export function convertBmpToPng(buffer: Buffer): Buffer | null {
|
|
156
|
+
// Minimum BMP size: 14 (file header) + 40 (BITMAPINFOHEADER) = 54 bytes
|
|
157
|
+
if (buffer.length < 54 || buffer[0] !== 0x42 || buffer[1] !== 0x4d) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Parse BMP file header (14 bytes)
|
|
162
|
+
const pixelDataOffset = buffer.readUInt32LE(10);
|
|
163
|
+
|
|
164
|
+
// Parse DIB header (BITMAPINFOHEADER)
|
|
165
|
+
const dibHeaderSize = buffer.readUInt32LE(14);
|
|
166
|
+
if (dibHeaderSize < 40) return null; // Only support BITMAPINFOHEADER and larger
|
|
167
|
+
|
|
168
|
+
const width = buffer.readInt32LE(18);
|
|
169
|
+
const height = buffer.readInt32LE(22);
|
|
170
|
+
const bitsPerPixel = buffer.readUInt16LE(28);
|
|
171
|
+
const compression = buffer.readUInt32LE(30);
|
|
172
|
+
|
|
173
|
+
// Only support uncompressed (0) and BI_BITFIELDS (3) for 32-bit
|
|
174
|
+
if (compression !== 0 && !(compression === 3 && bitsPerPixel === 32)) return null;
|
|
175
|
+
|
|
176
|
+
// Only support 24-bit and 32-bit
|
|
177
|
+
if (bitsPerPixel !== 24 && bitsPerPixel !== 32) return null;
|
|
178
|
+
|
|
179
|
+
if (width <= 0) return null;
|
|
180
|
+
const absHeight = Math.abs(height);
|
|
181
|
+
if (absHeight === 0) return null;
|
|
182
|
+
const isTopDown = height < 0;
|
|
183
|
+
|
|
184
|
+
const bytesPerPixel = bitsPerPixel / 8;
|
|
185
|
+
|
|
186
|
+
// BMP rows are padded to 4-byte boundaries
|
|
187
|
+
const rowSize = Math.ceil((width * bytesPerPixel) / 4) * 4;
|
|
188
|
+
|
|
189
|
+
// Validate buffer has enough pixel data
|
|
190
|
+
if (pixelDataOffset + absHeight * rowSize > buffer.length) return null;
|
|
191
|
+
|
|
192
|
+
// PNG color type: 2 = RGB (24-bit), 6 = RGBA (32-bit)
|
|
193
|
+
const colorType = bitsPerPixel === 32 ? 6 : 2;
|
|
194
|
+
const pngBytesPerPixel = bitsPerPixel === 32 ? 4 : 3;
|
|
195
|
+
|
|
196
|
+
// Build IHDR data (13 bytes)
|
|
197
|
+
const ihdrData = Buffer.alloc(13);
|
|
198
|
+
ihdrData.writeUInt32BE(width, 0);
|
|
199
|
+
ihdrData.writeUInt32BE(absHeight, 4);
|
|
200
|
+
ihdrData[8] = 8; // bit depth
|
|
201
|
+
ihdrData[9] = colorType; // color type
|
|
202
|
+
ihdrData[10] = 0; // compression method (deflate)
|
|
203
|
+
ihdrData[11] = 0; // filter method (adaptive)
|
|
204
|
+
ihdrData[12] = 0; // interlace method (none)
|
|
205
|
+
|
|
206
|
+
// Build raw image data: filter byte (0 = None) + pixel data per row
|
|
207
|
+
const rawDataSize = absHeight * (1 + width * pngBytesPerPixel);
|
|
208
|
+
const rawData = Buffer.alloc(rawDataSize);
|
|
209
|
+
|
|
210
|
+
for (let y = 0; y < absHeight; y++) {
|
|
211
|
+
// BMP stores rows bottom-up by default; top-down if height is negative
|
|
212
|
+
const bmpRow = isTopDown ? y : absHeight - 1 - y;
|
|
213
|
+
const bmpRowOffset = pixelDataOffset + bmpRow * rowSize;
|
|
214
|
+
|
|
215
|
+
const pngRowOffset = y * (1 + width * pngBytesPerPixel);
|
|
216
|
+
rawData[pngRowOffset] = 0; // Filter type: None
|
|
217
|
+
|
|
218
|
+
for (let x = 0; x < width; x++) {
|
|
219
|
+
const bmpPixelOffset = bmpRowOffset + x * bytesPerPixel;
|
|
220
|
+
const pngPixelOffset = pngRowOffset + 1 + x * pngBytesPerPixel;
|
|
221
|
+
|
|
222
|
+
// Convert BGR(A) → RGB(A)
|
|
223
|
+
rawData[pngPixelOffset] = buffer[bmpPixelOffset + 2]!; // R
|
|
224
|
+
rawData[pngPixelOffset + 1] = buffer[bmpPixelOffset + 1]!; // G
|
|
225
|
+
rawData[pngPixelOffset + 2] = buffer[bmpPixelOffset]!; // B
|
|
226
|
+
|
|
227
|
+
if (bitsPerPixel === 32) {
|
|
228
|
+
rawData[pngPixelOffset + 3] = buffer[bmpPixelOffset + 3]!; // A
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Compress pixel data
|
|
234
|
+
const compressedData = zlib.deflateSync(rawData, { level: 9 });
|
|
235
|
+
|
|
236
|
+
// Assemble PNG file
|
|
237
|
+
return Buffer.concat([
|
|
238
|
+
Buffer.from(PNG_SIGNATURE),
|
|
239
|
+
buildPngChunk('IHDR', ihdrData),
|
|
240
|
+
buildPngChunk('IDAT', compressedData),
|
|
241
|
+
buildPngChunk('IEND', Buffer.alloc(0)),
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// =============================================================================
|
|
246
|
+
// Router
|
|
247
|
+
// =============================================================================
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Optimizes an image buffer based on its format.
|
|
251
|
+
* - PNG: re-compresses at zlib level 9 and strips metadata. Only returns result if smaller.
|
|
252
|
+
* - BMP: converts to PNG (always smaller than uncompressed BMP).
|
|
253
|
+
* - Other formats: returns null (unsupported / cannot be losslessly optimized further).
|
|
254
|
+
*/
|
|
255
|
+
export function optimizeImage(
|
|
256
|
+
buffer: Buffer,
|
|
257
|
+
extension: string
|
|
258
|
+
): { data: Buffer; newExtension: string } | null {
|
|
259
|
+
const ext = extension.toLowerCase();
|
|
260
|
+
|
|
261
|
+
if (ext === 'png') {
|
|
262
|
+
const optimized = optimizePng(buffer);
|
|
263
|
+
if (!optimized || optimized.length >= buffer.length) return null;
|
|
264
|
+
return { data: optimized, newExtension: 'png' };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (ext === 'bmp') {
|
|
268
|
+
const converted = convertBmpToPng(buffer);
|
|
269
|
+
if (!converted) return null;
|
|
270
|
+
return { data: converted, newExtension: 'png' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return null;
|
|
274
|
+
}
|