File size: 10,361 Bytes
02dc410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import base64
import re
import io
from PIL import Image, ImageDraw, ImageFont

class ReadmeProcessor:
    def __init__(self):
        """
        Initializes the ReadmeProcessor.
        """
        self.image_frames_base64 = []
        # Try to load a default font for annotation, fall back to a generic one if not found
        try:
            self.font = ImageFont.truetype("arial.ttf", 24) # Increased font size for 840x840 image
        except IOError:
            self.font = ImageFont.load_default()
        self.text_color = (255, 0, 0) # Red color for text (R, G, B)
        self.target_image_size = (240, 240) # New fixed size for all images

    def _parse_image_references(self, text: str) -> list[int]:
        """
        Parses a given text to find all image reference blocks like '[Image X, Y, Z]'
        or '[Image A]' and returns a flat, sorted list of unique 0-indexed image numbers.
        Duplicate image references within the same paragraph (across multiple blocks or within one)
        are handled. The list is sorted by the original image index to ensure "12345..." order.

        Args:
            text: The paragraph text to parse.

        Returns:
            A sorted list of unique 0-indexed integers representing image numbers.
            Returns an empty list if no valid references are found.
        """
        unique_image_indices = set()
        matches = re.findall(r'\[Image\s*((?:\d+\s*(?:,\s*\d+\s*)*))\]', text)
        for match_str in matches:
            for num_str in match_str.split(','):
                try:
                    img_num = int(num_str.strip()) - 1 # Adjust to 0-indexed
                    if 0 <= img_num < len(self.image_frames_base64):
                        unique_image_indices.add(img_num)
                    else:
                        print(f"Warning: Image index {img_num + 1} out of bounds. Skipping in block '{match_str}'.")
                except ValueError:
                    print(f"Warning: Could not parse image number from '{num_str.strip()}' in block '{match_str}'. Skipping.")

        # Return a sorted list of unique indices to maintain order (1, 2, 3, ...)
        return sorted(list(unique_image_indices))

    def _process_single_image_for_display(self, image_index: int) -> str | None:
        """
        Loads an image, resizes it to the target_image_size, annotates it with its
        1-indexed display number, and returns its base64 encoding.

        Args:
            image_index: The 0-indexed position of the image in self.image_frames_base64.

        Returns:
            A base64 encoded string of the resized and annotated image in PNG format,
            or None if an error occurs.
        """
        try:
            img_data_b64 = self.image_frames_base64[image_index]
            img = Image.open(io.BytesIO(base64.b64decode(img_data_b64))).convert("RGB")

            # Resize the image to the fixed target size (840x840)
            img = img.resize(self.target_image_size, Image.LANCZOS)

            draw = ImageDraw.Draw(img)
            # Use 1-indexed number for display
            text_to_draw = f"{image_index + 1}" 
            
            # Get text bounding box for precise positioning
            try:
                # Use textbbox for PIL 9.2.0+
                bbox = draw.textbbox((0, 0), text_to_draw, font=self.font)
                text_width = bbox[2] - bbox[0]
                text_height = bbox[3] - bbox[1]
            except AttributeError:
                # Fallback for older PIL versions (less accurate)
                text_width, text_height = draw.textsize(text_to_draw, font=self.font)

            # Position text at the top-left corner with a small padding
            padding = 10 # Increased padding for larger image
            text_x = padding
            text_y = padding
            
            draw.text((text_x, text_y), text_to_draw, font=self.font, fill=self.text_color)
            
            buffered = io.BytesIO()
            img.save(buffered, format="PNG")
            return base64.b64encode(buffered.getvalue()).decode('utf-8')

        except (IndexError, TypeError, ValueError, IOError) as e:
            print(f"Error processing image at index {image_index} for annotation: {e}. Skipping.")
            return None

    def _get_image_markdown_tag(self, base64_image_data: str, alt_text: str = "Annotated Image") -> str:
        """
        Generates a Markdown image tag from base64 data.
        """
        return f"![{alt_text}](data:image/png;base64,{base64_image_data})"


    def process_readme(self, readme_text: str, image_frames_base64: list[str]) -> str:
        """
        Processes the input README text, finds unique image references in each paragraph,
        resizes and annotates each image, and embeds them as individual Markdown images
        below the respective paragraph. Images are ordered by their original index,
        and displayed with a maximum of two images per line.

        Args:
            readme_text: The full README text content.
            image_frames_base64: A list of base64 encoded strings, representing image frames.

        Returns:
            The processed README text with embedded images.
        """
        processed_lines = []
        self.image_frames_base64 = image_frames_base64
        
        paragraphs = readme_text.split('\n\n')

        for paragraph in paragraphs:
            processed_lines.append(paragraph) # Add the original paragraph

            # Get all unique, sorted image indices for this paragraph
            unique_image_indices = self._parse_image_references(paragraph)

            if unique_image_indices:
                image_tags_for_paragraph = []
                for img_idx in unique_image_indices:
                    annotated_b64 = self._process_single_image_for_display(img_idx)
                    if annotated_b64:
                        image_tags_for_paragraph.append(self._get_image_markdown_tag(annotated_b64, f"Image {img_idx + 1}"))
                    else:
                        print(f"Could not process image {img_idx + 1} for paragraph: '{paragraph[:50]}...'")

                # Arrange image tags with max 2 per line
                if image_tags_for_paragraph:
                    processed_lines.append("\n") # Add a newline before image block
                    for i in range(0, len(image_tags_for_paragraph), 2):
                        row_images = image_tags_for_paragraph[i:i+2]
                        # Join with a space to place images side-by-side in Markdown
                        
                        #comment
                        # processed_lines.append(" ".join(row_images)) 
                        # Add an empty string (newline) after each row of images,
                        # to ensure the next row or paragraph starts on a new line.
                        # This avoids issues if the last row only has one image.
                        processed_lines.append("") 
            
            # Ensure there's always an empty line after a paragraph block
            # This handles cases where there were no images for the paragraph
            # or if the last image row already added a newline.
            if processed_lines and processed_lines[-1] != "":
                processed_lines.append("")

        return "\n".join(processed_lines).strip()

# --- Example Usage ---
if __name__ == "__main__":
    # Create some dummy base64 image data for demonstration
    def create_dummy_image_b64(width, height, color):
        img = Image.new('RGB', (width, height), color)
        buffered = io.BytesIO()
        img.save(buffered, format="PNG")
        return base64.b64encode(buffered.getvalue()).decode('utf-8')

    # Simulate 6 image frames with varying original sizes (they will all be resized to 840x840)
    dummy_frames = [
        create_dummy_image_b64(150, 80, (255, 200, 200)), # Light Red
        create_dummy_image_b64(220, 160, (200, 255, 200)), # Light Green
        create_dummy_image_b64(180, 140, (200, 200, 255)), # Light Blue
        create_dummy_image_b64(250, 170, (255, 255, 200)), # Light Yellow
        create_dummy_image_b64(190, 155, (200, 255, 255)), # Light Cyan
        create_dummy_image_b64(210, 145, (255, 200, 255))  # Light Magenta
    ]

    # Your example README text
    readme_content = """
The earliest verifiable publication of this video is on November 13, 2023. The video was posted to the Telegram channel @SerajSat at 11:03 AM [0]. This represents the first known instance of the video's circulation based on the provided sources.

The visual information within the keyframes shows a daytime scene with diffuse lighting, likely due to heavy smoke, dust, or fog, which obscures clear shadows for a more precise time-of-day analysis [Image 1, 1, 2, 6]. The presence of numerous Palestine Red Crescent Society (PRCS) ambulances gathered outside a building suggests an emergency situation [Image 3].

Given that the video was published on November 13, 2023, it is highly probable that the event depicted was filmed on or very shortly before that date and time. This conclusion is further supported by additional visual cues [Image 4, 5].
"""

    # Initialize the processor with dummy frames
    processor = ReadmeProcessor()

    # Process the README content
    processed_readme = processor.process_readme(readme_content, dummy_frames)

    # Print the result (this would typically be saved to a .md file or displayed)
    print("--- Processed README Content ---")
    print(processed_readme)

    # Save the processed README content to a file
    with open("processed_readme.md", "w") as f:
        f.write(processed_readme)

    # Example with no image references
    print("\n--- Processed README Content (No Images) ---")
    no_image_readme = "This paragraph has no images. Another paragraph follows."
    processed_no_image_readme = processor.process_readme(no_image_readme, dummy_frames)
    print(processed_no_image_readme)

    # Example with out-of-bounds image reference
    print("\n--- Processed README Content (Out-of-bounds Image) ---")
    out_of_bounds_readme = "This paragraph references an image that doesn't exist [Image 99]."
    processed_out_of_bounds_readme = processor.process_readme(out_of_bounds_readme, dummy_frames)
    print(processed_out_of_bounds_readme)