natural-pdf 0.1.0__py3-none-any.whl

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 (52) hide show
  1. natural_pdf/__init__.py +55 -0
  2. natural_pdf/analyzers/__init__.py +6 -0
  3. natural_pdf/analyzers/layout/__init__.py +1 -0
  4. natural_pdf/analyzers/layout/base.py +151 -0
  5. natural_pdf/analyzers/layout/docling.py +247 -0
  6. natural_pdf/analyzers/layout/layout_analyzer.py +166 -0
  7. natural_pdf/analyzers/layout/layout_manager.py +200 -0
  8. natural_pdf/analyzers/layout/layout_options.py +78 -0
  9. natural_pdf/analyzers/layout/paddle.py +240 -0
  10. natural_pdf/analyzers/layout/surya.py +151 -0
  11. natural_pdf/analyzers/layout/tatr.py +251 -0
  12. natural_pdf/analyzers/layout/yolo.py +165 -0
  13. natural_pdf/analyzers/text_options.py +60 -0
  14. natural_pdf/analyzers/text_structure.py +270 -0
  15. natural_pdf/analyzers/utils.py +57 -0
  16. natural_pdf/core/__init__.py +3 -0
  17. natural_pdf/core/element_manager.py +457 -0
  18. natural_pdf/core/highlighting_service.py +698 -0
  19. natural_pdf/core/page.py +1444 -0
  20. natural_pdf/core/pdf.py +653 -0
  21. natural_pdf/elements/__init__.py +3 -0
  22. natural_pdf/elements/base.py +761 -0
  23. natural_pdf/elements/collections.py +1345 -0
  24. natural_pdf/elements/line.py +140 -0
  25. natural_pdf/elements/rect.py +122 -0
  26. natural_pdf/elements/region.py +1793 -0
  27. natural_pdf/elements/text.py +304 -0
  28. natural_pdf/ocr/__init__.py +56 -0
  29. natural_pdf/ocr/engine.py +104 -0
  30. natural_pdf/ocr/engine_easyocr.py +179 -0
  31. natural_pdf/ocr/engine_paddle.py +204 -0
  32. natural_pdf/ocr/engine_surya.py +171 -0
  33. natural_pdf/ocr/ocr_manager.py +191 -0
  34. natural_pdf/ocr/ocr_options.py +114 -0
  35. natural_pdf/qa/__init__.py +3 -0
  36. natural_pdf/qa/document_qa.py +396 -0
  37. natural_pdf/selectors/__init__.py +4 -0
  38. natural_pdf/selectors/parser.py +354 -0
  39. natural_pdf/templates/__init__.py +1 -0
  40. natural_pdf/templates/ocr_debug.html +517 -0
  41. natural_pdf/utils/__init__.py +3 -0
  42. natural_pdf/utils/highlighting.py +12 -0
  43. natural_pdf/utils/reading_order.py +227 -0
  44. natural_pdf/utils/visualization.py +223 -0
  45. natural_pdf/widgets/__init__.py +4 -0
  46. natural_pdf/widgets/frontend/viewer.js +88 -0
  47. natural_pdf/widgets/viewer.py +765 -0
  48. natural_pdf-0.1.0.dist-info/METADATA +295 -0
  49. natural_pdf-0.1.0.dist-info/RECORD +52 -0
  50. natural_pdf-0.1.0.dist-info/WHEEL +5 -0
  51. natural_pdf-0.1.0.dist-info/licenses/LICENSE +21 -0
  52. natural_pdf-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,227 @@
1
+ """
2
+ Reading order utilities for natural-pdf.
3
+ """
4
+ from typing import List, Dict, Any, Callable, Optional
5
+
6
+
7
+ def establish_reading_order(elements: List[Dict[str, Any]],
8
+ algorithm: str = 'basic') -> List[Dict[str, Any]]:
9
+ """
10
+ Establish reading order for a collection of elements.
11
+
12
+ Args:
13
+ elements: List of elements to order
14
+ algorithm: Algorithm to use ('basic', 'column', 'complex')
15
+
16
+ Returns:
17
+ List of elements in reading order
18
+ """
19
+ if algorithm == 'basic':
20
+ return _basic_reading_order(elements)
21
+ elif algorithm == 'column':
22
+ return _column_reading_order(elements)
23
+ elif algorithm == 'complex':
24
+ return _complex_reading_order(elements)
25
+ else:
26
+ # Default to basic
27
+ return _basic_reading_order(elements)
28
+
29
+
30
+ def _basic_reading_order(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
31
+ """
32
+ Basic top-to-bottom, left-to-right reading order.
33
+
34
+ Args:
35
+ elements: List of elements to order
36
+
37
+ Returns:
38
+ List of elements in reading order
39
+ """
40
+ # Simple sort by y0 (top), then by x0 (left)
41
+ return sorted(elements, key=lambda e: (
42
+ e.get('top', e.get('y0', 0)),
43
+ e.get('x0', 0)
44
+ ))
45
+
46
+
47
+ def _column_reading_order(elements: List[Dict[str, Any]],
48
+ column_threshold: float = 0.2,
49
+ x_tolerance: float = 10.0) -> List[Dict[str, Any]]:
50
+ """
51
+ Reading order that accounts for columns.
52
+
53
+ This is more complex as it needs to detect columns first,
54
+ then read each column in order.
55
+
56
+ Args:
57
+ elements: List of elements to order
58
+ column_threshold: Percentage overlap threshold for column detection (0.0 to 1.0)
59
+ x_tolerance: Horizontal tolerance for determining column edges
60
+
61
+ Returns:
62
+ List of elements in reading order
63
+ """
64
+ if not elements:
65
+ return []
66
+
67
+ # 1. Group elements by line
68
+ lines = group_elements_by_line(elements)
69
+
70
+ # 2. For each line, find the x-coordinate ranges (potential column boundaries)
71
+ line_x_ranges = []
72
+ for line in lines:
73
+ for el in line:
74
+ x0 = el.get('x0', 0)
75
+ x1 = el.get('x1', 0)
76
+ line_x_ranges.append((x0, x1))
77
+
78
+ # If we don't have enough ranges to detect columns, just use basic ordering
79
+ if len(line_x_ranges) < 3:
80
+ return _basic_reading_order(elements)
81
+
82
+ # 3. Detect columns by clustering x-coordinate ranges
83
+ def overlaps(range1, range2, threshold=column_threshold):
84
+ """Determine if two ranges overlap by more than threshold percentage."""
85
+ # Calculate overlap
86
+ overlap_start = max(range1[0], range2[0])
87
+ overlap_end = min(range1[1], range2[1])
88
+ overlap = max(0, overlap_end - overlap_start)
89
+
90
+ # Calculate lengths
91
+ len1 = range1[1] - range1[0]
92
+ len2 = range2[1] - range2[0]
93
+
94
+ # Calculate overlap as percentage of the shorter range
95
+ shorter_len = min(len1, len2)
96
+ if shorter_len == 0:
97
+ return False
98
+
99
+ return overlap / shorter_len >= threshold
100
+
101
+ # Cluster x-ranges into columns
102
+ columns = []
103
+ for x_range in line_x_ranges:
104
+ # Skip zero-width ranges
105
+ if x_range[1] - x_range[0] <= 0:
106
+ continue
107
+
108
+ # Try to find an existing column to add to
109
+ added = False
110
+ for col in columns:
111
+ if any(overlaps(x_range, r) for r in col):
112
+ col.append(x_range)
113
+ added = True
114
+ break
115
+
116
+ # If not added to an existing column, create a new one
117
+ if not added:
118
+ columns.append([x_range])
119
+
120
+ # 4. Get column boundaries by averaging x-ranges in each column
121
+ column_bounds = []
122
+ for col in columns:
123
+ left = sum(r[0] for r in col) / len(col)
124
+ right = sum(r[1] for r in col) / len(col)
125
+ column_bounds.append((left, right))
126
+
127
+ # Sort columns by x-coordinate (left to right)
128
+ column_bounds.sort(key=lambda b: b[0])
129
+
130
+ # 5. Assign each element to a column
131
+ element_columns = {}
132
+ for el in elements:
133
+ # Get element x-coordinates
134
+ el_x0 = el.get('x0', 0)
135
+ el_x1 = el.get('x1', 0)
136
+ el_center = (el_x0 + el_x1) / 2
137
+
138
+ # Find the column this element belongs to
139
+ for i, (left, right) in enumerate(column_bounds):
140
+ # Extend bounds by tolerance
141
+ extended_left = left - x_tolerance
142
+ extended_right = right + x_tolerance
143
+
144
+ # Check if center point is within extended column bounds
145
+ if extended_left <= el_center <= extended_right:
146
+ element_columns[el] = i
147
+ break
148
+ else:
149
+ # If no column found, assign to nearest column
150
+ distances = [(i, min(abs(el_center - left), abs(el_center - right)))
151
+ for i, (left, right) in enumerate(column_bounds)]
152
+ nearest_col = min(distances, key=lambda d: d[1])[0]
153
+ element_columns[el] = nearest_col
154
+
155
+ # 6. Sort elements by column, then by vertical position
156
+ sorted_elements = []
157
+ for col_idx, _ in enumerate(column_bounds):
158
+ # Get elements in this column
159
+ col_elements = [el for el in elements if element_columns.get(el) == col_idx]
160
+ # Sort by top coordinate
161
+ col_elements.sort(key=lambda e: e.get('top', e.get('y0', 0)))
162
+ # Add to final list
163
+ sorted_elements.extend(col_elements)
164
+
165
+ return sorted_elements
166
+
167
+
168
+ def _complex_reading_order(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
169
+ """
170
+ Complex reading order that accounts for various document structures.
171
+
172
+ This considers columns, text flow around images, tables, etc.
173
+
174
+ Args:
175
+ elements: List of elements to order
176
+
177
+ Returns:
178
+ List of elements in reading order
179
+ """
180
+ # TODO: Implement complex layout analysis
181
+ # For now, fall back to column-aware reading order
182
+ return _column_reading_order(elements)
183
+
184
+
185
+ def group_elements_by_line(elements: List[Dict[str, Any]],
186
+ tolerance: float = 3.0) -> List[List[Dict[str, Any]]]:
187
+ """
188
+ Group elements into lines based on vertical position.
189
+
190
+ Args:
191
+ elements: List of elements to group
192
+ tolerance: Maximum vertical distance for elements to be considered on the same line
193
+
194
+ Returns:
195
+ List of lists, where each sublist contains elements on the same line
196
+ """
197
+ if not elements:
198
+ return []
199
+
200
+ # Sort by top coordinate
201
+ sorted_elements = sorted(elements, key=lambda e: e.get('top', e.get('y0', 0)))
202
+
203
+ lines = []
204
+ current_line = [sorted_elements[0]]
205
+ current_top = sorted_elements[0].get('top', sorted_elements[0].get('y0', 0))
206
+
207
+ for element in sorted_elements[1:]:
208
+ element_top = element.get('top', element.get('y0', 0))
209
+
210
+ # If element is close enough to current line's top, add to current line
211
+ if abs(element_top - current_top) <= tolerance:
212
+ current_line.append(element)
213
+ else:
214
+ # Otherwise, start a new line
215
+ lines.append(current_line)
216
+ current_line = [element]
217
+ current_top = element_top
218
+
219
+ # Add the last line
220
+ if current_line:
221
+ lines.append(current_line)
222
+
223
+ # Sort elements within each line by x0
224
+ for line in lines:
225
+ line.sort(key=lambda e: e.get('x0', 0))
226
+
227
+ return lines
@@ -0,0 +1,223 @@
1
+ """
2
+ Visualization utilities for natural-pdf.
3
+ """
4
+ from typing import List, Dict, Tuple, Optional, Union, Any, Set
5
+ import io
6
+ import math
7
+ import random
8
+ import itertools # Added for cycling
9
+ from PIL import Image, ImageDraw, ImageFont
10
+
11
+ # Define a base list of visually distinct colors for highlighting
12
+ # Format: (R, G, B)
13
+ _BASE_HIGHLIGHT_COLORS = [
14
+ (255, 0, 0), # Red
15
+ (0, 255, 0), # Green
16
+ (0, 0, 255), # Blue
17
+ (255, 0, 255), # Magenta
18
+ (0, 255, 255), # Cyan
19
+ (255, 165, 0), # Orange
20
+ (128, 0, 128), # Purple
21
+ (0, 128, 0), # Dark Green
22
+ (0, 0, 128), # Navy
23
+ (255, 215, 0), # Gold
24
+ (75, 0, 130), # Indigo
25
+ (240, 128, 128), # Light Coral
26
+ (32, 178, 170), # Light Sea Green
27
+ (138, 43, 226), # Blue Violet
28
+ (160, 82, 45), # Sienna
29
+ ]
30
+
31
+ # Default Alpha for highlight fills
32
+ DEFAULT_FILL_ALPHA = 100
33
+
34
+ class ColorManager:
35
+ """
36
+ Manages color assignment for highlights, ensuring consistency for labels.
37
+ """
38
+ def __init__(self, alpha: int = DEFAULT_FILL_ALPHA):
39
+ """
40
+ Initializes the ColorManager.
41
+
42
+ Args:
43
+ alpha (int): The default alpha transparency (0-255) for highlight fills.
44
+ """
45
+ self._alpha = alpha
46
+ # Shuffle the base colors to avoid the same sequence every time
47
+ self._available_colors = random.sample(_BASE_HIGHLIGHT_COLORS, len(_BASE_HIGHLIGHT_COLORS))
48
+ self._color_cycle = itertools.cycle(self._available_colors)
49
+ self._labels_colors: Dict[str, Tuple[int, int, int, int]] = {}
50
+
51
+ def _get_rgba_color(self, rgb: Tuple[int, int, int]) -> Tuple[int, int, int, int]:
52
+ """Applies the instance's alpha to an RGB tuple."""
53
+ return (*rgb, self._alpha)
54
+
55
+ def get_color(self, label: Optional[str] = None,
56
+ force_cycle: bool = False) -> Tuple[int, int, int, int]:
57
+ """
58
+ Gets an RGBA color tuple.
59
+
60
+ If a label is provided, it returns a consistent color for that label.
61
+ If no label is provided, it cycles through the available colors (unless force_cycle=False).
62
+ If force_cycle is True, it always returns the next color in the cycle, ignoring the label.
63
+
64
+ Args:
65
+ label (Optional[str]): The label associated with the highlight.
66
+ force_cycle (bool): If True, ignore the label and always get the next cycle color.
67
+
68
+ Returns:
69
+ Tuple[int, int, int, int]: An RGBA color tuple (0-255).
70
+ """
71
+ if force_cycle:
72
+ # Always get the next color, don't store by label
73
+ rgb = next(self._color_cycle)
74
+ return self._get_rgba_color(rgb)
75
+
76
+ if label is not None:
77
+ if label in self._labels_colors:
78
+ # Return existing color for this label
79
+ return self._labels_colors[label]
80
+ else:
81
+ # New label, get next color and store it
82
+ rgb = next(self._color_cycle)
83
+ rgba = self._get_rgba_color(rgb)
84
+ self._labels_colors[label] = rgba
85
+ return rgba
86
+ else:
87
+ # No label and not forced cycle - get next color from cycle
88
+ rgb = next(self._color_cycle)
89
+ return self._get_rgba_color(rgb)
90
+
91
+ def get_label_colors(self) -> Dict[str, Tuple[int, int, int, int]]:
92
+ """Returns the current mapping of labels to colors."""
93
+ return self._labels_colors.copy()
94
+
95
+ def reset(self) -> None:
96
+ """Resets the color cycle and clears the label-to-color mapping."""
97
+ # Re-shuffle and reset the cycle
98
+ self._available_colors = random.sample(_BASE_HIGHLIGHT_COLORS, len(_BASE_HIGHLIGHT_COLORS))
99
+ self._color_cycle = itertools.cycle(self._available_colors)
100
+ self._labels_colors = {}
101
+
102
+ # --- Global color state and functions removed ---
103
+ # HIGHLIGHT_COLORS, _color_cycle, _current_labels_colors, _used_colors_iterator
104
+ # get_next_highlight_color(), reset_highlight_colors()
105
+
106
+ def create_legend(labels_colors: Dict[str, Tuple[int, int, int, int]],
107
+ width: int = 200,
108
+ item_height: int = 30) -> Image.Image:
109
+ """
110
+ Create a legend image for the highlighted elements.
111
+
112
+ Args:
113
+ labels_colors: Dictionary mapping labels to colors
114
+ width: Width of the legend image
115
+ item_height: Height of each legend item
116
+
117
+ Returns:
118
+ PIL Image with the legend
119
+ """
120
+ # Calculate the height based on the number of labels
121
+ height = len(labels_colors) * item_height + 10 # 10px padding
122
+
123
+ # Create a white image
124
+ legend = Image.new('RGBA', (width, height), (255, 255, 255, 255))
125
+ draw = ImageDraw.Draw(legend)
126
+
127
+ # Try to load a font, use default if not available
128
+ try:
129
+ # Use a commonly available font, adjust size
130
+ font = ImageFont.truetype("DejaVuSans.ttf", 12)
131
+ except IOError:
132
+ try:
133
+ font = ImageFont.truetype("Arial.ttf", 12)
134
+ except IOError:
135
+ font = ImageFont.load_default()
136
+
137
+ # Draw each legend item
138
+ y = 5 # Start with 5px padding
139
+ for label, color in labels_colors.items():
140
+ # Get the color components
141
+ # Handle potential case where alpha isn't provided (use default 255)
142
+ if len(color) == 3:
143
+ r, g, b = color
144
+ alpha = 255 # Assume opaque if alpha is missing
145
+ else:
146
+ r, g, b, alpha = color
147
+
148
+ # Calculate the apparent color when drawn on white background
149
+ # Alpha blending formula: result = (source * alpha) + (dest * (1-alpha))
150
+ # Where alpha is normalized to 0-1 range
151
+ alpha_norm = alpha / 255.0
152
+ apparent_r = int(r * alpha_norm + 255 * (1 - alpha_norm))
153
+ apparent_g = int(g * alpha_norm + 255 * (1 - alpha_norm))
154
+ apparent_b = int(b * alpha_norm + 255 * (1 - alpha_norm))
155
+
156
+ # Use solid color that matches the apparent color of the semi-transparent highlight
157
+ legend_color = (apparent_r, apparent_g, apparent_b, 255)
158
+
159
+ # Draw the color box
160
+ draw.rectangle([(10, y), (30, y + item_height - 5)], fill=legend_color)
161
+
162
+ # Draw the label text
163
+ draw.text((40, y + (item_height // 2) - 6), label, fill=(0, 0, 0, 255), font=font)
164
+
165
+ # Move to the next position
166
+ y += item_height
167
+
168
+ return legend
169
+
170
+ def merge_images_with_legend(image: Image.Image,
171
+ legend: Image.Image,
172
+ position: str = 'right') -> Image.Image:
173
+ """
174
+ Merge an image with a legend.
175
+
176
+ Args:
177
+ image: Main image
178
+ legend: Legend image
179
+ position: Position of the legend ('right', 'bottom', 'top', 'left')
180
+
181
+ Returns:
182
+ Merged image
183
+ """
184
+ if not legend:
185
+ return image # Return original image if legend is None or empty
186
+
187
+ # Determine background color from top-left pixel (safer than assuming white)
188
+ bg_color = image.getpixel((0,0)) if image.mode == 'RGBA' else (255, 255, 255, 255)
189
+
190
+ if position == 'right':
191
+ # Create a new image with extra width for the legend
192
+ merged_width = image.width + legend.width
193
+ merged_height = max(image.height, legend.height)
194
+ merged = Image.new('RGBA', (merged_width, merged_height), bg_color)
195
+ merged.paste(image, (0, 0))
196
+ merged.paste(legend, (image.width, 0), legend if legend.mode == 'RGBA' else None) # Handle transparency
197
+ elif position == 'bottom':
198
+ # Create a new image with extra height for the legend
199
+ merged_width = max(image.width, legend.width)
200
+ merged_height = image.height + legend.height
201
+ merged = Image.new('RGBA', (merged_width, merged_height), bg_color)
202
+ merged.paste(image, (0, 0))
203
+ merged.paste(legend, (0, image.height), legend if legend.mode == 'RGBA' else None)
204
+ elif position == 'top':
205
+ # Create a new image with extra height for the legend
206
+ merged_width = max(image.width, legend.width)
207
+ merged_height = image.height + legend.height
208
+ merged = Image.new('RGBA', (merged_width, merged_height), bg_color)
209
+ merged.paste(legend, (0, 0), legend if legend.mode == 'RGBA' else None)
210
+ merged.paste(image, (0, legend.height))
211
+ elif position == 'left':
212
+ # Create a new image with extra width for the legend
213
+ merged_width = image.width + legend.width
214
+ merged_height = max(image.height, legend.height)
215
+ merged = Image.new('RGBA', (merged_width, merged_height), bg_color)
216
+ merged.paste(legend, (0, 0), legend if legend.mode == 'RGBA' else None)
217
+ merged.paste(image, (legend.width, 0))
218
+ else:
219
+ # Invalid position, return the original image
220
+ print(f"Warning: Invalid legend position '{position}'. Returning original image.")
221
+ merged = image
222
+
223
+ return merged
@@ -0,0 +1,4 @@
1
+ from .viewer import SimpleInteractiveViewerWidget as InteractiveViewerWidget
2
+
3
+ # Also provide the original implementation for reference
4
+ from .viewer import InteractiveViewerWidget as _OriginalInteractiveViewerWidget
@@ -0,0 +1,88 @@
1
+ // natural_pdf/widgets/frontend/viewer.js
2
+ // Minimal version for debugging module loading
3
+
4
+ (function() {
5
+ // Use a flag to prevent multiple definitions if this script runs multiple times
6
+ if (window.interactiveViewerWidgetDefined) {
7
+ console.log("[DEBUG] viewer_widget already defined. Skipping re-definition.");
8
+ // If it was already defined, maybe trigger a manual load if require is available?
9
+ // This is tricky because the initial load might have failed partially.
10
+ if (typeof require !== 'undefined') {
11
+ console.log("[DEBUG] Attempting require(['viewer_widget'])...");
12
+ try {
13
+ require(['viewer_widget'], function(module) {
14
+ console.log("[DEBUG] Manual require succeeded:", module);
15
+ }, function(err) {
16
+ console.error("[DEBUG] Manual require failed:", err);
17
+ });
18
+ } catch (e) {
19
+ console.error("[DEBUG] Error during manual require:", e);
20
+ }
21
+ }
22
+ return;
23
+ }
24
+ window.interactiveViewerWidgetDefined = true;
25
+ console.log("[DEBUG] Defining viewer_widget module for the first time...");
26
+
27
+ // Check for requirejs *after* setting the flag, before defining
28
+ if (typeof requirejs === 'undefined') {
29
+ console.error('[DEBUG] requirejs is still not defined. Widget frontend cannot load.');
30
+ // Maybe display an error in the widget area itself?
31
+ // This suggests a fundamental issue with the Jupyter environment setup.
32
+ return;
33
+ }
34
+ if (typeof define !== 'function' || !define.amd) {
35
+ console.error('[DEBUG] define is not a function or define.amd is missing. Cannot define module.');
36
+ return;
37
+ }
38
+
39
+ // Clear any previous potentially failed definition
40
+ require.undef('viewer_widget');
41
+
42
+ // Define the module
43
+ define('viewer_widget', ['@jupyter-widgets/base'], function(widgets) {
44
+ console.log("[DEBUG] viewer_widget define callback executed.");
45
+ console.log("[DEBUG] @jupyter-widgets/base loaded:", widgets);
46
+
47
+ // Define a very simple view class
48
+ class InteractiveViewerView extends widgets.DOMWidgetView {
49
+ render() {
50
+ console.log("[DEBUG] InteractiveViewerView: render() called.");
51
+ this.el.textContent = 'Minimal Widget Loaded!'; // Simple text content
52
+ this.el.style.border = '2px solid green';
53
+ this.el.style.padding = '10px';
54
+
55
+ // Log received data
56
+ this.model.on('change:image_uri', () => console.log("[DEBUG] image_uri changed:", this.model.get('image_uri') ? 'Present' : 'Empty'), this);
57
+ this.model.on('change:page_dimensions', () => console.log("[DEBUG] page_dimensions changed:", this.model.get('page_dimensions')), this);
58
+ this.model.on('change:elements', () => console.log("[DEBUG] elements changed:", this.model.get('elements').length), this);
59
+
60
+ // Log initial data
61
+ console.log("[DEBUG] Initial image_uri:", this.model.get('image_uri') ? 'Present' : 'Empty');
62
+ console.log("[DEBUG] Initial page_dimensions:", this.model.get('page_dimensions'));
63
+ console.log("[DEBUG] Initial elements count:", this.model.get('elements').length);
64
+ }
65
+
66
+ remove() {
67
+ console.log("[DEBUG] InteractiveViewerView: remove() called.");
68
+ super.remove();
69
+ }
70
+ }
71
+
72
+ console.log("[DEBUG] viewer_widget module definition returning view.");
73
+ // Return the view class
74
+ return {
75
+ InteractiveViewerView: InteractiveViewerView
76
+ };
77
+ }, function(err) {
78
+ // Error callback for the define function
79
+ console.error("[DEBUG] Error loading module dependencies:", err);
80
+ const failedId = err.requireModules && err.requireModules[0];
81
+ if (failedId === 'react' || failedId === 'react-dom' || failedId === 'htm') {
82
+ console.error(`[DEBUG] Failed to load CDN dependency: ${failedId}. Check network connection and CDN availability.`);
83
+ } else if (failedId === '@jupyter-widgets/base') {
84
+ console.error("[DEBUG] Failed to load @jupyter-widgets/base. Ensure ipywidgets frontend is installed and enabled.");
85
+ }
86
+ });
87
+
88
+ })();