Version 0 of Picture of the Day Demo

Updated 2023-08-31 03:04:22 by kpv

Keith Vetter - 2023-08-30: A few days ago I posted Picture of the Day, a module that lets you download pictures from either Wikipedia's or Wikimedia Commons' Picture of the Day. That code contained a CLI demo showing how to use the module.

Here is a GUI demo exercising the Picture of the Day module.

This demo requires that module. You can either download and save it as "wiki_potd.tcl". Alternatively, this demo can download the code off the Wiki and install it directly. To be safe, the demo fetches the Wiki page, scrapes the code and evaluates it in a safe interpreter. It then extracts the functionality from the safe interpeter.

##+##########################################################################
#
# wiki_pod_demo.tcl -- shows off how to use wiki_potd module
#
# Requires the "Picture of the Day" module. You can either download it
# from https://wiki.tcl-lang.org/page/Picture+of+the+Day (save it as
# wiki_potd.tcl), or this demo can download and install from the web.
# (It does so safely by using a safe interpreter).
#
# by Keith Vetter 2023-08-02
#
package require Tk
package require Img
package require http
package require tls
http::register https 443 [list ::tls::socket -tls1 1]
package require tdom

set module_name wiki_potd.tcl

set text_font [concat [font actual TkDefaultFont] -size 15]
set big_font [concat [font actual TkDefaultFont] -size 18]
set big_bold_font [concat [font actual TkDefaultFont] -size 18 -weight bold]

proc Main {} {
    GetNewImage Commons
}
proc DoDisplay {} {
    wm title . "Wikipedia Picture of the Day Demo"
    image create photo ::img::img -width 500 -height 500
    ::ttk::label .img -image ::img::img -borderwidth 3 -relief ridge
    ::ttk::label .desc -textvariable ::DESC -borderwidth 0 -relief ridge -anchor center \
        -justify center -font $::big_font
    ::ttk::frame .left
    text .log -height 3 -font $::text_font -width 0 -bd 3 -relief ridge -wrap word
    .log tag config title -font $::big_bold_font
    .log insert end "Welcome to Wiki Picture of the Day Demo" title
    grid .left .img -sticky news
    grid ^ .desc -sticky news
    grid .log - -sticky news -row 10
    grid rowconfigure . 10 -weight 1

    ::ttk::frame .left.new -relief raised -borderwidth 3 -padding {0 0 0 .1i}
    ::ttk::label .left.new.l -text "Random POTD Image" -font $::text_font
    ::ttk::button .left.new.w -text "Wikipedia" -command [list GetNewImage Wikipedia]
    ::ttk::button .left.new.c -text "Commons" -command [list GetNewImage Commons]
    grid .left.new.l -sticky ew -pady {0 .05i}

    grid .left.new.w
    grid .left.new.c

    ::ttk::button .left.save -text Save -command SaveImage
    grid .left.new -row 1 -pady .2i -padx .2i
    grid rowconfigure .left 100 -weight 1
    grid .left.save -row 101 -pady .2i
}

proc ClearOldSizes {} {
    foreach w [winfo child .left] {
        if {$w ne ".left.new" && $w ne ".left.save"} {
            destroy $w
        }
    }
    destroy .mf
}

proc GetNewImage {service} {
    destroy .mf
    GuiLogger "====================="
    ::img::img blank
    set ::DESC "fetching new $service POTD..."

    set maxWidth [expr {[winfo screenwidth .] * .5}]
    set maxHeight [expr {[winfo screenheight .] * .8}]
    set fitness [list $maxWidth $maxHeight]

    lassign [::POTD::RandomPOTD $service $fitness] ::meta ::all
    UpdateGUI $::meta $::all
}
proc UpdateGUI {meta all} {
    ClearOldSizes

    set date [dict get $meta date]
    set pdate [clock format [clock scan $date -format "%Y/%m/%d"] -format "%B %d, %Y"]
    set service [dict get $meta service]
    set row 2
    ::ttk::label .left.l_$row -text "$service POTD\n$pdate" -anchor c -justify c
    grid .left.l_$row -row $row -sticky ew

    if {$all eq {}} {
        ::ttk::label .left.nothing -text "No sizes found" -anchor c
        set bestfit -1
    } else {
        foreach item $all {
            incr row
            lassign $item width height url
            set w ".left.b_$row"
            set txt "[Comma $width] x [Comma $height]"
            ::ttk::button $w -text $txt -command [list ShowImage $width $height $url]
            grid $w -row $row -sticky ew
        }
    }
    set bestfit [dict get $meta bestfit]
    if {$bestfit != -1} {
        ShowImage {*}[lindex $all $bestfit]
        lassign [lindex $all $bestfit] width height _
        set size " ([Comma $width] x [Comma $height])"
    } else {
        image create photo ::img::img -width 500 -height 500
        ::img::img put seashell -to 0 0 500 500
        MessageBox [dict get $meta desc]
        set ::img_data ""
        set size ""
    }
    set desc [string trim [dict get $meta desc]]
    set first [FirstSentence $desc]
    set ::DESC "$first\n$service POTD for $pdate$size"
    GuiLogger "$desc\n$service POTD for $pdate$size"
}
proc FirstSentence {para} {
    set n [string first "." $para]
    if {$n == [string length $para] - 1} {
        return $para
    }
    # https://stackoverflow.com/questions/3788220/extract-first-sentence-from-string-of-text
    set re {(^.*?[a-z0-9\)]{2,}[.!?])\s+\W*[A-Z]}
    set n [regexp $re $para _ sentence]
    if {$n} { return $sentence }
    return $para
}
proc Comma {num} {
    while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {}
    return $num
}
proc GuiLogger {msg} {
    .log insert end "\n\u2022 $msg"
    .log see end
}
proc ShowImage {width height url} {
    set ::img_data [::POTD::DownloadUrl $url]
    image delete ::img::img
    image create photo ::img::img -data $::img_data
    .desc config -wraplength [image width ::img::img]
    GuiLogger "Showing [Comma $width] x [Comma $height]"
}
proc SaveImage {} {
    if {$::img_data eq ""} return
    set service [dict get $::meta service]
    set service [string tolower [string index $service 0]]
    set date [regsub -all "/" [dict get $::meta date] "_"]
    set fname "potd_${service}_${date}.jpg"
    set fout [open $fname wb]
    puts -nonewline $fout $::img_data
    close $fout
    set w [MessageBox "Saved image to $fname"]
    after 3000 [list destroy $w]
}
proc MessageBox {msg} {
    destroy .mf
    ::ttk::frame .mf -borderwidth 3 -relief groove
    ::ttk::label .mf.l -text $msg -font $::big_bold_font
    ::ttk::button .mf.b -text Ok -command {destroy .mf}

    grid .mf.l -padx .4i -pady .4i
    grid .mf.b -pady .2i

    place .mf -in .img -relx .5 -rely .5 -anchor c
    return .mf
}
proc ErrorBox {msg details} {
    tk_messageBox -message $msg -detail $details -icon error -title "Wiki Potd Demo" \
        -parent .
    exit 1
}

namespace eval ::WikiPotD {
    # Code to fetch from tcler's Wiki "Picture of the Day" code

    variable url https://wiki.tcl-lang.org/page/Picture+of+the+Day
    variable fname $module_name
}

proc ::WikiPotD::Load {} {
    variable fname
    variable url

    set fullname [::WikiPotD::_Search $fname]
    if {$fullname ne ""} {
        GuiLogger "Loading Wiki PotD module from $fname"
        source $fname
        return
    }
    set msg "Module Wiki PotD not found"
    set details "It can be found at $url\n\n"
    append details "Do you want to download and install it (safely) from the web?"

    set resp [tk_messageBox -message $msg -detail $details -icon question \
                  -title "Wiki Potd Demo" -type yesno -parent .]
    if {$resp ne "yes"} {
        puts stderr "Download module Wiki PotD from $url"
        exit 1
    }

    set code [::WikiPotD::_Fetch]
    ::WikiPotD::_SafeLoadCode $code
}
proc ::WikiPotD::_Search {fname} {
    GuiLogger "Searching for Wiki PotD module"
    set dirs [list . .. [file dirname [file normalize $::argv0]]]
    foreach dir $dirs {
        set fullname [file join $dir $fname]
        if {[file exists $fullname]} { return $fullname }
    }
    return ""
}

proc ::WikiPotD::_Fetch {} {
    variable url

    set html [::WikiPotD::_DownloadUrl $url]
    set code [::WikiPotD::_ExtractCode $html]
    return $code

}
proc ::WikiPotD::_DownloadUrl {url} {
    # Downloads a given URL

    GuiLogger "Fetching tcler's Wiki page for Picture of the Day"
    set token [::http::geturl $url]
    set html [::http::data $token]
    set ncode [::http::ncode $token]
    ::http::cleanup $token

    if {$ncode != 200} {
        ErrorBox "Error Installing Wiki POTD" "Failed to download $url with code $ncode"
    }
    return $html
}
proc ::WikiPotD::_ExtractCode {html {index 1}} {
    set n [catch {set dom [::dom parse -html $html]}]
    if {$n} {ErrorBox "Error Installing Wiki POTD" "Bad HTML: $emsg" }

    GuiLogger "Scraping Wiki page for code section"
    set xpath {//pre[contains(@class, "sh_sourceCode")]}
    set code_nodes [$dom selectNodes $xpath]

    set cnt [llength $code_nodes]
    if {$cnt == 0} {
        ErrorBox "Error Installing Wiki POTD" "Scraping error: No code sections found"
    }
    set code [[lindex $code_nodes $index-1] asText]
    return $code
}
proc ::WikiPotD::_SafeLoadCode {code} {
    # Evaluates $code in a safe interpreter then extracts the good stuff

    set in [interp create -safe]
    interp expose $in source
    interp eval $in { proc package {args} {} }
    interp eval $in { namespace eval ::http {} }
    interp eval $in { proc ::http::register {args} {} }
    interp eval $in [list eval $code]

    namespace eval ::POTD {}
    set vars [interp eval $in {info vars ::POTD::* }]
    foreach var $vars {
        # If any var is an array then this code breaks
        set value [interp eval $in [list set $var]]
        set $var $value
        # puts "set $var $value"
    }
    set funcs [interp eval $in {info procs ::POTD::* }]
    foreach func $funcs {
        # puts "CopyProc \$in $func"
        ::WikiPotD::_CopyProc $in $func
    }

    interp delete $in
}
proc ::WikiPotD::_CopyProc {in pname} {
    set args {}
    foreach arg [interp eval $in info args $pname] {
        if {[interp eval $in info default $pname $arg _default_]} {
            set default [interp eval $in set _default_]
            lappend args [list $arg $default]
        } else {
            lappend args $arg
        }
    }
    set body [interp eval $in info body $pname]
    uplevel "#0" [list proc $pname $args $body]
}

if {$tcl_interactive} return
DoDisplay
::WikiPotD::Load
::POTD::SetLogger GuiLogger
Main