feat: compress image data for html report

This commit is contained in:
lilong.129
2025-06-08 23:48:23 +08:00
parent 14cef72f5a
commit cf360c8c46
5 changed files with 149 additions and 39 deletions

View File

@@ -1 +1 @@
v5.0.0-beta-2506082208
v5.0.0-beta-2506082348

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
@@ -195,7 +196,7 @@ func (g *HTMLReportGenerator) parseLogTime(timeStr string) (time.Time, error) {
return time.Time{}, fmt.Errorf("unable to parse time: %s", timeStr)
}
// encodeImageToBase64 encodes an image file to base64 string
// encodeImageToBase64 encodes an image file to base64 string with compression
func (g *HTMLReportGenerator) encodeImageToBase64(imagePath string) string {
// Convert relative path to absolute path
if !filepath.IsAbs(imagePath) {
@@ -207,13 +208,21 @@ func (g *HTMLReportGenerator) encodeImageToBase64(imagePath string) string {
return ""
}
data, err := os.ReadFile(imagePath)
// Read and compress the image using the unified compression function
// Enable resize with max width 800px for HTML reports
compressedData, err := uixt.CompressImageFile(imagePath, true, 800)
if err != nil {
log.Warn().Err(err).Str("path", imagePath).Msg("failed to read image file")
return ""
log.Warn().Err(err).Str("path", imagePath).Msg("failed to compress image, using original")
// Fallback to original image if compression fails
data, readErr := os.ReadFile(imagePath)
if readErr != nil {
log.Warn().Err(readErr).Str("path", imagePath).Msg("failed to read image file")
return ""
}
return base64.StdEncoding.EncodeToString(data)
}
return base64.StdEncoding.EncodeToString(data)
return base64.StdEncoding.EncodeToString(compressedData)
}
// formatDuration formats duration from milliseconds to human readable format
@@ -1612,14 +1621,14 @@ const htmlTemplate = `<!DOCTYPE html>
<div class="sub-action-content">
<div class="sub-action-left">
{{if $subAction.Arguments}}
<div class="arguments">Arguments: {{safeHTML (toJSON $subAction.Arguments)}}</div>
{{end}}
{{if $subAction.Thought}}
<div class="thought">{{$subAction.Thought}}</div>
{{end}}
{{if $subAction.Arguments}}
<div class="arguments">Arguments: {{safeHTML (toJSON $subAction.Arguments)}}</div>
{{end}}
{{if $subAction.ModelName}}
<div class="model-name-container">
<span class="model-label">🤖 Model:</span>
@@ -1711,10 +1720,13 @@ const htmlTemplate = `<!DOCTYPE html>
{{end}}
<!-- Screenshots -->
{{if $step.Attachments}}{{if $step.Attachments.ScreenResults}}
{{if $step.Attachments}}
{{$attachments := $step.Attachments}}
{{if eq (printf "%T" $attachments) "map[string]interface {}"}}
{{if index $attachments "screen_results"}}
<div class="screenshots-section">
<h4>Screenshots</h4>
{{range $screenshot := $step.Attachments.ScreenResults}}
{{range $screenshot := index $attachments "screen_results"}}
{{$base64Image := encodeImageBase64 $screenshot.ImagePath}}
{{if $base64Image}}
<div class="screenshot-item">
@@ -1731,7 +1743,9 @@ const htmlTemplate = `<!DOCTYPE html>
{{end}}
{{end}}
</div>
{{end}}{{end}}
{{end}}
{{end}}
{{end}}
<!-- Step Logs -->
{{$stepLogs := getStepLogs $step}}

View File

@@ -90,16 +90,14 @@ func TestStartToGoal(t *testing.T) {
- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。
- 连接线的转折次数不能超过两次。
3. 游戏界面:
- 游戏界面通常是一个矩形区域,内含多个图案或图标,排列成行和列。
- 图案或图标在未选中状态下背景为白色,选中状态下背景为绿色
4. 重试机制:
- 游戏失败后可以点击「立即复活」按钮观看视频广告30秒点击屏幕右上角关闭图标后可继续游戏。
- 若无法再复活,可以点击「立即挑战」按钮,重新开始游戏。
注意事项:
1、当连接错误时顶部的红心会减少一个需及时调整策略避免红心变为0个后游戏失败
2、不要连续 2 次点击同一个图案
3、不要犯重复的错误
- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色
- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」
4、游戏攻略:建议多次使用道具,可以降低游戏难度
- 优先使用「减少种类」道具,可以将图案种类随机减少一种
- 遇到困难时,推荐使用「随机打乱」道具,可以获得很多新的消除机会
- 观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏
5、结束游戏
- 游戏失败,且无法再「立即复活」后,游戏结束,停止游戏
请严格按照以上游戏规则,开始游戏
`
@@ -121,8 +119,8 @@ func TestStartToGoal(t *testing.T) {
err := testCase.Dump2JSON("start_llk_game.json")
require.Nil(t, err)
err = hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
// err = hrp.NewRunner(t).Run(testCase)
// assert.Nil(t, err)
}
func TestAIAction(t *testing.T) {

View File

@@ -81,7 +81,6 @@ Target: User will give you a screenshot, an instruction and some previous logs i
Restriction:
- Don't give extra actions or plans beyond the instruction. ONLY plan for what the instruction requires. For example, don't try to submit the form if the instruction is only to fill something.
- Always give ONLY ONE action in ` + "`log`" + ` field (or null if no action should be done), instead of multiple actions. Supported actions are click, long_press, type, scroll, drag, press_home, press_back, wait, finished.
- Don't repeat actions in the previous logs.
- Bbox is the bounding box of the element to be located. It's an array of 4 numbers, representing [x1, y1, x2, y2] coordinates in 1000x1000 relative coordinates system.

View File

@@ -213,7 +213,7 @@ func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err
}
// compress screenshot
compressBufSource, err := compressImageBuffer(bufSource)
compressBufSource, err := compressImageBufferWithOptions(bufSource, false, 800)
if err != nil {
return nil, errors.Wrapf(code.DeviceScreenShotError,
"compress screenshot failed %v", err)
@@ -291,7 +291,8 @@ func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
return nil
}
func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) {
// compressImageBufferWithOptions compresses image buffer with advanced options
func compressImageBufferWithOptions(raw *bytes.Buffer, enableResize bool, maxWidth int) (compressed *bytes.Buffer, err error) {
rawSize := raw.Len()
// decode image from buffer
img, format, err := image.Decode(raw)
@@ -299,28 +300,126 @@ func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error
return nil, err
}
var buf bytes.Buffer
// Get original image dimensions
bounds := img.Bounds()
originalWidth := bounds.Dx()
originalHeight := bounds.Dy()
switch format {
// compress image
case "jpeg", "png":
jpegOptions := &jpeg.Options{Quality: 60}
err = jpeg.Encode(&buf, img, jpegOptions)
if err != nil {
return nil, err
}
// Calculate new dimensions for compression if resize is enabled
var newWidth, newHeight int
var resizedImg image.Image = img
if enableResize && originalWidth > maxWidth {
ratio := float64(maxWidth) / float64(originalWidth)
newWidth = maxWidth
newHeight = int(float64(originalHeight) * ratio)
resizedImg = resizeImage(img, newWidth, newHeight)
} else {
newWidth = originalWidth
newHeight = originalHeight
}
// Determine JPEG quality based on image size for optimal compression
jpegQuality := 60 // Default quality for better compression
if newWidth*newHeight > 500000 { // For very large images, use lower quality
jpegQuality = 50
} else if newWidth*newHeight < 100000 { // For small images, use higher quality
jpegQuality = 70
}
var buf bytes.Buffer
switch strings.ToLower(format) {
case "jpeg", "jpg":
// Use adaptive JPEG compression quality
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
case "png":
// Convert PNG to JPEG for better compression
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
case "gif":
// Keep GIF format but with reduced colors for better compression
err = gif.Encode(&buf, resizedImg, &gif.Options{NumColors: 64})
default:
return nil, fmt.Errorf("unsupported image format: %s", format)
// Default to JPEG for unknown formats
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
}
if err != nil {
return nil, err
}
compressedSize := buf.Len()
log.Debug().Int("rawSize", rawSize).Int("compressedSize", compressedSize).
log.Debug().
Int("rawSize", rawSize).
Int("originalWidth", originalWidth).
Int("originalHeight", originalHeight).
Int("newWidth", newWidth).
Int("newHeight", newHeight).
Int("jpegQuality", jpegQuality).
Int("compressedSize", compressedSize).
Bool("resized", enableResize && originalWidth > maxWidth).
Msg("compress image buffer")
// return compressed image buffer
return &buf, nil
}
// resizeImage resizes an image using simple nearest neighbor algorithm
func resizeImage(src image.Image, width, height int) image.Image {
srcBounds := src.Bounds()
srcWidth := srcBounds.Dx()
srcHeight := srcBounds.Dy()
// Create a new image with the target dimensions
dst := image.NewRGBA(image.Rect(0, 0, width, height))
// Simple nearest neighbor resizing
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
// Map destination coordinates to source coordinates
srcX := x * srcWidth / width
srcY := y * srcHeight / height
// Ensure we don't go out of bounds
if srcX >= srcWidth {
srcX = srcWidth - 1
}
if srcY >= srcHeight {
srcY = srcHeight - 1
}
// Copy pixel from source to destination
dst.Set(x, y, src.At(srcBounds.Min.X+srcX, srcBounds.Min.Y+srcY))
}
}
return dst
}
// CompressImageFile compresses an image file and returns the compressed data
func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byte, error) {
// Read the original image file
file, err := os.Open(imagePath)
if err != nil {
return nil, fmt.Errorf("failed to open image file: %w", err)
}
defer file.Close()
// Read file content into buffer
var buf bytes.Buffer
_, err = buf.ReadFrom(file)
if err != nil {
return nil, fmt.Errorf("failed to read image file: %w", err)
}
// Compress using the buffer compression function
compressedBuf, err := compressImageBufferWithOptions(&buf, enableResize, maxWidth)
if err != nil {
return nil, fmt.Errorf("failed to compress image: %w", err)
}
return compressedBuf.Bytes(), nil
}
// MarkUIOperation add operation mark for UI operation
func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordinates []float64) error {
if actionType == "" || len(actionCoordinates) == 0 {