documentation-hub 5.7.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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Rule Engine Type Definitions for the blank line management system.
3
+ *
4
+ * Rules are evaluated in priority order:
5
+ * 1. Removal rules (checked first - absolute constraints)
6
+ * 2. Addition rules (checked second - absolute requirements)
7
+ * 3. Preservation fallback (no rule matched - keep original if existed)
8
+ */
9
+
10
+ import type { Document, Paragraph, Table, TableCell } from "docxmlater";
11
+
12
+ /**
13
+ * Context provided to each rule for evaluation.
14
+ */
15
+ export interface RuleContext {
16
+ doc: Document;
17
+ /** Index of the position being evaluated (body element index or cell para index) */
18
+ currentIndex: number;
19
+ /** The element at the current position */
20
+ currentElement: Paragraph | Table;
21
+ /** Element before the current position */
22
+ prevElement?: Paragraph | Table;
23
+ /** Element after the current position */
24
+ nextElement?: Paragraph | Table;
25
+ /** Whether this is a body-level or cell-level evaluation */
26
+ scope: "body" | "cell";
27
+ /** The cell if scope is 'cell' */
28
+ cell?: TableCell;
29
+ /** All paragraphs in the cell (if scope is 'cell') */
30
+ cellParagraphs?: Paragraph[];
31
+ /** Index within the cell's paragraphs (if scope is 'cell') */
32
+ cellParaIndex?: number;
33
+ /** The table containing the cell (if scope is 'cell') */
34
+ parentTable?: Table;
35
+ }
36
+
37
+ /**
38
+ * A blank line rule definition.
39
+ */
40
+ export interface BlankLineRule {
41
+ /** Unique identifier for the rule */
42
+ id: string;
43
+ /** Whether this rule removes or adds blank lines */
44
+ action: "remove" | "add";
45
+ /** Whether the rule applies to body, cell, or both contexts */
46
+ scope: "body" | "cell" | "both";
47
+ /**
48
+ * Returns true if this rule matches the given context.
49
+ *
50
+ * For 'remove' rules: returns true if a blank at this position should be removed.
51
+ * For 'add' rules: returns true if a blank should exist at this position.
52
+ */
53
+ matches(context: RuleContext): boolean;
54
+ }
55
+
56
+ /**
57
+ * Options passed to the rule-based blank line processing engine.
58
+ */
59
+ export interface BlankLineProcessingOptions {
60
+ /** List bullet settings for indentation rules */
61
+ listBulletSettings?: {
62
+ indentationLevels: Array<{
63
+ level: number;
64
+ symbolIndent: number;
65
+ textIndent: number;
66
+ }>;
67
+ };
68
+ /** Stop bold+colon blank lines after this heading text is found in a 1x1 table */
69
+ stopBoldColonAfterHeading?: string;
70
+ /** Normal style formatting for blank lines (from UI settings, pre-converted to twips) */
71
+ normalStyleFormatting?: {
72
+ spaceBefore: number;
73
+ spaceAfter: number;
74
+ lineSpacing: number;
75
+ fontSize: number;
76
+ fontFamily: string;
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Result from the rule-based blank line processing.
82
+ */
83
+ export interface RuleEngineResult {
84
+ /** Number of blank lines removed by removal rules */
85
+ removed: number;
86
+ /** Number of blank lines added by addition rules */
87
+ added: number;
88
+ /** Number of blank lines preserved because no rule matched but original had one */
89
+ preserved: number;
90
+ /** Number of indentation adjustments made */
91
+ indentationFixed: number;
92
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared types for the BlankLineManager module.
3
+ */
4
+
5
+ /**
6
+ * Common options for blank paragraph creation and insertion
7
+ */
8
+ export interface BlankLineOptions {
9
+ /** Spacing after blank paragraphs in twips (default: 120 = 6pt) */
10
+ spacingAfter: number;
11
+ /** Spacing before blank paragraphs in twips */
12
+ spacingBefore?: number;
13
+ /** Line spacing in twips */
14
+ lineSpacing?: number;
15
+ /** Font size in points */
16
+ fontSize?: number;
17
+ /** Font family name */
18
+ fontFamily?: string;
19
+ /** Mark blank paragraphs as preserved to prevent removal (default: true) */
20
+ markAsPreserved: boolean;
21
+ /** Style to apply to blank paragraphs (default: 'Normal') */
22
+ style: string;
23
+ }
24
+
25
+ export const DEFAULT_BLANK_LINE_OPTIONS: BlankLineOptions = {
26
+ spacingAfter: 120,
27
+ markAsPreserved: true,
28
+ style: "Normal",
29
+ };
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Embedded image border detection and cropping.
3
+ *
4
+ * Screen-captured images often include the original document's visible border.
5
+ * When the pipeline then applies its own border via borderAndCenterLargeImages(),
6
+ * the result is a double-border. This module detects that embedded dark border
7
+ * (+ white gap) and crops it away so the pipeline's border sits cleanly against
8
+ * the actual content.
9
+ *
10
+ * Uses the existing `canvas` package for pixel analysis and docxmlater's
11
+ * Image.updateImageData() to replace the buffer in-place.
12
+ */
13
+
14
+ import { createCanvas, loadImage } from "canvas";
15
+ import { Document, Image, ImageRun, Revision } from "docxmlater";
16
+
17
+ // ── Detection constants ──────────────────────────────────────────────
18
+ const DARK_THRESHOLD = 80; // luminance <= this = border pixel
19
+ const LIGHT_THRESHOLD = 230; // luminance >= this = gap/padding pixel
20
+ const EDGE_CONSENSUS = 0.65; // 65 % of scan lines must detect border
21
+ const MIN_BORDERED_EDGES = 3; // at least 3 of 4 edges
22
+ const MAX_BORDER_THICKNESS = 4; // border line max 4 px
23
+ const MIN_GAP_THICKNESS = 2; // white gap at least 2 px
24
+ const MAX_SCAN_DEPTH = 25; // scan max 25 px from edge
25
+ const SAMPLE_INTERVAL = 5; // sample every 5th column / row
26
+ const MAX_CROP_FRACTION = 0.15; // never crop > 15 % from one edge
27
+ const MIN_DIMENSION_PX = 80; // skip images < 80 px
28
+
29
+ const EMUS_PER_PIXEL = 9525;
30
+
31
+ type Edge = "top" | "bottom" | "left" | "right";
32
+
33
+ // ── Public API ───────────────────────────────────────────────────────
34
+
35
+ export interface CropResult {
36
+ croppedCount: number;
37
+ skippedCount: number;
38
+ errorCount: number;
39
+ }
40
+
41
+ interface CropRect {
42
+ top: number;
43
+ bottom: number;
44
+ left: number;
45
+ right: number;
46
+ }
47
+
48
+ interface EdgeResult {
49
+ detected: boolean;
50
+ cropPosition: number;
51
+ }
52
+
53
+ /**
54
+ * Iterate every raster image in the document, detect embedded borders,
55
+ * and crop them away.
56
+ */
57
+ export async function cropEmbeddedImageBorders(
58
+ doc: Document,
59
+ log: { debug: Function; info: Function; warn: Function },
60
+ ): Promise<CropResult> {
61
+ const result: CropResult = { croppedCount: 0, skippedCount: 0, errorCount: 0 };
62
+
63
+ const images = collectImages(doc);
64
+ log.debug(`Found ${images.length} images to analyse for embedded borders`);
65
+
66
+ for (const image of images) {
67
+ try {
68
+ await processOneImage(image, result, log);
69
+ } catch (err) {
70
+ result.errorCount++;
71
+ log.warn(
72
+ `Error processing image for embedded border crop: ${err instanceof Error ? err.message : String(err)}`,
73
+ );
74
+ }
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ // ── Image collection ─────────────────────────────────────────────────
81
+
82
+ function collectImages(doc: Document): Image[] {
83
+ const images: Image[] = [];
84
+ for (const para of doc.getAllParagraphs()) {
85
+ for (const item of para.getContent()) {
86
+ if (item instanceof Image) {
87
+ images.push(item);
88
+ }
89
+ if (item instanceof ImageRun) {
90
+ images.push(item.getImageElement());
91
+ }
92
+ if (item instanceof Revision) {
93
+ for (const run of item.getRuns()) {
94
+ if (run instanceof ImageRun) {
95
+ images.push(run.getImageElement());
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return images;
102
+ }
103
+
104
+ // ── Per-image processing ─────────────────────────────────────────────
105
+
106
+ async function processOneImage(
107
+ image: Image,
108
+ result: CropResult,
109
+ log: { debug: Function; info: Function; warn: Function },
110
+ ): Promise<void> {
111
+ const ext = image.getExtension()?.toLowerCase();
112
+
113
+ // Skip non-raster formats
114
+ if (ext === "svg" || ext === "emf" || ext === "wmf") {
115
+ result.skippedCount++;
116
+ return;
117
+ }
118
+
119
+ const buf = image.getImageDataSafe();
120
+ if (!buf || buf.length === 0) {
121
+ result.skippedCount++;
122
+ return;
123
+ }
124
+
125
+ // Load into canvas to get pixel data
126
+ const img = await loadImage(buf);
127
+ const w = img.width;
128
+ const h = img.height;
129
+
130
+ if (w < MIN_DIMENSION_PX || h < MIN_DIMENSION_PX) {
131
+ result.skippedCount++;
132
+ return;
133
+ }
134
+
135
+ const canvas = createCanvas(w, h);
136
+ const ctx = canvas.getContext("2d");
137
+ ctx.drawImage(img, 0, 0);
138
+ const imageData = ctx.getImageData(0, 0, w, h);
139
+ const pixels = imageData.data; // Uint8ClampedArray [r,g,b,a, ...]
140
+
141
+ const cropRect = detectEmbeddedBorder(pixels, w, h);
142
+ if (!cropRect) {
143
+ result.skippedCount++;
144
+ return;
145
+ }
146
+
147
+ // Validate: never crop more than MAX_CROP_FRACTION from any edge
148
+ if (
149
+ cropRect.top > h * MAX_CROP_FRACTION ||
150
+ cropRect.bottom > h * MAX_CROP_FRACTION ||
151
+ cropRect.left > w * MAX_CROP_FRACTION ||
152
+ cropRect.right > w * MAX_CROP_FRACTION
153
+ ) {
154
+ log.debug(`Skipping image: crop exceeds ${MAX_CROP_FRACTION * 100}% on an edge`);
155
+ result.skippedCount++;
156
+ return;
157
+ }
158
+
159
+ // Validate minimum remaining dimensions
160
+ const newW = w - cropRect.left - cropRect.right;
161
+ const newH = h - cropRect.top - cropRect.bottom;
162
+ if (newW < MIN_DIMENSION_PX || newH < MIN_DIMENSION_PX) {
163
+ log.debug(`Skipping image: cropped dimensions ${newW}x${newH} too small`);
164
+ result.skippedCount++;
165
+ return;
166
+ }
167
+
168
+ // Crop via canvas
169
+ const cropCanvas = createCanvas(newW, newH);
170
+ const cropCtx = cropCanvas.getContext("2d");
171
+ cropCtx.drawImage(
172
+ canvas,
173
+ cropRect.left,
174
+ cropRect.top,
175
+ newW,
176
+ newH,
177
+ 0,
178
+ 0,
179
+ newW,
180
+ newH,
181
+ );
182
+
183
+ // Preserve format: JPEG for JPEG inputs, PNG otherwise
184
+ const isJpeg = ext === "jpeg" || ext === "jpg";
185
+ const croppedBuffer = isJpeg
186
+ ? cropCanvas.toBuffer("image/jpeg", { quality: 0.92 })
187
+ : cropCanvas.toBuffer("image/png");
188
+
189
+ // Replace image data
190
+ await image.updateImageData(croppedBuffer);
191
+
192
+ // Update display dimensions proportionally
193
+ const origWidthEMU = image.getWidth();
194
+ const origHeightEMU = image.getHeight();
195
+ const newWidthEMU = Math.round(origWidthEMU * (newW / w));
196
+ const newHeightEMU = Math.round(origHeightEMU * (newH / h));
197
+ image.setSize(newWidthEMU, newHeightEMU);
198
+
199
+ log.debug(
200
+ `Cropped embedded border: ${w}x${h} → ${newW}x${newH} (removed T:${cropRect.top} B:${cropRect.bottom} L:${cropRect.left} R:${cropRect.right})`,
201
+ );
202
+ result.croppedCount++;
203
+ }
204
+
205
+ // ── Border detection ─────────────────────────────────────────────────
206
+
207
+ function detectEmbeddedBorder(
208
+ pixels: Uint8ClampedArray,
209
+ width: number,
210
+ height: number,
211
+ ): CropRect | null {
212
+ const edges: Edge[] = ["top", "bottom", "left", "right"];
213
+ const results: Record<Edge, EdgeResult> = {} as Record<Edge, EdgeResult>;
214
+ let detectedCount = 0;
215
+
216
+ for (const edge of edges) {
217
+ results[edge] = analyzeEdge(pixels, width, height, edge);
218
+ if (results[edge].detected) detectedCount++;
219
+ }
220
+
221
+ if (detectedCount < MIN_BORDERED_EDGES) return null;
222
+
223
+ return {
224
+ top: results.top.detected ? results.top.cropPosition : 0,
225
+ bottom: results.bottom.detected ? results.bottom.cropPosition : 0,
226
+ left: results.left.detected ? results.left.cropPosition : 0,
227
+ right: results.right.detected ? results.right.cropPosition : 0,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Analyze one edge of the image for the border+gap pattern.
233
+ *
234
+ * Samples every SAMPLE_INTERVAL-th line perpendicular to the edge.
235
+ * Each scan line looks inward for: dark border (1-4 px) then white gap (2-20 px).
236
+ * Uses the median crop position from all detecting scan lines.
237
+ */
238
+ function analyzeEdge(
239
+ pixels: Uint8ClampedArray,
240
+ width: number,
241
+ height: number,
242
+ edge: Edge,
243
+ ): EdgeResult {
244
+ const perpLength = edge === "top" || edge === "bottom" ? width : height;
245
+ const sampleCount = Math.floor(perpLength / SAMPLE_INTERVAL);
246
+ if (sampleCount === 0) return { detected: false, cropPosition: 0 };
247
+
248
+ const cropPositions: number[] = [];
249
+
250
+ for (let s = 0; s < sampleCount; s++) {
251
+ const lineIndex = s * SAMPLE_INTERVAL;
252
+ const pos = scanLine(pixels, width, height, edge, lineIndex);
253
+ if (pos !== null) cropPositions.push(pos);
254
+ }
255
+
256
+ const needed = Math.ceil(sampleCount * EDGE_CONSENSUS);
257
+ if (cropPositions.length < needed) {
258
+ return { detected: false, cropPosition: 0 };
259
+ }
260
+
261
+ // Use median crop position (robust against corner artifacts)
262
+ cropPositions.sort((a, b) => a - b);
263
+ const median = cropPositions[Math.floor(cropPositions.length / 2)];
264
+
265
+ return { detected: true, cropPosition: median };
266
+ }
267
+
268
+ /**
269
+ * Scan one line from the given edge inward, looking for the
270
+ * dark-border + white-gap pattern.
271
+ *
272
+ * @returns crop position (pixels from edge) where content starts, or null
273
+ */
274
+ function scanLine(
275
+ pixels: Uint8ClampedArray,
276
+ width: number,
277
+ height: number,
278
+ edge: Edge,
279
+ lineIndex: number,
280
+ ): number | null {
281
+ const maxDepth = MAX_SCAN_DEPTH;
282
+ let borderPixels = 0;
283
+ let gapPixels = 0;
284
+ let inBorder = true;
285
+
286
+ for (let depth = 0; depth < maxDepth; depth++) {
287
+ const lum = getPixelLuminance(pixels, width, height, edge, lineIndex, depth);
288
+
289
+ if (inBorder) {
290
+ if (lum <= DARK_THRESHOLD) {
291
+ borderPixels++;
292
+ if (borderPixels > MAX_BORDER_THICKNESS) return null; // too thick for a border
293
+ } else if (borderPixels > 0 && lum >= LIGHT_THRESHOLD) {
294
+ // Transition: dark → light (border ended, gap started)
295
+ inBorder = false;
296
+ gapPixels = 1;
297
+ } else if (borderPixels === 0 && lum >= LIGHT_THRESHOLD) {
298
+ // No dark border found at all — not a border pattern
299
+ return null;
300
+ } else {
301
+ // Mid-tone pixel before finding border — not a clean border
302
+ if (borderPixels === 0) return null;
303
+ // After some border pixels, a mid-tone could be anti-aliasing; keep scanning
304
+ borderPixels++;
305
+ if (borderPixels > MAX_BORDER_THICKNESS) return null;
306
+ }
307
+ } else {
308
+ // In gap region
309
+ if (lum >= LIGHT_THRESHOLD) {
310
+ gapPixels++;
311
+ } else {
312
+ // Hit non-light pixel — gap ended, content starts
313
+ if (gapPixels >= MIN_GAP_THICKNESS) {
314
+ return depth; // content starts here
315
+ }
316
+ return null; // gap too thin
317
+ }
318
+ }
319
+ }
320
+
321
+ // Reached max scan depth while still in gap — content starts just past gap
322
+ if (!inBorder && gapPixels >= MIN_GAP_THICKNESS) {
323
+ return borderPixels + gapPixels;
324
+ }
325
+
326
+ return null;
327
+ }
328
+
329
+ /**
330
+ * Get luminance (0-255) for a pixel identified by edge, perpendicular line
331
+ * index, and depth from the edge.
332
+ */
333
+ function getPixelLuminance(
334
+ pixels: Uint8ClampedArray,
335
+ width: number,
336
+ height: number,
337
+ edge: Edge,
338
+ lineIndex: number,
339
+ depth: number,
340
+ ): number {
341
+ let x: number;
342
+ let y: number;
343
+
344
+ switch (edge) {
345
+ case "top":
346
+ x = lineIndex;
347
+ y = depth;
348
+ break;
349
+ case "bottom":
350
+ x = lineIndex;
351
+ y = height - 1 - depth;
352
+ break;
353
+ case "left":
354
+ x = depth;
355
+ y = lineIndex;
356
+ break;
357
+ case "right":
358
+ x = width - 1 - depth;
359
+ y = lineIndex;
360
+ break;
361
+ }
362
+
363
+ // Clamp to image bounds
364
+ x = Math.min(Math.max(x, 0), width - 1);
365
+ y = Math.min(Math.max(y, 0), height - 1);
366
+
367
+ const idx = (y * width + x) * 4;
368
+ const r = pixels[idx];
369
+ const g = pixels[idx + 1];
370
+ const b = pixels[idx + 2];
371
+ return getLuminance(r, g, b);
372
+ }
373
+
374
+ /** Standard perceived luminance (ITU-R BT.601). */
375
+ function getLuminance(r: number, g: number, b: number): number {
376
+ return 0.299 * r + 0.587 * g + 0.114 * b;
377
+ }