computerlyrik/dymoprint

420p label offset

Opened this issue ยท 106 comments

The labels are printed offset to the bottom (when reading the text) of the label.

Attached is a photo of the result (tested with release v2.2.1 on Linux):

  • a 12mm tape (black on white), with four prints in increasing tape-width specification.
  • a 6mm tape (black on yellow) with the same four prints.
    My 19mm tape ran out before I could test and is on order now, but I suspect that aligns correctly.

A possibility could be that the position of the tape is wrongly referenced:

  • The cartridges all sit in the cartridge-holder, with the cartridge edge facing the top of the text being the fixed reference surface for the cartridge. This is the unlabeled face of the cartridge, with the labeled side facing the door of the printspace.
  • However, the 6mm and 12mm (and presumably 9mm) cartridges are the same height.
    -- The 6mm tape sits centered in the oversized housing, with spacers above and below.
    -- The 12mm tape takes up the whole height of the cartridge.
    -- The printhead thus sticks out on the side of the cartridge with the label.
  • The 19mm tape housing is higher, but again starts from this top reference surface.

Also attached is a schematic (apologies to your eyes)
dymo420p_dymoprint_offset
arect4673

(I lack any skill in printer driver development...)

It is very strange because it uses the same cartridge models as PnP (D1), so I assume they used the same printing header. Why would it work differently?
Have you cross checked with official software or .. well, official method of use?

However, if this is shown to be a consistent behavior on this model of label printers then I guess it could be a matter of adding an offset for this particular model.

The unit has a little on-device keyboard and that works fine, the mode-switch-to-storage-enclosed "dymo mini" (or something like that) windows software also prints perfectly aligned. The official windows app from their site didn't see my labelprinter.
I looked at youtube video's of the PnP with D1 cartridges, and they look to be printing in the same orientation - so I'm at an utter loss.

How would I go about experimenting with offsets? I don't have the first clue about driver development.

khrise commented

I can confirm the behavior for my 420p. Using the official windows app, it prints correctly. Using dymoprint under arch linux, I have the same offsets as described by @Mousketeer .
I'd be happy to help or try things out. But, same over here: It would be great to get some (even rough) directions where to start.
@tomek-szczesny , can you help here?

@khrise I'm afraid we know pretty much the same about this code, as I'm not a contributor. Just someone who likes their PnP and tests new features. :)
But fear not, this is not a driver per se, just a program that creates USB packets and sends them.
I think that a portion of the code that differentiates between various models of Dymo printers must be found, and add a line that overrides offset value if it exists. If not, such value must be added.

My understanding of Python is very limited, I can read it mostly but not contribute, unfortunately.

As luck would have it my new 19mm tape came in this week, here are some tests:
A QR, large (5000px) jpg, and text at size 19 (dymoprint -t 19 -qr "contents" -p image.jpg) on 19mm tape. The top of the lowercase letters is just about in the centre of the tape.
2023-11-04-10-30-10-917
And, again on 19mm tape, text with the different sizes (-t NN).
img1(1)
How the 12mm tape (green) is seated in the machine compared to the 19mm:
align
And this is also interesting (coincidence?):
coincidence

So various clues:

  • The 19mm tape print is not centered, but too far to the bottom (side of cartridge with identification label)
  • The print offset is in the wrong direction

As for the material properties of the tapes:

  • The 19 and 12mm tapes are aligned at the top of the cartridge
  • The top of the 6mm tape sits at approx 3mm from the top of the cartridge.
  • 9mm still unknown

An older version of the full windows software suite works perfectly.

Any hints on where to start digging would be greatly appreciated :-)

Excellent work! If you're able to calculate necessary offsets for each tape size, that would be half job done. :)
The code suggests the print resolution is 8 dots per mm, and offsets must be added in dots, not mm.

D1 series cartridges (that fits PnP for example) appear to be centered regardless of tape size, which confirms your findings. Here's a photo of 9mm and 12mm I have.
obraz

So clearly a table must be added, with offsets per each printer model and tape size. I suggest doing it in such a way that if a printer is not present in table, 0 offset is assumed, for maximum compatibility.

I think this is the place to start:
https://github.com/computerlyrik/dymoprint/blob/2476b1722b0fbede7720be0d554e591c672fc54d/src/dymoprint/dymo_print_engines.py#L395C34-L395C34

Here, label_matrix is created, which I assume contains raw print data.
Then, dymo_devs is created and populated, which contains a list of connected printers. At some point one printer is chosen, and by reading this variable we may figure out which printer is in use.

Now, just before the printing is about to begin, here:

print("Printing label..")
the label_matrix can be manipulated to implement offset.

I'm not familiar with good Python practices, but knowing other projects, a table with offsets per each printer and tape width should be stored in a separate constants file. For example here:
https://github.com/computerlyrik/dymoprint/blob/master/src/dymoprint/constants.py

I can offer a check whether your changes did not break the PnP compatibility. :)

Now when I think about it, this will not solve the problem, sorry. Like I said, I can only speculate because I don't know anything more than any of you.

label_matrix contains only the data to be printed, so it is only as wide as the selected tape size. The actual USB data sending happens in this file:
https://github.com/computerlyrik/dymoprint/blob/master/src/dymoprint/labeler.py#L205
If someone completely understands this file, I guess the problem will be solved quickly.
I speculate that it's the PnP printer itself that centers received data in the printing area. If print rows are shorter than maximum, the printer may choose to print on its header center, which made sense in older models (I suppose D1 was the first tape cassette type they had).
Looking at four test prints on 19mm tape I see that 420p behaves differently, aligning the print area to the bottom. This has to be compensated for, because the code assumes automatic centering.

I fear that in order to print on your printers correctly, the printer must always think the tape is 19mm wide, and label_matrix have extra blank rows (or columns) to move the print area properly.

khrise commented

Mmh, ok, I'm getting somewhere. If I change the y-part of the position to 0 here:
https://vscode.dev/github/computerlyrik/dymoprint/blob/master/src/dymoprint/dymo_print_engines.py#L299
I get the following result. The position of the text appears to be correct, but the upper part of the text is cropped.
image
(Printing on 9mm tape)

maresb commented

If you run dymoprint --preview, does what you see on the screen correspond at all to what's being printed?

maresb commented

If dymoprint --preview appears correct, then probably the issue is how the data is being encoded and sent to the printer. It could be that whatever scheme works for encoding up to 12mm breaks down at 19mm and requires some adjustment.

khrise commented

Good point - tried the preview at the very moment. And nope: the preview is also "broken" in a similar way.
image
So the positioning appears to be correct, but the image does not contain the whole text.

The vscode links don't work on my desktop, just saying.

Yup, that's exactly why I later said my first comment isn't the best idea.
I think what should be done is to add more columns to label_matrix, to match (19 * 8) matrix width, and make this call:
https://github.com/computerlyrik/dymoprint/blob/2476b1722b0fbede7720be0d554e591c672fc54d/src/dymoprint/dymo_print_engines.py#L506C1-L506C79
always with 19mm tape width.

The trick is to add correct amount of blank columns on front and back of existing matrix for each tape size.

maresb commented

It seems like it works for me. Could the cutoff be a result of the adjustment you made?

$ dymoprint --version
dymoprint 2.2.1
$ dymoprint --preview -t 19 test
Demo mode: showing label..
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€    โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€      โ–€โ–€โ–€โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€                                                   โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ                                                       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ                                                        โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ  โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–„โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€โ–€โ–€          โ–€โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€                      โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€                            โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€               โ–„                 โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€        โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–„โ–„        โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€       โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€        โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ                 โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ              โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„ โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ          โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€       โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€              โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ                  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ        โ–„โ–„โ–„โ–„        โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ       โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ       โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–€       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„                   โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„                โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„          โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„  โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–€                                         โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€                                                      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€                                                       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ        โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„      โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„     โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„    โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ
khrise commented

Yes, definitly. Using the code from the repo, I get the same (correct) preview. However, I changed the y-offset in dymo_print_engines.py#L299 to 0, just to find out where I can adjust the erroneous offset in the printed labels (the original issue). With this change, the text on the label gets printed as shown above. And also the preview shows that the image already contains only half of the text. So, the image doesn't get rendered correctly, now we have to find how to fix that, in order to implement "switches" for the 420p.
I assume it's either somewhere in render_engine.render_text or render_engine.merge_render

khrise commented

But, as @tomek-szczesny says, I might as well be on the wrong track.

maresb commented

Taking a quick look, I'm quite skeptical of this line:

return int(8 * tape_size / 12)

I'd try adding import math and replace int(...) by math.ceil(...)

@khrise, I'll try to put everything I know now in simpler terms:

Let's call label_matrix our "canvas", which is of a size of a label, and what the CLI preview shows.
What you edited is a routine that plots text on a canvas, thus does not affect pictures, barcodes, QR and so on.
I would say that our canvas and its contents are completely fine.

As far as I can see, the canvas is sent to the printer verbatim with no changes. This means it has variable width.
The problem is how the printer interprets canvas smaller than the printer head - which in your case is 19mm, and in PnP case it's 12mm.
I made a similar test to yours T19 prints which I find the most valuable. This is a 12mm tape.
obraz

It appears both printer models behave the same, as indicated in the programming manual lined in the labeler.py file header:
https://download.dymo.com/dymo/technical-data-sheets/LW%20450%20Series%20Technical%20Reference.pdf
This is a programming manual of a completely different Dymo product, so things may or may not work.

Here on page 15 we can read that the printer is made aware how many bytes per line will be sent using "Set Bytes Per Line" command, and that the right side of the label will be left blank.
This is in fact what we observe, the printer prints the canvas on the leftmost edge of the label. This is wrong, and this is probably wrong in all printers supported by dymoprint today.

I'll share another picture where I use a lot of 9mm labels. The one on top has been printed using a handheld Dymo 160, and those on the lower end using PnP and dymoprint. The text is too low. If I used 6mm labels, I would probably notice this before.
obraz

The solution:
On the same page of dymo documentation, you can see a "Set Dot Tab" command, which is supposed to shift the whole print away from the leftmost edge.
This command is sent to the printer here, before it starts printing:

self.dotTab(dottab)

You could try experimenting with adding +20 to dottab variable one line before and see if it makes any difference.

If yes, then all we need is a dottab table for each printer and each label size, instead of whatever method is in use right now.

If not, then we know this function simply doesn't work and may be removed. After all, this is a documentation of a completely different Dymo product and not everything must be implemented in our label printers.
If it doesn't work, we'll have to achieve the same result by increasing canvas size, thus adding more "white space" under the existing canvas. This is what I called "adding columns to label_matrix".

khrise commented

And the winner is... @maresb ! :) At least, he pointed me to the relevant code. int as compared to math.ceil doesn't make so big a difference, in that case. However, I experimented a little and found out that return int(16 * tape_size / 12) in max_bytes_per_line appears to do the trick. I still need to verify that this works with other tape widths, too. I tried with 12mm an it worked like a charm.

@tomek-szczesny : Increasing dotTab makes the problem even bigger, moving the text further down on the label.

khrise commented

Well, it's not that simple, apparently. I will figure out appropriate max_byte_per_lines for other type widths, and get back to you here.

I think this may take you a long time to figure out a proper setup for all tape widths and all supported printers, if you wish to do it by guessing which variable should be tweaked.

I'm glad to know dottab manipulation works. Have you tried reducing it then? Maybe it's too big? Just don't make it negative.

maresb commented

Hey, cool! Thanks a lot for all the experimentation @khrise! I don't think there are so many tape sizes, so maybe we just replace that formula with a lookup dictionary.

khrise commented

@maresb , that's what I think, too. A lookup table per device with sane defaults (or fallback to the existing method).
I'm not too familiar with python development, so would you be able to prepare something, or give me a few hints on how you would implement a device specific config? If not, I'd be able to come up with "something". :)

@tomek-szczesny , dotTab manipulation doesn't get us anywhere. The effective dotTab varies between 0 and 5 or so. Like I said, increasing it makes the situation worse, and we can't set it below 0, which we would need to get the text further "up".

dottab manipulation does work, and it represents 8 dots, or ~1mm. Just like the documentation said.
In my case it moves the text up like it should, no idea why it works differently in your code. Perhaps you didn't undo some of your previous hacks.
obraz
I removed the matrix optimization routine just to be sure I'm working with real dottab values. It's completely unnecessay on such small and slow printers anyway.

while [] not in lines and max(line[0] for line in lines) == 0:
lines = [line[1:] for line in lines]
dottab += 1
for line in lines:
while len(line) > 0 and line[-1] == 0:
del line[-1]

Since dottab cannot work with fractions it's not worth trying to solve the problem this way - we could end up with 0,5mm offset in the worst case.

The solution - and I'm completely sure of this - is to expand canvas appropriately, after it's fully populated.

However, I experimented a little and found out that return int(16 * tape_size / 12) in max_bytes_per_line appears to do the trick.

Because it artificially increases the label size in uncontrolled way.

label_height = DymoLabeler.max_bytes_per_line(self.tape_size) * 8

I mean, yeah it works for one printer and one tape size, but it breaks the math in the whole program. "bytes per line" loses its meaning, and builds up such a mess I swear I'll fork this project if you pull a hack like that.

The best solution, that will always work per given printer head size, is to expand canvas up to the maximum tape size for the printer - by adding the right amount of empty lines on the bottom and top. The bottom side lines will move the content up, and the top lines must be added to round the dot number to full bytes.
Since the 19mm tape reportedly is not centered like other sizes are, this cannot be done algebraically and LUT will be necessary. Probably the best idea is to build a LUT for each printer model, before someone else shows up with yet another Dymo and misaligned prints.

But given that my credibility in this thread has been thrown to trash and some of you want to guess how to proceed, despite asking for advice beforehand... go ahead, I won't spoil the fun. Good luck!

maresb commented

But given that my credibility in this thread has been thrown to trash and some of you want to guess how to proceed, despite asking for advice beforehand... go ahead, I won't spoil the fun. Good luck!

@tomek-szczesny, I greatly value your insights and perspective, but please let's not have this tone. It is not constructive and is frankly quite offensive.

And the winner is...

Also @khrise this is a collaboration and not a contest. I'm glad what I wrote was useful, but I think @tomek-szczesny has a fairly solid understanding here, better than mine.

Back to the subject matter:

  • Let's get rid of dottab and line trimming. It adds complexity and doesn't seem helpful.
  • Does anyone know exactly how many pixels tall the 420p can print? Or have some idea about how to figure this out?

Didn't mean to sound aggressive at all, just wanted to get the message across, that either we seek the best working and maintainable solution the meritocratic way, or.. that other approach. I'm glad we agree.

I think the dottab related trimming can stay, it doesn't seem to do any harm. I saw no change in performance after removing it. And it may become necessary again if dymoprint start supporting much larger printers. Just wanted to point out it's not currently necessary.

To test the 420p maximum label width in pixels I can't think of any other way than experimenting. I created a pattern that can be used for testing that. A high resolution photo of the portion that didn't fit will unambiguously tell us how many pixels did fit on a print. In order to print it I would temporarily modify software to think that label is 20mm wide and doesn't do any image scaling whatsoever. I think Dymo doesn't crash when it receives data that doesn't fit on the print.
obraz

Officially it has 180dpi resolution, which means 7,087 dots per mm. So it is confirmed:

PIXELS_PER_MM = 7

This could be true for all Dymos, I'm used to Japanese 8 dots per mm printers (203dpi).
Unfortunately I've never seen a thermal printer that can report supported pixel width.

Now, onto the actual offset problem:

I suggest adding some code to print_label method:

def print_label(label_bitmap, margin=DEFAULT_MARGIN, tape_size: int = 12):

Now ideally we would like to modify the picture before it gets converted into a byte array for dymo printer, but at the same time we have to know beforehand what printer and label size are we dealing with.
So I think we would have to move things around a bit.
The code generating label_matrix should be moved after the printer is detected, so right before the dymo labeler object is created:
# convert the image to the proper matrix for the dymo labeler object
label_rotated = label_bitmap.transpose(Image.ROTATE_270)
labelstream = label_rotated.tobytes()
label_stream_row_length = int(math.ceil(label_bitmap.height / 8))
if len(labelstream) // label_stream_row_length != label_bitmap.width:
die(
"An internal problem was encountered while processing the label "
"bitmap!"
)
label_rows = [
labelstream[i : i + label_stream_row_length]
for i in range(0, len(labelstream), label_stream_row_length)
]
label_matrix = [
array.array("B", label_row).tolist() for label_row in label_rows
]

# create dymo labeler object
lm = DymoLabeler(devout, devin, synwait=syn_wait, tape_size=tape_size)

Then, we will create a bigger picture and paste the original print_label onto it, thus creating an offset.
This will fit nicely right at the beginning of print_label generating code, mentioned above.
It is worth noting the picture size must be rounded to 8 pixels, so it may be packed into bytes afterwards.

The pseudocode would go like this:

# convert the image to the proper matrix for the dymo labeler object
offset = lookup_table[printer_model, tape_size]
offset_rem = 8 - (offset % 8)

label = Image.new("1", (label_bitmap.width, label_bitmap.height + offset + offset_rem), )
label.paste(label_bitmap, (0, offset_rem))
label_rotated = label.transpose(Image.ROTATE_270)
labelstream = label_rotated.tobytes()
(...)
khrise commented

Didn't mean to sound aggressive at all

Phew, that didn't work well, and I still need to recover a little, to be honest.

Anyway, apart from all my hacking and guessing, I tried some of your suggestions, with some success.
First of all, I managed to print your image to see how many pixels the p420 can print. I disabled image scaling and printed the picture "raw". (Btw: the command line always requires a text, it won't allow printing an image only, is that intended?). However, I had to rotate your image by 180 degrees, because the got printed top-aligned, (otherwise the 140 was printed, but the 40 and the 20 was not - this might explain our dtop-moves-text-up-or-down discrepancies above). That's the result.

p420

Does that look plausible?

I think Dymo doesn't crash when it receives data that doesn't fit on the print.

It doesn't. However, I had to disable the check in the code.

Then, we will create a bigger picture and paste the original print_label onto it, thus creating an offset.

This looks good and achieves good results. Only tested for 12mm tape until now. Using your pseude code from above, I found an offset of 4 to achieve good results for a 12mm tape.

khrise commented

Oh no, I need to print that on a 19mm tape, right?

Yup, we wanted to see the 19mm tape. Although I'm not entirely sure what we need that dot number for, @maresb?

You also need to fool the code that 19mm tape is in fact bigger, like 21mm.
On your test print on 12mm tape there are visible borders on both sides which are not caused by hardware, we'd like to avoid that for this test.
Anyway the pattern appears to work nicely, I see 79 dots across, which is 11.15mm. Which is weird, why not 80? I'll do the same pattern on my printer later.

Indeed, we'll have to take into account that your printer may have different firmware and stuff. Hopefully a look up table of offsets for each possible mode of operation will solve this problem once and for all, and for all of us. :)
I'll buy a 6mm tape for PnP and find the optimal offset for this model as well.

The interesting side effect we will achieve here is that 9mm and 6mm prints will be essentially without any margins. :)

(Btw: the command line always requires a text, it won't allow printing an image only, is that intended?).

That should become another issue, in my opinion, feel free to create it. I suspect this was the first functionality implemented, thus used to be mandatory.

maresb commented

I feel like there's something very fundamental I'm not understanding here. I've tried rereading this thread, but I still don't get it...

On one hand it seems like we're passing an image of the incorrect height which is causing the label to not be centered. So it seems like we should simply use a canvas of the correct size, no? Like do a LUT from label width to canvas height. Then what is all this talk about offsets, and making things dependent on device?

I also counted 79 dots and was slightly confused. Maybe the topmost and the bottommost pixels are not reliably printable?

I agree that image-only should be supported, and should be opened in a new issue.

maresb commented

Ah, the point is that the canvas size for a given machine is always the same, but when printing on a smaller-sized label we need to map onto a subset of the canvas according to how the label is physically positioned?

Yes, pretty much.
Both PnP and 420p do not know what tape size is currently in use. Even handheld Dymo 160 requires user input on that.
This means that the whole printing head is active, and printer firmware behaves the same regardless of the medium size.

dymoprint uses variable image size, which PnP by default aligns downwards, not on the center. Thus as I showed before, printouts on 9mm tape are misaligned. I suspect that 6mm tape printouts are even worse, but let's be honest, nobody tested that to this day...

Because of that, we have to have a method of adding offset per each tape size. Knowing all the variables we could calculate that in code. But because tape alignments in 420p do not follow a logical pattern, as shown in the first posts, it has to be a look-up table. Moreover, 420p has a larger printing head, so offsets will be different.

I believe that since @khrise owns 420p with different tape sizes, and I volunteered to buy a 6mm tape for PnP, we will have all corner cases covered. Should anyone file an issue on centering, it will be a matter of updating a LUT.

I anticipate future Dymo products that will get hardware and firmware changes, and we will be well prepared for them.

maresb commented

Thanks @tomek-szczesny for the clarification.

Playing around with my PnP it seems like of my 64 columns numbered 0...63, the following don't print: 0,1,2,3,4,63.

It seems a bit glitchy though, so my results might not be completely reliable.

@tomek-szczesny I own a 420p and a 6mm tape :-) I have limited time in the next days, but given specific instructions I can test for sure.

To be clear, I think this represents the offsets best:

Tape size Offset from bottom Offset from top Case
19 0 0 19 + sides
12 7 0 12 + sides
9 ? ? ?
6 approx 10.5 approx 3 12 + sides, tape floats in the middle
(top = when oriented for reading; the identification label is then at the bottom)

Even more intriguing: if you use the device itself to print (so no pc involved) and tell (lie to) it that it has a 19mm tape loaded and then print a 40pt text/letter with a box around it, it will draw the vertical sides of the box beyond both bottom (logical) and also the top edge (!) of a 12mm tape tape (no trace of the horizontal components of the box). If my table above is correct that means on all tapes the top is borderless.

I'll also try to take mine apart and take some macro shots of the printhead and tape alignment, for your pixel peeping pleasures. (not before Wednesday 'tough)
I'll see if I have a nearly-spent 6mm cartridge I can take apart - that is most likely a one-way operation 'tough.

maresb commented

Am I correct that in this table, all measurements are in mm, and it represents the physical position of the tape, so that in theory, tape size + bottom offset + top offset = 19 mm?

And your comment means that the 420p can print pixels all the way up to the top edge for all tape sizes?

maresb commented

BTW regarding printable area, I can achieve solid black output by putting the line

    label_bitmap.paste(1, [0, 0, label_bitmap.width, label_bitmap.height])

at the top of the print_label function.

I think I do have several bad columns up top:

image

@Mousketeer thanks for the data and willingness to help, but then again, I can't see a reason to pop your printer or cartridge open. When the code is done, we'll simply test offsets and find the most suitable ones. There's no need to understand what exactly Dymo engineers were thinking.

@maresb how about you just draw a black square in gimp and try printing that instead. ;)
I think there may be some problem with data transfer. Notice the two dots on the bottom of your picture, which look like they were carried over from the other side of the print.
On the picture from @khrise one dot also looks like it has been carried over, instead of being printed as the 80th dot.

The print area should be about 11mm so there's definitely something wrong.
I'll look into this myself in the following days.

khrise commented

One other quick note, regarding printable area, the 19mm tape itself is 19mm wide, but that thin sheet which does the actual printing (whatever this is called), is only 18mm wide (at least for my black on red 19mm tape cartridge). Whatever this tells us, but it might support @tomek-szczesny 's point that we just have to determine the required offsets empirically rather than calculate stuff.
PXL_20231106_070511420 MP

Using your

BTW regarding printable area, I can achieve solid black output by putting the line [...]

technique, I get the following result:
(My 19mm cartridge is somewhat messed up after a tape jam, it doesn't rollup the thermo-sheet any more. But that's pretty nice for this experiment - note the unprinted pixel in the lower end)

PXL_20231106_070623240

maresb commented

@maresb how about you just draw a black square in gimp and try printing that instead. ;)

Because I don't trust the correctness of dymoprint, and I want to get as close as possible to the raw output. ๐Ÿ˜‚

I agree that these borders are very suspicious with these extra dots being printed, especially since it seems reproducible.

My 12mm tape is detected to have a max byte length of 8, so 64 pixels. Using 180 dpi, this comes to 9.03mm printable. I tried increasing max byte length to 9 and this didn't change the print, but as you say, it might be messing up the math somewhere else. Which is also why I wanted to paint everything black as late as possible.

maresb commented

Oh, I just cleaned the print head as per the instructions at https://www.youtube.com/watch?v=tu3jLmO06zE and now things look much better:

image

maresb commented

Also checkerboard and inverse checkerboard. The vertical banding is quite visible by eye, and I'm guessing it's due to variation in the motor speed.

    for x in range(label_bitmap.width):
        for y in range(label_bitmap.height):
            label_bitmap.putpixel((x, y), (x+y+1) % 2)

image

Thank you all for your inputs. We'll have to keep in mind that the alignment of tape inside cartridges isn't perfect either, especially 9mm and 6mm which are centred in the same plastic case as 12mm. I believe this is why some small margins exist using official software.

I'll try to come up with a better test pattern, that will directly tell us what's the usable print area for each printer and tape, in absolute dot coordinates terms. The one I proposed before assumed that lower end will always get printed reliably.

Then we will have reliable data to calculate offsets and actual printable area for each tape and printer. I suggest we keep this data in dots rather than millimeters in code, because that's what the hardware is dealing with, and we will lose precision on conversions back and forth.

maresb commented

I can now print 64 columns total. When I increase from 8-bit to 9-bit my printer prints columns 8-73.

khrise commented

If we go the "increase the canvas size and place the rendered bitmap with the correct (LUT-backed) offset inside"-route, then we must use the same max_bytes_per_line for each tape width, basically taking the tape width out of the equation, right? At least, for the 420p. (Or, we'll have to increase the max_bytes_per_line by the same LUT-backed offset).
Am I understanding this correctly?

maresb commented

@khrise, that agrees with my understanding.

@tomek-szczesny, here is the test pattern I've been using:

    # Draw 6 vertical stripes
    for x in range(0,12,2):
        for y in range(label_bitmap.height):
            label_bitmap.putpixel((x, y), 1)

    # Draw 4 horizontal stripes at y=0 connecting 5 stripes
    for y in range(0,8,2):
        for x in range(8):
            label_bitmap.putpixel((x, y), 1)

    # Draw 4 horizontal stripes from the opposite side connecting 3 stripes
    for y in range(0,8,2):
        for x in range(4):
            label_bitmap.putpixel((x, label_bitmap.height - 1 - y), 1)

    # For reference, draw a horizontal stripe at y=55 connecting 4 stripes
    for x in range(6):
        label_bitmap.putpixel((x, 55), 1)

    # Draw a pattern indicating divisibility by two of the y-value.
    # This makes powers of 2 stick way out.
    x = 12
    for y in range(label_bitmap.height):
        label_bitmap.putpixel((x, y), 1)
        label_bitmap.putpixel((x + (y & -y).bit_length(), y), 1)

Here's what the output looks like:
image

The first one has max_bytes_per_line=8 and the second has max_bytes_per_line=9. The pattern makes it easy to figure out the binary value of y for any pixel, assuming you know the orientation. In the second pattern the dot sticking way out is y=64, and it's easy to count from there up to y=71. For purposes of orientation and cross-checking, I draw a few handy horizontal "lines" which don't print properly: 4 wide at the top (truncated in the pattern on the right), 4 thin at the bottom, and 1 medium at y=55.

max_bytes_per_line appears to be a metric of canvas size, and is calculated based on advertised tape width. There are two problems with this approach: First, the relation between tape size and printable area may not be a linear function, and may not be the same for each printer model. Secondly, we don't have to and shouldn't round the dot number to full bytes, as we have already shown.
Perhaps instead of canvas size and offset, we should focus on lower dot and upper dot that fits on the printout, and keep these values in the LUT. It will be easier to maintain especially if dymoprint also has a hardcoded test pattern function.
And most definitely we should operate on dots, not millimeters or bytes.

@maresb I was thinking about a similar pattern, but with checkerboard doubling its size on each iteration, left to right, up to 64 dots per square. On each iteration, it should always begin with the same color. I think this would be easier to read. Do you think you could implement it as a permanent option in dymoprint? A described pattern on hardcoded 192-dot wide picture.
Thanks to the largest squares, we will be able to tell how Dymo printers align received data. I'm still not sure which excessive bytes it discards, the first or last. This pattern will answer that, and everything else we want to know.

I could implement image manipulation procedures that apply LUT values, since I already wrote an essay how to do it.

Someone else would have to implement LUTs, and the owners of 420p would be asked to take pictures of a test pattern printed on various tape sizes.

What do you think? Am I missing something?

khrise commented

Someone else would have to implement LUTs, and the owners of 420p would be asked to take pictures of a test pattern printed on various tape sizes.

Happy to. As long as I don't have to do the interpretation :)
Seriously, I'm new to this whole topic, and you sort of lost me here and there, but focussing on the dots rather than measures makes sense to me.

For completeness' sake: Here is @maresb 's pattern on a 19mm tape, printed on 420p. The numbers represent the hard coded bytes per line. Don't know why it looks that different from @maresb 's prints.
You do the counting :)

PXL_20231106_103743884

Btw: I guess I should clean the print head, too.

Vertical lines are the hardest to print, because the whole print head has to be "active", thus consumes maximum power. Perhaps the battery is low, that may explain why some vertical lines are missing or incomplete.

16 bytes is 128 dots, which gives some 18mm. Try pushing it up to 17 bytes, together with 16 next to it for comparison. :)

khrise commented

Vertical lines are the hardest to print, because the whole print head has to be "active", thus consumes maximum power. Perhaps the battery is low, that may explain why some vertical lines are missing or incomplete.

Ah, that makes sense. Battery is loaded, but this pattern might indeed push the printer to its limits.

Here is 16 and 17:

PXL_20231106_110348703

khrise commented

I asked this chat guy for your checkboard pattern, and he came up with this:

PXL_20231106_113155290

@tomek-szczesny , is that anywhere near your described pattern?

edit: Well, I don't think so, actually :)

maresb commented

Thanks a lot @khrise for the test patterns!!!

On the 16 pattern:
y=0,1 is not visible at the top but I can see half of y=2.
Unfortunately the dot at y=64 is missing; it should be right in the middle where there is a blank.
Given that we can see a 4th line at the bottom, that indicates that y=127 is visible.

On the 17 pattern: by my count the topmost is y=10, and this is consistent with the expected offset of 8 from the 16 pattern.
The y=128 pixel is missing from the bottom, but counting down from the conspicuous gap I see a very faded y=135 at the bottom.

After cleaning I think it will look much better. I think this confirms that we're working with 128 pixels, so double that of my PnP.

@tomek-szczesny, I like your explanation of the printer struggling with vertical lines. Also, I like your idea regarding the checkerboard; I think it accomplishes the same intention of my pattern, but it is more robust and intuitive, although wider. I think it'd be good to add it to the CLI. (For orientation, perhaps we should print y-values of some squares once they reach sufficient size.)

I'm still not sure which excessive bytes it discards, the first or last.

It discards the first/topmost/small-y, printing the last/bottommost/large-y.

I meant a pattern more like this. I know my description may have been very.. lacking. Sorry about that. :)
(It's a sketch hastily made in Paint at work so pardon the precision and completeness)

image

maresb commented

@tomek-szczesny, that's exactly what I had in mind from your description.

I think it would benefit from a few minor tweaks:

  • Include a repetition with alternation at each level, making it twice as large; this breaks up the large black area in the lower-right
  • Go right-to-left so that the machine can warm up; I'm just slightly concerned about beginning with the finely-detailed pattern
  • Add another testing segment to test thin horizontal and vertical lines

It discards the first/topmost/small-y, printing the last/bottommost/large-y.

Okay, now, seriously.. When I was adding dottab, it moved the picture upwards, suggesting that the bottom is the beginning of a line. This is also consistent with image rotation by 270 degrees before it gets converter into a printer bit stream. In other words, it's rotated in such a way that the bottom of the text is on the left hand side - by printer convention, the beginning of a line.

@khrise reported the opposite behavior of dottab which may indicate that 420p feels differently about that..

But don't worry, I'll deal with image processing part.

Well, anyway I think the proposed pattern will tell us everything we could possibly ask about. The test print width of 192p and maximum square size of 64 dots is by design, so we do not lose orientation where the "beginning" is.

  • Include a repetition with alternation at each level, making it twice as large; this breaks up the large black area in the lower-right

I drew that in Paint and it was harder to read, at least for me.

Go right-to-left so that the machine can warm up; I'm just slightly concerned about beginning with the finely-detailed pattern

Every single line will be equally populated with dots at about 50%, except the last one, which I expect to have more than 50%. So again, we'll see. You can add a few vertical lines at the beginning to warm it up. :)

khrise commented

This is just for fun and to see if I can get the chat guy to understand me. From now on, I'll let the pros do the talking (and the checkerboard styling) :)

PXL_20231106_122642448

edit: the "chat guy" is chat gpt, obviously.

maresb commented

Chat guy is great! I wonder if he can understand what I'm talking about with adding an alternating repetition making the pattern twice as wide. Don't be shy about sharing the code too.

He actually hooked me up with the (y & -y).bit_length() trick for computing divisibility-by-two. ๐Ÿ˜‰

khrise commented

He actually hooked me up with the (y & -y).bit_length() trick for computing divisibility-by-two. ๐Ÿ˜‰

Uh, sophisticated :)

khrise commented

Don't be shy about sharing the code too.

Sure. The result of this method can be passed to DymoPrinterServer.print_label

    @staticmethod
    def generate_testpattern(max_bytes_per_line):
        # Calculate the number of dots per line
        dots_per_line = max_bytes_per_line * 8

        # Create an empty image
        image = Image.new('1', (dots_per_line, dots_per_line))

        # Initialize the size of the squares
        square_size = 1

        # Initialize the x-coordinate of the current line
        x = 0

        # Draw the checkerboard pattern
        while x < dots_per_line:
            # Iterate over each square in the current line
            for y in range(0, dots_per_line, square_size * 2):
                # Draw the squares
                for i in range(x, min(x + square_size, dots_per_line)):
                    for j in range(y, min(y + square_size, dots_per_line)):
                        image.putpixel((i, j), 1)
                    if y + square_size < dots_per_line:
                        for j in range(y + square_size, min(y + 2 * square_size, dots_per_line)):
                            image.putpixel((i, j), 0)

            # Move to the next line
            x += square_size

            # Double the size of the squares
            square_size *= 2

        return image
khrise commented

I wonder if he can understand what I'm talking about with adding an alternating repetition making the pattern twice as wide.

I think I'd translate that into "Can you add another vertical line for each square size with the color the other way round?" :)

PXL_20231106_131326749

But imho, this doesn't improve readability too much.

Except this script didn't generate the smallest, single dot squares. Or your printer didn't print them. :)
I assume this is a 19mm tape and we expect 128 dots across..

@maresb I was thinking this could be a dymoprint option that supports a parameter, the dot height of a test pattern.
If we get this to work, then it will be much easier to implement and test offset related code.

Alright, gang, I set aside some time tomorrow after work to look into this. I'll fork the project and refactor image generation code, so it supports two parameters from look-up tables: Printable area and offset.

I'll remove any references to bytes, specifically, and leave references to millimeters only where it is applicable due to the nature of print content.

I don't mean to push anyone, but @maresb, knowing that you are a busy person, are you able to pull off that test pattern generation in a reasonable time? If not I think I could take care of that as well, but I admit I would feel better with your assistance.

I don't even know Python after all. ;)

maresb commented

supports two parameters from look-up tables: Printable area and offset

What are the keys? Device ID and tape width?

are you able to pull off that test pattern generation in a reasonable time?

Things are getting really busy for me this week, so I'm not sure I'll be able to get to it. Anything you could get done would be great! We can always clean up whatever you write. For my current work I already dumped above everything I currently have, and I don't think this code needs to be particularly elegant.

My personal dream list for the pattern would be: solid black, horizontal alternating stripes (both parities), vertical alternating stripes, single-pixel checkerboard, growing checkerboard. For the growing checkerboard, I agree that alternation is not necessary. I think we could stop at 16. If we want to get really fancy we could try printing line numbers in the size-16 squares. But anything you implement in whatever way would be appreciated.

I don't even know Python after all. ;)

Tomorrow is a great day to learn! ๐Ÿ˜ And the chat guy is very helpful.

What are the keys? Device ID and tape width?

That is correct!
I was thinking I may start with two command line parameters for convenient guessing effort. :)
And then add LUT that will be used if no command line params are present.

Phew I suck at python, as it turns out. :D

I created a test pattern generator, it's in my repo and I did a pull request here.
3117199
It takes one argument for dot height of the pattern. I tested 64 and 70.

image

In 64 case, my printer printed only 62 dots, 0-61.
In 70 case, it captured dots 6-66, so even less, only 61.

I tried with black on white, the results are different:
image
All 64 dots are visible on 64-dot test. Similarly, 70-dot test shows dots 6-69, which are 64 dots in total.

I'm guessing it's the matter of what @khrise has shown a few days ago, that the thermal transfer sheet thingy is narrower than the label itself, and this may vary cartridge to cartridge - their width and mechanical misalignment.
I suggest we take that into consideration while creating the future LUT and make some 1-dot margins for each side.

Now if you're able to run this code with your 420p I would appreciate test prints of 128-dot test pattern on 19mm first, and then we'll decide how to proceed with smaller tapes. :)

maresb commented

Thanks @tomek-szczesny!!!

As per the README, you can install and run this with:

pipx install --force git+https://github.com/tomek-szczesny/dymoprint@test-label
dymoprint --test-pattern 128 xyz

where xyz is a required but irrelevant and discarded parameter.

khrise commented

Thanks!
Here's the result from 420p with 19mm tape:
PXL_20231109_104248397

Yes, I will clean the print head soon...

Bad timing, I pushed some changes at work today without testing.

Looks like you really need to clean the header, because there are hardly any single dots.
Could be an issue of a tape as well. Do you have some other 19mm tape?

khrise commented

Looks like you really need to clean the header, because there are hardly any single dots.

That would explain why there were no single dots on "my" test pattern the other day. The single pixels were in the image, but they didn't get printed somehow.

Could be an issue of a tape as well. Do you have some other 19mm tape?

Nope. I will order one, though.

Oh, I just cleaned the print head as per the instructions at https://www.youtube.com/watch?v=tu3jLmO06zE [...]

Ok, there is no brush in the 420p's cartridge tray. However, I did some cleansing with naive means.
Here is the result:

PXL_20231109_110614037

I pushed some changes at work today without testing

Where does that arrow thing comes from, all of a sudden? Is that part of your untested changes?

khrise commented

Oh, and one more thing: The dotTab command throws an error in the end. But that seems to occur after printing is finished.

File "~/.local/pipx/venvs/dymoprint/lib/python3.11/site-packages/dymoprint/labeler.py", line 210, in printLabel
    self.rawPrintLabel(lines, margin_px=margin_px)
File "~/.local/pipx/venvs/dymoprint/lib/python3.11/site-packages/dymoprint/labeler.py", line 225, in rawPrintLabel
    self.dotTab(dottab)
File "~/.local/pipx/venvs/dymoprint/lib/python3.11/site-packages/dymoprint/labeler.py", line 131, in dotTab
    raise ValueError
ValueError

Bad timing, I pushed some changes at work today without testing.

We might be better off using particular commits, rather than branch heads, at the moment.

My new code was meant to add a bunch of horizontal line, which do appear but in the wrong place apparently.

The "arrow" is probably a part of letter "x". Currently dymoprint treats text as a required parameter, a different problem to be solved someday.

In next 5-6 hours I will have this code fixed, got to get off work first. :) It may be dull but pays my bills after all.
From that moment I'll point a stable commit, good call.

I think I'll remove dottab and matrix optimization after all, it's a needless complication.

khrise commented

The "arrow" is probably a part of letter "x".

That sounds plausible, indeed.

I think I'll remove dottab and matrix optimization after all, it's a needless complication.

dito.

In next 5-6 hours I will have this code fixed, got to get off work first. :) It may be dull but pays my bills after all.

No worries, same over here.

Found the bug, but I'll let you know once I have it tested on my Dymo first.

maresb commented

We might be better off using particular commits, rather than branch heads, at the moment.

No problem. In the pipx install command, in the tomek-szczesny/dymoprint@test-label part you can replace test-label with any Git SHA (short or long). To see what is currently installed, you can run dymoprint --version and you'll get something like

dymoprint 0.0.post1.dev250+g6b2443b

where +g stands for Git, and 6b2443b is the short SHA.

I confirmed the code is working as expected. This is the commit 8d27f8d.

I added 10 interlaced lines on each end, so it's much easier to count how many dots are missing.
image

I'd like to also invite @Mousketeer to do some prints of a test pattern. :)
The instructions are above if you didn't keep track of the conversation #80 (comment)

Test pattern on all tapes I got.
Looks like 9mm tapes work well across some 62 dots. :)

PXL_20231109_173843435.jpg

PXL_20231109_173914945.jpg

khrise commented

In the pipx install command, in the tomek-szczesny/dymoprint@test-label part you can replace test-label with any Git SHA (short or long). To see what is currently installed, you can run dymoprint --version and you'll get something like

dymoprint 0.0.post1.dev250+g6b2443b

Mmh, that's weird. I installed using

pipx install --verbose --force git+https://github.com/computerlyrik/dymoprint@8d27f8dad0192a97d0e07b445ae6169488dc4944 

and end up with

 installed package dymoprint 2.2.1.post1.dev21+g8d27f8d, installed using Python 3.11.5
  These apps are now globally available
    - dymoprint
    - dymoprint_gui
done! โœจ ๐ŸŒŸ โœจ

and

~ >>> dymoprint --version                                                                                                            
dymoprint 2.2.1.post1.dev21+g8d27f8d
maresb commented

@khrise, that looks correct to me. The first 7 characters of 8d27f8dad0192a97d0e07b445ae6169488dc4944 are 8d27f8d which matches the short SHA you produced. (Visually I always get confused by the leading g character in case that's what's tripping you up.)

khrise commented

Uh, sorry - sure, you're right, seems that I'm tired :). However, I was irritated by the fact that my printed label looks so different from @tomek-szczesny 's. This is what led me to thinking I was using the wrong commit in the first place.

PXL_20231109_201710007

khrise commented

Whatever this tells us, but this is a dymoprint --test-pattern 64 xyz on a 12mm tape. Looks, ehm, different.

PXL_20231109_202814618

Please try dymoprint --test-pattern 128 " " instead, on all tape sizes.
The point is to know where the dots fall on smaller tapes. Your printer is different and we do not expect the same outcome. :)

I know we complicated things a little bit with commits everywhere, I'm sorry about that, I'm not used to cooperation on github. Usually I was all alone in my endeavors. :)
Anyway, the --test-pattern functionality is now in master branch of dymoprint. But the code you're using appears to be up to date, with vertical lines the way I made them today.

But seeing those results I think there may be something wrong with your printer or tapes. Looks like the tape isn't heated enough and the smallest dots get blurred or something.
That's why I was hoping for @Mousketeer 's input to compare and figure out what's going on.

We're on a good track, I can feel it.

In the meantime I investigated the code some more and figured out that bytes_per_line thing or whatever it was called, was intended to break up data packets into smaller chunks to avoid timeout errors. Only later the same function has been wrongly used to calculate dot size of a tape, for example in QR code render. There is a bit of cleanup ahead of me. But a day after tomorrow there's an Independence day in Poland, so that means I either get some extra time to work on it, or exactly the opposite. :)

khrise commented

But seeing those results I think there may be something wrong with your printer or tapes. Looks like the tape isn't heated enough and the smallest dots get blurred or something.

I bought the printer second hand, with a bunch of tapes, some original, some cheap compatibles. The printer itself doesn't look as if it has been used too much. But sure, something may be wrong with it. I guess even the tapes can get "old". So let's encourage @Mousketeer to do some printing, too.
Here is the test pattern on 12, 9, and 6mm:

12mm:
12mm
9mm:
9mm
6mm:
6mm

Thanks, this input is very valuable. It will serve us to build a preliminary LUT table, and the pattern is working as I hoped.

I noticed that tapes age significantly. I only have one original Dymo tape and its adhesive leaves a lot to be desired. It just falls off. But it must be the oldest one I have (not sure if it wasn't the one I got from @maresb ?)
I hope the fresh one will solve your problems.

Now I got a contributor badge, I am expected to give more informed answers. ;)

@khrise, giving a sober look at your test pattern prints, there is definitely something going on with your 19mm and 12mm tape. The smaller sizes rendered single dots with no problem, even though the print head was obviously the same and operating in the same conditions.
The worrying sign are the first two vertical lines too close to each other. Perhaps the motor struggles to keep a constant pace. But printing a vertical line is a corner case that should not happen in everyday use so I wouldn't worry about that too much.

Today I'll attempt to implement what we are all waiting for, as one of the parties has been cancelled ;)

maresb commented

I think it'd be helpful to add a single-pixel checkerboard part to the pattern.

Feel free to expand existing procedure, but I can't see it being too helpful on top of what's in there already.

I'm constructing a dictionary of dictionaries as we speak.. Lotta fun :D
I got preliminary values already, I'm super curious how this will turn out.

Like always, stuff is getting complicated.

In order to render an image, we'll have to know tape size and printer model beforehand. So printer detection stuff will have to get executed first.
Now the problem is that preview skipped printer detection because it was assumed all printers behave alike.
So either preview will require a working printer, or give inaccurate results. Both options suck, but I prefer the former.

maresb commented

So either preview will require a working printer, or give inaccurate results. Both options suck, but I prefer the former.

๐Ÿ’ฏ

Really the preview should only hook in at the very end right before things are sent to the printer and convert the bytes back into pixels.

63 insertions and 10 deletions in 2 hours. I truly suck at this. ;)

Anyway, looks like the contraption is working. I created a table for 420p as well, but the final LUT values in constants.py may require a bit of experimenting. :)

This is the commit I'm taking about: c9ff02d19186ec2af8d06aa56e234e29a3032b8d

Here is a test print, and old picture for comparison. All PnP tapes are centered along the common axis so this should look like that.
Moreover I discovered that T9 in fact has almost the same print area as T12, so I left 1 dot margins and called it a day. Hence the font size is almost the same.
T6 also benefits from better defined print area.
As a result we'll be able to fit larger text, or more text.
And I defined T19 to behave the same as T12 because otherwise the results are garbage.
obraz

obraz

maresb commented

63 insertions and 10 deletions in 2 hours. I truly suck at this. ;)

That's a really bad metric to check. Results are better, and they look quite good!

With a right front we could squeeze in 8 lines of text in there. ;)
The 6-line sample has been cropped by the text renderer.

But anyway I was hoping I could fit 3 readable lines on a 9mm tape and I guess it has just become possible.
obraz

maresb commented

Ya, this is why I'd really like to support pixel fonts!

Sorry, didn't have a single jiffy of time this week.
testpattern-2
Left is before any cleaning, right after a first round. This is on Aliexpress-grade nylon tape, 19mm

(this is with version dymoprint 2.2.1.post1.dev21+g8d27f8d, and it crashes after printing the pattern)

@tomek-szczesny I think the storage conditions significantly affect the aging, but also the type of label and the type of glue. I noticed (but not done any objective tests) that heating the underground a little before applying and pressing hard (in a point-like fashion, like a fingernail) helps. (obviously degrease the surface)

khrise commented

Awesome! I'll give it a try in a bit.

giving a sober look at your test pattern prints, there is definitely something going on with your 19mm and 12mm tape

Well, that would be the cause I like the most. :)

The worrying sign are the first two vertical lines too close to each other. Perhaps the motor struggles to keep a constant pace

For the 19mm tape, I'm not too surprised that it doesn't bring the best results, since the thermal sheet is not rolled-in by the gear wheel mechanic, but rather only gets pushed out along with the label tape. I can easily imagine that this affects the pushout "pace". Maybe the tape sticks to the print head for a tiny moment after printing a vertical line.
For the 12mm tape, I have no explanation other than "cheap tape". But anyway, my print quality requirements are rather low, to be honest. As long as I can read the text on the labels.

This is with the dymoprint 0.0.post1.dev250+g265c593 (from @tomek-szczesny's test-label branch (again on 19mm aliexpress nylon)
testpattern-2

I will re-try cleaning tomorrow and test with more tapes, because this just bad.

khrise commented

I will re-try cleaning tomorrow

Is there any advice on how to clean this thing?

maresb commented

Is there any advice on how to clean this thing?

I have the brush, and it's sort of like some type of paper or microfiber cloth backed with plastic.

I basically used the brush to delicately wipe any accumulated dust from the frighteningly delicate-looking print head. Does your model have a user's manual with any suggestions?

khrise commented

Does your model have a user's manual with any suggestions?

Ah, indeed, there is a german manual in the box. Apparently, the 420p is also shipped with some sort of brush, which is no longer there.
According to the manual, one should call the customer support, if there is no brush.

I don't think I got a brush with any Dymo, but the prints are fine so I wouldn't bother using it anyway.

Hmm looks like another 420p with similar symptoms. Maybe that's just how they work. And as long as typical scenarios work well I wouldn't mind lack of full vertical lines.

@Mousketeer it would be great if you could also try dymoprint --test-pattern 128 " " on all tape sizes you have.

And I invite you and @khrise to try my branch that should more or less support 420p already.
tomek-szczesny@c9ff02d
Just try your typical use cases with any tapes you like. This should work well with GUI but I haven't tested that yet.

EDIT: I wanted to point out I always use unbranded cheap replacement tapes (Some $4 a piece). They work very well, and my only original Dymo tape doesn't, as I said before. But that one may be really old.

khrise commented

Whew, that's pretty close already!
12mm tape:
PXL_20231111_061557944
In the afternoon, there will be some time to maybe figure out more precise LUT values.

khrise commented

BTW: GUI doesn't seem to work.

Traceback (most recent call last):
  File "~/.local/pipx/venvs/dymoprint/lib/python3.11/site-packages/dymoprint/gui.py", line 164, in print_label
    print_label(
  File "~/.local/pipx/venvs/dymoprint/lib/python3.11/site-packages/dymoprint/dymo_print_engines.py", line 293, in print_label
    label_rotated = label_bitmap.transpose(Image.ROTATE_270)
                    ^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'int' object has no attribute 'transpose'
zsh: IOT instruction (core dumped)  dymoprint_gui

I'll look into GUI in a minute. I thought it just invokes command line utility and I assumed it will work fine, lol.

That's strange you got different test pattern results on 12mm tape this time. There were no changes to test pattern code, and you may even confirm with the same branch you used last time.

Remembering these two pictures, looks like your test print results happened when the tape was aligned in two different ways inside the printer.
Perhaps there is a physical possibility to mount the 12mm tape in two different positions, somehow?
Or maybe it is a bug in my code.

The images below are puzzling, because what is shown as "not real" appears to be a proof that alignment was in fact real..

Or that printer has sime kind of tape sensor that adds its own offset, and that sensor is unreliable? Damn I don't know how to explain this.
obraz
obraz