package main import ( "encoding/base64" "encoding/json" "errors" "log" "net/http" "net/url" "strings" "github.com/playwright-community/playwright-go" ) type rawImageFile struct { url string blocks []map[string]int width *int height *int } type file struct { contentType string data []byte } type browser struct { db *database pw *playwright.Playwright browser *playwright.Browser } func installBrowser() error { opts := playwright.RunOptions{ Browsers: []string{"chromium"}, Verbose: true, } return playwright.Install(&opts) } type runOptions struct { db *database headless bool } func runBrowser(options runOptions) *browser { pw, err := playwright.Run() if err != nil { log.Fatal(err) } launchOptions := playwright.BrowserTypeLaunchOptions{ Headless: playwright.Bool(options.headless), } b, err := pw.Chromium.Launch(launchOptions) if err != nil { log.Fatal(err) } browser := browser{ db: options.db, pw: pw, browser: &b, } return &browser } func (b browser) stop() error { if b.browser != nil { (*b.browser).Close() } return b.pw.Stop() } func (b browser) newContext() (playwright.BrowserContext, error) { browser := *b.browser return browser.NewContext() } func (b browser) loadBrowserContext(platform string) (playwright.BrowserContext, error) { var secrets string row := b.db.QueryRow("select secrets from platforms where name = $1", platform) if err := row.Scan(&secrets); err != nil { return nil, err } var storageState playwright.OptionalStorageState if err := json.Unmarshal([]byte(secrets), &storageState); err != nil { return nil, err } browserNewContextOptions := playwright.BrowserNewContextOptions{ StorageState: &storageState, } ctx, err := (*b.browser).NewContext(browserNewContextOptions) if err != nil { return nil, err } return ctx, nil } func (b browser) saveBrowserContext(platform string, ctx playwright.BrowserContext) error { storageState, err := ctx.StorageState() if err != nil { return err } secrets, err := json.Marshal(storageState) if err != nil { return err } if _, err := b.db.Exec("update platforms set secrets = $1 where name = $2", secrets, platform); err != nil { return err } return nil } func parseDataURL(dataURL string) (*file, error) { if !strings.HasPrefix(dataURL, "data:") { return nil, errors.New("not data scheme") } parts := strings.SplitN(dataURL, ",", 2) if len(parts) != 2 { return nil, errors.New("invalid data URL") } raw, err := url.PathUnescape(parts[1]) if err != nil { return nil, err } var data []byte if strings.HasSuffix(parts[0], ";base64") { data, err = base64.StdEncoding.DecodeString(raw) if err != nil { return nil, err } } else { data = []byte(raw) } file := file{ contentType: http.DetectContentType(data), data: data, } return &file, nil } var draw = ` async function drawImage(imageFile) { const canvas = Object.assign(document.createElement("canvas"), { width: imageFile.width, height: imageFile.height, }); const image = (await new Promise((resolve) => { Object.assign(new Image(), { crossOrigin: "use-credentials", src: imageFile.url, onload() { resolve(this); }, }); })); const ctx = canvas.getContext("2d"); for (const q of imageFile.blocks) { ctx.drawImage(image, q.destX, q.destY, q.width, q.height, q.srcX, q.srcY, q.width, q.height); } const dataUrl = canvas.toDataURL(); return dataUrl; } ` var fetch = ` async function fetchImage(imageFile) { const res = await fetch(imageFile.url); const blob = await res.blob(); const dataUrl = await new Promise((resolve, reject) => { const fileReader = Object.assign(new FileReader(), { onload() { resolve(this.result); }, onerror(e) { const error = new Error(` + "`${e.type}: ${e.message}`" + `); reject(error); }, }); fileReader.readAsDataURL(blob); }); return dataUrl; } ` func (b browser) drawImage(page playwright.Page, imageFile rawImageFile) (*file, error) { if len(imageFile.blocks) > 0 { dataURL, err := page.Evaluate(draw, imageFile) if err != nil { return nil, err } return parseDataURL(dataURL.(string)) } if strings.HasPrefix(imageFile.url, "blob:") { dataURL, err := page.Evaluate(fetch, imageFile) if err != nil { return nil, err } return parseDataURL(dataURL.(string)) } res, err := page.Context().Request().Get(imageFile.url) if err != nil { return nil, err } data, err := res.Body() if err != nil { return nil, err } file := file{ contentType: http.DetectContentType(data), data: data, } return &file, nil }