mirror of
https://github.com/tenox7/wrp.git
synced 2024-10-14 16:24:17 +00:00
merge txt into master
This commit is contained in:
commit
3004962beb
2
go.mod
2
go.mod
@ -8,6 +8,7 @@ require (
|
||||
github.com/breml/rootcerts v0.2.17
|
||||
github.com/chromedp/cdproto v0.0.0-20240519224452-66462be74baa
|
||||
github.com/chromedp/chromedp v0.9.5
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/soniakeys/quant v1.0.0
|
||||
github.com/yuin/goldmark v1.7.2
|
||||
@ -21,6 +22,7 @@ require (
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -24,6 +24,8 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
@ -33,6 +35,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
|
293
ismap.go
Normal file
293
ismap.go
Normal file
@ -0,0 +1,293 @@
|
||||
// WRP ISMAP / ChromeDP routines
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/css"
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/input"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
)
|
||||
|
||||
func chromedpStart() (context.CancelFunc, context.CancelFunc) {
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", *headless),
|
||||
chromedp.Flag("hide-scrollbars", false),
|
||||
chromedp.Flag("enable-automation", false),
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
)
|
||||
if *userAgent != "" {
|
||||
opts = append(opts, chromedp.UserAgent(*userAgent))
|
||||
}
|
||||
actx, acncl = chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
return cncl, acncl
|
||||
}
|
||||
|
||||
// Determine what action to take
|
||||
func (rq *wrpReq) action() chromedp.Action {
|
||||
// Mouse Click
|
||||
if rq.mouseX > 0 && rq.mouseY > 0 {
|
||||
log.Printf("%s Mouse Click %d,%d\n", rq.r.RemoteAddr, rq.mouseX, rq.mouseY)
|
||||
return chromedp.MouseClickXY(float64(rq.mouseX)/float64(rq.zoom), float64(rq.mouseY)/float64(rq.zoom))
|
||||
}
|
||||
// Buttons
|
||||
if len(rq.buttons) > 0 {
|
||||
log.Printf("%s Button %v\n", rq.r.RemoteAddr, rq.buttons)
|
||||
switch rq.buttons {
|
||||
case "Bk":
|
||||
return chromedp.NavigateBack()
|
||||
case "St":
|
||||
return chromedp.Stop()
|
||||
case "Re":
|
||||
return chromedp.Reload()
|
||||
case "Bs":
|
||||
return chromedp.KeyEvent("\b")
|
||||
case "Rt":
|
||||
return chromedp.KeyEvent("\r")
|
||||
case "<":
|
||||
return chromedp.KeyEvent("\u0302")
|
||||
case "^":
|
||||
return chromedp.KeyEvent("\u0304")
|
||||
case "v":
|
||||
return chromedp.KeyEvent("\u0301")
|
||||
case ">":
|
||||
return chromedp.KeyEvent("\u0303")
|
||||
case "Up":
|
||||
return chromedp.KeyEvent("\u0308")
|
||||
case "Dn":
|
||||
return chromedp.KeyEvent("\u0307")
|
||||
case "All": // Select all
|
||||
return chromedp.KeyEvent("a", chromedp.KeyModifiers(input.ModifierCtrl))
|
||||
}
|
||||
}
|
||||
// Keys
|
||||
if len(rq.keys) > 0 {
|
||||
log.Printf("%s Sending Keys: %#v\n", rq.r.RemoteAddr, rq.keys)
|
||||
return chromedp.KeyEvent(rq.keys)
|
||||
}
|
||||
// Navigate to URL
|
||||
log.Printf("%s Processing Navigate Request for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
return chromedp.Navigate(rq.url)
|
||||
}
|
||||
|
||||
// Navigate to the desired URL.
|
||||
func (rq *wrpReq) navigate() {
|
||||
ctxErr(chromedp.Run(ctx, rq.action()), rq.w)
|
||||
}
|
||||
|
||||
// Handle context errors
|
||||
func ctxErr(err error, w io.Writer) {
|
||||
// TODO: callers should have retry logic, perhaps create another function
|
||||
// that takes ...chromedp.Action and retries with give up
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("Context error: %s", err)
|
||||
fmt.Fprintf(w, "Context error: %s<BR>\n", err)
|
||||
if err.Error() != "context canceled" {
|
||||
return
|
||||
}
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
log.Printf("Created new context, try again")
|
||||
fmt.Fprintln(w, "Created new context, try again")
|
||||
}
|
||||
|
||||
// https://github.com/chromedp/chromedp/issues/979
|
||||
func chromedpCaptureScreenshot(res *[]byte, h int64) chromedp.Action {
|
||||
if res == nil {
|
||||
panic("res cannot be nil") // TODO: do not panic here, return error
|
||||
}
|
||||
if h == 0 {
|
||||
return chromedp.CaptureScreenshot(res)
|
||||
}
|
||||
|
||||
return chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var err error
|
||||
*res, err = page.CaptureScreenshot().Do(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Capture Screenshot using CDP
|
||||
func (rq *wrpReq) captureScreenshot() {
|
||||
var styles []*css.ComputedStyleProperty
|
||||
var r, g, b int
|
||||
var bgColorSet bool
|
||||
var h int64
|
||||
var pngCap []byte
|
||||
chromedp.Run(ctx,
|
||||
emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false),
|
||||
chromedp.Location(&rq.url),
|
||||
chromedp.ComputedStyle("body", &styles, chromedp.ByQuery),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
_, _, _, _, _, s, err := page.GetLayoutMetrics().Do(ctx)
|
||||
if err == nil {
|
||||
h = int64(math.Ceil(s.Height))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
log.Printf("%s Landed on: %s, Height: %v\n", rq.r.RemoteAddr, rq.url, h)
|
||||
for _, style := range styles {
|
||||
if style.Name != "background-color" {
|
||||
continue
|
||||
}
|
||||
fmt.Sscanf(style.Value, "rgb(%d,%d,%d)", &r, &g, &b)
|
||||
bgColorSet = true
|
||||
break
|
||||
}
|
||||
if !bgColorSet {
|
||||
r = 255
|
||||
g = 255
|
||||
b = 255
|
||||
}
|
||||
height := int64(float64(rq.height) / rq.zoom)
|
||||
if rq.height == 0 && h > 0 {
|
||||
height = h + 30
|
||||
}
|
||||
chromedp.Run(
|
||||
ctx, emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), height, rq.zoom, false),
|
||||
chromedp.Sleep(*delay), // TODO(tenox): find a better way to determine if page is rendered
|
||||
)
|
||||
// Capture screenshot...
|
||||
ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngCap, rq.height)), rq.w)
|
||||
seq := shortuuid.New()
|
||||
imgPath := fmt.Sprintf("/img/%s.%s", seq, rq.imgType)
|
||||
mapPath := fmt.Sprintf("/map/%s.map", seq)
|
||||
ismap[mapPath] = *rq
|
||||
var sSize string
|
||||
var iW, iH int
|
||||
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":
|
||||
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 gifBuf bytes.Buffer
|
||||
err = gif.Encode(&gifBuf, gifPalette(i, rq.nColors), &gif.Options{})
|
||||
if err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
img[imgPath] = gifBuf
|
||||
sSize = fmt.Sprintf("%.0f KB", float32(len(gifBuf.Bytes()))/1024.0)
|
||||
iW = i.Bounds().Max.X
|
||||
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.nColors, 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: int(rq.jQual)})
|
||||
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, *defJpgQual, iW, iH, time.Since(st).Milliseconds())
|
||||
}
|
||||
rq.printUI(uiParams{
|
||||
bgColor: fmt.Sprintf("#%02X%02X%02X", r, g, b),
|
||||
pageHeight: fmt.Sprintf("%d PX", h),
|
||||
imgSize: sSize,
|
||||
imgURL: imgPath,
|
||||
mapURL: mapPath,
|
||||
imgWidth: iW,
|
||||
imgHeight: iH,
|
||||
})
|
||||
log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
}
|
||||
|
||||
func mapServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s ISMAP Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
|
||||
rq, ok := ismap[r.URL.Path]
|
||||
rq.r = r
|
||||
rq.w = w
|
||||
if !ok {
|
||||
fmt.Fprintf(w, "Unable to find map %s\n", r.URL.Path)
|
||||
log.Printf("Unable to find map %s\n", r.URL.Path)
|
||||
return
|
||||
}
|
||||
if !*noDel {
|
||||
defer delete(ismap, r.URL.Path)
|
||||
}
|
||||
n, err := fmt.Sscanf(r.URL.RawQuery, "%d,%d", &rq.mouseX, &rq.mouseY)
|
||||
if err != nil || n != 2 {
|
||||
fmt.Fprintf(w, "n=%d, err=%s\n", n, err)
|
||||
log.Printf("%s ISMAP n=%d, err=%s\n", r.RemoteAddr, n, err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s WrpReq from ISMAP: %+v\n", r.RemoteAddr, rq)
|
||||
if len(rq.url) < 4 {
|
||||
rq.printUI(uiParams{bgColor: "#FFFFFF"})
|
||||
return
|
||||
}
|
||||
rq.navigate() // TODO: if error from navigate do not capture
|
||||
rq.captureScreenshot()
|
||||
}
|
||||
|
||||
// TODO: merge this with html mode IMGZ
|
||||
func imgServerMap(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path)
|
||||
imgBuf, ok := img[r.URL.Path]
|
||||
if !ok || imgBuf.Bytes() == nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
if !*noDel {
|
||||
defer delete(img, r.URL.Path)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, ".gif"):
|
||||
w.Header().Set("Content-Type", "image/gif")
|
||||
case strings.HasSuffix(r.URL.Path, ".png"):
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
case strings.HasSuffix(r.URL.Path, ".jpg"):
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(imgBuf.Bytes())))
|
||||
w.Header().Set("Cache-Control", "max-age=0")
|
||||
w.Header().Set("Expires", "-1")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Write(imgBuf.Bytes())
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
@ -1,16 +1,22 @@
|
||||
// WRP TXT / Simple HTML Mode Routines
|
||||
package main
|
||||
|
||||
// TODO:
|
||||
// - image type based on form value
|
||||
// - also size and quality
|
||||
// - non overlaping image names atomic.int etc
|
||||
// - garbage collector / delete old images from map
|
||||
// - add image processing times counter to the footer
|
||||
// - img cache w/garbage collector / test back/button behavior in old browsers
|
||||
// - add referer header
|
||||
// - svg support
|
||||
// - BOG: DomainFromURL always prefixes with http instead of https
|
||||
// - incorrect cert support in both markdown and image download
|
||||
// - unify cdp and txt image handlers
|
||||
// - use goroutiness to process images
|
||||
// - get inner html from chromedp instead of html2markdown
|
||||
//
|
||||
// - BUG: DomainFromURL always prefixes with http instead of https
|
||||
// reproduces on vsi vms docs
|
||||
// - BUG: markdown table errors
|
||||
// reproduces on hacker news
|
||||
// - BUG: captcha errors using html to markdown, perhaps use cdp inner html + downloaded images
|
||||
// reproduces on https://www.cnn.com/cnn-underscored/electronics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -23,7 +29,6 @@ import (
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -32,6 +37,7 @@ import (
|
||||
|
||||
h2m "github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/JohannesKaufmann/html-to-markdown/plugin"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
@ -48,10 +54,6 @@ const imgZpfx = "/imgz/"
|
||||
|
||||
func init() {
|
||||
imgStor.img = make(map[string]imageContainer)
|
||||
// TODO: add garbage collector
|
||||
// think about how to remove old images
|
||||
// if removed from cache how to download them later if a browser goes back?
|
||||
// browser should cache on it's own... but it may request it, what then?
|
||||
}
|
||||
|
||||
type imageContainer struct {
|
||||
@ -87,112 +89,43 @@ func (i *imageStore) del(id string) {
|
||||
delete(i.img, id)
|
||||
}
|
||||
|
||||
func fetchImage(id, url string) error {
|
||||
func fetchImage(id, url, imgType string, maxSize, imgOpt int) (int, error) {
|
||||
log.Printf("Downloading IMGZ URL=%q for ID=%q", url, id)
|
||||
var img []byte
|
||||
var in []byte
|
||||
var err error
|
||||
switch url[:4] {
|
||||
case "http":
|
||||
r, err := http.Get(url) // TODO: possibly set a header "referer" here
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error downloading %q: %v", url, err)
|
||||
return 0, fmt.Errorf("Error downloading %q: %v", url, err)
|
||||
}
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Error %q HTTP Status Code: %v", url, r.StatusCode)
|
||||
return 0, fmt.Errorf("Error %q HTTP Status Code: %v", url, r.StatusCode)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
img, err = io.ReadAll(r.Body)
|
||||
in, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading %q: %v", url, err)
|
||||
return 0, fmt.Errorf("Error reading %q: %v", url, err)
|
||||
}
|
||||
case "data":
|
||||
idx := strings.Index(url, ",")
|
||||
if idx < 1 {
|
||||
return fmt.Errorf("image is embeded but unable to find coma: %q", url)
|
||||
return 0, fmt.Errorf("image is embeded but unable to find coma: %q", url)
|
||||
}
|
||||
img, err = base64.StdEncoding.DecodeString(url[idx+1:])
|
||||
in, err = base64.StdEncoding.DecodeString(url[idx+1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding image from url embed: %q: %v", url, err)
|
||||
return 0, fmt.Errorf("error decoding image from url embed: %q: %v", url, err)
|
||||
}
|
||||
}
|
||||
gif, err := smallGif(img)
|
||||
out, err := smallImg(in, imgType, maxSize, imgOpt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error scaling down image: %v", err)
|
||||
return 0, fmt.Errorf("Error scaling down image: %v", err)
|
||||
}
|
||||
imgStor.add(id, url, gif)
|
||||
return nil
|
||||
imgStor.add(id, url, out)
|
||||
return len(out), nil
|
||||
}
|
||||
|
||||
type astTransformer struct{}
|
||||
|
||||
func (t *astTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if link, ok := n.(*ast.Link); ok && entering {
|
||||
link.Destination = append([]byte("/?t=txt&url="), link.Destination...)
|
||||
}
|
||||
if img, ok := n.(*ast.Image); ok && entering {
|
||||
// TODO: dynamic extension based on form value
|
||||
id := fmt.Sprintf("txt%05d.gif", rand.Intn(99999)) // BUG: atomic.AddInt64 or something that ever increases - time based?
|
||||
err := fetchImage(id, string(img.Destination)) // TODO: use goroutines with waitgroup
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
n.Parent().RemoveChildren(n)
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
img.Destination = []byte(imgZpfx + id)
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (rq *wrpReq) captureMarkdown() {
|
||||
log.Printf("Processing Markdown conversion request for %v", rq.url)
|
||||
// TODO: bug - DomainFromURL always prefixes with http:// instead of https
|
||||
// this causes issues on some websites, fix or write a smarter DomainFromURL
|
||||
c := h2m.NewConverter(h2m.DomainFromURL(rq.url), true, nil)
|
||||
c.Use(plugin.GitHubFlavored())
|
||||
md, err := c.ConvertURL(rq.url) // We could also get inner html from chromedp
|
||||
if err != nil {
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Got %v bytes md from %v", len(md), rq.url)
|
||||
t := &astTransformer{}
|
||||
gm := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithParserOptions(parser.WithASTTransformers(util.Prioritized(t, 100))),
|
||||
)
|
||||
var ht bytes.Buffer
|
||||
err = gm.Convert([]byte(md), &ht)
|
||||
if err != nil {
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Rendered %v bytes html for %v", len(ht.String()), rq.url)
|
||||
rq.printHTML(printParams{
|
||||
text: string(asciify([]byte(ht.String()))),
|
||||
bgColor: "#FFFFFF",
|
||||
})
|
||||
}
|
||||
|
||||
func imgServerZ(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMGZ Request for %s", r.RemoteAddr, r.URL.Path)
|
||||
id := strings.Replace(r.URL.Path, imgZpfx, "", 1)
|
||||
img, err := imgStor.get(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Printf("%s IMGZ error for %s: %v", r.RemoteAddr, r.URL.Path, err)
|
||||
return
|
||||
}
|
||||
imgStor.del(id)
|
||||
w.Header().Set("Content-Type", http.DetectContentType(img))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(img)))
|
||||
w.Write(img)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// TODO set JPG/GIF/PNG type based on form...
|
||||
func smallGif(src []byte) ([]byte, error) {
|
||||
func smallImg(src []byte, imgType string, maxSize, imgOpt int) ([]byte, error) {
|
||||
t := http.DetectContentType(src)
|
||||
var err error
|
||||
var img image.Image
|
||||
@ -211,11 +144,99 @@ func smallGif(src []byte) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("image decode problem: %v", err)
|
||||
}
|
||||
img = resize.Thumbnail(uint(*txtImgSize), uint(*txtImgSize), img, resize.NearestNeighbor)
|
||||
var gifBuf bytes.Buffer
|
||||
err = gif.Encode(&gifBuf, gifPalette(img, 216), &gif.Options{})
|
||||
img = resize.Thumbnail(uint(maxSize), uint(maxSize), img, resize.NearestNeighbor)
|
||||
var outBuf bytes.Buffer
|
||||
switch imgType {
|
||||
case "png":
|
||||
err = png.Encode(&outBuf, img)
|
||||
case "gif":
|
||||
err = gif.Encode(&outBuf, gifPalette(img, int64(imgOpt)), &gif.Options{})
|
||||
case "jpg":
|
||||
err = jpeg.Encode(&outBuf, img, &jpeg.Options{Quality: imgOpt})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gif encode problem: %v", err)
|
||||
}
|
||||
return gifBuf.Bytes(), nil
|
||||
return outBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
type astTransformer struct {
|
||||
imgType string
|
||||
maxSize int
|
||||
imgOpt int
|
||||
totSize int
|
||||
}
|
||||
|
||||
func (t *astTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if link, ok := n.(*ast.Link); ok && entering {
|
||||
link.Destination = append([]byte("/?m=html&t="+t.imgType+"&s="+strconv.Itoa(t.maxSize)+"&url="), link.Destination...)
|
||||
}
|
||||
if img, ok := n.(*ast.Image); ok && entering {
|
||||
seq := shortuuid.New() + "." + t.imgType
|
||||
size, err := fetchImage(seq, string(img.Destination), t.imgType, t.maxSize, t.imgOpt) // TODO: use goroutines with waitgroup
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
n.Parent().RemoveChildren(n)
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
img.Destination = []byte(imgZpfx + seq)
|
||||
t.totSize += size
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (rq *wrpReq) captureMarkdown() {
|
||||
log.Printf("Processing Markdown conversion request for %v", rq.url)
|
||||
// TODO: bug - DomainFromURL always prefixes with http:// instead of https
|
||||
// this causes issues on some websites, fix or write a smarter DomainFromURL
|
||||
c := h2m.NewConverter(h2m.DomainFromURL(rq.url), true, nil)
|
||||
c.Use(plugin.GitHubFlavored())
|
||||
md, err := c.ConvertURL(rq.url) // We could also get inner html from chromedp
|
||||
if err != nil {
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Got %v bytes md from %v", len(md), rq.url)
|
||||
var imgOpt int
|
||||
switch rq.imgType {
|
||||
case "jpg":
|
||||
imgOpt = int(rq.jQual)
|
||||
case "gif":
|
||||
imgOpt = int(rq.nColors)
|
||||
}
|
||||
t := &astTransformer{imgType: rq.imgType, maxSize: int(rq.maxSize), imgOpt: imgOpt}
|
||||
gm := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithParserOptions(parser.WithASTTransformers(util.Prioritized(t, 100))),
|
||||
)
|
||||
var ht bytes.Buffer
|
||||
err = gm.Convert([]byte(md), &ht)
|
||||
if err != nil {
|
||||
http.Error(rq.w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Rendered %v bytes html for %v", len(ht.String()), rq.url)
|
||||
rq.printUI(uiParams{
|
||||
text: string(asciify([]byte(ht.String()))),
|
||||
bgColor: "#FFFFFF",
|
||||
imgSize: fmt.Sprintf("%.0f KB", float32(t.totSize)/1024.0),
|
||||
})
|
||||
}
|
||||
|
||||
func imgServerTxt(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMGZ Request for %s", r.RemoteAddr, r.URL.Path)
|
||||
id := strings.Replace(r.URL.Path, imgZpfx, "", 1)
|
||||
img, err := imgStor.get(id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Printf("%s IMGZ error for %s: %v", r.RemoteAddr, r.URL.Path, err)
|
||||
return
|
||||
}
|
||||
imgStor.del(id)
|
||||
w.Header().Set("Content-Type", http.DetectContentType(img))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(img)))
|
||||
w.Write(img)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
89
util.go
Normal file
89
util.go
Normal file
@ -0,0 +1,89 @@
|
||||
// wrp utility functions
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color/palette"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/MaxHalford/halfgone"
|
||||
"github.com/soniakeys/quant/median"
|
||||
)
|
||||
|
||||
func printMyIPs(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)
|
||||
}
|
||||
|
||||
func gifPalette(i image.Image, n int64) image.Image {
|
||||
switch n {
|
||||
case 2:
|
||||
i = halfgone.FloydSteinbergDitherer{}.Apply(halfgone.ImageToGray(i))
|
||||
case 216:
|
||||
var FastGifLut = [256]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}
|
||||
r := i.Bounds()
|
||||
// NOTE: the color index computation below works only for palette.WebSafe!
|
||||
p := image.NewPaletted(r, palette.WebSafe)
|
||||
if i64, ok := i.(image.RGBA64Image); ok {
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
c := i64.RGBA64At(x, y)
|
||||
r6 := FastGifLut[c.R>>8]
|
||||
g6 := FastGifLut[c.G>>8]
|
||||
b6 := FastGifLut[c.B>>8]
|
||||
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
c := i.At(x, y)
|
||||
r, g, b, _ := c.RGBA()
|
||||
r6 := FastGifLut[r&0xff]
|
||||
g6 := FastGifLut[g&0xff]
|
||||
b6 := FastGifLut[b&0xff]
|
||||
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
|
||||
}
|
||||
}
|
||||
}
|
||||
i = p
|
||||
default:
|
||||
q := median.Quantizer(n)
|
||||
i = q.Paletted(i)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func asciify(s []byte) []byte {
|
||||
a := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > 127 {
|
||||
a[i] = '.'
|
||||
continue
|
||||
}
|
||||
a[i] = s[i]
|
||||
}
|
||||
return a
|
||||
}
|
484
wrp.go
484
wrp.go
@ -13,17 +13,8 @@ import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color/palette"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -35,29 +26,26 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/MaxHalford/halfgone"
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/chromedp/cdproto/css"
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/input"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/soniakeys/quant/median"
|
||||
)
|
||||
|
||||
const version = "4.7.2"
|
||||
const version = "4.8.0"
|
||||
|
||||
var (
|
||||
addr = flag.String("l", ":8080", "Listen address:port, default :8080")
|
||||
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")
|
||||
defType = flag.String("t", "gif", "Image type: png|gif|jpg")
|
||||
wrpMode = flag.String("m", "ismap", "WRP Mode: ismap|html")
|
||||
defImgSize = flag.Int64("is", 200, "html mode default image size")
|
||||
defJpgQual = flag.Int64("q", 75, "Jpeg image quality, default 75%") // TODO: this should be form dropdown when jpeg is selected as image type
|
||||
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")
|
||||
delay = flag.Duration("s", 2*time.Second, "Delay/sleep after page is rendered and before screenshot is taken")
|
||||
userAgent = flag.String("ua", "", "override chrome user agent")
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("l", ":8080", "Listen address:port, default :8080")
|
||||
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")
|
||||
defType = flag.String("t", "gif", "Image type: png|gif|jpg|txt")
|
||||
txtImgSize = flag.Int("ts", 200, "txt mode image size") // make it default, this should come from the from
|
||||
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")
|
||||
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")
|
||||
userAgent = flag.String("ua", "", "override chrome user agent")
|
||||
srv http.Server
|
||||
actx, ctx context.Context
|
||||
acncl, cncl context.CancelFunc
|
||||
@ -76,12 +64,17 @@ type geom struct {
|
||||
c int64
|
||||
}
|
||||
|
||||
// TODO: there is a major overlap/duplication/triplication
|
||||
// between the 3 data structs, perhps we could reduce to just one?
|
||||
|
||||
// Data for html template
|
||||
type uiData struct {
|
||||
Version string
|
||||
WrpMode string
|
||||
URL string
|
||||
BgColor string
|
||||
NColors int64
|
||||
JQual int64
|
||||
Width int64
|
||||
Height int64
|
||||
Zoom float64
|
||||
@ -90,13 +83,14 @@ type uiData struct {
|
||||
ImgSize string
|
||||
ImgWidth int
|
||||
ImgHeight int
|
||||
MaxSize int64
|
||||
MapURL string
|
||||
PageHeight string
|
||||
TeXT string
|
||||
}
|
||||
|
||||
// Parameters for HTML print function
|
||||
type printParams struct {
|
||||
type uiParams struct {
|
||||
bgColor string
|
||||
pageHeight string
|
||||
imgSize string
|
||||
@ -109,27 +103,34 @@ type printParams struct {
|
||||
|
||||
// WRP Request
|
||||
type wrpReq struct {
|
||||
url string // url
|
||||
width int64 // width
|
||||
height int64 // height
|
||||
zoom float64 // zoom/scale
|
||||
colors int64 // #colors
|
||||
mouseX int64 // mouseX
|
||||
mouseY int64 // mouseY
|
||||
keys string // keys to send
|
||||
buttons string // Fn buttons
|
||||
imgType string // imgtype
|
||||
url string
|
||||
width int64
|
||||
height int64
|
||||
zoom float64
|
||||
nColors int64
|
||||
jQual int64
|
||||
mouseX int64
|
||||
mouseY int64
|
||||
keys string
|
||||
buttons string
|
||||
imgType string
|
||||
wrpMode string
|
||||
maxSize int64
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
}
|
||||
|
||||
// Parse HTML Form, Process Input Boxes, Etc.
|
||||
func (rq *wrpReq) parseForm() {
|
||||
rq.r.ParseForm()
|
||||
rq.wrpMode = rq.r.FormValue("m")
|
||||
if rq.wrpMode == "" {
|
||||
rq.wrpMode = *wrpMode
|
||||
}
|
||||
rq.url = rq.r.FormValue("url")
|
||||
if len(rq.url) > 1 && !strings.HasPrefix(rq.url, "http") {
|
||||
rq.url = fmt.Sprintf("http://www.google.com/search?q=%s", url.QueryEscape(rq.url))
|
||||
}
|
||||
// TODO: implement atoiOrZero
|
||||
rq.width, _ = strconv.ParseInt(rq.r.FormValue("w"), 10, 64)
|
||||
rq.height, _ = strconv.ParseInt(rq.r.FormValue("h"), 10, 64)
|
||||
if rq.width < 10 && rq.height < 10 {
|
||||
@ -140,26 +141,30 @@ func (rq *wrpReq) parseForm() {
|
||||
if rq.zoom < 0.1 {
|
||||
rq.zoom = 1.0
|
||||
}
|
||||
rq.colors, _ = strconv.ParseInt(rq.r.FormValue("c"), 10, 64)
|
||||
if rq.colors < 2 || rq.colors > 256 {
|
||||
rq.colors = defGeom.c
|
||||
rq.imgType = rq.r.FormValue("t")
|
||||
switch rq.imgType {
|
||||
case "png", "gif", "jpg":
|
||||
default:
|
||||
rq.imgType = *defType
|
||||
}
|
||||
rq.nColors, _ = strconv.ParseInt(rq.r.FormValue("c"), 10, 64)
|
||||
if rq.nColors < 2 || rq.nColors > 256 {
|
||||
rq.nColors = defGeom.c
|
||||
}
|
||||
rq.jQual, _ = strconv.ParseInt(rq.r.FormValue("q"), 10, 64)
|
||||
if rq.jQual < 1 || rq.jQual > 100 {
|
||||
rq.jQual = *defJpgQual
|
||||
}
|
||||
rq.keys = rq.r.FormValue("k")
|
||||
rq.buttons = rq.r.FormValue("Fn")
|
||||
rq.imgType = rq.r.FormValue("t")
|
||||
switch rq.imgType {
|
||||
case "png":
|
||||
case "gif":
|
||||
case "jpg":
|
||||
case "txt":
|
||||
default:
|
||||
rq.imgType = *defType
|
||||
rq.maxSize, _ = strconv.ParseInt(rq.r.FormValue("s"), 10, 64)
|
||||
if rq.maxSize == 0 {
|
||||
rq.maxSize = *defImgSize
|
||||
}
|
||||
log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq)
|
||||
}
|
||||
|
||||
// Display WP UI
|
||||
func (rq *wrpReq) printHTML(p printParams) {
|
||||
func (rq *wrpReq) printUI(p uiParams) {
|
||||
rq.w.Header().Set("Cache-Control", "max-age=0")
|
||||
rq.w.Header().Set("Expires", "-1")
|
||||
rq.w.Header().Set("Pragma", "no-cache")
|
||||
@ -169,12 +174,15 @@ func (rq *wrpReq) printHTML(p printParams) {
|
||||
}
|
||||
data := uiData{
|
||||
Version: version,
|
||||
WrpMode: rq.wrpMode,
|
||||
URL: rq.url,
|
||||
BgColor: p.bgColor,
|
||||
Width: rq.width,
|
||||
Height: rq.height,
|
||||
NColors: rq.colors,
|
||||
NColors: rq.nColors,
|
||||
JQual: rq.jQual,
|
||||
Zoom: rq.zoom,
|
||||
MaxSize: rq.maxSize,
|
||||
ImgType: rq.imgType,
|
||||
ImgSize: p.imgSize,
|
||||
ImgWidth: p.imgWidth,
|
||||
@ -190,253 +198,6 @@ func (rq *wrpReq) printHTML(p printParams) {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what action to take
|
||||
func (rq *wrpReq) action() chromedp.Action {
|
||||
// Mouse Click
|
||||
if rq.mouseX > 0 && rq.mouseY > 0 {
|
||||
log.Printf("%s Mouse Click %d,%d\n", rq.r.RemoteAddr, rq.mouseX, rq.mouseY)
|
||||
return chromedp.MouseClickXY(float64(rq.mouseX)/float64(rq.zoom), float64(rq.mouseY)/float64(rq.zoom))
|
||||
}
|
||||
// Buttons
|
||||
if len(rq.buttons) > 0 {
|
||||
log.Printf("%s Button %v\n", rq.r.RemoteAddr, rq.buttons)
|
||||
switch rq.buttons {
|
||||
case "Bk":
|
||||
return chromedp.NavigateBack()
|
||||
case "St":
|
||||
return chromedp.Stop()
|
||||
case "Re":
|
||||
return chromedp.Reload()
|
||||
case "Bs":
|
||||
return chromedp.KeyEvent("\b")
|
||||
case "Rt":
|
||||
return chromedp.KeyEvent("\r")
|
||||
case "<":
|
||||
return chromedp.KeyEvent("\u0302")
|
||||
case "^":
|
||||
return chromedp.KeyEvent("\u0304")
|
||||
case "v":
|
||||
return chromedp.KeyEvent("\u0301")
|
||||
case ">":
|
||||
return chromedp.KeyEvent("\u0303")
|
||||
case "Up":
|
||||
return chromedp.KeyEvent("\u0308")
|
||||
case "Dn":
|
||||
return chromedp.KeyEvent("\u0307")
|
||||
case "All": // Select all
|
||||
return chromedp.KeyEvent("a", chromedp.KeyModifiers(input.ModifierCtrl))
|
||||
}
|
||||
}
|
||||
// Keys
|
||||
if len(rq.keys) > 0 {
|
||||
log.Printf("%s Sending Keys: %#v\n", rq.r.RemoteAddr, rq.keys)
|
||||
return chromedp.KeyEvent(rq.keys)
|
||||
}
|
||||
// Navigate to URL
|
||||
log.Printf("%s Processing Navigate Request for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
return chromedp.Navigate(rq.url)
|
||||
}
|
||||
|
||||
// Navigate to the desired URL.
|
||||
func (rq *wrpReq) navigate() {
|
||||
ctxErr(chromedp.Run(ctx, rq.action()), rq.w)
|
||||
}
|
||||
|
||||
// Handle context errors
|
||||
func ctxErr(err error, w io.Writer) {
|
||||
// TODO: callers should have retry logic, perhaps create another function
|
||||
// that takes ...chromedp.Action and retries with give up
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("Context error: %s", err)
|
||||
fmt.Fprintf(w, "Context error: %s<BR>\n", err)
|
||||
if err.Error() != "context canceled" {
|
||||
return
|
||||
}
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
log.Printf("Created new context, try again")
|
||||
fmt.Fprintln(w, "Created new context, try again")
|
||||
}
|
||||
|
||||
// https://github.com/chromedp/chromedp/issues/979
|
||||
func chromedpCaptureScreenshot(res *[]byte, h int64) chromedp.Action {
|
||||
if res == nil {
|
||||
panic("res cannot be nil")
|
||||
}
|
||||
if h == 0 {
|
||||
return chromedp.CaptureScreenshot(res)
|
||||
}
|
||||
|
||||
return chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var err error
|
||||
*res, err = page.CaptureScreenshot().Do(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func gifPalette(i image.Image, n int64) image.Image {
|
||||
switch n {
|
||||
case 2:
|
||||
i = halfgone.FloydSteinbergDitherer{}.Apply(halfgone.ImageToGray(i))
|
||||
case 216:
|
||||
var FastGifLut = [256]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}
|
||||
r := i.Bounds()
|
||||
// NOTE: the color index computation below works only for palette.WebSafe!
|
||||
p := image.NewPaletted(r, palette.WebSafe)
|
||||
if i64, ok := i.(image.RGBA64Image); ok {
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
c := i64.RGBA64At(x, y)
|
||||
r6 := FastGifLut[c.R>>8]
|
||||
g6 := FastGifLut[c.G>>8]
|
||||
b6 := FastGifLut[c.B>>8]
|
||||
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
c := i.At(x, y)
|
||||
r, g, b, _ := c.RGBA()
|
||||
r6 := FastGifLut[r&0xff]
|
||||
g6 := FastGifLut[g&0xff]
|
||||
b6 := FastGifLut[b&0xff]
|
||||
p.SetColorIndex(x, y, uint8(36*r6+6*g6+b6))
|
||||
}
|
||||
}
|
||||
}
|
||||
i = p
|
||||
default:
|
||||
q := median.Quantizer(n)
|
||||
i = q.Paletted(i)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (rq *wrpReq) captureImage() {
|
||||
var styles []*css.ComputedStyleProperty
|
||||
var r, g, b int
|
||||
var bgColorSet bool
|
||||
var h int64
|
||||
var pngCap []byte
|
||||
chromedp.Run(ctx,
|
||||
emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), 10, rq.zoom, false),
|
||||
chromedp.Location(&rq.url),
|
||||
chromedp.ComputedStyle("body", &styles, chromedp.ByQuery),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
_, _, _, _, _, s, err := page.GetLayoutMetrics().Do(ctx)
|
||||
if err == nil {
|
||||
h = int64(math.Ceil(s.Height))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
log.Printf("%s Landed on: %s, Height: %v\n", rq.r.RemoteAddr, rq.url, h)
|
||||
for _, style := range styles {
|
||||
if style.Name != "background-color" {
|
||||
continue
|
||||
}
|
||||
fmt.Sscanf(style.Value, "rgb(%d,%d,%d)", &r, &g, &b)
|
||||
bgColorSet = true
|
||||
break
|
||||
}
|
||||
if !bgColorSet {
|
||||
r = 255
|
||||
g = 255
|
||||
b = 255
|
||||
}
|
||||
height := int64(float64(rq.height) / rq.zoom)
|
||||
if rq.height == 0 && h > 0 {
|
||||
height = h + 30
|
||||
}
|
||||
chromedp.Run(
|
||||
ctx, emulation.SetDeviceMetricsOverride(int64(float64(rq.width)/rq.zoom), height, rq.zoom, false),
|
||||
chromedp.Sleep(*delay), // TODO(tenox): find a better way to determine if page is rendered
|
||||
)
|
||||
// Capture screenshot...
|
||||
ctxErr(chromedp.Run(ctx, chromedpCaptureScreenshot(&pngCap, rq.height)), rq.w)
|
||||
seq := rand.Intn(9999)
|
||||
imgPath := fmt.Sprintf("/img/%04d.%s", seq, rq.imgType)
|
||||
mapPath := fmt.Sprintf("/map/%04d.map", seq)
|
||||
ismap[mapPath] = *rq
|
||||
var sSize string
|
||||
var iW, iH int
|
||||
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":
|
||||
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 gifBuf bytes.Buffer
|
||||
err = gif.Encode(&gifBuf, gifPalette(i, rq.colors), &gif.Options{})
|
||||
if err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
img[imgPath] = gifBuf
|
||||
sSize = fmt.Sprintf("%.0f KB", float32(len(gifBuf.Bytes()))/1024.0)
|
||||
iW = i.Bounds().Max.X
|
||||
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())
|
||||
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())
|
||||
}
|
||||
rq.printHTML(printParams{
|
||||
bgColor: fmt.Sprintf("#%02X%02X%02X", r, g, b),
|
||||
pageHeight: fmt.Sprintf("%d PX", h),
|
||||
imgSize: sSize,
|
||||
imgURL: imgPath,
|
||||
mapURL: mapPath,
|
||||
imgWidth: iW,
|
||||
imgHeight: iH,
|
||||
})
|
||||
log.Printf("%s Done with capture for %s\n", rq.r.RemoteAddr, rq.url)
|
||||
}
|
||||
|
||||
func asciify(s []byte) []byte {
|
||||
a := make([]byte, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > 127 {
|
||||
a[i] = '.'
|
||||
continue
|
||||
}
|
||||
a[i] = s[i]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Process HTTP requests to WRP '/' url
|
||||
func pageServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s Page Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
|
||||
rq := wrpReq{
|
||||
@ -445,80 +206,19 @@ func pageServer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
rq.parseForm()
|
||||
if len(rq.url) < 4 {
|
||||
rq.printHTML(printParams{bgColor: "#FFFFFF"})
|
||||
rq.printUI(uiParams{bgColor: "#FFFFFF"})
|
||||
return
|
||||
}
|
||||
rq.navigate() // TODO: if error from navigate do not capture
|
||||
if rq.imgType == "txt" {
|
||||
if rq.wrpMode == "html" {
|
||||
rq.captureMarkdown()
|
||||
return
|
||||
}
|
||||
rq.captureImage()
|
||||
rq.captureScreenshot()
|
||||
}
|
||||
|
||||
// Process HTTP requests to ISMAP '/map/' url
|
||||
func mapServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s ISMAP Request for %s [%+v]\n", r.RemoteAddr, r.URL.Path, r.URL.RawQuery)
|
||||
rq, ok := ismap[r.URL.Path]
|
||||
rq.r = r
|
||||
rq.w = w
|
||||
if !ok {
|
||||
fmt.Fprintf(w, "Unable to find map %s\n", r.URL.Path)
|
||||
log.Printf("Unable to find map %s\n", r.URL.Path)
|
||||
return
|
||||
}
|
||||
if !*noDel {
|
||||
defer delete(ismap, r.URL.Path)
|
||||
}
|
||||
n, err := fmt.Sscanf(r.URL.RawQuery, "%d,%d", &rq.mouseX, &rq.mouseY)
|
||||
if err != nil || n != 2 {
|
||||
fmt.Fprintf(w, "n=%d, err=%s\n", n, err)
|
||||
log.Printf("%s ISMAP n=%d, err=%s\n", r.RemoteAddr, n, err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s WrpReq from ISMAP: %+v\n", r.RemoteAddr, rq)
|
||||
if len(rq.url) < 4 {
|
||||
rq.printHTML(printParams{bgColor: "#FFFFFF"})
|
||||
return
|
||||
}
|
||||
rq.navigate() // TODO: if error from navigate do not capture
|
||||
rq.captureImage()
|
||||
}
|
||||
|
||||
// Process HTTP requests for images '/img/' url
|
||||
func imgServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s IMG Request for %s\n", r.RemoteAddr, r.URL.Path)
|
||||
imgBuf, ok := img[r.URL.Path]
|
||||
if !ok || imgBuf.Bytes() == nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
if !*noDel {
|
||||
defer delete(img, r.URL.Path)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, ".gif"):
|
||||
w.Header().Set("Content-Type", "image/gif")
|
||||
case strings.HasSuffix(r.URL.Path, ".png"):
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
case strings.HasSuffix(r.URL.Path, ".jpg"):
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(imgBuf.Bytes())))
|
||||
w.Header().Set("Cache-Control", "max-age=0")
|
||||
w.Header().Set("Expires", "-1")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Write(imgBuf.Bytes())
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// Process HTTP requests for Shutdown via '/shutdown/' url
|
||||
func haltServer(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s Shutdown Request for %s\n", r.RemoteAddr, r.URL.Path)
|
||||
w.Header().Set("Cache-Control", "max-age=0")
|
||||
w.Header().Set("Expires", "-1")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintf(w, "Shutting down WRP...\n")
|
||||
w.(http.Flusher).Flush()
|
||||
@ -529,8 +229,7 @@ func haltServer(w http.ResponseWriter, r *http.Request) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// returns html template, either from html file or built-in
|
||||
func tmpl(t string) string {
|
||||
func wrpTemplate(t string) string {
|
||||
var tmpl []byte
|
||||
fh, err := os.Open(t)
|
||||
if err != nil {
|
||||
@ -538,7 +237,7 @@ func tmpl(t string) string {
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
tmpl, err = ioutil.ReadAll(fh)
|
||||
tmpl, err = io.ReadAll(fh)
|
||||
if err != nil {
|
||||
goto builtin
|
||||
}
|
||||
@ -552,7 +251,7 @@ builtin:
|
||||
}
|
||||
defer fhs.Close()
|
||||
|
||||
tmpl, err = ioutil.ReadAll(fhs)
|
||||
tmpl, err = io.ReadAll(fhs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -560,33 +259,6 @@ builtin:
|
||||
return string(tmpl)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
var err error
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
@ -597,25 +269,15 @@ func main() {
|
||||
if len(os.Getenv("PORT")) > 0 {
|
||||
*addr = ":" + os.Getenv(("PORT"))
|
||||
}
|
||||
printIPs(*addr)
|
||||
printMyIPs(*addr)
|
||||
n, err := fmt.Sscanf(*fgeom, "%dx%dx%d", &defGeom.w, &defGeom.h, &defGeom.c)
|
||||
if err != nil || n != 3 {
|
||||
log.Fatalf("Unable to parse -g geometry flag / %s", err)
|
||||
}
|
||||
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", *headless),
|
||||
chromedp.Flag("hide-scrollbars", false),
|
||||
chromedp.Flag("enable-automation", false),
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
)
|
||||
if *userAgent != "" {
|
||||
opts = append(opts, chromedp.UserAgent(*userAgent))
|
||||
}
|
||||
actx, acncl = chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer acncl()
|
||||
ctx, cncl = chromedp.NewContext(actx)
|
||||
cncl, acncl = chromedpStart()
|
||||
defer cncl()
|
||||
defer acncl()
|
||||
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||