diff --git a/cdp.go b/cdp.go new file mode 100644 index 0000000..4e04a2f --- /dev/null +++ b/cdp.go @@ -0,0 +1,263 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "log" + "math" + "math/rand" + "net/http" + "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" +) + +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
\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 + }) +} + +// 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 := 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, "
Unable to decode page PNG screenshot:
%s
\n", err) + return + } + st := time.Now() + var gifBuf bytes.Buffer + err = gif.Encode(&gifBuf, gifPalette(i, rq.imgOpt), &gif.Options{}) + if err != nil { + log.Printf("%s Failed to encode GIF: %s\n", rq.r.RemoteAddr, err) + fmt.Fprintf(rq.w, "
Unable to encode GIF:
%s
\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.imgOpt, 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, "
Unable to decode page PNG screenshot:
%s
\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, "
Unable to encode JPG:
%s
\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) +} + +// 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.captureScreenshot() +} diff --git a/txt.go b/txt.go index 9821e69..f1791f8 100644 --- a/txt.go +++ b/txt.go @@ -87,7 +87,7 @@ 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) error { log.Printf("Downloading IMGZ URL=%q for ID=%q", url, id) var img []byte var err error @@ -115,7 +115,7 @@ func fetchImage(id, url string) error { return fmt.Errorf("error decoding image from url embed: %q: %v", url, err) } } - gif, err := smallGif(img) + gif, err := smallImg(img, imgType, maxSize, imgOpt) if err != nil { return fmt.Errorf("Error scaling down image: %v", err) } @@ -123,7 +123,46 @@ func fetchImage(id, url string) error { return nil } -type astTransformer struct{} +func smallImg(src []byte, imgType string, maxSize, imgOpt int) ([]byte, error) { + t := http.DetectContentType(src) + var err error + var img image.Image + switch t { + case "image/png": + img, err = png.Decode(bytes.NewReader(src)) + case "image/gif": + img, err = gif.Decode(bytes.NewReader(src)) + case "image/jpeg": + img, err = jpeg.Decode(bytes.NewReader(src)) + case "image/webp": + img, err = webp.Decode(bytes.NewReader(src)) + default: // TODO: also add svg + err = errors.New("unknown content type: " + t) + } + if err != nil { + return nil, fmt.Errorf("image decode problem: %v", err) + } + img = resize.Thumbnail(uint(*defImgSize), uint(*defImgSize), 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 outBuf.Bytes(), nil +} + +type astTransformer struct { + imgType string + maxSize int + imgOpt 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) { @@ -132,8 +171,8 @@ func (t *astTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } 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 + 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), t.imgType, t.maxSize, t.imgOpt) // TODO: use goroutines with waitgroup if err != nil { log.Print(err) n.Parent().RemoveChildren(n) @@ -145,6 +184,18 @@ func (t *astTransformer) Transform(node *ast.Document, reader text.Reader, pc pa }) } +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 +} + func (rq *wrpReq) captureMarkdown() { log.Printf("Processing Markdown conversion request for %v", rq.url) // TODO: bug - DomainFromURL always prefixes with http:// instead of https @@ -157,7 +208,7 @@ func (rq *wrpReq) captureMarkdown() { return } log.Printf("Got %v bytes md from %v", len(md), rq.url) - t := &astTransformer{} + t := &astTransformer{imgType: rq.imgType, maxSize: int(rq.maxSize), imgOpt: int(rq.imgOpt)} // TODO: maxSize still doesn't work gm := goldmark.New( goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions(parser.WithASTTransformers(util.Prioritized(t, 100))), @@ -190,32 +241,3 @@ func imgServerZ(w http.ResponseWriter, r *http.Request) { w.Write(img) w.(http.Flusher).Flush() } - -// TODO set JPG/GIF/PNG type based on form... -func smallGif(src []byte) ([]byte, error) { - t := http.DetectContentType(src) - var err error - var img image.Image - switch t { - case "image/png": - img, err = png.Decode(bytes.NewReader(src)) - case "image/gif": - img, err = gif.Decode(bytes.NewReader(src)) - case "image/jpeg": - img, err = jpeg.Decode(bytes.NewReader(src)) - case "image/webp": - img, err = webp.Decode(bytes.NewReader(src)) - default: // TODO: also add svg - err = errors.New("unknown content type: " + t) - } - 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{}) - if err != nil { - return nil, fmt.Errorf("gif encode problem: %v", err) - } - return gifBuf.Bytes(), nil -} diff --git a/wrp.go b/wrp.go index 49850d9..c8fcc23 100644 --- a/wrp.go +++ b/wrp.go @@ -15,14 +15,8 @@ import ( "fmt" "image" "image/color/palette" - "image/gif" - "image/jpeg" - "image/png" "io" - "io/ioutil" "log" - "math" - "math/rand" "net" "net/http" "net/url" @@ -36,27 +30,26 @@ import ( "time" "github.com/MaxHalford/halfgone" - "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.0" +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") + jpgQual = flag.Int("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 @@ -78,6 +71,7 @@ type geom struct { // Data for html template type uiData struct { Version string + WrpMode string URL string BgColor string NColors int64 @@ -89,6 +83,7 @@ type uiData struct { ImgSize string ImgWidth int ImgHeight int + MaxSize int64 MapURL string PageHeight string TeXT string @@ -118,6 +113,9 @@ type wrpReq struct { keys string // keys to send buttons string // Fn buttons imgType string // imgtype + wrpMode string // mode ismap/html + maxSize int64 // image max size for html mode + imgOpt int64 w http.ResponseWriter r *http.Request } @@ -125,10 +123,15 @@ type wrpReq struct { // 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 { @@ -139,20 +142,26 @@ 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, _ = strconv.ParseInt(rq.r.FormValue("c"), 10, 64) // TODO: this needs to be jpeg quality as well + if rq.colors < 2 || rq.colors > 256 { // ... but maybe not because of this? rq.colors = defGeom.c } rq.keys = rq.r.FormValue("k") rq.buttons = rq.r.FormValue("Fn") + rq.maxSize, _ = strconv.ParseInt(rq.r.FormValue("s"), 10, 64) + if rq.maxSize == 0 { + rq.maxSize = *defImgSize + } rq.imgType = rq.r.FormValue("t") switch rq.imgType { case "png": case "gif": + rq.imgOpt = defGeom.c case "jpg": - case "txt": + rq.imgOpt = int64(*jpgQual) default: rq.imgType = *defType + rq.imgOpt = 80 // TODO: fixme, this needs to be different based on image type } log.Printf("%s WrpReq from UI Form: %+v\n", rq.r.RemoteAddr, rq) } @@ -168,12 +177,14 @@ 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.colors, // TODO: this needs to be also jpeg quality Zoom: rq.zoom, + MaxSize: rq.maxSize, ImgType: rq.imgType, ImgSize: p.imgSize, ImgWidth: p.imgWidth, @@ -189,91 +200,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
\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: @@ -313,128 +239,6 @@ func gifPalette(i image.Image, n int64) image.Image { 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, "
Unable to decode page PNG screenshot:
%s
\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, "
Unable to encode GIF:
%s
\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, "
Unable to decode page PNG screenshot:
%s
\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, "
Unable to encode JPG:
%s
\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) @@ -448,43 +252,15 @@ func pageServer(w http.ResponseWriter, r *http.Request) { return } rq.navigate() // TODO: if error from navigate do not capture - if rq.imgType == "txt" { + if rq.wrpMode == "html" { rq.captureMarkdown() return } - rq.captureImage() -} - -// 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() + rq.captureScreenshot() } // Process HTTP requests for images '/img/' url +// TODO: merge this with html mode IMGZ 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] @@ -515,9 +291,6 @@ func imgServer(w http.ResponseWriter, r *http.Request) { // 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() @@ -537,7 +310,7 @@ func tmpl(t string) string { } defer fh.Close() - tmpl, err = ioutil.ReadAll(fh) + tmpl, err = io.ReadAll(fh) if err != nil { goto builtin } @@ -551,7 +324,7 @@ builtin: } defer fhs.Close() - tmpl, err = ioutil.ReadAll(fhs) + tmpl, err = io.ReadAll(fhs) if err != nil { log.Fatal(err) } @@ -601,19 +374,9 @@ func main() { 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) diff --git a/wrp.html b/wrp.html index c93c6c4..af34cc1 100644 --- a/wrp.html +++ b/wrp.html @@ -6,7 +6,7 @@
- {{ if ne .ImgType "txt" }} + {{ if eq .WrpMode "ismap" }} @@ -14,7 +14,13 @@ W H + {{ end }} + {{ if eq .WrpMode "html" }} + S + {{ end }} + {{ if eq .WrpMode "ismap" }} Z {{ end }} + M T - {{ if ne .ImgType "txt" }} C + {{ if eq .WrpMode "ismap" }} K