ariya / phantomjs

Scriptable Headless Browser
http://phantomjs.org
BSD 3-Clause "New" or "Revised" License
29.46k stars 5.76k forks source link

Render not honoring viewport size #10619

Closed ariya closed 8 years ago

ariya commented 12 years ago

mi...@rhiza.com commented:

Which version of PhantomJS are you using? Tip: run 'phantomjs --version'. 1.6.0

What steps will reproduce the problem?

  1. Set path.viewportSize to a height less than the page size
  2. Load url and run page.render()

What is the expected output? What do you see instead? I was expecting an output that matches the viewport size. Instead I receive an image which is the viewport width but the height is the height of the entire content on the page (see attached). Looking at issue 191 this might be the desired behavior when you are capturing output of a test. However, if you are taking screenshots for producing thumbnails you really want the image to be the viewport size. The other options is to use the clipRect, however the page I'm capturing resizes the content based upon the viewport of the browser. Therefore I really want phantomjs to honor my viewport settings and really set the viewport to that height. My view is this is a more common use case and if you want the height of the viewport to expand with the documents content then you should have to set that manually (ie page.makeViewportSizeDynamic()).

Which operating system are you using? Ubuntu

Did you use binary PhantomJS or did you compile it from source? Binary

Please provide any additional information below.

Here is the code to reproduce this behavior: url = "http://news.google.com"; width = 1024; height = 768; output_file = "/tmp/out.png" page.viewportSize = { width: width, height: height }; page.onLoadFinished = function (status) { if (status !== 'success') { console.log("badness"); phantom.exit(1); } console.log("Writing to " + output_file); page.render(output_file); phantom.exit(); }

page.open(url);

Disclaimer: This issue was migrated on 2013-03-15 from the project's former issue tracker on Google Code, Issue #619. :star2:   8 people had starred this issue at the time of migration.

ariya commented 12 years ago

ariya.hi...@gmail.com commented:

Viewport size in PhantomJS follows the same meaning as viewport in WebKit. A web page here is more like a long sheet of paper where the content is capture. A browser however adds the concept of window viewport which is scrollable, hence making the behavior different.

We may need to come up with a different API name, probably something like windowSize.

jamescooke commented 12 years ago

ja...@fublo.net commented:

We have the same issue with page.render(); We'd like to generate square images which are locked to the top left of the web page, so we set a viewportSize to match the square that we need.

When Phantom returns the "long sheet of paper" we then pass that to some other process that chops it back to being a square. All in all it takes a lot of processor on our server - both from Phantom assembling the large image, and then chopping the large image down.

Would it be possible that page.render() could be extended to accept some arguments regarding the size of the desired output?

ariya commented 12 years ago

ariya.hi...@gmail.com commented:

By "the size of the desired output" do you mean having only specific portion of the page rendered? That can be done already using clipRect.

This issue is more about viewport size which affects placement of various elements in the page.

 
Metadata Updates

jamescooke commented 12 years ago

ja...@fublo.net commented:

Clearly I need to experiment more with render() and clipRect(), thanks for the pointer.

ariya commented 12 years ago

mi...@rhiza.com commented:

As Ariya points out you can use clipRect if you simply want to produce an ouput of an exact size. For example clipRect = {top:0, left:0, width:640, height:480} will produce an image exactly 640x480. The issue I'm having is related to content that dynamically scales based upon the window size.

I attempted to set the window size of the browser using javascript (window.resizeTo) but it doesn't appear to work. Does anyone have any pointers of using this within phantomjs?

ariya commented 12 years ago

mi...@rhiza.com commented:

Is there any update on this issue? Is there any plans on adding window size as a core ability in phantomjs?

We have ran into this problem again while taking screenshots of a leaflet generated map. The issue is that the map paints polygons based upon the window size and not the view port size. However, the tiles are rendered in the viewport size. It could be argued that this is a bug in leaflet but its only showing itself in the context of using phantomjs to take screenshots.

ariya commented 11 years ago

joebeuck...@gmail.com commented:

It would be very useful to be able to test at different window sizes. Is there a way to do that currently?

ariya commented 11 years ago

harolds...@gmail.com commented:

I hope that this one gets fixed soon. Making screenshots is now not as nice as it could be. Clipping the image will result in missing the bottom part of the website. If that website uses an footer thats fixed to the bottom of the browser its not captured right now.

ariya commented 11 years ago

ncoo...@gmail.com commented:

bump on this... Modals used in the site that are positioned based on viewport size are currently filling the entire height of the content, and clipRect is cutting them off...

kcivey commented 11 years ago

ke...@iveys.org commented:

I'm running into this problem too. An example here: http://jsfiddle.net/kcivey/nf5r8/embedded/result/

In the browser, the circle is drawn around Dupont Circle, but when retrieved with

phantomjs rasterize.js 'http://jsfiddle.net/kcivey/nf5r8/embedded/result/' test.png

The PNG has the circle positioned southeast of where it should be.

kcivey commented 11 years ago

ke...@iveys.org commented:

This Leaflet change seems to have fixed my problem, so I guess it wasn't about viewport size: https://github.com/Leaflet/Leaflet/pull/1501

bjeanes commented 11 years ago

I also just ran into this and was surprised at the behavior, then further surprised that there isn't an existing way to do this still. I know I certainly would prefer the described behavior as opposed to just clipRect...

jbeuckm commented 11 years ago

I'm having good luck using an iframe and clipRect to get what I want. https://github.com/jbeuckm/Splasher

bjeanes commented 11 years ago

That's .... interesting. I'll have to play with that...

mattgibson commented 11 years ago

I'm trying to make a Cucumber scenario which tests if something is scrolled into view, and this is preventing it from working. Everything is visible all the time.

JamesMGreene commented 11 years ago

render renders all pages regardless of viewport size, just like a browser print.

You'll need to use clipRect to render only a slice of it. See http://phantomjs.org/api/webpage

mattgibson commented 11 years ago

Thanks. To clarify: I was expecting the screenshot to show only what the viewport would show, so I could judge whether my tests were accurate i.e. My test is saying that the element is visible in the viewport, but does this mean really visible in the viewport, or just visible somewhere on the page? The clipRect thing means I need to determine viewport size, etc, doesn't it? It would be good to be able to pass a parameter to clipRect so that it would use the viewport dimensions by default. I didn't see reference to anything like that in the docs.

JamesMGreene commented 11 years ago

clipRect {object} This property defines the rectangular area of the web page to be rasterized when WebPage#render is invoked. If no clipping rectangle is set, WebPage#render will process the entire web page.

So, perhaps you want to do something like the following:

function renderCurrentViewport(page, filename) {
  var viewportSize = page.viewportSize;
  var scrollOffsets = page.evaluate(function() {
    return {
      x: window.pageXOffset,
      y: window.pageYOffset
    };
  });
  page.clipRect = {
    top: scrollOffsets.y,
    left: scrollOffsets.x,
    height: viewportSize.height,
    width: viewportSize.width
  };
  page.render(filename);
}

page.open("http://yourUrlGoesHere.com/", function(status) {
  renderCurrentViewport(this, "screenshot.png");
  phantom.exit();
});
BlueHotDog commented 11 years ago

@JamesMGreene , the problem with your solution is that if we've some things drown on the top of the viewport, they'll get cut off.. since although you clip the viewport, the viewport is still incorrect..

JamesMGreene commented 11 years ago

@BlueHotDog: The last-mentioned workaround was strictly for @mattgibson's situation.

larkin commented 11 years ago

I use karma with phantomjs to run tests, and I have a test case in my code that requires me to check and see if the body can scroll inside of the window, but due to this I am unable to run the test in PhantomJS.

I couldn't find a workaround for this, so for now I skip this test (and log a warning) if /PhantomJS/.test(navigator.userAgent) is true.

benjohnson commented 11 years ago

I'm running into the same problem. I've got tests that rely on checking behaviour when the page is scrolled. I can set the height of the test elements to something ridiculous to make sure there's scrolling room, but this doesn't work in Phantom because it ignores my set viewport height of 1200 and instead sets it to the height of the test element.

There really needs to be a reliable way to set the viewport size regardless of page contents.

ToniChaz commented 10 years ago

Thank you Ariya, This solution has functioned for me:

  1. Set path.viewportSize to a height less than the page size
  2. Load url and run page.render()
ralfthewise commented 10 years ago

In hopes that this helps someone else in the future, we mostly solved this by forcing the 'body' element to the page viewportSize:

var width = 1024;
var height = 768;
var webpage = require('webpage');

page = webpage.create();
page.viewportSize = {width: width, height: height};
page.open('http://harness.io', function(status) {
    console.log(status);
    page.evaluate(function(w, h) {
      document.body.style.width = w + "px";
      document.body.style.height = h + "px";
    }, width, height);
    page.clipRect = {top: 0, left: 0, width: width, height: height};                                                                                                                           
    page.render('/tmp/test.png');
    phantom.exit();
});
syamksundar commented 10 years ago

I tried the above (setting viewport width & height), but I still didnt fix the isse.

Still Im observing issue : https://github.com/sindresorhus/pageres/issues/53

DEfusion commented 10 years ago

This is literally driving me crazy. I resorted to trying an iframe by doing the following:

page.open('about:blank', function (status) {
    if (status !== "success") {
        console.log('Unable to load url');
        phantom.exit();
    } else {
        page.includeJs(
            "https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js",
            function() {
                evaluateJsWithArgs(
                    function(url, w, h) {
                        console.log(url + ', ' + w + ', ' + h);
                        $('body').append('<iframe frameBorder="0" src="' + url + '" width="'+ w + '" height="' + h + '" id="screen-frame" />');
                        console.log($('#screen-frame').width() + ', ' + $('#screen-frame').height());
                    },
                    url,
                    page.viewportSize.width,
                    page.viewportSize.height
                );
            }
        );
    }
})

The iframe is reported at the correct width and height in the console, but render() still renders the iframe at the full height of the document that it contains and I can't get it not to.

DEfusion commented 10 years ago

Quick update to what I just posted, I realised it might be handling iframes the same way the iOS does and tried replacing with an <object> and that does the trick.

mrbeardy commented 10 years ago

Rendering at a higher resolution

Edit: Just noticed ralfthewise pointed out the method I mention in this comment. I tried his method on github.com but it cut half the page off, so there might be some middle-point between both of our solutions that might work better.

This doesn't really solve the original issue, but it's something that might be useful. You can actually trick PhantomJS into thinking it has a bigger viewport by manipulating certain attributes for the body and html DOM elements on the page that is being rendered:

// This is the name of "this" file that is being ran.
// This is just a convenience method so the injected JS is in the same file.
var this_file  = "render_size.js";

// When I tried running my DOM manipulation code
// with window.onload, it didn't work properly, so I'm using
// jQuery to handle all of that for me. You need to have jquery.min.js in the same
// folder as your script.
var jquery  = "jquery.min.js"

// I seemed to have some problems with injecting jQuery
// when it was already loaded in the site, so change this
// depending on if the page you're rendering already loads jQuery or not.
var jquery_needed = false;

var website   = "http://github.com"
var filename  = "github.png";

// ------------------------------

if ( typeof(phantom) == "undefined" ) {
    $(function() {
        $("body, html").css({
            "width": "2300px",
            "max-height": "500px",
            "overflow-x": "hidden"
        })
    })
} else {
    var page = require( "webpage" ).create();

    page.open( website, function( status ) {
        if ( status === "success" ) {
            if ( jquery_needed ) {
                if (!page.injectJs( jquery ) ) {
                    console.log( "!!! Failed to inject jQuery" );
                    phantom.exit();
                }   
            }

            if ( page.injectJs( this_file ) ) {
                console.log( "Injected. Rendering to " + filename + "..." );
                page.render( filename );
            } else {
                console.log ( "!!! Failed to inject myself." );
            }

            phantom.exit();
        }
    });
}

Running that file should output a file named github.png with a screenshot of the github.com homepage at exactly 2300px by 500px.

I've not tested this on a lot of websites, but it seemed to work on the few I tried it on.

DEfusion commented 10 years ago

@Sonshi You're right, I had tried @ralfthewise's method and it hadn't worked, however once you add set the overflow to hidden then it works, see my working test case at this gist (using a jsfiddle with some fixed elements and a cover background to test those):

https://gist.github.com/DEfusion/4cc10e23f9ed4d38244f

Only issue I can see is for some reason the version doing the body resize results in a slightly larger image (1032x776 for me) for the test file but isn't for other sites I've tried.

m4nuC commented 10 years ago

@DEfusion thanks for hints, I would never have thought of using the object tag and tore all my hair off!

survili commented 10 years ago

Hi all, any plans to provide solution for this issue ?

In my case, I'm including external js file into arbitrary page, which renders a widget at the bottom of the page. Currently this widget is rendered at the end of the whole page and not the viewport. To resolve this I'm using an ugly hack, and after the widget is rendered, I'm moving it to the correct position(this hack feels real bad, as this whole process is supposed to test that the widget renders correctly, that's why I've started using phantomjs..)

leemes commented 10 years ago

I also need this to be fixed. I suggest two separate APIs / functions for this:

  1. a function for capturing the page (like capture it is currently implemented)
  2. a function for capturing the viewport, respecting both the viewport size as well as the current scroll position (like you would see the page in a browser)

Those are two different things. And 2. can't be implemented with 1. + clipping, as for example "position: fixed" would not move with the scroll position as well as dynamic sized content (based on the viewport size) wouldn't scale like if you view it with the browser.

gaboom commented 10 years ago

+1 for support fixed render viewport size

aharpervc commented 10 years ago

:+1:

nicktrown commented 10 years ago

+1

ubunatic commented 10 years ago

+1 Edit 1: I just realized that PNGs render fine. Only PDF rendering suffers from the wrong printing offsets. Edit 2: FYI: I could fix the PDF problem by setting up a (bigger) pageSize:

width  = 1000
height =  600
page.set "viewportSize", width: width, height: height
page.set "clipRect", top: 0, left: 0, width: width, height: height
page.set "paperSize", width: "#{2*width}px", height: "#{2.1*height}px", margin: "0px"
vbauer commented 9 years ago

:+1:

irysius commented 9 years ago

:+1: This is an incredibly frustrating issue. To see that ClipRect does not do what a person assumes it does, visit any site with an position:absolute;top:0 navigation bar, and try to render that image half way down the page.

snoblenet commented 9 years ago

+1 for ability to set rendered page size

dhwaneetbhatt commented 9 years ago

Solution mentioned by @MrBeardy works perfectly. The trick is to set a max-height to the body, to force the content to shrink to that height.

astefanutti commented 9 years ago

I've just pushed #13422 that introduces a new mode option to the render API that can be valuated to:

As @leemes stated it correctly, it is not possible to meet the later generally by using the clipRect property. In some applications, like decktape that applies to pages having the position CSS property set to fixed or absolute, or relying on the transform CSS property, or pageres, this is a must-have.

astefanutti commented 9 years ago

I've just found out that SlimerJS has an onlyViewport option that does exactly that. That may be valuable to align the proposed API on this.

astefanutti commented 9 years ago

I've just updated #13422 to align the API on that of SlimerJS onlyViewport.

brunowego commented 9 years ago

+1

OleMchls commented 8 years ago

@astefanutti are there any plan to open a PR for that, and: @ariya will this get into 2.1?

astefanutti commented 8 years ago

@nesQuick there is #13422 already opened.

OleMchls commented 8 years ago

thanks @astefanutti for pointing me to it, and for your work in fixing it :tada:

astefanutti commented 8 years ago

@nesQuick Until it gets merged, you can get recent binaries containing the fix from here: https://github.com/astefanutti/decktape/tree/gh-pages/downloads.

LRP-sgravel commented 8 years ago

What's interesting with this issue is that when using VisualCeption, jQuery computes the right coordinates for floating elements but the screenshots size doesn't match. You end up with a screenshot of whatever would be under the floating element. We really need a fix for this viewport size problem.

cristianmafferra commented 8 years ago

Still an issue at version 2.1.1. PDF size is right, but its content is zoomed out, only top left area of the desired content is rendered.