@@ -5,6 +5,7 @@ package cmd
55
66import (
77 "bytes"
8+ "encoding/base64"
89 "fmt"
910 "image"
1011 "image/color"
@@ -63,7 +64,10 @@ Supports interactive mode for pages requiring manual interaction (e.g., CAPTCHA,
6364 // 3. Create Base Report Directory (Common)
6465 reportID := cfg .ReportID
6566 if reportID == "" {
66- reportID = fmt .Sprintf ("report-%s" , time .Now ().Format ("20060102-150405" ))
67+ // User requested Base64 representation of the date value
68+ ts := time .Now ().Format ("20060102-150405" )
69+ encoded := base64 .RawURLEncoding .EncodeToString ([]byte (ts ))
70+ reportID = fmt .Sprintf ("report-%s" , encoded )
6771 }
6872 if err := os .MkdirAll (reportID , 0755 ); err != nil {
6973 log .Fatalf ("Failed to create report directory: %v" , err )
@@ -242,108 +246,137 @@ func saveScreenshot(data []byte, targetURL string, userAgent string, outputDir s
242246}
243247
244248func addHeaderToImage (baseImg image.Image , info * utils.AccessInfo , TargetURL * string , userAgent string ) (image.Image , error ) {
249+ // Fixed width for standard output
250+ const fixedWidth = 1920
251+
245252 bounds := baseImg .Bounds ()
246- width := bounds .Dx ()
247- height := bounds .Dy ()
253+ screenshotWidth := bounds .Dx ()
254+ screenshotHeight := bounds .Dy ()
255+
256+ // Create final image with fixed width.
257+ // The height will be determined later, but width is always 1920.
258+ // We need to calculate text wrapping based on 1920px.
248259
249- // Calculate available width for text (with margins)
250260 textMarginX := 10
251- availableWidth := width - (textMarginX * 2 )
261+ availableWidth := fixedWidth - (textMarginX * 2 )
252262 charWidth := 8 // inconsolata.Regular8x16 character width
253263
254- // Wrap User-Agent to fit width without truncation
264+ // Wrap User-Agent to fit fixed width
255265 maxCharsPerLine := availableWidth / charWidth
256266 if maxCharsPerLine < 20 {
257267 maxCharsPerLine = 20
258- } // Safety
268+ }
269+
270+ // We can use simple wrapping now because we have plenty of space (1920px)
259271 uaLines := wrapText (userAgent , maxCharsPerLine )
260272
261273 now := info .Timestamp .UTC ().Format (time .RFC3339 )
262274
263- // Parse and format URL with Punycode/IDN handling
275+ // URL parsing
264276 targetURL := * TargetURL
265277 parsedURL , err := url .Parse (targetURL )
266278 if err != nil {
267279 log .Warnf ("Failed to parse target URL: %v" , err )
268280 parsedURL = & url.URL {Host : "invalid-url" }
269281 }
270-
271- // Convert host to Punycode (ASCII) form for display
272282 punycodeHost := parsedURL .Host
273283 if parsedURL .Host != "" {
274- asciiHost , err := idna .ToASCII (parsedURL .Host )
275- if err != nil {
276- log .Warnf ("IDNA ToASCII conversion failed for host %s: %v" , parsedURL .Host , err )
277- } else {
284+ if asciiHost , err := idna .ToASCII (parsedURL .Host ); err == nil {
278285 punycodeHost = asciiHost
279286 }
280287 }
281-
282- // Build origin (scheme + host + port in Punycode)
283288 origin := fmt .Sprintf ("%s://%s" , parsedURL .Scheme , punycodeHost )
284-
285- // Build full URL with Punycode host
286289 fullURLCopy := * parsedURL
287290 fullURLCopy .Host = punycodeHost
288291 fullURL := fullURLCopy .String ()
289292
290- // Truncate URLs if needed
291- maxOriginLen := maxCharsPerLine - 14 // "Origin: " length
293+ // Truncate URLs (using fixed width context, so usually no truncation needed)
294+ maxOriginLen := maxCharsPerLine - 14
292295 maxFullURLLen := maxCharsPerLine - 14
293296 truncatedOrigin := truncateString (origin , maxOriginLen )
294297 truncatedFullURL := truncateString (fullURL , maxFullURLLen )
295298
296299 // Build lines array
297300 lines := []string {
298301 fmt .Sprintf ("Access Information (down-force demo) - %s" , now ),
299- "--------------------------------------------------------------------------------" ,
302+ string ( bytes . Repeat ([] byte ( "-" ), maxCharsPerLine )), // Dynamic separator for 1920px
300303 fmt .Sprintf ("Origin: %s" , truncatedOrigin ),
301304 fmt .Sprintf ("Full URL: %s" , truncatedFullURL ),
302305 }
303306
304- // Add User-Agent with wrapping (no truncation)
305307 if len (uaLines ) > 0 {
306308 lines = append (lines , "User-Agent: " + uaLines [0 ])
307309 for i := 1 ; i < len (uaLines ); i ++ {
308- lines = append (lines , " " + uaLines [i ]) // Indent continuation lines
310+ lines = append (lines , " " + uaLines [i ])
309311 }
310312 } else {
311313 lines = append (lines , "User-Agent: " + userAgent )
312314 }
313315
314- lines = append (lines ,
315- "--------------------------------------------------------------------------------" ,
316- fmt .Sprintf ("IPv4 Address: %s" , info .FromIPv4 ),
317- )
316+ lines = append (lines , string (bytes .Repeat ([]byte ("-" ), maxCharsPerLine )))
317+
318+ // Helper to wrap generic text (even with 1920px, very long strings might need wrapping)
319+ wrapField := func (label , value string ) {
320+ fullLine := fmt .Sprintf ("%s%s" , label , value )
321+ if len ([]rune (fullLine )) <= maxCharsPerLine {
322+ lines = append (lines , fullLine )
323+ } else {
324+ lines = append (lines , wrapText (fullLine , maxCharsPerLine )... )
325+ }
326+ }
327+
328+ wrapField ("IPv4 Address: " , info .FromIPv4 )
318329 if info .FromIPv6 != "" {
319- lines = append ( lines , fmt . Sprintf ( "IPv6 Address: %s " , info .FromIPv6 ) )
330+ wrapField ( "IPv6 Address: " , info .FromIPv6 )
320331 }
321- lines = append (lines ,
322- fmt .Sprintf ("Location: %s" , info .Country ),
323- fmt .Sprintf ("ISP: %s" , info .ISP ),
324- fmt .Sprintf ("ASN: %s" , info .ASN ),
325- "--------------------------------------------------------------------------------" ,
326- )
327-
328- // Calculate header height based on line count
332+ wrapField ("Location: " , info .Country )
333+ wrapField ("ISP: " , info .ISP )
334+ wrapField ("ASN: " , info .ASN )
335+
336+ lines = append (lines , string (bytes .Repeat ([]byte ("-" ), maxCharsPerLine )))
337+
338+ // Calculate dimensions
329339 lineHeight := 16
330340 startY := 20
331341 headerHeight := startY + (len (lines ) * lineHeight ) + 10
332342
333- // Create new image with calculated header height
334- newImg := image .NewRGBA (image .Rect (0 , 0 , width , height + headerHeight ))
343+ // Validate minimal width (screenshot might be wider than 1920? unlikely for mobile but possible for 4k desktop)
344+ // User said "Always fixed 1920px". If screenshot > 1920, we should probably crop or resize or expand.
345+ // Usually "Standard Evidence" implies 1920 is the canvas. If screenshot is bigger, the canvas grows?
346+ // But user said "Always fix width to 1920px". So if screenshot > 1920, we might need to downscale or let it crop?
347+ // Safer assumption: Canvas Width = Max(1920, ScreenshotWidth). But user said "Fixed 1920".
348+ // Let's stick to 1920. If screenshot is larger, it gets cropped. But mobile is usually smaller.
349+
350+ finalWidth := fixedWidth
351+
352+ newImg := image .NewRGBA (image .Rect (0 , 0 , finalWidth , headerHeight + screenshotHeight ))
335353
336- headerRect := image . Rect ( 0 , 0 , width , headerHeight )
337- draw .Draw (newImg , headerRect , image .NewUniform (color .Black ), image.Point {}, draw .Src )
354+ // 1. Draw Header Background (Black) - Full Width
355+ draw .Draw (newImg , image . Rect ( 0 , 0 , finalWidth , headerHeight ) , image .NewUniform (color .Black ), image.Point {}, draw .Src )
338356
339- draw .Draw (
340- newImg ,
341- image .Rect (0 , headerHeight , width , headerHeight + height ),
342- baseImg ,
343- image.Point {},
344- draw .Src ,
345- )
357+ // 2. Draw Screenshot - Centered horizontally at the bottom
358+ // Calculate center X
359+ // If screenshotWidth < 1920: (1920 - w) / 2
360+ // If screenshotWidth > 1920: (1920 - w) / 2 (result is negative, drawing starts at negative, effectively cropping center)
346361
362+ var dstX int
363+ if screenshotWidth < finalWidth {
364+ dstX = (finalWidth - screenshotWidth ) / 2
365+ } else {
366+ // Center crop behavior implies dstX is negative?
367+ // Wait, user wants "padding" for mobile (narrow).
368+ // For wide, if I fix 1920, I must crop or resize. I'll center.
369+ dstX = (finalWidth - screenshotWidth ) / 2
370+ }
371+
372+ // Draw screenshot
373+ // Transparent padding is achieved because newImg is zero-initialized (transparent distinct from black).
374+ // We only filled the header rect with Black. The rest is transparent.
375+
376+ screenshotRect := image .Rect (dstX , headerHeight , dstX + screenshotWidth , headerHeight + screenshotHeight )
377+ draw .Draw (newImg , screenshotRect , baseImg , image.Point {}, draw .Src )
378+
379+ // 3. Draw Header Text
347380 face := inconsolata .Regular8x16
348381 d := & font.Drawer {
349382 Dst : newImg ,
@@ -352,7 +385,6 @@ func addHeaderToImage(baseImg image.Image, info *utils.AccessInfo, TargetURL *st
352385 }
353386
354387 startX := textMarginX
355-
356388 for i , line := range lines {
357389 d .Dot = fixed.Point26_6 {
358390 X : fixed .I (startX ),
@@ -361,7 +393,7 @@ func addHeaderToImage(baseImg image.Image, info *utils.AccessInfo, TargetURL *st
361393 d .DrawString (line )
362394 }
363395
364- log .Infof ("Header added to screenshot: origin=%s, full_url=%s, ua =%s" , truncatedOrigin , truncatedFullURL , userAgent )
396+ log .Infof ("Header added with fixed width %d: origin =%s" , finalWidth , truncatedOrigin )
365397
366398 return newImg , nil
367399}
0 commit comments