Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Placing an InlineBox at the beginning of non-first line #138

Open
spirali opened this issue Oct 16, 2024 · 2 comments
Open

Placing an InlineBox at the beginning of non-first line #138

spirali opened this issue Oct 16, 2024 · 2 comments
Labels
bug Something isn't working

Comments

@spirali
Copy link
Contributor

spirali commented Oct 16, 2024

My goal is to place an inline box at the beginning of a non-first line. When I place it on an index of "\n" character then it will be placed at the end of the previous line. When I place it to +1 position, then it will be placed after first character.

Examples

Box at index 7
offset7

Box at index 8
offset8

Question: How to place an inline box at the beginning of the second line?

Images was generated by the following code:

use image::codecs::png::PngEncoder;
use image::{self, Pixel, Rgba, RgbaImage};
use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem};
use parley::style::{FontStack, StyleProperty};
use parley::{FontContext, InlineBox, LayoutContext};
use peniko::Color;
use std::fs::File;
use std::path::PathBuf;
use swash::scale::image::Content;
use swash::scale::{Render, ScaleContext, Scaler, Source, StrikeWith};
use swash::zeno;
use swash::FontRef;
use zeno::{Format, Vector};

fn main() {
    // The text we are going to style and lay out
    let text = String::from(
        "Line 1\nLine 2\nLine 3",
    );

    // The display scale for HiDPI rendering
    let display_scale = 1.0;

    // Colours for rendering
    let text_color = Color::rgb8(0, 0, 0);
    let bg_color = Rgba([255, 255, 255, 255]);

    // Padding around the output image
    let padding = 20;

    // Create a FontContext, LayoutContext and ScaleContext
    //
    // These are all intended to be constructed rarely (perhaps even once per app (or once per thread))
    // and provide caches and scratch space to avoid allocations
    let mut font_cx = FontContext::new();
    let mut layout_cx = LayoutContext::new();
    let mut scale_cx = ScaleContext::new();

    // Setup some Parley text styles
    let brush_style = StyleProperty::Brush(text_color);
    let font_stack = FontStack::from("system-ui");

    let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, display_scale);

        // Set default text colour styles (set foreground text color)
        builder.push_default(brush_style);

        // Set default font family
        builder.push_default(font_stack);
        builder.push_default(StyleProperty::LineHeight(1.3));
        builder.push_default(StyleProperty::FontSize(16.0));

        builder.push_inline_box(InlineBox {
            id: 0,
            index: 8,
            width: 50.0,
            height: 50.0,
        });

    // Build the builder into a Layout
    // let mut layout: Layout<Color> = builder.build(&text);
    let mut layout: Layout<Color> = builder.build(&text);

    // Perform layout (including bidi resolution and shaping) with start alignment
    layout.break_all_lines(None);
    layout.align(None, Alignment::Start);

    // Create image to render into
    let width = layout.width().ceil() as u32 + (padding * 2);
    let height = layout.height().ceil() as u32 + (padding * 2);
    let mut img = RgbaImage::from_pixel(width, height, bg_color);

    // Iterate over laid out lines
    for line in layout.lines() {
        // Iterate over GlyphRun's within each line
        for item in line.items() {
            match item {
                PositionedLayoutItem::GlyphRun(glyph_run) => {
                    render_glyph_run(&mut scale_cx, &glyph_run, &mut img, padding);
                }
                PositionedLayoutItem::InlineBox(inline_box) => {
                    for x_off in 0..(inline_box.width.floor() as u32) {
                        for y_off in 0..(inline_box.height.floor() as u32) {
                            let x = inline_box.x as u32 + x_off + padding;
                            let y = inline_box.y as u32 + y_off + padding;
                            img.put_pixel(x, y, Rgba([0, 0, 0, 255]));
                        }
                    }
                }
            };
        }
    }

    // Write image to PNG file in examples/_output dir
    let output_path = {
        let mut path = PathBuf::new();
        path.push("_output");
        let _ = std::fs::create_dir(path.clone());
        path.push("swash_render.png");
        path
    };
    let output_file = File::create(output_path).unwrap();
    let png_encoder = PngEncoder::new(output_file);
    img.write_with_encoder(png_encoder).unwrap();
}

fn render_glyph_run(
    context: &mut ScaleContext,
    glyph_run: &GlyphRun<Color>,
    img: &mut RgbaImage,
    padding: u32,
) {
    // Resolve properties of the GlyphRun
    let mut run_x = glyph_run.offset();
    let run_y = glyph_run.baseline();
    let style = glyph_run.style();
    let color = style.brush;

    // Get the "Run" from the "GlyphRun"
    let run = glyph_run.run();

    // Resolve properties of the Run
    let font = run.font();
    let font_size = run.font_size();
    let normalized_coords = run.normalized_coords();

    // Convert from parley::Font to swash::FontRef
    let font_ref = FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();

    // Build a scaler. As the font properties are constant across an entire run of glyphs
    // we can build one scaler for the run and reuse it for each glyph.
    let mut scaler = context
        .builder(font_ref)
        .size(font_size)
        .hint(true)
        .normalized_coords(normalized_coords)
        .build();

    // Iterates over the glyphs in the GlyphRun
    for glyph in glyph_run.glyphs() {
        let glyph_x = run_x + glyph.x + (padding as f32);
        let glyph_y = run_y - glyph.y + (padding as f32);
        run_x += glyph.advance;

        render_glyph(img, &mut scaler, color, glyph, glyph_x, glyph_y);
    }
}

fn render_glyph(
    img: &mut RgbaImage,
    scaler: &mut Scaler,
    color: Color,
    glyph: Glyph,
    glyph_x: f32,
    glyph_y: f32,
) {
    // Compute the fractional offset
    // You'll likely want to quantize this in a real renderer
    let offset = Vector::new(glyph_x.fract(), glyph_y.fract());

    // Render the glyph using swash
    let rendered_glyph = Render::new(
        // Select our source order
        &[
            Source::ColorOutline(0),
            Source::ColorBitmap(StrikeWith::BestFit),
            Source::Outline,
        ],
    )
    // Select the simple alpha (non-subpixel) format
    .format(Format::Alpha)
    // Apply the fractional offset
    .offset(offset)
    // Render the image
    .render(scaler, glyph.id)
    .unwrap();

    let glyph_width = rendered_glyph.placement.width;
    let glyph_height = rendered_glyph.placement.height;
    let glyph_x = (glyph_x.floor() as i32 + rendered_glyph.placement.left) as u32;
    let glyph_y = (glyph_y.floor() as i32 - rendered_glyph.placement.top) as u32;

    match rendered_glyph.content {
        Content::Mask => {
            let mut i = 0;
            for pixel_y in 0..glyph_height {
                for pixel_x in 0..glyph_width {
                    let x = glyph_x + pixel_x;
                    let y = glyph_y + pixel_y;
                    let alpha = rendered_glyph.data[i];
                    let color = Rgba([color.r, color.g, color.b, alpha]);
                    img.get_pixel_mut(x, y).blend(&color);
                    i += 1;
                }
            }
        }
        Content::SubpixelMask => unimplemented!(),
        Content::Color => {
            let row_size = glyph_width as usize * 4;
            for (pixel_y, row) in rendered_glyph.data.chunks_exact(row_size).enumerate() {
                for (pixel_x, pixel) in row.chunks_exact(4).enumerate() {
                    let x = glyph_x + pixel_x as u32;
                    let y = glyph_y + pixel_y as u32;
                    let color = Rgba(pixel.try_into().expect("Not RGBA"));
                    img.get_pixel_mut(x, y).blend(&color);
                }
            }
        }
    };
}
@nicoburns nicoburns added the bug Something isn't working label Oct 17, 2024
@nicoburns
Copy link
Contributor

I think this is just a bug. At a conference this week, but I will take a look when I get a chance.

@spirali
Copy link
Contributor Author

spirali commented Nov 3, 2024

I have been trying to investigate the problem. It seems that if I put an inlinebox at the beginning of the second line then it creates 3 items: [run, inlinebox, run]. When it processes items for line breaks then it triggers a mandatory break when processing the 3rd item (2nd run) so it also includes the inlinebox in the first line.

Do you have any idea if this is a line break problem? I would expect that the line break should occur during processing of the first run, but I am not so sure how the code works.

spirali added a commit to spirali/parley that referenced this issue Nov 8, 2024
spirali added a commit to spirali/parley that referenced this issue Nov 8, 2024
github-merge-queue bot pushed a commit that referenced this issue Nov 10, 2024
When a inline box is placed at the beginning of the non-first line then
line breaking is wrong.

Lets have the following text:

```
Line1\nLine2
       |
       \- Inlinebox here
```

It produces the following items in layout:

[run, inlinebox, run]

But the line breaking occurs when the second run is processed. So
inlinebox ends on the previous line.
This PR fixes it by checking that current cluster_idx is mandatory break
and ends line before processing inline boxes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants