add jpeg encoding

This commit is contained in:
Antoni Sawicki 2022-12-08 00:55:56 -08:00
parent ecb2cc0c06
commit 1c17b39ea5
3 changed files with 67 additions and 38 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,12 @@ 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
-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 +86,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.

94
wrp.go
View File

@ -17,6 +17,7 @@ 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"
@ -46,7 +47,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 +139,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 +300,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 +328,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 "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 "jpg":
i, err := png.Decode(bytes.NewReader(pngCap))
if err != nil {
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)
return
}
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())
case "png": case "png":
pngbuf := bytes.NewBuffer(pngcap) pngBuf := bytes.NewBuffer(pngCap)
img[imgpath] = *pngbuf img[imgPath] = *pngBuf
cfg, _, _ := image.DecodeConfig(pngbuf) cfg, _, _ := image.DecodeConfig(pngBuf)
ssize = fmt.Sprintf("%.0f KB", float32(len(pngbuf.Bytes()))/1024.0) sSize = fmt.Sprintf("%.0f KB", float32(len(pngBuf.Bytes()))/1024.0)
iw = cfg.Width iW = cfg.Width
ih = cfg.Height iH = cfg.Height
log.Printf("%s Got PNG image: %s, Size: %s, Res: %dx%d\n", rq.r.RemoteAddr, imgpath, ssize, iw, ih) log.Printf("%s Got PNG image: %s, Size: %s, Res: %dx%d\n", rq.r.RemoteAddr, imgPath, sSize, iW, iH)
} }
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 +445,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 +459,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()
} }

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>