display.save() unexpected behavior

Hi

I am trying to understand how display.save() calculates the image size to save. I have read the API docs but it seems to be behaving differently, or my understanding could also be wrong. I am using version 2013.1076 on Mac. I am including the test code below. My config.lua is setup for width = 320, height = 480, scale = “letterbox” and using landscape mode. I get the following results and sizes in Simulator for a group with width=100 and height=40:

iphone:          100, 40

iphone 4/4s:  100, 40

iphone 5:       100, 40

ipad:              107, 43

ipad Retina:   107, 43

driod: 150, 60

s3:      225, 90

I want to save the image in exact size (100,40).

Does someone know how it’s coming up with 107, 43 for iPad, for instance? Any insight is helpful.

thanks - Kay

local dir = system.TemporaryDirectory; local mw, mh = 100, 40; local fn="savetest2.png"; local grp = display.newGroup(); local gr = display.newRect(grp,0,0,mw,mh); display.save( grp, fn, dir); grp:removeSelf(); local img = display.newImage(fn, dir, 0,50, true); -- load full res no dynamic scaling local imgW = img.width; local imgH = img.height; print("file dims = " .. fn, imgW, imgH);

It saves in device pixels, not your virtual screen pixels… So to save it at a specific resolution, you’ll need to scale your pic or group using display.contentScaleX and Y appropriately…

But your numbers don’t look right… I think they should be much larger… Maybe something else is going on…

And another problem you’ll probably run into… The compressor for PNG files is set at almost lossless in the SDK. And the compressor for JPG’s (if you change the filename extension, it saves in that format) is pretyy messed up… For example, if you change your existing code to use the jpg format, you will get a big white rectangle around your saved image… And although they tried to fix the jpg compressor setting for android, the files that are saved are still heavily over compressed, producing a file that is one tenth the size of the same file captured on iOS.

But that’s getting ahead of where you are… Try factoring in display.contentScaleX and Y…

Thank you, mpappas, for the reply. Yes, that’s what I thought it was doing (calculating the size dividing by contentScaleX and Y). But even that does not work right. If you check the results/sizes I posted, you will find that it calculates the same size for iphone (contentScale=1), and iphone 4/4s/5 (contentScale=.5). It should have been double the size of my group (2 x 100,40 = 200,80) for contentScale .5. At least documentations says so, or what I understood from it.

I have seen that jpeg low compression while testing it but didn’t see that white border, at least in simulator. Luckily, my issue is consistent on mac simulator, iphone 4s and ipad ret (on windows simulator it’s fine).

I want to use it for dynamic masks (so only pngs and no jpgs) but this behavior is coming in the way.

Any other thoughts or comments guys.

Your numbers aren’t from real devices, are they?

If you run in the simulator, I believe display.save() does NOT capture the same size image as on the real device. The contentScaleX, Y come back correctly for the devices you listed (0.5, 1.0, etc), but the display.save() captures in the MAC (or windows) device pixels… Which is NOT the same as a mobile device… If you build for a real device and run the tests, I believe you will see different sized saved jpgs than those captured inside the simulator.

Are those real display.save() size numbers from devices, or the simulator?

This would also explain you not seeing the white borders on jpgs – it only happens on iOS.

You are right, those numbers are from simulator (mac) and yes that could be the reason for me not seeing white borders. I did test it on ipad (iphone) where it all started but don’t have those number right now with me, will post back shortly.

You definitely should see bigger numbers (as you expected) on the actual devices.

I am masking as well - applying a mask to my group… Don’t forget to scale the mask too… (and there’s a trick to setting the referencePoint properly on group masks as well, shown in the mask documentation I believe).

The masking of the group all works fine with the jpgs, just that the final display.save() creates a big honking white border on iOS. (and although the png’s don’t have the white border, at 960x640  = 1 MB, the captured images are just too darn large for me to use).

Also, you originally asked why it is coming up 107,43 for the ipad (and similar numbers for other devices). My guess is that it is because when you change the simulator to mimic the different devices, your simulator reformats on screen and the new device is roughly (but not exactly) the same size onscreen, and converting your 100x40 image results in roughly the same amount of pixels on different simulated devices (but are actually display pixels, on a Mac). If you doubled the size of the simulator device onscreen, I would guess that the number of pixels you get back increases as well (never actually tried it though).

Checked on device. I get 98 x 38 image for 100x40 group if I scale it down to contentScaleX x contentScaleY. I am not sure but rounding could be causing this difference. Doing more testing, will post back when I find more.

I will also check jpegs to see how they work… 

Very interesting… I just ran your code on my iPad3, and got similar results…

I changed the code to do a few things slightly differently, but to achieve the same effect.

Yet I got different results-- like yours, close to what it should be, but not perfect (which of course means there’s something wrong somewhere…)

[lua]

local dir = system.TemporaryDirectory
local mw, mh = 100, 40
local fn=“savetest2.jpg”

local grp = display.newGroup()
local gr = display.newRect(grp,0,0,mw-2,mh-2)
gr.strokeWidth = 2
gr:setStrokeColor(192, 64, 32,255)
gr:setFillColor(64, 192, 192,255)
gr.x = 160
gr.y = 240

local imgW = gr.contentWidth; local imgH = gr.contentHeight
print("group dims1 = " … imgW, imgH)

–gr.xScale = display.contentScaleX
–gr.yScale = display.contentScaleY
gr.width = 100 * display.contentScaleX
gr.height = 40 * display.contentScaleY
print("group dims2 = " … gr.contentWidth, gr.contentHeight)

display.save( grp, fn, dir)
grp:removeSelf()
grp = nil

local img = display.newImage(fn, dir, 110,50, true) – load full res no dynamic scaling
imgW = img.width; local imgH = img.height
print("file dims = " … fn, imgW, imgH)
print("scaleX,Y = " … display.contentScaleX, display.contentScaleY)

[/lua]

When done with a config file for 320x480, it gives one result… If the config is set for 640x960, a close to 100x100 image, but slightly different result… It does look like some kind of rounding error like you said, like when it scales the image down for the capture, one of the factors is rounded somehow, and the final calculation is then off. Not clear to me exactly how though.

My code has a bunch of alternate things to measure / ways to change a few things, maybe it will give you some further ideas on how to pinpoint the discrepancy (Corona bug?)

Interestingly, I am seeing my jpg white border bug at differing levels of white thickness/alignment at different resolution (320x480 versus 640x960) settings…  Very odd white jpg border behavior…

I see that instead of scaling the group you are scaling the rect. I did try that too with same results. I don’t understand why you are using dims-2 and then stroke+2 and filling (I mean for masking case strokeless rect would be less code?).

To make things more confusing here is what I found while using different dims (doesn’t matter if number is used as width or height). First number is the actual group size (width or height) and the second is the result/image size. Notice that the difference is not consistent.

200   to 201   +1

400   to 401   +1

450   to 448    -2

480   to 482   +2

500   to 499    -1

1000 to 998    -2

I wish someone from coronalabs team chimes in and explain this to us.

Ops, yes I meant to scale the grp… I changed the gr to grp references below.

I was setting a border color was so I could make sure it was getting all 4 edges, not dropping some in some cases we are seeing weird sizes.

I took out the extra test code, fixed the grp vs. gr references, and had the following code:

[lua]

local dir = system.TemporaryDirectory
local mw, mh = 100, 40
local fn=“savetest2.jpg”

local grp = display.newGroup()
local rect = display.newRect(grp,0,0,mw,mh)
rect:setFillColor(64, 192, 192,255)

local imgW = grp.contentWidth; local imgH = grp.contentHeight
print("group dims1 = " … imgW, imgH)

grp.xScale = display.contentScaleX  – Change groups x scale (could instead set width = 100*display.contentScaleX)
grp.yScale = display.contentScaleY  – Change groups y scale (could instead set height = 40*display.contentScaleY)

print("group dims2 = " … grp.contentWidth, grp.contentHeight)

grp.x = 160     – Entire group needs to be onscreen to save properly, so center it
grp.y = 240

display.save( grp, fn, dir)
grp:removeSelf()
grp = nil

local img = display.newImage(fn, dir, 110,50, true) – load full res no dynamic scaling
imgW = img.width; local imgH = img.height
print("file dims = " … fn, imgW, imgH)
print("scaleX,Y = " … display.contentScaleX, display.contentScaleY)

[/lua]


On my iPad3, the saved file is 100x41 pixels when using a 640x960 virtual screen (content.width, height in config.lua)

On my iPad3, the saved file is 98x38 pixels when using a 320x480 virtual screen.

In some size/config instances saving jpgs, the extra rows of pixels appear to be solid white on my iPad (when it is saved oversized I believe).

My config.lua is:

[lua]

application =
{
    content =
    {
            width = 320,
            height = 480,
            scale = “letterbox”,
            fps = 60,            
    },        
        
}

[/lua]

When the code above prints the dims2 size, those numbers are integers… Hmmm. I just put 40 (the height) in my calculator, and multiplied by what it says my iPad display.contentScaleX is (0.234375)…

The result was 9.375, not 9 (which is what it said dims2 contentHeight is). If that’s what display.save() ends up using to determine it’s “size”, then that means that it’s not going to be the right amount of device pixels…

>> The result was 9.375, not 9…

When I read that it was like an Aha moment but it didn’t last long :). Your finding is right but it does not seem to be the reason (or the only one). I also noticed that if I assign a value like “.55” to xScale, at least in simulator it prints as “0.55000001192093”.

After reading your post I used the following to check for different sizes:

[lua]

local dir = system.TemporaryDirectory;

local csx,csy = display.contentScaleX, display.contentScaleY;

local t = {200, 400, 450, 480, 500, 1000}

for i=1, #t do

    local g = display.newGroup();

    r = display.newRect(g, 0,0, t[i], t[i]);

    g:scale(csx,csy);

    

    local fn = “file”…i;

    display.save( g, fn, dir);

    local img = display.newImage(fn, dir, 0,50, true); – true is to load full res without dynamic scaling

    imgW = img.width; imgH = img.height;

    print("dim# " … i … ": "… t[i] … “,”… t[i]*csx … " - " … g.contentWidth … “,” … g.contentHeight … 

        " - " … fn … " > " … imgW … ", " … imgH);

    

    –  local f = “.55”; g.xScale = f; print(f, g.xScale);

    

    img:removeSelf(); img=nil;

    g:removeSelf(); g=nil;

end

[/lua]

I don’t know how to add line numbers to the above (help?). The commented line is the one that shows that weird number.

And this is what I get for 200 size:

dim# 1: 200,46.875 - 46,46     - file1 > 201, 201

The contentScale is 0.234375 (which is 480/2048).

Now 1) 46.875 is not 46 as you discovered

and  2) 201 * 0.234375 = 47.109375 which is neither 46.875 nor 46

where this number is coming from, is the question.

Does that mean dynamic scaling is slightly off in other areas too?

I am about to give up on this with hopes that after weekend some miracle would happen and Rob show up with some description of what’s going on. Or I will submit a bug.

oops, I used “lua” tags this time to add code and it messed up the formatting. Code below:

local dir = system.TemporaryDirectory; local csx,csy = display.contentScaleX, display.contentScaleY; local t = {200, 400, 450, 480, 500, 1000} for i=1, #t do local g = display.newGroup(); r = display.newRect(g, 0,0, t[i], t[i]); g:scale(csx,csy); local fn = "file"..i; display.save( g, fn, dir); local img = display.newImage(fn, dir, 0,50, true); -- true is to load full res without dynamic scaling imgW = img.width; imgH = img.height; print("dim# " .. i .. ": ".. t[i] .. ",".. t[i]\*csx .. " - " .. g.contentWidth .. "," .. g.contentHeight .. " - " .. fn .. " \> " .. imgW .. ", " .. imgH); -- local f = ".55"; g.xScale = f; print(f, g.xScale); img:removeSelf(); img=nil; g:removeSelf(); g=nil; end

Hmm… I am thinking this is an artifact of dynamic scaling, but also that the cause is math (and the API), not a proper “SDK bug”.

It seems to reason that if the actual device is 5x5 pixels (using small numbers to be able to visualize easy), and we make a virtual screen 3x3 pixels… Then we tell display.save() we want a capture of a 2x3 pixel area, the math (integer) would work out with fractions… If you floor (or ceil) the numbers (in display.save), you end up with a few extra (or missing) pixels.

If that is the root cause, then (with the current API), you would only occasionally be able to get an exact display.save() size (when the device size was an exact multiple of the virtual screen).

I think it’s not due to ceil or floor because it’s not consistent. I need to double-check but I think it does work as expected for the scaleFactor of .5 (iphone4/5).

This code prints the same results we are getting from display.save (only there’s no actual save).

[lua]

       
    local g = display.newGroup();
    g:scale(display.contentScaleX, display.contentScaleY)
     
    for i=40, 100 do
        r = display.newRect(g, 0,0, i, i);
        
        print(" *****************")
        print(“virtual size " … i … “: contentWidth,Height = " …  g.contentWidth … “,” … g.contentHeight)
        
        print(“rescaled size " … i … “: contentWidth,Height = " …  g.contentWidth / display.contentScaleX … “,” … g.contentHeight / display.contentScaleY)
        print(” *****************”)
        
        r:removeSelf()
        r=nil        
    end
    
    print(” – Done.”)

[/lua]

This shows the result of scaling virtual pixels by the contentScaleX, Y (which is needed to do a display.save). It scales down (like we need to do for the display.save), then print the new contentWidth and Height (which are always truncated to integers – that is where we are losing our precision)…

So what appears to be happening is that since contentWidth and contentHeight are kept as integers, the extra resolution is lost. It seems display.save uses these variables in determining the capture area, since the sizes returned above for 40 and 100 exactly match our test case for display.save as well (100x40 --> 98,38 - same results here as in the display.save case). There apparently is no alternate way to specify the region (like using floats, or an alternate api to use device pixels).

Missing is one thing, what about gain if you see the output I provided in one of the messages above i.e.

“480   to 482   +2” shows a gain of 2 pixels.

Agreed, something else is going on as well - perhaps the sdk is trying to adjust at certain common thresholds (320, 480, 640, etc).

Some “resolution” on this by CoronaLabs would be helpful.

@CoronaLabs – Is there a way we can make pixel level accurate display.save() (capture, etc)? Having the ability to capture 100x100 (arbitrary size) images across platforms would be helpful.

It’s close now, but without some more precise way to size or specify the capture region, we aren’t able to get an exact size image across platforms with content scaling enabled…

Anyone from staff, please (confirm this bug or workaround or explanation)?

display.save will save the object based on the device’s pixel resolution and not the pixel width and height of the object on the screen. This means it will vary from one type of device to the next. The saved size depends on the dynamic scaling ands scaling mode: letterbox, zoomEven, zoomStretch.

You can display the save image at it’s intended size using display.newImageRect, using the original object width and height in the API call.

The simulators will save the object at different resolutions, depending on the zoom level. You should zoom all the way in (if your display supports it) to match that of the device you’re simulating.

If you are expecting to get the exact height and width values from your saved images, I don’t think that’s possible because of math rounding errors caused by devices that are not scaled the same was your original layout.

Thank you so much Tom for the reply. However, 1) I am more concerned about the device and not much about the simulator. It would be nice if it behaved like the device though, and 2) something is still not right. Please read the following from http://docs.coronalabs.com/api/library/display/save.html:

_ "NOTE: When dynamic content scaling is enabled, display.save() saves the image in the device’s native resolution. For instance, if this method is used to save a 100 x 200 pixel display object, it will be saved as a 100 x 200 image on iPhone3 but it will be a 200 x 400 image on an iPhone4 (which would have the same content dimensions but more actual pixels). This is assuming that the config.lua file specifies the content width/height as 320x480 respectively."_

According to the above, 100 px would be 100px on iphone3, 200 on iphone4, and so on (given that 320x480 width/height and letterbox in config.lua does not change)…  = size / contentScale. But that’s not what we see in our examples above (on device). Please test the code I submitted in bug report or given above to see what it does.

About rounding: It’s hard to get 3 or more pixels added to the result by rounding alone, unless it’s been done more than once or being rounded first and then multiplied with a big number (in one of the tests above it changed 40px to 43)?

Please tell us if we are overlooking something here or better how does corona internally do the conversion math? Any details and specifics here would be a great help.