Compare commits

...

4 Commits

Author SHA1 Message Date
Antoni Sawicki d8f5c6fb28 print my own IP addresses 2022-12-08 01:49:46 -08:00
Antoni Sawicki c816ef712a readme update 2022-12-08 01:03:04 -08:00
Antoni Sawicki d28924583c move png to first case 2022-12-08 00:56:24 -08:00
Antoni Sawicki 1c17b39ea5 add jpeg encoding 2022-12-08 00:55:56 -08:00
3 changed files with 101 additions and 43 deletions

View File

@ -17,7 +17,7 @@ A browser-in-browser "proxy" server that allows to use historical / vintage web
* You can re-capture page screenshot without reloading by using **St** (Stop). This is useful if page didn't render fully before screenshot is taken. * You can re-capture page screenshot without reloading by using **St** (Stop). This is useful if page didn't render fully before screenshot is taken.
* You can also reload and re-capture current page with **Re** (Reload). * You can also reload and re-capture current page with **Re** (Reload).
* To send keystrokes, fill **K** input box and press **Go**. There also are buttons for backspace, enter and arrow keys. * To send keystrokes, fill **K** input box and press **Go**. There also are buttons for backspace, enter and arrow keys.
* Prefer PNG over GIF if your browser supports it. PNG is much faster, whereas GIF requires a lot of additional processing on both client and server to encode/decode. * Prefer PNG over GIF if your browser supports it. PNG is much faster, whereas GIF requires a lot of additional processing on both client and server to encode/decode. Jpeg encoding is also quite fast.
* GIF images are by default encoded with 216 colors, "web safe" palette. This uses an ultra fast but not very accurate color mapping algorithm. If you want better color representation switch to 256 color mode. * GIF images are by default encoded with 216 colors, "web safe" palette. This uses an ultra fast but not very accurate color mapping algorithm. If you want better color representation switch to 256 color mode.
## Customization ## Customization
@ -54,12 +54,13 @@ Fortunately ACI allows port 80 without encryption.
```text ```text
-l listen address:port (default :8080) -l listen address:port (default :8080)
-t image type gif or png (default gif) -t image type gif, png or jpg (default gif)
-g image geometry, WxHxC, height can be 0 for unlimited (default 1152x600x216) -g image geometry, WxHxC, height can be 0 for unlimited (default 1152x600x216)
C (number of colors) is only used for GIF C (number of colors) is only used for GIF
-q Jpeg image quality, default 80%
-h headless mode, hide browser window on the server (default true) -h headless mode, hide browser window on the server (default true)
-d chromedp debug logging (default false) -d chromedp debug logging (default false)
-n do not free maps and gif images after use (default false) -n do not free maps and images after use (default false)
-ui html template file (default "wrp.html") -ui html template file (default "wrp.html")
-s delay/sleep after page is rendered before screenshot is taken (default 2s) -s delay/sleep after page is rendered before screenshot is taken (default 2s)
``` ```
@ -86,7 +87,7 @@ used with PNG and lots of memory on a client side.
**Z** is zoom or scale **Z** is zoom or scale
**C** is colors, for GIF images only (unused in PNG) **C** is colors, for GIF images only (unused in PNG, JPG)
**K** is keystroke input, you can type some letters in it and when you click Go it will be typed in the remote browser. **K** is keystroke input, you can type some letters in it and when you click Go it will be typed in the remote browser.

132
wrp.go
View File

@ -17,12 +17,14 @@ import (
"image" "image"
"image/color/palette" "image/color/palette"
"image/gif" "image/gif"
"image/jpeg"
"image/png" "image/png"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"math" "math"
"math/rand" "math/rand"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -46,7 +48,8 @@ var (
addr = flag.String("l", ":8080", "Listen address:port, default :8080") addr = flag.String("l", ":8080", "Listen address:port, default :8080")
headless = flag.Bool("h", true, "Headless mode / hide browser window (default true)") headless = flag.Bool("h", true, "Headless mode / hide browser window (default true)")
noDel = flag.Bool("n", false, "Do not free maps and images after use") noDel = flag.Bool("n", false, "Do not free maps and images after use")
defType = flag.String("t", "gif", "Image type: gif|png") defType = flag.String("t", "gif", "Image type: png|gif|jpg")
jpgQual = flag.Int("q", 80, "Jpeg image quality, default 80%")
fgeom = flag.String("g", "1152x600x216", "Geometry: width x height x colors, height can be 0 for unlimited") fgeom = flag.String("g", "1152x600x216", "Geometry: width x height x colors, height can be 0 for unlimited")
htmFnam = flag.String("ui", "wrp.html", "HTML template file for the UI") htmFnam = flag.String("ui", "wrp.html", "HTML template file for the UI")
delay = flag.Duration("s", 2*time.Second, "Delay/sleep after page is rendered and before screenshot is taken") delay = flag.Duration("s", 2*time.Second, "Delay/sleep after page is rendered and before screenshot is taken")
@ -137,7 +140,11 @@ func (rq *wrpReq) parseForm() {
rq.keys = rq.r.FormValue("k") rq.keys = rq.r.FormValue("k")
rq.buttons = rq.r.FormValue("Fn") rq.buttons = rq.r.FormValue("Fn")
rq.imgType = rq.r.FormValue("t") rq.imgType = rq.r.FormValue("t")
if rq.imgType != "gif" && rq.imgType != "png" { switch rq.imgType {
case "png":
case "gif":
case "jpg":
default:
rq.imgType = *defType rq.imgType = *defType
} }
log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq) log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq)
@ -294,7 +301,7 @@ func (rq *wrpReq) capture() {
var styles []*css.ComputedStyleProperty var styles []*css.ComputedStyleProperty
var r, g, b int var r, g, b int
var h int64 var h int64
var pngcap []byte var pngCap []byte
chromedp.Run(ctx, chromedp.Run(ctx,
emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false), emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false),
chromedp.Location(&rq.url), chromedp.Location(&rq.url),
@ -322,51 +329,71 @@ func (rq *wrpReq) capture() {
chromedp.Sleep(*delay), // TODO(tenox): find a better way to determine if page is rendered chromedp.Sleep(*delay), // TODO(tenox): find a better way to determine if page is rendered
) )
// Capture screenshot... // Capture screenshot...
ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngcap, rq.height)), rq.w) ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngCap, rq.height)), rq.w)
seq := rand.Intn(9999) seq := rand.Intn(9999)
imgpath := fmt.Sprintf("/img/%04d.%s", seq, rq.imgType) imgPath := fmt.Sprintf("/img/%04d.%s", seq, rq.imgType)
mappath := fmt.Sprintf("/map/%04d.map", seq) mapPath := fmt.Sprintf("/map/%04d.map", seq)
ismap[mappath] = *rq ismap[mapPath] = *rq
var ssize string var sSize string
var iw, ih int var iW, iH int
switch rq.imgType { switch rq.imgType {
case "png":
pngBuf := bytes.NewBuffer(pngCap)
img[imgPath] = *pngBuf
cfg, _, _ := image.DecodeConfig(pngBuf)
sSize = fmt.Sprintf("%.0f KB", float32(len(pngBuf.Bytes()))/1024.0)
iW = cfg.Width
iH = cfg.Height
log.Printf("%s Got PNG image: %s, Size: %s, Res: %dx%d\n", rq.r.RemoteAddr, imgPath, sSize, iW, iH)
case "gif": case "gif":
i, err := png.Decode(bytes.NewReader(pngcap)) i, err := png.Decode(bytes.NewReader(pngCap))
if err != nil { if err != nil {
log.Printf("%s Failed to decode PNG screenshot: %s\n", rq.r.RemoteAddr, err) log.Printf("%s Failed to decode PNG screenshot: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to decode page PNG screenshot:<BR>%s<BR>\n", err) fmt.Fprintf(rq.w, "<BR>Unable to decode page PNG screenshot:<BR>%s<BR>\n", err)
return return
} }
st := time.Now() st := time.Now()
var gifbuf bytes.Buffer var gifBuf bytes.Buffer
err = gif.Encode(&gifbuf, gifPalette(i, rq.colors), &gif.Options{}) err = gif.Encode(&gifBuf, gifPalette(i, rq.colors), &gif.Options{})
if err != nil { if err != nil {
log.Printf("%s Failed to encode GIF: %s\n", rq.r.RemoteAddr, err) log.Printf("%s Failed to encode GIF: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to encode GIF:<BR>%s<BR>\n", err) fmt.Fprintf(rq.w, "<BR>Unable to encode GIF:<BR>%s<BR>\n", err)
return return
} }
img[imgpath] = gifbuf img[imgPath] = gifBuf
ssize = fmt.Sprintf("%.0f KB", float32(len(gifbuf.Bytes()))/1024.0) sSize = fmt.Sprintf("%.0f KB", float32(len(gifBuf.Bytes()))/1024.0)
iw = i.Bounds().Max.X iW = i.Bounds().Max.X
ih = i.Bounds().Max.Y iH = i.Bounds().Max.Y
log.Printf("%s Encoded GIF image: %s, Size: %s, Colors: %d, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgpath, ssize, rq.colors, iw, ih, time.Since(st).Milliseconds()) log.Printf("%s Encoded GIF image: %s, Size: %s, Colors: %d, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgPath, sSize, rq.colors, iW, iH, time.Since(st).Milliseconds())
case "png": case "jpg":
pngbuf := bytes.NewBuffer(pngcap) i, err := png.Decode(bytes.NewReader(pngCap))
img[imgpath] = *pngbuf if err != nil {
cfg, _, _ := image.DecodeConfig(pngbuf) log.Printf("%s Failed to decode PNG screenshot: %s\n", rq.r.RemoteAddr, err)
ssize = fmt.Sprintf("%.0f KB", float32(len(pngbuf.Bytes()))/1024.0) fmt.Fprintf(rq.w, "<BR>Unable to decode page PNG screenshot:<BR>%s<BR>\n", err)
iw = cfg.Width return
ih = cfg.Height }
log.Printf("%s Got PNG image: %s, Size: %s, Res: %dx%d\n", rq.r.RemoteAddr, imgpath, ssize, iw, ih) st := time.Now()
var jpgBuf bytes.Buffer
err = jpeg.Encode(&jpgBuf, i, &jpeg.Options{Quality: *jpgQual})
if err != nil {
log.Printf("%s Failed to encode JPG: %s\n", rq.r.RemoteAddr, err)
fmt.Fprintf(rq.w, "<BR>Unable to encode JPG:<BR>%s<BR>\n", err)
return
}
img[imgPath] = jpgBuf
sSize = fmt.Sprintf("%.0f KB", float32(len(jpgBuf.Bytes()))/1024.0)
iW = i.Bounds().Max.X
iH = i.Bounds().Max.Y
log.Printf("%s Encoded JPG image: %s, Size: %s, Quality: %d, Res: %dx%d, Time: %vms\n", rq.r.RemoteAddr, imgPath, sSize, *jpgQual, iW, iH, time.Since(st).Milliseconds())
} }
rq.printHTML(printParams{ rq.printHTML(printParams{
bgColor: fmt.Sprintf("#%02X%02X%02X", r, g, b), bgColor: fmt.Sprintf("#%02X%02X%02X", r, g, b),
pageHeight: fmt.Sprintf("%d PX", h), pageHeight: fmt.Sprintf("%d PX", h),
imgSize: ssize, imgSize: sSize,
imgURL: imgpath, imgURL: imgPath,
mapURL: mappath, mapURL: mapPath,
imgWidth: iw, imgWidth: iW,
imgHeight: ih, imgHeight: iH,
}) })
log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url) log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url)
} }
@ -419,8 +446,8 @@ func mapServer(w http.ResponseWriter, r *http.Request) {
// Process HTTP requests for images '/img/' url // Process HTTP requests for images '/img/' url
func imgServer(w http.ResponseWriter, r *http.Request) { func imgServer(w http.ResponseWriter, r *http.Request) {
log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path) log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path)
imgbuf, ok := img[r.URL.Path] imgBuf, ok := img[r.URL.Path]
if !ok || imgbuf.Bytes() == nil { if !ok || imgBuf.Bytes() == nil {
fmt.Fprintf(w, "Unable to find image %s\n", r.URL.Path) fmt.Fprintf(w, "Unable to find image %s\n", r.URL.Path)
log.Printf("%s Unable to find image %s\n", r.RemoteAddr, r.URL.Path) log.Printf("%s Unable to find image %s\n", r.RemoteAddr, r.URL.Path)
return return
@ -433,12 +460,14 @@ func imgServer(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/gif") w.Header().Set("Content-Type", "image/gif")
case strings.HasPrefix(r.URL.Path, ".png"): case strings.HasPrefix(r.URL.Path, ".png"):
w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Type", "image/png")
case strings.HasPrefix(r.URL.Path, ".jpg"):
w.Header().Set("Content-Type", "image/jpeg")
} }
w.Header().Set("Content-Length", strconv.Itoa(len(imgbuf.Bytes()))) w.Header().Set("Content-Length", strconv.Itoa(len(imgBuf.Bytes())))
w.Header().Set("Cache-Control", "max-age=0") w.Header().Set("Cache-Control", "max-age=0")
w.Header().Set("Expires", "-1") w.Header().Set("Expires", "-1")
w.Header().Set("Pragma", "no-cache") w.Header().Set("Pragma", "no-cache")
w.Write(imgbuf.Bytes()) w.Write(imgBuf.Bytes())
w.(http.Flusher).Flush() w.(http.Flusher).Flush()
} }
@ -489,13 +518,42 @@ builtin:
return string(tmpl) return string(tmpl)
} }
// Main... // Print my own IP addresses
func printIPs(b string) {
ap := strings.Split(b, ":")
if len(ap) < 1 {
log.Fatal("Wrong format of ipaddress:port")
}
log.Printf("Listen address: %v", b)
if ap[0] != "" && ap[0] != "0.0.0.0" {
return
}
a, err := net.InterfaceAddrs()
if err != nil {
log.Print("Unable to get interfaces: ", err)
return
}
var m string
for _, i := range a {
n, ok := i.(*net.IPNet)
if !ok || n.IP.IsLoopback() || strings.Contains(n.IP.String(), ":") {
continue
}
m = m + n.IP.String() + " "
}
log.Print("My IP addresses: ", m)
}
// Main
func main() { func main() {
var err error var err error
flag.Parse() flag.Parse()
log.Printf("Web Rendering Proxy Version %s\n", version)
log.Printf("Args: %q", os.Args)
if len(os.Getenv("PORT")) > 0 { if len(os.Getenv("PORT")) > 0 {
*addr = ":" + os.Getenv(("PORT")) *addr = ":" + os.Getenv(("PORT"))
} }
printIPs(*addr)
n, err := fmt.Sscanf(*fgeom, "%dx%dx%d", &defGeom.w, &defGeom.h, &defGeom.c) n, err := fmt.Sscanf(*fgeom, "%dx%dx%d", &defGeom.w, &defGeom.h, &defGeom.c)
if err != nil || n != 3 { if err != nil || n != 3 {
log.Fatalf("Unable to parse -g geometry flag / %s", err) log.Fatalf("Unable to parse -g geometry flag / %s", err)
@ -529,8 +587,6 @@ func main() {
http.HandleFunc("/shutdown/", haltServer) http.HandleFunc("/shutdown/", haltServer)
http.HandleFunc("/favicon.ico", http.NotFound) http.HandleFunc("/favicon.ico", http.NotFound)
log.Printf("Web Rendering Proxy Version %s\n", version)
log.Printf("Args: %q", os.Args)
log.Printf("Default Img Type: %v, Geometry: %+v", *defType, defGeom) log.Printf("Default Img Type: %v, Geometry: %+v", *defType, defGeom)
htmlTmpl, err = template.New("wrp.html").Parse(tmpl(*htmFnam)) htmlTmpl, err = template.New("wrp.html").Parse(tmpl(*htmFnam))
@ -538,7 +594,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
log.Printf("Starting WRP http server on %s\n", *addr) log.Print("Starting WRP http server")
srv.Addr = *addr srv.Addr = *addr
err = srv.ListenAndServe() err = srv.ListenAndServe()
if err != nil { if err != nil {

View File

@ -21,8 +21,9 @@
<OPTION VALUE="1.3" {{ if eq .Zoom 1.3}}SELECTED{{end}}>1.3 x</OPTION> <OPTION VALUE="1.3" {{ if eq .Zoom 1.3}}SELECTED{{end}}>1.3 x</OPTION>
</SELECT> </SELECT>
T <SELECT NAME="t"> T <SELECT NAME="t">
<OPTION VALUE="gif" {{ if eq .ImgType "gif"}}SELECTED{{end}}>GIF</OPTION>
<OPTION VALUE="png" {{ if eq .ImgType "png"}}SELECTED{{end}}>PNG</OPTION> <OPTION VALUE="png" {{ if eq .ImgType "png"}}SELECTED{{end}}>PNG</OPTION>
<OPTION VALUE="gif" {{ if eq .ImgType "gif"}}SELECTED{{end}}>GIF</OPTION>
<OPTION VALUE="jpg" {{ if eq .ImgType "jpg"}}SELECTED{{end}}>JPG</OPTION>
</SELECT> </SELECT>
C <SELECT NAME="c"> C <SELECT NAME="c">
<OPTION VALUE="256" {{ if eq .NColors 256}}SELECTED{{end}}>256</OPTION> <OPTION VALUE="256" {{ if eq .NColors 256}}SELECTED{{end}}>256</OPTION>