@@ -17,6 +17,7 @@ import {
1717 BaseException ,
1818 DrawOPS ,
1919 FeatureTest ,
20+ MathClamp ,
2021 shadow ,
2122 Util ,
2223 warn ,
@@ -1034,6 +1035,171 @@ function makePathFromDrawOPS(data) {
10341035 return path ;
10351036}
10361037
1038+ /**
1039+ * Maps between page IDs and page numbers, allowing bidirectional conversion
1040+ * between the two representations. This is useful when the page numbering
1041+ * in the PDF document doesn't match the default sequential ordering.
1042+ */
1043+ class PagesMapper {
1044+ /**
1045+ * Maps page IDs to their corresponding page numbers.
1046+ * @type {Uint32Array|null }
1047+ */
1048+ static #idToPageNumber = null ;
1049+
1050+ /**
1051+ * Maps page numbers to their corresponding page IDs.
1052+ * @type {Uint32Array|null }
1053+ */
1054+ static #pageNumberToId = null ;
1055+
1056+ /**
1057+ * Previous mapping of page IDs to page numbers.
1058+ * @type {Uint32Array|null }
1059+ */
1060+ static #prevIdToPageNumber = null ;
1061+
1062+ /**
1063+ * The total number of pages.
1064+ * @type {number }
1065+ */
1066+ static #pagesNumber = 0 ;
1067+
1068+ /**
1069+ * Gets the total number of pages.
1070+ * @returns {number } The number of pages.
1071+ */
1072+ get pagesNumber ( ) {
1073+ return PagesMapper . #pagesNumber;
1074+ }
1075+
1076+ /**
1077+ * Sets the total number of pages and initializes default mappings
1078+ * where page IDs equal page numbers (1-indexed).
1079+ * @param {number } n - The total number of pages.
1080+ */
1081+ set pagesNumber ( n ) {
1082+ if ( PagesMapper . #pagesNumber === n ) {
1083+ return ;
1084+ }
1085+ PagesMapper . #pagesNumber = n ;
1086+ if ( n === 0 ) {
1087+ PagesMapper . #pageNumberToId = null ;
1088+ PagesMapper . #idToPageNumber = null ;
1089+ }
1090+ }
1091+
1092+ #init( ) {
1093+ if ( PagesMapper . #pageNumberToId) {
1094+ return ;
1095+ }
1096+ const n = PagesMapper . #pagesNumber;
1097+
1098+ // Allocate a single array for better memory locality.
1099+ const array = new Uint32Array ( 3 * n ) ;
1100+ const pageNumberToId = ( PagesMapper . #pageNumberToId = array . subarray ( 0 , n ) ) ;
1101+ const idToPageNumber = ( PagesMapper . #idToPageNumber = array . subarray (
1102+ n ,
1103+ 2 * n
1104+ ) ) ;
1105+ for ( let i = 0 ; i < n ; i ++ ) {
1106+ pageNumberToId [ i ] = idToPageNumber [ i ] = i + 1 ;
1107+ }
1108+ PagesMapper . #prevIdToPageNumber = array . subarray ( 2 * n ) ;
1109+ }
1110+
1111+ /**
1112+ * Move a set of pages to a new position while keeping ID→number mappings in
1113+ * sync.
1114+ *
1115+ * @param {Set<number> } selectedPages - Page numbers being moved (1-indexed).
1116+ * @param {number[] } pagesToMove - Ordered list of page numbers to move.
1117+ * @param {number } index - Zero-based insertion index in the page-number list.
1118+ */
1119+ movePages ( selectedPages , pagesToMove , index ) {
1120+ this . #init( ) ;
1121+ const pageNumberToId = PagesMapper . #pageNumberToId;
1122+ const idToPageNumber = PagesMapper . #idToPageNumber;
1123+ PagesMapper . #prevIdToPageNumber. set ( idToPageNumber ) ;
1124+ const movedCount = pagesToMove . length ;
1125+ const mappedPagesToMove = new Uint32Array ( movedCount ) ;
1126+ let removedBeforeTarget = 0 ;
1127+
1128+ for ( let i = 0 ; i < movedCount ; i ++ ) {
1129+ const pageIndex = pagesToMove [ i ] - 1 ;
1130+ mappedPagesToMove [ i ] = pageNumberToId [ pageIndex ] ;
1131+ if ( pageIndex < index ) {
1132+ removedBeforeTarget += 1 ;
1133+ }
1134+ }
1135+
1136+ const pagesNumber = PagesMapper . #pagesNumber;
1137+ // target index after removing elements that were before it
1138+ let adjustedTarget = index - removedBeforeTarget ;
1139+ const remainingLen = pagesNumber - movedCount ;
1140+ adjustedTarget = MathClamp ( adjustedTarget , 0 , remainingLen ) ;
1141+
1142+ // Create the new mapping.
1143+ // First copy over the pages that are not being moved.
1144+ // Then insert the moved pages at the target position.
1145+ for ( let i = 0 , r = 0 ; i < pagesNumber ; i ++ ) {
1146+ if ( ! selectedPages . has ( i + 1 ) ) {
1147+ pageNumberToId [ r ++ ] = pageNumberToId [ i ] ;
1148+ }
1149+ }
1150+
1151+ // Shift the pages after the target position.
1152+ pageNumberToId . copyWithin (
1153+ adjustedTarget + movedCount ,
1154+ adjustedTarget ,
1155+ remainingLen
1156+ ) ;
1157+ // Finally insert the moved pages.
1158+ pageNumberToId . set ( mappedPagesToMove , adjustedTarget ) ;
1159+
1160+ for ( let i = 0 , ii = pagesNumber ; i < ii ; i ++ ) {
1161+ idToPageNumber [ pageNumberToId [ i ] - 1 ] = i + 1 ;
1162+ }
1163+ }
1164+
1165+ getNewPageNumber ( pageNumber ) {
1166+ return PagesMapper . #prevIdToPageNumber[
1167+ PagesMapper . #pageNumberToId[ pageNumber - 1 ] - 1
1168+ ] ;
1169+ }
1170+
1171+ /**
1172+ * Gets the page number for a given page ID.
1173+ * @param {number } id - The page ID (1-indexed).
1174+ * @returns {number } The page number, or the ID itself if no mapping exists.
1175+ */
1176+ getPageNumber ( id ) {
1177+ return PagesMapper . #idToPageNumber?. [ id - 1 ] ?? id ;
1178+ }
1179+
1180+ /**
1181+ * Gets the page ID for a given page number.
1182+ * @param {number } pageNumber - The page number (1-indexed).
1183+ * @returns {number } The page ID, or the page number itself if no mapping
1184+ * exists.
1185+ */
1186+ getPageId ( pageNumber ) {
1187+ return PagesMapper . #pageNumberToId?. [ pageNumber - 1 ] ?? pageNumber ;
1188+ }
1189+
1190+ /**
1191+ * Gets or creates a singleton instance of PagesMapper.
1192+ * @returns {PagesMapper } The singleton instance.
1193+ */
1194+ static get instance ( ) {
1195+ return shadow ( this , "instance" , new PagesMapper ( ) ) ;
1196+ }
1197+
1198+ getMapping ( ) {
1199+ return PagesMapper . #pageNumberToId. subarray ( 0 , this . pagesNumber ) ;
1200+ }
1201+ }
1202+
10371203export {
10381204 applyOpacity ,
10391205 ColorScheme ,
@@ -1054,6 +1220,7 @@ export {
10541220 makePathFromDrawOPS ,
10551221 noContextMenu ,
10561222 OutputScale ,
1223+ PagesMapper ,
10571224 PageViewport ,
10581225 PDFDateString ,
10591226 PixelsPerInch ,
0 commit comments