2

From this discussion on StackOverflow, I am well able to save an image out of a QML item into a file as png/jpeg.

How can I overlay or merge two different qml layers & merge them into one, to save it into a png/jpeg ?

Note: I am able to save a single QQuickItem. Just need to know how to overlay 2 QQuickItems

TheWaterProgrammer
  • 7,055
  • 12
  • 70
  • 159

2 Answers2

5

Just have the two qml objects be children of a root Item and then grab that root item, it will capture all its content.

Just make sure the root item is big enough to enclose the children, and the children are not in negative space, because it will only capture what's inside of the footprint of the root item.

You can also do manual composition, from C++ or even QML.

The problem described in your comment is you can't move stuff around, so what can you do? Instead of having the original QML objects as parents of the same root, you can have two Image elements, then you capture item A and set the capture result to serve as a source of image A, then do the same for item B, and finally, you capture the root item, which will capture the two images together.

OK, here is a quick example, it looks a little complicated, because grabs are async, and you have to wait for the individual grab results to be completed before you can grab the "final" root item, thus the usage of the timer. In this example, different items are laid out in a row, but you can compose them any way that you like:

ApplicationWindow {
  id: window
  visible: true
  width: 640
  height: 480

  Rectangle {
    id: s1
    visible: false
    width: 200
    height: 200
    color: "red"
  }
  Rectangle {
    id: s2
    visible: false
    width: 200
    height: 200
    color: "blue"
  }

  Row {
    id: joiner
    visible: false
    Image { id: t1 }
    Image { id: t2 }
  }

  Image {
    id: result
    y: 200
  }

  Timer {
    id: finish
    interval: 10
    onTriggered: joiner.grabToImage(function(res) {result.source = res.url})
  }

  Component.onCompleted: {
    s1.grabToImage(function(res) {t1.source = res.url})
    s2.grabToImage(function(res) {t2.source = res.url; finish.start() })
  }
}

First the two rectangles are captured and used as sources for the images in joiner, then joiner is captured and displayed in the result image, all objects except the final result image are hidden.

Even easier, you can use this nifty little helper to quickly join any number of items in a single image:

  Item {
    id: joinHelper
    visible: false
    property Component ic: Image { }
    property var cb: null

    Row { id: joiner }

    Timer {
      id: finish
      interval: 100
      onTriggered: joiner.grabToImage(joinHelper.cb)
    }

    function join(callback) {
      if (arguments.length < 2) return // no items were passed
      var i
      if (joiner.children.length) { // clean previous captures
        for (i = 0; i < joiner.children.length; ++i) {
          joiner.children[i].destroy()
        }
      }
      cb = callback // set callback for later
      for (i = 1; i < arguments.length; ++i) { // for every item passed
        var img = ic.createObject(joiner) // create empty image
        // need to capture img by "value" because of JS scoping rules
        // otherwise you end up with only one image - the final one
        arguments[i].grabToImage(function(temp){ return function(res){temp.source = res.url}}(img))
      }
      finish.start() // trigger the finishing step
    }
  }

And you use it like this:

joinHelper.join(function(res) { result.source = res.url }, s1, s2)

It still uses a row, but you can easily tweak it to do your own layouting. It works by passing the final callback and all items you want to capture, internally it creates an image for every item, puts them in the container, and then triggers the finishing timer.

Note that depending on how fast the system is and how complex the items are and what their count is, you may need to up the timer interval, because the final callback needs to be executed only after all captures were completed, the image sources were assigned and the images were resized to give the row its proper dimensions.

I also annotated most things to make it easier to understand.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • this sounds like an option but my app is quite a lot of qml layers. I don't have the luxury to ask everyone to put the qml components to save in one layer. Is there another C++ way to overlay different QML layers ? – TheWaterProgrammer Jul 10 '17 at 08:17
  • didn't understand the fresh edit you made. you have some code sample that illustrates what you mean ? you mean take `QImage`s from the the captures & merge them ? I still need a code sample to illustrate what you mean. thanks for your attention on this until yet. appreciate – TheWaterProgrammer Jul 10 '17 at 08:29
  • let me understand & try out the sample you have put on your answer. – TheWaterProgrammer Jul 10 '17 at 08:58
  • Thanks a lot until yet. One last question: after everything is done I obviously want to pass the final result to a C++ method which is of course a slot defined. I can do that in `Timer: onTriggered`? What do you suggest ? – TheWaterProgrammer Jul 10 '17 at 11:46
  • 1
    @HAL9000-Kernel what happens to the final result is defined by the callback you pass, replace `result.source = res.url` with whatever you want to do with the grab result. – dtech Jul 10 '17 at 11:49
  • 1
    But yes, you can modify it to skip passing a callback and instead write the callback in the timer handler, if you want to limit it to doing one thing. The way I wrote it gives you the flexibility to have an arbitrary grab result handler. If you remove the callback parameter, make sure to modify the code to do the parameter loop from zero instead of skipping one. – dtech Jul 10 '17 at 11:57
  • my save slot in C++ side is exactly as defined [in this link](https://stackoverflow.com/questions/11011391/saving-qml-image) which I mentioned in the question. it accepts a `QQuickItem`. Any suggestion on how I can add that as a callback in the Timer ? – TheWaterProgrammer Jul 10 '17 at 12:02
  • My slot is `Capturer::save(QQuickItem *item)` – TheWaterProgrammer Jul 10 '17 at 12:04
  • 1
    Then you will remove `joiner.grabToImage(joinHelper.cb)` and will pass `joiner` to `save()`. This means you can remove the `callback` and the `cb` property that holds it. You still need to use the timer to give the whole thing the time to complete thou. – dtech Jul 10 '17 at 12:05
  • I got your logic to work till a point where I see both the images on the saved png on my filesystem. but seems like the join logic is placing the images side by side. Not overlaying as I wanted. Could that be because of the `Row` element you have used ? – TheWaterProgrammer Jul 10 '17 at 12:25
  • may be I miscommunicated - I needed the images overlaid on each other. – TheWaterProgrammer Jul 10 '17 at 12:27
  • 1
    Just replace the Row with an Item – dtech Jul 10 '17 at 12:32
  • Really incredible. It works. I have some issue with incorrect overlay because of my qml items having some separate widths & heights. But the overlay does work to start with at least. thanks a million – TheWaterProgrammer Jul 10 '17 at 13:04
  • 1
    You can use `width: childrenRect.width; height: childrenRect.height` in the `Item` to make it automatically scale up to fit all children. – dtech Jul 10 '17 at 13:15
  • `childrenRect.width; height` looks very smart way to do this. seems like something with the anchoring is not correct though. one of my qml layers is putting up a Open GL processed image & the anchoring for that is something I need to fix. thanks anyways – TheWaterProgrammer Jul 10 '17 at 14:52
  • **should the size of the qml layers overlaid be taken into account somewhere in your code ?** my qml layers have complex `width, height & anchoring` applied to them. **I accepted your answer anyways** & sorry about so many questions. just in case you have a suggestion. – TheWaterProgrammer Jul 10 '17 at 14:54
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/148793/discussion-between-hal9000-kernel-and-dtech). – TheWaterProgrammer Jul 10 '17 at 14:59
2

This question seems to be closely related to this one

The solution is to render the Items in question into an texture of a second item, that you don't need to render to the screen. You can compose this Item as you want by adding multiple ShaderEffectSources as children, position them relatively to each other as you like, and set their sources to the Items you want to grab to Image.

Then you grab the Item to Image.

A generic example, that exposes a function to grab a list of Items to one Image, where each Item is stacked ontop of each other, with an opacity of 0.2 each:

import QtQuick 2.0

Rectangle {
    id: root
    visible: false
    color: 'white'
    function grabMultipleToImage(url, objects) {
        imgRep.url = url
        width = Math.max.apply(root, objects.map(function(e) { return e.width }))
        height = Math.max.apply(root, objects.map(function(e) { return e.height }))
        imgRep.ready = 0
        imgRep.model = objects
    }

    Repeater {
        id: imgRep
        onReadyChanged: {
            if (ready > 0 && ready === model.length) {
                console.log(root.width, root.height, imgRep.url)
                root.grabToImage(function (res) { res.saveToFile(imgRep.url); model = null })
            }
        }


        property int ready: 0
        property string url
        delegate: ShaderEffectSource {
            sourceItem: modelData
            width: modelData.width
            height: modelData.height
            opacity: 0.2
            live: false
            Component.onCompleted: { imgRep.ready++ }
        }
    }
}

The usage of this would be like this:

import QtQuick 2.7
import QtQuick.Controls 2.0

ApplicationWindow {
    id: myWindow
    visible: true
    width: 600
    height: 600
    color: 'white'

    Button {
        text: 'grab'
        onClicked: {
            test.grabMultipleToImage('testimg.jpg', [rect1, rect2, rect3])
        }
    }

    ImageGrabber {
        id: test

    }

    Rectangle {
        x: 100
        y: 205
        id: rect1
        color: 'blue'
        width: 10
        height: 20
    }
    Rectangle {
        x: 250
        y: 12
        id: rect2
        color: 'green'
        width: 20
        height: 30
    }
    Rectangle {
        x: 100
        y: 100
        id: rect3
        color: 'red'
        width: 100
        height: 5
    }
}

But if you have the need for more complex merging, you can also create this object manually and grab it when ever you want.

Without metering it, according to the note from the documentation

Note: This function will render the item to an offscreen surface and copy that surface from the GPU's memory into the CPU's memory, which can be quite costly. For "live" preview, use layers or ShaderEffectSource.

this solution should be more efficient than using Images with ItemGrabResults as sources for it keeps the stuff in the GPU memory until it is grabed and stored.