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.
Jeff Smith 2023-09-01 : Below is an online demo using CloudTk. This demo runs “Picture of the Day Demo” in an Alpine Linux Docker Container. It is a 29.2MB image which is made up of Alpine Linux + tclkit + Picture-of-the-Day-Demo.kit + tls1.7.18 + libx11 + libxft + fontconfig. It is run under a user account in the Container. The Container is restrictive with permissions for "Other" removed for "execute" and "read" for certain directories.
N.B This demo is better viewed in Full Screen mode via the noVNC tab at the side of the window.
##+########################################################################## # # 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