I have some functions and structs for loading and drawing an image with OpenGL. The two main places I would like feedback are:

1. I've tried to separate the OpenGL-specific code into the implementation so the caller doesn't have to know what rendering API I'm using. Can I do a better job of separating the OpenGL-specific code from the code necessary to specify the image?

2. I'm not happy with my `convert_pixels_to_render_coordinates` function. It works, it's just klunky. Passing 7 floats is just begging for mis-use. Is there any way I can simplify things, or make it easier to use? I show my usage at the end.


image.h


```c
#ifndef IMAGE_H
#define IMAGE_H

struct rectangle
{
    float x, y, width, height;
};

void rectangle_vertices(const struct rectangle *rectangle,
                        float *vertices);


/*
 * Calculate the y_start and height of an image in 
 * render coordinates by "glueing" two points on
 * the image to two points on the coordinate system
 */
void convert_pixels_to_render_coordinates(
    float image_height,
    float px_0, float px_1,
    float coord_0, float coord_1,
    float *y_offset, 
    float *height);

struct image
{
    struct rectangle rect;
    void *render_data;
};

void set_position(struct image *image, 
                  float new_x, float new_y);

void load_image(struct image *image,
                const char *pathname);

void render_image(struct image *image);

#endif
```

image.c
```c
include "image.h"
#include "gl_header.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#include <stdbool.h>

void rectangle_vertices(const struct rectangle *rectangle,
                        float *vertices)
{
    vertices[0] = rectangle->x; 
    vertices[1] = rectangle->y;

    vertices[2] = rectangle->x + rectangle->width; 
    vertices[3] = rectangle->y;

    vertices[4] = rectangle->x + rectangle->width; 
    vertices[5] = rectangle->y - rectangle->height;

    vertices[6] = rectangle->x;                    
    vertices[7] = rectangle->y - rectangle->height;
}

void convert_pixels_to_render_coordinates(
    float image_dim,
    float px_0, float px_1,
    float coord_0, float coord_1,
    float *offset, 
    float *length)
{
    const float px_delta = px_1 - px_0;
    const float coord_delta = coord_0 - coord_1;

    /* Get the size of the image in render coordinates by solving the equation

        px_delta           *length
       -----------  =   -------------
        image_dim        coord_delta
    */
    *length = image_dim * coord_delta / px_delta;
    
    /* Solving this equation gives us the offset to our first point:
      
       image_dim        offset
      ----------- =    --------
         px_0           *length
        
       And then we need to apply the coord offset
    */
    *offset = coord_0 + (px_0 * (*length) / image_dim);
}

static const struct rectangle FULL_UV_COORDS = {0.f, 0.f, 1.f, 1.f};

struct image_impl
{
    GLuint texture;
    GLuint buffers[2];
};

#define VERTEX_BUFFER buffers[0]
#define UV_BUFFER buffers[1]

void load_image(struct image *image, const char *pathname)
{
    stbi_set_flip_vertically_on_load(true);
    image->render_data = malloc(sizeof(struct image_impl));
    if (!image->render_data)
    {
        fprintf(stderr, "Out of memory\n");
        exit(1);
    }

    struct image_impl *impl = (struct image_impl *)image->render_data;
    glGenBuffers(2, impl->buffers);

    int width_px, height_px, channels;
    unsigned char *pixels = stbi_load(pathname, &width_px, &height_px, &channels, 0);

    set_position(image, image->rect.x, image->rect.y);

    const struct rectangle *texture_rect = &FULL_UV_COORDS;
    GLfloat note_uv[8] = {};
    rectangle_vertices(texture_rect, note_uv);
    glBindBuffer(GL_ARRAY_BUFFER, impl->UV_BUFFER);
    glBufferData(GL_ARRAY_BUFFER, sizeof(note_uv), note_uv, GL_STATIC_DRAW);

    glGenTextures(1, &impl->texture);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, impl->texture);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width_px, height_px,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    glGenerateMipmap(GL_TEXTURE_2D);

    stbi_image_free(pixels);
}

void render_image(struct image *image)
{
    struct image_impl *impl = (struct image_impl *)image->render_data;
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, impl->texture);

    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, impl->VERTEX_BUFFER);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);

    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, impl->UV_BUFFER);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
}

void set_position(struct image *image,
                  float new_x, float new_y)
{
    image->rect.x = new_x;
    image->rect.y = new_y;

    struct image_impl *impl = (struct image_impl *)image->render_data;
    GLfloat note_vertices[8] = {};
    rectangle_vertices(&image->rect, note_vertices);
    glBindBuffer(GL_ARRAY_BUFFER, impl->VERTEX_BUFFER);
    glBufferData(GL_ARRAY_BUFFER, sizeof(note_vertices), note_vertices, GL_STATIC_DRAW);
}

#undef VERTEX_BUFFER
#undef UV_BUFFER
```

Usage:

```c
convert_pixels_to_render_coordinates(
            256,
            0.f, 74.f,
            .5f, .45f,
            &state->bass_clef.rect.y,
            &state->bass_clef.rect.height);
state->bass_clef.rect.x = -1.f;
state->bass_clef.rect.width = .1f;

load_image(&state->bass_clef,
           "./res/bass-clef.png");
```

And to try to explain where all these numbers are coming from, here's a (marked) image that I'm trying to load with this:

[![example bass clef with markings][1]][1]

The two red dots are the y-coordinates (0.f, 74.f) where the clef needs to intersect lines on the staff.

The .5f and .45f are the y-values in OpenGL coordinates [-1..1] of the lines on the staff those dots need to touch. From this we calculate the y offset and height of the image rectangle for the clef.


  [1]: https://i.sstatic.net/IP9N6.png