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